diff --git a/.gitignore b/.gitignore index fd187d3f735..25e0600022b 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,10 @@ local.properties .loadpath .recommenders +# config/emacs/lsp-java/Makefile will create a symlink named .settings +# Also exclude that, for consistency. +.settings + # External tool builders .externalToolBuilders/ diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..98e43fa75c2 --- /dev/null +++ b/Makefile @@ -0,0 +1,131 @@ +# -*- makefile -*- + +.PHONY: all assemble build build-notest clean check test run tmp-clean checkstyle + +## https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties +## GRADLE_USER_HOME=/tmp/jabref-home +## default: ~/.gradle + +# None of these helped in getting temporary files under /tmp/jabref +# +# TMPDIR=/tmp/jabref +# TEMP=/tmp/jabref +# GRADLE_OPTS=-Djava.io.tmpdir=/tmp/jabref +# JAVA_OPTS=-Djava.io.tmpdir=/tmp/jabref +# JVM_OPTS=-Djava.io.tmpdir=/tmp/jabref +# ./gradlew -Djava.io.tmpdir=/tmp/jabref + + +GRADLE = ./gradlew + + +all: build + +# https://devdocs.jabref.org/getting-into-the-code/guidelines-for-setting-up-a-local-workspace +# +# Generate additional source code: ./gradlew assemble +# +assemble: + ./gradlew assemble +# + +build: + make tmp-clean + make checkstyle + $(GRADLE) build + make tmp-clean +# + +build-notest: + make checkstyle + make tmp-clean + $(GRADLE) build -x test + make tmp-clean +# + +clean: + $(GRADLE) clean + make tmp-clean +# + +check: + make tmp-clean + make checkstyle + $(GRADLE) check + make tmp-clean +# + +test: + make tmp-clean + make checkstyle + $(GRADLE) test + make tmp-clean +# + +run: + make tmp-clean + make checkstyle + $(GRADLE) run + make tmp-clean +# + + +tmp-clean: + +old-tmp-clean: + rm -rf /tmp/journal[0-9][0-9][0-9][0-9][0-9][0-9]* \ + /tmp/junit[0-9][0-9][0-9][0-9][0-9][0-9]* \ + /tmp/gradle-worker-* \ + /tmp/LICENSE*.md \ + /tmp/INPROC-2016*.pdf \ + /tmp/[0-9][0-9][0-9][0-9][0-9][0-9][0-9]*.tmp +# + +checkstyle: + ./gradlew checkstyleMain 2>&1 | sed -e 's|[[]ant[:]checkstyle] [[]ERROR] ||g' + ./gradlew checkstyletest 2>&1 | sed -e 's|[[]ant[:]checkstyle] [[]ERROR] ||g' +# + + +PMD = pmd -f text -R ../tools/pmd/pmd-java-rules.xml -cache /tmp/pmd-jabref-cache + +pmd: +# $(PMD) -d src/main/java/org/jabref/model/openoffice +# $(PMD) -d src/main/java/org/jabref/logic/openoffice +# +# +# - Ignore problems in old files (OOBibBase, Bootstrap, DetectOpenOfficeInstallation). +# - Ignore UnusedPrivateMethod for @FXML initalize / addStyleFile +# +# - Ignore IdenticalCatchBranches : PMD seems to ignore that textually +# identical branches can lead to different code based on type of the +# exception. +# +# - Ignore some warnings in OOBibStyle.java for old stuff to be removed +# + $(PMD) -d src/main/java/org/jabref/logic/openoffice \ + | egrep -v 'src/main/java/org/jabref/gui/openoffice/OOBibBase.java' \ + | egrep -v 'src/main/java/org/jabref/gui/openoffice/Bootstrap.java' \ + | egrep -v 'src/main/java/org/jabref/gui/openoffice/DetectOpenOfficeInstallation.java' \ + | egrep -v 'src/main/java/org/jabref/gui/openoffice/ManageCitationsDialogView.java' \ + | egrep -v 'src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java' \ + | egrep -v 'src/main/java/org/jabref/logic/openoffice/OpenOfficePreferences.java' \ + | egrep -v 'src/main/java/org/jabref/logic/openoffice/style/OOPreFormatter.java' \ + | egrep -v "UnusedPrivateMethod.*initialize" \ + | egrep -v 'UnusedPrivateMethod:.*addStyleFile' \ + | egrep -v '\sIdenticalCatchBranches:\s' \ + | egrep -v '\sPreserveStackTrace:\s' \ + | egrep -v '\sUseUtilityClass:\s' \ + | egrep -v 'ShortVariable: Avoid variables with short names like (a|b|db|aa|bb)$$' \ + | egrep -v 'model/openoffice/style/CitedKeys.java:.*LooseCoupling:.*Avoid.*LinkedHashMap' \ + | egrep -v 'OOBibStyle.java.*names like (i1|al|to|j)$$' \ + | egrep -v 'OOBibStyle.java.*UseVarargs:' \ + | egrep -v 'OOBibStyleGetCitationMarker.java.*PrematureDeclaration:' \ + | egrep -v 'OOBibStyleGetCitationMarker.java.*SimplifiedTernary:' \ + | egrep -v 'OOBibStyleGetCitationMarker.java.*ShortVariable:.*like (j)$$' \ + | egrep -v 'OOBibStyleGetNumCitationMarker.java.*ShortVariable:.*like (na|nb)$$' \ + | egrep -v 'OOBibStyleGetNumCitationMarker.java.*(PrematureDeclaration|EmptyIfStmt):' \ + | egrep -v 'NamedRangeReferenceMark.java.*PrematureDeclaration:' \ + | egrep -v 'OOFormatBibliography.java.*(EmptyIfStmt|PrematureDeclaration):' + + diff --git a/build.gradle b/build.gradle index 3e5c3633dc1..22e91932522 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,12 @@ modularity.patchModule("test3", "sourcecode_2.12-0.1.4.jar") // See also https://github.com/java9-modularity/gradle-modules-plugin/issues/165 modularity.disableEffectiveArgumentsAdjustment() +tasks.withType(JavaCompile) { + options.compilerArgs += [ + // "-Xlint:deprecation", "-Xlint:unchecked", "-Xlint:removal" + ] +} + sourceSets { main { java { diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f66102da3a8..f1e8d4a7013 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -19,4 +19,10 @@ * [Readings on Coding](readings-on-coding/README.md) * [Readings on JavaFX](readings-on-coding/javafx.md) * [Useful development tooling](readings-on-coding/tools.md) - +* [The OpenOffice/LibreOffice panel](openoffice/README.md) + * [Overview](openoffice/overview.md) + * [Order of appearance of citation groups](openoffice/order-of-appearance.md) + * [Problems](openoffice/problems.md) + * [Code reorganization](openoffice/code-reorganization.md) + * [About `OOError`, `OOResult` and `OOVoidResult`](openoffice/ooresult-ooerror.md) + * [Alternatives to using OOResult and OOVoidResult in OOBibBase](openoffice/ooresult-alternatives.md) diff --git a/docs/openoffice/README.md b/docs/openoffice/README.md new file mode 100644 index 00000000000..5587cc8291e --- /dev/null +++ b/docs/openoffice/README.md @@ -0,0 +1 @@ +# OpenOffice/LibreOffice integration diff --git a/docs/openoffice/code-reorganization.md b/docs/openoffice/code-reorganization.md new file mode 100644 index 00000000000..35c3159a325 --- /dev/null +++ b/docs/openoffice/code-reorganization.md @@ -0,0 +1,188 @@ + +# Code reorganization + +Why + +- Separate backend +- Separate GUI code (dialogs) and logic +- Data is now organized around `Citation`, `CitationGroup` instead of arrays for citation group + fields, and arrays of arrays for citation fields. + Also take `citationKey` as the central data unit, this is what we start with: unresolved `citationKeys` + do not stop processing. Although we cannot sort them by author and year, we can still emit a marker + that acts as a placeholder and shows the user the problematic key. + +## Result + +### Layers + +![Layers](layers-v1.svg) + +### By directories + +- `model` + - `util` : general utilities + - (`OOPair`, `OOTuple3`) collect two or three objects without creating a new class + - `OOResult` : while an Optional.empty can comunicate failure, it cannot provide details. + `OOResult` allows an arbitrary error object to be provided in case of failure. + - `OOVoidResult` : for functions returning no result on success, only diagnostics on failure. + - `OOListUtil`: some utilities working on List + - `uno` : helpers for various tasks via UNO. + These are conceptually independent of JabRef code and logic. + - `ootext` : to separate decisions on the format of references and citation marks from + the actual insertion into the document, the earlier method + [OOUtil.insertOOFormattedTextAtCurrentLocation](https://github.com/JabRef/jabref/blob/475b2989ffa8ec61c3327c62ed8f694149f83220/src/main/java/org/jabref/logic/openoffice/OOUtil.java#L112) + was extended to handle new tags that describe actions earlier done in code. + - This became [OOTextIntoOO.write](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java#L149) + - `(change)` Now all output to the document goes through this, not only those from Layout. This allows the citation markers and `jstyle:Title` to use these tags. + - This allows some backward-compatible extensions to jstyle. + `(change)` [Added](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/style/OOBibStyle.java#L92) + some extra keywords, in `{prefix}_MARKUP_BEFORE`, `{prefix}_MARKUP_AFTER` pairs to allow bracketing some parts of citation marks with text and/or open/close tag pairs. + - [OOFormat](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/model/openoffice/ootext/OOFormat.java) + contains helpers to create the appropriate tags + - [OOText](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/model/openoffice/ootext/OOText.java) formalizes + the distinction from `String`. I did not change `String` to `OOText` in old code, (in particular in OOStyle). + - `rangesort` : ordering objects that have an `XTextRange`, optionally with an extra integer to break ties. + - `RangeSort.partitionAndSortRanges` : since `XTextRangeCompare` can only compare `XTextRange` values in + the same `XText`, we partition them accordingly and only sort within each partiion. + - `RangeSortable` (interface), `RangeSortEntry` (implements) : + When we replace `XTextRange` of citation marks in footnotes with the range of the footnote mark, + multiple citation marks may be mapped to the same location. To preserve the order between these, + `RangeSortable` allows this order to be indicated by returning appropriate indices from `getIndexInPosition` + - `RangeSortVisual` : sort in top-to-bottom left-to-right order. + Needs a functional `XTextViewCursor`. + Works on `RangeSortable` values. + - `FunctionalTextViewCursor` : helper to get a functional `XTextViewCursor` (cannot always) + - `RangeOverlapWithin` : check for overlaps within a set of `XTextRange` values. Probably O(n*log(n)). Used for all-to-all check of protected ranges. + - `RangeOverlapBetween` : check for overlaps between two sets of `XTextRange` values. Assumes one set is small. O(n*k). + Used for checking if the cursor is in a protected range. + - `backend` : interfaces to be provided by backends. + May change as new backends may need different APIs. + - `style` : data structures and interfaces used while going from ordered list of citation groups + to formatted citation markers and bibliography. Does not communicate with the document. Too long to fit here, starting a new section. + +## model/style + +At the core, + +- we have `Citation` values + - represented in the document by their `citationKey` + - each may have a `pageInfo` +- A citation group (`CitationGroup`) has + - a list of citations (`citationsInStorageOrder`) + - an identifier `CitationGroupId groupId` + - this allows to refer to the group + - also used to associate the group to its citation markers location (outside the style part, + in [Backend](https://github.com/antalk2/jabref/blob/fed0952cbdaf7a76bcb09b3db5ac48f34f5ca388/src/main/java/org/jabref/logic/openoffice/backend/Backend52.java#L46)) + - `OODataModel dataModel` is here, in order to handle old (Jabref5.2) structure where pageInfo belonged to + CitationGroup not Citation + - `referenceMarkNameForLinking` is optional: can be used to crosslink to the citation marker + from the bibliography. +- `CitationGroups` represents the collection of citation groups. +Processing starts with creating a `CitationGroups` instance from the data stored in the document. + +- `CitedKey` represents a cited source, with ordered backreferences (using `CitationPath`) to the correponding +citations. + +- `CitedKeys` is just an order-preserving collection of `CitedKeys` that also supports lookup by +`citationKey`. While producing citation markers, we also create a corresponding `CitedKeys` +instance, and store it in `CitationGroups.bibliography`. This is already sorted, its entries have +`uniqueLetter` or `number` assigned, but not converted to markup yet. + +Common processing steps: + +- We need `globalOrder` for the citation groups (provided externally) +`CitationGroups.setGlobalOrder()` +- We need to look up each citationKey in the bibliography databases: + - `CitationGroups.lookupCitations` collects the cited keys, + looks up each, then distributes the results to the citations. + Uses a temporary `CitedKeys` instance, based on unsorted citations and citation groups. +- `CitationGroups.imposeLocalOrder` fills `localOrder` in each `CitationGroup` + +- Now we have order of appearance for the citations (`globalOrder` and `localOrder`). + We can create a `CitedKeys` instance (`bibliography`) according to this order. + +- For citations numbered in order of first appearance we number the sources and distribute the numbers +to the corresponding citations. +- For citations numbered in order of bibliography, we sort the bibliography, number, distribute. + +- For author-year citations we have to decide on the letters `uniqueLetter` used to distinguish +sources. This needs order of first appearance of the sources and recognizing clashing citation markers. +This is done in logic, in [`OOProcessAuthorYearMarkers.createUniqueLetters()`](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java#L49) +- We also mark first appearance of each source ([`setIsFirstAppearanceOfSourceInCitations`](https://github.com/antalk2/jabref/blob/fed0952cbdaf7a76bcb09b3db5ac48f34f5ca388/src/main/java/org/jabref/logic/openoffice/style/OOProcessAuthorYearMarkers.java#L146)) + +The entry point for this processing is: [`OOProcess.produceCitationMarkers`](https://github.com/antalk2/jabref/blob/fed0952cbdaf7a76bcb09b3db5ac48f34f5ca388/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java#L69). +It fills + +- each `CitationGroup.citationMarker` +- `CitationGroups.bibliography` + - From bibliography `OOFormatBibliography.formatBibliography()` creates an `OOText` + ready to be written to the document. + + +## logic/style + +- `StyleLoader` : not changed (knows about default styles) Used by GUI +- `OOPreFormatter` : LaTeX code to unicode and OOText tags. (not changed) +- `OOBibStyle` : is mostly concerned by loading/parsing jstyle files and presenting its pieces +to the rest. Originally it also contains code to format numeric and author-year citation markers. + - Details of their new implementations are in + [`OOBibStyleGetNumCitationMarker`](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetNumCitationMarker.java) and + [`OOBibStyleGetCitationMarker`](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/logic/openoffice/style/OOBibStyleGetCitationMarker.java) + - The new implementations + - support pageInfo for each citation + - support unresolved citations + - instead of `List` and (`List` plus arrays and database) they expect + more self-contained entries `List`, `List`. + - We have distinct methods for `getNormalizedCitationMarker(CitationMarkerNormEntry)` and + `getNumCitationMarkerForBibliography(CitationMarkerNumericBibEntry)`. + - The corresponding interfaces in model/style: + - `CitationMarkerNumericEntry` + - `CitationMarkerEntry` + - `CitationMarkerNumericBibEntry` + - `CitationMarkerNormEntry` + describe their expected input entries. +- [`OOProcess.produceCitationMarkers`](https://github.com/antalk2/jabref/blob/fed0952cbdaf7a76bcb09b3db5ac48f34f5ca388/src/main/java/org/jabref/logic/openoffice/style/OOProcess.java#L69) +is the main entry point for style application. Calls to specific implementations +in `OOProcessCitationKeyMarkers`, `OOProcessNumericMarkers` and `OOProcessAuthorYearMarkers` +according to jstyle flags. + +## logic/backend + +Details of encoding and retrieving data stored in a document as well as +the citation maker locations. Also contains dataModel-dependent code +(which could probably be moved out once the datamodel is settled). + +Creating and finding the bibliography (providing a cursor to write at) should be here too. +These are currently in `UpdateBibliography` + +## logic/frontend + +- `OOFrontend` : has a `Backend` and `CitationGroups` + - Its constructor creates a backend, reads data from the document and creates a CitationGroups instance. + - provides functionality that requires both access to the document and the CitationGroups instance +- `RangeForOverlapCheck` used in `OOFrontend` +- `UpdateBibliography` : Create, find and update the bibliography in the document using output from + `produceCitationMarkers()` +- `UpdateCitationMarkers` create `CitationGroup`, update citation markers using output from + `produceCitationMarkers()` + + +## logic/action + +GUI-independent part of implementations of GUI actions. + +## gui + +- `OOError` : common error messages and dialog titles + - adds `title` to `Jabrefexception` + - converts from some common exception types using type-specific message + - contains some dialog messages that do not correspond to exceptions + +- `OOBibBase2` : most activity was moved out from here to parts discussed above. + - connecting / selecting a document moved to `OOBibBaseConnect` + - the rest connects higher parts of the GUI to actions in logic + - does argument and precondition checking + - catches all exceptions + - shows error and warning dialogs + - adds `enterUndoContext`, `leaveUndoContext` around action code + diff --git a/docs/openoffice/layers-v1.svg b/docs/openoffice/layers-v1.svg new file mode 100644 index 00000000000..f9b8239d504 --- /dev/null +++ b/docs/openoffice/layers-v1.svg @@ -0,0 +1,915 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + document content (UNO) + + frontend + + actions + + OOBibBase2 + + backend + + style + + OOTextIntoOO + + rangesort + data in doc, ranges + order ranges + fill ranges + markup text + XTextDocument + Backend, CitationGroups + + + + GUI: + BibEntry, BibDatabase, OOBibStyle + provides input in terms of these types + + provides connection to doc + Cite, Update, Merge, Separate, Manage, Export + Connect + Load Style + Create OOFrontend instance + Catch exceptions, Undo + Forward requests to actions + Check preconditions + + locations + citation keys + pageInfo + citation type + + + lookup, localOrder, number, + uniqueLetter, sort bibliography, + format citationMarkers, + format bibliography + + + or visually + within XText + + checkRangeOverlaps, checkRangeOverlapsWithCursor + connects the parts below + getVisuallySortedCitationGroups, imposeGlobalOrder + UpdateCitationMarkers, UpdateBibliography + lock screen refresh + GUI-independent part of actions + + + diff --git a/docs/openoffice/ooresult-alternatives.md b/docs/openoffice/ooresult-alternatives.md new file mode 100644 index 00000000000..27d5dbb7e11 --- /dev/null +++ b/docs/openoffice/ooresult-alternatives.md @@ -0,0 +1,505 @@ + +# Alternatives to `OOResult` during precondition checking in `OOBibBase` + +(Talk about ADRs prompted me to think about alternatives to what I used.) + +Situation: + +- some tests return no data, only report problems +- we may need to get some resources that might not be available + (for example: connection to a document, a functional textview cursor) +- some test depend on these resources + + +One strategy could be to use a single try-catch around the whole body, +then showing a message based on the type of exceptions thrown. + +## [base case] + +```java +try { + A a = f(); + B b = g(a); + realAction(a,b); +} catch (FirstExceptionType ex) { + showDialog( title, messageForFirstExceptionType(ex) ); +} catch (SecondExceptionType ex) { + showDialog( title, messageForSecondExceptionType(ex) ); +} catch (Exception ex) { + showDialog( title, messageForOtherExceptions(ex) ); +} +``` + +This our base case. + +It is not clear from the code, nor within the catch branches (unless +we start looking into stack traces) which call (`f()`, `g(a)` or +`realAction(a,b)`) resulted in the exception. This limits the +specificity of the message and makes it hard to think about the "why" +can we get this exception here? + +## Catch around each call? + +A more detailed strategy would be to try-catch around each call. +In case we need a result from the call, this means either increasingly indented +code (try-in-try). + +```java +try { + A a = f(); + try { + B b = g(a); + try { + realAction(ab); + } catch (...){ + showDialog(); + } + } catch (G ex) { + showDialog(title, ex); // title describes which GUI action we are in + } +} catch (F ex) { + // could an F be thrown in g? + showDialog( title, ex ); +} +``` + +or (declare and fill later) + +```java +A a = null; +try { + a = f(); +} catch (F ex) { + showDialog(title, ex); + return; +} +B b = null; +try { + b = g(a); +} catch (G ex) { + showDialog(title, ex); + return; +} +try { + realAction(ab); +} catch (...){ + showDialog(); +} +``` + +In either case, the code becomes littered with exception handling code. + +## Catch in wrappers? + +We might push the try-catch into its own function. +If the wrapper is called multiple times, this may reduce duplication of the +catch-and-assign-message part. + +We can show an error dialog here: `title` carries some information +from the caller, the exeption caught brings some from below. + +We still need to notify the action handler (the caller) about +failure. Since we have shown the dialog, we do not need to provide a +message. + +### Notify caller with `Optional` result + +With `Optional` we get something like this: + +#### [dialog in wrap, return Optional] + +```java +Optional wrap_f(String title) { + try { + return Optional.of(f()); + } catch (F ex) { + showDialog(title, ex); + return Optional.empty(); + } +} + +Optional wrap_g(String title, A a) { + try { + return Optional.of(g(a)); + } catch (G ex) { + showDialog(title, ex); + return Optional.empty(); + } +} +``` + +and use it like this: + +```java +Optional a = wrap_f(title); +if (a.isEmpty()) { return; } + +Optional b = wrap_g(title, a.get()); +if (b.isEmpty()) { return; } + +try { + realAction(a.get(), b.get()); +} catch (...) { +} +``` + +This looks fairly regular. + +If `g` did not need `a`, we could simplify to + +```java +Optional a = wrap_f(title); +Optional b = wrap_g(title); +if (a.isEmpty() || b.isEmpty()) { return; } + +try { + realAction(a.get(), b.get()); +} catch (...) { +} +``` + +### Notify caller with `Result` result + +With `Result` we get something like this: + +#### [dialog in wrap, return OOResult] + +```java +OOResult wrap_f() { + try { + return OOResult.ok(f()); + } catch (F ex) { + return OOResult.error(OOError.from(ex)); + } catch (F2 ex) { + String message = "..."; + return OOResult.error(new OOError(message, ex)); // [1] + } +} +// [1] : this OOError constructor (explicit message but no title) is missing + +Optional wrap_g(A a) { + try { + return OOResult.ok(g(a)); + } catch (G ex) { + return OOResult.error(OOError.from(ex)); + } +} +``` + +and use it like this: + +```java +OOResult a = wrap_f(); +if (testDialog(title, a)) { // [1] + return; +} + +// [1] needs boolean testDialog(String title, OOResultLike... a); +// where OOResultLike is an interface with `OOVoidResult asVoidResult()` +// and is implemented by OOResult and OOVoidResult + +OOResult b = wrap_g(a.get()); +if (testDialog(title, b)) { return; } // (checkstyle makes this 3 lines) + +try { + realAction(a.get(), b.get()); +} catch (...) { +} +``` + +If `g` did not need `a`, we could simplify to + +```java +Optional a = wrap_f(); +Optional b = wrap_g(); +if (testDialog(title, a, b)) { // a single dialog can show both messages + return; +} + +try { + realAction(a.get(), b.get()); +} catch (...) { +} +``` + +### Notify caller by throwing an exception + +Or we can throw an exception to notify the caller. + +To simplify code in the caller, I assume we are using an exception +type not used elsewhere, but shared by all precondition checks. + +#### [dialog in wrap, PreconditionException] + +```java +A wrap_f(String title) throws PreconditionException { + try { + return f(); + } catch (F ex) { + showDialog(title, ex) + throw new PreconditionException(); + } +} + +B wrap_g(String title, A a) throws PreconditionException { + try { + return g(a); + } catch (G ex) { + showDialog(title, ex); + throw new PreconditionException(); + } +} +``` + +use + +```java +try { + A a = wrap_f(title); + B b = wrap_g(title, a); + try { + realAction(a, b); + } catch (...) { + showDialog(...) + } +} catch( PreconditionException ) { + // Only precondition checks get us here. + return; +} +``` + +or (since PreconditionException is not thrown from realAction) + +```java +try { + A a = wrap_f(title); + B b = wrap_g(title, a); + realAction(a, b); +} catch (...) { + // Only realAction gets us here + showDialog(...) +} catch( PreconditionException ) { + // Only precondition checks get us here. + return; +} +``` + +or (separate try-catch for preconditions and realAction) + +```java +A a = null; +B b = null; +try { + a = wrap_f(title); + b = wrap_g(title, a); +} catch( PreconditionException ) { + return; +} +try { + realAction(a, b); +} catch (...) { +} +``` + +or to reduce passing around the title part: + +#### [PreconditionException, dialog in catch] + +```java +A wrap_f() throws PreconditionException { + try { + return f(); + } catch (F ex) { + throw new PreconditionException(message, ex); + } +} + +B wrap_g(A a) throws PreconditionException { + try { + return g(a); + } catch (G ex) { + throw new PreconditionException(message, ex); + } +} +``` + +use + +```java +try { + A a = wrap_f(); + B b = wrap_g(a); + try { + realAction(a, b); + } catch (...) { + showDialog(...); + } +} catch(PreconditionException ex) { + showDialog(title, ex.message ); + return; +} +``` + +or + +```java +try { + A a = wrap_f(); + B b = wrap_g(a); + realAction(a, b); +} catch (...) { + showDialog(...); +} catch(PreconditionException ex) { + showDialog(title, ex.message ); + return; +} +``` + + +## Push associating the message further down + +As [the developers guide](https://jabref.readthedocs.io/en/latest/getting-into-the-code/code-howtos/#throwing-and-catching-exceptions) +suggest, we could "Catch and wrap all API exceptions" and rethrow them +as a `JabRefException` or some exception derived from it. In this case the try-catch part goes even further down, and +in principle we could just + +```java +try { + A a = f(); + B b = g(a); + realAction(a, b); +} catch(JabRefException ex) { + showDialog(title, ex.message ); + return; +} +``` + +Constraints: + +- conversion to `JabRefException` cannot be done in `model` (since JabRefException is in `logic`) +- `JabRefException` expects a localized message. Or we need to remember +which `JabRefException` instances are localized and which need to be caught +for localizing the message. +- At the bottom we usually have very little information on higher level +contexts: at a failure like `NoSuchProperty` we cannot tell which set +of properties did we look in and why. +For messages originating too deeply, we might want to override or extend the message anyway. +- for each exeption we might want to handle programmatically, we need a variant based +on `JabRefException` + +So we might end up: + +```java +try { + A a = f(); + B b = g(a); + realAction(a, b); +} catch(FDerivedFromJabRefException ex) { + showDialog(title, messageForF ); +} catch(GDerivedFromJabRefException ex) { + showDialog(title, messageForG ); +} catch(JabRefException ex) { + showDialog(title, ex.message ); +} catch(Exception ex) { // [1] + showDialog(title, ex.message, ex ); + // [1] does "never catch Exception or Throwable" apply at this point? + // Probably should not: we are promising not to throw. +} +``` + +which looks very similar to the original version. + +This again loses the information: can `GDerivedFromJabRefException` +come from `realAction` or `f` or not? This is because we have pushed +down the last catch/throw indefinitely (eliminating `wrap_f`) into a +depth, where we cannot necessarily assign an appropriate message. + +To a lesser extent this also happens in `wrap_f`: it only knows about +the action that called it what we provide (`title` or nothing). It knows +the precondition it checks: probably an optimal location to assign a message. + +**Summary**: going from top to bottom, we move to increasingly more local context, +our knowledge shifts towards the "in which part of the code did we have a problem" +and away from the high level ("which action"). + +One natural point to meet information from these to levels +is the top level of action handlers. For precondition checking code +a wrapper around code elsewhere may be considered. +Using such wrappers may reduce duplication if called in multiple actions. + +We still have to signal failure to the action handler: the options considered +above were using an `Optional` and throwing an exception with the appropriate message. + +The more promising variants were + +- **[dialog in wrap, return Optional]** + `Optional wrap_f(String title)` (showDialog inside) + - pro: explicit return in caller + - con: explicit return in caller (boilerplate) + - con: passing in the title is repeated + - would be 'pro' if we wanted title to vary within an action + +- **[PreconditionException, dialog in catch]** + `A wrap_f() throws PreconditionException` + (with `showDialog` under `catch(PreconditionException ex)`) + - con: hidden control flow + - pro: no repeated `if(){return}` boilerplate + - pro: title used only once + +### [using OOResult] + +```java +final String title = "Could not insert citation"; + +OOResult odoc = getXTextDocument(); +if (testDialog(title, + odoc, + styleIsRequired(style), + selectedBibEntryIsRequired(entries, OOError::noEntriesSelectedForCitation))) { + return; +} +XTextDocument doc = odoc.get(); + +OOResult ofr = getFrontend(doc); +if (testDialog(title, ofr)) { + return; +} +OOFrontend fr = ofr.get(); + +OOResult cursor = getUserCursorForTextInsertion(doc); +if (testDialog(title, cursor)) { + return; +} +... +``` + + +### [using PreconditionException, dialog in catch] + +```java +final String title = "Could not insert citation"; + +try { + XTextDocument doc = getXTextDocument(); + styleIsRequired(style); + selectedBibEntryIsRequired(entries, OOError::noEntriesSelectedForCitation); + OOFrontend fr = getFrontend(doc); + XTextCursor cursor = getUserCursorForTextInsertion(doc); + ... +} catch (PreconditionException ex) { + showDialog(title, ex); +} catch (...) { +} +``` + +I would suggest using the latter, + +- probably using `OOError` for `PreconditionException` + - In this case `OOError` being in `gui` becomes an asset: we can be sure + code in `logic` cannot throw it. +- We lose the capability to collect mmessages in a single dialog (we + stop processing at the first problem). +- The division between precondition checking (only throws + PreconditionException) and `realAction`becomes invisible in the + action code. + diff --git a/docs/openoffice/ooresult-ooerror.md b/docs/openoffice/ooresult-ooerror.md new file mode 100644 index 00000000000..867ee55d009 --- /dev/null +++ b/docs/openoffice/ooresult-ooerror.md @@ -0,0 +1,232 @@ +# About `OOError`, `OOResult` and `OOVoidResult` + +## Context + +### Relieve GUI panel code + +On the question of where should we catch exceptions in relation to GUI +code it was suggested (Jonatan Asketorp +[here](https://github.com/koppor/jabref/pull/496#discussion_r629695493), "most +of them (all?) should be handled latest in the ViewModel.") that +catching them early could help simplifying the higher levels. + +### Same messages in different contexts + +Some types of exceptions are caught in *different GUI actions*, often +resulting in basically the same error dialog, possibly only differing in +the indicated context (which GUI action). + +Problems found during *precondition checking* (for example: do we have +a connection to a document) and error conditions (for example: lost +connection to a document during an action) can overlap. + +### OOBibBase as a precondition and exception handling layer + +Since most of the code originally in `OOBibBase` was moved to `logic` and +almost all GUI actions go through `OOBibBase`, it seemed a good location +to collect precondition checking and exception handling code. + +Note: some of the precondition checking still needs to stay in `OpenOfficePanel`: +for example to provide a list of selected `BibEntry` instances, it needs to go through some steps +from `frame.getCurrentLibraryTab()` to `(!entries.isEmpty() && checkThatEntriesHaveKeys(entries))` + +To avoid `OOBibBase` depending on the higher level `OpenOfficePanel` +message texts needed in `OOBibBase` were moved from `OpenOfficePanel` to `OOError`. +(Others stayed, but could be moved if that seems worthwile) + +## OOError + +- `OOError` is a collection of data used in error dialogs. + - It is a `JabRefException` with an added field: `localizedTitle` + - It can store: a dialog title, a localized message (optionally a non-localized message as well) and a `Throwable` + - I used it in `OOBibBase` as a unified format for errors to be shown in an error dialog. + - Static constructors in `OOError` provide uniform translation from some exception types to + `OOError` with the corresponding localized messages: + `public static OOError from(SomeException ex)` + There is also `public static OOError fromMisc(Exception ex)` for exception types + not handled individually. (It has a different name, to avoid ambiguity) + + - Another set of contructors provide messages for some preconditions. + For example `public static OOError noDataBaseIsOpenForCiting()` + +Some questions: + +- Should we use static data instead of static methods for the precondition-related messages? + - pro: why create a new instance for each error? + - con: `OOError.setTitle()` currently just sets `this.localizedTitle` and returns `this`. + For static instances this would modify a shared resource unless we create a new copy in `setTitle`. + However `setTitle` can be called repeatedly on the same object: as we bubble up, we can be more specific + about the context. + +- Should we remove title from `OOError`? + - pro: we almost always override its original value + - con: may need to duplicate the title in different files (preconditions for an action in OpenOfficePanel + and in OOBibBase) + +- Should we include `OOError.showErrorDialog` ? + - pro: since it was intended *for* error dialogs, it is nice to provide this. + - con: the reference to `DialogService` forces it to `gui`, thus it cannot be used in `logic` or `model` + +- Should we use `JabRefException` as base? + - pro: `JabRefException` is mentioned as the standard form of errors in the developers guide. + [All Exceptions we throw should be or extend JabRefException](https://jabref.readthedocs.io/en/latest/getting-into-the-code/code-howtos/#throwing-and-catching-exceptions) + - against: `JabRefException` is in `logic` cannot be used in model. + (Could this be resolved by moving `JabRefException` to `model`?) + +## OOResult + +During precondition checking + +1. some tests return no data, only report problems +2. we may need to get some resources that might not be available + (for example: connection to a document, a functional textview cursor) +3. some test depend on these resources + +While concentrating on these and on "do not throw exceptions here" +... using a [Result type](https://en.wikipedia.org/wiki/Result_type) as a return +value from precondition checking code seemed a good fit: + +- Instead of throwing an exception, we can return some data describing the problem. +- Conceptually it is a data structure that either holds the result (of a computation) or and error value. +- It can be considered as an extended `Optional`, that can provide details on "why empty"? +- It can be considered as an alternative to throwing an exception: we return an `error` instead. +- Methods throwing checked exceptions cannot be used with for example `List.map`. + Methods returning a Result could. + +- `Result` shares the problem (with any other solutions) that in a +function several types of errors may occur, but we can only return a +single error type. Java solves this using checked exceptions being all +descendants of Exception. (Also adds try/catch/catch to select cases +based on the exceptions type, and some checking against forgotten +cases of checked exception types) + +In `OOBibBase` I used `OOError` as the unified error type: it can +store error messages and wrap exceptions. It contains everything we need +for an error dialog. On the other hand it does not support programmatic +dissection. + +### Implementation + +Unlike `Optional` and `List`, `Result` (in the sense used here) did not get into +java standard libraries. There are some implementations of this idea for java on the net: + +- [bgerstle/result-java](https://github.com/bgerstle/result-java/) +- [MrKloan/result-type](https://github.com/MrKloan/result-type) +- [david-bakin](https://gist.github.com/david-bakin/35d55daeeaee1eb71cea) +- [vavr-try](https://www.baeldung.com/vavr-try) + +Generics allow an implementation built around + +```java +class OOResult { + private final Optional result; + private final Optional error; +} +``` + +with an assumption that at any time exactly one of `result` and `error` is present. + +> `class X { boolean isOK; Object data; }` expresses this assumption more directly, +> (but omits the relation between the type parameters `` and the type in `data`) + +- Since `OOResult` encodes the state `isOK` in `result.isPresent()` (and equivalently in `errror.isEmpty()`), + we cannot allow construction of instances where both values are `isEmpty`. + In particular, `OOResult.ok(null)` and `OOResult.error(null)` are not + allowed: it would make the state `isOK` ambiguous. + It would also break the similarity to `Optional` to allow both `isEmpty` and `isOK` to be true. + +- Not allowing null, has a consequence on `OOResult` + According to + [baeldung.com/java-void-type](https://www.baeldung.com/java-void-type), + the only possible value for `Void` is `null` which we excluded. + + `OOResult.ok(null)` would look strange: in this case we need + `ok()` without arguments. + +To solve this problem, I introduced + +```java +class OOVoidResult { + private final Optional error; + ... +} +``` + +with methods on the error side similar to those in `OOError`, and +`OOVoidResult.ok()` to construct the success case with no data. + +### The relation between `Optional` and `OOVoidResult` + +- Both `Optional` and `OOVoidResult` can store 0 or 1 values, + in this respect they are equivalent + + - Actually, `OOVoidResult` is just a wrapper around an `Optional` + +- In terms of communication to human readers when used, their + connotation in respect to success and failure is the opposite: + + - `Optional.empty()` normally suggests failure, `OOVoidResult.ok()` mean success. + - `Optional.of(something)` probably means success, `OOVoidResult.error(something)` indicates failure. + - `OOVoidResult` is "the other half" (the failure branch) of `OOResult` + + - its content is accessed through `getError`, `mapError`, `ifError`, not `get`, `map`, `ifPresent` + +`OOVoidResult` allows + +- a clear distinction between success and failure when + calls to "get" something that might not be available (`Optional`) and + calls to precondition checking where we can only get reasons for failure + (`OOVoidResult`) + appear together. + Using `Optional` for both is possible, but is more error-prone. + +- it also allows using uniform verbs (`isError`, `getError`, + `ifError`, return `OO{Void}Result.error`) for "we have a problem" + when + + - checking preconditions (`OOVoidResult`) is mixed with + - "I need an X" orelse "we have a problem" (`OOResult`) + +- at a functions head: + + - `OOVoidResult function()` says: no result, but may get an error message + - `Optional function()` says: a `String` result or nothing. + +**Summary**: technically could use `Optional` for both situation, but it would be less precise, +leaving more room for confusion and bugs. `OOVoidResult` forces use of `getError` instead of `get`, +and `isError` or `isOk` instead of `isPresent`or `isEmpty`. + +## What does OOResult buy us? + + +The promise of `Result` is that we can avoid throwing exceptions and +return errors instead. This allows the caller to handle these latter +as data, for example may summarize / collect them for example into a single +message dialog. + +Handling the result needs some code in the caller. If we only needed checks that +return only errors (not results), the code could look like this (with possibly more tests listed): + +```java +OOResult odoc = getXTextDocument(); +if (testDialog(title, + odoc, + styleIsRequired(style), + selectedBibEntryIsRequired(entries, OOError::noEntriesSelectedForCitation))) { + return; +} +``` + +with a reasonably small footstep. + +Dependencies of tests on earlier results complicates this: now we repeat the + +```java +if (testDialog(title, + ...)) { + return; +} +``` + +part several times. + diff --git a/docs/openoffice/order-of-appearance.md b/docs/openoffice/order-of-appearance.md new file mode 100644 index 00000000000..e43009b9f93 --- /dev/null +++ b/docs/openoffice/order-of-appearance.md @@ -0,0 +1,118 @@ + +# Order of appearance of citation groups (`globalOrder`) + +The order of appearance of citations is decided on +two levels: + +1. their order within each citation group (`localOrder`), and +2. the order of the citation +groups that appear as citation markers in the text (`globalOrder`). + +This page is about the latter: how to decide the order of appearance (numbering sequence) of a set +of citation markers? + +## Conceptually + +In a continuous text it is easy: take the textual order of citation markers. + +In the presence of figures, tables, footnotes/endnotes possibly far from the location they are +referred to in the text or wrapped around with text it becomes less obvious what is the correct +order. + +Examples: + +- References in footnotes: are they *after* the page content, or number them as if they appeared at + the footnote mark? (JabRef does the latter) +- A figure with references in its caption. Text may flow on either or both sides. + Where should we insert these in the sequence? +- In a two-column layout, a text frame or figure mostly, but not fully in the second column: shall + we consider it part of the second column? + + +## Technically + + +In LibreOffice, a document has a main text that supports the +[XText](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XText.html) +interface. +This allows several types of +[XTextContent](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextContent.html) +to be inserted. + +- Some of these allow text inside with further insertions. + +### Anchors + +- Many, but not all XTextContent types support getting a "technical" insertion point or text range + through [getAnchor](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextContent.html#ae82a8b42f6b2578549b68b4483a877d3). +- In Libreoffice positioning both a frame and its anchor seems hard: moving the frame tends to also + move the anchor. +- Consequence: producing an order of appearance for the citation groups based solely on `getAnchor` + calls may be impossible. + + - Allowing or requiring the user to insert "logical anchors" for frames and other "floating" parts + might help to alleviate these problems. + +### Sorting within a `Text` + +The text ranges occupied by the citation markers support the +[XTextRange](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextRange.html) +interface. + +- These provide access to the XText they are contained in. +- The [Text](https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1text_1_1Text.html) service +may support (optional) the [XTextRangeCompare](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextRangeCompare.html) +interface, that allows two XTextRange values to be compared if both belong to this `Text` + +### Visual ordering + +- The cursor used by the user is available as an + [XTextViewCursor](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextViewCursor.html) +- If we can get it and can set its position in the document to each XTextRange to be sorted, and ask its + [getPosition](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextViewCursor.html#a9b2bafd342ef75b5d504a9313dbb1389) + to provide coordinates "relative to the top left position of the first page of the document.", + then we can sort by these coordinates in top-to-bottom left-to-right order. +- Note: in some cases, for example when the cursor is in a comment (as in + `Libreoffice:[menu:Insert]/[Comment]`), the XTextViewCursor is not available (I know of no way to + get it). +- In some other cases, for example when an image is selected, the XTextViewCursor we normally receive is not 'functional': +we cannot position it for getting coordinates for the citation marks. +The [FunctionalTextViewCursor](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/model/openoffice/rangesort/FunctionalTextViewCursor.java) +class can solve this case by accessing and manipulating the cursor through [XSelectionSupplier](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1view_1_1XSelectionSupplier.html) + +Consequences of getting these visual coordinates and using them to order the citation markers + +- allows uniform handling of the markers. Works in footnotes, tables, frames (apparently anywhere) +- requires moving the user visible cursor to each position and with [screen + refresh](https://github.com/antalk2/jabref/blob/improve-reversibility-rebased-03/src/main/java/org/jabref/model/openoffice/uno/UnoScreenRefresh.java) + enabled. + `(problem)` This results in some user-visible flashing and scrolling around in the document view. +- The expression "relative to the top left position of the first page of the document" is + understood literally, "as on the screen". + `(problem)` Showing pages side by side or using a two-column layout + will result in markers in the top half of the second column or page to be sorted before those on the bottom + of the first column of the first page. + + +## JabRef + +Jabref uses the following steps for sorting sorting citation markers (providing `globalOrder`): + +1. the textranges of citation marks in footnotes are replaced by the textranges of the footnote + marks. +2. get the positions (coordinates) of these marks +3. sort in top-to-botton left-to-right order + + +`(problem)` In JabRef5.2 the positions of citation marks within the same footnote become +indistinguishable, thus their order after sorting may differ from their order in the footnote text. +This caused problems for + +1. numbering order + `(solved)` by keeping track of the order-in-footnote of citation markers during sorting using + [getIndexInPosition](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortable.java#L21)) +2. `click:Merge`: It examines *consecutive* pairs of citation groups if they can be merged. Wrong +order may result in not discovering some mergeable pairs or attempting to merge in wrong order. +`(solved)` by not using visual order, only XTextRangeCompare-based order within each XText +[here](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/action/EditMerge.java#L325)) + diff --git a/docs/openoffice/overview.md b/docs/openoffice/overview.md new file mode 100644 index 00000000000..f3a6ed251f6 --- /dev/null +++ b/docs/openoffice/overview.md @@ -0,0 +1,297 @@ + +# Overview + +This is a partial overview of the OpenOffice/LibreOffice panel and the +code behind. + +- To access the panel: `JabRef:/[menu:View]/[OpenOffice/LibreOffice]` +- The user documentation is at +[https://docs.jabref.org/cite/openofficeintegration](https://docs.jabref.org/cite/openofficeintegration) + + +I am going to refer to OpenOffice Writer and LibreOffice Writer as +LibreOffice or LO: their UNO APIs are still mostly identical, but I +only tested with LibreOffice and differences do exist. + +## Subject + +- What is stored in a document, how. +- Generating citation markers and bibliography + - (excluding the bibliography entries, which is delegated to the layout module) + + +## The purpose of the panel + +- Allow the user to insert **citations** in a LibreOffice writer document. +- Automatically format these according to some prescribed style as **citation markers**. +- Generate a **bibliography**, also formatted according to the style. + - The bibliography consists of a title (e.g. "References") and a sorted list + of formatted bibliography entries, possibly prefixed with a marker (e.g. "[1]") +- It also allows some related activities: connect to a document, select a style, group ("Merge") the + citations for nicer output, ungroup ("Separate") them to move or delete them individually, + edit ("Manage") their page-info parts, and collect the database entries of cited sources + to a new database. + +## Citation types + +Citations (actually citation groups, see below) have three types +depending on how the citation marker is intended to appear in the +text: + +- **Parenthesized**: "(Smith, 2000)" +- **In-text**: "Smith (2000)" +- **Invisible**: no visible citation mark. + - An invisible citation mark lets the user to use any form for the citation + by taking control (and responsibility) back from the style. + - Like the other two citation types, they have a location in the document. + - In the bibliography these behave as the other two citation types. + - In LibreOffice (`LibreOffice:[Ctrl-F8]` or`LibreOffice:[menu:View]/[Field Shadings]`) + shows reference marks with gray background. Invisible citation marks appear as a thin gray rectangle. + +- These citation types correspond to `\citep{Smith2000}`, + `\citet{Smith2000}` in + [natbib](http://tug.ctan.org/macros/latex/contrib/natbib/natnotes.pdf) + and `\nocite{Smith2000}`. I will use `\citep`, `\citet` and `\citen` in "LaTeX pseudocode" below. + +## PageInfo + +The citations can be augmented with a string detailing which part +of a document is cited, for example "page 11" or "chapter 2". + +Sample citation markers (with LaTeX pseudocode): + +- `\citep[page 11]{Smith2000}` "(Smith, 2000; page 11)" +- `\citet[page 11]{Smith2000}` "Smith (2000; page 11)" +- `\citen[page 11]{Smith2000}` "" + +- This string is referred to as **`pageInfo`** in the code. +- In the GUI the labels "Cite special", "Extra information + (e.g. page number)" are used. + +## Citation groups + +Citations can be grouped. + +A group of parenthesized citations share the parentheses around, like this: + "(Smith, 2000; Jones 2001)". + +- Examples with pseudocode: + - `\citep{Smith2000,Jones2001}` "(Smith, 2000; Jones 2001)" + - `\citet{Smith2000,Jones2001}` "Smith (2000); Jones (2001)" + - `\citen{Smith2000,Jones2001}` "" + +From the user's point of view, citation groups can be created by + +1. Selecting multiple entries in a bibliography database, then + - `[click:Cite]` or + - `[click:Cite in-text]` or + - `[click:Cite special]` or + - `[click:Insert empty citation]` in the panel. + + This method allows any of the citation types to be used. + +2. `[click:Merge citations]` finds all sets of consecutive citations in the text and + replaces each with a group. + - `(change)` The new code only merges consecutive [parenthesized](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/action/EditMerge.java#L183) citations. + - This is inconsistent with the solution used in `[click:Cite]` + - My impression is that + - groups of in-text or invisible citations are probably not useful + - mixed groups are even less. However, with a numbered style + there is no visual difference between parenthesized and in-text + citations, the user may be left wondering why did merge not work. + - One way out could be to merge as a "parenthesized" + group. But then users switching between styles get a + surprise, we have unexpectedly overridden their choice. + - I would prefer a visible log-like warning that does not require + a click to close and lets me see multiple warnings. + Could the main window have such an area at the bottom? + - Starting with JabRef 5.3 there is also `[click:Separate citations]` + that breaks all groups to single citations. + - This allows + - deleting individual citations + - moving individual citations around (between citation groups) + - (copy does not work) + - (Moving a citation within a group has no effect on the final output + due to sorting of citations within groups. See [Sorting within a citation group](#localOrder)) + +In order to manage single citations and groups uniformly, we +consider each citation in the document to belong to a citation +group, even if it means a group containing a single citation. + +Citation groups correspond to citation markers in the document. The latter is empty for invisible +citation groups. When creating the citation markers, the citations in the group +are processed together. + +# Citation styles + +The details of how to format the bibliography and the citation markers are described in a text file. + +- These normally use `.jstyle` extension, and I will refer to them as jstyle files. +- See the [User documentation](https://docs.jabref.org/cite/openofficeintegration#the-style-file) + for details. +- I will refer to keywords in jstyle files as `jstyle:keyword` below. + + +Four major types citation of styles can be described by a jstyle. + +- (1) `jstyle:BibTeXKeyCitations` + + - The citation markers show the citationKey. + - It is not fully implemented + - does not produce markers before the bibliography entries + - does not show pageInfo + - It is not advertised in the [User documentation](https://docs.jabref.org/cite/openofficeintegration#the-style-file). + - Its intended purpose may be + + - (likely) a proper style, with "[Smith2000]" style citation markers + - (possibly) a style for "draft mode" that + - can avoid lookup of citation markers in the database when only the + citation markers are updated + - can produce unique citation markers trivially (only needs local information) + - makes the citation keys visible to the user + - can work without knowing the order of appearance of citation groups + - In case we expect to handle larger documents, a "draft mode" + minimizing work during `[click:Cite]` may be useful. + +- There are two types of numbered (`jstyle:IsNumberEntries`) citation styles: + + - (2) Citations numbered in order of first appearance (`jstyle:IsSortByPosition`) + - (3) Citations numbered according to their order in the sorted bibliography + +- (4) Author-year styles + +# Sorting + +## Sorting te bibliography + +The bibliography is sorted in (author, year, title) order + +- except for `jstyle:IsSortByPosition`, that uses the order of first + appearance of the cited sources. + +## Ordering the citations + +The order of appearance of citations (as considered during numbering and adding letters after the +year to ensure that citation markers uniquely identify sources in the bibliography) is decided on +two levels. + +1. Their order within each citation group (`localOrder`), and +2. the order of the citation groups (citation markers) in the text (`globalOrder`). + +### Sorting within a citation group (`localOrder`) + +The order of citations within a citation group is controlled by +`jstyle:MultiCiteChronological`. + +- true asks for (year, author, title) ordering, +- false for (author, year, title). +- (There is no option for "in the order provided by the user"). + + +For author-year citation styles this ordering is used directly. + +- The (author, year, title) order promotes discovering citations + sharing authors and year and emitting them in a shorter form. For + example as "(Smith 2000a,b)". + +For numbered styles, the citations within a group are sorted again +during generation of the citation marker, now by the numbers +themselves. The result of this sorting is not saved, only affects the citation marker. + +- Series of consecutive number are replaced with ranges: for example "[1-5; 11]" + +### Order of the citation groups (`globalOrder`) + +The location of each citation group in the document is provided by the +user. In a text with no insets, footnotes, figures etc. this directly +provides the order. In the presence of these, it becomes more +complicated, see [Order of appearance of +citation groups](order-of-appearance.md). + +### Order of the citations + +- `globalOrder` and `localOrder` together provide the order of appearance of + citations +- This also provides the order of first appearance of the cited sources. + + First appearance order of sources is used + - in `jstyle:IsSortByPosition` numbered styles + - in author-year styles: first appearance of "Smith200a" + should precede that of "Smith200b". + To achieve this, the sources get the letters + according the order of their first appearance. + - This seems to contradict the statement "The bibliography is + sorted in (author, year, title) order" above. + It does not. As of JabRef 5.3 both are true. + Consequence: in the references + Smith2000b may precede Smith2000a. + ([reported](https://github.com/JabRef/jabref/issues/7805)) + - Some author-year citation styles prescribe a higher threshold on + the number of authors for switching to "FirstAuthor et al." form + (`jstyle:MaxAuthors`) at the first citation of a source + (`jstyle:MaxAuthorsFirst`) + + +# What is stored in a document (JabRef5.2) + +- Each group of citations has a reference mark. + + (Reference marks are shown in LibreOffice in Navigator, under "References". + To show the Navigator: `LibreOffice:[menu:View]/[Navigator]` or `LibreOffice:[key:F5]`) + + Its purposes: + + 1. The text range of the reference mark tells where to write or update the citation mark. + 2. The name of the reference mark + + - Lets us select only those reference marks that belong to us + - Encodes the citation type + - Contains the list of citation keys that belong to this group + - It may contain an extra number, to make the name unique in the document + - Format: `"JR_cite{number}_{type}_{citationKeys}"`, where + - `{number}` is either empty or an unsigned integer (it can be zero) to make the name unique + - `{type}` is 1, 2, or 3 for parenthesized, in-text and invisible + - `{citationKeys}` contains the comma-separated list of citation keys + - Examples: + - `JR_cite_1_Smith2000` (empty number part, parenthesized, single citation) + - `JR_cite0_2_Smith2000,Jones2001` (number part is 0, in-text, two citations) + - `JR_cite1_3_Smith2000,Jones2001` (number part is 1, invisible, two citations) + +- Each group of citations may have an associated pageInfo. + + - In LibreOffice, these can be found at + `LibreOffice:/[menu:File]/[Properties]/[Custom Properties]` + - The property names are identical to the name of the reference mark + corresponding to the citation group. + - JabRef 5.2 never cleans up these, they are left around. + `(problem)` New citations may "pick up" these unexpectedly. + +- The bibliography, if not found, is created at the end of the document. + - The location and extent of the bibliography is the content of the Section named `"JR_bib"`. + (In LibreOffice Sections are listed in the Navigator panel, under "Sections") + - JabRef 5.2 also creates a bookmark named `"JR_bib_end"`, but does + not use it. During bibliography update it attempts to create it again without + removing the old bookmark. The result is a new bookmark, with a number appended to its name + (by LibreOffice, to ensure unique names of bookmarks). + - [Correction in new code](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/frontend/UpdateBibliography.java#L147): + remove the old before creating the new. + +# How does it interact with the document? + +- "stateless" + JabRef is only loosely coupled to the document. + Between two GUI actions it does not receive any information from LibreOffice. + It cannot distinguish between the user changing a single character in the document or rewriting everything. + +- Access data + - During a `[click:cite]` or `[click:Update]` we need the reference mark names. + - Get all reference mark names + - Filter (only ours) + - Parse: gives citation type (for the group), citation keys + - Access/store pageInfo: based on reference mark name and property name being equal + - Creating a citation group: (`[click:cite]`) + - Creates a reference mark at the cursor, with a name as described above. +- Update (refreshing citation markers and bibliography): + - citation markers: the content of the reference mark + - bibliography: the content of the Section (in LibreOffice sense) + named `"JR_bib"`. diff --git a/docs/openoffice/problems.md b/docs/openoffice/problems.md new file mode 100644 index 00000000000..c23b39a7d25 --- /dev/null +++ b/docs/openoffice/problems.md @@ -0,0 +1,97 @@ +# Problems in JabRef 5.2 + +## pageInfo should belong to citations, not citation groups + +- Creating `[click:Separate]` revealed + a `(problem)`: pageInfo strings are conceptually associated with + citations, but the implementation associates them to citation groups. + The number of available + pageInfo slots changes during`[click:Merge]` and `[click:Separate]` while the number of citations + remains fixed. + - The proposed solution was to change the association. + - Not only reference marks (citation groups) need unique identifiers, but also citations. + Possible encoding for reference mark names: + `JR_cite{type}_{number1}_{citationKey1},{number2}_{citationKey2}` + where `{type}` encodes the citation type (for the group), `{citationKey1}` is made unique by choosing an appropriate number for `{number1}` + This would allow + `JR_cite_{number1}_{citationKey1}` to be used as a property name for storing the pageInfo. + + Changes required to + - reference mark search, name generation and parsing + - name generation and parsing for properties storing pageInfo values + - in-memory representation + - JabRef 5.2 does not collect pageInfo values, accesses only when needed. + So it would be change to code accessing them. + - The proposed representation does collect, to allow separation of getting from the document + and processing + - insertion of pageInfo into citation markers: JabRef 5.2 injects a single pageInfo before the closing parenthesis, + now we need to handle several values + - `[click:Manage citations]` should work on citations, not citation groups. + + +## Backend + +The choice of how do we represent the data and the citation marks in the document has consequences +on usability. + +Reference marks have some features that make it easy to mess up citations in a document + +- They are **not visible** by default, the user is not aware of their boundaries +(`LO:[key:Ctrl-F8]`, `LO:[View]/[Field shadings]` helps) + +- They are **not atomic**: + - the user can edit the content. This will be lost on `[click:Update]` + If an `As character` or `To character` anchor is inserted, the corresponding frame or footnote is deleted. + - by pressing Enter within, the user can break a reference mark into two parts. + The second part is now outside the reference mark: `[click:Update]` will leave it as is, and replace the first part + with the full text for the citation mark. + - If the space separating to citation marks is deleted, the user cannot reliably type between the + marks. + The text typed usually becomes part of one of the marks. No visual clue as to which one. + Note: `[click:Merge]` then `[click:Separate]` adds a single space between. The user can + position the cursor before or after it. In either case the cursor is on a boundary: it is not + clear if it is in or out of a reference mark. + Special case: a reference mark at the start or end of a paragraph: the cursor is usually considered to be within at the coresponding edge. +- (good) They can be moved (Ctrl-X,Ctrl-V) +- They cannot be copied. (Ctrl-C, Ctrl-V) copies the text without the reference mark. +- Reference marks are lost if the document is saved as docx. + +- I know of no way to insert text into an empty text range denoted by a reference mark + - JabRef 5.3 recreates the reference mark (using [insertReferenceMark](https://github.com/JabRef/jabref/blob/475b2989ffa8ec61c3327c62ed8f694149f83220/src/main/java/org/jabref/gui/openoffice/OOBibBase.java#L1072)) + [here](https://github.com/JabRef/jabref/blob/475b2989ffa8ec61c3327c62ed8f694149f83220/src/main/java/org/jabref/gui/openoffice/OOBibBase.java#L706) + - `(change)` I preferred to (try to) avoid this: + [NamedRangeReferenceMark.nrGetFillCursor](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java#L225) + returns a cursor between two invisible + spaces, to provide the caller a location it can safely write some text. [NamedRangeReferenceMark.nrCleanFillCursor](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java#L432) + removes these invisible spaces unless the content would become empty or a single character. By + keeping the content at least two characters, we avoid the ambiguity at the edges: a cursor + positioned between two characters inside is always within the reference mark. (At the edges it + may or may not be inside.) + +- `(change)` `[click:Cite]` at reference mark edges: [safeInsertSpacesBetweenReferenceMarks](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/logic/openoffice/backend/NamedRangeReferenceMark.java#L67) ensures the we are not inside, by starting two new paragraphs, inserting two spaces between them, then removing the new paragraph marks. +- `(change)` [guiActionInsertEntry](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/gui/openoffice/OOBibBase2.java#L624) +checks if the cursor is in a citation mark or the bibliography. + +- `(change)` `[click:Update]` does an [exhaustive check](https://github.com/antalk2/jabref/blob/122d5133fa6c7b44245c5ba5600d398775718664/src/main/java/org/jabref/gui/openoffice/OOBibBase2.java#L927) +for overlaps between protected ranges (citation marks and bibliography). This can become slow if there are many citations. + + +It would be nice if we could have a backend with better properties. We probably need multiple +backends for different purposes. This would be made easier if the backend were separated from the +rest of the code. This would be the purpose of +[logic/openoffice/backend](https://github.com/antalk2/jabref/tree/improve-reversibility-rebased-03/src/main/java/org/jabref/logic/openoffice/backend). + +## Undo + +- JabRef 5.3 does not collect the effects of GUI actions on the document into larger Undo actions. +This makes the Undo functionality of LO impractial. +- `(change)` collect the effects of GUI actions into large chunks: now a GUI action can be undone +with a single click. + - except the effect on pageInfo: that is stored at the document level and is not restored by Undo. + +## Block screen refresh + +- LibreOffice has support in [XModel](https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XModel.html#a7b7d36374033ee9210ec0ac5c1a90d9f) +to "suspend some notifications to the controllers which are used for display updates." + +- `(change)` Now we are using this facility. diff --git a/src/main/java/org/jabref/gui/openoffice/OOBibBase2.java b/src/main/java/org/jabref/gui/openoffice/OOBibBase2.java new file mode 100644 index 00000000000..d8523f0cd77 --- /dev/null +++ b/src/main/java/org/jabref/gui/openoffice/OOBibBase2.java @@ -0,0 +1,925 @@ +package org.jabref.gui.openoffice; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.jabref.gui.DialogService; +import org.jabref.logic.JabRefException; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.openoffice.NoDocumentFoundException; +import org.jabref.logic.openoffice.action.EditInsert; +import org.jabref.logic.openoffice.action.EditMerge; +import org.jabref.logic.openoffice.action.EditSeparate; +import org.jabref.logic.openoffice.action.ExportCited; +import org.jabref.logic.openoffice.action.ManageCitations; +import org.jabref.logic.openoffice.action.Update; +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.logic.openoffice.frontend.RangeForOverlapCheck; +import org.jabref.logic.openoffice.style.OOBibStyle; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.CitationEntry; +import org.jabref.model.openoffice.rangesort.FunctionalTextViewCursor; +import org.jabref.model.openoffice.style.CitationGroupId; +import org.jabref.model.openoffice.style.CitationType; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoCrossRef; +import org.jabref.model.openoffice.uno.UnoCursor; +import org.jabref.model.openoffice.uno.UnoRedlines; +import org.jabref.model.openoffice.uno.UnoStyle; +import org.jabref.model.openoffice.uno.UnoUndo; +import org.jabref.model.openoffice.util.OOResult; +import org.jabref.model.openoffice.util.OOVoidResult; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.NotRemoveableException; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.comp.helper.BootstrapException; +import com.sun.star.container.NoSuchElementException; +import com.sun.star.lang.DisposedException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class for manipulating the Bibliography of the currently started + * document in OpenOffice. + */ +class OOBibBase2 { + + private static final Logger LOGGER = LoggerFactory.getLogger(OOBibBase2.class); + + /* variables */ + private final DialogService dialogService; + + /* + * After inserting a citation, if ooPrefs.getSyncWhenCiting() returns true, + * shall we also update the bibliography? + */ + private final boolean refreshBibliographyDuringSyncWhenCiting; + + /* + * Shall we add "Cited on pages: ..." to resolved bibliography entries? + */ + private final boolean alwaysAddCitedOnPages; + + private final OOBibBaseConnect connection; + + /* + * Constructor + */ + public OOBibBase2(Path loPath, DialogService dialogService) + throws + BootstrapException, + CreationException { + + this.dialogService = dialogService; + this.connection = new OOBibBaseConnect(loPath, dialogService); + + this.refreshBibliographyDuringSyncWhenCiting = false; + this.alwaysAddCitedOnPages = false; + } + + public void guiActionSelectDocument(boolean autoSelectForSingle) { + final String title = Localization.lang("Problem connecting"); + + try { + + this.connection.selectDocument(autoSelectForSingle); + + } catch (NoDocumentFoundException ex) { + OOError.from(ex).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (WrappedTargetException + | IndexOutOfBoundsException + | NoSuchElementException ex) { + LOGGER.warn("Problem connecting", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } + + if (this.isConnectedToDocument()) { + dialogService.notify(Localization.lang("Connected to document") + ": " + + this.getCurrentDocumentTitle().orElse("")); + } + } + + /** + * A simple test for document availability. + * + * See also `isDocumentConnectionMissing` for a test + * actually attempting to use the connection. + * + */ + public boolean isConnectedToDocument() { + return this.connection.isConnectedToDocument(); + } + + /** + * @return true if we are connected to a document + */ + public boolean isDocumentConnectionMissing() { + return this.connection.isDocumentConnectionMissing(); + } + + /** + * Either return an XTextDocument or return JabRefException. + */ + public OOResult getXTextDocument() { + return this.connection.getXTextDocument(); + } + + /** + * The title of the current document, or Optional.empty() + */ + public Optional getCurrentDocumentTitle() { + return this.connection.getCurrentDocumentTitle(); + } + + /* ****************************************************** + * + * Tools to collect and show precondition test results + * + * ******************************************************/ + + void showDialog(OOError err) { + err.showErrorDialog(dialogService); + } + + void showDialog(String title, OOError err) { + err.setTitle(title).showErrorDialog(dialogService); + } + + OOVoidResult collectResults(String title, List> results) { + String msg = (results.stream() + .filter(OOVoidResult::isError) + .map(e -> e.getError().getLocalizedMessage()) + .collect(Collectors.joining("\n\n"))); + if (msg.isEmpty()) { + return OOVoidResult.ok(); + } else { + return OOVoidResult.error(new OOError(title, msg)); + } + } + + boolean testDialog(OOVoidResult res) { + return res.ifError(ex -> ex.showErrorDialog(dialogService)).isError(); + } + + boolean testDialog(String title, OOVoidResult res) { + return res.ifError(e -> showDialog(e.setTitle(title))).isError(); + } + + boolean testDialog(String title, List> results) { + return testDialog(title, collectResults(title, results)); + } + + @SafeVarargs + final boolean testDialog(String title, OOVoidResult... results) { + List> resultList = Arrays.asList(results); + return testDialog(collectResults(title, resultList)); + } + + /* + * + * Get the cursor positioned by the user for inserting text. + * + */ + OOResult getUserCursorForTextInsertion(XTextDocument doc, String title) { + + // Get the cursor positioned by the user. + XTextCursor cursor = UnoCursor.getViewCursor(doc).orElse(null); + + // Check for crippled XTextViewCursor + Objects.requireNonNull(cursor); + try { + cursor.getStart(); + } catch (com.sun.star.uno.RuntimeException ex) { + String msg = + Localization.lang("Please move the cursor" + + " to the location for the new citation.") + + "\n" + + Localization.lang("I cannot insert to the cursors current location."); + return OOResult.error(new OOError(title, msg, ex)); + } + return OOResult.ok(cursor); + } + + /** + * This may move the view cursor. + */ + OOResult getFunctionalTextViewCursor(XTextDocument doc, + String title) { + String messageOnFailureToObtain = + Localization.lang("Please move the cursor into the document text.") + + "\n" + + Localization.lang("To get the visual positions of your citations" + + " I need to move the cursor around," + + " but could not get it."); + OOResult result = FunctionalTextViewCursor.get(doc); + if (result.isError()) { + LOGGER.warn(result.getError()); + } + return result.mapError(detail -> new OOError(title, messageOnFailureToObtain)); + } + + private static OOVoidResult checkRangeOverlaps(XTextDocument doc, OOFrontend frontend) { + final String title = "checkRangeOverlaps"; + boolean requireSeparation = false; + int maxReportedOverlaps = 10; + try { + return (frontend.checkRangeOverlaps(doc, + new ArrayList<>(), + requireSeparation, + maxReportedOverlaps) + .mapError(OOError::from)); + } catch (NoDocumentException ex) { + return OOVoidResult.error(OOError.from(ex).setTitle(title)); + } catch (WrappedTargetException ex) { + return OOVoidResult.error(OOError.fromMisc(ex).setTitle(title)); + } + } + + private static OOVoidResult checkRangeOverlapsWithCursor(XTextDocument doc, OOFrontend frontend) { + final String title = "checkRangeOverlapsWithCursor"; + + List> userRanges; + userRanges = frontend.viewCursorRanges(doc); + + boolean requireSeparation = false; + OOVoidResult res; + try { + res = frontend.checkRangeOverlapsWithCursor(doc, + userRanges, + requireSeparation); + } catch (NoDocumentException ex) { + return OOVoidResult.error(OOError.from(ex).setTitle(title)); + } catch (WrappedTargetException ex) { + return OOVoidResult.error(OOError.fromMisc(ex).setTitle(title)); + } + + if (res.isError()) { + final String xtitle = Localization.lang("The cursor is in protected area."); + return OOVoidResult.error(new OOError(xtitle, + xtitle + "\n" + + res.getError().getLocalizedMessage() + "\n")); + } + return res.mapError(OOError::from); + } + + /* + * + * Tests for preconditions. + * + */ + + private static OOVoidResult checkIfOpenOfficeIsRecordingChanges(XTextDocument doc) { + + String title = Localization.lang("Recording and/or Recorded changes"); + try { + boolean recordingChanges = UnoRedlines.getRecordChanges(doc); + int nRedlines = UnoRedlines.countRedlines(doc); + if (recordingChanges || nRedlines > 0) { + String msg = ""; + if (recordingChanges) { + msg += Localization.lang("Cannot work with" + + " [Edit]/[Track Changes]/[Record] turned on."); + } + if (nRedlines > 0) { + if (recordingChanges) { + msg += "\n"; + } + msg += Localization.lang("Changes by JabRef" + + " could result in unexpected interactions with" + + " recorded changes."); + msg += "\n"; + msg += Localization.lang("Use [Edit]/[Track Changes]/[Manage] to resolve them first."); + } + return OOVoidResult.error(new OOError(title, msg)); + } + } catch (WrappedTargetException ex) { + String msg = Localization.lang("Error while checking if Writer" + + " is recording changes or has recorded changes."); + return OOVoidResult.error(new OOError(title, msg, ex)); + } + return OOVoidResult.ok(); + } + + OOVoidResult styleIsRequired(OOBibStyle style) { + if (style == null) { + return OOVoidResult.error(OOError.noValidStyleSelected()); + } else { + return OOVoidResult.ok(); + } + } + + OOResult getFrontend(XTextDocument doc) { + final String title = "getFrontend"; + try { + return OOResult.ok(new OOFrontend(doc)); + } catch (NoDocumentException ex) { + return OOResult.error(OOError.from(ex).setTitle(title)); + } catch (WrappedTargetException + | RuntimeException ex) { + return OOResult.error(OOError.fromMisc(ex).setTitle(title)); + } + } + + OOVoidResult databaseIsRequired(List databases, + Supplier fun) { + if (databases == null || databases.isEmpty()) { + return OOVoidResult.error(fun.get()); + } else { + return OOVoidResult.ok(); + } + } + + OOVoidResult selectedBibEntryIsRequired(List entries, + Supplier fun) { + if (entries == null || entries.isEmpty()) { + return OOVoidResult.error(fun.get()); + } else { + return OOVoidResult.ok(); + } + } + + /* + * Checks existence and also checks if it is not an internal name. + */ + private OOVoidResult checkStyleExistsInTheDocument(String familyName, + String styleName, + XTextDocument doc, + String labelInJstyleFile, + String pathToStyleFile) + throws + WrappedTargetException { + + Optional internalName = UnoStyle.getInternalNameOfStyle(doc, familyName, styleName); + + if (internalName.isEmpty()) { + String msg = + switch (familyName) { + case UnoStyle.PARAGRAPH_STYLES -> + Localization.lang("The %0 paragraph style '%1' is missing from the document", + labelInJstyleFile, + styleName); + case UnoStyle.CHARACTER_STYLES -> + Localization.lang("The %0 character style '%1' is missing from the document", + labelInJstyleFile, + styleName); + default -> + throw new IllegalArgumentException("Expected " + UnoStyle.CHARACTER_STYLES + + " or " + UnoStyle.PARAGRAPH_STYLES + + " for familyName"); + } + + "\n" + + Localization.lang("Please create it in the document or change in the file:") + + "\n" + + pathToStyleFile; + return OOVoidResult.error(new OOError("StyleIsNotKnown", msg)); + } + + if (!internalName.get().equals(styleName)) { + String msg = + switch (familyName) { + case UnoStyle.PARAGRAPH_STYLES -> + Localization.lang("The %0 paragraph style '%1' is a display name for '%2'.", + labelInJstyleFile, + styleName, + internalName.get()); + case UnoStyle.CHARACTER_STYLES -> + Localization.lang("The %0 character style '%1' is a display name for '%2'.", + labelInJstyleFile, + styleName, + internalName.get()); + default -> + throw new IllegalArgumentException("Expected " + UnoStyle.CHARACTER_STYLES + + " or " + UnoStyle.PARAGRAPH_STYLES + + " for familyName"); + } + + "\n" + + Localization.lang("Please use the latter in the style file below" + + " to avoid localization problems.") + + "\n" + + pathToStyleFile; + return OOVoidResult.error(new OOError("StyleNameIsNotInternal", msg)); + } + return OOVoidResult.ok(); + } + + public OOVoidResult checkStylesExistInTheDocument(OOBibStyle style, XTextDocument doc) { + + String pathToStyleFile = style.getPath(); + + List> results = new ArrayList<>(); + try { + results.add(checkStyleExistsInTheDocument(UnoStyle.PARAGRAPH_STYLES, + style.getReferenceHeaderParagraphFormat(), + doc, + "ReferenceHeaderParagraphFormat", + pathToStyleFile)); + results.add(checkStyleExistsInTheDocument(UnoStyle.PARAGRAPH_STYLES, + style.getReferenceParagraphFormat(), + doc, + "ReferenceParagraphFormat", + pathToStyleFile)); + if (style.isFormatCitations()) { + results.add(checkStyleExistsInTheDocument(UnoStyle.CHARACTER_STYLES, + style.getCitationCharacterFormat(), + doc, + "CitationCharacterFormat", + pathToStyleFile)); + } + } catch (WrappedTargetException ex) { + results.add(OOVoidResult.error(new OOError("Other error in checkStyleExistsInTheDocument", + ex.getMessage(), + ex))); + } + + return collectResults("checkStyleExistsInTheDocument failed", results); + } + + /* + * + * ManageCitationsDialogView + * + */ + public Optional> guiActionGetCitationEntries() { + + final Optional> FAIL = Optional.empty(); + final String title = Localization.lang("Problem collecting citations"); + + OOResult odoc = getXTextDocument(); + if (testDialog(title, odoc.asVoidResult())) { + return FAIL; + } + XTextDocument doc = odoc.get(); + + if (testDialog(title, checkIfOpenOfficeIsRecordingChanges(doc))) { + LOGGER.warn(title); + return FAIL; + } + + try { + + return Optional.of(ManageCitations.getCitationEntries(doc)); + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + return FAIL; + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + return FAIL; + } catch (WrappedTargetException ex) { + LOGGER.warn(title, ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + return FAIL; + } + } + + /** + * Apply editable parts of citationEntries to the document: store + * pageInfo. + * + * Does not change presentation. + * + * Note: we use no undo context here, because only + * DocumentConnection.setUserDefinedStringPropertyValue() is called, + * and Undo in LO will not undo that. + * + * GUI: "Manage citations" dialog "OK" button. + * Called from: ManageCitationsDialogViewModel.storeSettings + * + *

+ * Currently the only editable part is pageInfo. + *

+ * Since the only call to applyCitationEntries() only changes + * pageInfo w.r.t those returned by getCitationEntries(), we can + * do with the following restrictions: + *

    + *
  • Missing pageInfo means no action.
  • + *
  • Missing CitationEntry means no action (no attempt to remove + * citation from the text).
  • + *
+ */ + public void guiActionApplyCitationEntries(List citationEntries) { + + final String title = Localization.lang("Problem modifying citation"); + + OOResult odoc = getXTextDocument(); + if (testDialog(title, odoc.asVoidResult())) { + return; + } + XTextDocument doc = odoc.get(); + + try { + + ManageCitations.applyCitationEntries(doc, citationEntries); + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (PropertyVetoException + | IllegalTypeException + | WrappedTargetException + | com.sun.star.lang.IllegalArgumentException ex) { + LOGGER.warn(title, ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } + } + + /** + * + * Creates a citation group from {@code entries} at the cursor. + * + * Uses LO undo context "Insert citation". + * + * Note: Undo does not remove or reestablish custom properties. + * + * @param entries The entries to cite. + * + * @param database The database the entries belong to (all of them). + * Used when creating the citation mark. + * + * Consistency: for each entry in {@entries}: looking it up in + * {@code syncOptions.get().databases} (if present) + * should yield {@code database}. + * + * @param style The bibliography style we are using. + * + * @param citationType Indicates whether it is an in-text + * citation, a citation in parenthesis or + * an invisible citation. + * + * @param pageInfo A single page-info for these entries. + * Attributed to the last entry. + * + * @param syncOptions Indicates whether in-text citations + * should be refreshed in the document. + * Optional.empty() indicates no refresh. + * Otherwise provides options for refreshing + * the reference list. + */ + public void guiActionInsertEntry(List entries, + BibDatabase database, + OOBibStyle style, + CitationType citationType, + String pageInfo, + Optional syncOptions) { + + final String title = "Could not insert citation"; + + OOResult odoc = getXTextDocument(); + if (testDialog(title, + odoc.asVoidResult(), + styleIsRequired(style), + selectedBibEntryIsRequired(entries, OOError::noEntriesSelectedForCitation))) { + return; + } + XTextDocument doc = odoc.get(); + + OOResult frontend = getFrontend(doc); + if (testDialog(title, frontend.asVoidResult())) { + return; + } + + OOResult cursor = getUserCursorForTextInsertion(doc, title); + if (testDialog(title, cursor.asVoidResult())) { + return; + } + + if (testDialog(title, checkRangeOverlapsWithCursor(doc, frontend.get()))) { + return; + } + + if (testDialog(title, + checkStylesExistInTheDocument(style, doc), + checkIfOpenOfficeIsRecordingChanges(doc))) { + return; + } + + /* + * For sync we need a FunctionalTextViewCursor. + */ + OOResult fcursor = null; + if (syncOptions.isPresent()) { + fcursor = getFunctionalTextViewCursor(doc, title); + if (testDialog(title, fcursor.asVoidResult())) { + return; + } + } + + syncOptions + .map(e -> e.setUpdateBibliography(this.refreshBibliographyDuringSyncWhenCiting)) + .map(e -> e.setAlwaysAddCitedOnPages(this.alwaysAddCitedOnPages)); + + if (syncOptions.isPresent()) { + if (testDialog(databaseIsRequired(syncOptions.get().databases, + OOError::noDataBaseIsOpenForSyncingAfterCitation))) { + return; + } + } + + try { + UnoUndo.enterUndoContext(doc, "Insert citation"); + + EditInsert.insertCitationGroup(doc, + frontend.get(), + cursor.get(), + entries, + database, + style, + citationType, + pageInfo); + + if (syncOptions.isPresent()) { + Update.resyncDocument(doc, style, fcursor.get(), syncOptions.get()); + } + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + return; + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + return; + } catch (CreationException + | IllegalTypeException + | NotRemoveableException + | PropertyVetoException + | WrappedTargetException ex) { + LOGGER.warn("Could not insert entry", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + return; + } finally { + UnoUndo.leaveUndoContext(doc); + } + } + + /** + * GUI action "Merge citations" + * + */ + public void guiActionMergeCitationGroups(List databases, OOBibStyle style) { + + final String title = Localization.lang("Problem combining cite markers"); + + OOResult odoc = getXTextDocument(); + if (testDialog(title, + odoc.asVoidResult(), + styleIsRequired(style), + databaseIsRequired(databases, OOError::noDataBaseIsOpen))) { + return; + } + XTextDocument doc = odoc.get(); + + OOResult fcursor = getFunctionalTextViewCursor(doc, title); + + if (testDialog(title, + fcursor.asVoidResult(), + checkStylesExistInTheDocument(style, doc), + checkIfOpenOfficeIsRecordingChanges(doc))) { + return; + } + + try { + UnoUndo.enterUndoContext(doc, "Merge citations"); + + OOFrontend frontend = new OOFrontend(doc); + boolean madeModifications = EditMerge.mergeCitationGroups(doc, frontend, style); + if (madeModifications) { + UnoCrossRef.refresh(doc); + Update.SyncOptions syncOptions = new Update.SyncOptions(databases); + Update.resyncDocument(doc, style, fcursor.get(), syncOptions); + } + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (CreationException + | IllegalTypeException + | NotRemoveableException + | PropertyVetoException + | WrappedTargetException + | com.sun.star.lang.IllegalArgumentException ex) { + LOGGER.warn("Problem combining cite markers", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } finally { + UnoUndo.leaveUndoContext(doc); + fcursor.get().restore(doc); + } + } // MergeCitationGroups + + /** + * GUI action "Separate citations". + * + * Do the opposite of MergeCitationGroups. + * Combined markers are split, with a space inserted between. + */ + public void guiActionSeparateCitations(List databases, OOBibStyle style) { + + final String title = Localization.lang("Problem during separating cite markers"); + + OOResult odoc = getXTextDocument(); + if (testDialog(title, + odoc.asVoidResult(), + styleIsRequired(style), + databaseIsRequired(databases, OOError::noDataBaseIsOpen))) { + return; + } + + XTextDocument doc = odoc.get(); + OOResult fcursor = getFunctionalTextViewCursor(doc, title); + + if (testDialog(title, + fcursor.asVoidResult(), + checkStylesExistInTheDocument(style, doc), + checkIfOpenOfficeIsRecordingChanges(doc))) { + return; + } + + try { + UnoUndo.enterUndoContext(doc, "Separate citations"); + + OOFrontend frontend = new OOFrontend(doc); + boolean madeModifications = EditSeparate.separateCitations(doc, frontend, databases, style); + if (madeModifications) { + UnoCrossRef.refresh(doc); + Update.SyncOptions syncOptions = new Update.SyncOptions(databases); + Update.resyncDocument(doc, style, fcursor.get(), syncOptions); + } + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (CreationException + | IllegalTypeException + | NotRemoveableException + | PropertyVetoException + | WrappedTargetException + | com.sun.star.lang.IllegalArgumentException ex) { + LOGGER.warn("Problem during separating cite markers", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } finally { + UnoUndo.leaveUndoContext(doc); + fcursor.get().restore(doc); + } + } + + /** + * GUI action for "Export cited" + * + * Does not refresh the bibliography. + * + * @param returnPartialResult If there are some unresolved keys, + * shall we return an otherwise nonempty result, or Optional.empty()? + */ + public Optional exportCitedHelper(List databases, + boolean returnPartialResult) { + + final Optional FAIL = Optional.empty(); + final String title = Localization.lang("Unable to generate new library"); + + OOResult odoc = getXTextDocument(); + if (testDialog(title, + odoc.asVoidResult(), + databaseIsRequired(databases, OOError::noDataBaseIsOpenForExport))) { + return FAIL; + } + XTextDocument doc = odoc.get(); + + try { + + ExportCited.GenerateDatabaseResult result; + try { + UnoUndo.enterUndoContext(doc, "Changes during \"Export cited\""); + result = ExportCited.generateDatabase(doc, databases); + } finally { + // There should be no changes, thus no Undo entry should appear + // in LibreOffice. + UnoUndo.leaveUndoContext(doc); + } + + if (!result.newDatabase.hasEntries()) { + dialogService.showErrorDialogAndWait( + Localization.lang("Unable to generate new library"), + Localization.lang("Your OpenOffice/LibreOffice document references" + + " no citation keys" + + " which could also be found in your current library.")); + return FAIL; + } + + List unresolvedKeys = result.unresolvedKeys; + if (!unresolvedKeys.isEmpty()) { + dialogService.showErrorDialogAndWait( + Localization.lang("Unable to generate new library"), + Localization.lang("Your OpenOffice/LibreOffice document references" + + " at least %0 citation keys" + + " which could not be found in your current library." + + " Some of these are %1.", + String.valueOf(unresolvedKeys.size()), + String.join(", ", unresolvedKeys))); + if (returnPartialResult) { + return Optional.of(result.newDatabase); + } else { + return FAIL; + } + } + return Optional.of(result.newDatabase); + } catch (NoDocumentException ex) { + OOError.from(ex).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (WrappedTargetException + | com.sun.star.lang.IllegalArgumentException ex) { + LOGGER.warn("Problem generating new database.", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } + return FAIL; + } + + /** + * GUI action, refreshes citation markers and bibliography. + * + * @param databases Must have at least one. + * @param style Style. + * + */ + public void guiActionUpdateDocument(List databases, OOBibStyle style) { + + final String title = Localization.lang("Unable to synchronize bibliography"); + + try { + + OOResult odoc = getXTextDocument(); + if (testDialog(title, + odoc.asVoidResult(), + styleIsRequired(style))) { + return; + } + + XTextDocument doc = odoc.get(); + + OOResult fcursor = getFunctionalTextViewCursor(doc, title); + + if (testDialog(title, + fcursor.asVoidResult(), + checkStylesExistInTheDocument(style, doc), + checkIfOpenOfficeIsRecordingChanges(doc))) { + return; + } + + OOFrontend frontend = new OOFrontend(doc); + if (testDialog(title, checkRangeOverlaps(doc, frontend))) { + return; + } + + List unresolvedKeys; + try { + UnoUndo.enterUndoContext(doc, "Refresh bibliography"); + + Update.SyncOptions syncOptions = new Update.SyncOptions(databases); + syncOptions + .setUpdateBibliography(true) + .setAlwaysAddCitedOnPages(this.alwaysAddCitedOnPages); + + unresolvedKeys = Update.synchronizeDocument(doc, frontend, style, fcursor.get(), syncOptions); + + } finally { + UnoUndo.leaveUndoContext(doc); + fcursor.get().restore(doc); + } + + if (!unresolvedKeys.isEmpty()) { + String msg = Localization.lang( + "Your OpenOffice/LibreOffice document references the citation key '%0'," + + " which could not be found in your current library.", + unresolvedKeys.get(0)); + dialogService.showErrorDialogAndWait(title, msg); + return; + } + + } catch (NoDocumentException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (DisposedException ex) { + OOError.from(ex).setTitle(title).showErrorDialog(dialogService); + } catch (CreationException + | WrappedTargetException + | com.sun.star.lang.IllegalArgumentException ex) { + LOGGER.warn("Could not update bibliography", ex); + OOError.fromMisc(ex).setTitle(title).showErrorDialog(dialogService); + } + } + +} diff --git a/src/main/java/org/jabref/gui/openoffice/OOBibBaseConnect.java b/src/main/java/org/jabref/gui/openoffice/OOBibBaseConnect.java new file mode 100644 index 00000000000..728b5cf541e --- /dev/null +++ b/src/main/java/org/jabref/gui/openoffice/OOBibBaseConnect.java @@ -0,0 +1,255 @@ +package org.jabref.gui.openoffice; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jabref.gui.DialogService; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.openoffice.NoDocumentFoundException; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoCast; +import org.jabref.model.openoffice.uno.UnoTextDocument; +import org.jabref.model.openoffice.util.OOResult; + +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.frame.XDesktop; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.lang.XComponent; +import com.sun.star.lang.XMultiComponentFactory; +import com.sun.star.text.XTextDocument; +import com.sun.star.uno.XComponentContext; + +/** + * Establish connection to a document opened in OpenOffice or LibreOffice. + */ +class OOBibBaseConnect { + + /* variables */ + private final DialogService dialogService; + private final XDesktop xDesktop; + + /** + * Created when connected to a document. + * + * Cleared (to null) when we discover we lost the connection. + */ + private XTextDocument xTextDocument; + + /* + * Constructor + */ + public OOBibBaseConnect(Path loPath, DialogService dialogService) + throws + BootstrapException, + CreationException { + + this.dialogService = dialogService; + this.xDesktop = simpleBootstrap(loPath); + } + + private XDesktop simpleBootstrap(Path loPath) + throws + CreationException, + BootstrapException { + + // Get the office component context: + XComponentContext context = org.jabref.gui.openoffice.Bootstrap.bootstrap(loPath); + XMultiComponentFactory sem = context.getServiceManager(); + + // Create the desktop, which is the root frame of the + // hierarchy of frames that contain viewable components: + Object desktop; + try { + desktop = sem.createInstanceWithContext("com.sun.star.frame.Desktop", context); + } catch (com.sun.star.uno.Exception e) { + throw new CreationException(e.getMessage()); + } + return UnoCast.cast(XDesktop.class, desktop).get(); + } + + private static List getTextDocuments(XDesktop desktop) + throws + NoSuchElementException, + WrappedTargetException { + + List result = new ArrayList<>(); + + XEnumerationAccess enumAccess = desktop.getComponents(); + XEnumeration compEnum = enumAccess.createEnumeration(); + + while (compEnum.hasMoreElements()) { + Object next = compEnum.nextElement(); + XComponent comp = UnoCast.cast(XComponent.class, next).get(); + Optional doc = UnoCast.cast(XTextDocument.class, comp); + if (doc.isPresent()) { + result.add(doc.get()); + } + } + return result; + } + + /** + * Run a dialog allowing the user to choose among the documents in `list`. + * + * @return Null if no document was selected. Otherwise the + * document selected. + * + */ + private static XTextDocument selectDocumentDialog(List list, + DialogService dialogService) { + + class DocumentTitleViewModel { + + private final XTextDocument xTextDocument; + private final String description; + + public DocumentTitleViewModel(XTextDocument xTextDocument) { + this.xTextDocument = xTextDocument; + this.description = UnoTextDocument.getFrameTitle(xTextDocument).orElse(""); + } + + public XTextDocument getXtextDocument() { + return xTextDocument; + } + + @Override + public String toString() { + return description; + } + } + + List viewModel = (list.stream() + .map(DocumentTitleViewModel::new) + .collect(Collectors.toList())); + + // This whole method is part of a background task when + // auto-detecting instances, so we need to show dialog in FX + // thread + Optional selectedDocument = + (dialogService + .showChoiceDialogAndWait(Localization.lang("Select document"), + Localization.lang("Found documents:"), + Localization.lang("Use selected document"), + viewModel)); + + return (selectedDocument + .map(DocumentTitleViewModel::getXtextDocument) + .orElse(null)); + } + + /** + * Choose a document to work with. + * + * Assumes we have already connected to LibreOffice or OpenOffice. + * + * If there is a single document to choose from, selects that. + * If there are more than one, shows selection dialog. + * If there are none, throws NoDocumentFoundException + * + * After successful selection connects to the selected document + * and extracts some frequently used parts (starting points for + * managing its content). + * + * Finally initializes this.xTextDocument with the selected + * document and parts extracted. + * + */ + public void selectDocument(boolean autoSelectForSingle) + throws + NoDocumentFoundException, + NoSuchElementException, + WrappedTargetException { + + XTextDocument selected; + List textDocumentList = getTextDocuments(this.xDesktop); + if (textDocumentList.isEmpty()) { + throw new NoDocumentFoundException("No Writer documents found"); + } else if (textDocumentList.size() == 1 && autoSelectForSingle) { + selected = textDocumentList.get(0); // Get the only one + } else { // Bring up a dialog + selected = OOBibBaseConnect.selectDocumentDialog(textDocumentList, + this.dialogService); + } + + if (selected == null) { + return; + } + + this.xTextDocument = selected; + } + + /** + * Mark the current document as missing. + * + */ + private void forgetDocument() { + this.xTextDocument = null; + } + + /** + * A simple test for document availability. + * + * See also `isDocumentConnectionMissing` for a test + * actually attempting to use teh connection. + * + */ + public boolean isConnectedToDocument() { + return this.xTextDocument != null; + } + + /** + * @return true if we are connected to a document + */ + public boolean isDocumentConnectionMissing() { + XTextDocument doc = this.xTextDocument; + + if (doc == null) { + return true; + } + + if (UnoTextDocument.isDocumentConnectionMissing(doc)) { + forgetDocument(); + return true; + } + return false; + } + + /** + * Either return a valid XTextDocument or throw + * NoDocumentException. + */ + public XTextDocument getXTextDocumentOrThrow() + throws + NoDocumentException { + if (isDocumentConnectionMissing()) { + throw new NoDocumentException("Not connected to document"); + } + return this.xTextDocument; + } + + public OOResult getXTextDocument() { + if (isDocumentConnectionMissing()) { + return OOResult.error(OOError.from(new NoDocumentException())); + } + return OOResult.ok(this.xTextDocument); + } + + /** + * The title of the current document, or Optional.empty() + */ + public Optional getCurrentDocumentTitle() { + if (isDocumentConnectionMissing()) { + return Optional.empty(); + } else { + return UnoTextDocument.getFrameTitle(this.xTextDocument); + } + } + +} // end of OOBibBaseConnect diff --git a/src/main/java/org/jabref/gui/openoffice/OOError.java b/src/main/java/org/jabref/gui/openoffice/OOError.java new file mode 100644 index 00000000000..b296ffe30c0 --- /dev/null +++ b/src/main/java/org/jabref/gui/openoffice/OOError.java @@ -0,0 +1,141 @@ +package org.jabref.gui.openoffice; + +import org.jabref.gui.DialogService; +import org.jabref.logic.JabRefException; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.openoffice.NoDocumentFoundException; +import org.jabref.model.openoffice.uno.NoDocumentException; + +import com.sun.star.lang.DisposedException; + +class OOError extends JabRefException { + + private String localizedTitle; + + public OOError(String title, String localizedMessage) { + super(localizedMessage, localizedMessage); + this.localizedTitle = title; + } + + public OOError(String title, String localizedMessage, Throwable cause) { + super(localizedMessage, localizedMessage, cause); + this.localizedTitle = title; + } + + public String getTitle() { + return localizedTitle; + } + + public OOError setTitle(String title) { + localizedTitle = title; + return this; + } + + public void showErrorDialog(DialogService dialogService) { + dialogService.showErrorDialogAndWait(getTitle(), getLocalizedMessage()); + } + + /* + * Conversions from exception caught + */ + + public static OOError from(JabRefException err) { + return new OOError( + Localization.lang("JabRefException"), + err.getLocalizedMessage(), + err); + } + + // For DisposedException + public static OOError from(DisposedException err) { + return new OOError( + Localization.lang("Connection lost"), + Localization.lang("Connection to OpenOffice/LibreOffice has been lost." + + " Please make sure OpenOffice/LibreOffice is running," + + " and try to reconnect."), + err); + } + + // For NoDocumentException + public static OOError from(NoDocumentException err) { + return new OOError( + Localization.lang("Not connected to document"), + Localization.lang("Not connected to any Writer document." + + " Please make sure a document is open," + + " and use the 'Select Writer document' button" + + " to connect to it."), + err); + } + + // For NoDocumentFoundException + public static OOError from(NoDocumentFoundException err) { + return new OOError( + Localization.lang("No Writer documents found"), + Localization.lang("Could not connect to any Writer document." + + " Please make sure a document is open" + + " before using the 'Select Writer document' button" + + " to connect to it."), + err); + } + + public static OOError fromMisc(Exception err) { + return new OOError( + "Exception", + err.getMessage(), + err); + } + + /* + * Messages for error dialog. These are not thrown. + */ + + // noDataBaseIsOpenForCiting + public static OOError noDataBaseIsOpenForCiting() { + return new OOError( + Localization.lang("No database"), + Localization.lang("No bibliography database is open for citation.") + + "\n" + + Localization.lang("Open one before citing.")); + } + + public static OOError noDataBaseIsOpenForSyncingAfterCitation() { + return new OOError( + Localization.lang("No database"), + Localization.lang("No database is open for updating citation markers after citing.") + + "\n" + + Localization.lang("Open one before citing.")); + } + + // noDataBaseIsOpenForExport + public static OOError noDataBaseIsOpenForExport() { + return new OOError( + Localization.lang("No database is open"), + Localization.lang("We need a database to export from. Open one.")); + } + + // noDataBaseIsOpenForExport + public static OOError noDataBaseIsOpen() { + return new OOError( + Localization.lang("No database is open"), + Localization.lang("This operation requires a bibliography database.")); + } + + // noValidStyleSelected + public static OOError noValidStyleSelected() { + return new OOError(Localization.lang("No valid style file defined"), + Localization.lang("No bibliography style is selected for citation.") + + "\n" + + Localization.lang("Select one before citing.") + + "\n" + + Localization.lang("You must select either a valid style file," + + " or use one of the default styles.")); + } + + // noEntriesSelectedForCitation + public static OOError noEntriesSelectedForCitation() { + return new OOError(Localization.lang("No entries selected for citation"), + Localization.lang("No bibliography entries are selected for citation.") + + "\n" + + Localization.lang("Select some before citing.")); + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/NoDocumentFoundException.java b/src/main/java/org/jabref/logic/openoffice/NoDocumentFoundException.java new file mode 100644 index 00000000000..2759a9931aa --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/NoDocumentFoundException.java @@ -0,0 +1,12 @@ +package org.jabref.logic.openoffice; + +public class NoDocumentFoundException extends Exception { + + public NoDocumentFoundException(String message) { + super(message); + } + + public NoDocumentFoundException() { + super("No Writer documents found"); + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/EditInsert.java b/src/main/java/org/jabref/logic/openoffice/action/EditInsert.java new file mode 100644 index 00000000000..0004d3fe6db --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/EditInsert.java @@ -0,0 +1,118 @@ +package org.jabref.logic.openoffice.action; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.logic.openoffice.frontend.UpdateCitationMarkers; +import org.jabref.logic.openoffice.style.OOBibStyle; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationMarkerEntry; +import org.jabref.model.openoffice.style.CitationType; +import org.jabref.model.openoffice.style.NonUniqueCitationMarker; +import org.jabref.model.openoffice.style.OODataModel; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoScreenRefresh; +import org.jabref.model.openoffice.util.OOListUtil; +import org.jabref.model.strings.StringUtil; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.NotRemoveableException; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; + +public class EditInsert { + + private EditInsert() { + /**/ + } + + /** + * In insertEntry we receive BibEntry values from the GUI. + * + * In the document we store citations by their citation key. + * + * If the citation key is missing, the best we can do is to notify the user. Or the programmer, + * that we cannot accept such input. + * + */ + private static String insertEntryGetCitationKey(BibEntry entry) { + Optional key = entry.getCitationKey(); + if (key.isEmpty()) { + throw new IllegalArgumentException("insertEntryGetCitationKey: cannot cite entries without citation key"); + } + return key.get(); + } + + /** + * @param cursor Where to insert. + * @param pageInfo A single pageInfo for a list of entries. This is what we get from the GUI. + */ + public static void insertCitationGroup(XTextDocument doc, + OOFrontend frontend, + XTextCursor cursor, + List entries, + BibDatabase database, + OOBibStyle style, + CitationType citationType, + String pageInfo) + throws + NoDocumentException, + NotRemoveableException, + WrappedTargetException, + PropertyVetoException, + CreationException, + IllegalTypeException { + + List citationKeys = OOListUtil.map(entries, EditInsert::insertEntryGetCitationKey); + + final int totalEntries = entries.size(); + List> pageInfos = OODataModel.fakePageInfos(pageInfo, totalEntries); + + List citations = new ArrayList<>(totalEntries); + for (int i = 0; i < totalEntries; i++) { + Citation cit = new Citation(citationKeys.get(i)); + cit.lookupInDatabases(Collections.singletonList(database)); + cit.setPageInfo(pageInfos.get(i)); + citations.add(cit); + } + + // The text we insert + OOText citeText = null; + if (style.isNumberEntries()) { + citeText = OOText.fromString("[-]"); // A dash only. Only refresh later. + } else { + citeText = style.createCitationMarker(citations, + citationType.inParenthesis(), + NonUniqueCitationMarker.FORGIVEN); + } + + if (StringUtil.isBlank(OOText.toString(citeText))) { + citeText = OOText.fromString("[?]"); + } + + try { + UnoScreenRefresh.lockControllers(doc); + UpdateCitationMarkers.createAndFillCitationGroup(frontend, + doc, + citationKeys, + pageInfos, + citationType, + citeText, + cursor, + style, + true /* insertSpaceAfter */); + } finally { + UnoScreenRefresh.unlockControllers(doc); + } + + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/EditMerge.java b/src/main/java/org/jabref/logic/openoffice/action/EditMerge.java new file mode 100644 index 00000000000..f91e8aec93d --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/EditMerge.java @@ -0,0 +1,344 @@ +package org.jabref.logic.openoffice.action; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.logic.openoffice.frontend.UpdateCitationMarkers; +import org.jabref.logic.openoffice.style.OOBibStyle; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.style.CitationType; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoScreenRefresh; +import org.jabref.model.openoffice.uno.UnoTextRange; +import org.jabref.model.openoffice.util.OOListUtil; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.NotRemoveableException; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class EditMerge { + + private static final Logger LOGGER = LoggerFactory.getLogger(EditMerge.class); + + private EditMerge() { + // hide constructor + } + + /* + * @return true if modified document + */ + public static boolean mergeCitationGroups(XTextDocument doc, OOFrontend frontend, OOBibStyle style) + throws + CreationException, + IllegalArgumentException, + IllegalTypeException, + NoDocumentException, + NotRemoveableException, + PropertyVetoException, + WrappedTargetException { + + boolean madeModifications; + + try { + UnoScreenRefresh.lockControllers(doc); + + List joinableGroups = EditMerge.scan(doc, frontend); + + for (JoinableGroupData joinableGroupData : joinableGroups) { + + List groups = joinableGroupData.group; + + List newCitations = (groups.stream() + .flatMap(group -> group.citationsInStorageOrder.stream()) + .collect(Collectors.toList())); + + CitationType citationType = groups.get(0).citationType; + List> pageInfos = frontend.backend.combinePageInfos(groups); + + frontend.removeCitationGroups(groups, doc); + XTextCursor textCursor = joinableGroupData.groupCursor; + textCursor.setString(""); // Also remove the spaces between. + + List citationKeys = OOListUtil.map(newCitations, Citation::getCitationKey); + + /* insertSpaceAfter: no, it is already there (or could be) */ + boolean insertSpaceAfter = false; + UpdateCitationMarkers.createAndFillCitationGroup(frontend, + doc, + citationKeys, + pageInfos, + citationType, + OOText.fromString("tmp"), + textCursor, + style, + insertSpaceAfter); + } + + madeModifications = !joinableGroups.isEmpty(); + + } finally { + UnoScreenRefresh.unlockControllers(doc); + } + + return madeModifications; + } + + private static class JoinableGroupData { + /** A list of consecutive citation groups only separated by spaces. */ + List group; + + /** A cursor covering the XTextRange of each entry in group (and the spaces between them) */ + XTextCursor groupCursor; + + JoinableGroupData(List group, XTextCursor groupCursor) { + this.group = group; + this.groupCursor = groupCursor; + } + } + + private static class ScanState { + + // Citation groups in the current group + List currentGroup; + + // A cursor that covers the Citation groups in currentGroup, including the space between + // them. + // null if currentGroup.isEmpty() + XTextCursor currentGroupCursor; + + // A cursor starting at the end of the last CitationGroup in + // currentGroup. null if currentGroup.isEmpty() + XTextCursor cursorBetween; + + // The last element of currentGroup. + // null if currentGroup.isEmpty() + CitationGroup prev; + + // The XTextRange for prev. + // null if currentGroup.isEmpty() + XTextRange prevRange; + + ScanState() { + reset(); + } + + void reset() { + currentGroup = new ArrayList<>(); + currentGroupCursor = null; + cursorBetween = null; + prev = null; + prevRange = null; + } + } + + /** + * Decide if group could be added to state.currentGroup + * + * @param group The CitationGroup to test + * @param currentRange The XTextRange corresponding to group. + * + * @return false if cannot add, true if can. If returned true, then state.cursorBetween and + * state.currentGroupCursor are expanded to end at the start of currentRange. + */ + private static boolean checkAddToGroup(ScanState state, CitationGroup group, XTextRange currentRange) { + + if (state.currentGroup.isEmpty()) { + return false; + } + + Objects.requireNonNull(state.currentGroupCursor); + Objects.requireNonNull(state.cursorBetween); + Objects.requireNonNull(state.prev); + Objects.requireNonNull(state.prevRange); + + // Only combine (Author 2000) type citations + if (group.citationType != CitationType.AUTHORYEAR_PAR) { + return false; + } + + if (state.prev != null) { + + // Even if we combine AUTHORYEAR_INTEXT citations, we would not mix them with AUTHORYEAR_PAR + if (group.citationType != state.prev.citationType) { + return false; + } + + if (!UnoTextRange.comparables(state.prevRange, currentRange)) { + return false; + } + + // Sanity check: the current range should start later than the previous. + int textOrder = UnoTextRange.compareStarts(state.prevRange, currentRange); + if (textOrder != (-1)) { + String msg = + String.format("MergeCitationGroups:" + + " \"%s\" supposed to be followed by \"%s\"," + + " but %s", + state.prevRange.getString(), + currentRange.getString(), + ((textOrder == 0) + ? "they start at the same position" + : "the start of the latter precedes the start of the first")); + LOGGER.warn(msg); + return false; + } + } + + if (state.cursorBetween == null) { + return false; + } + + Objects.requireNonNull(state.cursorBetween); + Objects.requireNonNull(state.currentGroupCursor); + + // assume: currentGroupCursor.getEnd() == cursorBetween.getEnd() + if (UnoTextRange.compareEnds(state.cursorBetween, state.currentGroupCursor) != 0) { + LOGGER.warn("MergeCitationGroups: cursorBetween.end != currentGroupCursor.end"); + throw new IllegalStateException("MergeCitationGroups failed"); + } + + /* + * Try to expand state.currentGroupCursor and state.cursorBetween by going right to reach + * rangeStart. + */ + XTextRange rangeStart = currentRange.getStart(); + boolean couldExpand = true; + XTextCursor thisCharCursor = + (currentRange.getText().createTextCursorByRange(state.cursorBetween.getEnd())); + + while (couldExpand && (UnoTextRange.compareEnds(state.cursorBetween, rangeStart) < 0)) { + // + // Check that we only walk through inline whitespace. + // + couldExpand = thisCharCursor.goRight((short) 1, true); + String thisChar = thisCharCursor.getString(); + thisCharCursor.collapseToEnd(); + if (thisChar.isEmpty() || "\n".equals(thisChar) || !thisChar.trim().isEmpty()) { + couldExpand = false; + if (!thisChar.isEmpty()) { + thisCharCursor.goLeft((short) 1, false); + } + break; + } + state.cursorBetween.goRight((short) 1, true); + state.currentGroupCursor.goRight((short) 1, true); + + // These two should move in sync: + if (UnoTextRange.compareEnds(state.cursorBetween, state.currentGroupCursor) != 0) { + LOGGER.warn("MergeCitationGroups: cursorBetween.end != currentGroupCursor.end (during expand)"); + throw new IllegalStateException("MergeCitationGroups failed"); + } + } + + return couldExpand; + } + + /** + * Add group to state.currentGroup + * Set state.cursorBetween to start at currentRange.getEnd() + * Expand state.currentGroupCursor to also cover currentRange + * Set state.prev to group, state.prevRange to currentRange + */ + private static void addToCurrentGroup(ScanState state, CitationGroup group, XTextRange currentRange) { + final boolean isNewGroup = state.currentGroup.isEmpty(); + if (!isNewGroup) { + Objects.requireNonNull(state.currentGroupCursor); + Objects.requireNonNull(state.cursorBetween); + Objects.requireNonNull(state.prev); + Objects.requireNonNull(state.prevRange); + } + + // Add the current entry to a group. + state.currentGroup.add(group); + + // Set up cursorBetween to start at currentRange.getEnd() + XTextRange rangeEnd = currentRange.getEnd(); + state.cursorBetween = currentRange.getText().createTextCursorByRange(rangeEnd); + + // If new group, create currentGroupCursor + if (isNewGroup) { + state.currentGroupCursor = (currentRange.getText() + .createTextCursorByRange(currentRange.getStart())); + } + + // include currentRange in currentGroupCursor + state.currentGroupCursor.goRight((short) (currentRange.getString().length()), true); + + if (UnoTextRange.compareEnds(state.cursorBetween, state.currentGroupCursor) != 0) { + LOGGER.warn("MergeCitationGroups: cursorBetween.end != currentGroupCursor.end"); + throw new IllegalStateException("MergeCitationGroups failed"); + } + + /* Store data about last entry in currentGroup */ + state.prev = group; + state.prevRange = currentRange; + } + + /** + * Scan the document for joinable groups. Return those found. + */ + private static List scan(XTextDocument doc, OOFrontend frontend) + throws + NoDocumentException, + WrappedTargetException { + List result = new ArrayList<>(); + + List groups = frontend.getCitationGroupsSortedWithinPartitions(doc, false /* mapFootnotesToFootnoteMarks */); + if (groups.isEmpty()) { + return result; + } + + ScanState state = new ScanState(); + + for (CitationGroup group : groups) { + + XTextRange currentRange = (frontend.getMarkRange(doc, group) + .orElseThrow(IllegalStateException::new)); + + /* + * Decide if we add group to the group. False when the group is empty. + */ + boolean addToGroup = checkAddToGroup(state, group, currentRange); + + /* + * Even if we do not add it to an existing group, we might use it to start a new group. + * + * Can it start a new group? + */ + boolean canStartGroup = (group.citationType == CitationType.AUTHORYEAR_PAR); + + if (!addToGroup) { + // close currentGroup + if (state.currentGroup.size() > 1) { + result.add(new JoinableGroupData(state.currentGroup, state.currentGroupCursor)); + } + // Start a new, empty group + state.reset(); + } + + if (addToGroup || canStartGroup) { + addToCurrentGroup(state, group, currentRange); + } + } + + // close currentGroup + if (state.currentGroup.size() > 1) { + result.add(new JoinableGroupData(state.currentGroup, state.currentGroupCursor)); + } + return result; + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/EditSeparate.java b/src/main/java/org/jabref/logic/openoffice/action/EditSeparate.java new file mode 100644 index 00000000000..66630a6f8d0 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/EditSeparate.java @@ -0,0 +1,99 @@ +package org.jabref.logic.openoffice.action; + +import java.util.List; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.logic.openoffice.frontend.UpdateCitationMarkers; +import org.jabref.logic.openoffice.style.OOBibStyle; +import org.jabref.logic.openoffice.style.OOProcess; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.openoffice.ootext.OOText; +import org.jabref.model.openoffice.style.Citation; +import org.jabref.model.openoffice.style.CitationGroup; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoScreenRefresh; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.NotRemoveableException; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; + +public class EditSeparate { + + private EditSeparate() { + /**/ + } + + public static boolean separateCitations(XTextDocument doc, + OOFrontend frontend, + List databases, + OOBibStyle style) + throws + CreationException, + IllegalTypeException, + NoDocumentException, + NotRemoveableException, + PropertyVetoException, + WrappedTargetException, + com.sun.star.lang.IllegalArgumentException { + + boolean madeModifications = false; + + // To reduce surprises in JabRef52 mode, impose localOrder to + // decide the visually last Citation in the group. Unless the + // style changed since refresh this is the last on the screen + // as well. + frontend.citationGroups.lookupCitations(databases); + frontend.citationGroups.imposeLocalOrder(OOProcess.comparatorForMulticite(style)); + + List groups = frontend.citationGroups.getCitationGroupsUnordered(); + + try { + UnoScreenRefresh.lockControllers(doc); + + for (CitationGroup group : groups) { + + XTextRange range1 = (frontend + .getMarkRange(doc, group) + .orElseThrow(IllegalStateException::new)); + XTextCursor textCursor = range1.getText().createTextCursorByRange(range1); + + List citations = group.citationsInStorageOrder; + if (citations.size() <= 1) { + continue; + } + + frontend.removeCitationGroup(group, doc); + // Now we own the content of citations + + // Create a citation group for each citation. + final int last = citations.size() - 1; + for (int i = 0; i < citations.size(); i++) { + boolean insertSpaceAfter = (i != last); + Citation citation = citations.get(i); + + UpdateCitationMarkers.createAndFillCitationGroup(frontend, + doc, + List.of(citation.citationKey), + List.of(citation.getPageInfo()), + group.citationType, + OOText.fromString(citation.citationKey), + textCursor, + style, + insertSpaceAfter); + + textCursor.collapseToEnd(); + } + + madeModifications = true; + } + } finally { + UnoScreenRefresh.unlockControllers(doc); + } + return madeModifications; + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/ExportCited.java b/src/main/java/org/jabref/logic/openoffice/action/ExportCited.java new file mode 100644 index 00000000000..c38cd4fd4cc --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/ExportCited.java @@ -0,0 +1,98 @@ +package org.jabref.logic.openoffice.action; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.openoffice.style.CitedKey; +import org.jabref.model.openoffice.style.CitedKeys; +import org.jabref.model.openoffice.uno.NoDocumentException; + +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextDocument; + +public class ExportCited { + + private ExportCited() { + /**/ + } + + public static class GenerateDatabaseResult { + /** + * null: not done; isEmpty: no unresolved + */ + public final List unresolvedKeys; + public final BibDatabase newDatabase; + + GenerateDatabaseResult(List unresolvedKeys, BibDatabase newDatabase) { + this.unresolvedKeys = unresolvedKeys; + this.newDatabase = newDatabase; + } + } + + /** + * + * @param databases The databases to look up the citation keys in the document from. + * @return A new database, with cloned entries. + * + * If a key is not found, it is added to result.unresolvedKeys + * + * Cross references (in StandardField.CROSSREF) are followed (not recursively): + * If the referenced entry is found, it is included in the result. + * If it is not found, it is silently ignored. + */ + public static GenerateDatabaseResult generateDatabase(XTextDocument doc, List databases) + throws + NoDocumentException, + WrappedTargetException { + + OOFrontend frontend = new OOFrontend(doc); + CitedKeys citationKeys = frontend.citationGroups.getCitedKeysUnordered(); + citationKeys.lookupInDatabases(databases); + + List unresolvedKeys = new ArrayList<>(); + BibDatabase resultDatabase = new BibDatabase(); + + List entriesToInsert = new ArrayList<>(); + Set seen = new HashSet<>(); // Only add crossReference once. + + for (CitedKey citation : citationKeys.values()) { + if (citation.getLookupResult().isEmpty()) { + unresolvedKeys.add(citation.citationKey); + continue; + } else { + BibEntry entry = citation.getLookupResult().get().entry; + BibDatabase loopDatabase = citation.getLookupResult().get().database; + + // If entry found + BibEntry clonedEntry = (BibEntry) entry.clone(); + + // Insert a copy of the entry + entriesToInsert.add(clonedEntry); + + // Check if the cloned entry has a cross-reference field + clonedEntry + .getField(StandardField.CROSSREF) + .ifPresent(crossReference -> { + boolean isNew = !seen.contains(crossReference); + if (isNew) { + // Add it if it is in the current library + loopDatabase + .getEntryByCitationKey(crossReference) + .ifPresent(entriesToInsert::add); + seen.add(crossReference); + } + }); + } + } + + resultDatabase.insertEntries(entriesToInsert); + return new GenerateDatabaseResult(unresolvedKeys, resultDatabase); + } + +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/ManageCitations.java b/src/main/java/org/jabref/logic/openoffice/action/ManageCitations.java new file mode 100644 index 00000000000..c8234df9ca0 --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/ManageCitations.java @@ -0,0 +1,38 @@ +package org.jabref.logic.openoffice.action; + +import java.util.List; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.model.openoffice.CitationEntry; +import org.jabref.model.openoffice.uno.NoDocumentException; + +import com.sun.star.beans.IllegalTypeException; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextDocument; + +public class ManageCitations { + + private ManageCitations() { + /**/ + } + + public static List getCitationEntries(XTextDocument doc) + throws + NoDocumentException, + WrappedTargetException { + OOFrontend frontend = new OOFrontend(doc); + return frontend.getCitationEntries(doc); + } + + public static void applyCitationEntries(XTextDocument doc, List citationEntries) + throws + NoDocumentException, + PropertyVetoException, + IllegalTypeException, + WrappedTargetException, + IllegalArgumentException { + OOFrontend frontend = new OOFrontend(doc); + frontend.applyCitationEntries(doc, citationEntries); + } +} diff --git a/src/main/java/org/jabref/logic/openoffice/action/Update.java b/src/main/java/org/jabref/logic/openoffice/action/Update.java new file mode 100644 index 00000000000..80263833aec --- /dev/null +++ b/src/main/java/org/jabref/logic/openoffice/action/Update.java @@ -0,0 +1,133 @@ +package org.jabref.logic.openoffice.action; + +import java.util.List; + +import org.jabref.logic.openoffice.frontend.OOFrontend; +import org.jabref.logic.openoffice.frontend.UpdateBibliography; +import org.jabref.logic.openoffice.frontend.UpdateCitationMarkers; +import org.jabref.logic.openoffice.style.OOBibStyle; +import org.jabref.logic.openoffice.style.OOProcess; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.openoffice.rangesort.FunctionalTextViewCursor; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.NoDocumentException; +import org.jabref.model.openoffice.uno.UnoScreenRefresh; + +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.text.XTextDocument; + +/** + * Update document: citation marks and bibliography + */ +public class Update { + + static final boolean USE_LOCK_CONTROLLERS = true; + + private Update() { + /**/ + } + + /** + * @return the list of unresolved citation keys + */ + private static List updateDocument(XTextDocument doc, + OOFrontend frontend, + List databases, + OOBibStyle style, + FunctionalTextViewCursor fcursor, + boolean doUpdateBibliography, + boolean alwaysAddCitedOnPages) + throws + CreationException, + NoDocumentException, + WrappedTargetException, + com.sun.star.lang.IllegalArgumentException { + + frontend.imposeGlobalOrder(doc, fcursor); + OOProcess.produceCitationMarkers(frontend.citationGroups, databases, style); + + try { + if (USE_LOCK_CONTROLLERS) { + UnoScreenRefresh.lockControllers(doc); + } + + UpdateCitationMarkers.applyNewCitationMarkers(doc, frontend, style); + + if (doUpdateBibliography) { + UpdateBibliography.rebuildBibTextSection(doc, + frontend, + frontend.citationGroups.getBibliography().get(), + style, + alwaysAddCitedOnPages); + } + + return frontend.citationGroups.getUnresolvedKeys(); + } finally { + if (USE_LOCK_CONTROLLERS && UnoScreenRefresh.hasControllersLocked(doc)) { + UnoScreenRefresh.unlockControllers(doc); + } + } + } + + public static class SyncOptions { + + public final List databases; + boolean updateBibliography; + boolean alwaysAddCitedOnPages; + + public SyncOptions(List databases) { + this.databases = databases; + this.updateBibliography = false; + this.alwaysAddCitedOnPages = false; + } + + public SyncOptions setUpdateBibliography(boolean value) { + this.updateBibliography = value; + return this; + } + + public SyncOptions setAlwaysAddCitedOnPages(boolean value) { + this.alwaysAddCitedOnPages = value; + return this; + } + } + + public static List synchronizeDocument(XTextDocument doc, + OOFrontend frontend, + OOBibStyle style, + FunctionalTextViewCursor fcursor, + SyncOptions syncOptions) + throws + CreationException, + NoDocumentException, + WrappedTargetException, + com.sun.star.lang.IllegalArgumentException { + + return Update.updateDocument(doc, + frontend, + syncOptions.databases, + style, + fcursor, + syncOptions.updateBibliography, + syncOptions.alwaysAddCitedOnPages); + } + + /* + * Reread document before sync + */ + public static List resyncDocument(XTextDocument doc, + OOBibStyle style, + FunctionalTextViewCursor fcursor, + SyncOptions syncOptions) + throws + CreationException, + NoDocumentException, + WrappedTargetException, + com.sun.star.lang.IllegalArgumentException { + + OOFrontend frontend = new OOFrontend(doc); + + return Update.synchronizeDocument(doc, frontend, style, fcursor, syncOptions); + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/notforproduction/TimeLap.java b/src/main/java/org/jabref/model/openoffice/notforproduction/TimeLap.java new file mode 100644 index 00000000000..53a1fc873d7 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/notforproduction/TimeLap.java @@ -0,0 +1,33 @@ +package org.jabref.model.openoffice.notforproduction; + +/* + * Measure execution time. + */ +public final class TimeLap { + + private TimeLap() { + /**/ + } + + /* + * Usage: + * long startTime = TimeLap.start(); + * // call to measure + * startTime = TimeLap.now("label1", startTime); + * // call to measure + * startTime = TimeLap.now("label2", startTime); + */ + // return time (nanoSeconds) for timing + public static long start() { + return System.nanoTime(); + } + + // return time (nanoSeconds) for next timing + @SuppressWarnings("PMD.SystemPrintln") + public static long now(String label, long startTime) { + long endTime = System.nanoTime(); + long duration = (endTime - startTime); // divide by 1000000 to get milliseconds. + System.out.printf("%-40s: %10.3f ms\n", label, duration / 1_000_000.0); + return endTime; + } +} diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index a27e30f60a7..02db45ff108 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -523,6 +523,10 @@ Moved\ group\ "%0".=Moved group "%0". Mr.\ DLib\ Privacy\ settings=Mr. DLib Privacy settings +No\ database\ is\ open=No database is open + +We\ need\ a\ database\ to\ export\ from.\ Open\ one.=We need a database to export from. Open one. + No\ recommendations\ received\ from\ Mr.\ DLib\ for\ this\ entry.=No recommendations received from Mr. DLib for this entry. Error\ while\ fetching\ recommendations\ from\ Mr.DLib.=Error while fetching recommendations from Mr.DLib. @@ -1026,6 +1030,7 @@ Cite\ special=Cite special Extra\ information\ (e.g.\ page\ number)=Extra information (e.g. page number) Manage\ citations=Manage citations Problem\ modifying\ citation=Problem modifying citation +Problem\ collecting\ citations=Problem collecting citations Citation=Citation Connecting...=Connecting... Could\ not\ resolve\ BibTeX\ entry\ for\ citation\ marker\ '%0'.=Could not resolve BibTeX entry for citation marker '%0'. @@ -1040,10 +1045,23 @@ Select\ Writer\ document=Select Writer document Sync\ OpenOffice/LibreOffice\ bibliography=Sync OpenOffice/LibreOffice bibliography Select\ which\ open\ Writer\ document\ to\ work\ on=Select which open Writer document to work on Connected\ to\ document=Connected to document + +Could\ not\ connect\ to\ any\ Writer\ document.\ Please\ make\ sure\ a\ document\ is\ open\ before\ using\ the\ 'Select\ Writer\ document'\ button\ to\ connect\ to\ it.=Could not connect to any Writer document. Please make sure a document is open before using the 'Select Writer document' button to connect to it. + +No\ Writer\ documents\ found=No Writer documents found + Insert\ a\ citation\ without\ text\ (the\ entry\ will\ appear\ in\ the\ reference\ list)=Insert a citation without text (the entry will appear in the reference list) Cite\ selected\ entries\ with\ extra\ information=Cite selected entries with extra information Ensure\ that\ the\ bibliography\ is\ up-to-date=Ensure that the bibliography is up-to-date + Your\ OpenOffice/LibreOffice\ document\ references\ the\ citation\ key\ '%0',\ which\ could\ not\ be\ found\ in\ your\ current\ library.=Your OpenOffice/LibreOffice document references the citation key '%0', which could not be found in your current library. + +This\ operation\ requires\ a\ bibliography\ database.=This operation requires a bibliography database. + +Your\ OpenOffice/LibreOffice\ document\ references\ at\ least\ %0\ citation\ keys\ which\ could\ not\ be\ found\ in\ your\ current\ library.\ Some\ of\ these\ are\ %1.=Your OpenOffice/LibreOffice document references at least %0 citation keys which could not be found in your current library. Some of these are %1. + +Your\ OpenOffice/LibreOffice\ document\ references\ no\ citation\ keys\ which\ could\ also\ be\ found\ in\ your\ current\ library.=Your OpenOffice/LibreOffice document references no citation keys which could also be found in your current library. + Unable\ to\ synchronize\ bibliography=Unable to synchronize bibliography Combine\ pairs\ of\ citations\ that\ are\ separated\ by\ spaces\ only=Combine pairs of citations that are separated by spaces only Autodetection\ failed=Autodetection failed @@ -1051,6 +1069,12 @@ Please\ wait...=Please wait... Connection\ lost=Connection lost The\ paragraph\ format\ is\ controlled\ by\ the\ property\ 'ReferenceParagraphFormat'\ or\ 'ReferenceHeaderParagraphFormat'\ in\ the\ style\ file.=The paragraph format is controlled by the property 'ReferenceParagraphFormat' or 'ReferenceHeaderParagraphFormat' in the style file. The\ character\ format\ is\ controlled\ by\ the\ citation\ property\ 'CitationCharacterFormat'\ in\ the\ style\ file.=The character format is controlled by the citation property 'CitationCharacterFormat' in the style file. + +Not\ connected\ to\ document=Not connected to document +Problem\ combining\ cite\ markers=Problem combining cite markers + +Problem\ during\ separating\ cite\ markers=Problem during separating cite markers + Automatically\ sync\ bibliography\ when\ inserting\ citations=Automatically sync bibliography when inserting citations Look\ up\ BibTeX\ entries\ in\ the\ active\ tab\ only=Look up BibTeX entries in the active tab only Look\ up\ BibTeX\ entries\ in\ all\ open\ libraries=Look up BibTeX entries in all open libraries @@ -1155,7 +1179,9 @@ Toggle\ quality\ assured=Toggle quality assured Toggle\ print\ status=Toggle print status Update\ keywords=Update keywords Write\ values\ of\ special\ fields\ as\ separate\ fields\ to\ BibTeX=Write values of special fields as separate fields to BibTeX +Problem\ connecting=Problem connecting Connection\ to\ OpenOffice/LibreOffice\ has\ been\ lost.\ Please\ make\ sure\ OpenOffice/LibreOffice\ is\ running,\ and\ try\ to\ reconnect.=Connection to OpenOffice/LibreOffice has been lost. Please make sure OpenOffice/LibreOffice is running, and try to reconnect. + JabRef\ will\ send\ at\ least\ one\ request\ per\ entry\ to\ a\ publisher.=JabRef will send at least one request per entry to a publisher. Correct\ the\ entry,\ and\ reopen\ editor\ to\ display/edit\ source.=Correct the entry, and reopen editor to display/edit source. Could\ not\ connect\ to\ running\ OpenOffice/LibreOffice.=Could not connect to running OpenOffice/LibreOffice. @@ -1553,6 +1579,22 @@ Custom=Custom Export\ cited=Export cited Unable\ to\ generate\ new\ library=Unable to generate new library +The\ cursor\ is\ in\ protected\ area.=The cursor is in protected area. +JabRefException=JabRefException +No\ bibliography\ database\ is\ open\ for\ citation.=No bibliography database is open for citation. + +No\ database\ is\ open\ for\ updating\ citation\ markers\ after\ citing.=No database is open for updating citation markers after citing. + +No\ bibliography\ entries\ are\ selected\ for\ citation.=No bibliography entries are selected for citation. +No\ bibliography\ style\ is\ selected\ for\ citation.=No bibliography style is selected for citation. +No\ database=No database + +No\ entries\ selected\ for\ citation=No entries selected for citation +Open\ one\ before\ citing.=Open one before citing. + +Select\ one\ before\ citing.=Select one before citing. +Select\ some\ before\ citing.=Select some before citing. + Found\ identical\ ranges=Found identical ranges Found\ overlapping\ ranges=Found overlapping ranges Found\ touching\ ranges=Found touching ranges @@ -2322,6 +2364,35 @@ Custom\ DOI\ URI=Custom DOI URI Use\ custom\ DOI\ base\ URI\ for\ article\ access=Use custom DOI base URI for article access Cited\ on\ pages=Cited on pages +Please\ move\ the\ cursor\ into\ the\ document\ text.=Please\ move\ the\ cursor\ into\ the\ document\ text. +To\ get\ the\ visual\ positions\ of\ your\ citations\ I\ need\ to\ move\ the\ cursor\ around,\ but\ could\ not\ get\ it.=To\ get\ the\ visual\ positions\ of\ your\ citations\ I\ need\ to\ move\ the\ cursor\ around,\ but\ could\ not\ get\ it. + +I\ cannot\ insert\ to\ the\ cursors\ current\ location.=I\ cannot\ insert\ to\ the\ cursors\ current\ location. + +Please\ move\ the\ cursor\ to\ the\ location\ for\ the\ new\ citation.=Please\ move\ the\ cursor\ to\ the\ location\ for\ the\ new\ citation. + +Please\ create\ it\ in\ the\ document\ or\ change\ in\ the\ file\:=Please create it in the document or change in the file: + +Please\ use\ the\ latter\ in\ the\ style\ file\ below\ to\ avoid\ localization\ problems.=Please use the latter in the style file below to avoid localization problems. + +The\ %0\ character\ style\ '%1'\ is\ a\ display\ name\ for\ '%2'.=The %0 character style '%1' is a display name for '%2'. + +The\ %0\ character\ style\ '%1'\ is\ missing\ from\ the\ document=The %0 character style '%1' is missing from the document + +The\ %0\ paragraph\ style\ '%1'\ is\ a\ display\ name\ for\ '%2'.=The %0 paragraph style '%1' is a display name for '%2'. + +The\ %0\ paragraph\ style\ '%1'\ is\ missing\ from\ the\ document=The %0 paragraph style '%1' is missing from the document + +Error\ while\ checking\ if\ Writer\ is\ recording\ changes\ or\ has\ recorded\ changes.=Error while checking if Writer is recording changes or has recorded changes. + +Cannot\ work\ with\ [Edit]/[Track\ Changes]/[Record]\ turned\ on.=Cannot work with [Edit]/[Track Changes]/[Record] turned on. + +Changes\ by\ JabRef\ could\ result\ in\ unexpected\ interactions\ with\ recorded\ changes.=Changes by JabRef could result in unexpected interactions with recorded changes. + +Recording\ and/or\ Recorded\ changes=Recording and/or Recorded changes + +Use\ [Edit]/[Track\ Changes]/[Manage]\ to\ resolve\ them\ first.=Use [Edit]/[Track Changes]/[Manage] to resolve them first. + Unable\ to\ find\ valid\ certification\ path\ to\ requested\ target(%0),\ download\ anyway?=Unable to find valid certification path to requested target(%0), download anyway? Download\ operation\ canceled.=Download operation canceled.