package org.jabref.gui.openoffice; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import org.jabref.logic.bibtex.comparator.FieldComparator; import org.jabref.logic.bibtex.comparator.FieldComparatorStack; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.Layout; import org.jabref.logic.openoffice.OOBibStyle; import org.jabref.logic.openoffice.OOPreFormatter; import org.jabref.logic.openoffice.OOUtil; import org.jabref.logic.openoffice.UndefinedBibtexEntry; import org.jabref.logic.openoffice.UndefinedParagraphFormatException; import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import com.sun.star.awt.Point; import com.sun.star.beans.IllegalTypeException; import com.sun.star.beans.NotRemoveableException; import com.sun.star.beans.PropertyExistException; import com.sun.star.beans.PropertyVetoException; import com.sun.star.beans.UnknownPropertyException; import com.sun.star.beans.XPropertyContainer; import com.sun.star.beans.XPropertySet; import com.sun.star.comp.helper.Bootstrap; import com.sun.star.comp.helper.BootstrapException; import com.sun.star.container.NoSuchElementException; import com.sun.star.container.XEnumeration; import com.sun.star.container.XEnumerationAccess; import com.sun.star.container.XNameAccess; import com.sun.star.container.XNamed; import com.sun.star.document.XDocumentPropertiesSupplier; import com.sun.star.frame.XComponentLoader; import com.sun.star.frame.XController; import com.sun.star.frame.XDesktop; import com.sun.star.frame.XModel; import com.sun.star.lang.DisposedException; import com.sun.star.lang.IllegalArgumentException; import com.sun.star.lang.Locale; import com.sun.star.lang.WrappedTargetException; import com.sun.star.lang.XComponent; import com.sun.star.lang.XMultiComponentFactory; import com.sun.star.lang.XMultiServiceFactory; import com.sun.star.text.XBookmarksSupplier; import com.sun.star.text.XDocumentIndexesSupplier; import com.sun.star.text.XFootnote; import com.sun.star.text.XReferenceMarksSupplier; import com.sun.star.text.XText; import com.sun.star.text.XTextContent; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextDocument; import com.sun.star.text.XTextRange; import com.sun.star.text.XTextRangeCompare; import com.sun.star.text.XTextSection; import com.sun.star.text.XTextSectionsSupplier; import com.sun.star.text.XTextViewCursor; import com.sun.star.text.XTextViewCursorSupplier; import com.sun.star.uno.Any; import com.sun.star.uno.Type; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XComponentContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Class for manipulating the Bibliography of the currently start document in OpenOffice. */ class OOBibBase { private static final OOPreFormatter POSTFORMATTER = new OOPreFormatter(); private static final String BIB_SECTION_NAME = "JR_bib"; private static final String BIB_SECTION_END_NAME = "JR_bib_end"; private static final String BIB_CITATION = "JR_cite"; private static final Pattern CITE_PATTERN = Pattern.compile(OOBibBase.BIB_CITATION + "\\d*_(\\d*)_(.*)"); private static final String CHAR_STYLE_NAME = "CharStyleName"; private static final int AUTHORYEAR_PAR = 1; private static final int AUTHORYEAR_INTEXT = 2; private static final int INVISIBLE_CIT = 3; private static final Log LOGGER = LogFactory.getLog(OOBibBase.class); private XMultiServiceFactory mxDocFactory; private XTextDocument mxDoc; private XText text; private final XDesktop xDesktop; private XTextViewCursorSupplier xViewCursorSupplier; private XComponent xCurrentComponent; private XPropertySet propertySet; private XPropertyContainer userProperties; private final boolean atEnd; private final Comparator<BibEntry> entryComparator; private final Comparator<BibEntry> yearAuthorTitleComparator; private final FieldComparator authComp = new FieldComparator(FieldName.AUTHOR); private final FieldComparator yearComp = new FieldComparator(FieldName.YEAR); private final FieldComparator titleComp = new FieldComparator(FieldName.TITLE); private final List<Comparator<BibEntry>> authorYearTitleList = new ArrayList<>(3); private final List<Comparator<BibEntry>> yearAuthorTitleList = new ArrayList<>(3); private final Map<String, String> uniquefiers = new HashMap<>(); private List<String> sortedReferenceMarks; public OOBibBase(String pathToOO, boolean atEnd) throws IOException, IllegalAccessException, InvocationTargetException, BootstrapException, CreationException, UnknownPropertyException, WrappedTargetException, IndexOutOfBoundsException, NoSuchElementException, NoDocumentException { authorYearTitleList.add(authComp); authorYearTitleList.add(yearComp); authorYearTitleList.add(titleComp); yearAuthorTitleList.add(yearComp); yearAuthorTitleList.add(authComp); yearAuthorTitleList.add(titleComp); entryComparator = new FieldComparatorStack<>(authorYearTitleList); yearAuthorTitleComparator = new FieldComparatorStack<>(yearAuthorTitleList); this.atEnd = atEnd; xDesktop = simpleBootstrap(pathToOO); selectDocument(); } public boolean isConnectedToDocument() { return xCurrentComponent != null; } public Optional<String> getCurrentDocumentTitle() { if (mxDoc == null) { return Optional.empty(); } else { try { return Optional .of(String.valueOf(OOUtil.getProperty(mxDoc.getCurrentController().getFrame(), "Title"))); } catch (UnknownPropertyException | WrappedTargetException e) { LOGGER.warn("Could not get document title", e); return Optional.empty(); } } } public void selectDocument() throws UnknownPropertyException, WrappedTargetException, IndexOutOfBoundsException, NoSuchElementException, NoDocumentException { List<XTextDocument> textDocumentList = getTextDocuments(); XTextDocument selected; if (textDocumentList.isEmpty()) { // No text documents found. throw new NoDocumentException("No Writer documents found"); } else if (textDocumentList.size() == 1) { // Get the only one selected = textDocumentList.get(0); } else { // Bring up a dialog selected = selectComponent(textDocumentList); } if (selected == null) { return; } xCurrentComponent = UnoRuntime.queryInterface(XComponent.class, selected); mxDoc = selected; UnoRuntime.queryInterface(XDocumentIndexesSupplier.class, xCurrentComponent); XModel xModel = UnoRuntime.queryInterface(XModel.class, xCurrentComponent); XController xController = xModel.getCurrentController(); xViewCursorSupplier = UnoRuntime.queryInterface(XTextViewCursorSupplier.class, xController); // get a reference to the body text of the document text = mxDoc.getText(); // Access the text document's multi service factory: mxDocFactory = UnoRuntime.queryInterface(XMultiServiceFactory.class, mxDoc); XDocumentPropertiesSupplier supp = UnoRuntime.queryInterface(XDocumentPropertiesSupplier.class, mxDoc); userProperties = supp.getDocumentProperties().getUserDefinedProperties(); propertySet = UnoRuntime.queryInterface(XPropertySet.class, userProperties); } private XDesktop simpleBootstrap(String pathToExecutable) throws IllegalAccessException, InvocationTargetException, BootstrapException, CreationException, IOException { ClassLoader loader = ClassLoader.getSystemClassLoader(); if (loader instanceof URLClassLoader) { URLClassLoader cl = (URLClassLoader) loader; Class<URLClassLoader> sysclass = URLClassLoader.class; try { Method method = sysclass.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); method.invoke(cl, new File(pathToExecutable).toURI().toURL()); } catch (SecurityException | NoSuchMethodException | MalformedURLException t) { LOGGER.error("Error, could not add URL to system classloader", t); cl.close(); throw new IOException("Error, could not add URL to system classloader", t); } } else { LOGGER.error("Error occured, URLClassLoader expected but " + loader.getClass() + " received. Could not continue."); } //Get the office component context: XComponentContext xContext = Bootstrap.bootstrap(); //Get the office service manager: XMultiComponentFactory xServiceManager = xContext.getServiceManager(); //Create the desktop, which is the root frame of the //hierarchy of frames that contain viewable components: Object desktop; try { desktop = xServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", xContext); } catch (Exception e) { throw new CreationException(e.getMessage()); } XDesktop resultDesktop = UnoRuntime.queryInterface(XDesktop.class, desktop); UnoRuntime.queryInterface(XComponentLoader.class, desktop); return resultDesktop; } private List<XTextDocument> getTextDocuments() throws NoSuchElementException, WrappedTargetException { List<XTextDocument> result = new ArrayList<>(); XEnumerationAccess enumAccess = xDesktop.getComponents(); XEnumeration componentEnumeration = enumAccess.createEnumeration(); // TODO: http://api.openoffice.org/docs/DevelopersGuide/OfficeDev/OfficeDev.xhtml#1_1_3_2_1_2_Frame_Hierarchies while (componentEnumeration.hasMoreElements()) { Object nextElement = componentEnumeration.nextElement(); XComponent component = UnoRuntime.queryInterface(XComponent.class, nextElement); XTextDocument document = UnoRuntime.queryInterface(XTextDocument.class, component); if (document != null) { result.add(document); } } return result; } public void setCustomProperty(String property, String value) throws UnknownPropertyException, NotRemoveableException, PropertyExistException, IllegalTypeException, IllegalArgumentException { if (propertySet.getPropertySetInfo().hasPropertyByName(property)) { userProperties.removeProperty(property); } if (value != null) { userProperties.addProperty(property, com.sun.star.beans.PropertyAttribute.REMOVEABLE, new Any(Type.STRING, value)); } } public Optional<String> getCustomProperty(String property) throws UnknownPropertyException, WrappedTargetException { if (propertySet.getPropertySetInfo().hasPropertyByName(property)) { return Optional.ofNullable(propertySet.getPropertyValue(property).toString()); } return Optional.empty(); } public void updateSortedReferenceMarks() throws WrappedTargetException, NoSuchElementException { sortedReferenceMarks = getSortedReferenceMarks(getReferenceMarks()); } /** * This method inserts a cite marker in the text for the given BibEntry, * and may refresh the bibliography. * @param entries The entries to cite. * @param database The database the entry belongs to. * @param style The bibliography style we are using. * @param inParenthesis Indicates whether it is an in-text citation or a citation in parenthesis. * This is not relevant if numbered citations are used. * @param withText Indicates whether this should be a normal citation (true) or an empty * (invisible) citation (false). * @param sync Indicates whether the reference list should be refreshed. * @throws IllegalTypeException * @throws PropertyExistException * @throws NotRemoveableException * @throws UnknownPropertyException * @throws UndefinedCharacterFormatException * @throws NoSuchElementException * @throws WrappedTargetException * @throws IOException * @throws PropertyVetoException * @throws CreationException * @throws BibEntryNotFoundException * @throws UndefinedParagraphFormatException */ public void insertEntry(List<BibEntry> entries, BibDatabase database, List<BibDatabase> allBases, OOBibStyle style, boolean inParenthesis, boolean withText, String pageInfo, boolean sync) throws IllegalArgumentException, UnknownPropertyException, NotRemoveableException, PropertyExistException, IllegalTypeException, UndefinedCharacterFormatException, WrappedTargetException, NoSuchElementException, PropertyVetoException, IOException, CreationException, BibEntryNotFoundException, UndefinedParagraphFormatException { try { XTextViewCursor xViewCursor = xViewCursorSupplier.getViewCursor(); if (entries.size() > 1) { if (style.getBooleanCitProperty(OOBibStyle.MULTI_CITE_CHRONOLOGICAL)) { entries.sort(yearAuthorTitleComparator); } else { entries.sort(entryComparator); } } String keyString = String.join(",", entries.stream().map(entry -> entry.getCiteKeyOptional().orElse("")).collect(Collectors.toList())); // Insert bookmark: String bName = getUniqueReferenceMarkName(keyString, withText ? inParenthesis ? OOBibBase.AUTHORYEAR_PAR : OOBibBase.AUTHORYEAR_INTEXT : OOBibBase.INVISIBLE_CIT); // If we should store metadata for page info, do that now: if (pageInfo != null) { LOGGER.info("Storing page info: " + pageInfo); setCustomProperty(bName, pageInfo); } xViewCursor.getText().insertString(xViewCursor, " ", false); if (style.isFormatCitations()) { XPropertySet xCursorProps = UnoRuntime.queryInterface(XPropertySet.class, xViewCursor); String charStyle = style.getCitationCharacterFormat(); try { xCursorProps.setPropertyValue(CHAR_STYLE_NAME, charStyle); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException ex) { // Setting the character format failed, so we throw an exception that // will result in an error message for the user. Before that, // delete the space we inserted: xViewCursor.goLeft((short) 1, true); xViewCursor.setString(""); throw new UndefinedCharacterFormatException(charStyle); } } xViewCursor.goLeft((short) 1, false); Map<BibEntry, BibDatabase> databaseMap = new HashMap<>(); for (BibEntry entry : entries) { databaseMap.put(entry, database); } String citeText = style.isNumberEntries() ? "-" : style.getCitationMarker(entries, databaseMap, inParenthesis, null, null); insertReferenceMark(bName, citeText, xViewCursor, withText, style); xViewCursor.collapseToEnd(); xViewCursor.goRight((short) 1, false); XTextRange position = xViewCursor.getEnd(); if (sync) { // To account for numbering and for uniqiefiers, we must refresh the cite markers: updateSortedReferenceMarks(); refreshCiteMarkers(allBases, style); // Insert it at the current position: rebuildBibTextSection(allBases, style); } // Go back to the relevant position: xViewCursor.gotoRange(position, false); } catch (DisposedException ex) { // We need to catch this one here because the OpenOfficePanel class is // loaded before connection, and therefore cannot directly reference // or catch a DisposedException (which is in a OO JAR file). throw new ConnectionLostException(ex.getMessage()); } } /** * Refresh all cite markers in the document. * @param databases The databases to get entries from. * @param style The bibliography style to use. * @return A list of those referenced BibTeX keys that could not be resolved. * @throws UndefinedCharacterFormatException * @throws NoSuchElementException * @throws IllegalArgumentException * @throws WrappedTargetException * @throws BibEntryNotFoundException * @throws CreationException * @throws IOException * @throws PropertyVetoException * @throws UnknownPropertyException */ public List<String> refreshCiteMarkers(List<BibDatabase> databases, OOBibStyle style) throws WrappedTargetException, IllegalArgumentException, NoSuchElementException, UndefinedCharacterFormatException, UnknownPropertyException, PropertyVetoException, IOException, CreationException, BibEntryNotFoundException { try { return refreshCiteMarkersInternal(databases, style); } catch (DisposedException ex) { // We need to catch this one here because the OpenOfficePanel class is // loaded before connection, and therefore cannot directly reference // or catch a DisposedException (which is in a OO JAR file). throw new ConnectionLostException(ex.getMessage()); } } public List<String> getJabRefReferenceMarks(XNameAccess nameAccess) { String[] names = nameAccess.getElementNames(); // Remove all reference marks that don't look like JabRef citations: List<String> result = new ArrayList<>(); if (names != null) { for (String name : names) { if (CITE_PATTERN.matcher(name).find()) { result.add(name); } } } return result; } private List<String> refreshCiteMarkersInternal(List<BibDatabase> databases, OOBibStyle style) throws WrappedTargetException, IllegalArgumentException, NoSuchElementException, UndefinedCharacterFormatException, UnknownPropertyException, PropertyVetoException, CreationException, BibEntryNotFoundException { List<String> cited = findCitedKeys(); Map<String, BibDatabase> linkSourceBase = new HashMap<>(); Map<BibEntry, BibDatabase> entries = findCitedEntries(databases, cited, linkSourceBase); XNameAccess xReferenceMarks = getReferenceMarks(); List<String> names; if (style.isSortByPosition()) { // We need to sort the reference marks according to their order of appearance: names = sortedReferenceMarks; } else if (style.isNumberEntries()) { // We need to sort the reference marks according to the sorting of the bibliographic // entries: SortedMap<BibEntry, BibDatabase> newMap = new TreeMap<>(entryComparator); for (Map.Entry<BibEntry, BibDatabase> bibtexEntryBibtexDatabaseEntry : entries.entrySet()) { newMap.put(bibtexEntryBibtexDatabaseEntry.getKey(), bibtexEntryBibtexDatabaseEntry.getValue()); } entries = newMap; // Rebuild the list of cited keys according to the sort order: cited.clear(); for (BibEntry entry : entries.keySet()) { cited.add(entry.getCiteKeyOptional().orElse(null)); } names = Arrays.asList(xReferenceMarks.getElementNames()); } else { names = sortedReferenceMarks; } // Remove all reference marks that don't look like JabRef citations: List<String> tmp = new ArrayList<>(); for (String name : names) { if (CITE_PATTERN.matcher(name).find()) { tmp.add(name); } } names = tmp; Map<String, Integer> numbers = new HashMap<>(); int lastNum = 0; // First compute citation markers for all citations: String[] citMarkers = new String[names.size()]; String[][] normCitMarkers = new String[names.size()][]; String[][] bibtexKeys = new String[names.size()][]; int minGroupingCount = style.getIntCitProperty(OOBibStyle.MINIMUM_GROUPING_COUNT); int[] types = new int[names.size()]; for (int i = 0; i < names.size(); i++) { Matcher citeMatcher = CITE_PATTERN.matcher(names.get(i)); if (citeMatcher.find()) { String typeStr = citeMatcher.group(1); int type = Integer.parseInt(typeStr); types[i] = type; // Remember the type in case we need to uniquefy. String[] keys = citeMatcher.group(2).split(","); bibtexKeys[i] = keys; BibEntry[] cEntries = new BibEntry[keys.length]; for (int j = 0; j < cEntries.length; j++) { BibDatabase database = linkSourceBase.get(keys[j]); Optional<BibEntry> tmpEntry = Optional.empty(); if (database != null) { tmpEntry = database.getEntryByKey(keys[j]); } if (tmpEntry.isPresent()) { cEntries[j] = tmpEntry.get(); } else { LOGGER.info("BibTeX key not found: '" + keys[j] + '\''); LOGGER.info("Problem with reference mark: '" + names.get(i) + '\''); cEntries[j] = new UndefinedBibtexEntry(keys[j]); } } String[] normCitMarker = new String[keys.length]; String citationMarker; if (style.isBibtexKeyCiteMarkers()) { StringBuilder sb = new StringBuilder(); normCitMarkers[i] = new String[keys.length]; for (int j = 0; j < keys.length; j++) { normCitMarkers[i][j] = cEntries[j].getCiteKeyOptional().orElse(null); sb.append(cEntries[j].getCiteKeyOptional().orElse("")); if (j < (keys.length - 1)) { sb.append(','); } } citationMarker = sb.toString(); } else if (style.isNumberEntries()) { if (style.isSortByPosition()) { // We have sorted the citation markers according to their order of appearance, // so we simply count up for each marker referring to a new entry: List<Integer> num = new ArrayList<>(keys.length); for (int j = 0; j < keys.length; j++) { if (cEntries[j] instanceof UndefinedBibtexEntry) { num.add(j, -1); } else { num.add(j, lastNum + 1); if (numbers.containsKey(keys[j])) { num.set(j, numbers.get(keys[j])); } else { numbers.put(keys[j], num.get(j)); lastNum = num.get(j); } } } citationMarker = style.getNumCitationMarker(num, minGroupingCount, false); for (int j = 0; j < keys.length; j++) { normCitMarker[j] = style.getNumCitationMarker(Collections.singletonList(num.get(j)), minGroupingCount, false); } } else { // We need to find the number of the cited entry in the bibliography, // and use that number for the cite marker: List<Integer> num = findCitedEntryIndex(names.get(i), cited); if (num.isEmpty()) { throw new BibEntryNotFoundException(names.get(i), Localization .lang("Could not resolve BibTeX entry for citation marker '%0'.", names.get(i))); } else { citationMarker = style.getNumCitationMarker(num, minGroupingCount, false); } for (int j = 0; j < keys.length; j++) { List<Integer> list = new ArrayList<>(1); list.add(num.get(j)); normCitMarker[j] = style.getNumCitationMarker(list, minGroupingCount, false); } } } else { if (cEntries.length > 1) { if (style.getBooleanCitProperty(OOBibStyle.MULTI_CITE_CHRONOLOGICAL)) { Arrays.sort(cEntries, yearAuthorTitleComparator); } else { Arrays.sort(cEntries, entryComparator); } // Update key list to match the new sorting: for (int j = 0; j < cEntries.length; j++) { bibtexKeys[i][j] = cEntries[j].getCiteKeyOptional().orElse(null); } } citationMarker = style.getCitationMarker(Arrays.asList(cEntries), entries, type == OOBibBase.AUTHORYEAR_PAR, null, null); // We need "normalized" (in parenthesis) markers for uniqueness checking purposes: for (int j = 0; j < cEntries.length; j++) { normCitMarker[j] = style.getCitationMarker(Collections.singletonList(cEntries[j]), entries, true, null, new int[] {-1}); } } citMarkers[i] = citationMarker; normCitMarkers[i] = normCitMarker; } } uniquefiers.clear(); if (!style.isBibtexKeyCiteMarkers() && !style.isNumberEntries()) { // See if there are duplicate citations marks referring to different entries. If so, we need to // use uniquefiers: Map<String, List<String>> refKeys = new HashMap<>(); Map<String, List<Integer>> refNums = new HashMap<>(); for (int i = 0; i < citMarkers.length; i++) { String[] markers = normCitMarkers[i]; // compare normalized markers, since the actual markers can be different for (int j = 0; j < markers.length; j++) { String marker = markers[j]; String currentKey = bibtexKeys[i][j]; if (refKeys.containsKey(marker)) { // Ok, we have seen this exact marker before. if (!refKeys.get(marker).contains(currentKey)) { // ... but not for this entry. refKeys.get(marker).add(currentKey); refNums.get(marker).add(i); } } else { List<String> l = new ArrayList<>(1); l.add(currentKey); refKeys.put(marker, l); List<Integer> l2 = new ArrayList<>(1); l2.add(i); refNums.put(marker, l2); } } } // Go through the collected lists and see where we need to uniquefy: for (Map.Entry<String, List<String>> stringListEntry : refKeys.entrySet()) { List<String> keys = stringListEntry.getValue(); if (keys.size() > 1) { // This marker appears for more than one unique entry: int uniq = 'a'; for (String key : keys) { // Update the map of uniquefiers for the benefit of both the following generation of new // citation markers, and for the method that builds the bibliography: uniquefiers.put(key, String.valueOf((char) uniq)); uniq++; } } } // Finally, go through all citation markers, and update those referring to entries in our current list: int maxAuthorsFirst = style.getIntCitProperty(OOBibStyle.MAX_AUTHORS_FIRST); Set<String> seenBefore = new HashSet<>(); for (int j = 0; j < bibtexKeys.length; j++) { boolean needsChange = false; int[] firstLimAuthors = new int[bibtexKeys[j].length]; String[] uniquif = new String[bibtexKeys[j].length]; BibEntry[] cEntries = new BibEntry[bibtexKeys[j].length]; for (int k = 0; k < bibtexKeys[j].length; k++) { String currentKey = bibtexKeys[j][k]; firstLimAuthors[k] = -1; if (maxAuthorsFirst > 0) { if (!seenBefore.contains(currentKey)) { firstLimAuthors[k] = maxAuthorsFirst; } seenBefore.add(currentKey); } String uniq = uniquefiers.get(currentKey); Optional<BibEntry> tmpEntry = Optional.empty(); if (uniq == null) { if (firstLimAuthors[k] > 0) { needsChange = true; BibDatabase database = linkSourceBase.get(currentKey); if (database != null) { tmpEntry = database.getEntryByKey(currentKey); } } else { BibDatabase database = linkSourceBase.get(currentKey); if (database != null) { tmpEntry = database.getEntryByKey(currentKey); } } uniquif[k] = ""; } else { needsChange = true; BibDatabase database = linkSourceBase.get(currentKey); if (database != null) { tmpEntry = database.getEntryByKey(currentKey); } uniquif[k] = uniq; } if (tmpEntry.isPresent()) { cEntries[k] = tmpEntry.get(); } } if (needsChange) { citMarkers[j] = style.getCitationMarker(Arrays.asList(cEntries), entries, types[j] == OOBibBase.AUTHORYEAR_PAR, uniquif, firstLimAuthors); } } } // Refresh all reference marks with the citation markers we computed: boolean hadBibSection = getBookmarkRange(OOBibBase.BIB_SECTION_NAME) != null; // Check if we are supposed to set a character format for citations: boolean mustTestCharFormat = style.isFormatCitations(); for (int i = 0; i < names.size(); i++) { Object referenceMark = xReferenceMarks.getByName(names.get(i)); XTextContent bookmark = UnoRuntime.queryInterface(XTextContent.class, referenceMark); XTextCursor cursor = bookmark.getAnchor().getText().createTextCursorByRange(bookmark.getAnchor()); if (mustTestCharFormat) { // If we are supposed to set character format for citations, must run a test before we // delete old citation markers. Otherwise, if the specified character format doesn't // exist, we end up deleting the markers before the process crashes due to a the missing // format, with catastrophic consequences for the user. mustTestCharFormat = false; // need to do this only once XPropertySet xCursorProps = UnoRuntime.queryInterface(XPropertySet.class, cursor); String charStyle = style.getCitationCharacterFormat(); try { xCursorProps.setPropertyValue(CHAR_STYLE_NAME, charStyle); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException ex) { throw new UndefinedCharacterFormatException(charStyle); } } text.removeTextContent(bookmark); insertReferenceMark(names.get(i), citMarkers[i], cursor, types[i] != OOBibBase.INVISIBLE_CIT, style); if (hadBibSection && (getBookmarkRange(OOBibBase.BIB_SECTION_NAME) == null)) { // We have overwritten the marker for the start of the reference list. // We need to add it again. cursor.collapseToEnd(); OOUtil.insertParagraphBreak(text, cursor); insertBookMark(OOBibBase.BIB_SECTION_NAME, cursor); } } List<String> unresolvedKeys = new ArrayList<>(); for (BibEntry entry : entries.keySet()) { if (entry instanceof UndefinedBibtexEntry) { String key = ((UndefinedBibtexEntry) entry).getKey(); if (!unresolvedKeys.contains(key)) { unresolvedKeys.add(key); } } } return unresolvedKeys; } private List<String> getSortedReferenceMarks(final XNameAccess nameAccess) throws WrappedTargetException, NoSuchElementException { XTextViewCursorSupplier cursorSupplier = UnoRuntime.queryInterface(XTextViewCursorSupplier.class, mxDoc.getCurrentController()); XTextViewCursor viewCursor = cursorSupplier.getViewCursor(); XTextRange initialPos = viewCursor.getStart(); List<String> names = Arrays.asList(nameAccess.getElementNames()); List<Point> positions = new ArrayList<>(names.size()); for (String name : names) { XTextContent textContent = UnoRuntime.queryInterface(XTextContent.class, nameAccess.getByName(name)); XTextRange range = textContent.getAnchor(); // Check if we are inside a footnote: if (UnoRuntime.queryInterface(XFootnote.class, range.getText()) != null) { // Find the linking footnote marker: XFootnote footer = UnoRuntime.queryInterface(XFootnote.class, range.getText()); // The footnote's anchor gives the correct position in the text: range = footer.getAnchor(); } positions.add(findPosition(viewCursor, range)); } Set<ComparableMark> set = new TreeSet<>(); for (int i = 0; i < positions.size(); i++) { set.add(new ComparableMark(names.get(i), positions.get(i))); } List<String> result = new ArrayList<>(set.size()); for (ComparableMark mark : set) { result.add(mark.getName()); } viewCursor.gotoRange(initialPos, false); return result; } public void rebuildBibTextSection(List<BibDatabase> databases, OOBibStyle style) throws NoSuchElementException, WrappedTargetException, IllegalArgumentException, CreationException, PropertyVetoException, UnknownPropertyException, UndefinedParagraphFormatException { List<String> cited = findCitedKeys(); Map<String, BibDatabase> linkSourceBase = new HashMap<>(); Map<BibEntry, BibDatabase> entries = findCitedEntries(databases, cited, linkSourceBase); // Although entries are redefined without use, this also updates linkSourceBase List<String> names = sortedReferenceMarks; if (style.isSortByPosition()) { // We need to sort the entries according to their order of appearance: entries = getSortedEntriesFromSortedRefMarks(names, linkSourceBase); } else { SortedMap<BibEntry, BibDatabase> newMap = new TreeMap<>(entryComparator); for (Map.Entry<BibEntry, BibDatabase> bibtexEntryBibtexDatabaseEntry : findCitedEntries(databases, cited, linkSourceBase).entrySet()) { newMap.put(bibtexEntryBibtexDatabaseEntry.getKey(), bibtexEntryBibtexDatabaseEntry.getValue()); } entries = newMap; } clearBibTextSectionContent2(); populateBibTextSection(entries, style); } public XNameAccess getReferenceMarks() { XReferenceMarksSupplier supplier = UnoRuntime.queryInterface(XReferenceMarksSupplier.class, xCurrentComponent); return supplier.getReferenceMarks(); } private String getUniqueReferenceMarkName(String bibtexKey, int type) { XNameAccess xNamedRefMarks = getReferenceMarks(); int i = 0; String name = OOBibBase.BIB_CITATION + '_' + type + '_' + bibtexKey; while (xNamedRefMarks.hasByName(name)) { name = OOBibBase.BIB_CITATION + i + '_' + type + '_' + bibtexKey; i++; } return name; } private Map<BibEntry, BibDatabase> findCitedEntries(List<BibDatabase> databases, List<String> keys, Map<String, BibDatabase> linkSourceBase) { Map<BibEntry, BibDatabase> entries = new LinkedHashMap<>(); for (String key : keys) { boolean found = false; for (BibDatabase database : databases) { Optional<BibEntry> entry = database.getEntryByKey(key); if (entry.isPresent()) { entries.put(entry.get(), database); linkSourceBase.put(key, database); found = true; break; } } if (!found) { entries.put(new UndefinedBibtexEntry(key), null); } } return entries; } private List<String> findCitedKeys() throws NoSuchElementException, WrappedTargetException { XNameAccess xNamedMarks = getReferenceMarks(); String[] names = xNamedMarks.getElementNames(); List<String> keys = new ArrayList<>(); for (String name1 : names) { Object bookmark = xNamedMarks.getByName(name1); UnoRuntime.queryInterface(XTextContent.class, bookmark); List<String> newKeys = parseRefMarkName(name1); for (String key : newKeys) { if (!keys.contains(key)) { keys.add(key); } } } return keys; } private Map<BibEntry, BibDatabase> getSortedEntriesFromSortedRefMarks(List<String> names, Map<String, BibDatabase> linkSourceBase) { Map<BibEntry, BibDatabase> newList = new LinkedHashMap<>(); for (String name : names) { Matcher citeMatcher = CITE_PATTERN.matcher(name); if (citeMatcher.find()) { String[] keys = citeMatcher.group(2).split(","); for (String key : keys) { BibDatabase database = linkSourceBase.get(key); Optional<BibEntry> origEntry = Optional.empty(); if (database != null) { origEntry = database.getEntryByKey(key); } if (origEntry.isPresent()) { if (!newList.containsKey(origEntry.get())) { newList.put(origEntry.get(), database); } } else { LOGGER.info("BibTeX key not found: '" + key + "'"); LOGGER.info("Problem with reference mark: '" + name + "'"); newList.put(new UndefinedBibtexEntry(key), null); } } } } return newList; } private Point findPosition(XTextViewCursor cursor, XTextRange range) { cursor.gotoRange(range, false); return cursor.getPosition(); } /** * Extract the list of bibtex keys from a reference mark name. * @param name The reference mark name. * @return The list of bibtex keys encoded in the name. */ public List<String> parseRefMarkName(String name) { List<String> keys = new ArrayList<>(); Matcher citeMatcher = CITE_PATTERN.matcher(name); if (citeMatcher.find()) { String[] keystring = citeMatcher.group(2).split(","); for (String aKeystring : keystring) { if (!keys.contains(aKeystring)) { keys.add(aKeystring); } } } return keys; } /** * Resolve the bibtex key from a citation reference marker name, and look up * the index of the key in a list of keys. * @param citRefName The name of the ReferenceMark representing the citation. * @param keys A List of bibtex keys representing the entries in the bibliography. * @return the indices of the cited keys, -1 if a key is not found. Returns null if the ref name * could not be resolved as a citation. */ private List<Integer> findCitedEntryIndex(String citRefName, List<String> keys) { Matcher citeMatcher = CITE_PATTERN.matcher(citRefName); if (citeMatcher.find()) { List<String> keyStrings = Arrays.asList(citeMatcher.group(2).split(",")); List<Integer> result = new ArrayList<>(keyStrings.size()); for (String key : keyStrings) { int ind = keys.indexOf(key); result.add(ind == -1 ? -1 : 1 + ind); } return result; } else { return Collections.emptyList(); } } public String getCitationContext(XNameAccess nameAccess, String refMarkName, int charBefore, int charAfter, boolean htmlMarkup) throws NoSuchElementException, WrappedTargetException { Object referenceMark = nameAccess.getByName(refMarkName); XTextContent bookmark = UnoRuntime.queryInterface(XTextContent.class, referenceMark); XTextCursor cursor = bookmark.getAnchor().getText().createTextCursorByRange(bookmark.getAnchor()); String citPart = cursor.getString(); int flex = 8; for (int i = 0; i < charBefore; i++) { try { cursor.goLeft((short) 1, true); if ((i >= (charBefore - flex)) && Character.isWhitespace(cursor.getString().charAt(0))) { break; } } catch (IndexOutOfBoundsException ex) { LOGGER.warn("Problem going left", ex); } } int length = cursor.getString().length(); int added = length - citPart.length(); cursor.collapseToStart(); for (int i = 0; i < (charAfter + length); i++) { try { cursor.goRight((short) 1, true); if (i >= ((charAfter + length) - flex)) { String strNow = cursor.getString(); if (Character.isWhitespace(strNow.charAt(strNow.length() - 1))) { break; } } } catch (IndexOutOfBoundsException ex) { LOGGER.warn("Problem going right", ex); } } String result = cursor.getString(); if (htmlMarkup) { result = result.substring(0, added) + "<b>" + citPart + "</b>" + result.substring(length); } return result.trim(); } private void insertFullReferenceAtCursor(XTextCursor cursor, Map<BibEntry, BibDatabase> entries, OOBibStyle style, String parFormat) throws UndefinedParagraphFormatException, IllegalArgumentException, UnknownPropertyException, PropertyVetoException, WrappedTargetException { Map<BibEntry, BibDatabase> correctEntries; // If we don't have numbered entries, we need to sort the entries before adding them: if (style.isSortByPosition()) { // Use the received map directly correctEntries = entries; } else { // Sort map Map<BibEntry, BibDatabase> newMap = new TreeMap<>(entryComparator); newMap.putAll(entries); correctEntries = newMap; } int number = 1; for (Map.Entry<BibEntry, BibDatabase> entry : correctEntries.entrySet()) { if (entry.getKey() instanceof UndefinedBibtexEntry) { continue; } OOUtil.insertParagraphBreak(text, cursor); if (style.isNumberEntries()) { int minGroupingCount = style.getIntCitProperty(OOBibStyle.MINIMUM_GROUPING_COUNT); OOUtil.insertTextAtCurrentLocation(text, cursor, style.getNumCitationMarker(Collections.singletonList(number++), minGroupingCount, true), Collections.emptyList()); } Layout layout = style.getReferenceFormat(entry.getKey().getType()); layout.setPostFormatter(POSTFORMATTER); OOUtil.insertFullReferenceAtCurrentLocation(text, cursor, layout, parFormat, entry.getKey(), entry.getValue(), uniquefiers.get(entry.getKey().getCiteKeyOptional().orElse(null))); } } private void createBibTextSection2(boolean end) throws IllegalArgumentException, CreationException { XTextCursor mxDocCursor = text.createTextCursor(); if (end) { mxDocCursor.gotoEnd(false); } OOUtil.insertParagraphBreak(text, mxDocCursor); // Create a new TextSection from the document factory and access it's XNamed interface XNamed xChildNamed; try { xChildNamed = UnoRuntime.queryInterface(XNamed.class, mxDocFactory.createInstance("com.sun.star.text.TextSection")); } catch (Exception e) { throw new CreationException(e.getMessage()); } // Set the new sections name to 'Child_Section' xChildNamed.setName(OOBibBase.BIB_SECTION_NAME); // Access the Child_Section's XTextContent interface and insert it into the document XTextContent xChildSection = UnoRuntime.queryInterface(XTextContent.class, xChildNamed); text.insertTextContent(mxDocCursor, xChildSection, false); } private void clearBibTextSectionContent2() throws NoSuchElementException, WrappedTargetException, IllegalArgumentException, CreationException { // Check if the section exists: XTextSectionsSupplier supplier = UnoRuntime.queryInterface(XTextSectionsSupplier.class, mxDoc); if (supplier.getTextSections().hasByName(OOBibBase.BIB_SECTION_NAME)) { XTextSection section = (XTextSection) ((Any) supplier.getTextSections().getByName(OOBibBase.BIB_SECTION_NAME)) .getObject(); // Clear it: XTextCursor cursor = text.createTextCursorByRange(section.getAnchor()); cursor.gotoRange(section.getAnchor(), false); cursor.setString(""); } else { createBibTextSection2(atEnd); } } private void populateBibTextSection(Map<BibEntry, BibDatabase> entries, OOBibStyle style) throws NoSuchElementException, WrappedTargetException, PropertyVetoException, UnknownPropertyException, UndefinedParagraphFormatException, IllegalArgumentException, CreationException { XTextSectionsSupplier supplier = UnoRuntime.queryInterface(XTextSectionsSupplier.class, mxDoc); XTextSection section = (XTextSection) ((Any) supplier.getTextSections().getByName(OOBibBase.BIB_SECTION_NAME)) .getObject(); XTextCursor cursor = text.createTextCursorByRange(section.getAnchor()); OOUtil.insertTextAtCurrentLocation(text, cursor, (String) style.getProperty(OOBibStyle.TITLE), (String) style.getProperty(OOBibStyle.REFERENCE_HEADER_PARAGRAPH_FORMAT)); insertFullReferenceAtCursor(cursor, entries, style, (String) style.getProperty(OOBibStyle.REFERENCE_PARAGRAPH_FORMAT)); insertBookMark(OOBibBase.BIB_SECTION_END_NAME, cursor); } private XTextContent insertBookMark(String name, XTextCursor position) throws IllegalArgumentException, CreationException { Object bookmark; try { bookmark = mxDocFactory.createInstance("com.sun.star.text.Bookmark"); } catch (Exception e) { throw new CreationException(e.getMessage()); } // name the bookmark XNamed xNamed = UnoRuntime.queryInterface(XNamed.class, bookmark); xNamed.setName(name); // get XTextContent interface XTextContent xTextContent = UnoRuntime.queryInterface(XTextContent.class, bookmark); // insert bookmark at the end of the document // instead of mxDocText.getEnd you could use a text cursor's XTextRange interface or any XTextRange text.insertTextContent(position, xTextContent, true); position.collapseToEnd(); return xTextContent; } private void insertReferenceMark(String name, String citationText, XTextCursor position, boolean withText, OOBibStyle style) throws UnknownPropertyException, WrappedTargetException, PropertyVetoException, IllegalArgumentException, UndefinedCharacterFormatException, CreationException { // Check if there is "page info" stored for this citation. If so, insert it into // the citation text before inserting the citation: Optional<String> pageInfo = getCustomProperty(name); String citText; if ((pageInfo.isPresent()) && !pageInfo.get().isEmpty()) { citText = style.insertPageInfo(citationText, pageInfo.get()); } else { citText = citationText; } Object bookmark; try { bookmark = mxDocFactory.createInstance("com.sun.star.text.ReferenceMark"); } catch (Exception e) { throw new CreationException(e.getMessage()); } // Name the reference XNamed xNamed = UnoRuntime.queryInterface(XNamed.class, bookmark); xNamed.setName(name); if (withText) { position.setString(citText); XPropertySet xCursorProps = UnoRuntime.queryInterface(XPropertySet.class, position); // Set language to [None]: xCursorProps.setPropertyValue("CharLocale", new Locale("zxx", "", "")); if (style.isFormatCitations()) { String charStyle = style.getCitationCharacterFormat(); try { xCursorProps.setPropertyValue(CHAR_STYLE_NAME, charStyle); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException ex) { throw new UndefinedCharacterFormatException(charStyle); } } } else { position.setString(""); } // get XTextContent interface XTextContent xTextContent = UnoRuntime.queryInterface(XTextContent.class, bookmark); position.getText().insertTextContent(position, xTextContent, true); // Check if we should italicize the "et al." string in citations: boolean italicize = style.getBooleanCitProperty(OOBibStyle.ITALIC_ET_AL); if (italicize) { String etAlString = style.getStringCitProperty(OOBibStyle.ET_AL_STRING); int index = citText.indexOf(etAlString); if (index >= 0) { italicizeOrBold(position, true, index, index + etAlString.length()); } } position.collapseToEnd(); } private void italicizeOrBold(XTextCursor position, boolean italicize, int start, int end) throws UnknownPropertyException, PropertyVetoException, IllegalArgumentException, WrappedTargetException { XTextRange range = position.getStart(); XTextCursor cursor = position.getText().createTextCursorByRange(range); cursor.goRight((short) start, false); cursor.goRight((short) (end - start), true); XPropertySet xcp = UnoRuntime.queryInterface(XPropertySet.class, cursor); if (italicize) { xcp.setPropertyValue("CharPosture", com.sun.star.awt.FontSlant.ITALIC); } else { xcp.setPropertyValue("CharWeight", com.sun.star.awt.FontWeight.BOLD); } } private void removeReferenceMark(String name) throws NoSuchElementException, WrappedTargetException { XNameAccess xReferenceMarks = getReferenceMarks(); if (xReferenceMarks.hasByName(name)) { Object referenceMark = xReferenceMarks.getByName(name); XTextContent bookmark = UnoRuntime.queryInterface(XTextContent.class, referenceMark); text.removeTextContent(bookmark); } } /** * Get the XTextRange corresponding to the named bookmark. * @param name The name of the bookmark to find. * @return The XTextRange for the bookmark. * @throws WrappedTargetException * @throws NoSuchElementException */ private XTextRange getBookmarkRange(String name) throws NoSuchElementException, WrappedTargetException { XNameAccess xNamedBookmarks = getBookmarks(); // retrieve bookmark by name if (!xNamedBookmarks.hasByName(name)) { return null; } Object foundBookmark = xNamedBookmarks.getByName(name); XTextContent xFoundBookmark = UnoRuntime.queryInterface(XTextContent.class, foundBookmark); return xFoundBookmark.getAnchor(); } private XNameAccess getBookmarks() { // query XBookmarksSupplier from document model and get bookmarks collection XBookmarksSupplier xBookmarksSupplier = UnoRuntime.queryInterface(XBookmarksSupplier.class, xCurrentComponent); XNameAccess xNamedBookmarks = xBookmarksSupplier.getBookmarks(); return xNamedBookmarks; } public void combineCiteMarkers(List<BibDatabase> databases, OOBibStyle style) throws IOException, WrappedTargetException, NoSuchElementException, IllegalArgumentException, UndefinedCharacterFormatException, UnknownPropertyException, PropertyVetoException, CreationException, BibEntryNotFoundException { XNameAccess nameAccess = getReferenceMarks(); // TODO: doesn't work for citations in footnotes/tables List<String> names = getSortedReferenceMarks(nameAccess); final XTextRangeCompare compare = UnoRuntime.queryInterface(XTextRangeCompare.class, text); int piv = 0; boolean madeModifications = false; while (piv < (names.size() - 1)) { XTextRange range1 = UnoRuntime.queryInterface(XTextContent.class, nameAccess.getByName(names.get(piv))) .getAnchor().getEnd(); XTextRange range2 = UnoRuntime.queryInterface(XTextContent.class, nameAccess.getByName(names.get(piv + 1))) .getAnchor().getStart(); if (range1.getText() != range2.getText()) { piv++; continue; } XTextCursor mxDocCursor = range1.getText().createTextCursorByRange(range1); mxDocCursor.goRight((short) 1, true); boolean couldExpand = true; while (couldExpand && (compare.compareRegionEnds(mxDocCursor, range2) > 0)) { couldExpand = mxDocCursor.goRight((short) 1, true); } String cursorText = mxDocCursor.getString(); // Check if the string contains no line breaks and only whitespace: if ((cursorText.indexOf('\n') == -1) && cursorText.trim().isEmpty()) { // If we are supposed to set character format for citations, test this before // making any changes. This way we can throw an exception before any reference // marks are removed, preventing damage to the user's document: if (style.isFormatCitations()) { XPropertySet xCursorProps = UnoRuntime.queryInterface(XPropertySet.class, mxDocCursor); String charStyle = style.getCitationCharacterFormat(); try { xCursorProps.setPropertyValue(CHAR_STYLE_NAME, charStyle); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException ex) { // Setting the character format failed, so we throw an exception that // will result in an error message for the user: throw new UndefinedCharacterFormatException(charStyle); } } List<String> keys = parseRefMarkName(names.get(piv)); keys.addAll(parseRefMarkName(names.get(piv + 1))); removeReferenceMark(names.get(piv)); removeReferenceMark(names.get(piv + 1)); List<BibEntry> entries = new ArrayList<>(); for (String key : keys) { for (BibDatabase database : databases) { Optional<BibEntry> entry = database.getEntryByKey(key); if (entry.isPresent()) { entries.add(entry.get()); break; } } } Collections.sort(entries, new FieldComparator(FieldName.YEAR)); String keyString = String.join(",", entries.stream().map(entry -> entry.getCiteKeyOptional().orElse("")) .collect(Collectors.toList())); // Insert bookmark: String bName = getUniqueReferenceMarkName(keyString, OOBibBase.AUTHORYEAR_PAR); insertReferenceMark(bName, "tmp", mxDocCursor, true, style); names.set(piv + 1, bName); madeModifications = true; } piv++; } if (madeModifications) { updateSortedReferenceMarks(); refreshCiteMarkers(databases, style); } } public static XTextDocument selectComponent(List<XTextDocument> list) throws UnknownPropertyException, WrappedTargetException, IndexOutOfBoundsException { String[] values = new String[list.size()]; int ii = 0; for (XTextDocument doc : list) { values[ii] = String.valueOf(OOUtil.getProperty(doc.getCurrentController().getFrame(), "Title")); ii++; } JList<String> sel = new JList<>(values); sel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); sel.setSelectedIndex(0); int ans = JOptionPane.showConfirmDialog(null, new JScrollPane(sel), Localization.lang("Select document"), JOptionPane.OK_CANCEL_OPTION); if (ans == JOptionPane.OK_OPTION) { return list.get(sel.getSelectedIndex()); } else { return null; } } private static class ComparableMark implements Comparable<ComparableMark> { private final String name; private final Point position; public ComparableMark(String name, Point position) { this.name = name; this.position = position; } @Override public int compareTo(ComparableMark other) { if (position.Y == other.position.Y) { return position.X - other.position.X; } else { return position.Y - other.position.Y; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof ComparableMark) { ComparableMark other = (ComparableMark) o; return (this.position.X == other.position.X) && (this.position.Y == other.position.Y) && Objects.equals(this.name, other.name); } return false; } public String getName() { return name; } @Override public int hashCode() { return Objects.hash(position, name); } } public BibDatabase generateDatabase(List<BibDatabase> databases) throws NoSuchElementException, WrappedTargetException { BibDatabase resultDatabase = new BibDatabase(); List<String> cited = findCitedKeys(); // For each cited key for (String key : cited) { // Loop through the available databases for (BibDatabase loopDatabase : databases) { Optional<BibEntry> entry = loopDatabase.getEntryByKey(key); // If entry found if (entry.isPresent()) { BibEntry clonedEntry = (BibEntry) entry.get().clone(); // Insert a copy of the entry resultDatabase.insertEntry(clonedEntry); // Check if the cloned entry has a crossref field clonedEntry.getField(FieldName.CROSSREF).ifPresent(crossref -> { // If the crossref entry is not already in the database if (!resultDatabase.getEntryByKey(crossref).isPresent()) { // Add it if it is in the current library loopDatabase.getEntryByKey(crossref).ifPresent(resultDatabase::insertEntry); } }); // Be happy with the first found BibEntry and move on to next key break; } } } return resultDatabase; } }