de.undercouch.citeproc.CSL Maven / Gradle / Ivy
package de.undercouch.citeproc;
import de.undercouch.citeproc.csl.CSLCitation;
import de.undercouch.citeproc.csl.CSLCitationItem;
import de.undercouch.citeproc.csl.CSLCitationItemBuilder;
import de.undercouch.citeproc.csl.CSLItemData;
import de.undercouch.citeproc.csl.CSLItemDataBuilder;
import de.undercouch.citeproc.csl.internal.GeneratedCitation;
import de.undercouch.citeproc.csl.internal.RenderContext;
import de.undercouch.citeproc.csl.internal.SSort;
import de.undercouch.citeproc.csl.internal.SStyle;
import de.undercouch.citeproc.csl.internal.format.AsciiDocFormat;
import de.undercouch.citeproc.csl.internal.format.FoFormat;
import de.undercouch.citeproc.csl.internal.format.Format;
import de.undercouch.citeproc.csl.internal.format.HtmlFormat;
import de.undercouch.citeproc.csl.internal.format.TextFormat;
import de.undercouch.citeproc.csl.internal.locale.LLocale;
import de.undercouch.citeproc.helper.CSLUtils;
import de.undercouch.citeproc.output.Bibliography;
import de.undercouch.citeproc.output.Citation;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* The citation processor.
*
* In order to use the processor in your application you first have to
* create an {@link ItemDataProvider} that provides citation item data. For
* example, the following dummy provider returns always the same data:
*
*
* public class MyItemProvider implements ItemDataProvider {
* @Override
* public CSLItemData retrieveItem(String id) {
* return new CSLItemDataBuilder()
* .id(id)
* .type(CSLType.ARTICLE_JOURNAL)
* .title("A dummy journal article")
* .author("John", "Smith")
* .issued(2013, 9, 6)
* .containerTitle("Dummy journal")
* .build();
* }
*
* @Override
* public String[] getIds() {
* String ids[] = {"ID-0", "ID-1", "ID-2"};
* return ids;
* }
* }
*
* Now you can instantiate the CSL processor.
*
*
* CSL citeproc = new CSL(new MyItemProvider(), "ieee");
* citeproc.setOutputFormat("html");
*
* Ad-hoc usage
*
* You may also use {@link #makeAdhocBibliography(String, CSLItemData...)} or
* {@link #makeAdhocBibliography(String, String, CSLItemData...)} to create
* ad-hoc bibliographies from CSL items.
*
*
* CSLItemData item = new CSLItemDataBuilder()
* .type(CSLType.WEBPAGE)
* .title("citeproc-java: A Citation Style Language (CSL) processor for Java")
* .author("Michel", "Kraemer")
* .issued(2014, 7, 13)
* .URL("http://michel-kraemer.github.io/citeproc-java/")
* .accessed(2014, 7, 13)
* .build();
*
* String bibl = CSL.makeAdhocBibliography("ieee", item).makeString();
*
* @author Michel Kraemer
*/
public class CSL {
/**
* The output format
*/
private Format outputFormat = new HtmlFormat();
/**
* {@code true} if the processor should convert URLs and DOIs in the output
* to links.
* @see #setConvertLinks(boolean)
*/
private boolean convertLinks = false;
/**
* The CSL style used to render citations and bibliographies
*/
private final SStyle style;
/**
* The localization data used to render citations and bibliographies
*/
private final LLocale locale;
/**
* An object that provides citation item data
*/
private final ItemDataProvider itemDataProvider;
/**
* An object that provides abbreviations (may be {@code null})
*/
private final AbbreviationProvider abbreviationProvider;
/**
* Citation items registered through {@link #registerCitationItems(String...)}
*/
private final Map registeredItems = new LinkedHashMap<>();
/**
* Contains the same items as {@link #registeredItems} but sorted
*/
private final List sortedItems = new ArrayList<>();
/**
* A list of generated citations sorted by their index
*/
private List generatedCitations = new ArrayList<>();
/**
* Constructs a new citation processor
* @param itemDataProvider an object that provides citation item data
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @throws IOException if the CSL style could not be loaded
*/
public CSL(ItemDataProvider itemDataProvider, String style) throws IOException {
this(itemDataProvider, new DefaultLocaleProvider(), null, style, null);
}
/**
* Constructs a new citation processor
* @param itemDataProvider an object that provides citation item data
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param lang an RFC 4646 identifier for the citation locale (e.g. en-US
)
* @throws IOException if the CSL style could not be loaded
*/
public CSL(ItemDataProvider itemDataProvider, String style, String lang) throws IOException {
this(itemDataProvider, new DefaultLocaleProvider(), null, style, lang);
}
/**
* Constructs a new citation processor
* @param itemDataProvider an object that provides citation item data
* @param localeProvider an object that provides CSL locales
* @param abbreviationProvider an object that provides abbreviations
* (may be {@code null})
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param lang an RFC 4646 identifier for the citation locale (e.g.
* {@code en-US}). If this argument is {@code null}, the default locale of
* the citation style will be used. If the citation style does not define
* a default locale, the method falls back to {@code en-US}.
* @throws IOException if the CSL style could not be loaded
*/
public CSL(ItemDataProvider itemDataProvider, LocaleProvider localeProvider,
AbbreviationProvider abbreviationProvider, String style,
String lang) throws IOException {
// load style if needed
if (!isStyle(style)) {
style = retrieveStyle(style);
}
this.itemDataProvider = itemDataProvider;
this.abbreviationProvider = abbreviationProvider;
// TODO parse style and locale directly from URL if possible
// TODO instead of loading them into strings first
this.style = loadStyle(style);
if (lang == null) {
lang = this.style.getDefaultLocale();
if (lang == null) {
lang = "en-US";
}
}
// load locale
String strLocale = localeProvider.retrieveLocale(lang);
LLocale locale = loadLocale(strLocale);
for (LLocale l : this.style.getLocales()) {
if (l.getLang() == null ||
(l.getLang().getLanguage().equals(locale.getLang().getLanguage()) &&
(l.getLang().getCountry().isEmpty() ||
l.getLang().getCountry().equals(locale.getLang().getCountry())))) {
// additional localization data in the style file overrides or
// augments the data from the locale file
locale = locale.merge(l);
}
}
this.locale = locale;
}
/**
* Get a list of supported output formats
* @return the formats
*/
public static List getSupportedOutputFormats() {
return Arrays.asList("asciidoc", "fo", "html", "text");
}
private static Set getAvailableFiles(String prefix,
String knownName, String extension) throws IOException {
Set result = new LinkedHashSet<>();
// first load a file that is known to exist
String name = prefix + knownName + "." + extension;
URL knownUrl = CSL.class.getResource("/" + name);
if (knownUrl != null && knownUrl.toString().endsWith(".jar!/" + name)) {
URL url;
try {
final String path = knownUrl.toString();
url = new URL(path.substring(0, path.length() - name.length() - 2));
} catch (MalformedURLException e) {
final String path = knownUrl.getPath();
url = new URL(path.substring(0, path.length() - name.length() - 2));
}
try (InputStream inputStream = url.openStream();
ZipInputStream zip = new ZipInputStream(inputStream)) {
ZipEntry e;
while ((e = zip.getNextEntry()) != null) {
if (e.getName().endsWith("." + extension) &&
(prefix.isEmpty() || e.getName().startsWith(prefix))) {
result.add(e.getName().substring(
prefix.length(), e.getName().length() - 4));
}
}
}
}
return result;
}
/**
* Calculates a list of available citation styles
* @return the list
* @throws IOException if the citation styles could not be loaded
*/
public static Set getSupportedStyles() throws IOException {
return getAvailableFiles("", "ieee", "csl");
}
/**
* Checks if a given citation style is supported
* @param style the citation style's name
* @return true if the style is supported, false otherwise
*/
public static boolean supportsStyle(String style) {
String styleFileName = style;
if (!styleFileName.endsWith(".csl")) {
styleFileName = styleFileName + ".csl";
}
if (!styleFileName.startsWith("/")) {
styleFileName = "/" + styleFileName;
}
URL url = CSL.class.getResource(styleFileName);
return (url != null);
}
/**
* Calculates a list of available citation locales
* @return the list
* @throws IOException if the citation locales could not be loaded
*/
public static Set getSupportedLocales() throws IOException {
return getAvailableFiles("locales-", "en-US", "xml");
}
/**
* Checks if the given String contains the serialized XML representation
* of a style
* @param style the string to examine
* @return true if the String is XML, false otherwise
*/
private static boolean isStyle(String style) {
for (int i = 0; i < style.length(); ++i) {
char c = style.charAt(i);
if (!Character.isWhitespace(c)) {
return (c == '<');
}
}
return false;
}
/**
* Determines whether the given citation style contains instructions to
* format bibliographies.
* @param style the style
* @return {@code true} if the style can be used to format bibliographies
*/
private static boolean canFormatBibliographies(SStyle style) {
return style.getBibliography() != null;
}
/**
* Determines whether the given citation style contains instructions to
* format bibliographies
* @param style the citation style. May either be a serialized XML
* representation of the style or a style name such as ieee
.
* In the latter case, the style is loaded from the classpath (e.g.
* /ieee.csl
)
* @return {@code true} if the style can be used to format bibliographies
* @throws IOException if the style could not be loaded or parsed
*/
public static boolean canFormatBibliographies(String style) throws IOException {
if (!isStyle(style)) {
style = retrieveStyle(style);
}
SStyle ss = loadStyle(style);
return canFormatBibliographies(ss);
}
private static SStyle loadStyle(String style) throws IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new IOException("Could not create document builder", e);
}
// load style
Document styleDocument;
try {
styleDocument = builder.parse(new InputSource(
new StringReader(style)));
} catch (SAXException e) {
throw new IOException("Could not parse style", e);
}
return new SStyle(styleDocument);
}
/**
* Retrieves a CSL style from the classpath. For example, if the given name
* is ieee
this method will load the file /ieee.csl
* @param styleName the style's name
* @return the serialized XML representation of the style
* @throws IOException if the style could not be loaded
*/
private static String retrieveStyle(String styleName) throws IOException {
URL url;
if (styleName.startsWith("http://") || styleName.startsWith("https://")) {
try {
// try to load matching style from classpath
return retrieveStyle(styleName.substring(styleName.lastIndexOf('/') + 1));
} catch (FileNotFoundException e) {
// there is no matching style in classpath
url = new URL(styleName);
}
} else {
// normalize file name
if (!styleName.endsWith(".csl")) {
styleName = styleName + ".csl";
}
if (!styleName.startsWith("/")) {
styleName = "/" + styleName;
}
// try to find style in classpath
url = CSL.class.getResource(styleName);
if (url == null) {
throw new FileNotFoundException("Could not find style in "
+ "classpath: " + styleName);
}
}
// load style
String result = CSLUtils.readURLToString(url, "UTF-8");
// handle dependent styles
if (isDependent(result)) {
String independentParentLink;
try {
independentParentLink = getIndependentParentLink(result);
} catch (ParserConfigurationException | IOException | SAXException e) {
throw new IOException("Could not load independent parent style", e);
}
if (independentParentLink == null) {
throw new IOException("Dependent style does not have an "
+ "independent parent");
}
return retrieveStyle(independentParentLink);
}
return result;
}
/**
* Test if the given string represents a dependent style
* @param style the style
* @return true if the string is a dependent style, false otherwise
*/
private static boolean isDependent(String style) {
if (!style.trim().startsWith("<")) {
return false;
}
Pattern p = Pattern.compile("rel\\s*=\\s*\"\\s*independent-parent\\s*\"");
Matcher m = p.matcher(style);
return m.find();
}
/**
* Parse a string representing a dependent parent style and
* get link to its independent parent style
* @param style the dependent style
* @return the link to the parent style or null
if the link
* could not be found
* @throws ParserConfigurationException if the XML parser could not be created
* @throws IOException if the string could not be read
* @throws SAXException if the string could not be parsed
*/
public static String getIndependentParentLink(String style)
throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
InputSource src = new InputSource(new StringReader(style));
Document doc = builder.parse(src);
NodeList links = doc.getElementsByTagName("link");
for (int i = 0; i < links.getLength(); ++i) {
Node n = links.item(i);
Node relAttr = n.getAttributes().getNamedItem("rel");
if (relAttr != null) {
if ("independent-parent".equals(relAttr.getTextContent())) {
Node hrefAttr = n.getAttributes().getNamedItem("href");
if (hrefAttr != null) {
return hrefAttr.getTextContent();
}
}
}
}
return null;
}
private static LLocale loadLocale(String strLocale) throws IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new IOException("Could not create document builder", e);
}
Document localeDocument;
try {
localeDocument = builder.parse(new InputSource(
new StringReader(strLocale)));
} catch (SAXException e) {
throw new IOException("Could not parse locale", e);
}
return new LLocale(localeDocument);
}
/**
* Sets the processor's output format
* @param format the format (one of {@code "asciidoc"}, {@code "fo"},
* {@code "html"}, and {@code "text"}.
*/
public void setOutputFormat(String format) {
if ("asciidoc".equals(format)) {
setOutputFormat(new AsciiDocFormat());
} else if ("fo".equals(format)) {
setOutputFormat(new FoFormat());
} else if ("html".equals(format)) {
setOutputFormat(new HtmlFormat());
} else if ("text".equals(format)) {
setOutputFormat(new TextFormat());
} else {
throw new IllegalArgumentException("Unknown output format: `" +
format + "'. Supported formats: `asciidoc', `fo', `html', `text'.");
}
}
/**
* Sets the processor's output format
* @param outputFormat the format
*/
public void setOutputFormat(Format outputFormat) {
this.outputFormat = outputFormat;
outputFormat.setConvertLinks(convertLinks);
}
/**
* Specifies if the processor should convert URLs and DOIs in the output
* to links. How links are created depends on the output format that has
* been set with {@link #setOutputFormat(String)}
* @param convert true if URLs and DOIs should be converted to links
*/
public void setConvertLinks(boolean convert) {
convertLinks = convert;
outputFormat.setConvertLinks(convert);
}
/**
* Fetches the item data for the given citation items and adds it to
* {@link #registeredItems}. Also, sorts the items according to the sorting
* specified in the style's bibliography element and stores the result in
* {@link #sortedItems}. If the style does not have a bibliography element
* or no sorting is specified, the items will just be appended to
* {@link #sortedItems}. In addition, the method updates any items already
* stored in {@link #sortedItems} and coming after the generated ones.
* Updated items will be returned in the given {@code updatedItems} set
* (unless it is {@code null}).
* @param ids the IDs of the citation items to register
* @param updatedItems an empty set that will be filled with the citation
* items the method had to update (may be {@code null})
* @param unsorted {@code true} if any sorting specified in the style
* should be ignored
* @return a list of registered citation item data
*/
private List registerItems(Collection ids,
Set updatedItems, boolean unsorted) {
List result = new ArrayList<>();
SSort.SortComparator comparator = null;
for (String id : ids) {
// check if item has already been registered
CSLItemData itemData = registeredItems.get(id);
if (itemData != null) {
result.add(itemData);
continue;
}
// fetch item data
itemData = itemDataProvider.retrieveItem(id);
if (itemData == null) {
throw new IllegalArgumentException("Missing citation " +
"item with ID: " + id);
}
// register item
if (unsorted || style.getBibliography() == null ||
style.getBibliography().getSort() == null) {
// We don't have to sort. Add item to the end of the list.
itemData = new CSLItemDataBuilder(itemData)
.citationNumber(String.valueOf(registeredItems.size() + 1))
.build();
sortedItems.add(itemData);
} else {
// We have to sort. Find insert point.
if (comparator == null) {
comparator = style.getBibliography().getSort()
.comparator(style, locale, abbreviationProvider);
}
int i = Collections.binarySearch(sortedItems, itemData, comparator);
if (i < 0) {
i = -(i + 1);
} else {
// binarySearch thinks we found the item in the list but
// this is impossible. It's more likely that the comparator
// returned 0 because no key was given or it did not yield
// sensible results. Just append the item to the list.
i = sortedItems.size();
}
// determine citation number depending on sort direction
int citationNumber;
int citationNumberDirection = comparator.getCitationNumberDirection();
if (citationNumberDirection > 0) {
citationNumber = i + 1;
} else {
citationNumber = sortedItems.size() + 1 - i;
}
// create new item data with citation data and add it to
// the list of sorted items
itemData = new CSLItemDataBuilder(itemData)
.citationNumber(String.valueOf(citationNumber))
.build();
sortedItems.add(i, itemData);
// determine if we need to update the following items or
// the preceding ones (depending on the sort direction)
IntStream idStream;
if (citationNumberDirection > 0) {
idStream = IntStream.range(i + 1, sortedItems.size());
} else {
int e = i;
idStream = IntStream.range(0, e).map(n -> e - 1 - n);
}
// update the other items if necessary
idStream.forEach(j -> {
CSLItemData item2 = sortedItems.get(j);
// determine new citation number
int citationNumber2;
if (citationNumberDirection > 0) {
citationNumber2 = j + 1;
} else {
citationNumber2 = sortedItems.size() - j;
}
// create new item data with new citation number
item2 = new CSLItemDataBuilder(item2)
.citationNumber(String.valueOf(citationNumber2))
.build();
// overwrite existing item data
sortedItems.set(j, item2);
registeredItems.put(item2.getId(), item2);
// store updated item
if (updatedItems != null) {
updatedItems.add(item2);
}
});
}
// save registered item data
registeredItems.put(itemData.getId(), itemData);
result.add(itemData);
}
return result;
}
/**
* Introduces the given citation IDs to the processor. The processor will
* call {@link ItemDataProvider#retrieveItem(String)} for each ID to get
* the respective citation item. The retrieved items will be added to the
* bibliography, so you don't have to call {@link #makeCitation(String...)}
* for each of them anymore.
* @param ids the IDs to register
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public void registerCitationItems(String... ids) {
registerCitationItems(ids, false);
}
/**
* Introduces the given citation IDs to the processor. The processor will
* call {@link ItemDataProvider#retrieveItem(String)} for each ID to get
* the respective citation item. The retrieved items will be added to the
* bibliography, so you don't have to call {@link #makeCitation(String...)}
* for each of them anymore.
* @param ids the IDs to register
* @param unsorted true if items should not be sorted in the bibliography
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public void registerCitationItems(String[] ids, boolean unsorted) {
registerCitationItems(Arrays.asList(ids), unsorted);
}
/**
* Introduces the given citation IDs to the processor. The processor will
* call {@link ItemDataProvider#retrieveItem(String)} for each ID to get
* the respective citation item. The retrieved items will be added to the
* bibliography, so you don't have to call {@link #makeCitation(String...)}
* for each of them anymore.
* @param ids the IDs to register
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public void registerCitationItems(Collection ids) {
registerCitationItems(ids, false);
}
/**
* Introduces the given citation IDs to the processor. The processor will
* call {@link ItemDataProvider#retrieveItem(String)} for each ID to get
* the respective citation item. The retrieved items will be added to the
* bibliography, so you don't have to call {@link #makeCitation(String...)}
* for each of them anymore.
* @param ids the IDs to register
* @param unsorted true if items should not be sorted in the bibliography
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public void registerCitationItems(Collection ids, boolean unsorted) {
registeredItems.clear();
sortedItems.clear();
registerItems(ids, null, unsorted);
}
/**
* Get an unmodifiable collection of all citation items that have been
* registered with the processor so far
* @return the registered citation items
*/
public Collection getRegisteredItems() {
return Collections.unmodifiableCollection(sortedItems);
}
/**
* Generates citation strings that can be inserted into the text. The
* method calls {@link ItemDataProvider#retrieveItem(String)} for each of the given
* IDs to request the corresponding citation item. Additionally, it saves
* the IDs, so {@link #makeBibliography()} will generate a bibliography
* that only consists of the retrieved citation items.
* @param ids IDs of citation items for which strings should be generated
* @return citations strings that can be inserted into the text
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public List makeCitation(String... ids) {
return makeCitation(Arrays.asList(ids));
}
/**
* Generates citation strings that can be inserted into the text. The
* method calls {@link ItemDataProvider#retrieveItem(String)} for each of the given
* IDs to request the corresponding citation item. Additionally, it saves
* the IDs, so {@link #makeBibliography()} will generate a bibliography
* that only consists of the retrieved citation items.
* @param ids IDs of citation items for which strings should be generated
* @return citations strings that can be inserted into the text
* @throws IllegalArgumentException if one of the given IDs refers to
* citation item data that does not exist
*/
public List makeCitation(Collection ids) {
CSLCitationItem[] items = new CSLCitationItem[ids.size()];
int i = 0;
for (String id : ids) {
items[i++] = new CSLCitationItem(id);
}
return makeCitation(new CSLCitation(items));
}
/**
* Perform steps to prepare the given citation for rendering. Register
* citation items and sort them. Return a prepared citation that can be
* passed to {@link #renderCitation(CSLCitation)}
* @param citation the citation to render
* @param updatedItems an empty set that will be filled with citation
* items that had to be updated while rendering the given one (may be
* {@code null})
* @return the prepared citation
*/
private CSLCitation preRenderCitation(CSLCitation citation,
Set updatedItems) {
// get item IDs
int len = citation.getCitationItems().length;
List itemIds = new ArrayList<>(len);
CSLCitationItem[] items = citation.getCitationItems();
for (int i = 0; i < len; i++) {
CSLCitationItem item = items[i];
itemIds.add(item.getId());
}
// register items
List registeredItems = registerItems(itemIds,
updatedItems, false);
// prepare items
CSLCitationItem[] preparedItems = new CSLCitationItem[len];
for (int i = 0; i < len; i++) {
CSLCitationItem item = items[i];
CSLItemData itemData = registeredItems.get(i);
// overwrite locator
if (item.getLocator() != null) {
itemData = new CSLItemDataBuilder(itemData)
.locator(item.getLocator())
.build();
}
preparedItems[i] = new CSLCitationItemBuilder(item)
.itemData(itemData)
.build();
}
// sort array of items
boolean unsorted = false;
if (citation.getProperties() != null &&
citation.getProperties().getUnsorted() != null) {
unsorted = citation.getProperties().getUnsorted();
}
if (!unsorted && style.getCitation().getSort() != null) {
Comparator itemComparator =
style.getCitation().getSort().comparator(style, locale,
abbreviationProvider);
Arrays.sort(preparedItems, (a, b) -> itemComparator.compare(
a.getItemData(), b.getItemData()));
}
return new CSLCitation(preparedItems,
citation.getCitationID(), citation.getProperties());
}
/**
* Render the given prepared citation
* @param preparedCitation the citation to render. The citation must have
* been prepared by {@link #preRenderCitation(CSLCitation, Set)}
* @return the rendered string
*/
private String renderCitation(CSLCitation preparedCitation) {
// render items
RenderContext ctx = new RenderContext(style, locale, null, abbreviationProvider,
preparedCitation, Collections.unmodifiableList(generatedCitations));
style.getCitation().render(ctx);
return outputFormat.formatCitation(ctx);
}
/**
* Generates citation strings that can be inserted into the text. The
* method calls {@link ItemDataProvider#retrieveItem(String)} for each item in the
* given set to request the corresponding citation item data. Additionally,
* it saves the requested citation IDs, so {@link #makeBibliography()} will
* generate a bibliography that only consists of the retrieved items.
* @param citation a set of citation items for which strings should be generated
* @return citations strings that can be inserted into the text
* @throws IllegalArgumentException if the given set of citation items
* refers to citation item data that does not exist
*/
public List makeCitation(CSLCitation citation) {
Set updatedItems = new LinkedHashSet<>();
CSLCitation preparedCitation = preRenderCitation(citation, updatedItems);
String text = renderCitation(preparedCitation);
// re-render updated citations
List result = new ArrayList<>();
if (!updatedItems.isEmpty()) {
List oldGeneratedCitations = generatedCitations;
generatedCitations = new ArrayList<>(oldGeneratedCitations.size());
for (int i = 0; i < oldGeneratedCitations.size(); i++) {
GeneratedCitation gc = oldGeneratedCitations.get(i);
boolean needsUpdate = false;
for (CSLItemData updatedItemData : updatedItems) {
for (CSLCitationItem item : gc.getOriginal().getCitationItems()) {
if (item.getId().equals(updatedItemData.getId())) {
needsUpdate = true;
break;
}
}
}
if (!needsUpdate) {
generatedCitations.add(gc);
continue;
}
// prepare citation again (!)
CSLCitation upc = preRenderCitation(gc.getOriginal(), null);
// render it again
String ut = renderCitation(upc);
if (!ut.equals(gc.getGenerated().getText())) {
// render result was different
Citation uc = new Citation(i, ut);
generatedCitations.add(new GeneratedCitation(
gc.getOriginal(), upc, uc));
result.add(uc);
}
}
}
// generate citation
Citation generatedCitation = new Citation(generatedCitations.size(), text);
generatedCitations.add(new GeneratedCitation(citation,
preparedCitation, generatedCitation));
result.add(generatedCitation);
return result;
}
/**
* Generates a bibliography for the registered citations
* @return the bibliography
*/
public Bibliography makeBibliography() {
return makeBibliography(null);
}
/**
* Generates a bibliography for the registered citations. Depending
* on the selection mode selects, includes, or excludes bibliography
* items whose fields and field values match the fields and field values
* from the given example item data objects.
* @param mode the selection mode
* @param selection the example item data objects that contain
* the fields and field values to match
* @return the bibliography
*/
public Bibliography makeBibliography(SelectionMode mode, CSLItemData... selection) {
return makeBibliography(mode, selection, null);
}
/**
* Generates a bibliography for the registered citations. Depending
* on the selection mode selects, includes, or excludes bibliography
* items whose fields and field values match the fields and field values
* from the given example item data objects.
* Note: This method will be deprecated in the next release.
* @param mode the selection mode
* @param selection the example item data objects that contain
* the fields and field values to match
* @param quash regardless of the item data in {@code selection}
* skip items if all fields/values from this list match
* @return the bibliography
*/
public Bibliography makeBibliography(SelectionMode mode,
CSLItemData[] selection, CSLItemData[] quash) {
return makeBibliography(item -> {
boolean include = true;
if (selection != null) {
switch (mode) {
case INCLUDE:
include = false;
for (CSLItemData s : selection) {
if (itemDataEqualsAny(item, s)) {
include = true;
break;
}
}
break;
case EXCLUDE:
for (CSLItemData s : selection) {
if (itemDataEqualsAny(item, s)) {
include = false;
break;
}
}
break;
case SELECT:
for (CSLItemData s : selection) {
if (!itemDataEqualsAny(item, s)) {
include = false;
break;
}
}
break;
}
}
if (include && quash != null) {
boolean match = true;
for (CSLItemData s : quash) {
if (!itemDataEqualsAny(item, s)) {
match = false;
break;
}
}
if (match) {
include = false;
}
}
return include;
});
}
/**
* Generates a bibliography for registered citations
* @param filter a function to apply to each registered citation item to
* determine if it should be included in the bibliography or not (may
* be {@code null} if all items should be included)
* @return the bibliography
*/
public Bibliography makeBibliography(Predicate filter) {
if (!canFormatBibliographies(style)) {
throw new IllegalStateException("The citation style does " +
"not contain instructions to format bibliographies");
}
List filteredItems;
if (filter == null) {
filteredItems = sortedItems;
} else {
filteredItems = new ArrayList<>();
for (CSLItemData item : sortedItems) {
if (filter.test(item)) {
filteredItems.add(item);
}
}
}
List entries = new ArrayList<>();
for (int i = 0; i < filteredItems.size(); i++) {
CSLItemData item = filteredItems.get(i);
RenderContext ctx = new RenderContext(style, locale, item,
abbreviationProvider);
style.getBibliography().render(ctx);
if (!ctx.getResult().isEmpty()) {
entries.add(outputFormat.formatBibliographyEntry(ctx, i));
}
}
return outputFormat.makeBibliography(entries.toArray(new String[0]),
style.getBibliography());
}
/**
* Resets the processor's state
*/
public void reset() {
outputFormat = new HtmlFormat();
convertLinks = false;
registeredItems.clear();
sortedItems.clear();
generatedCitations.clear();
}
/**
* Creates an ad hoc bibliography from the given citation items using the
* "html"
output format.
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param items the citation items to add to the bibliography
* @return the bibliography
* @throws IOException if the underlying JavaScript files or the CSL style
* could not be loaded
* @see #makeAdhocBibliography(String, String, CSLItemData...)
*/
public static Bibliography makeAdhocBibliography(String style, CSLItemData... items)
throws IOException {
return makeAdhocBibliography(style, "html", items);
}
/**
* Creates an ad hoc bibliography from the given citation items.
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param outputFormat the processor's output format (one of
* "html"
, "text"
, "asciidoc"
,
* "fo"
, or "rtf"
)
* @param items the citation items to add to the bibliography
* @return the bibliography
* @throws IOException if the CSL style could not be loaded
*/
public static Bibliography makeAdhocBibliography(String style, String outputFormat,
CSLItemData... items) throws IOException {
ItemDataProvider provider = new ListItemDataProvider(items);
CSL csl = new CSL(provider, style);
csl.setOutputFormat(outputFormat);
String[] ids = new String[items.length];
for (int i = 0; i < items.length; ++i) {
ids[i] = items[i].getId();
}
csl.registerCitationItems(ids);
return csl.makeBibliography();
}
/**
* Creates an ad hoc citation from the given citation items using the
* "html"
output format.
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param items the citation items
* @return a list of generated citations strings that can be inserted into the text
* @see #makeCitation(String...)
* @throws IOException if the CSL style could not be loaded
*/
public static List makeAdhocCitation(String style, CSLItemData... items)
throws IOException {
return makeAdhocCitation(style, "html", items);
}
/**
* Creates an ad hoc citation from the given citation items.
* @param style the citation style to use. May either be a serialized
* XML representation of the style or a style's name such as ieee
.
* In the latter case, the processor loads the style from the classpath (e.g.
* /ieee.csl
)
* @param outputFormat the processor's output format (one of
* "html"
, "text"
, "asciidoc"
,
* "fo"
, or "rtf"
)
* @param items the citation items
* @return a list of generated citations strings that can be inserted into the text
* @see #makeCitation(String...)
* @throws IOException if the CSL style could not be loaded
*/
public static List makeAdhocCitation(String style, String outputFormat,
CSLItemData... items) throws IOException {
ItemDataProvider provider = new ListItemDataProvider(items);
CSL csl = new CSL(provider, style);
csl.setOutputFormat(outputFormat);
String[] ids = new String[items.length];
for (int i = 0; i < items.length; ++i) {
ids[i] = items[i].getId();
}
return csl.makeCitation(ids);
}
/**
* Test if any of the attributes of {@code b} match the ones of {@code a}.
* Note: This method will be deprecated in the next release
* @param a the first object
* @param b the object to match against
* @return {@code true} if the match succeeds
*/
private static boolean itemDataEqualsAny(CSLItemData a, CSLItemData b) {
if (a == b) {
return true;
}
if (b == null) {
return false;
}
if (b.getId() != null && Objects.equals(a.getId(), b.getId())) {
return true;
}
if (b.getType() != null && Objects.equals(a.getType(), b.getType())) {
return true;
}
if (b.getCategories() != null && Arrays.equals(a.getCategories(), b.getCategories())) {
return true;
}
if (b.getLanguage() != null && Objects.equals(a.getLanguage(), b.getLanguage())) {
return true;
}
if (b.getJournalAbbreviation() != null && Objects.equals(a.getJournalAbbreviation(), b.getJournalAbbreviation())) {
return true;
}
if (b.getShortTitle() != null && Objects.equals(a.getShortTitle(), b.getShortTitle())) {
return true;
}
if (b.getAuthor() != null && Arrays.equals(a.getAuthor(), b.getAuthor())) {
return true;
}
if (b.getCollectionEditor() != null && Arrays.equals(a.getCollectionEditor(), b.getCollectionEditor())) {
return true;
}
if (b.getComposer() != null && Arrays.equals(a.getComposer(), b.getComposer())) {
return true;
}
if (b.getContainerAuthor() != null && Arrays.equals(a.getContainerAuthor(), b.getContainerAuthor())) {
return true;
}
if (b.getDirector() != null && Arrays.equals(a.getDirector(), b.getDirector())) {
return true;
}
if (b.getEditor() != null && Arrays.equals(a.getEditor(), b.getEditor())) {
return true;
}
if (b.getEditorialDirector() != null && Arrays.equals(a.getEditorialDirector(), b.getEditorialDirector())) {
return true;
}
if (b.getInterviewer() != null && Arrays.equals(a.getInterviewer(), b.getInterviewer())) {
return true;
}
if (b.getIllustrator() != null && Arrays.equals(a.getIllustrator(), b.getIllustrator())) {
return true;
}
if (b.getOriginalAuthor() != null && Arrays.equals(a.getOriginalAuthor(), b.getOriginalAuthor())) {
return true;
}
if (b.getRecipient() != null && Arrays.equals(a.getRecipient(), b.getRecipient())) {
return true;
}
if (b.getReviewedAuthor() != null && Arrays.equals(a.getReviewedAuthor(), b.getReviewedAuthor())) {
return true;
}
if (b.getTranslator() != null && Arrays.equals(a.getTranslator(), b.getTranslator())) {
return true;
}
if (b.getAccessed() != null && Objects.equals(a.getAccessed(), b.getAccessed())) {
return true;
}
if (b.getContainer() != null && Objects.equals(a.getContainer(), b.getContainer())) {
return true;
}
if (b.getEventDate() != null && Objects.equals(a.getEventDate(), b.getEventDate())) {
return true;
}
if (b.getIssued() != null && Objects.equals(a.getIssued(), b.getIssued())) {
return true;
}
if (b.getOriginalDate() != null && Objects.equals(a.getOriginalDate(), b.getOriginalDate())) {
return true;
}
if (b.getSubmitted() != null && Objects.equals(a.getSubmitted(), b.getSubmitted())) {
return true;
}
if (b.getAbstrct() != null && Objects.equals(a.getAbstrct(), b.getAbstrct())) {
return true;
}
if (b.getAnnote() != null && Objects.equals(a.getAnnote(), b.getAnnote())) {
return true;
}
if (b.getArchive() != null && Objects.equals(a.getArchive(), b.getArchive())) {
return true;
}
if (b.getArchiveLocation() != null && Objects.equals(a.getArchiveLocation(), b.getArchiveLocation())) {
return true;
}
if (b.getArchivePlace() != null && Objects.equals(a.getArchivePlace(), b.getArchivePlace())) {
return true;
}
if (b.getAuthority() != null && Objects.equals(a.getAuthority(), b.getAuthority())) {
return true;
}
if (b.getCallNumber() != null && Objects.equals(a.getCallNumber(), b.getCallNumber())) {
return true;
}
if (b.getChapterNumber() != null && Objects.equals(a.getChapterNumber(), b.getChapterNumber())) {
return true;
}
if (b.getCitationNumber() != null && Objects.equals(a.getCitationNumber(), b.getCitationNumber())) {
return true;
}
if (b.getCitationLabel() != null && Objects.equals(a.getCitationLabel(), b.getCitationLabel())) {
return true;
}
if (b.getCollectionNumber() != null && Objects.equals(a.getCollectionNumber(), b.getCollectionNumber())) {
return true;
}
if (b.getCollectionTitle() != null && Objects.equals(a.getCollectionTitle(), b.getCollectionTitle())) {
return true;
}
if (b.getContainerTitle() != null && Objects.equals(a.getContainerTitle(), b.getContainerTitle())) {
return true;
}
if (b.getContainerTitleShort() != null && Objects.equals(a.getContainerTitleShort(), b.getContainerTitleShort())) {
return true;
}
if (b.getDimensions() != null && Objects.equals(a.getDimensions(), b.getDimensions())) {
return true;
}
if (b.getDOI() != null && Objects.equals(a.getDOI(), b.getDOI())) {
return true;
}
if (b.getEdition() != null && Objects.equals(a.getEdition(), b.getEdition())) {
return true;
}
if (b.getEvent() != null && Objects.equals(a.getEvent(), b.getEvent())) {
return true;
}
if (b.getEventPlace() != null && Objects.equals(a.getEventPlace(), b.getEventPlace())) {
return true;
}
if (b.getFirstReferenceNoteNumber() != null && Objects.equals(a.getFirstReferenceNoteNumber(), b.getFirstReferenceNoteNumber())) {
return true;
}
if (b.getGenre() != null && Objects.equals(a.getGenre(), b.getGenre())) {
return true;
}
if (b.getISBN() != null && Objects.equals(a.getISBN(), b.getISBN())) {
return true;
}
if (b.getISSN() != null && Objects.equals(a.getISSN(), b.getISSN())) {
return true;
}
if (b.getIssue() != null && Objects.equals(a.getIssue(), b.getIssue())) {
return true;
}
if (b.getJurisdiction() != null && Objects.equals(a.getJurisdiction(), b.getJurisdiction())) {
return true;
}
if (b.getKeyword() != null && Objects.equals(a.getKeyword(), b.getKeyword())) {
return true;
}
if (b.getLocator() != null && Objects.equals(a.getLocator(), b.getLocator())) {
return true;
}
if (b.getMedium() != null && Objects.equals(a.getMedium(), b.getMedium())) {
return true;
}
if (b.getNote() != null && Objects.equals(a.getNote(), b.getNote())) {
return true;
}
if (b.getNumber() != null && Objects.equals(a.getNumber(), b.getNumber())) {
return true;
}
if (b.getNumberOfPages() != null && Objects.equals(a.getNumberOfPages(), b.getNumberOfPages())) {
return true;
}
if (b.getNumberOfVolumes() != null && Objects.equals(a.getNumberOfVolumes(), b.getNumberOfVolumes())) {
return true;
}
if (b.getOriginalPublisher() != null && Objects.equals(a.getOriginalPublisher(), b.getOriginalPublisher())) {
return true;
}
if (b.getOriginalPublisherPlace() != null && Objects.equals(a.getOriginalPublisherPlace(), b.getOriginalPublisherPlace())) {
return true;
}
if (b.getOriginalTitle() != null && Objects.equals(a.getOriginalTitle(), b.getOriginalTitle())) {
return true;
}
if (b.getPage() != null && Objects.equals(a.getPage(), b.getPage())) {
return true;
}
if (b.getPageFirst() != null && Objects.equals(a.getPageFirst(), b.getPageFirst())) {
return true;
}
if (b.getPMCID() != null && Objects.equals(a.getPMCID(), b.getPMCID())) {
return true;
}
if (b.getPMID() != null && Objects.equals(a.getPMID(), b.getPMID())) {
return true;
}
if (b.getPublisher() != null && Objects.equals(a.getPublisher(), b.getPublisher())) {
return true;
}
if (b.getPublisherPlace() != null && Objects.equals(a.getPublisherPlace(), b.getPublisherPlace())) {
return true;
}
if (b.getReferences() != null && Objects.equals(a.getReferences(), b.getReferences())) {
return true;
}
if (b.getReviewedTitle() != null && Objects.equals(a.getReviewedTitle(), b.getReviewedTitle())) {
return true;
}
if (b.getScale() != null && Objects.equals(a.getScale(), b.getScale())) {
return true;
}
if (b.getSection() != null && Objects.equals(a.getSection(), b.getSection())) {
return true;
}
if (b.getSource() != null && Objects.equals(a.getSource(), b.getSource())) {
return true;
}
if (b.getStatus() != null && Objects.equals(a.getStatus(), b.getStatus())) {
return true;
}
if (b.getTitle() != null && Objects.equals(a.getTitle(), b.getTitle())) {
return true;
}
if (b.getTitleShort() != null && Objects.equals(a.getTitleShort(), b.getTitleShort())) {
return true;
}
if (b.getURL() != null && Objects.equals(a.getURL(), b.getURL())) {
return true;
}
if (b.getVersion() != null && Objects.equals(a.getVersion(), b.getVersion())) {
return true;
}
if (b.getVolume() != null && Objects.equals(a.getVolume(), b.getVolume())) {
return true;
}
return b.getYearSuffix() != null && Objects.equals(a.getYearSuffix(), b.getYearSuffix());
}
}