diff --git a/.gitattributes b/.gitattributes index ce4ff37a5ff..e1c2aa7771a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,3 +24,5 @@ *.ser binary *.png binary *.jpg binary +*.pptx binary +*.svg binary diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a2a30c00fc6..10a896e04b6 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,11 +1,3 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Java CI with Maven on: @@ -14,6 +6,10 @@ on: pull_request: branches: [ "main", "release/**" ] +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + jobs: build: strategy: @@ -36,6 +32,14 @@ jobs: java-version: ${{ matrix.jdk }} distribution: 'temurin' cache: maven + server-id: ukp-oss-snapshots + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Set up Maven + uses: stCarolas/setup-maven@v5 + with: + maven-version: 3.9.9 - name: Set up cache date run: echo "CACHE_DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV @@ -50,9 +54,20 @@ jobs: ${{ runner.os }}-maven- - name: Build with Maven - run: mvn --no-transfer-progress -B clean verify --file pom.xml + if: "!(matrix.os == 'ubuntu-latest' && github.event_name != 'pull_request')" + run: mvn --show-version --batch-mode --no-transfer-progress clean verify + + - name: Build with Maven and publish artifacts + if: matrix.os == 'ubuntu-latest' && github.event_name != 'pull_request' + env: + # `MAVEN_USERNAME` and `MAVEN_PASSWORD` are used in `~/.m2/settings.xml` created by `setup-java` action + MAVEN_USERNAME: ${{ secrets.UKP_MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.UKP_MAVEN_TOKEN }} + run: mvn --show-version --batch-mode --errors --no-transfer-progress -DdeployAtEnd=true -DskipTests clean deploy - # Fails with error message - no idea why... - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - # - name: Update dependency graph - # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + - name: Capture build artifacts + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: inception/inception-app-webapp/target/inception-app-webapp-*-standalone.jar diff --git a/inception/inception-active-learning/src/main/resources/META-INF/asciidoc/user-guide/annotation_activeLearning.adoc b/inception/inception-active-learning/src/main/resources/META-INF/asciidoc/user-guide/annotation_activeLearning.adoc index 1accbfa9082..17071d8e482 100644 --- a/inception/inception-active-learning/src/main/resources/META-INF/asciidoc/user-guide/annotation_activeLearning.adoc +++ b/inception/inception-active-learning/src/main/resources/META-INF/asciidoc/user-guide/annotation_activeLearning.adoc @@ -5,7 +5,7 @@ Active learning is a family of methods which seeks to optimize the learning rate Open the Active Learning sidebar on the left of the screen. You can choose from a list of all layers for which recommenders have been configured and then start an active learning session on that layer. -image::activeLearning2.png[select, 350, 350, align="center"] +image::images/activeLearning2.png[select, 350, 350, align="center"] The system will start showing recommendations, one by one, according to the <> learning strategy. For every recommendation, it shows the related text, the @@ -14,7 +14,7 @@ given score and the closest score calculated for another suggestion made by the One can now _Annotate_, _Reject_ or _Skip_ this recommendation in the Active Learning sidebar: -image::activeLearning3.png[align="center"] +image::images/activeLearning3.png[align="center"] When using the _Annotate_, _Reject_ or _Skip_ buttons, the system automatically jumps to the next suggestion for the user to inspect. However, at times it may be necessary to go back to a recently inspected suggestion in order to review it. The *History* panel shows the 50 most recent actions. Clicking on the text of an item loads it in the main annotation editor. It is also possible to delete items from the history, e.g. wrongly rejected items. diff --git a/inception/inception-agreement/pom.xml b/inception/inception-agreement/pom.xml index b5ec5b18009..f2a2e0b96e2 100644 --- a/inception/inception-agreement/pom.xml +++ b/inception/inception-agreement/pom.xml @@ -183,11 +183,6 @@ inception-io-webanno-tsv test - - de.tudarmstadt.ukp.inception.app - inception-schema - test - org.dkpro.core dkpro-core-api-lexmorph-asl diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementSummary.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementSummary.java index 7c26061f09a..98b363d9f22 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementSummary.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementSummary.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.DoubleStream; @@ -61,7 +62,7 @@ public void merge(AgreementSummary aResult) + "] but encountered [" + aResult.type + "]"); } - if (!feature.equals(aResult.feature)) { + if (!Objects.equals(feature, aResult.feature)) { throw new IllegalArgumentException("All merged results must have the same feature [" + feature + "] but encountered [" + aResult.feature + "]"); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementUtils.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementUtils.java index fc57de2f957..037a2eaa8a3 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementUtils.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/AgreementUtils.java @@ -44,6 +44,7 @@ import org.dkpro.statistics.agreement.coding.CodingAnnotationStudy; import org.dkpro.statistics.agreement.coding.ICodingAnnotationStudy; +import de.tudarmstadt.ukp.clarin.webanno.agreement.measures.AgreementMeasure; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.FullCodingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.Configuration; @@ -106,7 +107,8 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, // This happens in our test cases when we feed the process with uninitialized CASes. // We should just do the right thing here which is: do nothing - if (ts.getType(aType) == null) { + var type = ts.getType(aType); + if (type == null) { // All positions are irrelevant var irrelevantSets = aDiff.getPositions().stream() // .map(aDiff::getConfigurationSet) // @@ -118,13 +120,14 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, } // Check that the feature really exists instead of just getting a NPE later - if (ts.getType(aType).getFeatureByBaseName(aFeature) == null) { + if (aFeature != null && type.getFeatureByBaseName(aFeature) == null) { throw new IllegalArgumentException( "Type [" + aType + "] has no feature called [" + aFeature + "]"); } - var isPrimitiveFeature = ts.getType(aType).getFeatureByBaseName(aFeature).getRange() - .isPrimitive(); + var isPrimitiveFeature = aFeature != null + ? type.getFeatureByBaseName(aFeature).getRange().isPrimitive() + : true; nextPosition: for (var p : aDiff.getPositions()) { var cfgSet = aDiff.getConfigurationSet(p); @@ -136,7 +139,7 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, } // If the feature on a position is set, then it is a subposition - var isSubPosition = p.getFeature() != null; + var isSubPosition = p.getLinkFeature() != null; // Check if this position is irrelevant: // - if we are looking for a primitive type and encounter a subposition @@ -149,7 +152,8 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, // Check if subposition is for the feature we are looking for or for a different // feature - if (isSubPosition && !aFeature.equals(cfgSet.getPosition().getFeature())) { + if (isSubPosition && (aFeature == null + || !aFeature.equals(cfgSet.getPosition().getLinkFeature()))) { cfgSet.addTags(Tag.IRRELEVANT); continue nextPosition; } @@ -203,7 +207,7 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, if (cfg.getPosition() instanceof RelationPosition pos) { var arc = cfg.getFs(user, aCasMap); - var adapter = (RelationDiffAdapter) aDiff.getTypeAdapters().get(pos.getType()); + var adapter = (RelationDiffAdapter) aDiff.getAdapters().get(pos.getType()); // Check if the source of the relation is stacked var source = getFeature(arc, adapter.getSourceFeature(), AnnotationFS.class); @@ -238,8 +242,8 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, // If the position feature is set (subposition), then it must match the feature we // are calculating agreement over - assert cfgSet.getPosition().getFeature() == null - || cfgSet.getPosition().getFeature().equals(aFeature); + assert cfgSet.getPosition().getLinkFeature() == null + || cfgSet.getPosition().getLinkFeature().equals(aFeature); if (!containsAny(cfgSet.getTags(), INCOMPLETE_LABEL, INCOMPLETE_POSITION)) { cfgSet.addTags(COMPLETE); @@ -262,17 +266,20 @@ private static FullCodingAgreementResult makeCodingStudy(CasDiff aDiff, private static Object extractValueForAgreement(Configuration cfg, String user, Map aCasMap, String aFeature) { + if (aFeature == null) { + return AgreementMeasure.POSITION; + } + var fs = cfg.getFs(user, aCasMap); var linkIndex = cfg.getAID(user).index; - var isPrimitiveFeature = fs.getType().getFeatureByBaseName(aFeature).getRange() - .isPrimitive(); + var type = fs.getType(); + var isPrimitiveFeature = type.getFeatureByBaseName(aFeature).getRange().isPrimitive(); // If the feature on a position is set, then it is a subposition var isSubPosition = linkIndex != -1; // BEGIN PARANOIA - assert fs.getType().getFeatureByBaseName(aFeature).getRange() - .isPrimitive() == isPrimitiveFeature; + assert type.getFeatureByBaseName(aFeature).getRange().isPrimitive() == isPrimitiveFeature; // primitive implies not subposition - if this is primitive and subposition, we // should never have gotten here in the first place. assert !isPrimitiveFeature || !isSubPosition; @@ -286,12 +293,12 @@ private static Object extractValueForAgreement(Configuration cfg, String user, if (!isPrimitiveFeature && isSubPosition) { // Link feature / sub-position return extractLinkFeatureValueForAgreement(fs, aFeature, linkIndex, - cfg.getPosition().getLinkCompareBehavior()); + cfg.getPosition().getLinkFeatureMultiplicityMode()); } throw new IllegalStateException("Should never get here: primitive: " - + fs.getType().getFeatureByBaseName(aFeature).getRange().isPrimitive() - + "; subpos: " + isSubPosition); + + type.getFeatureByBaseName(aFeature).getRange().isPrimitive() + "; subpos: " + + isSubPosition); } private static Object extractLinkFeatureValueForAgreement(FeatureStructure aFs, String aFeature, diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure.java index 6c9107557f5..1a9bf6c8301 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure.java @@ -23,11 +23,16 @@ import org.apache.uima.cas.CAS; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public interface AgreementMeasure { + static final String POSITION = ""; + R getAgreement(Map aCasMap); + AnnotationLayer getLayer(); + AnnotationFeature getFeature(); DefaultAgreementTraits getTraits(); diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport.java index ab049eaabe7..19a8e500cb3 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport.java @@ -25,6 +25,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.AgreementResult_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.agreement.FullAgreementResult_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public interface AgreementMeasureSupport aLayer, + IModel aFeature, IModel aModel) + { + return new EmptyPanel(aId); + } + default Panel createTraitsEditor(String aId, IModel aFeature, IModel aModel) { - return new EmptyPanel(aId); + return createTraitsEditor(aId, aFeature.map(AnnotationFeature::getLayer), aFeature, aModel); + } + + default AgreementMeasure createMeasure(AnnotationFeature aFeature, T aTraits) + { + return createMeasure(aFeature.getLayer(), aFeature, aTraits); } - AgreementMeasure createMeasure(AnnotationFeature aFeature, T aTraits); + AgreementMeasure createMeasure(AnnotationLayer aLayer, AnnotationFeature aFeature, + T aTraits); T createTraits(); diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistry.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistry.java index c0d918c4753..6f1c6d4465c 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistry.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistry.java @@ -20,6 +20,7 @@ import java.util.List; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public interface AgreementMeasureSupportRegistry { @@ -28,9 +29,9 @@ public interface AgreementMeasureSupportRegistry AgreementMeasureSupport getAgreementMeasureSupport(String aId); - List> getAgreementMeasureSupports(AnnotationFeature aFeature); - - AgreementMeasure getMeasure(AnnotationFeature aFeature, String aMeasure, - DefaultAgreementTraits aTraits); + List> getAgreementMeasureSupports(AnnotationLayer aLayer, + AnnotationFeature aFeature); + AgreementMeasure getMeasure(AnnotationLayer aLayer, AnnotationFeature aFeature, + String aMeasure, DefaultAgreementTraits aTraits); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistryImpl.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistryImpl.java index b68b4696e94..d93bae9307e 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistryImpl.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupportRegistryImpl.java @@ -35,6 +35,7 @@ import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.support.logging.BaseLoggers; @Component @@ -95,21 +96,20 @@ public AgreementMeasureSupport getAgreementMeasureSupport(String aId) @Override public List> getAgreementMeasureSupports( - AnnotationFeature aFeature) + AnnotationLayer aLayer, AnnotationFeature aFeature) { return agreementMeasures.stream() // - .filter(factory -> factory.accepts(aFeature)) // + .filter(factory -> factory.accepts(aLayer, aFeature)) // .sorted(comparing(AgreementMeasureSupport::getName)) // .collect(toList()); } @Override - public AgreementMeasure getMeasure(AnnotationFeature aFeature, String aMeasure, - DefaultAgreementTraits traits) + public AgreementMeasure getMeasure(AnnotationLayer aLayer, AnnotationFeature aFeature, + String aMeasure, DefaultAgreementTraits traits) { - AgreementMeasureSupport ams = getAgreementMeasureSupport(aMeasure); - - var measure = ams.createMeasure(aFeature, traits); + var ams = getAgreementMeasureSupport(aMeasure); + var measure = ams.createMeasure(aLayer, aFeature, traits); return measure; } } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport_ImplBase.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport_ImplBase.java index 52b6116af97..4ef3f832fff 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport_ImplBase.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureSupport_ImplBase.java @@ -23,6 +23,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.FullAgreementResult_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public abstract class AgreementMeasureSupport_ImplBase aFeature, - IModel aModel) + public Panel createTraitsEditor(String aId, IModel aLayer, + IModel aFeature, IModel aModel) { return new DefaultAgreementTraitsEditor(aId, aFeature, (IModel) aModel); diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure_ImplBase.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure_ImplBase.java index 149b54067e7..aa05158f6c1 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure_ImplBase.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasure_ImplBase.java @@ -20,19 +20,28 @@ import java.io.Serializable; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public abstract class AgreementMeasure_ImplBase implements AgreementMeasure { + private final AnnotationLayer layer; private final AnnotationFeature feature; private final T traits; - public AgreementMeasure_ImplBase(AnnotationFeature aFeature, T aTraits) + public AgreementMeasure_ImplBase(AnnotationLayer aLayer, AnnotationFeature aFeature, T aTraits) { + layer = aLayer; feature = aFeature; traits = aTraits; } + @Override + public AnnotationLayer getLayer() + { + return layer; + } + @Override public AnnotationFeature getFeature() { diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasure.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasure.java index 87eaf557ae7..aab3e457e5e 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasure.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasure.java @@ -19,7 +19,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.agreement.AgreementUtils.makeCodingStudy; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.getDiffAdapters; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.DiffAdapterRegistry.getDiffAdapters; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toCollection; diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasureSupport.java index c341778a37a..268b25b9cd9 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/cohenkappa/CohenKappaAgreementMeasureSupport.java @@ -22,6 +22,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.AbstractCodingAgreementMeasureSupport; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.FullCodingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; public class CohenKappaAgreementMeasureSupport @@ -49,8 +50,8 @@ public String getName() } @Override - public AgreementMeasure createMeasure(AnnotationFeature aFeature, - DefaultAgreementTraits aTraits) + public AgreementMeasure createMeasure(AnnotationLayer aLayer, + AnnotationFeature aFeature, DefaultAgreementTraits aTraits) { return new CohenKappaAgreementMeasure(aFeature, aTraits, annotationService); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasure.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasure.java index 3bfd384a87b..56bbf27ec54 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasure.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasure.java @@ -19,7 +19,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.agreement.AgreementUtils.makeCodingStudy; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.getDiffAdapters; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.DiffAdapterRegistry.getDiffAdapters; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toCollection; diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasureSupport.java index f7881a5f5e4..8a5f4b610c7 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/fleisskappa/FleissKappaAgreementMeasureSupport.java @@ -22,6 +22,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.AbstractCodingAgreementMeasureSupport; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.FullCodingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; public class FleissKappaAgreementMeasureSupport @@ -49,8 +50,8 @@ public String getName() } @Override - public AgreementMeasure createMeasure(AnnotationFeature aFeature, - DefaultAgreementTraits aTraits) + public AgreementMeasure createMeasure(AnnotationLayer aLayer, + AnnotationFeature aFeature, DefaultAgreementTraits aTraits) { return new FleissKappaAgreementMeasure(aFeature, aTraits, annotationService); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasure.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasure.java index b4ee6b2da08..8399f1042f1 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasure.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasure.java @@ -19,7 +19,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.agreement.AgreementUtils.makeCodingStudy; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.getDiffAdapters; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.DiffAdapterRegistry.getDiffAdapters; import static java.lang.Double.NaN; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toCollection; diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasureSupport.java index d29551fd43f..abe4fe3f30c 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalpha/KrippendorffAlphaAgreementMeasureSupport.java @@ -25,6 +25,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.AbstractCodingAgreementMeasureSupport; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.coding.FullCodingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; public class KrippendorffAlphaAgreementMeasureSupport @@ -48,19 +49,19 @@ public String getId() @Override public String getName() { - return "Krippendorff's Kappa (coding / nominal)"; + return "Krippendorff's Alpha (coding / nominal)"; } @Override - public AgreementMeasure createMeasure(AnnotationFeature aFeature, - DefaultAgreementTraits aTraits) + public AgreementMeasure createMeasure(AnnotationLayer aLayer, + AnnotationFeature aFeature, DefaultAgreementTraits aTraits) { return new KrippendorffAlphaAgreementMeasure(aFeature, aTraits, annotationService); } @Override - public Panel createTraitsEditor(String aId, IModel aFeature, - IModel aModel) + public Panel createTraitsEditor(String aId, IModel aLayer, + IModel aFeature, IModel aModel) { return new KrippendorffAlphaAgreementTraitsEditor(aId, aFeature, aModel); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasure.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasure.java index 25204cd9c0a..bfaab1a8046 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasure.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasure.java @@ -35,6 +35,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.measures.DefaultAgreementTraits; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.unitizing.FullUnitizingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; public class KrippendorffAlphaUnitizingAgreementMeasure extends AgreementMeasure_ImplBase aCasMap) { - var typeName = getFeature().getLayer().getName(); + var typeName = getLayer().getName(); // Calculate a character offset continuum. We assume here that the documents // all have the same size - since the users cannot change the document sizes, this should be @@ -79,9 +80,9 @@ public FullUnitizingAgreementResult getAgreement(Map aCasMap) } var raterIdx = study.addRater(set.getKey()); - var f = t.getFeatureByBaseName(getFeature().getName()); + var f = getFeature() != null ? t.getFeatureByBaseName(getFeature().getName()) : null; for (var ann : cas. select(t)) { - var featureValue = FSUtil.getFeature(ann, f, Object.class); + var featureValue = f != null ? FSUtil.getFeature(ann, f, Object.class) : POSITION; if (featureValue instanceof Collection) { for (var value : (Collection) featureValue) { study.addUnit(ann.getBegin(), ann.getEnd() - ann.getBegin(), raterIdx, @@ -98,7 +99,8 @@ public FullUnitizingAgreementResult getAgreement(Map aCasMap) LOG.trace("Units in study : {}", study.getUnitCount()); LOG.trace("Raters im study: {}", study.getRaterCount()); - var result = new FullUnitizingAgreementResult(typeName, getFeature().getName(), study, + var featureName = getFeature() != null ? getFeature().getName() : null; + var result = new FullUnitizingAgreementResult(typeName, featureName, study, new ArrayList<>(aCasMap.keySet()), getTraits().isExcludeIncomplete()); if (result.isEmpty()) { diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasureSupport.java index 9bb1e6d713f..f6c0c0303ed 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/krippendorffalphaunitizing/KrippendorffAlphaUnitizingAgreementMeasureSupport.java @@ -32,6 +32,7 @@ import de.tudarmstadt.ukp.clarin.webanno.agreement.results.unitizing.FullUnitizingAgreementResult; import de.tudarmstadt.ukp.clarin.webanno.agreement.results.unitizing.PairwiseUnitizingAgreementTable; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; public class KrippendorffAlphaUnitizingAgreementMeasureSupport @@ -55,11 +56,9 @@ public String getName() } @Override - public boolean accepts(AnnotationFeature aFeature) + public boolean accepts(AnnotationLayer aLayer, AnnotationFeature aFeature) { - var layer = aFeature.getLayer(); - - if (SpanLayerSupport.TYPE.equals(layer.getType())) { + if (SpanLayerSupport.TYPE.equals(aLayer.getType())) { return true; } @@ -67,15 +66,15 @@ public boolean accepts(AnnotationFeature aFeature) } @Override - public AgreementMeasure createMeasure(AnnotationFeature aFeature, - DefaultAgreementTraits aTraits) + public AgreementMeasure createMeasure(AnnotationLayer aLayer, + AnnotationFeature aFeature, DefaultAgreementTraits aTraits) { - return new KrippendorffAlphaUnitizingAgreementMeasure(aFeature, aTraits); + return new KrippendorffAlphaUnitizingAgreementMeasure(aLayer, aFeature, aTraits); } @Override - public Panel createTraitsEditor(String aId, IModel aFeature, - IModel aModel) + public Panel createTraitsEditor(String aId, IModel aLayer, + IModel aFeature, IModel aModel) { return new KrippendorffAlphaUnitizingAgreementTraitsEditor(aId, aFeature, aModel); } diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/AbstractCodingAgreementMeasureSupport.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/AbstractCodingAgreementMeasureSupport.java index 66d5771a397..7feaa2bae9e 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/AbstractCodingAgreementMeasureSupport.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/AbstractCodingAgreementMeasureSupport.java @@ -45,17 +45,31 @@ public abstract class AbstractCodingAgreementMeasureSupport { @Override - public boolean accepts(AnnotationFeature aFeature) + public boolean accepts(AnnotationLayer aLayer, AnnotationFeature aFeature) { - AnnotationLayer layer = aFeature.getLayer(); + if (aFeature == null) { + return false; + } + + if (!asList(SpanLayerSupport.TYPE, RelationLayerSupport.TYPE, + DocumentMetadataLayerSupport.TYPE).contains(aLayer.getType())) { + return false; + } + + if (!asList(SINGLE_TOKEN, TOKENS, SENTENCES).contains(aLayer.getAnchoringMode())) { + return false; + } + + if (aFeature != null) { + // Link features are supported (because the links generate sub-positions in the diff + // but multi-value primitives (e.g. multi-value strings) are not supported + if (aFeature.getMultiValueMode() != MultiValueMode.NONE + && aFeature.getLinkMode() == NONE) { + return false; + } + } - return asList(SpanLayerSupport.TYPE, RelationLayerSupport.TYPE, - DocumentMetadataLayerSupport.TYPE).contains(layer.getType()) - && asList(SINGLE_TOKEN, TOKENS, SENTENCES).contains(layer.getAnchoringMode()) - // Link features are supported (because the links generate sub-positions in the diff - // but multi-value primitives (e.g. multi-value strings) are not supported - && (aFeature.getMultiValueMode() == MultiValueMode.NONE - || aFeature.getLinkMode() != NONE); + return true; } @Override diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/CodingAgreementMeasure_ImplBase.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/CodingAgreementMeasure_ImplBase.java index e56bcb00da5..2f8c29bbf69 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/CodingAgreementMeasure_ImplBase.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/results/coding/CodingAgreementMeasure_ImplBase.java @@ -26,6 +26,6 @@ public abstract class CodingAgreementMeasure_ImplBase> { private List annotators; private DefaultAgreementTraits traits; + private AnnotationLayer layer; private AnnotationFeature feature; private AgreementMeasure measure; private Map> allAnnDocs; @@ -251,6 +253,13 @@ public T withTraits(DefaultAgreementTraits aTraits) return (T) this; } + @SuppressWarnings("unchecked") + public T withLayer(AnnotationLayer aLayer) + { + layer = aLayer; + return (T) this; + } + @SuppressWarnings("unchecked") public T withFeature(AnnotationFeature aFeature) { diff --git a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/task/CalculatePerDocumentAgreementTask.java b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/task/CalculatePerDocumentAgreementTask.java index 01742750577..df3de95214a 100644 --- a/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/task/CalculatePerDocumentAgreementTask.java +++ b/inception/inception-agreement/src/main/java/de/tudarmstadt/ukp/clarin/webanno/agreement/task/CalculatePerDocumentAgreementTask.java @@ -47,6 +47,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; @@ -183,6 +184,7 @@ public static class Builder> { private List annotators; private DefaultAgreementTraits traits; + private AnnotationLayer layer; private AnnotationFeature feature; private AgreementMeasure measure; private Map> allAnnDocs; @@ -207,6 +209,13 @@ public T withTraits(DefaultAgreementTraits aTraits) return (T) this; } + @SuppressWarnings("unchecked") + public T withLayer(AnnotationLayer aLayer) + { + layer = aLayer; + return (T) this; + } + @SuppressWarnings("unchecked") public T withFeature(AnnotationFeature aFeature) { diff --git a/inception/inception-agreement/src/test/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureTestSuite_ImplBase.java b/inception/inception-agreement/src/test/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureTestSuite_ImplBase.java index 3349a94b4fb..3fbb8740121 100644 --- a/inception/inception-agreement/src/test/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureTestSuite_ImplBase.java +++ b/inception/inception-agreement/src/test/java/de/tudarmstadt/ukp/clarin/webanno/agreement/measures/AgreementMeasureTestSuite_ImplBase.java @@ -65,12 +65,12 @@ import de.tudarmstadt.ukp.inception.annotation.feature.number.NumberFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistryImpl; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder; @ExtendWith(MockitoExtension.class) diff --git a/inception/inception-annotation-storage-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/casstorage/CasStorageServiceAction.java b/inception/inception-annotation-storage-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/casstorage/CasStorageServiceAction.java index 4bdeb97f06a..a6131b491f0 100644 --- a/inception/inception-annotation-storage-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/casstorage/CasStorageServiceAction.java +++ b/inception/inception-annotation-storage-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/casstorage/CasStorageServiceAction.java @@ -19,8 +19,10 @@ import org.apache.uima.cas.CAS; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + @FunctionalInterface public interface CasStorageServiceAction { - void apply(CAS aCas) throws Exception; + void apply(SourceDocument aDocument, String aDataOwner, CAS aCas) throws Exception; } diff --git a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java index 91abb9b388c..da6d93314a2 100644 --- a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java +++ b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java @@ -76,7 +76,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.ConcurentCasModificationException; import de.tudarmstadt.ukp.clarin.webanno.diag.CasDoctor; import de.tudarmstadt.ukp.clarin.webanno.diag.CasDoctorException; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.storage.config.CasStorageCacheProperties; import de.tudarmstadt.ukp.inception.annotation.storage.config.CasStorageServiceAutoConfiguration; @@ -679,34 +678,11 @@ public boolean deleteCas(SourceDocument aDocument, String aUsername) } @Override - public void analyzeAndRepair(SourceDocument aDocument, String aUsername, CAS aCas) + public void analyzeAndRepair(SourceDocument aDocument, String aDataOwner, CAS aCas) { - analyzeAndRepair(aDocument.getProject(), aDocument.getName(), aDocument.getId(), aUsername, - aCas); - } + var project = aDocument.getProject(); - /** - * Runs {@link CasDoctor} in repair mode on the given CAS (if repairs are active), otherwise it - * runs only in analysis mode. - *

- * Note: {@link CasDoctor} is an optional service. If no {@link CasDoctor} implementation - * is available, this method returns without doing anything. - * - * @param aProject - * the project - * @param aDocumentName - * the document name (used for logging) - * @param aDocumentId - * the aDocument ID (used for logging) - * @param aUsername - * the user owning the CAS (used for logging) - * @param aCas - * the CAS object - */ - private void analyzeAndRepair(Project aProject, String aDocumentName, long aDocumentId, - String aUsername, CAS aCas) - { - try (var logCtx = withProjectLogger(aProject)) { + try (var logCtx = withProjectLogger(project)) { if (casDoctor == null) { return; } @@ -715,18 +691,18 @@ private void analyzeAndRepair(Project aProject, String aDocumentName, long aDocu // because the repairs do an analysis as a pre- and post-condition. if (casDoctor.isRepairsActive()) { try { - casDoctor.repair(aProject, aCas); + casDoctor.repair(aDocument, aDataOwner, aCas); } catch (Exception e) { - throw new DataRetrievalFailureException("Error repairing CAS of user [" - + aUsername + "] for document [" + aDocumentName + "] (" + aDocumentId - + ") in project[" + aProject.getName() + "] (" + aProject.getId() + ")", + throw new DataRetrievalFailureException( + "Error repairing CAS of user [" + aDataOwner + "] for document " + + aDocument + " in project " + aDocument.getProject(), e); } } // If the repairs are not active, then we run the analysis explicitly else { - analyze(aProject, aDocumentName, aDocumentId, aUsername, aCas); + analyze(aDocument, aDataOwner, aCas); } } } @@ -737,59 +713,55 @@ private void analyzeAndRepair(Project aProject, String aDocumentName, long aDocu * Note: {@link CasDoctor} is an optional service. If no {@link CasDoctor} implementation * is available, this method returns without doing anything. * - * @param aProject - * the project - * @param aDocumentName - * the document name (used for logging) - * @param aDocumentId - * the aDocument ID (used for logging) - * @param aUsername + * @param aDocument + * the document + * @param aDataOwner * the user owning the CAS (used for logging) * @param aCas * the CAS object */ - private void analyze(Project aProject, String aDocumentName, long aDocumentId, String aUsername, - CAS aCas) + private void analyze(SourceDocument aDocument, String aDataOwner, CAS aCas) { if (casDoctor == null) { return; } + var project = aDocument.getProject(); + try { - casDoctor.analyze(aProject, aCas); + casDoctor.analyze(aDocument, aDataOwner, aCas); } catch (CasDoctorException e) { var detailMsg = new StringBuilder(); - detailMsg.append("CAS Doctor found problems for user [").append(aUsername) - .append("] in document [").append(aDocumentName).append("] (") - .append(aDocumentId).append(") in project [").append(aProject.getName()) - .append("] (").append(aProject.getId()).append(")\n"); + detailMsg.append("CAS Doctor found problems for user [").append(aDataOwner) + .append("] in document ").append(aDocument).append(" in project ") + .append(project).append("\n"); e.getDetails().forEach( m -> detailMsg.append(String.format("- [%s] %s%n", m.level, m.message))); throw new DataRetrievalFailureException(detailMsg.toString()); } catch (Exception e) { - throw new DataRetrievalFailureException("Error analyzing CAS of user [" + aUsername - + "] in document [" + aDocumentName + "] (" + aDocumentId + ") in project[" - + aProject.getName() + "] (" + aProject.getId() + ")", e); + throw new DataRetrievalFailureException("Error analyzing CAS of user [" + aDataOwner + + "] in document " + aDocument + " in project " + project, e); } } @Override - public void exportCas(SourceDocument aDocument, String aUser, OutputStream aStream) + public void exportCas(SourceDocument aDocument, String aDataOwner, OutputStream aStream) throws IOException { // Ensure that the CAS is not being re-written and temporarily unavailable while we export // it, then add this info to a mini-session to ensure that write-access is known try (var session = CasStorageSession.openNested(true)) { - try (var access = new WithExclusiveAccess(aDocument, aUser)) { - session.add(aDocument.getId(), aUser, EXCLUSIVE_WRITE_ACCESS, access.getHolder()); + try (var access = new WithExclusiveAccess(aDocument, aDataOwner)) { + session.add(aDocument.getId(), aDataOwner, EXCLUSIVE_WRITE_ACCESS, + access.getHolder()); - driver.exportCas(aDocument, aUser, aStream); + driver.exportCas(aDocument, aDataOwner, aStream); } finally { - session.remove(aDocument.getId(), aUser); + session.remove(aDocument.getId(), aDataOwner); } } catch (IOException e) { @@ -801,19 +773,20 @@ public void exportCas(SourceDocument aDocument, String aUser, OutputStream aStre } @Override - public void importCas(SourceDocument aDocument, String aUser, InputStream aStream) + public void importCas(SourceDocument aDocument, String aDataOwner, InputStream aStream) throws IOException { // Ensure that the CAS is not being re-written and temporarily unavailable while we export // it, then add this info to a mini-session to ensure that write-access is known try (var session = CasStorageSession.openNested(true)) { - try (var access = new WithExclusiveAccess(aDocument, aUser)) { - session.add(aDocument.getId(), aUser, EXCLUSIVE_WRITE_ACCESS, access.getHolder()); + try (var access = new WithExclusiveAccess(aDocument, aDataOwner)) { + session.add(aDocument.getId(), aDataOwner, EXCLUSIVE_WRITE_ACCESS, + access.getHolder()); - driver.importCas(aDocument, aUser, aStream); + driver.importCas(aDocument, aDataOwner, aStream); } finally { - session.remove(aDocument.getId(), aUser); + session.remove(aDocument.getId(), aDataOwner); } } catch (IOException e) { @@ -825,39 +798,40 @@ public void importCas(SourceDocument aDocument, String aUser, InputStream aStrea } @Override - public void upgradeCas(SourceDocument aDocument, String aUser) throws IOException + public void upgradeCas(SourceDocument aDocument, String aDataOwner) throws IOException { Validate.notNull(aDocument, "Source document must be specified"); - Validate.notBlank(aUser, "User must be specified"); + Validate.notBlank(aDataOwner, "Data owner must be specified"); - forceActionOnCas(aDocument, aUser, // + forceActionOnCas(aDocument, aDataOwner, // (doc, user) -> driver.readCas(doc, user), - (cas) -> schemaService.upgradeCas(cas, aDocument, aUser), // + (doc, user, cas) -> schemaService.upgradeCas(cas, doc, user), // true); } @Override - public void forceActionOnCas(SourceDocument aDocument, String aUser, + public void forceActionOnCas(SourceDocument aDocument, String aDataOwner, CasStorageServiceLoader aLoader, CasStorageServiceAction aAction, boolean aSave) throws IOException { // Ensure that the CAS is not being re-written and temporarily unavailable while we check // upgrade it, then add this info to a mini-session to ensure that write-access is known try (var session = CasStorageSession.openNested(true)) { - try (var access = new WithExclusiveAccess(aDocument, aUser)) { - session.add(aDocument.getId(), aUser, EXCLUSIVE_WRITE_ACCESS, access.getHolder()); + try (var access = new WithExclusiveAccess(aDocument, aDataOwner)) { + session.add(aDocument.getId(), aDataOwner, EXCLUSIVE_WRITE_ACCESS, + access.getHolder()); - var cas = aLoader.load(aDocument, aUser); + var cas = aLoader.load(aDocument, aDataOwner); access.setCas(cas); - aAction.apply(cas); + aAction.apply(aDocument, aDataOwner, cas); if (aSave) { - realWriteCas(aDocument, aUser, cas); + realWriteCas(aDocument, aDataOwner, cas); } } finally { - session.remove(aDocument.getId(), aUser); + session.remove(aDocument.getId(), aDataOwner); } } catch (IOException e) { @@ -1118,7 +1092,7 @@ public void beforeLayerConfigurationChanged(LayerConfigurationChangedEvent aEven private void realWriteCas(SourceDocument aDocument, String aUserName, CAS aCas) throws IOException { - analyze(aDocument.getProject(), aDocument.getName(), aDocument.getId(), aUserName, aCas); + analyze(aDocument, aUserName, aCas); if (CasStorageSession.exists()) { var session = CasStorageSession.get(); diff --git a/inception/inception-api-annotation/pom.xml b/inception/inception-api-annotation/pom.xml index 7d6c98f5953..578886590ec 100644 --- a/inception/inception-api-annotation/pom.xml +++ b/inception/inception-api-annotation/pom.xml @@ -108,6 +108,14 @@ org.dkpro.core dkpro-core-api-segmentation-asl + + org.dkpro.core + dkpro-core-api-ner-asl + + + org.dkpro.core + dkpro-core-api-lexmorph-asl + commons-io @@ -230,21 +238,11 @@ uimaj-document-annotation test - - org.dkpro.core - dkpro-core-api-ner-asl - test - org.dkpro.core dkpro-core-api-syntax-asl test - - org.dkpro.core - dkpro-core-api-lexmorph-asl - test - org.dkpro.core dkpro-core-api-coref-asl diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/actionbar/ActionBar.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/actionbar/ActionBar.java index b73a2a99f2b..285620f531d 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/actionbar/ActionBar.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/actionbar/ActionBar.java @@ -66,8 +66,9 @@ protected void onInitialize() { super.onInitialize(); + var page = (AnnotationPageBase) getPage(); for (var ext : getExtensions()) { - ext.onInitialize((AnnotationPageBase) getPage()); + ext.onInitialize(page); activeExtensions.add(ext.getId()); } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/config/AnnotationAutoConfiguration.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/config/AnnotationAutoConfiguration.java index d09856943de..21c31dfdf3b 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/config/AnnotationAutoConfiguration.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/config/AnnotationAutoConfiguration.java @@ -17,10 +17,13 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.config; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.coloring.ColoringServiceImpl; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.preferences.AnnotationEditorDefaultPreferencesProperties; @@ -35,6 +38,10 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.RenderNotificationRenderStep; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationEndpointFeatureSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.span.TokenAttachedSpanChangeListener; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemExtension; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemRegistry; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemRegistryImpl; import de.tudarmstadt.ukp.inception.documents.api.RepositoryProperties; import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.rendering.coloring.ColoringService; @@ -103,4 +110,18 @@ public RelationEndpointFeatureSupport relationEndpointFeatureSupport() { return new RelationEndpointFeatureSupport(); } + + @Bean + public TokenAttachedSpanChangeListener tokenAttachedSpanChangeListener( + AnnotationSchemaService aSchemaService) + { + return new TokenAttachedSpanChangeListener(aSchemaService); + } + + @Bean + ContextMenuItemRegistry contextMenuItemRegistry( + @Lazy @Autowired(required = false) List aExtensions) + { + return new ContextMenuItemRegistryImpl(aExtensions); + } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java index 59c9c423be6..9c226244a4f 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import org.apache.uima.cas.CAS; @@ -61,6 +62,7 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase; import de.tudarmstadt.ukp.inception.documents.api.DocumentAccess; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; +import de.tudarmstadt.ukp.inception.editor.ContextMenuLookup; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; @@ -335,6 +337,8 @@ protected void handleException(AjaxRequestTarget aTarget, Exception aException) public abstract AnnotationActionHandler getAnnotationActionHandler(); + public abstract Optional getContextMenuLookup(); + public abstract void writeEditorCas(CAS aCas) throws IOException, AnnotationException; /** diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategy.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategy.java index 0d254b01ec9..3b907fee303 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategy.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategy.java @@ -17,10 +17,14 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging; -import static org.apache.commons.lang3.StringUtils.splitPreserveAllTokens; +import static java.lang.String.join; +import static java.util.Arrays.asList; +import static java.util.regex.Pattern.compile; +import static java.util.stream.Collectors.joining; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import org.apache.uima.cas.CAS; import org.apache.wicket.Component; @@ -37,22 +41,45 @@ public class LineOrientedPagingStrategy { private static final long serialVersionUID = -991967885210129525L; + static final String CR = "\r"; // carriage return (CR) (classic Mac) + static final String LF = "\n"; // line feed (LF) (Unix) + static final String CRLF = "\r\n"; // CRLF (Windows) + static final String NEL = "\u0085"; // Next Line (NEL) + static final String LINE_SEPARATOR = "\u2028"; // Line Separator + static final String PARAGRAPH_SEPARATOR = "\u2029"; // Paragraph Separator + + // Mind that CRLF must come before CR and LF here so it matches as a unit! + static final List LINE_SEPARATORS = asList(CRLF, CR, LF, NEL, LINE_SEPARATOR, + PARAGRAPH_SEPARATOR); + + static final Pattern LINE_PATTERN = compile("[^" + join("", LINE_SEPARATORS) + "]+" // + + "|" + join("|", LINE_SEPARATORS)); + static final Pattern LINE_SPLITTER_PATTERN = compile(LINE_SEPARATORS.stream() // + .map(Pattern::quote) // + .collect(joining("|"))); + @Override public List units(CAS aCas, int aFirstIndex, int aLastIndex) { - // We need to preserve all tokens so we can add a +1 for the line breaks of empty lines. - String[] lines = splitPreserveAllTokens(aCas.getDocumentText(), '\n'); + var text = aCas.getDocumentText(); + var matcher = LINE_SPLITTER_PATTERN.matcher(text); - List units = new ArrayList<>(); - int beginOffset = 0; - for (int i = 0; i < Math.min(lines.length, aLastIndex); i++) { + var unitStart = 0; + var unitEnd = 0; + var index = 1; - if (i >= aFirstIndex) { - units.add(new Unit(i + 1, beginOffset, beginOffset + lines[i].length())); - } + var units = new ArrayList(); + while (matcher.find()) { + unitEnd = matcher.start(); + units.add(new Unit(index, unitStart, unitEnd)); + unitStart = matcher.end(); + index++; + } - // The +1 below accounts for the line break which is not included in the token - beginOffset += lines[i].length() + 1; + if (unitStart < text.length()) { + if (!text.substring(unitStart).isBlank()) { + units.add(new Unit(index, unitStart, text.length())); + } } return units; @@ -61,8 +88,8 @@ public List units(CAS aCas, int aFirstIndex, int aLastIndex) @Override public Component createPositionLabel(String aId, IModel aModel) { - Label label = new Label(aId, () -> { - AnnotatorState state = aModel.getObject(); + var label = new Label(aId, () -> { + var state = aModel.getObject(); return String.format("%d-%d / %d lines [doc %d / %d]", state.getFirstVisibleUnitIndex(), state.getLastVisibleUnitIndex(), state.getUnitCount(), state.getDocumentIndex() + 1, state.getNumberOfDocuments()); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategy.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategy.java index 5a9090909b1..ce513b58728 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategy.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategy.java @@ -17,23 +17,22 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.LINE_SPLITTER_PATTERN; import static java.lang.String.format; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.text.AnnotationFS; import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.paging.Unit; -import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; public class TokenWrappingPagingStrategy extends PagingStrategy_ImplBase @@ -50,14 +49,14 @@ public TokenWrappingPagingStrategy(int aMaxLineLength) @Override public List units(CAS aCas, int aFirstIndex, int aLastIndex) { - Iterator tokenIterator = WebAnnoCasUtil.selectTokens(aCas).iterator(); + var tokenIterator = aCas.select(Token.class).iterator(); var units = new ArrayList(); int currentUnitStart = 0; int currentUnitEnd = 0; while (tokenIterator.hasNext()) { - AnnotationFS currentToken = tokenIterator.next(); + var currentToken = tokenIterator.next(); if (currentToken.getBegin() < currentUnitEnd) { throw new IllegalStateException(format( @@ -65,14 +64,14 @@ public List units(CAS aCas, int aFirstIndex, int aLastIndex) currentToken.getBegin(), currentToken.getEnd(), currentUnitEnd)); } + // Add units for each of the lines in the gap var gap = aCas.getDocumentText().substring(currentUnitEnd, currentToken.getBegin()); int gapStart = currentUnitEnd; - int lineBreakIndex = gap.indexOf("\n"); - while (lineBreakIndex > -1) { - currentUnitEnd = gapStart + lineBreakIndex; + var matcher = LINE_SPLITTER_PATTERN.matcher(gap); + while (matcher.find()) { + currentUnitEnd = gapStart + matcher.start(); units.add(new Unit(units.size() + 1, currentUnitStart, currentUnitEnd)); - currentUnitStart = currentUnitEnd + 1; // +1 because of the line break character - lineBreakIndex = gap.indexOf("\n", lineBreakIndex + 1); + currentUnitStart = gapStart + matcher.end(); } var unitNonEmpty = (currentUnitEnd - currentUnitStart) > 0; @@ -92,10 +91,28 @@ public List units(CAS aCas, int aFirstIndex, int aLastIndex) currentUnitEnd = currentToken.getEnd(); } + // Finish current unit if (currentUnitEnd - currentUnitStart > 0) { units.add(new Unit(units.size() + 1, currentUnitStart, currentUnitEnd)); + currentUnitStart = -1; } + // // Add any line breaks at the end of the document + // if (aCas.getDocumentText().length() - currentUnitEnd > 0) { + // var gap = aCas.getDocumentText().substring(currentUnitEnd, + // aCas.getDocumentText().length()); + // int gapStart = currentUnitEnd; + // var matcher = LINE_SPLITTER_PATTERN.matcher(gap); + // while (matcher.find()) { + // currentUnitEnd = gapStart + matcher.start(); + // if (currentUnitStart == -1) { + // currentUnitStart = currentUnitEnd; + // } + // units.add(new Unit(units.size() + 1, currentUnitStart, currentUnitEnd)); + // currentUnitStart = gapStart + matcher.end(); + // } + // } + return units; } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/PreRendererImpl.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/PreRendererImpl.java index 35774b75ad0..f58cdfdf2ac 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/PreRendererImpl.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/PreRendererImpl.java @@ -98,7 +98,7 @@ public void render(VDocument aResponse, RenderRequest aRequest) Validate.notNull(cas, "CAS cannot be null"); - if (aRequest.getVisibleLayers().isEmpty() || isEmpty(documentText)) { + if (isEmpty(documentText)) { return; } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureDiffMode.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureDiffMode.java new file mode 100644 index 00000000000..13bb37f8e1b --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureDiffMode.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.feature.link; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum LinkFeatureDiffMode +{ + /** + * The link target is considered to be part of the position. Two links that have the same target + * will be considered to be at the same position. Thus, linking the same target in multiple + * roles will be considered stacking. Linking different targets in the same role is viable. + */ + @JsonProperty("include") + INCLUDE, + + /** + * The link role is considered to be part of the position. Two links that have the same role but + * different targets it will be considered stacking. + */ + @JsonProperty("exclude") + EXCLUDE; + + public static final LinkFeatureDiffMode DEFAULT_LINK_DIFF_MODE = EXCLUDE; +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureMultiplicityMode.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureMultiplicityMode.java index 05ca6ba1b1a..78c12b9c15f 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureMultiplicityMode.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureMultiplicityMode.java @@ -37,11 +37,11 @@ public enum LinkFeatureMultiplicityMode ONE_TARGET_MULTIPLE_ROLES, /** - * Include role and target into considered part of the position. There can be multiple links to - * the same target with the same or different roles. + * Role and target are considered to be part of the position. There can be multiple links to the + * same target with the same or different roles. */ @JsonProperty("n-targets-n-roles") MULTIPLE_TARGETS_MULTIPLE_ROLES; - public static final LinkFeatureMultiplicityMode DEFAULT_LINK_MULTIPLICITY = ONE_TARGET_MULTIPLE_ROLES; + public static final LinkFeatureMultiplicityMode DEFAULT_LINK_MULTIPLICITY_MODE = ONE_TARGET_MULTIPLE_ROLES; } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java index cad7a88f2d2..644c2d2f657 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java @@ -19,6 +19,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.LinkMode.WITH_ROLE; import static de.tudarmstadt.ukp.clarin.webanno.model.MultiValueMode.ARRAY; +import static de.tudarmstadt.ukp.inception.schema.api.feature.MaterializedLink.toMaterializedLink; import static org.apache.commons.collections4.CollectionUtils.disjunction; import static org.apache.uima.cas.CAS.TYPE_NAME_FS_ARRAY; import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; @@ -61,7 +62,6 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureType; import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; -import de.tudarmstadt.ukp.inception.schema.api.feature.MaterializedLink; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; /** @@ -243,6 +243,7 @@ public void setFeatureValue(CAS aCas, AnnotationFeature aFeature, int aAddress, FeatureSupport.super.setFeatureValue(aCas, aFeature, aAddress, aValue); } + @SuppressWarnings("unchecked") @Override public List unwrapFeatureValue(AnnotationFeature aFeature, CAS aCAS, Object aValue) @@ -327,12 +328,14 @@ public boolean isFeatureValueEqual(AnnotationFeature aFeature, FeatureStructure FeatureStructure aFS2) { List links1 = getFeatureValue(aFeature, aFS1); - var matLinks1 = links1.stream() - .map(link -> MaterializedLink.toMaterializedLink(aFS1, aFeature, link)).toList(); + var matLinks1 = links1.stream() // + .map(link -> toMaterializedLink(aFS1, aFeature, link))// + .toList(); List links2 = getFeatureValue(aFeature, aFS2); - var matLinks2 = links2.stream() - .map(link -> MaterializedLink.toMaterializedLink(aFS2, aFeature, link)).toList(); + var matLinks2 = links2.stream()// + .map(link -> toMaterializedLink(aFS2, aFeature, link))// + .toList(); return disjunction(matLinks1, matLinks2).isEmpty(); } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraits.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraits.java index 6353670844c..0f30d4f09b1 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraits.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraits.java @@ -17,13 +17,15 @@ */ package de.tudarmstadt.ukp.inception.annotation.feature.link; -import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.DEFAULT_LINK_MULTIPLICITY; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.DEFAULT_LINK_DIFF_MODE; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.DEFAULT_LINK_MULTIPLICITY_MODE; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; /** * Traits for link features. @@ -34,9 +36,17 @@ public class LinkFeatureTraits { private static final long serialVersionUID = -8450181605003189055L; + @JsonProperty("defaultSlots") private List defaultSlots = new ArrayList<>(); + + @JsonProperty("enableRoleLabels") private boolean enableRoleLabels = true; - private LinkFeatureMultiplicityMode compareMode = DEFAULT_LINK_MULTIPLICITY; + + @JsonProperty("compareMode") + private LinkFeatureMultiplicityMode compareMode = DEFAULT_LINK_MULTIPLICITY_MODE; + + @JsonProperty("diffMode") + private LinkFeatureDiffMode diffMode = DEFAULT_LINK_DIFF_MODE; public LinkFeatureTraits() { @@ -63,13 +73,23 @@ public void setEnableRoleLabels(boolean aEnableRoleLabels) enableRoleLabels = aEnableRoleLabels; } - public LinkFeatureMultiplicityMode getCompareMode() + public LinkFeatureMultiplicityMode getMultiplicityMode() { return compareMode; } - public void setCompareMode(LinkFeatureMultiplicityMode aCompareMode) + public void setMultiplicityMode(LinkFeatureMultiplicityMode aCompareMode) { compareMode = aCompareMode; } + + public LinkFeatureDiffMode getDiffMode() + { + return diffMode; + } + + public void setDiffMode(LinkFeatureDiffMode aDiffMode) + { + diffMode = aDiffMode; + } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraitsEditor.html b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraitsEditor.html index 0184d0fc8ee..f38c824124a 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraitsEditor.html +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureTraitsEditor.html @@ -29,6 +29,14 @@ +

+ +
+ - +
+
+
+
+ + +
-
- -
+
@@ -37,7 +37,7 @@
-
+
diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/NumberFeatureTraitsEditor.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/NumberFeatureTraitsEditor.java index d4945d09e28..77176ca8216 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/NumberFeatureTraitsEditor.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/NumberFeatureTraitsEditor.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.annotation.feature.number; +import static de.tudarmstadt.ukp.inception.annotation.feature.number.NumberFeatureTraits.EditorType.SPINNER; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; import java.io.Serializable; @@ -31,7 +33,6 @@ import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; -import org.apache.wicket.model.PropertyModel; import org.apache.wicket.spring.injection.annot.SpringBean; import org.wicketstuff.jquery.core.Options; import org.wicketstuff.kendo.ui.form.NumberTextField; @@ -70,7 +71,7 @@ public NumberFeatureTraitsEditor(String aId, feature = aFeature; traits = Model.of(readTraits()); - Form form = new Form(MID_FORM, CompoundPropertyModel.of(traits)) + var form = new Form(MID_FORM, CompoundPropertyModel.of(traits)) { private static final long serialVersionUID = 4456748721289266655L; @@ -82,8 +83,6 @@ protected void onSubmit() } }; form.setOutputMarkupPlaceholderTag(true); - form.add(visibleWhen( - () -> traits.getObject().isLimited() && feature.getObject().getTagset() == null)); add(form); Class clazz = Integer.class; @@ -101,25 +100,30 @@ protected void onSubmit() } } - DropDownChoice editorType = new DropDownChoice<>( - CID_EDITOR_TYPE); - editorType.setModel(PropertyModel.of(traits, "editorType")); + var limited = new CheckBox("limited"); + limited.add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, + target -> target.add(form))); + form.add(limited); + + var editorType = new DropDownChoice(CID_EDITOR_TYPE); + // editorType.setModel(PropertyModel.of(traits, "editorType")); editorType.setChoices(Arrays.asList(NumberFeatureTraits.EditorType.values())); editorType.add(new LambdaAjaxFormComponentUpdatingBehavior("change")); - editorType.add(visibleWhen(() -> isEditorTypeSelectionPossible())); + editorType.add(visibleWhen( + () -> traits.getObject().isLimited() && isEditorTypeSelectionPossible())); form.add(editorType); var minimum = new NumberTextField<>("minimum", clazz, options); - minimum.setModel(PropertyModel.of(traits, "minimum")); + minimum.add(visibleWhen(() -> traits.getObject().isLimited())); form.add(minimum); var maximum = new NumberTextField<>("maximum", clazz, options); - maximum.setModel(PropertyModel.of(traits, "maximum")); + maximum.add(visibleWhen(() -> traits.getObject().isLimited())); form.add(maximum); minimum.add(new LambdaAjaxFormComponentUpdatingBehavior("change", target -> { - BigDecimal min = new BigDecimal(traits.getObject().getMinimum().toString()); - BigDecimal max = new BigDecimal(traits.getObject().getMaximum().toString()); + var min = new BigDecimal(traits.getObject().getMinimum().toString()); + var max = new BigDecimal(traits.getObject().getMaximum().toString()); if (min.compareTo(max) > 0) { traits.getObject().setMaximum(traits.getObject().getMinimum()); } @@ -127,19 +131,13 @@ protected void onSubmit() })); maximum.add(new LambdaAjaxFormComponentUpdatingBehavior("change", target -> { - BigDecimal min = new BigDecimal(traits.getObject().getMinimum().toString()); - BigDecimal max = new BigDecimal(traits.getObject().getMaximum().toString()); + var min = new BigDecimal(traits.getObject().getMinimum().toString()); + var max = new BigDecimal(traits.getObject().getMaximum().toString()); if (min.compareTo(max) > 0) { traits.getObject().setMinimum(traits.getObject().getMaximum()); } target.add(form); })); - - CheckBox multipleRows = new CheckBox("limited"); - multipleRows.setModel(PropertyModel.of(traits, "limited")); - multipleRows.add( - new LambdaAjaxFormComponentUpdatingBehavior("change", target -> target.add(form))); - add(multipleRows); } /** @@ -168,9 +166,9 @@ private UimaPrimitiveFeatureSupport_ImplBase getFeatureSupp */ private Traits readTraits() { - Traits result = new Traits(); + var result = new Traits(); - NumberFeatureTraits t = getFeatureSupport().readTraits(feature.getObject()); + var t = getFeatureSupport().readTraits(feature.getObject()); result.setLimited(t.isLimited()); result.setMinimum(t.getMinimum()); @@ -186,13 +184,13 @@ private Traits readTraits() */ private void writeTraits() { - NumberFeatureTraits t = new NumberFeatureTraits(); + var t = new NumberFeatureTraits(); t.setLimited(traits.getObject().isLimited()); t.setMinimum(traits.getObject().getMinimum()); t.setMaximum(traits.getObject().getMaximum()); - t.setEditorType(isEditorTypeSelectionPossible() ? traits.getObject().getEditorType() - : NumberFeatureTraits.EditorType.SPINNER); + t.setEditorType( + isEditorTypeSelectionPossible() ? traits.getObject().getEditorType() : SPINNER); getFeatureSupport().writeTraits(feature.getObject(), t); } @@ -216,9 +214,9 @@ public boolean isLimited() return limited; } - public void setLimited(boolean limited) + public void setLimited(boolean aLimited) { - this.limited = limited; + limited = aLimited; } public Number getMinimum() @@ -226,9 +224,9 @@ public Number getMinimum() return minimum; } - public void setMinimum(Number minimum) + public void setMinimum(Number aMinimum) { - this.minimum = minimum; + minimum = aMinimum; } public Number getMaximum() @@ -236,9 +234,9 @@ public Number getMaximum() return maximum; } - public void setMaximum(Number maximum) + public void setMaximum(Number aMaximum) { - this.maximum = maximum; + maximum = aMaximum; } public NumberFeatureTraits.EditorType getEditorType() @@ -246,9 +244,9 @@ public NumberFeatureTraits.EditorType getEditorType() return editorType; } - public void setEditorType(NumberFeatureTraits.EditorType editorType) + public void setEditorType(NumberFeatureTraits.EditorType aEditorType) { - this.editorType = editorType; + editorType = aEditorType; } } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/LayerFactory.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/LayerFactory.java new file mode 100644 index 00000000000..ba313c302a5 --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/LayerFactory.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer; + +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.CHARACTERS; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; +import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.NO_OVERLAP; +import static de.tudarmstadt.ukp.clarin.webanno.model.ValidationMode.NEVER; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer.Builder; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS; +import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; + +public class LayerFactory +{ + public static Builder namedEntityLayer(Project aProject) + { + return AnnotationLayer.builder() // + .withProject(aProject) // + .forJCasClass(NamedEntity.class) // + .withUiName("Named entity") // + .withType(SpanLayerSupport.TYPE) // + .withBuiltIn(true) // + .withAnchoringMode(TOKENS) // + .withOverlapMode(NO_OVERLAP) // + .withCrossSentence(false); + } + + public static Builder partOfSpeechLayer(Project aProject, AnnotationFeature tokenPosFeature) + { + return AnnotationLayer.builder() // + .withProject(aProject) // + .forJCasClass(POS.class) // + .withUiName("Part of speech") // + .withType(SpanLayerSupport.TYPE) // + .withBuiltIn(true) // + .withAnchoringMode(SINGLE_TOKEN) // + .withOverlapMode(NO_OVERLAP) // + .withAttachType(tokenPosFeature.getLayer()) // + .withAttachFeature(tokenPosFeature) // + .withCrossSentence(false); + + } + + public static Builder tokenLayer(Project aProject) + { + return AnnotationLayer.builder() // + .withProject(aProject) // + .forJCasClass(Token.class) // + .withType(SpanLayerSupport.TYPE) // + .withBuiltIn(true) // + .withAnchoringMode(CHARACTERS) // + .withOverlapMode(NO_OVERLAP) // + .withValidationMode(NEVER) // + .withReadonly(true) // + .withCrossSentence(false) // + .withEnabled(false); + } +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationEndpointChangeListener.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationEndpointChangeListener.java new file mode 100644 index 00000000000..c9e8b345baf --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationEndpointChangeListener.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer.relation; + +import org.apache.uima.jcas.tcas.Annotation; +import org.springframework.context.event.EventListener; + +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanMovedEvent; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; + +public class RelationEndpointChangeListener +{ + private final AnnotationSchemaService schemaService; + + public RelationEndpointChangeListener(AnnotationSchemaService aSchemaService) + { + schemaService = aSchemaService; + } + + @EventListener + public void onSpanMovedEvent(SpanMovedEvent aEvent) + { + var span = aEvent.getAnnotation(); + var cas = span.getCAS(); + + for (var relLayer : schemaService.listAttachedRelationLayers(aEvent.getLayer())) { + var relAdapter = (RelationAdapter) schemaService.getAdapter(relLayer); + var maybeRelType = relAdapter.getAnnotationType(cas); + if (!maybeRelType.isPresent()) { + continue; + } + + var relCandidates = cas. select(maybeRelType.get()) // + .at(aEvent.getOldBegin(), aEvent.getOldEnd()) // + .asList(); + + for (var relCandidate : relCandidates) { + if (!span.equals(relAdapter.getTargetAnnotation(relCandidate))) { + continue; + } + + relCandidate.removeFromIndexes(); + relCandidate.setBegin(span.getBegin()); + relCandidate.setEnd(span.getEnd()); + relCandidate.addToIndexes(); + } + } + } +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java index ec962b18c58..b327633a71b 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.annotation.layer.relation; +import static java.lang.Math.max; +import static java.lang.Math.min; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.groupingBy; import static org.apache.commons.lang3.StringUtils.abbreviate; @@ -71,14 +73,16 @@ public class RelationRenderer { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + public static final String REL_EXTENSION_ID = "rel"; + public static final VID VID_BEFORE = VID.builder() // - .withExtensionId("rel") // + .withExtensionId(REL_EXTENSION_ID) // .withAnnotationId(0) // .withExtensionPayload("before") // .build(); public static final VID VID_AFTER = VID.builder() // - .withExtensionId("rel") // + .withExtensionId(REL_EXTENSION_ID) // .withAnnotationId(1) // .withExtensionPayload("after") // .build(); @@ -150,8 +154,8 @@ public List selectAnnotationsInWindow(RenderRequest aRequest, int aW var targetFs = getTargetFs(rel); if (sourceFs instanceof Annotation source && targetFs instanceof Annotation target) { - var relBegin = Math.min(source.getBegin(), target.getBegin()); - var relEnd = Math.max(source.getEnd(), target.getEnd()); + var relBegin = min(source.getBegin(), target.getBegin()); + var relEnd = max(source.getEnd(), target.getEnd()); if (overlapping(relBegin, relEnd, aWindowBegin, aWindowEnd)) { result.add(rel); @@ -260,12 +264,18 @@ public List render(RenderRequest aRequest, List aFea labelFeatures); case WHEN_SELECTED: if (aRequest.getState() == null || isSelected(aRequest, aFS, sourceFs, targetFs)) { + // State == null is when we render for the annotation sidebar... return renderRelationAsArcs(aRequest, aVDocument, aFS, typeAdapter, sourceFs, targetFs, labelFeatures); } return renderRelationOnLabel(aVDocument, typeAdapter, sourceFs, targetFs, labelFeatures); case NEVER: + if (aRequest.getState() == null) { + // State == null is when we render for the annotation sidebar... + return renderRelationAsArcs(aRequest, aVDocument, aFS, typeAdapter, sourceFs, + targetFs, labelFeatures); + } return renderRelationOnLabel(aVDocument, typeAdapter, sourceFs, targetFs, labelFeatures); default: diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/DeleteSpanAnnotationRequest.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/DeleteSpanAnnotationRequest.java new file mode 100644 index 00000000000..a4da40a5f7b --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/DeleteSpanAnnotationRequest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer.span; + +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.text.AnnotationFS; + +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + +public class DeleteSpanAnnotationRequest + extends SpanAnnotationRequest_ImplBase +{ + private final AnnotationFS annotation; + + public DeleteSpanAnnotationRequest(SourceDocument aDocument, String aDocumentOwner, CAS aCas, + AnnotationFS aAnnotation) + { + this(null, aDocument, aDocumentOwner, aCas, aAnnotation); + } + + private DeleteSpanAnnotationRequest(DeleteSpanAnnotationRequest aOriginal, + SourceDocument aDocument, String aDocumentOwner, CAS aCas, AnnotationFS aAnnotation) + { + super(null, aDocument, aDocumentOwner, aCas, aAnnotation.getBegin(), aAnnotation.getEnd()); + annotation = aAnnotation; + } + + public AnnotationFS getAnnotation() + { + return annotation; + } + + @Override + public DeleteSpanAnnotationRequest changeSpan(int aBegin, int aEnd) + { + throw new UnsupportedOperationException("Cannot change span when deleting span annotation"); + } +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SegmentationUnitAdapter.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SegmentationUnitAdapter.java new file mode 100644 index 00000000000..4ae5dbe66fe --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SegmentationUnitAdapter.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer.span; + +import static de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter.SpanOption.TRIM; + +import java.util.Set; + +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.jcas.tcas.Annotation; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.IllegalPlacementException; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; + +public class SegmentationUnitAdapter +{ + private static final Set SEGMENTATION_TYPES = Set.of(Token._TypeName, + Sentence._TypeName); + + private final SpanAdapter spanAdapter; + + public boolean accepts(String aTypeName) + { + return SEGMENTATION_TYPES.contains(aTypeName); + } + + public SegmentationUnitAdapter(SpanAdapter aSpanAdapter) + { + spanAdapter = aSpanAdapter; + } + + public AnnotationFS handle(CreateSpanAnnotationRequest aRequest) throws AnnotationException + { + if (Token._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + return splitUnit(aRequest, Token.class); + } + + if (Sentence._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + return splitUnit(aRequest, Sentence.class); + } + + throw new IllegalPlacementException( + "Annotation type not supported: " + spanAdapter.getAnnotationTypeName()); + } + + public AnnotationFS handle(MoveSpanAnnotationRequest aRequest) throws AnnotationException + { + var ann = aRequest.getAnnotation(); + if (aRequest.getBegin() == ann.getBegin() && aRequest.getEnd() == ann.getEnd()) { + // NOP + return ann; + } + + if (Token._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + return handleTokenMove(aRequest); + } + + if (Sentence._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + return handleSentenceMove(aRequest); + } + + throw new IllegalPlacementException( + "Annotation type not supported: " + spanAdapter.getAnnotationTypeName()); + } + + public void handle(DeleteSpanAnnotationRequest aRequest) throws AnnotationException + { + if (Token._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + deleteAndMergeUnit(aRequest.getDocument(), aRequest.getDocumentOwner(), + aRequest.getCas(), (Annotation) aRequest.getAnnotation(), Token.class); + return; + } + + if (Sentence._TypeName.equals(spanAdapter.getAnnotationTypeName())) { + deleteAndMergeUnit(aRequest.getDocument(), aRequest.getDocumentOwner(), + aRequest.getCas(), (Annotation) aRequest.getAnnotation(), Sentence.class); + return; + } + + throw new IllegalPlacementException( + "Annotation type not supported: " + spanAdapter.getAnnotationTypeName()); + } + + private void deleteAndMergeUnit(SourceDocument aDocument, + String aDocumentOwner, CAS aCas, Annotation aUnit, Class aClass) + throws AnnotationException + { + // First try to merge with the preceding unit + var precedingUnit = aCas.select(aClass).preceding(aUnit).limit(1).singleOrNull(); + if (precedingUnit != null) { + var oldBegin = precedingUnit.getBegin(); + var oldEnd = precedingUnit.getEnd(); + spanAdapter.moveSpanAnnotation(aCas, precedingUnit, precedingUnit.getBegin(), + aUnit.getEnd(), TRIM); + spanAdapter.publishEvent(() -> new SpanMovedEvent(this, aDocument, aDocumentOwner, + spanAdapter.getLayer(), precedingUnit, oldBegin, oldEnd)); + return; + } + + // Then try to merge with the following unit + var followingUnit = aCas.select(aClass).preceding(aUnit).limit(1).singleOrNull(); + if (followingUnit != null) { + var oldBegin = followingUnit.getBegin(); + var oldEnd = followingUnit.getEnd(); + spanAdapter.moveSpanAnnotation(aCas, followingUnit, aUnit.getBegin(), + followingUnit.getEnd(), TRIM); + spanAdapter.publishEvent(() -> new SpanMovedEvent(this, aDocument, aDocumentOwner, + spanAdapter.getLayer(), followingUnit, oldBegin, oldEnd)); + return; + } + + throw new IllegalPlacementException("The last unit cannot be deleted."); + } + + private AnnotationFS handleSentenceMove(MoveSpanAnnotationRequest aRequest) + throws AnnotationException + { + throw new IllegalPlacementException("Moving/resizing units currently not supported"); + } + + private AnnotationFS handleTokenMove(MoveSpanAnnotationRequest aRequest) + throws AnnotationException + { + throw new IllegalPlacementException("Moving/resizing units currently not supported"); + + // var cas = aRequest.getCas(); + // var ann = (Annotation) aRequest.getAnnotation(); + // + // if (aRequest.getBegin() != ann.getBegin() && aRequest.getEnd() != ann.getEnd()) { + // throw new IllegalPlacementException( + // "Can only resize at start or at end. Cannot move or resize at both ends at the same + // time."); + // } + // + // if (aRequest.getBegin() != ann.getBegin()) { + // // Expand at begin + // if (aRequest.getBegin() < ann.getBegin()) { + // + // } + // + // // Reduce at begin + // if (aRequest.getBegin() > ann.getBegin()) { + // + // } + // } + // + // if (aRequest.getEnd() != ann.getEnd()) { + // // Expand at end + // if (aRequest.getEnd() > ann.getEnd()) { + // + // } + // + // // Reduce at end + // if (aRequest.getEnd() < ann.getEnd()) { + // var followingToken = cas.select(Token.class).following(ann).singleOrNull(); + // if (followingToken == null) { + // // We make the last token smaller, so we need to add a new last token in order + // // to keep the entire text covered in tokens. + // } + // else { + // // We have a following token that we can enlarge + // } + // } + // } + // + // return ann; + } + + private AnnotationFS splitUnit(CreateSpanAnnotationRequest aRequest, + Class unitType) + throws AnnotationException + { + if (aRequest.getBegin() != aRequest.getEnd()) { + throw new IllegalPlacementException( + "Can only split unit, not create an entirely new one."); + } + + var cas = aRequest.getCas(); + var unit = cas.select(unitType) // + .covering(aRequest.getBegin(), aRequest.getBegin()) // + .singleOrNull(); + + var oldBegin = unit.getBegin(); + var oldEnd = unit.getEnd(); + var head = spanAdapter.moveSpanAnnotation(cas, unit, unit.getBegin(), aRequest.getBegin(), + TRIM); + spanAdapter.publishEvent(() -> new SpanMovedEvent(this, aRequest.getDocument(), + aRequest.getDocumentOwner(), spanAdapter.getLayer(), head, oldBegin, oldEnd)); + + var tail = spanAdapter.createSpanAnnotation(cas, aRequest.getEnd(), oldEnd, TRIM); + spanAdapter.publishEvent(() -> new SpanCreatedEvent(this, aRequest.getDocument(), + aRequest.getDocumentOwner(), spanAdapter.getLayer(), tail)); + return tail; + } +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapter.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapter.java index ccaa06abadd..4edff8c5e49 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapter.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapter.java @@ -17,8 +17,10 @@ */ package de.tudarmstadt.ukp.inception.annotation.layer.span; +import static de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter.SpanOption.TRIM; import static de.tudarmstadt.ukp.inception.support.uima.ICasUtil.selectByAddr; import static java.lang.System.currentTimeMillis; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.apache.uima.cas.text.AnnotationPredicates.colocated; import static org.apache.uima.fit.util.CasUtil.getType; @@ -35,6 +37,7 @@ import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; +import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -52,6 +55,7 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; +import de.tudarmstadt.ukp.inception.support.text.TrimUtils; /** * Manage interactions with annotations on a span layer. @@ -59,10 +63,17 @@ public class SpanAdapter extends TypeAdapter_ImplBase { + public enum SpanOption + { + TRIM; + } + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final List behaviors; + private final SegmentationUnitAdapter segmentationUnitAdapter; + public SpanAdapter(LayerSupportRegistry aLayerSupportRegistry, FeatureSupportRegistry aFeatureSupportRegistry, ApplicationEventPublisher aEventPublisher, AnnotationLayer aLayer, @@ -80,6 +91,8 @@ public SpanAdapter(LayerSupportRegistry aLayerSupportRegistry, .sorted(AnnotationAwareOrderComparator.INSTANCE) // .toList(); } + + segmentationUnitAdapter = new SegmentationUnitAdapter(this); } /** @@ -108,6 +121,10 @@ public AnnotationFS add(SourceDocument aDocument, String aDataOwner, CAS aCas, i public AnnotationFS handle(CreateSpanAnnotationRequest aRequest) throws AnnotationException { + if (segmentationUnitAdapter.accepts(getAnnotationTypeName())) { + return segmentationUnitAdapter.handle(aRequest); + } + var request = aRequest; // Adjust the creation request (e.g. adjust offsets to the configured granularity) or @@ -155,6 +172,16 @@ public AnnotationFS move(SourceDocument aDocument, String aDocumentOwner, CAS aC public AnnotationFS handle(MoveSpanAnnotationRequest aRequest) throws AnnotationException { + if (segmentationUnitAdapter.accepts(getAnnotationTypeName())) { + return segmentationUnitAdapter.handle(aRequest); + } + + var ann = aRequest.getAnnotation(); + if (aRequest.getBegin() == ann.getBegin() && aRequest.getEnd() == ann.getEnd()) { + // NOP + return ann; + } + var request = aRequest; // Adjust the move request (e.g. adjust offsets to the configured granularity) or @@ -176,11 +203,15 @@ public AnnotationFS handle(MoveSpanAnnotationRequest aRequest) throws Annotation return request.getAnnotation(); } - private AnnotationFS createSpanAnnotation(CAS aCas, int aBegin, int aEnd) + AnnotationFS createSpanAnnotation(CAS aCas, int aBegin, int aEnd, SpanOption... aOptions) throws AnnotationException { var type = CasUtil.getType(aCas, getAnnotationTypeName()); - var newAnnotation = aCas.createAnnotation(type, aBegin, aEnd); + var newAnnotation = (Annotation) aCas.createAnnotation(type, aBegin, aEnd); + + if (asList(aOptions).contains(TRIM)) { + TrimUtils.trim(aCas.getDocumentText(), newAnnotation); + } LOG.trace("Created span annotation {}-{} [{}]", newAnnotation.getBegin(), newAnnotation.getEnd(), newAnnotation.getCoveredText()); @@ -196,8 +227,8 @@ private AnnotationFS createSpanAnnotation(CAS aCas, int aBegin, int aEnd) return newAnnotation; } - private AnnotationFS moveSpanAnnotation(CAS aCas, AnnotationFS aAnnotation, int aBegin, - int aEnd) + AnnotationFS moveSpanAnnotation(CAS aCas, AnnotationFS aAnnotation, int aBegin, int aEnd, + SpanOption... aOptions) { var oldCoveredText = aAnnotation.getCoveredText(); var oldBegin = aAnnotation.getBegin(); @@ -207,6 +238,10 @@ private AnnotationFS moveSpanAnnotation(CAS aCas, AnnotationFS aAnnotation, int aAnnotation.setBegin(aBegin); aAnnotation.setEnd(aEnd); + if (asList(aOptions).contains(TRIM)) { + TrimUtils.trim(aCas.getDocumentText(), (Annotation) aAnnotation); + } + LOG.trace("Moved span annotation from {}-{} [{}] to {}-{} [{}]", oldBegin, oldEnd, oldCoveredText, aAnnotation.getBegin(), aAnnotation.getEnd(), aAnnotation.getCoveredText()); @@ -218,16 +253,27 @@ private AnnotationFS moveSpanAnnotation(CAS aCas, AnnotationFS aAnnotation, int @Override public void delete(SourceDocument aDocument, String aDocumentOwner, CAS aCas, VID aVid) + throws AnnotationException { var fs = selectByAddr(aCas, AnnotationFS.class, aVid.getId()); - aCas.removeFsFromIndexes(fs); + handle(new DeleteSpanAnnotationRequest(aDocument, aDocumentOwner, aCas, fs)); + } + + public void handle(DeleteSpanAnnotationRequest aRequest) throws AnnotationException + { + if (segmentationUnitAdapter.accepts(getAnnotationTypeName())) { + segmentationUnitAdapter.handle(aRequest); + } + + aRequest.getCas().removeFsFromIndexes(aRequest.getAnnotation()); // delete associated attachFeature if (getAttachTypeName() != null) { - detatch(aCas, fs); + detatch(aRequest.getCas(), aRequest.getAnnotation()); } - publishEvent(() -> new SpanDeletedEvent(this, aDocument, aDocumentOwner, getLayer(), fs)); + publishEvent(() -> new SpanDeletedEvent(this, aRequest.getDocument(), + aRequest.getDocumentOwner(), getLayer(), aRequest.getAnnotation())); } public AnnotationFS restore(SourceDocument aDocument, String aDocumentOwner, CAS aCas, VID aVid) diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAnchoringModeBehavior.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAnchoringModeBehavior.java index 8e2d99bc26c..6755b2c0062 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAnchoringModeBehavior.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAnchoringModeBehavior.java @@ -40,7 +40,7 @@ * {@code AnnotationServiceAutoConfiguration#spanAnchoringModeBehavior}. *

*/ -@Order(10) +@Order(100) public class SpanAnchoringModeBehavior extends SpanLayerBehavior { @@ -70,6 +70,11 @@ private > T onRequest(TypeAdapter aA T aRequest) throws AnnotationException { + if (Token.class.getName().equals(aAdapter.getAnnotationTypeName()) + || Sentence.class.getName().equals(aAdapter.getAnnotationTypeName())) { + return aRequest; + } + if (aRequest.getBegin() == aRequest.getEnd()) { if (!aAdapter.getLayer().getAnchoringMode().isZeroSpanAllowed()) { throw new IllegalPlacementException( diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanCrossSentenceBehavior.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanCrossSentenceBehavior.java index 78419f57add..3154e9ee49d 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanCrossSentenceBehavior.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanCrossSentenceBehavior.java @@ -39,6 +39,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.MultipleSentenceCoveredException; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.rendering.vmodel.VComment; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; @@ -87,6 +88,11 @@ private > T onRequest(TypeAdapter aA T aRequest) throws AnnotationException { + if (Token.class.getName().equals(aAdapter.getAnnotationTypeName()) + || Sentence.class.getName().equals(aAdapter.getAnnotationTypeName())) { + return aRequest; + } + if (aAdapter.getLayer().isCrossSentence()) { return aRequest; } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanOverlapBehavior.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanOverlapBehavior.java index 5b37de44550..3eca517ed59 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanOverlapBehavior.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanOverlapBehavior.java @@ -38,6 +38,8 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.IllegalPlacementException; import de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.rendering.vmodel.VComment; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; @@ -93,6 +95,11 @@ private > T onRequest(TypeAdapter aA T aRequest) throws AnnotationException { + if (Token.class.getName().equals(aAdapter.getAnnotationTypeName()) + || Sentence.class.getName().equals(aAdapter.getAnnotationTypeName())) { + return aRequest; + } + final CAS aCas = aRequest.getCas(); final int aBegin = aRequest.getBegin(); final int aEnd = aRequest.getEnd(); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRenderer.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRenderer.java index 7b3ff861ef1..7594b697f73 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRenderer.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRenderer.java @@ -40,6 +40,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.rendering.Renderer_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureTraits; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationRenderer; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; @@ -208,6 +209,7 @@ private void renderSlots(RenderRequest aRequest, VDocument aVDocument, Annotatio var typeAdapter = getTypeAdapter(); var aWindowBegin = aVDocument.getWindowBegin(); var aWindowEnd = aVDocument.getWindowEnd(); + var layer = typeAdapter.getLayer(); int fi = 0; nextFeature: for (var feat : typeAdapter.listFeatures()) { @@ -217,6 +219,8 @@ private void renderSlots(RenderRequest aRequest, VDocument aVDocument, Annotatio } if (feat.getMultiValueMode() == ARRAY && feat.getLinkMode() == WITH_ROLE) { + var traits = getTraits(feat, LinkFeatureTraits.class); + List links = typeAdapter.getFeatureValue(feat, aFS); for (var li = 0; li < links.size(); li++) { var link = links.get(li); @@ -234,12 +238,16 @@ private void renderSlots(RenderRequest aRequest, VDocument aVDocument, Annotatio .withSlot(li) // .build(); + var label = traits.map(LinkFeatureTraits::isEnableRoleLabels).orElse(false) + ? link.role + : feat.getUiName(); + var arc = VArc.builder() // - .withLayer(typeAdapter.getLayer()) // + .withLayer(layer) // .withVid(vid) // .withSource(aSource) // .withTarget(target) // - .withLabel(link.role) // + .withLabel(label) // .build(); aSpansAndSlots.add(arc); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListener.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListener.java new file mode 100644 index 00000000000..99601b5157b --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListener.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer.span; + +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; +import static org.apache.uima.fit.util.FSUtil.getFeature; + +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.jcas.tcas.Annotation; +import org.springframework.context.event.EventListener; + +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; + +public class TokenAttachedSpanChangeListener +{ + private final AnnotationSchemaService schemaService; + + public TokenAttachedSpanChangeListener(AnnotationSchemaService aSchemaService) + { + schemaService = aSchemaService; + } + + @EventListener + public void onSpanMovedEvent(SpanMovedEvent aEvent) + { + adjustAttachedAnnotations(aEvent); + adjustSingleTokenAnchoredAnnotations(aEvent); + adjustTokenAnchoredAnnotations(aEvent); + } + + void adjustTokenAnchoredAnnotations(SpanMovedEvent aEvent) + { + var unit = aEvent.getAnnotation(); + var cas = unit.getCAS(); + + var multiTokenLayers = schemaService.listAnnotationLayer(aEvent.getProject()).stream() // + .filter(layer -> layer.getAnchoringMode() == TOKENS) // + .toList(); + + for (var layer : multiTokenLayers) { + var adapter = schemaService.getAdapter(layer); + var maybeType = adapter.getAnnotationType(cas); + + if (maybeType.isEmpty()) { + continue; + } + + for (var ann : cas. select(maybeType.get()).asList()) { + if (ann.getBegin() != aEvent.getOldBegin() && ann.getEnd() != aEvent.getOldEnd()) { + continue; + } + + ann.removeFromIndexes(); + if (ann.getBegin() == aEvent.getOldBegin()) { + ann.setBegin(unit.getBegin()); + } + + if (ann.getEnd() == aEvent.getOldEnd()) { + ann.setEnd(unit.getEnd()); + } + ann.addToIndexes(); + } + } + } + + void adjustSingleTokenAnchoredAnnotations(SpanMovedEvent aEvent) + { + var unit = aEvent.getAnnotation(); + var cas = unit.getCAS(); + + var singleTokenLayers = schemaService.listAnnotationLayer(aEvent.getProject()).stream() // + .filter(layer -> layer.getAnchoringMode() == SINGLE_TOKEN) // + .toList(); + + for (var layer : singleTokenLayers) { + var adapter = schemaService.getAdapter(layer); + if (adapter == null) { + continue; + } + + var maybeType = adapter.getAnnotationType(cas); + + if (maybeType.isEmpty()) { + continue; + } + + for (var ann : cas. select(maybeType.get()) + .at(aEvent.getOldBegin(), aEvent.getOldEnd()).asList()) { + moveAnnotation(unit, ann); + } + } + } + + void adjustAttachedAnnotations(SpanMovedEvent aEvent) + { + var unit = aEvent.getAnnotation(); + for (var attachFeature : schemaService.listAttachedSpanFeatures(aEvent.getLayer())) { + var attachedAnnotation = getFeature(unit, attachFeature.getName(), Annotation.class); + + if (attachedAnnotation == null) { + continue; + } + + moveAnnotation(unit, attachedAnnotation); + } + } + + private void moveAnnotation(AnnotationFS aUnit, Annotation aAnn) + { + aAnn.removeFromIndexes(); + aAnn.setBegin(aUnit.getBegin()); + aAnn.setEnd(aUnit.getEnd()); + aAnn.addToIndexes(); + } + +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemExtension.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemExtension.java new file mode 100644 index 00000000000..bcafbbdb044 --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemExtension.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.menu; + +import org.wicketstuff.jquery.ui.widget.menu.IMenuItem; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; + +public interface ContextMenuItemExtension + extends Extension +{ + @Override + default String getId() + { + return getClass().getName(); + } + + @Override + default boolean accepts(AnnotationPageBase aPage) + { + return true; + } + + IMenuItem createMenuItem(VID aVid, int aClientX, int aClientY); +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistry.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistry.java new file mode 100644 index 00000000000..c87459a4ca5 --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistry.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.menu; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.inception.support.extensionpoint.ExtensionPoint; + +public interface ContextMenuItemRegistry + extends ExtensionPoint +{ +} diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistryImpl.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistryImpl.java new file mode 100644 index 00000000000..3fc02bdc563 --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/menu/ContextMenuItemRegistryImpl.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.menu; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.inception.support.extensionpoint.ExtensionPoint_ImplBase; + +public class ContextMenuItemRegistryImpl + extends ExtensionPoint_ImplBase + implements ContextMenuItemRegistry +{ + public ContextMenuItemRegistryImpl( + @Lazy @Autowired(required = false) List aExtensions) + { + super(aExtensions); + } +} diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/settings_document-import-export.adoc b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_boolean.adoc similarity index 64% rename from inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/settings_document-import-export.adoc rename to inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_boolean.adoc index dda0a3f1f85..08c938148a9 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/settings_document-import-export.adoc +++ b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_boolean.adoc @@ -14,26 +14,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -= Document Im-/Export - -Control the importing and exporting of documents: - -.Document import/export settings in the `settings.properties` file -[cols="4*", options="header"] -|=== -| Setting -| Description -| Default -| Example - -| document-import.max-tokens -| Token-count limit for imported documents -| 2000000 -| 0 _(no limit)_ - -| document-import.max-sentences -| Sentence-count limit for imported documents -| 20000 -| 0 _(no limit)_ -|=== - +[[sect_layers_feature_boolean]] += Boolean Feature diff --git a/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_link.adoc b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_link.adoc new file mode 100644 index 00000000000..7ce2abf940f --- /dev/null +++ b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_link.adoc @@ -0,0 +1,60 @@ +// Licensed to the Technische Universität Darmstadt under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The Technische Universität Darmstadt +// licenses this file to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +[[sect_layers_feature_link]] += Link features + +Link features allow an annotation to connect to one or more span annotations. +Optionally, each of the links may have a role label. +Link features are for example used to model event structures where the owner of the feature is the event trigger and the spans linked to represent the particpants or circumstances related to the event. + +A link feature may be configured to link only to a particular span layer or to any span layer. +When linking to a particular span layer, an armed slot may be filled simply by selecting the span of text to fill it. +An appropriate span annotation will be created automatically. +If the link feature is configured to any layer, an existing span annotation needs to be selected as a slot filler. + +.Link feature properties +[cols="1v,2", options="header"] +|==== +| Property | Description + +| Role labels +| Allows users to add a role label to each slot when linking anntations. + If disabled the UI labels of annotations will be displayed instead of role labels. + This property is enabled by default. + +| Comparison +| Determines whether to consider this link feature when comparing the annotations of the layer to which this feature belongs. +By default, link featurse are **ignored when comparing annotations**. That means, two annotations at the same position with the same feature values but different links will be considered equal. +If you want to consider the links when comparing annotations, you can change this setting to **consider when comparing annotations**. +Note that even when the links are considered, two annotations at the same position are still considered to be stacked and will not be auto-merge by any merge strategies that do not support stacking. + +| Multiplicity +| Determines how links are compared to each other e.g. when calulating agreement or when merging annotations during curation. + Use *Target can be linked in multiple different roles* if a link target can appear in multiple roles with respect to the same source span. + In this mode, if an annotator links multiple targets using the same role, the links will be considered stacked and not be not auto-merged by curation or used for agreement calculation. + Use *Target should be linked in only one role* if you expect that a link target should only appear in a single roles with respect to the same source span. + In this mode, if an annotator links the same target in multiple roles, the links will be considered stacked and not be auto-merged by curation or used for agreement calculation. + Use *Target can be linked in multiple roles (same or different)* if you expect that a link target should be linked multiple times with different roles as well as different targets can be linked with the same role. + In this mode, there is no stacking. + +| Tagset +| The tagset controlling the possible values for the link roles. + +| Default slots +| For each of the specificed roles, an empty slot will be visible in the UI. + This can save the annotator time to create a slot for frequently used slots. +|==== diff --git a/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_number.adoc b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_number.adoc new file mode 100644 index 00000000000..25f32714021 --- /dev/null +++ b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_number.adoc @@ -0,0 +1,36 @@ +// Licensed to the Technische Universität Darmstadt under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The Technische Universität Darmstadt +// licenses this file to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +[[sect_layers_feature_number]] += Number features + +.Number feature properties +[cols="1v,2", options="header"] +|==== +| Property | Description + +| Limited +| If enabled a minimum and maximum value can be set for the number feature. + +| Minimum +| Only visible if *Limited* is enabled. Determines the minimum value of the limited number feature. + +| Maximum +| Only visible if *Limited* is enabled. Determines the maximum value of the limited number feature. + +| Editor Type +| Select which editor should be used for modifying this features value. +|==== \ No newline at end of file diff --git a/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_string.adoc b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_string.adoc new file mode 100644 index 00000000000..2a5049049a1 --- /dev/null +++ b/inception/inception-api-annotation/src/main/resources/META-INF/asciidoc/user-guide/projects_layers_feature_string.adoc @@ -0,0 +1,77 @@ +// Licensed to the Technische Universität Darmstadt under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The Technische Universität Darmstadt +// licenses this file to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +[[sect_layers_feature_string]] += String features + +A string feature either holds a short tag (optionally from a restricted tag set) or a note (i.e. a multi-line text). + +When no tagset is associated with the string feature, it is displayed to the user simply as a +single line input field. You can enable the *multiple rows* option to turn it into a multi-line +text area. If you do so, additional options appear allowing to configure the size of the text area +which can be fixed or dynamic (i.e. automatically adjust to the text area content). + +Optionally, a <> can be associated with a string feature (unless you enabled multiple rows). If string feature is associated with a tagset, there are different options +as to which type of *editor type* (i.e. input field) is displayed to the user. + +.Editor types for string features with tagsets +[cols="1v,2", options="header"] +|==== +| Editor type | Description + +| Auto +| An editor is chosen automatically depending on the size of the tagset and whether annotators can add to it. + +| Radio group +| Each tag is shown as a button. Only one button can be active at a time. Best for quick access to small tagsets. Does not allow annotators to add new tags (yet). + +| Combo box +| A text field with auto-completion and button that opens a drop-down list showing all possible tags and their descriptions. Best for mid-sized tagsets. + +| Autocomplete +| A text field with auto-completion. A dropdown opens when the user starts typing into the field and it displays matching tags. There is no way to browse all available tags. Best for large tagsets. + +|==== + +The tagset size thresholds used by the *Auto* mode to determine which editor to choose can be globally configured by an administrator via the <> file. +Because the radio group editor does not support adding new tags (yet), it chosen automatically only if the associated tagset does not allow annotators to add new tags. + +.String feature properties +[cols="1v,2", options="header"] +|==== +| Property | Description + +| Tagset +| The tagset controlling the possible values for a string feature. + +| Show only when constraints apply +| Display the feature only if any constraint rules apply to it (cf. <>) + +| Editor type +| The type of input field shown to the annotators. + +| Multiple Rows +| If enabled the textfield will be replaced by a textarea which expands on focus. This also enables options to set the size of the textarea and disables tagsets. + +| Dynamic Size +| If enabled the textfield will dynamically resize itself based on the content. This disables collapsed and expanded row settings. + +| Collapsed Rows +| Set the number of rows for the textarea when it is collapsed and not focused. + +| Expanded Rows +| Set the number of rows for the textarea when it is expanded and not focused. +|==== diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategyTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategyTest.java new file mode 100644 index 00000000000..3dcd7f05c8d --- /dev/null +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/LineOrientedPagingStrategyTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging; + +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.CR; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.CRLF; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.LF; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.LINE_SEPARATOR; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.NEL; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.PARAGRAPH_SEPARATOR; +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.uima.fit.factory.JCasFactory; +import org.junit.jupiter.api.Test; + +class LineOrientedPagingStrategyTest +{ + @Test + void testMixedLineBreaks() throws Exception + { + var sut = new LineOrientedPagingStrategy(); + + var jcas = JCasFactory.createJCas(); + jcas.setDocumentText( // + "Line1" + CR + // + "Line2" + LF + // + "Line3" + CRLF + // + "Line4" + NEL + // + "Line5" + LINE_SEPARATOR + // + "Line6" + PARAGRAPH_SEPARATOR + // + "Line7"); + + assertThat(sut.units(jcas.getCas())) + .extracting(u -> jcas.getDocumentText().substring(u.getBegin(), u.getEnd())) + .containsExactly( // + "Line1", // + "Line2", // + "Line3", // + "Line4", // + "Line5", // + "Line6", // + "Line7"); + } + + @Test + void testConsecutiveLineBreaks() throws Exception + { + var sut = new LineOrientedPagingStrategy(); + + var jcas = JCasFactory.createJCas(); + jcas.setDocumentText( // + "Line1" + CR + CR + // + "Line2" + LF + LF + // + "Line3" + CRLF + CRLF + // + "Line4" + NEL + NEL + // + "Line5" + LINE_SEPARATOR + LINE_SEPARATOR + // + "Line6" + PARAGRAPH_SEPARATOR + PARAGRAPH_SEPARATOR + // + "Line7"); + + assertThat(sut.units(jcas.getCas())) + .extracting(u -> jcas.getDocumentText().substring(u.getBegin(), u.getEnd())) + .containsExactly( // + "Line1", "", // + "Line2", "", // + "Line3", "", // + "Line4", "", // + "Line5", "", // + "Line6", "", // + "Line7"); + } + + @Test + void testEndingWithNewline() throws Exception + { + var sut = new LineOrientedPagingStrategy(); + + var jcas = JCasFactory.createJCas(); + jcas.setDocumentText( // + "Line1" + CR + CR); + + assertThat(sut.units(jcas.getCas())) + .extracting(u -> jcas.getDocumentText().substring(u.getBegin(), u.getEnd())) + .containsExactly( // + "Line1", ""); + } +} diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategyTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategyTest.java index 8f80b10759c..c054868b723 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategyTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/paging/TokenWrappingPagingStrategyTest.java @@ -17,13 +17,14 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.CR; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.LineOrientedPagingStrategy.LINE_SEPARATORS; +import static org.apache.commons.lang3.StringUtils.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -import org.apache.commons.lang3.StringUtils; import org.apache.uima.fit.factory.JCasBuilder; import org.apache.uima.fit.factory.JCasFactory; -import org.apache.uima.jcas.JCas; import org.junit.jupiter.api.Test; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; @@ -34,10 +35,10 @@ public class TokenWrappingPagingStrategyTest @Test public void thatMultipleConsecutiveLineBreaksWork() throws Exception { - TokenWrappingPagingStrategy sut = new TokenWrappingPagingStrategy(120); + var sut = new TokenWrappingPagingStrategy(120); - JCas jcas = JCasFactory.createJCas(); - JCasBuilder builder = new JCasBuilder(jcas); + var jcas = JCasFactory.createJCas(); + var builder = new JCasBuilder(jcas); builder.add("See-", Token.class); builder.add("\n"); builder.add("\n"); @@ -57,22 +58,23 @@ public void thatMultipleConsecutiveLineBreaksWork() throws Exception @Test public void thatLinesOfDifferntLenghtsWork() throws Exception { - TokenWrappingPagingStrategy sut = new TokenWrappingPagingStrategy(10); + var sut = new TokenWrappingPagingStrategy(10); - JCas jcas = JCasFactory.createJCas(); - JCasBuilder builder = new JCasBuilder(jcas); - builder.add(StringUtils.repeat("a", 20), Token.class); + var jcas = JCasFactory.createJCas(); + var builder = new JCasBuilder(jcas); + builder.add(repeat("a", 20), Token.class); builder.add("\n"); - builder.add(StringUtils.repeat("b", 15), Token.class); + builder.add(repeat("b", 15), Token.class); builder.add("\n"); - builder.add(StringUtils.repeat("c", 11), Token.class); + builder.add(repeat("c", 11), Token.class); builder.add("\n"); - builder.add(StringUtils.repeat("d", 10), Token.class); + builder.add(repeat("d", 10), Token.class); builder.add("\n"); - builder.add(StringUtils.repeat("e", 9), Token.class); + builder.add(repeat("e", 9), Token.class); builder.add("\n"); - builder.add(StringUtils.repeat("f", 1), Token.class); + builder.add(repeat("f", 1), Token.class); builder.add("\n"); + builder.add(repeat("g", 1), Token.class); builder.close(); assertThat(sut.units(jcas.getCas())) // @@ -86,18 +88,19 @@ public void thatLinesOfDifferntLenghtsWork() throws Exception tuple(37, 48, "ccccccccccc"), // tuple(49, 59, "dddddddddd"), // tuple(60, 69, "eeeeeeeee"), // - tuple(70, 71, "f")); + tuple(70, 71, "f"), // + tuple(72, 73, "g")); } @Test public void thatWrappingWork() throws Exception { - TokenWrappingPagingStrategy sut = new TokenWrappingPagingStrategy(11); + var sut = new TokenWrappingPagingStrategy(11); - JCas jcas = JCasFactory.createJCas(); - JCasBuilder builder = new JCasBuilder(jcas); + var jcas = JCasFactory.createJCas(); + var builder = new JCasBuilder(jcas); for (int n = 0; n < 10; n++) { - builder.add(StringUtils.repeat("a", 3), Token.class); + builder.add(repeat("a", 3), Token.class); builder.add(" "); } builder.close(); @@ -113,4 +116,54 @@ public void thatWrappingWork() throws Exception tuple(24, 35, "aaa aaa aaa"), // tuple(36, 39, "aaa")); } + + @Test + void testMixedLineBreaks() throws Exception + { + var sut = new TokenWrappingPagingStrategy(10); + + var jcas = JCasFactory.createJCas(); + var builder = new JCasBuilder(jcas); + var i = 1; + for (var sep : LINE_SEPARATORS) { + builder.add(repeat("a", 3) + i, Token.class); + builder.add(" "); + builder.add(repeat("b", 3) + i, Token.class); + builder.add(" "); + builder.add(repeat("c", 3) + i, Token.class); + builder.add(sep); + i++; + } + builder.add("end", Token.class); + builder.close(); + + assertThat(sut.units(jcas.getCas())) + .extracting(u -> jcas.getDocumentText().substring(u.getBegin(), u.getEnd())) + .containsExactly( // + "aaa1 bbb1", "ccc1", // + "aaa2 bbb2", "ccc2", // + "aaa3 bbb3", "ccc3", // + "aaa4 bbb4", "ccc4", // + "aaa5 bbb5", "ccc5", // + "aaa6 bbb6", "ccc6", // + "end"); + } + + @Test + void testEndingWithNewline() throws Exception + { + var sut = new TokenWrappingPagingStrategy(10); + + var jcas = JCasFactory.createJCas(); + var builder = new JCasBuilder(jcas); + builder.add(repeat("a", 3), Token.class); + builder.add(CR); + builder.add(CR); + builder.close(); + + assertThat(sut.units(jcas.getCas())) + .extracting(u -> jcas.getDocumentText().substring(u.getBegin(), u.getEnd())) + .containsExactly( // + "aaa"); + } } diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupportTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupportTest.java index 57117b2735b..6dfc0cf0482 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupportTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupportTest.java @@ -19,6 +19,7 @@ import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.JCasFactory.createJCasFromPath; +import static org.apache.uima.fit.util.FSUtil.setFeature; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -26,14 +27,8 @@ import static org.mockito.Mockito.when; import java.util.ArrayList; -import java.util.List; -import org.apache.uima.cas.ArrayFS; -import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; -import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.JCas; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -86,11 +81,11 @@ public void setUp() throws Exception @Test public void testAccepts() { - AnnotationFeature feat1 = new AnnotationFeature("string", "LinkType"); + var feat1 = new AnnotationFeature("string", "LinkType"); feat1.setMode(MultiValueMode.ARRAY); feat1.setLinkMode(LinkMode.WITH_ROLE); - AnnotationFeature feat2 = new AnnotationFeature("Dummy feature", "someType"); + var feat2 = new AnnotationFeature("Dummy feature", "someType"); assertThat(sut.accepts(feat1)).isTrue(); assertThat(sut.accepts(feat2)).isFalse(); @@ -99,17 +94,17 @@ public void testAccepts() @Test public void testWrapUnwrap() throws Exception { - CAS cas = jcas.getCas(); + var cas = jcas.getCas(); - List links = new ArrayList<>(); + var links = new ArrayList(); links.add(new LinkWithRoleModel("role", "label", 3)); - AnnotationFS targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); + var targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); - ArrayFS array = cas.createArrayFS(1); - FeatureStructure linkFS = cas.createFS(linkType); - FSUtil.setFeature(linkFS, slotFeature.getLinkTypeRoleFeatureName(), "role"); - FSUtil.setFeature(linkFS, slotFeature.getLinkTypeTargetFeatureName(), targetFS); + var array = cas.createArrayFS(1); + var linkFS = cas.createFS(linkType); + setFeature(linkFS, slotFeature.getLinkTypeRoleFeatureName(), "role"); + setFeature(linkFS, slotFeature.getLinkTypeTargetFeatureName(), targetFS); array.set(0, linkFS); assertThat(sut.wrapFeatureValue(slotFeature, cas, array)).isEqualTo(links); @@ -128,15 +123,15 @@ public void thatUsingOutOfTagsetValueInClosedTagsetProducesException() throws Ex { final String role = "TAG-NOT-IN-LIST"; - CAS cas = jcas.getCas(); + var cas = jcas.getCas(); - TagSet slotFeatureTagset = new TagSet(); + var slotFeatureTagset = new TagSet(); slotFeatureTagset.setCreateTag(false); slotFeature.setTagset(slotFeatureTagset); - AnnotationFS hostFS = cas.createAnnotation(hostType, 0, cas.getDocumentText().length()); - AnnotationFS targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); + var hostFS = cas.createAnnotation(hostType, 0, cas.getDocumentText().length()); + var targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); when(schemaService.existsTag(role, slotFeatureTagset)).thenReturn(false); @@ -152,15 +147,15 @@ public void thatUsingOutOfTagsetValueInOpenTagsetAddsNewValue() throws Exception { final String role = "TAG-NOT-IN-LIST"; - CAS cas = jcas.getCas(); + var cas = jcas.getCas(); - TagSet slotFeatureTagset = new TagSet(); + var slotFeatureTagset = new TagSet(); slotFeatureTagset.setCreateTag(true); slotFeature.setTagset(slotFeatureTagset); - AnnotationFS hostFS = cas.createAnnotation(hostType, 0, cas.getDocumentText().length()); - AnnotationFS targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); + var hostFS = cas.createAnnotation(hostType, 0, cas.getDocumentText().length()); + var targetFS = cas.createAnnotation(targetType, 0, cas.getDocumentText().length()); when(schemaService.existsTag(role, slotFeatureTagset)).thenReturn(false); diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/chain/ChainAdapterTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/chain/ChainAdapterTest.java index a5d62850f67..a6b143479aa 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/chain/ChainAdapterTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/chain/ChainAdapterTest.java @@ -48,7 +48,6 @@ import de.tudarmstadt.ukp.dkpro.core.api.coref.type.CoreferenceChain; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAnchoringModeBehavior; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanCrossSentenceBehavior; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerBehavior; @@ -56,6 +55,7 @@ import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; @ExtendWith(MockitoExtension.class) public class ChainAdapterTest diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapterTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapterTest.java index 64109b46558..b8997c92385 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapterTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapterTest.java @@ -22,8 +22,8 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.NO_OVERLAP; import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.OVERLAP_ONLY; import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.STACKING_ONLY; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_SOURCE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_SOURCE; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_TARGET; import static java.util.Arrays.asList; import static org.apache.uima.fit.util.JCasUtil.select; import static org.assertj.core.api.Assertions.assertThat; @@ -56,12 +56,12 @@ import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @ExtendWith(MockitoExtension.class) @@ -100,14 +100,13 @@ public void setup() throws Exception document.setProject(project); // Set up annotation schema with POS and Dependency - AnnotationLayer tokenLayer = new AnnotationLayer(Token.class.getName(), "Token", - SpanLayerSupport.TYPE, project, true, SINGLE_TOKEN, NO_OVERLAP); + var tokenLayer = new AnnotationLayer(Token.class.getName(), "Token", SpanLayerSupport.TYPE, + project, true, SINGLE_TOKEN, NO_OVERLAP); tokenLayer.setId(1l); - AnnotationFeature tokenLayerPos = new AnnotationFeature(1l, tokenLayer, "pos", - POS.class.getName()); + var tokenLayerPos = new AnnotationFeature(1l, tokenLayer, "pos", POS.class.getName()); - AnnotationLayer posLayer = new AnnotationLayer(POS.class.getName(), "POS", - SpanLayerSupport.TYPE, project, true, SINGLE_TOKEN, NO_OVERLAP); + var posLayer = new AnnotationLayer(POS.class.getName(), "POS", SpanLayerSupport.TYPE, + project, true, SINGLE_TOKEN, NO_OVERLAP); posLayer.setId(2l); depLayer = new AnnotationLayer(Dependency.class.getName(), "Dependency", @@ -130,27 +129,27 @@ public void setup() throws Exception @Test public void thatRelationAttachmentBehaviorOnCreateWorks() throws Exception { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); - List tokens = new ArrayList<>(select(jcas, Token.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var tokens = new ArrayList<>(select(jcas, Token.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(1); - AnnotationFS dep = sut.add(document, username, source, target, jcas.getCas()); + var dep = sut.add(document, username, source, target, jcas.getCas()); assertThat(FSUtil.getFeature(dep, FEAT_REL_SOURCE, Token.class)).isEqualTo(tokens.get(0)); assertThat(FSUtil.getFeature(dep, FEAT_REL_TARGET, Token.class)).isEqualTo(tokens.get(1)); @@ -161,24 +160,24 @@ public void thatRelationCrossSentenceBehaviorOnCreateThrowsException() throws Ex { depLayer.setCrossSentence(false); - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(posAnnotations.size() - 1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(posAnnotations.size() - 1); assertThatExceptionOfType(MultipleSentenceCoveredException.class) .isThrownBy(() -> sut.add(document, username, source, target, jcas.getCas())) @@ -188,24 +187,24 @@ public void thatRelationCrossSentenceBehaviorOnCreateThrowsException() throws Ex @Test public void thatRelationCrossSentenceBehaviorOnValidateGeneratesErrors() throws Exception { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(posAnnotations.size() - 1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(posAnnotations.size() - 1); depLayer.setCrossSentence(true); sut.add(document, username, source, target, jcas.getCas()); @@ -219,27 +218,27 @@ public void thatRelationCrossSentenceBehaviorOnValidateGeneratesErrors() throws @Test public void thatCreatingRelationWorks() throws Exception { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); - List tokens = new ArrayList<>(select(jcas, Token.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var tokens = new ArrayList<>(select(jcas, Token.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(1); - AnnotationFS dep1 = sut.add(document, username, source, target, jcas.getCas()); + var dep1 = sut.add(document, username, source, target, jcas.getCas()); assertThat(FSUtil.getFeature(dep1, FEAT_REL_SOURCE, Token.class)).isEqualTo(tokens.get(0)); assertThat(FSUtil.getFeature(dep1, FEAT_REL_TARGET, Token.class)).isEqualTo(tokens.get(1)); @@ -248,24 +247,24 @@ public void thatCreatingRelationWorks() throws Exception @Test public void thatRelationOverlapBehaviorOnCreateWorks() throws Exception { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(1); // First annotation should work depLayer.setOverlapMode(ANY_OVERLAP); @@ -295,24 +294,24 @@ public void thatRelationOverlapBehaviorOnCreateWorks() throws Exception @Test public void thatRelationOverlapBehaviorOnValidateGeneratesErrors() throws Exception { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - for (Token t : select(jcas, Token.class)) { - POS pos = new POS(jcas, t.getBegin(), t.getEnd()); + for (var t : select(jcas, Token.class)) { + var pos = new POS(jcas, t.getBegin(), t.getEnd()); t.setPos(pos); pos.addToIndexes(); } - RelationAdapter sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, - null, depLayer, FEAT_REL_TARGET, FEAT_REL_SOURCE, + var sut = new RelationAdapter(layerSupportRegistry, featureSupportRegistry, null, depLayer, + FEAT_REL_TARGET, FEAT_REL_SOURCE, () -> asList(dependencyLayerGovernor, dependencyLayerDependent), behaviors, constraintsService); - List posAnnotations = new ArrayList<>(select(jcas, POS.class)); + var posAnnotations = new ArrayList<>(select(jcas, POS.class)); - POS source = posAnnotations.get(0); - POS target = posAnnotations.get(1); + var source = posAnnotations.get(0); + var target = posAnnotations.get(1); // Create two annotations stacked annotations depLayer.setOverlapMode(ANY_OVERLAP); diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java index be20468ef5a..d34cb8ae574 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java @@ -55,7 +55,6 @@ import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistryImpl; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; @@ -63,6 +62,7 @@ import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; @ExtendWith(MockitoExtension.class) public class RelationRendererTest diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapterTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapterTest.java index 2cd28a6140e..c82c21393d5 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapterTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanAdapterTest.java @@ -48,10 +48,10 @@ import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @ExtendWith(MockitoExtension.class) @@ -102,11 +102,11 @@ public void thatSpanCrossSentenceBehaviorOnCreateThrowsException() { neLayer.setCrossSentence(false); - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); assertThatExceptionOfType(MultipleSentenceCoveredException.class) .isThrownBy(() -> sut.add(document, username, jcas.getCas(), 0, @@ -118,11 +118,11 @@ public void thatSpanCrossSentenceBehaviorOnCreateThrowsException() public void thatSpanCrossSentenceBehaviorOnValidateReturnsErrorMessage() throws AnnotationException { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test .\nThis is sentence two ."); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); // Add two annotations neLayer.setCrossSentence(true); @@ -139,11 +139,11 @@ public void thatSpanCrossSentenceBehaviorOnValidateReturnsErrorMessage() @Test public void thatSpanOverlapBehaviorOnCreateWorks() throws AnnotationException { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test ."); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); // First time should work neLayer.setOverlapMode(ANY_OVERLAP); @@ -173,11 +173,11 @@ public void thatSpanOverlapBehaviorOnCreateWorks() throws AnnotationException @Test public void thatSpanOverlapBehaviorOnValidateGeneratesErrors() throws AnnotationException { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test ."); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); // Add two annotations neLayer.setOverlapMode(ANY_OVERLAP); @@ -212,11 +212,11 @@ public void thatSpanOverlapBehaviorOnValidateGeneratesErrors() throws Annotation @Test public void thatSpanAnchoringAndOverlapBehaviorsWorkInConcert() throws AnnotationException { - TokenBuilder builder = new TokenBuilder<>(Token.class, Sentence.class); + var builder = new TokenBuilder<>(Token.class, Sentence.class); builder.buildTokens(jcas, "This is a test ."); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); // First time should work - we annotate the whole word "This" neLayer.setOverlapMode(ANY_OVERLAP); @@ -253,8 +253,8 @@ public void thatAdjacentAnnotationsDoNotOverlap() throws AnnotationException new NamedEntity(jcas, 0, 4).addToIndexes(); new NamedEntity(jcas, 4, 5).addToIndexes(); - SpanAdapter sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, - neLayer, () -> asList(), behaviors, constraintsService); + var sut = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, neLayer, + () -> asList(), behaviors, constraintsService); neLayer.setOverlapMode(NO_OVERLAP); assertThat(sut.validate(jcas.getCas())).isEmpty(); diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRendererTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRendererTest.java index 0468e7eb314..74a5b273a00 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRendererTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/SpanRendererTest.java @@ -41,13 +41,13 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VComment; import de.tudarmstadt.ukp.inception.rendering.vmodel.VCommentType; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; @ExtendWith(MockitoExtension.class) public class SpanRendererTest diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListenerTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListenerTest.java new file mode 100644 index 00000000000..ff0c6fff128 --- /dev/null +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/span/TokenAttachedSpanChangeListenerTest.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.layer.span; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import java.util.List; +import java.util.Objects; + +import org.apache.uima.fit.factory.JCasFactory; +import org.apache.uima.jcas.JCas; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tudarmstadt.ukp.clarin.webanno.constraints.ConstraintsService; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS; +import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.layer.LayerFactory; +import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; + +@ExtendWith(MockitoExtension.class) +class TokenAttachedSpanChangeListenerTest +{ + private @Mock AnnotationSchemaService schemaService; + private @Mock ConstraintsService constraintsService; + + private Project project; + private SourceDocument doc; + + private AnnotationLayer tokenLayer; + private AnnotationLayer namedEntityLayer; + private AnnotationLayer posLayer; + private AnnotationFeature tokenPosFeature; + + private TokenAttachedSpanChangeListener sut; + + private JCas cas; + + private LayerSupportRegistryImpl layerRegistry; + protected List features; + private List layers; + + @BeforeEach + void setup() throws Exception + { + cas = JCasFactory.createJCas(); + + sut = new TokenAttachedSpanChangeListener(schemaService); + + project = Project.builder() // + .build(); + + doc = SourceDocument.builder() // + .withProject(project) // + .build(); + + tokenLayer = LayerFactory.tokenLayer(project).build(); + + tokenPosFeature = AnnotationFeature.builder() // + .withLayer(tokenLayer) // + .withName(Token._FeatName_pos) // + .withType(POS._TypeName) // + .build(); + + namedEntityLayer = LayerFactory.namedEntityLayer(project).build(); + + posLayer = LayerFactory.partOfSpeechLayer(project, tokenPosFeature).build(); + + layers = asList(tokenLayer, namedEntityLayer, posLayer); + features = asList(tokenPosFeature); + + var featureSupportRegistry = new FeatureSupportRegistryImpl(asList()); + featureSupportRegistry.init(); + + var layerBehaviorRegistry = new LayerBehaviorRegistryImpl(asList()); + layerBehaviorRegistry.init(); + + layerRegistry = new LayerSupportRegistryImpl(asList(new SpanLayerSupport( + featureSupportRegistry, null, layerBehaviorRegistry, constraintsService))); + layerRegistry.init(); + + lenient().when(schemaService.listAnnotationLayer(project)) + .thenReturn(asList(tokenLayer, namedEntityLayer, posLayer)); + + lenient().when(schemaService.getAdapter(any())).thenAnswer(call -> { + var layer = call.getArgument(0, AnnotationLayer.class); + var support = layerRegistry.findExtension(layer).get(); + return support.createAdapter(layer, () -> features); + }); + + lenient().when(schemaService.listAttachedSpanFeatures(any())).thenAnswer(call -> { + var layer = call.getArgument(0, AnnotationLayer.class); + return layers.stream() // + .filter(l -> Objects.equals(l.getAttachType(), layer)) // + .map(AnnotationLayer::getAttachFeature) // + .toList(); + }); + } + + @Test + void testAdjustTokenAnchoredAnnotations() + { + cas.setDocumentText("1 2 3 4"); + + var ne = new NamedEntity(cas, 0, 3); + var token1 = new Token(cas, 0, 1); + var token2 = new Token(cas, 2, 5); + asList(ne, token1, token2).forEach(cas::addFsToIndexes); + + sut.adjustTokenAnchoredAnnotations( + new SpanMovedEvent(this, doc, "user", tokenLayer, token2, 2, 3)); + + assertThat(ne.getBegin()).isEqualTo(token1.getBegin()).isEqualTo(0); + assertThat(ne.getEnd()).isEqualTo(token2.getEnd()).isEqualTo(5); + } + + @Test + void testAdjustSingleTokenAnchoredAnnotations() + { + cas.setDocumentText("1 2 3 4"); + + var pos = new POS(cas, 0, 1); + var token = new Token(cas, 0, 3); + asList(pos, token).forEach(cas::addFsToIndexes); + + sut.adjustSingleTokenAnchoredAnnotations( + new SpanMovedEvent(this, doc, "user", tokenLayer, token, 0, 1)); + + assertThat(pos.getBegin()).isEqualTo(token.getBegin()).isEqualTo(0); + assertThat(pos.getEnd()).isEqualTo(token.getEnd()).isEqualTo(3); + } + + @Test + void testAdjustAttachedAnnotations() + { + cas.setDocumentText("1 2 3 4"); + + var pos = new POS(cas, 0, 1); + var token = new Token(cas, 0, 3); + token.setPos(pos); + asList(pos, token).forEach(cas::addFsToIndexes); + + sut.adjustAttachedAnnotations( + new SpanMovedEvent(this, doc, "user", tokenLayer, token, 0, 1)); + + assertThat(pos.getBegin()).isEqualTo(token.getBegin()).isEqualTo(0); + assertThat(pos.getEnd()).isEqualTo(token.getEnd()).isEqualTo(3); + } +} diff --git a/inception/inception-api-editor/pom.xml b/inception/inception-api-editor/pom.xml index d4dc31ec8ca..f189fb03c43 100644 --- a/inception/inception-api-editor/pom.xml +++ b/inception/inception-api-editor/pom.xml @@ -92,10 +92,6 @@ org.apache.wicket wicket-spring - - org.wicketstuff - wicketstuff-jquery-ui - diff --git a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorBase.java b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorBase.java index 68c2cbacdc0..0a1b80a3b1b 100644 --- a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorBase.java +++ b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorBase.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import org.apache.commons.lang3.Validate; @@ -253,4 +254,6 @@ protected void handleError(String aMessage, Exception e) LOG.error("{}: {}", aMessage, e.getMessage(), e); error(aMessage + ": " + e.getMessage()); } + + public abstract Optional getContextMenuLookup(); } diff --git a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtension.java b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtension.java index 1e3d4e5e8da..0c746ba11ff 100644 --- a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtension.java +++ b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtension.java @@ -23,7 +23,6 @@ import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.wicketstuff.jquery.ui.widget.menu.IMenuItem; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; @@ -74,11 +73,6 @@ default void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState) // Do nothing by default } - default void generateContextMenuItems(List aItems) - { - // Do nothing by default - } - default List lookupLazyDetails(SourceDocument aDocument, User aUser, CAS aCas, VID aVid, AnnotationLayer aLayer) { diff --git a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistry.java b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistry.java index 2c432d37a2d..b6083e36160 100644 --- a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistry.java +++ b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistry.java @@ -22,7 +22,6 @@ import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.wicketstuff.jquery.ui.widget.menu.IMenuItem; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; @@ -40,6 +39,4 @@ void fireAction(AnnotationActionHandler aActionHandler, AnnotatorState aModelObj throws IOException, AnnotationException; void fireRenderRequested(AjaxRequestTarget aTarget, AnnotatorState aState); - - void generateContextMenuItems(List aItems); } diff --git a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistryImpl.java b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistryImpl.java index 23946c739af..a1a1c8d55fa 100644 --- a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistryImpl.java +++ b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/AnnotationEditorExtensionRegistryImpl.java @@ -33,7 +33,6 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.wicketstuff.jquery.ui.widget.menu.IMenuItem; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.editor.config.AnnotationEditorAutoConfiguration; @@ -100,10 +99,10 @@ public AnnotationEditorExtension getExtension(String aId) if (aId == null) { return null; } - else { - return extensions.stream().filter(ext -> aId.equals(ext.getBeanName())).findFirst() - .orElse(null); - } + + return extensions.stream() // + .filter(ext -> aId.equals(ext.getBeanName())) // + .findFirst().orElse(null); } @Override @@ -111,8 +110,7 @@ public void fireAction(AnnotationActionHandler aActionHandler, AnnotatorState aM AjaxRequestTarget aTarget, CAS aCas, VID aParamId, String aAction) throws IOException, AnnotationException { - for (AnnotationEditorExtension ext : getExtensions()) { - + for (var ext : getExtensions()) { if (!ext.getBeanName().equals(aParamId.getExtensionId())) { continue; } @@ -123,16 +121,8 @@ public void fireAction(AnnotationActionHandler aActionHandler, AnnotatorState aM @Override public void fireRenderRequested(AjaxRequestTarget aTarget, AnnotatorState aState) { - for (AnnotationEditorExtension ext : getExtensions()) { + for (var ext : getExtensions()) { ext.renderRequested(aTarget, aState); } } - - @Override - public void generateContextMenuItems(List aItems) - { - for (AnnotationEditorExtension ext : getExtensions()) { - ext.generateContextMenuItems(aItems); - } - } } diff --git a/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/ContextMenuLookup.java b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/ContextMenuLookup.java new file mode 100644 index 00000000000..456c8990f4f --- /dev/null +++ b/inception/inception-api-editor/src/main/java/de/tudarmstadt/ukp/inception/editor/ContextMenuLookup.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.editor; + +import de.tudarmstadt.ukp.inception.support.wicket.ContextMenu; + +public interface ContextMenuLookup +{ + public ContextMenu getContextMenu(); +} diff --git a/inception/inception-app-webapp/pom.xml b/inception/inception-app-webapp/pom.xml index e48aff2cc83..b3ea573cd02 100644 --- a/inception/inception-app-webapp/pom.xml +++ b/inception/inception-app-webapp/pom.xml @@ -810,7 +810,7 @@ left ./user-guide/ ${project.basedir}/../ - ./user-guide/images + ./user-guide/ @@ -827,7 +827,7 @@ left ./developer-guide/ ${project.basedir}/../ - ./developer-guide/images + ./developer-guide/ @@ -844,7 +844,7 @@ left ./admin-guide/ ${project.basedir}/../ - ./admin-guide/images + ./admin-guide/ @@ -1325,7 +1325,7 @@ preamble ./user-guide/ ${project.basedir}/../ - ./user-guide/images + ./user-guide/ @@ -1342,7 +1342,7 @@ preamble ./developer-guide/ ${project.basedir}/../ - ./developer-guide/images + ./developer-guide/ @@ -1359,6 +1359,7 @@ preamble ./admin-guide/ ${project.basedir}/../ + ./admin-guide/ diff --git a/inception/inception-bom/pom.xml b/inception/inception-bom/pom.xml index db2be07d80d..b4e99937569 100644 --- a/inception/inception-bom/pom.xml +++ b/inception/inception-bom/pom.xml @@ -294,7 +294,7 @@ de.tudarmstadt.ukp.inception.app - inception-imls-support-llm + inception-imls-llm-support 35.0-SNAPSHOT diff --git a/inception/inception-brat-editor/pom.xml b/inception/inception-brat-editor/pom.xml index 0513773c447..905c3e33ed6 100644 --- a/inception/inception-brat-editor/pom.xml +++ b/inception/inception-brat-editor/pom.xml @@ -214,11 +214,6 @@ dkpro-core-tokit-asl test - - de.tudarmstadt.ukp.inception.app - inception-schema - test - de.tudarmstadt.ukp.inception.app inception-constraints diff --git a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/annotation/BratAnnotationEditor.java b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/annotation/BratAnnotationEditor.java index b95dce12e51..45bb6dbc9ea 100644 --- a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/annotation/BratAnnotationEditor.java +++ b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/annotation/BratAnnotationEditor.java @@ -113,11 +113,10 @@ protected DiamAjaxBehavior createDiamBehavior() @Override protected AnnotationEditorProperties getProperties() { - var props = new AnnotationEditorProperties(); + var props = super.getProperties(); // The factory is the JS call. Cf. the "globalName" in build.js and the factory method // defined in main.ts props.setEditorFactory("Brat.factory()"); - props.setDiamAjaxCallbackUrl(getDiamBehavior().getCallbackUrl().toString()); props.setStylesheetSources(asList(referenceToUrl(servletContext, BratCssReference.get()))); props.setScriptSources(asList(referenceToUrl(servletContext, BratResourceReference.get()))); return props; diff --git a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java index f11fae1f8ef..5106a241ca5 100644 --- a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java +++ b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java @@ -20,6 +20,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.brat.schema.BratSchemaGeneratorImpl.getBratTypeName; import static de.tudarmstadt.ukp.clarin.webanno.model.ScriptDirection.RTL; import static java.util.Arrays.asList; +import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.uima.cas.text.AnnotationPredicates.covering; import static org.apache.uima.cas.text.AnnotationPredicates.overlappingAtBegin; @@ -251,11 +252,11 @@ private static List getArgument(VID aGovernorFs, VID aDependentFs) private void renderText(VDocument aVDoc, GetDocumentResponse aResponse, RenderRequest aRequest) { - if (!aRequest.isIncludeText()) { + if (!aRequest.isIncludeText() || isEmpty(aVDoc.getText())) { return; } - String visibleText = aVDoc.getText(); + var visibleText = aVDoc.getText(); char replacementChar = 0; if (StringUtils.isNotEmpty(properties.getWhiteSpaceReplacementCharacter())) { replacementChar = properties.getWhiteSpaceReplacementCharacter().charAt(0); @@ -267,9 +268,13 @@ private void renderText(VDocument aVDoc, GetDocumentResponse aResponse, RenderRe private void renderBratTokensFromText(GetDocumentResponse aResponse, VDocument aVDoc) { - List bratTokenOffsets = new ArrayList<>(); - String visibleText = aVDoc.getText(); - BreakIterator bi = BreakIterator.getWordInstance(Locale.ROOT); + if (isEmpty(aVDoc.getText())) { + return; + } + + var bratTokenOffsets = new ArrayList(); + var visibleText = aVDoc.getText(); + var bi = BreakIterator.getWordInstance(Locale.ROOT); bi.setText(visibleText); int last = bi.first(); int cur = bi.next(); diff --git a/inception/inception-brat-editor/src/main/ts/src/visualizer/Visualizer.ts b/inception/inception-brat-editor/src/main/ts/src/visualizer/Visualizer.ts index 05252407f06..4322a7e33c8 100644 --- a/inception/inception-brat-editor/src/main/ts/src/visualizer/Visualizer.ts +++ b/inception/inception-brat-editor/src/main/ts/src/visualizer/Visualizer.ts @@ -2846,7 +2846,7 @@ export class Visualizer { return path } - // Render curve pointing to the rught annotation endpoint + // Render curve pointing to the right annotation endpoint let cornerx = to - (this.rtlmode ? -1 : 1) * ufoCatcherMod * this.arcSlant // TODO: duplicates above in part, make funcs diff --git a/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/message/JsonDiffTest.java b/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/message/JsonDiffTest.java index f752709e4c7..8cec79c13c4 100644 --- a/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/message/JsonDiffTest.java +++ b/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/message/JsonDiffTest.java @@ -17,13 +17,13 @@ */ package de.tudarmstadt.ukp.clarin.webanno.brat.message; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.File; import org.junit.jupiter.api.Test; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.flipkart.zjsonpatch.JsonDiff; public class JsonDiffTest @@ -31,26 +31,31 @@ public class JsonDiffTest @Test public void testJsonDiff() throws Exception { - String f_base = "src/test/resources/brat_normal.json"; - String f_addedMiddle = "src/test/resources/brat_added_entity_near_middle.json"; - String f_removedMiddle = "src/test/resources/brat_removed_entity_in_middle.json"; - String f_removedEnd = "src/test/resources/brat_removed_entity_near_end.json"; - - MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter(); - - ObjectMapper mapper = jsonConverter.getObjectMapper(); - - JsonNode base = mapper.readTree(new File(f_base)); - JsonNode addedMiddle = mapper.readTree(new File(f_addedMiddle)); - JsonNode removedMiddle = mapper.readTree(new File(f_removedMiddle)); - JsonNode removedEnd = mapper.readTree(new File(f_removedEnd)); - - JsonNode d_addedMiddle = JsonDiff.asJson(base, addedMiddle); - JsonNode d_removedMiddle = JsonDiff.asJson(base, removedMiddle); - JsonNode d_removedEnd = JsonDiff.asJson(base, removedEnd); - - System.out.println(d_addedMiddle); - System.out.println(d_removedMiddle); - System.out.println(d_removedEnd); + var f_base = "src/test/resources/brat_normal.json"; + var f_addedMiddle = "src/test/resources/brat_added_entity_near_middle.json"; + var f_removedMiddle = "src/test/resources/brat_removed_entity_in_middle.json"; + var f_removedEnd = "src/test/resources/brat_removed_entity_near_end.json"; + + var jsonConverter = new MappingJackson2HttpMessageConverter(); + + var mapper = jsonConverter.getObjectMapper(); + + var base = mapper.readTree(new File(f_base)); + var addedMiddle = mapper.readTree(new File(f_addedMiddle)); + var removedMiddle = mapper.readTree(new File(f_removedMiddle)); + var removedEnd = mapper.readTree(new File(f_removedEnd)); + + var d_addedMiddle = JsonDiff.asJson(base, addedMiddle); + var d_removedMiddle = JsonDiff.asJson(base, removedMiddle); + var d_removedEnd = JsonDiff.asJson(base, removedEnd); + + assertThat(d_addedMiddle.toString()) // + .isEqualTo("[{\"op\":\"add\",\"path\":\"/entities/7\",\"value\":" // + + "[\"198\",\"0_de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS\"," // + + "[[29,32]],\"LALA\",\"#8dd3c7\",null]}]"); + assertThat(d_removedMiddle.toString()) // + .isEqualTo("[{\"op\":\"remove\",\"path\":\"/entities/5\"}]"); + assertThat(d_removedEnd.toString()) // + .isEqualTo("[{\"op\":\"remove\",\"path\":\"/entities/10\"}]"); } } diff --git a/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImplTest.java b/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImplTest.java index cbdb4051202..afa991cd324 100644 --- a/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImplTest.java +++ b/inception/inception-brat-editor/src/test/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImplTest.java @@ -65,7 +65,6 @@ import de.tudarmstadt.ukp.inception.annotation.feature.number.NumberFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistryImpl; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; @@ -73,7 +72,8 @@ import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.support.json.JSONUtil; @ExtendWith(MockitoExtension.class) @@ -171,7 +171,7 @@ void thatSentenceOrientedStrategyRenderCorrectly() throws Exception () -> asList(posFeature)); }); - var jsonFilePath = "target/test-output/output-sentence-oriented.json"; + var jsonFilePath = "target/test-output/paging-sentence-oriented.json"; var file = "src/test/resources/tcf04-karin-wl.xml"; var cas = JCasFactory.createJCas().getCas(); @@ -201,15 +201,15 @@ void thatSentenceOrientedStrategyRenderCorrectly() throws Exception JSONUtil.generatePrettyJson(response, new File(jsonFilePath)); - assertThat(contentOf(new File("src/test/resources/output-sentence-oriented.json"), UTF_8)) + assertThat(contentOf(new File("src/test/resources/paging-sentence-oriented.json"), UTF_8)) .isEqualToNormalizingNewlines(contentOf(new File(jsonFilePath), UTF_8)); } @Test void thatLineOrientedStrategyRenderCorrectly() throws Exception { - var jsonFilePath = "target/test-output/multiline.json"; - var file = "src/test/resources/multiline.txt"; + var jsonFilePath = "target/test-output/paging-line-oriented.json"; + var file = "src/test/resources/paging-line-oriented.txt"; var cas = JCasFactory.createJCas().getCas(); var reader = createReader(TextReader.class, TextReader.PARAM_SOURCE_LOCATION, file); @@ -239,15 +239,15 @@ void thatLineOrientedStrategyRenderCorrectly() throws Exception JSONUtil.generatePrettyJson(response, new File(jsonFilePath)); - assertThat(contentOf(new File("src/test/resources/multiline.json"), UTF_8)) - .isEqualToNormalizingNewlines(contentOf(new File(jsonFilePath), UTF_8)); + assertThat(contentOf(new File(jsonFilePath), UTF_8)).isEqualToNormalizingNewlines( + contentOf(new File("src/test/resources/paging-line-oriented.json"), UTF_8)); } @Test void thatTokenWrappingStrategyRenderCorrectly() throws Exception { - var jsonFilePath = "target/test-output/longlines.json"; - var file = "src/test/resources/longlines.txt"; + var jsonFilePath = "target/test-output/paging-token-wrapping.json"; + var file = "src/test/resources/paging-token-wrapping.txt"; var cas = JCasFactory.createJCas().getCas(); var reader = createReader(TextReader.class, TextReader.PARAM_SOURCE_LOCATION, file); @@ -277,7 +277,7 @@ void thatTokenWrappingStrategyRenderCorrectly() throws Exception JSONUtil.generatePrettyJson(response, new File(jsonFilePath)); - assertThat(contentOf(new File("src/test/resources/longlines.json"), UTF_8)) + assertThat(contentOf(new File("src/test/resources/paging-token-wrapping.json"), UTF_8)) .isEqualToNormalizingNewlines(contentOf(new File(jsonFilePath), UTF_8)); } diff --git a/inception/inception-brat-editor/src/test/resources/multiline.json b/inception/inception-brat-editor/src/test/resources/paging-line-oriented.json similarity index 79% rename from inception/inception-brat-editor/src/test/resources/multiline.json rename to inception/inception-brat-editor/src/test/resources/paging-line-oriented.json index 483a7016da4..ba431850487 100644 --- a/inception/inception-brat-editor/src/test/resources/multiline.json +++ b/inception/inception-brat-editor/src/test/resources/paging-line-oriented.json @@ -1,12 +1,12 @@ { "action" : "getDocument", - "text" : "This is line 1. This is line 3. This is line 4. ", + "text" : "This is line 1. This is line 3. This is line 4.", "windowBegin" : 0, - "windowEnd" : 49, + "windowEnd" : 48, "args" : { }, "rtl_mode" : false, "font_zoom" : 100, "sentence_number_offset" : 1, "token_offsets" : [ [ 0, 4 ], [ 5, 7 ], [ 8, 12 ], [ 13, 14 ], [ 14, 15 ], [ 17, 21 ], [ 22, 24 ], [ 25, 29 ], [ 30, 31 ], [ 31, 32 ], [ 33, 37 ], [ 38, 40 ], [ 41, 45 ], [ 46, 47 ], [ 47, 48 ] ], - "sentence_offsets" : [ [ 0, 15 ], [ 16, 16 ], [ 17, 32 ], [ 33, 48 ], [ 49, 49 ] ] + "sentence_offsets" : [ [ 0, 15 ], [ 16, 16 ], [ 17, 32 ], [ 33, 48 ] ] } \ No newline at end of file diff --git a/inception/inception-brat-editor/src/test/resources/multiline.tsv b/inception/inception-brat-editor/src/test/resources/paging-line-oriented.tsv similarity index 100% rename from inception/inception-brat-editor/src/test/resources/multiline.tsv rename to inception/inception-brat-editor/src/test/resources/paging-line-oriented.tsv diff --git a/inception/inception-brat-editor/src/test/resources/multiline.txt b/inception/inception-brat-editor/src/test/resources/paging-line-oriented.txt similarity index 100% rename from inception/inception-brat-editor/src/test/resources/multiline.txt rename to inception/inception-brat-editor/src/test/resources/paging-line-oriented.txt diff --git a/inception/inception-brat-editor/src/test/resources/output-sentence-oriented.json b/inception/inception-brat-editor/src/test/resources/paging-sentence-oriented.json similarity index 100% rename from inception/inception-brat-editor/src/test/resources/output-sentence-oriented.json rename to inception/inception-brat-editor/src/test/resources/paging-sentence-oriented.json diff --git a/inception/inception-brat-editor/src/test/resources/longlines.json b/inception/inception-brat-editor/src/test/resources/paging-token-wrapping.json similarity index 100% rename from inception/inception-brat-editor/src/test/resources/longlines.json rename to inception/inception-brat-editor/src/test/resources/paging-token-wrapping.json diff --git a/inception/inception-brat-editor/src/test/resources/longlines.txt b/inception/inception-brat-editor/src/test/resources/paging-token-wrapping.txt similarity index 100% rename from inception/inception-brat-editor/src/test/resources/longlines.txt rename to inception/inception-brat-editor/src/test/resources/paging-token-wrapping.txt diff --git a/inception/inception-concept-linking/src/main/resources/META-INF/asciidoc/user-guide/annotation_concept-linking.adoc b/inception/inception-concept-linking/src/main/resources/META-INF/asciidoc/user-guide/annotation_concept-linking.adoc index bd835bcfd50..e9ccb952cde 100644 --- a/inception/inception-concept-linking/src/main/resources/META-INF/asciidoc/user-guide/annotation_concept-linking.adoc +++ b/inception/inception-concept-linking/src/main/resources/META-INF/asciidoc/user-guide/annotation_concept-linking.adoc @@ -36,7 +36,7 @@ the concept feature in the right sidebar of the annotation editor and start typi a concept. A ranked list of candidates is then displayed in the form of a drop-down menu. In order to make the disambiguation process easier, descriptions are shown for each candidate. -image::concept-linking2.png[align="center"] +image::images/concept-linking2.png[align="center"] The suggestions are updated every time it receives new input. @@ -50,4 +50,4 @@ It is possible to combine the NEL with the existing Named Entity Recommenders fo which makes the annotation process even faster. The recommender needs to be set up in the <>. -image::concept-linking4.png[align="center"] \ No newline at end of file +image::images/concept-linking4.png[align="center"] \ No newline at end of file diff --git a/inception/inception-constraints/src/main/resources/META-INF/asciidoc/user-guide/constraints.adoc b/inception/inception-constraints/src/main/resources/META-INF/asciidoc/user-guide/constraints.adoc index 79de63849e8..9f54d339ce0 100644 --- a/inception/inception-constraints/src/main/resources/META-INF/asciidoc/user-guide/constraints.adoc +++ b/inception/inception-constraints/src/main/resources/META-INF/asciidoc/user-guide/constraints.adoc @@ -172,7 +172,7 @@ Pos { In the UI, the tags that were matched by the constraints are bold and come first in the list of tags: -image::constraints.png[align="center"] +image::images/constraints.png[align="center"] === Constraining a relation layer based on its endpoints diff --git a/inception/inception-constraints/src/test/java/de/tudarmstadt/ukp/clarin/webanno/constraints/export/ConstraintsExporterTest.java b/inception/inception-constraints/src/test/java/de/tudarmstadt/ukp/clarin/webanno/constraints/export/ConstraintsExporterTest.java index defc0ea3701..2d7b87e0517 100644 --- a/inception/inception-constraints/src/test/java/de/tudarmstadt/ukp/clarin/webanno/constraints/export/ConstraintsExporterTest.java +++ b/inception/inception-constraints/src/test/java/de/tudarmstadt/ukp/clarin/webanno/constraints/export/ConstraintsExporterTest.java @@ -94,7 +94,8 @@ void thatExportingAndImportingAgainWorks() throws Exception // Export the project var exportRequest = new FullProjectExportRequest(sourceProject, null, false); - var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test"); + var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test", + exportRequest.getFilenamePrefix()); var exportedProject = new ExportedProject(); try (var zos = new ZipOutputStream(new FileOutputStream(exportFile))) { diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiff.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiff.java index 0401058157c..f7b12db26a7 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiff.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiff.java @@ -17,60 +17,53 @@ */ package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff; -import static de.tudarmstadt.ukp.clarin.webanno.model.LinkMode.NONE; -import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; -import static java.util.Collections.emptyList; +import static de.tudarmstadt.ukp.inception.schema.api.feature.MaterializedLink.toMaterializedLinks; +import static java.lang.System.currentTimeMillis; import static java.util.Collections.emptySet; -import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Stream.concat; - +import static org.apache.commons.collections4.CollectionUtils.disjunction; +import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN; +import static org.apache.uima.cas.CAS.TYPE_NAME_BYTE; +import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE; +import static org.apache.uima.cas.CAS.TYPE_NAME_FLOAT; +import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER; +import static org.apache.uima.cas.CAS.TYPE_NAME_LONG; +import static org.apache.uima.cas.CAS.TYPE_NAME_SHORT; +import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; +import static org.apache.uima.cas.CAS.TYPE_NAME_STRING_ARRAY; +import static org.apache.uima.fit.util.FSUtil.getFeature; + +import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; -import org.apache.commons.lang3.StringUtils; import org.apache.uima.cas.ArrayFS; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.SofaFS; -import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.cas.AnnotationBase; import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.docmeta.DocumentMetadataDiffAdapter; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationDiffAdapter; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; -import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; -import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureTraits; -import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; -import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; -import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; -import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; public class CasDiff { - private final static Logger LOG = LoggerFactory.getLogger(CasDiff.class); + private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); Map casses = new LinkedHashMap<>(); @@ -82,8 +75,6 @@ public class CasDiff private final Map diffAdapters = new HashMap<>(); - private boolean recurseIntoLinkFeatures = false; - private CasDiff(int aBegin, int aEnd, Iterable aAdapters) { begin = aBegin; @@ -133,7 +124,7 @@ public static CasDiff doDiff(Iterable aAdapters, return new CasDiff(0, 0, aAdapters); } - var startTime = System.currentTimeMillis(); + var startTime = currentTimeMillis(); var diff = new CasDiff(aBegin, aEnd, aAdapters); @@ -146,7 +137,7 @@ public static CasDiff doDiff(Iterable aAdapters, } } - LOG.trace("CASDiff completed in {} ms", System.currentTimeMillis() - startTime); + LOG.trace("Completed in {} ms", currentTimeMillis() - startTime); return diff; } @@ -155,14 +146,14 @@ private DiffAdapter getAdapter(String aType) { var adapter = diffAdapters.get(aType); if (adapter == null) { - LOG.warn("No diff adapter for type [" + aType + "] -- treating as without features"); + LOG.warn("No diff adapter for type [{}] -- treating as without features", aType); adapter = new SpanDiffAdapter(aType, emptySet()); diffAdapters.put(aType, adapter); } return adapter; } - public Map getTypeAdapters() + public Map getAdapters() { return diffAdapters; } @@ -172,6 +163,26 @@ public Map getCasMap() return casses; } + public Collection getPositions() + { + return configSets.keySet(); + } + + /** + * @param aPosition + * a position. + * @return the configuration set for the given position. + */ + public ConfigurationSet getConfigurationSet(Position aPosition) + { + return configSets.get(aPosition); + } + + public DiffResult toResult() + { + return new DiffResult(this); + } + /** * CASes are added to the diff one after another, building the diff iteratively. A CAS can be * added multiple times for different types. Make sure a CAS is not added twice with the same @@ -192,20 +203,20 @@ private void addCas(String aCasGroupId, CAS aCas, String aType) // null elements in the list can occur if a user has never worked on a CAS // We add these to the internal list above, but then we bail out here. if (aCas == null) { - LOG.debug("CAS group [" + aCasGroupId + "] does not contain a CAS"); + LOG.debug("CAS group [{}] does not contain a CAS", aCasGroupId); return; } if (LOG.isDebugEnabled()) { - LOG.debug("Processing CAS group [" + aCasGroupId + "]"); + LOG.debug("Processing CAS group [{}]", aCasGroupId); String collectionId = null; String documentId = null; try { var dmd = WebAnnoCasUtil.getDocumentMetadata(aCas); - collectionId = FSUtil.getFeature(dmd, "collectionId", String.class); - documentId = FSUtil.getFeature(dmd, "documentId", String.class); - LOG.debug("User [" + collectionId + "] - Document [" + documentId + "]"); + collectionId = getFeature(dmd, "collectionId", String.class); + documentId = getFeature(dmd, "documentId", String.class); + LOG.debug("User [{}] - Document [{}]", collectionId, documentId); } catch (IllegalArgumentException e) { // We use this information only for debugging - so we can ignore if the information @@ -215,8 +226,7 @@ private void addCas(String aCasGroupId, CAS aCas, String aType) var type = aCas.getTypeSystem().getType(aType); if (type == null) { - LOG.debug("CAS group [" + aCasGroupId + "] contains no annotations of type [" + aType - + "]"); + LOG.debug("CAS group [{}] contains no annotations of type [{}]", aCasGroupId, aType); return; } @@ -231,25 +241,24 @@ private void addCas(String aCasGroupId, CAS aCas, String aType) } if (annotations.isEmpty()) { - LOG.debug("CAS group [" + aCasGroupId + "] contains no annotations of type [" + aType - + "]"); + LOG.debug("CAS group [{}] contains no annotations of type [{}]", aCasGroupId, aType); return; } - LOG.debug("CAS group [" + aCasGroupId + "] contains [" + annotations.size() - + "] annotations of type [" + aType + "]"); + LOG.debug("CAS group [{}] contains [{}] annotations of type [{}]", aCasGroupId, + annotations.size(), aType); var posBefore = configSets.keySet().size(); LOG.debug("Positions before: [{}]", posBefore); - for (var fs : annotations) { + for (var ann : annotations) { var positions = new ArrayList(); // Get/create configuration set at the current position - positions.add(adapter.getPosition(fs)); + positions.add(adapter.getPosition(ann)); // Generate secondary positions for multi-link features - positions.addAll(adapter.generateSubPositions(fs)); + positions.addAll(adapter.generateSubPositions(ann)); for (var pos : positions) { var configSet = configSets.get(pos); @@ -263,12 +272,14 @@ private void addCas(String aCasGroupId, CAS aCas, String aType) + configSet.getPosition().getClass() + "]"; // Merge FS into current set - addConfiguration(configSet, aCasGroupId, fs); + addConfiguration(configSet, aCasGroupId, ann); } } - LOG.debug("Positions after: [{}] (delta: {})", configSets.keySet().size(), - (configSets.keySet().size() - posBefore)); + if (LOG.isDebugEnabled()) { + LOG.debug("Positions after: [{}] (delta: {})", configSets.keySet().size(), + (configSets.keySet().size() - posBefore)); + } } private void addConfiguration(ConfigurationSet aSet, String aCasGroupId, FeatureStructure aFS) @@ -278,165 +289,161 @@ private void addConfiguration(ConfigurationSet aSet, String aCasGroupId, Feature } var position = aSet.getPosition(); - if (position.getFeature() == null) { - // Check if this configuration is already present - Configuration configuration = null; - for (var cfg : aSet.getConfigurations()) { - // Handle main positions - if (equalsFS(cfg.getRepresentative(casses), aFS)) { - configuration = cfg; - break; - } - } + if (position.isLinkFeaturePosition()) { + addLinkConfiguration(aSet, aCasGroupId, aFS); + return; + } - // Not found, add new one - if (configuration == null) { - configuration = new Configuration(position); - aSet.addConfiguration(configuration); - } + addBaseConfiguration(aSet, aCasGroupId, aFS); + } - configuration.add(aCasGroupId, aFS); - } - else { - var feat = aFS.getType().getFeatureByBaseName(position.getFeature()); + private void addLinkConfiguration(ConfigurationSet aSet, String aCasGroupId, + FeatureStructure aFS) + { + var position = aSet.getPosition(); + var feat = aFS.getType().getFeatureByBaseName(position.getLinkFeature()); - // If the CAS has not been upgraded yet to include the feature, then there are no - // configurations for it. - if (feat == null) { - return; - } + // If the CAS has not been upgraded yet to include the feature, then there are no + // configurations for it. + if (feat == null) { + return; + } - // For each slot at the given position in the FS-to-be-added, we need find a - // corresponding configuration - - var links = FSUtil.getFeature(aFS, feat, ArrayFS.class); - for (var i = 0; i < links.size(); i++) { - var link = links.get(i); - var adapter = getAdapter(aFS.getType().getName()); - var decl = adapter.getLinkFeature(position.getFeature()); - - // Check if this configuration is already present - Configuration configuration = null; - switch (position.getLinkCompareBehavior()) { - case ONE_TARGET_MULTIPLE_ROLES: { - var role = link.getStringValue( - link.getType().getFeatureByBaseName(decl.getRoleFeature())); - if (!role.equals(position.getRole())) { - continue; - } + // For each slot at the given position in the FS-to-be-added, we need find a + // corresponding configuration + var links = getFeature(aFS, feat, ArrayFS.class); + linkLoop: for (var i = 0; i < links.size(); i++) { + var link = links.get(i); + var adapter = getAdapter(aFS.getType().getName()); + var decl = adapter.getLinkFeature(position.getLinkFeature()); - var target = (AnnotationFS) link.getFeatureValue( - link.getType().getFeatureByBaseName(decl.getTargetFeature())); - - cfgLoop: for (var cfg : aSet.getConfigurations()) { - var repFS = cfg.getRepresentative(casses); - var repAID = cfg.getRepresentativeAID(); - var repLink = FSUtil.getFeature(repFS, - repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) - .get(repAID.index); - var repTarget = (AnnotationFS) repLink.getFeatureValue( - repLink.getType().getFeatureByBaseName(decl.getTargetFeature())); - - // Compare targets - if (equalsAnnotationFS(repTarget, target)) { - configuration = cfg; - break cfgLoop; - } - } - break; + // Check if this configuration is already present + Configuration configuration = null; + switch (position.getLinkFeatureMultiplicityMode()) { + case ONE_TARGET_MULTIPLE_ROLES: { + var role = link + .getStringValue(link.getType().getFeatureByBaseName(decl.getRoleFeature())); + if (!Objects.equals(role, position.getLinkRole())) { + continue linkLoop; } - case MULTIPLE_TARGETS_ONE_ROLE: { - var target = (AnnotationFS) link.getFeatureValue( - link.getType().getFeatureByBaseName(decl.getTargetFeature())); - if (!(target.getBegin() == position.getLinkTargetBegin() - && target.getEnd() == position.getLinkTargetEnd())) { - continue; - } - var role = link.getStringValue( - link.getType().getFeatureByBaseName(decl.getRoleFeature())); - - cfgLoop: for (Configuration cfg : aSet.getConfigurations()) { - var repFS = cfg.getRepresentative(casses); - var repAID = cfg.getRepresentativeAID(); - var repLink = FSUtil.getFeature(repFS, - repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) - .get(repAID.index); - var linkRole = repLink.getStringValue( - repLink.getType().getFeatureByBaseName(decl.getRoleFeature())); - - // Compare roles - if (role.equals(linkRole)) { - configuration = cfg; - break cfgLoop; - } + var target = (Annotation) link.getFeatureValue( + link.getType().getFeatureByBaseName(decl.getTargetFeature())); + + cfgLoop: for (var cfg : aSet.getConfigurations()) { + var repFS = cfg.getRepresentative(casses); + var repAID = cfg.getRepresentativeAID(); + var repLink = getFeature(repFS, + repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) + .get(repAID.index); + var repTarget = (Annotation) repLink.getFeatureValue( + repLink.getType().getFeatureByBaseName(decl.getTargetFeature())); + + // Compare targets + if (samePosition(repTarget, target)) { + configuration = cfg; + break cfgLoop; } - break; } - case MULTIPLE_TARGETS_MULTIPLE_ROLES: { - var target = (AnnotationFS) link.getFeatureValue( - link.getType().getFeatureByBaseName(decl.getTargetFeature())); - if (!(target.getBegin() == position.getLinkTargetBegin() - && target.getEnd() == position.getLinkTargetEnd())) { - continue; - } - - var role = link.getStringValue( - link.getType().getFeatureByBaseName(decl.getRoleFeature())); - if (!role.equals(position.getRole())) { - continue; - } + break; + } + case MULTIPLE_TARGETS_ONE_ROLE: { + var target = (AnnotationFS) link.getFeatureValue( + link.getType().getFeatureByBaseName(decl.getTargetFeature())); + if (!(target.getBegin() == position.getLinkTargetBegin() + && target.getEnd() == position.getLinkTargetEnd())) { + continue linkLoop; + } - cfgLoop: for (Configuration cfg : aSet.getConfigurations()) { - var repFS = cfg.getRepresentative(casses); - var repAID = cfg.getRepresentativeAID(); - var repLink = FSUtil.getFeature(repFS, - repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) - .get(repAID.index); - var linkRole = repLink.getStringValue( - repLink.getType().getFeatureByBaseName(decl.getRoleFeature())); - var repTarget = (AnnotationFS) repLink.getFeatureValue( - repLink.getType().getFeatureByBaseName(decl.getTargetFeature())); - - // Compare role and target - if (role.equals(linkRole) && equalsAnnotationFS(repTarget, target)) { - configuration = cfg; - break cfgLoop; - } + var role = link + .getStringValue(link.getType().getFeatureByBaseName(decl.getRoleFeature())); + + cfgLoop: for (var cfg : aSet.getConfigurations()) { + var repFS = cfg.getRepresentative(casses); + var repAID = cfg.getRepresentativeAID(); + var repLink = getFeature(repFS, + repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) + .get(repAID.index); + var linkRole = repLink.getStringValue( + repLink.getType().getFeatureByBaseName(decl.getRoleFeature())); + + // Compare roles + if (Objects.equals(role, linkRole)) { + configuration = cfg; + break cfgLoop; } - break; } - default: - throw new IllegalStateException("Unknown link target comparison mode [" - + position.getLinkCompareBehavior() + "]"); + break; + } + case MULTIPLE_TARGETS_MULTIPLE_ROLES: { + var target = (Annotation) link.getFeatureValue( + link.getType().getFeatureByBaseName(decl.getTargetFeature())); + if (!(target.getBegin() == position.getLinkTargetBegin() + && target.getEnd() == position.getLinkTargetEnd())) { + continue linkLoop; } - // Not found, add new one - if (configuration == null) { - configuration = new Configuration(position); - aSet.addConfiguration(configuration); + var role = link + .getStringValue(link.getType().getFeatureByBaseName(decl.getRoleFeature())); + if (!Objects.equals(role, position.getLinkRole())) { + continue linkLoop; } - configuration.add(aCasGroupId, aFS, position.getFeature(), i); + cfgLoop: for (var cfg : aSet.getConfigurations()) { + var repFS = cfg.getRepresentative(casses); + var repAID = cfg.getRepresentativeAID(); + var repLink = getFeature(repFS, + repFS.getType().getFeatureByBaseName(decl.getName()), ArrayFS.class) + .get(repAID.index); + var linkRole = repLink.getStringValue( + repLink.getType().getFeatureByBaseName(decl.getRoleFeature())); + var repTarget = (Annotation) repLink.getFeatureValue( + repLink.getType().getFeatureByBaseName(decl.getTargetFeature())); + + // Compare role and target + if (Objects.equals(role, linkRole) && samePosition(repTarget, target)) { + configuration = cfg; + break cfgLoop; + } + } + break; + } + default: + throw new IllegalStateException("Unknown link feature multiplicity mode [" + + position.getLinkFeatureMultiplicityMode() + "]"); } - } - aSet.addCasGroupId(aCasGroupId); - } + // Not found, add new one + if (configuration == null) { + configuration = new Configuration(position); + aSet.addConfiguration(configuration); + } - public Collection getPositions() - { - return configSets.keySet(); + configuration.add(aCasGroupId, aFS, position.getLinkFeature(), i); + aSet.addCasGroupId(aCasGroupId); + } } - /** - * @param aPosition - * a position. - * @return the configuration set for the given position. - */ - public ConfigurationSet getConfigurationSet(Position aPosition) + private void addBaseConfiguration(ConfigurationSet aSet, String aCasGroupId, + FeatureStructure aFS) { - return configSets.get(aPosition); + // Check if this configuration is already present + Configuration configuration = null; + for (var cfg : aSet.getConfigurations()) { + if (equalsFS(cfg.getRepresentative(casses), aFS)) { + configuration = cfg; + break; + } + } + + // Not found, add new one + if (configuration == null) { + configuration = new Configuration(aSet.getPosition()); + aSet.addConfiguration(configuration); + } + + configuration.add(aCasGroupId, aFS); + aSet.addCasGroupId(aCasGroupId); } /** @@ -483,326 +490,197 @@ private boolean equalsFS(FeatureStructure aFS1, FeatureStructure aFS2) return true; } + var type1Features = type1.getFeatures().stream().map(Feature::getShortName); + var type2Features = type2.getFeatures().stream().map(Feature::getShortName); + // Only consider label features. In particular these must not include position features // such as begin, end, etc. Mind that the types may come from different CASes at different // levels of upgrading, so it could be that the types actually have slightly different // features. - var labelFeatures = adapter.getLabelFeatures(); - var sortedFeatures = concat(type1.getFeatures().stream().map(Feature::getShortName), - type2.getFeatures().stream().map(Feature::getShortName)) // - .filter(labelFeatures::contains) // - .sorted() // - .distinct() // - .collect(toCollection(ArrayList::new)); - - if (!recurseIntoLinkFeatures) { - // #1795 Chili REC: We can/should change CasDiff2 such that it does not recurse into - // link features (or rather into any features that are covered by their own - // sub-positions). So when when comparing two spans that differ only in their slots - // (sub-positions) the main position could still exhibit agreement. - sortedFeatures.removeIf(f -> adapter.getLinkFeature(f) != null); - } + var labelFeatures = adapter.getFeatures(); + var linkFeatures = adapter.getLinkFeatures(); + var sortedFeatures = concat(type1Features, type2Features) // + .filter(f -> labelFeatures.contains(f) || linkFeatures.contains(f)) // + .sorted() // + .distinct() // + .collect(toCollection(ArrayList::new)); nextFeature: for (var feature : sortedFeatures) { - var f1 = type1.getFeatureByBaseName(feature); - var f2 = type2.getFeatureByBaseName(feature); - - Type range = (f1 != null) ? f1.getRange() : (f2 != null ? f2.getRange() : null); - - // If both feature structures do not declare the feature, then they must have the same - // value (no value) - if (range == null) { - continue nextFeature; - } - - // If both features are declared but their range differs, then the comparison is false - if (f1 != null && f2 != null && !f1.getRange().equals(f2.getRange())) { - return false; - } - - // When we get here, f1 or f2 can still be null - - switch (range.getName()) { - case CAS.TYPE_NAME_STRING_ARRAY: { - var value1 = f1 != null ? FSUtil.getFeature(aFS1, f1, Set.class) : null; - if (value1 == null) { - value1 = emptySet(); - } - var value2 = f2 != null ? FSUtil.getFeature(aFS2, f2, Set.class) : null; - if (value2 == null) { - value2 = emptySet(); - } - if (!value1.equals(value2)) { - return false; - } - break; - } - case CAS.TYPE_NAME_BOOLEAN: { - boolean value1 = f1 != null ? aFS1.getBooleanValue(f1) : false; - boolean value2 = f2 != null ? aFS2.getBooleanValue(f2) : false; - - if (value1 != value2) { - return false; - } - break; - } - case CAS.TYPE_NAME_BYTE: { - byte value1 = f1 != null ? aFS1.getByteValue(f1) : 0; - byte value2 = f2 != null ? aFS2.getByteValue(f2) : 0; - - if (value1 != value2) { - return false; - } - break; - } - case CAS.TYPE_NAME_DOUBLE: { - double value1 = f1 != null ? aFS1.getDoubleValue(f1) : 0.0d; - double value2 = f2 != null ? aFS2.getDoubleValue(f2) : 0.0d; - - if (value1 != value2) { + var featureValueEqual = equalsPrimitiveOrMultiValueFeature(aFS1, aFS2, feature); + if (featureValueEqual != null) { + if (!featureValueEqual) { return false; } - break; - } - case CAS.TYPE_NAME_FLOAT: { - float value1 = f1 != null ? aFS1.getFloatValue(f1) : 0.0f; - float value2 = f2 != null ? aFS2.getFloatValue(f2) : 0.0f; - if (value1 != value2) { - return false; - } - break; - } - case CAS.TYPE_NAME_INTEGER: { - int value1 = f1 != null ? aFS1.getIntValue(f1) : 0; - int value2 = f2 != null ? aFS2.getIntValue(f2) : 0; - - if (value1 != value2) { - return false; - } - break; - } - case CAS.TYPE_NAME_LONG: { - long value1 = f1 != null ? aFS1.getLongValue(f1) : 0l; - long value2 = f2 != null ? aFS2.getLongValue(f2) : 0l; - - if (value1 != value2) { - return false; - } - break; + continue nextFeature; } - case CAS.TYPE_NAME_SHORT: { - short value1 = f1 != null ? aFS1.getShortValue(f1) : 0; - short value2 = f2 != null ? aFS2.getShortValue(f2) : 0; - if (value1 != value2) { + if (adapter.isIncludeInDiff(feature)) { + var lfd = adapter.getLinkFeature(feature); + var links1 = toMaterializedLinks(aFS1, feature, lfd.getRoleFeature(), + lfd.getTargetFeature()); + var links2 = toMaterializedLinks(aFS2, feature, lfd.getRoleFeature(), + lfd.getTargetFeature()); + var linksEqual = disjunction(links1, links2).isEmpty(); + if (!linksEqual) { return false; } - break; - } - case CAS.TYPE_NAME_STRING: { - String value1 = f1 != null ? aFS1.getStringValue(f1) : null; - String value2 = f2 != null ? aFS2.getStringValue(f2) : null; - if (!StringUtils.equals(value1, value2)) { - return false; - } - break; - } - default: { - // Must be some kind of feature structure then - var valueFS1 = f1 != null ? aFS1.getFeatureValue(f1) : null; - var valueFS2 = f2 != null ? aFS2.getFeatureValue(f2) : null; - - // Ignore the SofaFS - we already checked that the CAS is the same. - if (valueFS1 instanceof SofaFS) { - continue; - } + continue nextFeature; - // If the feature value is an annotation, we just check the position is the same, - // but we do not go in deeper. If we we wanted to know differences on this type, - // then it should have been added as an entry type. - // - // Q: Why do we not check if they are the same based on the CAS address? - // A: Because we are checking across CASes and addresses can differ. + // // #1795 Chili REC: We can/should change CasDiff2 such that it does not recurse + // into + // // link features (or rather into any features that are covered by their own + // // sub-positions). So when when comparing two spans that differ only in their + // slots + // // (sub-positions) the main position could still exhibit agreement. + // if (!equalsNonPrimitiveFeature(aFS1, aFS2, feature)) { + // return false; + // } // - // Q: Why do we not check recursively? - // A: Because e.g. for chains, this would mean we consider the whole chain as a - // single annotation, but we want to consider each link as an annotation - var ts1 = aFS1.getCAS().getTypeSystem(); - if (ts1.subsumes(ts1.getType(CAS.TYPE_NAME_ANNOTATION), type1)) { - if (!equalsAnnotationFS((AnnotationFS) aFS1, (AnnotationFS) aFS2)) { - return false; - } - } - - // If the feature type is not an annotation we are still in the "feature tier" - // just dealing with structured features. It is ok to check these deeply. - if (!equalsFS(valueFS1, valueFS2)) { - return false; - } - } + // continue nextFeature; } } return true; } - private boolean equalsAnnotationFS(AnnotationFS aFS1, AnnotationFS aFS2) + private boolean equalsNonPrimitiveFeature(FeatureStructure aFS1, FeatureStructure aFS2, + String feature) { - // Null check - if (aFS1 == null || aFS2 == null) { - return false; - } - - // Position check - var adapter = getAdapter(aFS1.getType().getName()); - var pos1 = adapter.getPosition(aFS1); - var pos2 = adapter.getPosition(aFS2); + // Must be some kind of feature structure then + var f1 = aFS1.getType().getFeatureByBaseName(feature); + var f2 = aFS2.getType().getFeatureByBaseName(feature); + var valueFS1 = f1 != null ? aFS1.getFeatureValue(f1) : null; + var valueFS2 = f2 != null ? aFS2.getFeatureValue(f2) : null; - return pos1.compareTo(pos2) == 0; - } - - public static List getDiffAdapters(AnnotationSchemaService schemaService, - Collection aLayers) - { - if (aLayers.isEmpty()) { - return emptyList(); + if (valueFS1 == null && valueFS2 == null) { + return true; } - var project = aLayers.iterator().next().getProject(); + if (valueFS1 == null || valueFS2 == null) { + return false; + } - var featuresByLayer = schemaService.listSupportedFeatures(project).stream() // - .collect(groupingBy(AnnotationFeature::getLayer)); + // Ignore the SofaFS - we already checked that the CAS is the same. + if (valueFS1 instanceof SofaFS) { + return true; + } - var adapters = new ArrayList(); - nextLayer: for (var layer : aLayers) { - if (!layer.isEnabled()) { - continue nextLayer; + // If the feature value is an annotation, we just check the position is the same, + // but we do not go in deeper. If we we wanted to know differences on this type, + // then it should have been added as an entry type. + // + // Q: Why do we not check if they are the same based on the CAS address? + // A: Because we are checking across CASes and addresses can differ. + // + // Q: Why do we not check recursively? + // A: Because e.g. for chains, this would mean we consider the whole chain as a + // single annotation, but we want to consider each link as an annotation + if (valueFS1 instanceof Annotation ann1 && valueFS1 instanceof Annotation ann2) { + if (!samePosition(ann1, ann2)) { + return false; } + } - var labelFeatures = new LinkedHashSet(); - nextFeature: for (var f : featuresByLayer.getOrDefault(layer, emptyList())) { - if (!f.isEnabled() || !f.isCuratable()) { - continue nextFeature; - } - - // Link features are treated separately from primitive label features - if (!NONE.equals(f.getLinkMode())) { - continue nextFeature; - } + // If the feature type is not an annotation we are still in the "feature tier" + // just dealing with structured features. It is ok to check these deeply. + if (!equalsFS(valueFS1, valueFS2)) { + return false; + } - labelFeatures.add(f.getName()); - } + return true; + } - DiffAdapter_ImplBase adapter; - switch (layer.getType()) { - case SpanLayerSupport.TYPE: { - adapter = new SpanDiffAdapter(layer.getName(), labelFeatures); - break; - } - case RelationLayerSupport.TYPE: { - var typeAdpt = (RelationAdapter) schemaService.getAdapter(layer); - adapter = new RelationDiffAdapter(layer.getName(), typeAdpt.getSourceFeatureName(), - typeAdpt.getTargetFeatureName(), labelFeatures); - break; - } - case DocumentMetadataLayerSupport.TYPE: { - adapter = new DocumentMetadataDiffAdapter(layer.getName(), labelFeatures); - break; - } - default: - LOG.debug("Curation for layer type [{}] not supported - ignoring", layer.getType()); - continue nextLayer; - } + private Boolean equalsPrimitiveOrMultiValueFeature(FeatureStructure aFS1, FeatureStructure aFS2, + String feature) + { + var f1 = aFS1.getType().getFeatureByBaseName(feature); + var f2 = aFS2.getType().getFeatureByBaseName(feature); - adapters.add(adapter); + var range = (f1 != null) ? f1.getRange() : (f2 != null ? f2.getRange() : null); - nextFeature: for (var f : featuresByLayer.getOrDefault(layer, emptyList())) { - if (!f.isEnabled()) { - continue nextFeature; - } + // If none of the feature structures declare the feature, then they must have the same + // value (no value) + if (range == null) { + return true; + } - switch (f.getLinkMode()) { - case NONE: - // Nothing to do here - break; - case SIMPLE: - adapter.addLinkFeature(f.getName(), f.getLinkTypeRoleFeatureName(), - f.getLinkTypeTargetFeatureName(), ONE_TARGET_MULTIPLE_ROLES); - break; - case WITH_ROLE: { - var typeAdpt = schemaService.getAdapter(layer); - var traits = typeAdpt.getFeatureTraits(f, LinkFeatureTraits.class) - .orElse(new LinkFeatureTraits()); - adapter.addLinkFeature(f.getName(), f.getLinkTypeRoleFeatureName(), - f.getLinkTypeTargetFeatureName(), traits.getCompareMode()); - break; - } - default: - throw new IllegalStateException("Unknown link mode [" + f.getLinkMode() + "]"); - } + // If both features are declared but their range differs, then the comparison is false + if (f1 != null && f2 != null && !f1.getRange().equals(f2.getRange())) { + return false; + } - labelFeatures.add(f.getName()); + // When we get here, f1 or f2 can still be null + switch (range.getName()) { + case TYPE_NAME_STRING_ARRAY: { + var value1 = f1 != null ? getFeature(aFS1, f1, Set.class) : null; + if (value1 == null) { + value1 = emptySet(); } + var value2 = f2 != null ? getFeature(aFS2, f2, Set.class) : null; + if (value2 == null) { + value2 = emptySet(); + } + return value1.equals(value2); } - - // If the token/sentence layer is not editable, we do not offer curation of the tokens. - // Instead the tokens are obtained from a random template CAS when initializing the CAS - we - // assume here that the tokens have never been modified. - if (!schemaService.isSentenceLayerEditable(project)) { - adapters.removeIf(adapter -> Sentence._TypeName.equals(adapter.getType())); + case TYPE_NAME_BOOLEAN: { + boolean value1 = f1 != null ? aFS1.getBooleanValue(f1) : false; + boolean value2 = f2 != null ? aFS2.getBooleanValue(f2) : false; + return value1 == value2; } - - if (!schemaService.isTokenLayerEditable(project)) { - adapters.removeIf(adapter -> Token._TypeName.equals(adapter.getType())); + case TYPE_NAME_BYTE: { + byte value1 = f1 != null ? aFS1.getByteValue(f1) : 0; + byte value2 = f2 != null ? aFS2.getByteValue(f2) : 0; + return value1 == value2; + } + case TYPE_NAME_DOUBLE: { + double value1 = f1 != null ? aFS1.getDoubleValue(f1) : 0.0d; + double value2 = f2 != null ? aFS2.getDoubleValue(f2) : 0.0d; + return value1 == value2; + } + case TYPE_NAME_FLOAT: { + float value1 = f1 != null ? aFS1.getFloatValue(f1) : 0.0f; + float value2 = f2 != null ? aFS2.getFloatValue(f2) : 0.0f; + return value1 == value2; } + case TYPE_NAME_INTEGER: { + int value1 = f1 != null ? aFS1.getIntValue(f1) : 0; + int value2 = f2 != null ? aFS2.getIntValue(f2) : 0; + return value1 == value2; + } + case TYPE_NAME_LONG: { + long value1 = f1 != null ? aFS1.getLongValue(f1) : 0l; + long value2 = f2 != null ? aFS2.getLongValue(f2) : 0l; + return value1 == value2; + } + case TYPE_NAME_SHORT: { + short value1 = f1 != null ? aFS1.getShortValue(f1) : 0; + short value2 = f2 != null ? aFS2.getShortValue(f2) : 0; + return value1 == value2; + } + case TYPE_NAME_STRING: { + var value1 = f1 != null ? aFS1.getStringValue(f1) : null; + var value2 = f2 != null ? aFS2.getStringValue(f2) : null; - return adapters; + return Objects.equals(value1, value2); + } + } + return null; } - public DiffResult toResult() + private boolean samePosition(Annotation aFS1, Annotation aFS2) { - return new DiffResult(this); - } + // Null check + if (aFS1 == null || aFS2 == null) { + return false; + } - // private Set entryTypes = new LinkedHashSet<>(); - - // /** - // * Clear the attachment to CASes allowing the class to be serialized. - // */ - // public void detach() - // { - // if (cases != null) { - // cases.clear(); - // } - // } - - // /** - // * Rebuilds the diff with the current offsets and entry types. This can be used to fix the - // diff - // * after reattaching to CASes that have changed. Mind that the diff results can be different - // * due to the changes. - // */ - // public void rebuild() - // { - // Map oldCases = cases; - // cases = new HashMap<>(); - // - // for (String t : entryTypes) { - // for (Entry e : oldCases.entrySet()) { - // addCas(e.getKey(), e.getValue(), t); - // } - // } - // } - - // /** - // * Attach CASes back so that representatives can be resolved. CASes must not have been changed - // * or upgraded between detaching and reattaching - the CAS addresses of the feature structures - // * must still be the same. - // */ - // public void attach(Map aCases) - // { - // cases = new HashMap<>(aCases); - // } + // Position check + var adapter = getAdapter(aFS1.getType().getName()); + var pos1 = adapter.getPosition(aFS1); + var pos2 = adapter.getPosition(aFS2); + + return pos1.compareTo(pos2) == 0; + } } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/Configuration.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/Configuration.java index c794bccea2a..6dd97b769f4 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/Configuration.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/Configuration.java @@ -46,13 +46,7 @@ public class Configuration private final Position position; final Map fsAddresses = new TreeMap<>(); - private Map> extras; - - /** - * Flag indicating that there is at least once CAS group containing more than one annotation at - * this position - i.e. a stacked annotation. - */ - private boolean stacked = false; + private Map> duplicates; public String getRepresentativeCasGroupId() { @@ -74,9 +68,13 @@ public Position getPosition() return position; } - public boolean isStacked() + /** + * @return flag indicating that there is at least once CAS group containing more than one + * annotation with the same values at this position - i.e. a duplicate annotation. + */ + public boolean containsDuplicates() { - return stacked; + return duplicates != null; } /** @@ -87,12 +85,11 @@ public void add(String aCasGroupId, AID aAID) { var old = fsAddresses.put(aCasGroupId, aAID); if (old != null) { - if (extras == null) { - extras = new TreeMap<>(); + if (duplicates == null) { + duplicates = new TreeMap<>(); } - var list = extras.computeIfAbsent(aCasGroupId, $ -> new ArrayList()); + var list = duplicates.computeIfAbsent(aCasGroupId, $ -> new ArrayList()); list.add(old); - stacked = true; } } @@ -136,11 +133,11 @@ public boolean contains(String aCasGroupId, AID aAID) return true; } - if (extras == null) { + if (duplicates == null) { return false; } - var list = extras.get(aCasGroupId); + var list = duplicates.get(aCasGroupId); if (list == null) { return false; } @@ -184,8 +181,8 @@ private List getFses(String aCasG var allFs = new ArrayList(); allFs.add(ICasUtil.selectFsByAddr(cas, aid.addr)); - if (extras != null) { - var list = extras.get(aCasGroupId); + if (duplicates != null) { + var list = duplicates.get(aCasGroupId); if (list != null) { for (var eAid : list) { allFs.add(ICasUtil.selectFsByAddr(cas, eAid.addr)); @@ -213,9 +210,9 @@ public String toString() sb.append(e.getKey()); sb.append(": "); sb.append(e.getValue()); - if (extras != null) { - sb.append(" (extras: "); - for (var entries : extras.entrySet()) { + if (duplicates != null) { + sb.append(" (duplicates: "); + for (var entries : duplicates.entrySet()) { sb.append(" {"); sb.append(entries.getKey()); sb.append(entries.getValue().stream().map(String::valueOf) diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/ConfigurationSet.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/ConfigurationSet.java index 8d54ee6714c..ab79352ff0f 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/ConfigurationSet.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/ConfigurationSet.java @@ -84,6 +84,8 @@ public void addValue(String aDataOwner, Object aValue) } /** + * @param aDataOwner + * owner of the values to be retrieved * @return values added during agreement calculation (not by {@link CasDiff}!) */ public Set getValues(String aDataOwner) @@ -164,6 +166,25 @@ public Position getPosition() return position; } + /** + * @return flag indicating that there is at least once CAS group containing more than one + * annotation at this position - i.e. a stacked annotation. + */ + public boolean containsStackedConfigurations() + { + // User has annotated the position with multiple different configurations + for (var casGroupId : getCasGroupIds()) { + if (getConfigurations(casGroupId).size() > 1) { + return true; + } + } + + // User has annotate the position multiple times with the same configuration + return getConfigurations().stream() // + .filter(Configuration::containsDuplicates) // + .findAny().isPresent(); + } + @Override public String toString() { diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffAdapterRegistry.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffAdapterRegistry.java new file mode 100644 index 00000000000..b4f17198856 --- /dev/null +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffAdapterRegistry.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff; + +import static de.tudarmstadt.ukp.clarin.webanno.model.LinkMode.NONE; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.EXCLUDE; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.groupingBy; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter_ImplBase; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.docmeta.DocumentMetadataDiffAdapter; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationDiffAdapter; +import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureTraits; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; + +public class DiffAdapterRegistry +{ + private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static List getDiffAdapters(AnnotationSchemaService aSchemaService, + Collection aLayers) + { + if (aLayers.isEmpty()) { + return emptyList(); + } + + var project = aLayers.iterator().next().getProject(); + + var featuresByLayer = aSchemaService.listSupportedFeatures(project).stream() // + .collect(groupingBy(AnnotationFeature::getLayer)); + + var adapters = new ArrayList(); + nextLayer: for (var layer : aLayers) { + if (!layer.isEnabled()) { + continue nextLayer; + } + + var featuresToCompare = collectFeaturesToCompare(featuresByLayer, layer); + + DiffAdapter_ImplBase adapter; + switch (layer.getType()) { + case SpanLayerSupport.TYPE: { + adapter = new SpanDiffAdapter(layer.getName(), featuresToCompare); + break; + } + case RelationLayerSupport.TYPE: { + var typeAdpt = (RelationAdapter) aSchemaService.getAdapter(layer); + adapter = new RelationDiffAdapter(layer.getName(), typeAdpt.getSourceFeatureName(), + typeAdpt.getTargetFeatureName(), featuresToCompare); + break; + } + case DocumentMetadataLayerSupport.TYPE: { + adapter = new DocumentMetadataDiffAdapter(layer.getName(), featuresToCompare); + break; + } + default: + LOG.debug("Layer type [{}] not supported - ignoring", layer.getType()); + continue nextLayer; + } + + adapters.add(adapter); + + nextFeature: for (var f : featuresByLayer.getOrDefault(layer, emptyList())) { + if (!f.isEnabled()) { + continue nextFeature; + } + + switch (f.getLinkMode()) { + case NONE: + // Nothing to do here + break; + case SIMPLE: + adapter.addLinkFeature(f.getName(), f.getLinkTypeRoleFeatureName(), + f.getLinkTypeTargetFeatureName(), ONE_TARGET_MULTIPLE_ROLES, EXCLUDE); + break; + case WITH_ROLE: { + var typeAdpt = aSchemaService.getAdapter(layer); + var traits = typeAdpt.getFeatureTraits(f, LinkFeatureTraits.class) + .orElse(new LinkFeatureTraits()); + adapter.addLinkFeature(f.getName(), f.getLinkTypeRoleFeatureName(), + f.getLinkTypeTargetFeatureName(), traits.getMultiplicityMode(), + traits.getDiffMode()); + break; + } + default: + throw new IllegalStateException("Unknown link mode [" + f.getLinkMode() + "]"); + } + + featuresToCompare.add(f.getName()); + } + } + + // If the token/sentence layer is not editable, we do not offer curation of the tokens. + // Instead the tokens are obtained from a random template CAS when initializing the CAS - we + // assume here that the tokens have never been modified. + if (!aSchemaService.isSentenceLayerEditable(project)) { + adapters.removeIf(adapter -> Sentence._TypeName.equals(adapter.getType())); + } + + if (!aSchemaService.isTokenLayerEditable(project)) { + adapters.removeIf(adapter -> Token._TypeName.equals(adapter.getType())); + } + + return adapters; + } + + private static Set collectFeaturesToCompare( + Map> featuresByLayer, AnnotationLayer layer) + { + var features = new LinkedHashSet(); + + nextFeature: for (var f : featuresByLayer.getOrDefault(layer, emptyList())) { + if (!f.isEnabled() || !f.isCuratable()) { + continue nextFeature; + } + + // Link features are treated separately from primitive label features + if (!NONE.equals(f.getLinkMode())) { + continue nextFeature; + } + + features.add(f.getName()); + } + + return features; + } +} diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffResult.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffResult.java index 10f3ee00045..4f06b830c78 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffResult.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/DiffResult.java @@ -162,7 +162,7 @@ public boolean isAgreementWithExceptions(ConfigurationSet aConfigurationSet, return aConfigurationSet.getConfigurations().size() == 1; } - Set exceptions = new HashSet<>(asList(aCasGroupIDsToIgnore)); + var exceptions = new HashSet<>(asList(aCasGroupIDsToIgnore)); return aConfigurationSet.getConfigurations().stream() // Ignore configuration sets containing only exceptions and nothing else .filter(cfg -> !subtract(cfg.getCasGroupIds(), exceptions).isEmpty()) @@ -300,7 +300,8 @@ public void print(PrintStream aOut) for (var p : getPositions()) { var configurationSet = getConfigurationSet(p); aOut.printf("=== %s -> %s %s%n", p, - isAgreement(configurationSet) ? "AGREE" : "DISAGREE", + configurationSet.containsStackedConfigurations() ? "STACKED" + : isAgreement(configurationSet) ? "AGREE" : "DISAGREE", isComplete(configurationSet) ? "COMPLETE" : "INCOMPLETE"); for (var cfg : configurationSet.getConfigurations()) { aOut.printf(" %s%n", cfg); diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/LinkFeatureDecl.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/LinkFeatureDecl.java index 5ebba568893..d1e422a8eef 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/LinkFeatureDecl.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/LinkFeatureDecl.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff; +import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode; import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; public class LinkFeatureDecl @@ -24,15 +25,17 @@ public class LinkFeatureDecl private final String name; private final String roleFeature; private final String targetFeature; - private final LinkFeatureMultiplicityMode compareBehavior; + private final LinkFeatureMultiplicityMode multiplicityMode; + private LinkFeatureDiffMode diffMode; public LinkFeatureDecl(String aName, String aRoleFeature, String aTargetFeature, - LinkFeatureMultiplicityMode aCompareBehavior) + LinkFeatureMultiplicityMode aLinkFeatureMultiplicityMode, LinkFeatureDiffMode aDiffMode) { name = aName; roleFeature = aRoleFeature; targetFeature = aTargetFeature; - compareBehavior = aCompareBehavior; + multiplicityMode = aLinkFeatureMultiplicityMode; + diffMode = aDiffMode; } public String getName() @@ -50,9 +53,14 @@ public String getTargetFeature() return targetFeature; } - public LinkFeatureMultiplicityMode getCompareBehavior() + public LinkFeatureMultiplicityMode getMultiplicityMode() { - return compareBehavior; + return multiplicityMode; + } + + public LinkFeatureDiffMode getDiffMode() + { + return diffMode; } @Override @@ -69,6 +77,14 @@ public String toString() builder.append(", targetFeature="); builder.append(getTargetFeature()); } + if (getMultiplicityMode() != null) { + builder.append(", multiplicity="); + builder.append(getMultiplicityMode()); + } + if (getDiffMode() != null) { + builder.append(", diff="); + builder.append(getDiffMode()); + } builder.append("]"); return builder.toString(); } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter.java index 06bf9f22e3b..ed96d95e102 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter.java @@ -22,11 +22,9 @@ import java.util.Set; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.jcas.cas.AnnotationBase; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkFeatureDecl; -import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; public interface DiffAdapter { @@ -39,13 +37,22 @@ public interface DiffAdapter LinkFeatureDecl getLinkFeature(String aFeature); - Set getLabelFeatures(); + /** + * @return names of features that the adapter should compare. Link features are not included + * here. + */ + Set getFeatures(); - Position getPosition(FeatureStructure aFS); + /** + * @return names of link features that the adapter should compare. Non-link features are not + * included here. + */ + Set getLinkFeatures(); - Position getPosition(FeatureStructure aFS, String aFeature, String aRole, int aLinkTargetBegin, - int aLinkTargetEnd, LinkFeatureMultiplicityMode aLinkCompareBehavior); + Position getPosition(AnnotationBase aFS); List selectAnnotationsInWindow(CAS aCas, int aWindowBegin, int aWindowEnd); + + boolean isIncludeInDiff(String aFeature); } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter_ImplBase.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter_ImplBase.java index b6a72e06a82..6fdda65c56d 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter_ImplBase.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/DiffAdapter_ImplBase.java @@ -21,16 +21,13 @@ import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; -import org.apache.uima.cas.ArrayFS; -import org.apache.uima.cas.FeatureStructure; -import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.fit.util.FSUtil; -import org.apache.uima.jcas.cas.AnnotationBase; - import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkFeatureDecl; +import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode; import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; public abstract class DiffAdapter_ImplBase @@ -38,21 +35,21 @@ public abstract class DiffAdapter_ImplBase { private final String type; - private final Set labelFeatures; + private final Set features; - private final List linkFeatures = new ArrayList<>(); + private final Map linkFeatures = new LinkedHashMap<>(); - public DiffAdapter_ImplBase(String aType, Set aLabelFeatures) + public DiffAdapter_ImplBase(String aType, Set aFeatures) { type = aType; - labelFeatures = unmodifiableSet(new HashSet<>(aLabelFeatures)); + features = unmodifiableSet(new HashSet<>(aFeatures)); } public void addLinkFeature(String aName, String aRoleFeature, String aTargetFeature, - LinkFeatureMultiplicityMode aCompareBehavior) + LinkFeatureMultiplicityMode aCompareBehavior, LinkFeatureDiffMode aDiffMode) { - linkFeatures - .add(new LinkFeatureDecl(aName, aRoleFeature, aTargetFeature, aCompareBehavior)); + linkFeatures.put(aName, new LinkFeatureDecl(aName, aRoleFeature, aTargetFeature, + aCompareBehavior, aDiffMode)); } @Override @@ -62,15 +59,26 @@ public String getType() } @Override - public Set getLabelFeatures() + public Set getFeatures() { - return labelFeatures; + return features; + } + + @Override + public Set getLinkFeatures() + { + return linkFeatures.keySet(); + } + + public List getLinkFeaturesDecls() + { + return new ArrayList<>(linkFeatures.values()); } @Override public LinkFeatureDecl getLinkFeature(String aFeature) { - for (var decl : linkFeatures) { + for (var decl : linkFeatures.values()) { if (decl.getName().equals(aFeature)) { return decl; } @@ -79,35 +87,12 @@ public LinkFeatureDecl getLinkFeature(String aFeature) } @Override - public Position getPosition(FeatureStructure aFS) - { - return getPosition(aFS, null, null, -1, -1, null); - } - - @Override - public List generateSubPositions(AnnotationBase aFs) + public boolean isIncludeInDiff(String aFeature) { - var subPositions = new ArrayList(); - - for (var decl : linkFeatures) { - var linkFeature = aFs.getType().getFeatureByBaseName(decl.getName()); - var array = FSUtil.getFeature(aFs, linkFeature, ArrayFS.class); - - if (array == null) { - continue; - } - - for (var linkFS : array.toArray()) { - var role = linkFS.getStringValue( - linkFS.getType().getFeatureByBaseName(decl.getRoleFeature())); - var target = (AnnotationFS) linkFS.getFeatureValue( - linkFS.getType().getFeatureByBaseName(decl.getTargetFeature())); - var pos = getPosition(aFs, decl.getName(), role, target.getBegin(), target.getEnd(), - decl.getCompareBehavior()); - subPositions.add(pos); - } + var decl = getLinkFeature(aFeature); + if (decl == null) { + return false; } - - return subPositions; + return decl.getDiffMode() == LinkFeatureDiffMode.INCLUDE; } } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position.java index c4cbd40f53a..5afdb592e4e 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position.java @@ -34,12 +34,20 @@ public interface Position */ String getType(); + String getCollectionId(); + + String getDocumentId(); + + String toMinimalString(); + + boolean isLinkFeaturePosition(); + /** * @return the feature if this is a sub-position for a link feature. */ - String getFeature(); + String getLinkFeature(); - String getRole(); + String getLinkRole(); int getLinkTargetBegin(); @@ -48,13 +56,5 @@ public interface Position /** * @return the way in which links are compared and labels for links are generated. */ - LinkFeatureMultiplicityMode getLinkCompareBehavior(); - - String getCollectionId(); - - String getDocumentId(); - - String toMinimalString(); - - boolean isLinkFeaturePosition(); + LinkFeatureMultiplicityMode getLinkFeatureMultiplicityMode(); } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position_ImplBase.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position_ImplBase.java index a8aab15533a..7d869ec8ff3 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position_ImplBase.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/api/Position_ImplBase.java @@ -31,12 +31,10 @@ public abstract class Position_ImplBase private static final long serialVersionUID = -1237180459049008357L; private final String type; - private final String feature; - - private final LinkFeatureMultiplicityMode linkCompareBehavior; - - private final String role; + private final String linkFeature; + private final LinkFeatureMultiplicityMode linkFeatureMultiplicityMode; + private final String linkRole; private final int linkTargetBegin; private final int linkTargetEnd; @@ -46,21 +44,34 @@ public abstract class Position_ImplBase private final String linkTargetText; // END: For debugging only - not included in compareTo/hashCode/equals! + public Position_ImplBase(String aCollectionId, String aDocumentId, String aType) + { + type = aType; + collectionId = aCollectionId; + documentId = aDocumentId; + + linkFeature = null; + linkRole = null; + linkFeatureMultiplicityMode = null; + linkTargetBegin = -1; + linkTargetEnd = -1; + linkTargetText = null; + + } + public Position_ImplBase(String aCollectionId, String aDocumentId, String aType, String aFeature, String aRole, int aLinkTargetBegin, int aLinkTargetEnd, String aLinkTargetText, LinkFeatureMultiplicityMode aBehavior) { type = aType; - feature = aFeature; - - linkCompareBehavior = aBehavior; + collectionId = aCollectionId; + documentId = aDocumentId; - role = aRole; + linkFeature = aFeature; + linkRole = aRole; + linkFeatureMultiplicityMode = aBehavior; linkTargetBegin = aLinkTargetBegin; linkTargetEnd = aLinkTargetEnd; - - collectionId = aCollectionId; - documentId = aDocumentId; linkTargetText = aLinkTargetText; } @@ -71,21 +82,21 @@ public String getType() } @Override - public String getFeature() + public String getLinkFeature() { - return feature; + return linkFeature; } @Override - public String getRole() + public String getLinkRole() { - return role; + return linkRole; } @Override public boolean isLinkFeaturePosition() { - return getFeature() != null; + return getLinkFeature() != null; } @Override @@ -118,9 +129,9 @@ public String getDocumentId() } @Override - public LinkFeatureMultiplicityMode getLinkCompareBehavior() + public LinkFeatureMultiplicityMode getLinkFeatureMultiplicityMode() { - return linkCompareBehavior; + return linkFeatureMultiplicityMode; } @Override @@ -131,12 +142,13 @@ public int compareTo(Position aOther) return typeCmp; } - int featureCmp = ObjectUtils.compare(feature, aOther.getFeature()); + int featureCmp = ObjectUtils.compare(linkFeature, aOther.getLinkFeature()); if (featureCmp != 0) { return featureCmp; } - int linkCmpCmp = ObjectUtils.compare(linkCompareBehavior, aOther.getLinkCompareBehavior()); + int linkCmpCmp = ObjectUtils.compare(linkFeatureMultiplicityMode, + aOther.getLinkFeatureMultiplicityMode()); if (linkCmpCmp != 0) { // If the linkCompareBehavior is not the same, then we are dealing with different // positions @@ -145,17 +157,17 @@ public int compareTo(Position aOther) // If linkCompareBehavior is equal, then we still only have to continue if it is non- // null. - if (linkCompareBehavior == null) { + if (linkFeatureMultiplicityMode == null) { return linkCmpCmp; } // If we are dealing with sub-positions generated for link features, then we need to // check this, otherwise linkTargetBegin, linkTargetEnd, linkCompareBehavior, // feature and role are all unset. - switch (linkCompareBehavior) { + switch (linkFeatureMultiplicityMode) { case ONE_TARGET_MULTIPLE_ROLES: // Include role into position - return ObjectUtils.compare(role, aOther.getRole()); + return ObjectUtils.compare(linkRole, aOther.getLinkRole()); case MULTIPLE_TARGETS_ONE_ROLE: // Include target into position if (linkTargetBegin != aOther.getLinkTargetBegin()) { @@ -164,7 +176,7 @@ public int compareTo(Position aOther) return linkTargetEnd - aOther.getLinkTargetEnd(); case MULTIPLE_TARGETS_MULTIPLE_ROLES: - var roleCmp = ObjectUtils.compare(role, aOther.getRole()); + var roleCmp = ObjectUtils.compare(linkRole, aOther.getLinkRole()); if (roleCmp != 0) { return roleCmp; } @@ -177,7 +189,7 @@ public int compareTo(Position aOther) return linkTargetEnd - aOther.getLinkTargetEnd(); default: throw new IllegalStateException( - "Unknown link target comparison mode [" + linkCompareBehavior + "]"); + "Unknown link target comparison mode [" + linkFeatureMultiplicityMode + "]"); } } @@ -190,30 +202,31 @@ public boolean equals(final Object other) Position_ImplBase castOther = (Position_ImplBase) other; var result = Objects.equals(type, castOther.type) // - && Objects.equals(feature, castOther.feature) // - && Objects.equals(linkCompareBehavior, castOther.linkCompareBehavior); + && Objects.equals(linkFeature, castOther.linkFeature) // + && Objects.equals(linkFeatureMultiplicityMode, + castOther.linkFeatureMultiplicityMode); // If the base properties are equal, then we have to continue only linkCompareBehavior if it // is non-null. - if (!result && linkCompareBehavior == null) { + if (!result && linkFeatureMultiplicityMode == null) { return false; } - switch (linkCompareBehavior) { + switch (linkFeatureMultiplicityMode) { case ONE_TARGET_MULTIPLE_ROLES: // Include role into position - return Objects.equals(role, castOther.role); + return Objects.equals(linkRole, castOther.linkRole); case MULTIPLE_TARGETS_ONE_ROLE: // Include target into position return Objects.equals(linkTargetBegin, castOther.linkTargetBegin) // && Objects.equals(linkTargetEnd, castOther.linkTargetEnd); case MULTIPLE_TARGETS_MULTIPLE_ROLES: - return Objects.equals(role, castOther.role) // + return Objects.equals(linkRole, castOther.linkRole) // && Objects.equals(linkTargetBegin, castOther.linkTargetBegin) // && Objects.equals(linkTargetEnd, castOther.linkTargetEnd); default: throw new IllegalStateException( - "Unknown link target comparison mode [" + linkCompareBehavior + "]"); + "Unknown link target comparison mode [" + linkFeatureMultiplicityMode + "]"); } } @@ -222,17 +235,17 @@ public int hashCode() { var builder = new HashCodeBuilder() // .append(type) // - .append(feature) // - .append(linkCompareBehavior); + .append(linkFeature) // + .append(linkFeatureMultiplicityMode); - if (linkCompareBehavior == null) { + if (linkFeatureMultiplicityMode == null) { return builder.toHashCode(); } - switch (linkCompareBehavior) { + switch (linkFeatureMultiplicityMode) { case ONE_TARGET_MULTIPLE_ROLES: // Include role into position - builder.append(role); + builder.append(linkRole); break; case MULTIPLE_TARGETS_ONE_ROLE: // Include target into position @@ -240,13 +253,13 @@ public int hashCode() builder.append(linkTargetEnd); break; case MULTIPLE_TARGETS_MULTIPLE_ROLES: - builder.append(role); + builder.append(linkRole); builder.append(linkTargetBegin); builder.append(linkTargetEnd); break; default: throw new IllegalStateException( - "Unknown link target comparison mode [" + linkCompareBehavior + "]"); + "Unknown link target comparison mode [" + linkFeatureMultiplicityMode + "]"); } return builder.toHashCode(); @@ -269,13 +282,13 @@ protected void toStringFragment(StringBuilder builder) else { builder.append(getType()); } - if (getFeature() != null) { + if (getLinkFeature() != null) { builder.append(", linkFeature="); - builder.append(getFeature()); - switch (getLinkCompareBehavior()) { + builder.append(getLinkFeature()); + switch (getLinkFeatureMultiplicityMode()) { case ONE_TARGET_MULTIPLE_ROLES: builder.append(", role="); - builder.append(getRole()); + builder.append(getLinkRole()); break; case MULTIPLE_TARGETS_ONE_ROLE: builder.append(", linkTarget=("); @@ -285,7 +298,7 @@ protected void toStringFragment(StringBuilder builder) break; case MULTIPLE_TARGETS_MULTIPLE_ROLES: builder.append(", role="); - builder.append(getRole()); + builder.append(getLinkRole()); builder.append(", linkTarget=("); builder.append(getLinkTargetBegin()).append('-').append(getLinkTargetEnd()); builder.append(')'); diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentMetadataDiffAdapter.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentMetadataDiffAdapter.java index b480a9b5b3b..da6c9d8f035 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentMetadataDiffAdapter.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentMetadataDiffAdapter.java @@ -19,20 +19,20 @@ import static java.util.Arrays.asList; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.apache.uima.cas.ArrayFS; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; +import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.cas.AnnotationBase; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; -import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanRenderer; -import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; public class DocumentMetadataDiffAdapter extends DiffAdapter_ImplBase @@ -58,29 +58,42 @@ public List selectAnnotationsInWindow(CAS aCas, int aWindowBegin } @Override - public Position getPosition(FeatureStructure aFS, String aFeature, String aRole, - int aLinkTargetBegin, int aLinkTargetEnd, - LinkFeatureMultiplicityMode aLinkCompareBehavior) + public Position getPosition(AnnotationBase aFS) { - String collectionId = null; - String documentId = null; - try { - var dmd = WebAnnoCasUtil.getDocumentMetadata(aFS.getCAS()); - collectionId = FSUtil.getFeature(dmd, "collectionId", String.class); - documentId = FSUtil.getFeature(dmd, "documentId", String.class); - } - catch (IllegalArgumentException e) { - // We use this information only for debugging - so we can ignore if the information - // is missing. - } + return DocumentPosition.builder() // + .forAnnotation(aFS) // + .build(); + } + + @Override + public List generateSubPositions(AnnotationBase aFs) + { + var subPositions = new ArrayList(); + + for (var decl : getLinkFeaturesDecls()) { + var linkFeature = aFs.getType().getFeatureByBaseName(decl.getName()); + var array = FSUtil.getFeature(aFs, linkFeature, ArrayFS.class); + + if (array == null) { + continue; + } - String linkTargetText = null; - if (aLinkTargetBegin != -1 && aFS.getCAS().getDocumentText() != null) { - linkTargetText = aFS.getCAS().getDocumentText().substring(aLinkTargetBegin, - aLinkTargetEnd); + for (var linkFS : array.toArray()) { + var role = linkFS.getStringValue( + linkFS.getType().getFeatureByBaseName(decl.getRoleFeature())); + var target = (AnnotationFS) linkFS.getFeatureValue( + linkFS.getType().getFeatureByBaseName(decl.getTargetFeature())); + var pos = DocumentPosition.builder() // + .forAnnotation(aFs) // + .withLinkFeature(decl.getName()) // + .withLinkFeatureMultiplicityMode(decl.getMultiplicityMode()) // + .withLinkRole(role) // + .withLinkTarget(target) // + .build(); + subPositions.add(pos); + } } - return new DocumentPosition(collectionId, documentId, getType(), aFeature, aRole, - aLinkTargetBegin, aLinkTargetEnd, linkTargetText, aLinkCompareBehavior); + return subPositions; } } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentPosition.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentPosition.java index fcbe921b23c..5f1834326fd 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentPosition.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/docmeta/DocumentPosition.java @@ -17,8 +17,13 @@ */ package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.docmeta; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.fit.util.FSUtil; +import org.apache.uima.jcas.cas.AnnotationBase; + import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position_ImplBase; import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; +import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; /** * Represents a document position. @@ -28,6 +33,13 @@ public class DocumentPosition { private static final long serialVersionUID = -1020728944030217843L; + private DocumentPosition(Builder builder) + { + super(builder.collectionId, builder.documentId, builder.type, builder.linkFeature, + builder.linkRole, builder.linkTargetBegin, builder.linkTargetEnd, + builder.linkTargetText, builder.linkFeatureMultiplicityMode); + } + public DocumentPosition(String aCollectionId, String aDocumentId, String aType, String aFeature, String aRole, int aLinkTargetBegin, int aLinkTargetEnd, String aLinkTargetText, LinkFeatureMultiplicityMode aLinkCompareBehavior) @@ -39,7 +51,7 @@ public DocumentPosition(String aCollectionId, String aDocumentId, String aType, @Override public String toString() { - StringBuilder builder = new StringBuilder(); + var builder = new StringBuilder(); builder.append("Document ["); toStringFragment(builder); builder.append(']'); @@ -51,4 +63,114 @@ public String toMinimalString() { return "Document"; } + + public static Builder builder() + { + return new Builder(); + } + + public static final class Builder + { + private String collectionId; + private String documentId; + private String type; + + private String linkFeature = null; + private String linkRole = null; + private int linkTargetBegin = -1; + private int linkTargetEnd = -1; + private String linkTargetText = null; + private LinkFeatureMultiplicityMode linkFeatureMultiplicityMode = null; + + private Builder() + { + } + + public Builder forAnnotation(AnnotationBase aAnnotation) + { + var cas = aAnnotation.getCAS(); + try { + var dmd = WebAnnoCasUtil.getDocumentMetadata(cas); + collectionId = FSUtil.getFeature(dmd, "collectionId", String.class); + documentId = FSUtil.getFeature(dmd, "documentId", String.class); + } + catch (IllegalArgumentException e) { + // We use this information only for debugging - so we can ignore if the information + // is missing. + collectionId = null; + documentId = null; + } + + type = aAnnotation.getType().getName(); + + return this; + } + + public Builder withCollectionId(String aCollectionId) + { + collectionId = aCollectionId; + return this; + } + + public Builder withDocumentId(String aDocumentId) + { + documentId = aDocumentId; + return this; + } + + public Builder withType(String aType) + { + type = aType; + return this; + } + + public Builder withLinkFeature(String aFeature) + { + linkFeature = aFeature; + return this; + } + + public Builder withLinkRole(String aRole) + { + linkRole = aRole; + return this; + } + + public Builder withLinkTarget(AnnotationFS aLinkTarget) + { + linkTargetBegin = aLinkTarget.getBegin(); + linkTargetEnd = aLinkTarget.getEnd(); + linkTargetText = aLinkTarget.getCoveredText(); + return this; + } + + public Builder withLinkTargetBegin(int aLinkTargetBegin) + { + linkTargetBegin = aLinkTargetBegin; + return this; + } + + public Builder withLinkTargetEnd(int aLinkTargetEnd) + { + linkTargetEnd = aLinkTargetEnd; + return this; + } + + public Builder withLinkTargetText(String aLinkTargetText) + { + linkTargetText = aLinkTargetText; + return this; + } + + public Builder withLinkFeatureMultiplicityMode(LinkFeatureMultiplicityMode aBehavior) + { + linkFeatureMultiplicityMode = aBehavior; + return this; + } + + public DocumentPosition build() + { + return new DocumentPosition(this); + } + } } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/internal/AID.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/internal/AID.java index 1cbe86aec76..0a4ad9afff7 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/internal/AID.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/internal/AID.java @@ -73,7 +73,7 @@ public boolean equals(Object obj) @Override public String toString() { - StringBuilder builder = new StringBuilder(); + var builder = new StringBuilder(); builder.append("AID [addr="); builder.append(addr); if (feature != null) { diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/relation/RelationDiffAdapter.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/relation/RelationDiffAdapter.java index 2fbcf8d03fb..ee8c2c160ff 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/relation/RelationDiffAdapter.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/relation/RelationDiffAdapter.java @@ -17,12 +17,16 @@ */ package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_SOURCE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_SOURCE; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_TARGET; +import static java.lang.Math.max; +import static java.lang.Math.min; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static org.apache.uima.cas.text.AnnotationPredicates.overlapping; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -31,12 +35,12 @@ import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.FSUtil; +import org.apache.uima.jcas.cas.AnnotationBase; import org.apache.uima.jcas.tcas.Annotation; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter_ImplBase; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; -import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationRenderer; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; @@ -51,15 +55,15 @@ public class RelationDiffAdapter private String targetFeature; public RelationDiffAdapter(String aType, String aSourceFeature, String aTargetFeature, - String... aLabelFeatures) + String... aFeatures) { - this(aType, aSourceFeature, aTargetFeature, new HashSet<>(asList(aLabelFeatures))); + this(aType, aSourceFeature, aTargetFeature, new HashSet<>(asList(aFeatures))); } public RelationDiffAdapter(String aType, String aSourceFeature, String aTargetFeature, - Set aLabelFeatures) + Set aFeatures) { - super(aType, aLabelFeatures); + super(aType, aFeatures); sourceFeature = aSourceFeature; targetFeature = aTargetFeature; } @@ -80,16 +84,14 @@ public String getTargetFeature() @Override public List selectAnnotationsInWindow(CAS aCas, int aWindowBegin, int aWindowEnd) { - // return selectCovered(aCas, CasUtil.getType(aCas, getType()), aWindowBegin, aWindowEnd); - var result = new ArrayList(); for (var rel : aCas. select(getType())) { var sourceFs = getSourceFs(rel); var targetFs = getTargetFs(rel); if (sourceFs instanceof Annotation source && targetFs instanceof Annotation target) { - var relBegin = Math.min(source.getBegin(), target.getBegin()); - var relEnd = Math.max(source.getEnd(), target.getEnd()); + var relBegin = min(source.getBegin(), target.getBegin()); + var relEnd = max(source.getEnd(), target.getEnd()); if (overlapping(relBegin, relEnd, aWindowBegin, aWindowEnd)) { result.add(rel); @@ -98,14 +100,13 @@ public List selectAnnotationsInWindow(CAS aCas, int aWindowBegin, in } return result; - } @Override - public Position getPosition(FeatureStructure aFS, String aFeature, String aRole, - int aLinkTargetBegin, int aLinkTargetEnd, - LinkFeatureMultiplicityMode aLinkCompareBehavior) + public Position getPosition(AnnotationBase aFS) { + int aLinkTargetBegin = -1; + int aLinkTargetEnd = -1; var type = aFS.getType(); var sourceFS = (AnnotationFS) aFS.getFeatureValue(type.getFeatureByBaseName(sourceFeature)); var targetFS = (AnnotationFS) aFS.getFeatureValue(type.getFeatureByBaseName(targetFeature)); @@ -134,8 +135,15 @@ public Position getPosition(FeatureStructure aFS, String aFeature, String aRole, sourceFS != null ? sourceFS.getCoveredText() : null, targetFS != null ? targetFS.getBegin() : -1, targetFS != null ? targetFS.getEnd() : -1, - targetFS != null ? targetFS.getCoveredText() : null, aFeature, aRole, - aLinkTargetBegin, aLinkTargetEnd, linkTargetText, aLinkCompareBehavior); + targetFS != null ? targetFS.getCoveredText() : null, null, null, aLinkTargetBegin, + aLinkTargetEnd, linkTargetText, null); + } + + @Override + public Collection generateSubPositions(AnnotationBase aFs) + { + // Relation layers do not support link features + return emptyList(); } private FeatureStructure getSourceFs(FeatureStructure fs) diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanDiffAdapter.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanDiffAdapter.java index 5276c0f0150..70cd8012b38 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanDiffAdapter.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanDiffAdapter.java @@ -20,15 +20,16 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.apache.uima.cas.ArrayFS; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.cas.text.AnnotationPredicates; import org.apache.uima.fit.util.FSUtil; +import org.apache.uima.jcas.cas.AnnotationBase; import org.apache.uima.jcas.tcas.Annotation; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter_ImplBase; @@ -37,9 +38,6 @@ import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; -import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanRenderer; -import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; public class SpanDiffAdapter extends DiffAdapter_ImplBase @@ -56,56 +54,64 @@ public class SpanDiffAdapter public static final SpanDiffAdapter NER_DIFF_ADAPTER = new SpanDiffAdapter( NamedEntity.class.getName(), "value", "identifier"); - public SpanDiffAdapter(String aType, String... aLabelFeatures) + public SpanDiffAdapter(String aType, String... aFeatures) { - this(aType, new HashSet<>(asList(aLabelFeatures))); + this(aType, new HashSet<>(asList(aFeatures))); } - public SpanDiffAdapter(String aType, Set aLabelFeatures) + public SpanDiffAdapter(String aType, Set aFeatures) { - super(aType, aLabelFeatures); + super(aType, aFeatures); } - /** - * @see SpanRenderer#selectAnnotationsInWindow - */ @Override public List selectAnnotationsInWindow(CAS aCas, int aWindowBegin, int aWindowEnd) { - return aCas.select(getType()).coveredBy(0, aWindowEnd) // + return aCas.select(getType()) // + .coveredBy(0, aWindowEnd) // .includeAnnotationsWithEndBeyondBounds() // .map(fs -> (Annotation) fs) // - .filter(ann -> AnnotationPredicates.overlapping(ann, aWindowBegin, aWindowEnd)) // + .filter(ann -> ann.overlapping(aWindowBegin, aWindowEnd)) // .collect(toList()); } @Override - public Position getPosition(FeatureStructure aFS, String aFeature, String aRole, - int aLinkTargetBegin, int aLinkTargetEnd, - LinkFeatureMultiplicityMode aLinkCompareBehavior) + public Position getPosition(AnnotationBase aFS) { - AnnotationFS annoFS = (AnnotationFS) aFS; - - String collectionId = null; - String documentId = null; - try { - var dmd = WebAnnoCasUtil.getDocumentMetadata(aFS.getCAS()); - collectionId = FSUtil.getFeature(dmd, "collectionId", String.class); - documentId = FSUtil.getFeature(dmd, "documentId", String.class); - } - catch (IllegalArgumentException e) { - // We use this information only for debugging - so we can ignore if the information - // is missing. - } + return SpanPosition.builder() // + .forAnnotation((Annotation) aFS) // + .build(); + } + + @Override + public List generateSubPositions(AnnotationBase aFs) + { + var subPositions = new ArrayList(); + + for (var decl : getLinkFeaturesDecls()) { + var linkFeature = aFs.getType().getFeatureByBaseName(decl.getName()); + var array = FSUtil.getFeature(aFs, linkFeature, ArrayFS.class); + + if (array == null) { + continue; + } - String linkTargetText = null; - if (aLinkTargetBegin != -1 && aFS.getCAS().getDocumentText() != null) { - linkTargetText = aFS.getCAS().getDocumentText().substring(aLinkTargetBegin, - aLinkTargetEnd); + for (var linkFS : array.toArray()) { + var role = linkFS.getStringValue( + linkFS.getType().getFeatureByBaseName(decl.getRoleFeature())); + var target = (AnnotationFS) linkFS.getFeatureValue( + linkFS.getType().getFeatureByBaseName(decl.getTargetFeature())); + var pos = SpanPosition.builder() // + .forAnnotation((Annotation) aFs) // + .withLinkFeature(decl.getName()) // + .withLinkFeatureMultiplicityMode(decl.getMultiplicityMode()) // + .withLinkRole(role) // + .withLinkTarget(target) // + .build(); + subPositions.add(pos); + } } - return new SpanPosition(collectionId, documentId, getType(), annoFS.getBegin(), - annoFS.getEnd(), annoFS.getCoveredText(), aFeature, aRole, aLinkTargetBegin, - aLinkTargetEnd, linkTargetText, aLinkCompareBehavior); + return subPositions; } } diff --git a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanPosition.java b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanPosition.java index d5ec2d75402..8af398fd6f3 100644 --- a/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanPosition.java +++ b/inception/inception-curation-legacy/src/main/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/span/SpanPosition.java @@ -19,9 +19,14 @@ import java.util.Objects; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.fit.util.FSUtil; +import org.apache.uima.jcas.tcas.Annotation; + import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position_ImplBase; import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode; +import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; /** * Represents a span position in the text. @@ -35,22 +40,20 @@ public class SpanPosition private final int end; private final String text; - public SpanPosition(String aCollectionId, String aDocumentId, String aType, int aBegin, - int aEnd, String aText) + private SpanPosition(Builder builder) { - super(aCollectionId, aDocumentId, aType, null, null, 0, 0, null, null); - begin = aBegin; - end = aEnd; - text = aText; + super(builder.collectionId, builder.documentId, builder.type, builder.linkFeature, + builder.linkRole, builder.linkTargetBegin, builder.linkTargetEnd, + builder.linkTargetText, builder.linkFeatureMultiplicityMode); + begin = builder.begin; + end = builder.end; + text = builder.text; } public SpanPosition(String aCollectionId, String aDocumentId, String aType, int aBegin, - int aEnd, String aText, String aFeature, String aRole, int aLinkTargetBegin, - int aLinkTargetEnd, String aLinkTargetText, - LinkFeatureMultiplicityMode aLinkCompareBehavior) + int aEnd, String aText) { - super(aCollectionId, aDocumentId, aType, aFeature, aRole, aLinkTargetBegin, aLinkTargetEnd, - aLinkTargetText, aLinkCompareBehavior); + super(aCollectionId, aDocumentId, aType); begin = aBegin; end = aEnd; text = aText; @@ -138,11 +141,11 @@ public String toMinimalString() var builder = new StringBuilder(); builder.append(begin).append('-').append(end).append(" [").append(text).append(']'); - var linkCompareBehavior = getLinkCompareBehavior(); + var linkCompareBehavior = getLinkFeatureMultiplicityMode(); if (linkCompareBehavior != null) { switch (linkCompareBehavior) { case ONE_TARGET_MULTIPLE_ROLES: - builder.append(" role: [").append(getRole()).append(']'); + builder.append(" role: [").append(getLinkRole()).append(']'); break; case MULTIPLE_TARGETS_ONE_ROLE: builder.append(" -> [").append(getLinkTargetBegin()).append('-') @@ -150,9 +153,9 @@ public String toMinimalString() .append(']'); break; case MULTIPLE_TARGETS_MULTIPLE_ROLES: - builder.append(" -> ").append(getRole()).append("@[").append(getLinkTargetBegin()) - .append('-').append(getLinkTargetEnd()).append(" [") - .append(getLinkTargetText()).append(']'); + builder.append(" -> ").append(getLinkRole()).append("@[") + .append(getLinkTargetBegin()).append('-').append(getLinkTargetEnd()) + .append(" [").append(getLinkTargetText()).append(']'); break; default: throw new IllegalStateException( @@ -162,4 +165,139 @@ public String toMinimalString() return builder.toString(); } + + public static Builder builder() + { + return new Builder(); + } + + public static final class Builder + { + private String collectionId; + private String documentId; + private String type; + + private int begin; + private int end; + private String text; + + private String linkFeature = null; + private String linkRole = null; + private int linkTargetBegin = -1; + private int linkTargetEnd = -1; + private String linkTargetText = null; + private LinkFeatureMultiplicityMode linkFeatureMultiplicityMode = null; + + private Builder() + { + } + + public Builder forAnnotation(Annotation aAnnotation) + { + var cas = aAnnotation.getCAS(); + try { + var dmd = WebAnnoCasUtil.getDocumentMetadata(cas); + collectionId = FSUtil.getFeature(dmd, "collectionId", String.class); + documentId = FSUtil.getFeature(dmd, "documentId", String.class); + } + catch (IllegalArgumentException e) { + // We use this information only for debugging - so we can ignore if the information + // is missing. + collectionId = null; + documentId = null; + } + + type = aAnnotation.getType().getName(); + begin = aAnnotation.getBegin(); + end = aAnnotation.getEnd(); + text = aAnnotation.getCoveredText(); + + return this; + } + + public Builder withCollectionId(String aCollectionId) + { + collectionId = aCollectionId; + return this; + } + + public Builder withDocumentId(String aDocumentId) + { + documentId = aDocumentId; + return this; + } + + public Builder withType(String aType) + { + type = aType; + return this; + } + + public Builder withLinkFeature(String aFeature) + { + linkFeature = aFeature; + return this; + } + + public Builder withLinkRole(String aRole) + { + linkRole = aRole; + return this; + } + + public Builder withLinkTarget(AnnotationFS aLinkTarget) + { + linkTargetBegin = aLinkTarget.getBegin(); + linkTargetEnd = aLinkTarget.getEnd(); + linkTargetText = aLinkTarget.getCoveredText(); + return this; + } + + public Builder withLinkTargetBegin(int aLinkTargetBegin) + { + linkTargetBegin = aLinkTargetBegin; + return this; + } + + public Builder withLinkTargetEnd(int aLinkTargetEnd) + { + linkTargetEnd = aLinkTargetEnd; + return this; + } + + public Builder withLinkTargetText(String aLinkTargetText) + { + linkTargetText = aLinkTargetText; + return this; + } + + public Builder withLinkFeatureMultiplicityMode(LinkFeatureMultiplicityMode aBehavior) + { + linkFeatureMultiplicityMode = aBehavior; + return this; + } + + public Builder withBegin(int aBegin) + { + begin = aBegin; + return this; + } + + public Builder withEnd(int aEnd) + { + end = aEnd; + return this; + } + + public Builder withText(String aText) + { + text = aText; + return this; + } + + public SpanPosition build() + { + return new SpanPosition(this); + } + } } diff --git a/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiffTest.java b/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiffTest.java index 53d4e50cb7d..21630fcac5b 100644 --- a/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiffTest.java +++ b/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CasDiffTest.java @@ -32,6 +32,8 @@ import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CurationTestUtils.makeLinkHostFS; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationDiffAdapter.DEPENDENCY_DIFF_ADAPTER; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter.POS_DIFF_ADAPTER; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.EXCLUDE; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.INCLUDE; import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.MULTIPLE_TARGETS_MULTIPLE_ROLES; import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.MULTIPLE_TARGETS_ONE_ROLE; import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; @@ -488,7 +490,7 @@ public void two_annotators__a_b__incomplete() throws Exception } @Nested - class MultipleTargetsMultipleRolesTests + class MultipleTargetsMultipleRolesIncludeLinksTests { private static final SpanDiffAdapter HOST_TYPE_ADAPTER; private static final SpanDiffAdapter FILLER_ADAPTER; @@ -500,7 +502,64 @@ class MultipleTargetsMultipleRolesTests static { HOST_TYPE_ADAPTER = new SpanDiffAdapter(HOST_TYPE); HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", - MULTIPLE_TARGETS_MULTIPLE_ROLES); + MULTIPLE_TARGETS_MULTIPLE_ROLES, INCLUDE); + + FILLER_ADAPTER = new SpanDiffAdapter(SLOT_FILLER_TYPE, "value"); + } + + @BeforeEach + void setup() throws Exception + { + jcasA = createJCas(createMultiLinkWithRoleTestTypeSystem()); + jcasB = createJCas(createMultiLinkWithRoleTestTypeSystem()); + + casByUser = new LinkedHashMap(); + casByUser.put("user1", jcasA.getCas()); + casByUser.put("user2", jcasB.getCas()); + } + + @Test + public void one_annotator__stacked_no_label_hosts__different_labels__stacked() + throws Exception + { + makeLinkHostFS(jcasA, 0, 0, // + makeLinkFS(jcasA, "slot1", 0, 0)); + + makeLinkHostFS(jcasA, 0, 0, // + makeLinkFS(jcasA, "slot2", 0, 0)); + + casByUser.remove("user2"); + + var result = doDiff(asList(HOST_TYPE_ADAPTER, FILLER_ADAPTER), casByUser).toResult(); + + assertSpanPositionConfigurations(result.getConfigurationSets()) // + .containsExactlyInAnyOrder( // + tuple("LinkHost", 0, 0, -1, -1, null, Set.of("user1")), // + tuple("SlotFiller", 0, 0, -1, -1, null, Set.of("user1")), // + tuple("LinkHost", 0, 0, 0, 0, "slot1", Set.of("user1")), // + tuple("LinkHost", 0, 0, 0, 0, "slot2", Set.of("user1"))); + assertSpanPositionConfigurations(result.getDifferingConfigurationSets().values()) // + .containsExactlyInAnyOrder( // + tuple("LinkHost", 0, 0, -1, -1, null, Set.of("user1"))); + assertThat(result.getIncompleteConfigurationSets()).isEmpty(); + assertThat(calculateState(result)).isEqualTo(STACKED); + } + } + + @Nested + class MultipleTargetsMultipleRolesExcludeLinksTests + { + private static final SpanDiffAdapter HOST_TYPE_ADAPTER; + private static final SpanDiffAdapter FILLER_ADAPTER; + + private JCas jcasA; + private JCas jcasB; + private Map casByUser; + + static { + HOST_TYPE_ADAPTER = new SpanDiffAdapter(HOST_TYPE); + HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", + MULTIPLE_TARGETS_MULTIPLE_ROLES, EXCLUDE); FILLER_ADAPTER = new SpanDiffAdapter(SLOT_FILLER_TYPE, "value"); } @@ -559,6 +618,31 @@ public void one_annotator__different_labels__agreement() throws Exception assertThat(calculateState(result)).isEqualTo(AGREE); } + @Test + public void one_annotator__stacked_no_label_hosts__different_labels__agreement() + throws Exception + { + makeLinkHostFS(jcasA, 0, 0, // + makeLinkFS(jcasA, "slot1", 0, 0)); + + makeLinkHostFS(jcasA, 0, 0, // + makeLinkFS(jcasA, "slot2", 0, 0)); + + casByUser.remove("user2"); + + var result = doDiff(asList(HOST_TYPE_ADAPTER, FILLER_ADAPTER), casByUser).toResult(); + + assertSpanPositionConfigurations(result.getConfigurationSets()) // + .containsExactlyInAnyOrder( // + tuple("LinkHost", 0, 0, -1, -1, null, Set.of("user1")), // + tuple("SlotFiller", 0, 0, -1, -1, null, Set.of("user1")), // + tuple("LinkHost", 0, 0, 0, 0, "slot1", Set.of("user1")), // + tuple("LinkHost", 0, 0, 0, 0, "slot2", Set.of("user1"))); + assertThat(result.getDifferingConfigurationSets().values()).isEmpty(); + assertThat(result.getIncompleteConfigurationSets()).isEmpty(); + assertThat(calculateState(result)).isEqualTo(AGREE); + } + @Test public void one_annotator__different_positions__agreement() throws Exception { @@ -747,7 +831,8 @@ class OneTargetMultipleRolesTests static { HOST_TYPE_ADAPTER = new SpanDiffAdapter(HOST_TYPE); - HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES); + HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES, + EXCLUDE); FILLER_ADAPTER = new SpanDiffAdapter(SLOT_FILLER_TYPE, "value"); } @@ -872,10 +957,12 @@ public void two_annotators__disagreement() throws Exception var result = doDiff(asList(HOST_TYPE_ADAPTER), casByUser).toResult(); assertSpanPositionConfigurations(result.getConfigurationSets()) // + .as("ConfigurationSets") // .containsExactlyInAnyOrder( // tuple("LinkHost", 0, 0, -1, -1, null, Set.of("user1", "user2")), // tuple("LinkHost", 0, 0, 0, 0, "slot1", Set.of("user1", "user2"))); assertSpanPositionConfigurations(result.getDifferingConfigurationSets().values()) // + .as("DifferingConfigurationSets") // .containsExactlyInAnyOrder( // tuple("LinkHost", 0, 0, 0, 0, "slot1", Set.of("user1", "user2"))); assertThat(result.getIncompleteConfigurationSets()).isEmpty(); @@ -969,7 +1056,8 @@ class MultipleTargetsOneRoleTests static { HOST_TYPE_ADAPTER = new SpanDiffAdapter(HOST_TYPE); - HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", MULTIPLE_TARGETS_ONE_ROLE); + HOST_TYPE_ADAPTER.addLinkFeature("links", "role", "target", MULTIPLE_TARGETS_ONE_ROLE, + EXCLUDE); FILLER_ADAPTER = new SpanDiffAdapter(SLOT_FILLER_TYPE, "value"); } @@ -1127,7 +1215,7 @@ public void two_annotators__stacked() throws Exception cfg -> ((SpanPosition) cfg.getPosition()).getEnd(), // cfg -> cfg.getPosition().getLinkTargetBegin(), // cfg -> cfg.getPosition().getLinkTargetEnd(), // - cfg -> cfg.getPosition().getRole(), // + cfg -> cfg.getPosition().getLinkRole(), // cfg -> cfg.getCasGroupIds()); } } diff --git a/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CurationTestUtils.java b/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CurationTestUtils.java index 3842a7dc014..fefee225fe0 100644 --- a/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CurationTestUtils.java +++ b/inception/inception-curation-legacy/src/test/java/de/tudarmstadt/ukp/clarin/webanno/curation/casdiff/CurationTestUtils.java @@ -18,11 +18,16 @@ package de.tudarmstadt.ukp.clarin.webanno.curation.casdiff; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.RELATION_TYPE; +import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static de.tudarmstadt.ukp.inception.support.uima.FeatureStructureBuilder.buildFS; import static java.util.Arrays.asList; +import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION; +import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; +import static org.apache.uima.fit.factory.CasFactory.createCas; import static org.apache.uima.fit.factory.CollectionReaderFactory.createReader; +import static org.apache.uima.fit.factory.TypeSystemDescriptionFactory.createTypeSystemDescription; +import static org.apache.uima.util.CasCreationUtils.mergeTypeSystems; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -31,23 +36,18 @@ import org.apache.uima.UIMAException; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.collection.CollectionReader; import org.apache.uima.fit.factory.JCasFactory; -import org.apache.uima.fit.factory.TypeSystemDescriptionFactory; -import org.apache.uima.fit.util.FSCollectionFactory; import org.apache.uima.jcas.JCas; -import org.apache.uima.resource.metadata.TypeDescription; import org.apache.uima.resource.metadata.TypeSystemDescription; import org.apache.uima.resource.metadata.impl.TypeSystemDescription_impl; -import org.apache.uima.util.CasCreationUtils; import org.dkpro.core.io.conll.Conll2006Reader; import org.dkpro.core.io.xmi.XmiReader; import de.tudarmstadt.ukp.clarin.webanno.tsv.WebannoTsv2Reader; import de.tudarmstadt.ukp.clarin.webanno.tsv.WebannoTsv3XReader; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; @@ -68,21 +68,10 @@ public static JCas loadWebAnnoTsv3(String aPath) throws UIMAException, IOExcepti return jcas; } - public static JCas loadWebAnnoTsv3(File aPath) throws UIMAException, IOException - { - var reader = createReader( // - WebannoTsv3XReader.class, // - WebannoTsv3XReader.PARAM_SOURCE_LOCATION, aPath); - - var jcas = JCasFactory.createJCas(); - reader.getNext(jcas.getCas()); - return jcas; - } - public static Map load(String... aPaths) throws UIMAException, IOException { var casByUser = new LinkedHashMap(); - int n = 1; + var n = 1; for (var path : aPaths) { var cas = readConll2006(path); casByUser.put("user" + n, cas); @@ -91,61 +80,36 @@ public static Map load(String... aPaths) throws UIMAException, IOEx return casByUser; } - public static Map> loadWebAnnoTSV(TypeSystemDescription aTypes, - String... aPaths) - throws UIMAException, IOException - { - Map> casByUser = new LinkedHashMap<>(); - int n = 1; - for (String path : aPaths) { - CAS cas = readWebAnnoTSV(path, aTypes); - casByUser.put("user" + n, asList(cas)); - n++; - } - return casByUser; - } - - public static Map loadXMI(TypeSystemDescription aTypes, String... aPaths) - throws UIMAException, IOException - { - Map casByUser = new LinkedHashMap<>(); - int n = 1; - for (String path : aPaths) { - CAS cas = readXMI(path, aTypes); - casByUser.put("user" + n, cas); - n++; - } - return casByUser; - } - public static CAS readConll2006(String aPath) throws UIMAException, IOException { - CollectionReader reader = createReader(Conll2006Reader.class, + var reader = createReader( // + Conll2006Reader.class, // Conll2006Reader.PARAM_SOURCE_LOCATION, "src/test/resources/" + aPath); - CAS jcas = JCasFactory.createJCas().getCas(); + var cas = createCas(); - reader.getNext(jcas); + reader.getNext(cas); - return jcas; + return cas; } public static CAS readWebAnnoTSV(String aPath, TypeSystemDescription aType) throws UIMAException, IOException { - CollectionReader reader = createReader(WebannoTsv2Reader.class, + var reader = createReader( // + WebannoTsv2Reader.class, // WebannoTsv2Reader.PARAM_SOURCE_LOCATION, "src/test/resources/" + aPath); + CAS cas; if (aType != null) { - TypeSystemDescription builtInTypes = TypeSystemDescriptionFactory - .createTypeSystemDescription(); - List allTypes = new ArrayList<>(); + var builtInTypes = createTypeSystemDescription(); + var allTypes = new ArrayList(); allTypes.add(builtInTypes); allTypes.add(aType); - cas = JCasFactory.createJCas(CasCreationUtils.mergeTypeSystems(allTypes)).getCas(); + cas = createCas(mergeTypeSystems(allTypes)); } else { - cas = JCasFactory.createJCas().getCas(); + cas = createCas(); } reader.getNext(cas); @@ -156,70 +120,71 @@ public static CAS readWebAnnoTSV(String aPath, TypeSystemDescription aType) public static CAS readXMI(String aPath, TypeSystemDescription aType) throws UIMAException, IOException { - CollectionReader reader = createReader(XmiReader.class, XmiReader.PARAM_SOURCE_LOCATION, - "src/test/resources/" + aPath); - CAS jcas; + var reader = createReader( // + XmiReader.class, // + XmiReader.PARAM_SOURCE_LOCATION, "src/test/resources/" + aPath); + + CAS cas; if (aType != null) { - TypeSystemDescription builtInTypes = TypeSystemDescriptionFactory - .createTypeSystemDescription(); + TypeSystemDescription builtInTypes = createTypeSystemDescription(); List allTypes = new ArrayList<>(); allTypes.add(builtInTypes); allTypes.add(aType); - jcas = JCasFactory.createJCas(CasCreationUtils.mergeTypeSystems(allTypes)).getCas(); + cas = createCas(mergeTypeSystems(allTypes)); } else { - jcas = JCasFactory.createJCas().getCas(); + cas = createCas(); } - reader.getNext(jcas); + reader.getNext(cas); - return jcas; + return cas; } public static TypeSystemDescription createMultiLinkWithRoleTestTypeSytem() throws Exception { - List typeSystems = new ArrayList<>(); + var typeSystems = new ArrayList(); - TypeSystemDescription tsd = new TypeSystemDescription_impl(); + var tsd = new TypeSystemDescription_impl(); // Link type - TypeDescription linkTD = tsd.addType(LINK_TYPE, "", CAS.TYPE_NAME_TOP); - linkTD.addFeature("role", "", CAS.TYPE_NAME_STRING); - linkTD.addFeature("target", "", CAS.TYPE_NAME_ANNOTATION); + var linkTD = tsd.addType(LINK_TYPE, "", CAS.TYPE_NAME_TOP); + linkTD.addFeature("role", "", TYPE_NAME_STRING); + linkTD.addFeature("target", "", TYPE_NAME_ANNOTATION); // Link host - TypeDescription hostTD = tsd.addType(HOST_TYPE, "", CAS.TYPE_NAME_ANNOTATION); + var hostTD = tsd.addType(HOST_TYPE, "", TYPE_NAME_ANNOTATION); hostTD.addFeature("links", "", CAS.TYPE_NAME_FS_ARRAY, linkTD.getName(), false); typeSystems.add(tsd); - typeSystems.add(TypeSystemDescriptionFactory.createTypeSystemDescription()); + typeSystems.add(createTypeSystemDescription()); - return CasCreationUtils.mergeTypeSystems(typeSystems); + return mergeTypeSystems(typeSystems); } public static TypeSystemDescription createMultiLinkWithRoleTestTypeSystem(String... aFeatures) throws Exception { - List typeSystems = new ArrayList<>(); + var typeSystems = new ArrayList(); - TypeSystemDescription tsd = new TypeSystemDescription_impl(); + var tsd = new TypeSystemDescription_impl(); // Link type - TypeDescription linkTD = tsd.addType(LINK_TYPE, "", CAS.TYPE_NAME_TOP); - linkTD.addFeature("role", "", CAS.TYPE_NAME_STRING); - linkTD.addFeature("target", "", CAS.TYPE_NAME_ANNOTATION); + var linkTD = tsd.addType(LINK_TYPE, "", CAS.TYPE_NAME_TOP); + linkTD.addFeature("role", "", TYPE_NAME_STRING); + linkTD.addFeature("target", "", TYPE_NAME_ANNOTATION); // Link host - TypeDescription hostTD = tsd.addType(HOST_TYPE, "", CAS.TYPE_NAME_ANNOTATION); + var hostTD = tsd.addType(HOST_TYPE, "", TYPE_NAME_ANNOTATION); hostTD.addFeature("links", "", CAS.TYPE_NAME_FS_ARRAY, linkTD.getName(), false); - for (String feature : aFeatures) { - hostTD.addFeature(feature, "", CAS.TYPE_NAME_STRING); + for (var feature : aFeatures) { + hostTD.addFeature(feature, "", TYPE_NAME_STRING); } typeSystems.add(tsd); - typeSystems.add(TypeSystemDescriptionFactory.createTypeSystemDescription()); + typeSystems.add(createTypeSystemDescription()); - return CasCreationUtils.mergeTypeSystems(typeSystems); + return mergeTypeSystems(typeSystems); } public static TypeSystemDescription createCustomTypeSystem(String aType, String aTypeName, @@ -228,64 +193,45 @@ public static TypeSystemDescription createCustomTypeSystem(String aType, String { var type = new TypeSystemDescription_impl(); if (SpanLayerSupport.TYPE.equals(aType)) { - var td = type.addType(aTypeName, "", CAS.TYPE_NAME_ANNOTATION); + var td = type.addType(aTypeName, "", TYPE_NAME_ANNOTATION); for (var feature : aFeatures) { - td.addFeature(feature, "", CAS.TYPE_NAME_STRING); + td.addFeature(feature, "", TYPE_NAME_STRING); } } - else if (aType.equals(RELATION_TYPE)) { - var td = type.addType(aTypeName, "", CAS.TYPE_NAME_ANNOTATION); + else if (RelationLayerSupport.TYPE.equals(aType)) { + var td = type.addType(aTypeName, "", TYPE_NAME_ANNOTATION); td.addFeature(WebAnnoConst.FEAT_REL_TARGET, "", aAttacheType); td.addFeature(WebAnnoConst.FEAT_REL_SOURCE, "", aAttacheType); for (var feature : aFeatures) { - td.addFeature(feature, "", CAS.TYPE_NAME_STRING); + td.addFeature(feature, "", TYPE_NAME_STRING); } } return type; } - public static void makeLinkHostFS(JCas aCas, int aBegin, int aEnd, FeatureStructure... aLinks) + public static AnnotationFS makeLinkHostFS(JCas aCas, int aBegin, int aEnd, + FeatureStructure... aLinks) { - var hostType = aCas.getTypeSystem().getType(HOST_TYPE); - var hostA1 = aCas.getCas().createAnnotation(hostType, aBegin, aEnd); - hostA1.setFeatureValue(hostType.getFeatureByBaseName("links"), - FSCollectionFactory.createFSArray(aCas, asList(aLinks))); - aCas.getCas().addFsToIndexes(hostA1); - } - - public static AnnotationFS makeLinkHostMultiSPanFeatureFS(JCas aCas, int aBegin, int aEnd, - Feature aSpanFeature, String aValue, FeatureStructure... aLinks) - { - var hostType = aCas.getTypeSystem().getType(HOST_TYPE); - - var hostA1 = aCas.getCas().createAnnotation(hostType, aBegin, aEnd); - hostA1.setFeatureValue(hostType.getFeatureByBaseName("links"), - FSCollectionFactory.createFSArray(aCas, asList(aLinks))); - hostA1.setStringValue(aSpanFeature, aValue); - aCas.getCas().addFsToIndexes(hostA1); - - return hostA1; + return buildAnnotation(aCas.getCas(), HOST_TYPE) // + .at(aBegin, aEnd) // + .withFeature("links", asList(aLinks)) // + .buildAndAddToIndexes(); } public static FeatureStructure makeLinkFS(JCas aCas, String aSlotLabel, int aTargetBegin, int aTargetEnd) { - var slotFillerType = aCas.getTypeSystem().getType(SLOT_FILLER_TYPE); - - var filler1 = aCas.getCas().createAnnotation(slotFillerType, aTargetBegin, aTargetEnd); - aCas.getCas().addFsToIndexes(filler1); - - var linkType = aCas.getTypeSystem().getType(LINK_TYPE); - - var linkA1 = aCas.getCas().createFS(linkType); - linkA1.setStringValue(linkType.getFeatureByBaseName("role"), aSlotLabel); - linkA1.setFeatureValue(linkType.getFeatureByBaseName("target"), filler1); - aCas.getCas().addFsToIndexes(linkA1); - - return linkA1; + var filler = buildAnnotation(aCas.getCas(), SLOT_FILLER_TYPE) // + .at(aTargetBegin, aTargetEnd) // + .buildAndAddToIndexes(); + + return buildFS(aCas.getCas(), LINK_TYPE) // + .withFeature("role", aSlotLabel) // + .withFeature("target", filler) // + .buildAndAddToIndexes(); } } diff --git a/inception/inception-curation/pom.xml b/inception/inception-curation/pom.xml index 110c909810c..a0d900d08a2 100644 --- a/inception/inception-curation/pom.xml +++ b/inception/inception-curation/pom.xml @@ -53,6 +53,10 @@ de.tudarmstadt.ukp.inception.app inception-schema-api + + de.tudarmstadt.ukp.inception.app + inception-io-xml + de.tudarmstadt.ukp.inception.app inception-model diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/config/CurationServiceAutoConfiguration.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/config/CurationServiceAutoConfiguration.java index ccc256ecfee..f07cfd0fb06 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/config/CurationServiceAutoConfiguration.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/config/CurationServiceAutoConfiguration.java @@ -29,13 +29,13 @@ import de.tudarmstadt.ukp.clarin.webanno.api.export.DocumentImportExportService; import de.tudarmstadt.ukp.inception.curation.export.CuratedDocumentsExporter; import de.tudarmstadt.ukp.inception.curation.export.CurationWorkflowExporter; -import de.tudarmstadt.ukp.inception.curation.merge.DefaultMergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.MergeIncompleteStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactoryExtensionPoint; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactoryExtensionPointImpl; -import de.tudarmstadt.ukp.inception.curation.merge.ThresholdBasedMergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.ThresholdBasedMergeStrategyFactoryImpl; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.DefaultMergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeIncompleteStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactoryExtensionPoint; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactoryExtensionPointImpl; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategyFactoryImpl; import de.tudarmstadt.ukp.inception.curation.service.CurationMergeService; import de.tudarmstadt.ukp.inception.curation.service.CurationMergeServiceImpl; import de.tudarmstadt.ukp.inception.curation.service.CurationService; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/export/CuratedDocumentsExporter.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/export/CuratedDocumentsExporter.java index fdd3b94c56b..253cc12d69c 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/export/CuratedDocumentsExporter.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/export/CuratedDocumentsExporter.java @@ -95,12 +95,14 @@ public List> getImportDependencies() } /** - * Copy, if exists, curation documents to a folder that will be exported as Zip file + * Copy, if exists, curation documents to a folder that will be exported as ZIP file * * @param aStage - * The folder where curated documents are copied to be exported as Zip File + * The folder where curated documents are copied to be exported as ZIP File * @throws IOException + * if there was a problem writing the data * @throws ProjectExportException + * if there was a problem preparing the data */ @Override public void exportData(FullProjectExportRequest aRequest, ProjectExportTaskMonitor aMonitor, diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMerge.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMerge.java index 63fb7e10a4b..d23f6172526 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMerge.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMerge.java @@ -33,7 +33,6 @@ import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -46,8 +45,6 @@ import org.springframework.context.ApplicationEventPublisher; import de.tudarmstadt.ukp.clarin.webanno.api.type.CASMetadata; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.Configuration; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.ConfigurationSet; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.DiffResult; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationPosition; @@ -62,6 +59,7 @@ import de.tudarmstadt.ukp.inception.annotation.storage.CasMetadataUtils; import de.tudarmstadt.ukp.inception.curation.merge.strategy.DefaultMergeStrategy; import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategy; +import de.tudarmstadt.ukp.inception.io.xml.dkprocore.XmlNodeUtils; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.IllegalFeatureValueException; @@ -69,9 +67,6 @@ import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; -/** - * Do a merge CAS out of multiple user annotations - */ public class CasMerge { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -111,15 +106,9 @@ public MergeStrategy getMergeStrategy() return mergeStrategy; } - private List chooseConfigurationsToMerge(AnnotationLayer aLayer, - DiffResult aDiff, ConfigurationSet cfgs) - { - return mergeStrategy.chooseConfigurationsToMerge(aDiff, cfgs, aLayer); - } - /** * Using {@code DiffResult}, determine the annotations to be deleted from the randomly generated - * MergeCase. The initial Merge CAs is stored under a name {@code CurationPanel#CURATION_USER}. + * MergeCase. The initial Merge CAS is stored under a name {@code CurationPanel#CURATION_USER}. *

* Any similar annotations stacked in a {@code CasDiff2.Position} will be assumed a difference *

@@ -193,9 +182,7 @@ public Set mergeCas(DiffResult aDiff, SourceDocument aTargetDocument { context.setSilenceEvents(true); - var updated = 0; - var created = 0; - var messages = new LinkedHashSet(); + var localContext = new PerCasMergeContext(); // If there is nothing to merge, bail out if (aCasMap.isEmpty()) { @@ -227,150 +214,211 @@ public Set mergeCas(DiffResult aDiff, SourceDocument aTargetDocument // or as relation layers). // We process layer by layer so that we can order the layers (important to process tokens // and sentences before the others) + mergeSpanLayers(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, localContext, + type2layer, layerNames); + + // After the spans are in place, we can merge the link features + mergeLinkFeatures(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, + localContext, type2layer, layerNames); + + // Finally, we merge the relations + mergeRelationLayers(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, + localContext, type2layer, layerNames); + + LOG.trace("Merge complete. Created: {} Updated: {}", localContext.created, + localContext.updated); + + if (eventPublisher != null) { + eventPublisher + .publishEvent(new BulkAnnotationEvent(this, aTargetDocument, aTargetUsername)); + } + + return localContext.messages; + } + + private void mergeRelationLayers(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + ArrayList layerNames) + { for (var layerName : layerNames) { - var spanPositions = aDiff.getPositions().stream() + var relationPositions = aDiff.getPositions().stream() .filter(pos -> layerName.equals(pos.getType())) - .filter(pos -> pos instanceof SpanPosition) // - .map(pos -> (SpanPosition) pos) - // We don't process slot features here (they are span sub-positions) - .filter(pos -> pos.getFeature() == null) // + .filter(pos -> pos instanceof RelationPosition) + .map(pos -> (RelationPosition) pos) // .toList(); - if (spanPositions.isEmpty()) { + if (relationPositions.isEmpty()) { continue; } - LOG.debug("Processing [{}] span positions on layer [{}]", spanPositions.size(), + LOG.debug("Processing {} relation positions on layer [{}]", relationPositions.size(), layerName); - // First we merge the spans so that we can attach the relations to something later. - // Slots are also excluded for the moment - for (var spanPosition : spanPositions) { - LOG.trace(" | processing {}", spanPosition); - var layer = type2layer.get(spanPosition.getType()); - var cfgs = aDiff.getConfigurationSet(spanPosition); - - for (var cfgToMerge : chooseConfigurationsToMerge(layer, aDiff, cfgs)) { - try { - var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); - var result = mergeSpanAnnotation(aTargetDocument, aTargetUsername, - type2layer.get(spanPosition.getType()), aTargetCas, sourceFS); - LOG.trace(" `-> merged annotation with agreement"); - - switch (result.getState()) { - case CREATED: - created++; - break; - case UPDATED: - updated++; - break; - } - } - catch (AnnotationException e) { - LOG.trace(" `-> not merged annotation: {}", e.getMessage()); - messages.add(LogMessage.error(this, "%s", e.getMessage())); - } + for (var relationPosition : relationPositions) { + mergeRelationPosition(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, + localContext, type2layer, relationPosition); + } + } + } + + private void mergeRelationPosition(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + RelationPosition relationPosition) + { + LOG.trace(" | processing {}", relationPosition); + var layer = type2layer.get(relationPosition.getType()); + var cfgs = aDiff.getConfigurationSet(relationPosition); + + var cfgsToMerge = mergeStrategy.chooseConfigurationsToMerge(aDiff, cfgs, layer); + + for (var cfgToMerge : cfgsToMerge) { + var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); + try { + var result = mergeRelationAnnotation(aTargetDocument, aTargetUsername, + type2layer.get(relationPosition.getType()), aTargetCas, sourceFS); + + switch (result.getState()) { + case CREATED: + LOG.trace(" `-> merged relation annotation [{}] (created) -> [{}]", + sourceFS.getAddress(), result.getTargetFSAddress()); + localContext.created++; + break; + case UPDATED: + LOG.trace(" `-> merged relation annotation [{}] (updated) -> [{}]", + sourceFS.getAddress(), result.getTargetFSAddress()); + localContext.updated++; + break; } } + catch (AnnotationException e) { + LOG.trace(" `-> not merged relation annotation [{}]: {}", sourceFS.getAddress(), + e.getMessage()); + localContext.messages.add(LogMessage.error(this, "%s", e.getMessage())); + } } + } - // After the spans are in place, we can merge the slot features + private void mergeLinkFeatures(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + ArrayList layerNames) + { for (var layerName : layerNames) { - var slotPositions = aDiff.getPositions().stream() + var linkPositions = aDiff.getPositions().stream() .filter(pos -> layerName.equals(pos.getType())) .filter(pos -> pos instanceof SpanPosition) // .map(pos -> (SpanPosition) pos) // We only process slot features here - .filter(pos -> pos.getFeature() != null) // + .filter(Position::isLinkFeaturePosition) // .toList(); - if (slotPositions.isEmpty()) { + if (linkPositions.isEmpty()) { continue; } - LOG.debug("Processing {} slot positions on layer [{}]", slotPositions.size(), + LOG.debug("Processing {} link positions on layer [{}]", linkPositions.size(), layerName); - for (var slotPosition : slotPositions) { - LOG.trace(" | processing {}", slotPosition); - var layer = type2layer.get(slotPosition.getType()); - var cfgs = aDiff.getConfigurationSet(slotPosition); - - for (var cfgToMerge : chooseConfigurationsToMerge(layer, aDiff, cfgs)) { - try { - var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); - var sourceFsAid = cfgs.getConfigurations().get(0).getRepresentativeAID(); - mergeSlotFeature(aTargetDocument, aTargetUsername, - type2layer.get(slotPosition.getType()), aTargetCas, sourceFS, - sourceFsAid.feature, sourceFsAid.index); - LOG.trace(" `-> merged annotation with agreement"); - } - catch (AnnotationException e) { - LOG.trace(" `-> not merged annotation: {}", e.getMessage()); - messages.add(LogMessage.error(this, "%s", e.getMessage())); - } - } + for (var slotPosition : linkPositions) { + mergeLinkPosition(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, + localContext, type2layer, slotPosition); } } + } - // Finally, we merge the relations + private void mergeLinkPosition(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + SpanPosition slotPosition) + { + LOG.trace(" | processing {}", slotPosition); + var layer = type2layer.get(slotPosition.getType()); + var cfgs = aDiff.getConfigurationSet(slotPosition); + + for (var cfgToMerge : mergeStrategy.chooseConfigurationsToMerge(aDiff, cfgs, layer)) { + var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); + try { + var sourceFsAid = cfgs.getConfigurations().get(0).getRepresentativeAID(); + var result = mergeSlotFeature(aTargetDocument, aTargetUsername, + type2layer.get(slotPosition.getType()), aTargetCas, sourceFS, + sourceFsAid.feature, sourceFsAid.index); + LOG.trace(" `-> merged link annotation [{}] -> [{}]", sourceFS.getAddress(), + result.getTargetFSAddress()); + } + catch (AnnotationException e) { + LOG.trace(" `-> not merged link annotation [{}]: {}", sourceFS.getAddress(), + e.getMessage()); + localContext.messages.add(LogMessage.error(this, "%s", e.getMessage())); + } + } + } + + private void mergeSpanLayers(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + ArrayList layerNames) + { for (var layerName : layerNames) { - var relationPositions = aDiff.getPositions().stream() + var spanPositions = aDiff.getPositions().stream() .filter(pos -> layerName.equals(pos.getType())) - .filter(pos -> pos instanceof RelationPosition) - .map(pos -> (RelationPosition) pos) // + .filter(pos -> pos instanceof SpanPosition) // + .map(pos -> (SpanPosition) pos) + // We don't process slot features here (they are span sub-positions) + .filter(pos -> pos.getLinkFeature() == null) // .toList(); - if (relationPositions.isEmpty()) { + if (spanPositions.isEmpty()) { continue; } - LOG.debug("Processing {} relation positions on layer [{}]", relationPositions.size(), + LOG.debug("Processing [{}] span positions on layer [{}]", spanPositions.size(), layerName); - for (var relationPosition : relationPositions) { - LOG.trace(" | processing {}", relationPosition); - var layer = type2layer.get(relationPosition.getType()); - var cfgs = aDiff.getConfigurationSet(relationPosition); - - var cfgsToMerge = chooseConfigurationsToMerge(layer, aDiff, cfgs); - - if (cfgsToMerge.isEmpty()) { - continue; - } - - for (var cfgToMerge : cfgsToMerge) { - try { - var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); - var result = mergeRelationAnnotation(aTargetDocument, aTargetUsername, - type2layer.get(relationPosition.getType()), aTargetCas, sourceFS); - LOG.trace(" `-> merged annotation with agreement"); - - switch (result.getState()) { - case CREATED: - created++; - break; - case UPDATED: - updated++; - break; - } - } - catch (AnnotationException e) { - LOG.trace(" `-> not merged annotation: {}", e.getMessage()); - messages.add(LogMessage.error(this, "%s", e.getMessage())); - } - } + // First we merge the spans so that we can attach the relations/links to them later. + // Slots are also excluded for the moment + for (var spanPosition : spanPositions) { + mergeSpanPosition(aDiff, aTargetDocument, aTargetUsername, aTargetCas, aCasMap, + localContext, type2layer, spanPosition); } } + } - LOG.trace("Merge complete. Created: {} Updated: {}", created, updated); + private void mergeSpanPosition(DiffResult aDiff, SourceDocument aTargetDocument, + String aTargetUsername, CAS aTargetCas, Map aCasMap, + PerCasMergeContext localContext, Map type2layer, + SpanPosition spanPosition) + { + LOG.trace(" | processing {}", spanPosition); + var layer = type2layer.get(spanPosition.getType()); + var cfgs = aDiff.getConfigurationSet(spanPosition); - if (eventPublisher != null) { - eventPublisher - .publishEvent(new BulkAnnotationEvent(this, aTargetDocument, aTargetUsername)); + for (var cfgsToMerge : mergeStrategy.chooseConfigurationsToMerge(aDiff, cfgs, layer)) { + var sourceFS = (AnnotationFS) cfgsToMerge.getRepresentative(aCasMap); + try { + var result = mergeSpanAnnotation(aTargetDocument, aTargetUsername, + type2layer.get(spanPosition.getType()), aTargetCas, sourceFS); + + switch (result.getState()) { + case CREATED: + LOG.trace(" `-> merged span annotation [{}] (created) -> [{}]", + sourceFS.getAddress(), result.getTargetFSAddress()); + localContext.created++; + break; + case UPDATED: + LOG.trace(" `-> merged span annotation [{}] (updated) -> [{}]", + sourceFS.getAddress(), result.getTargetFSAddress()); + localContext.updated++; + break; + } + } + catch (AnnotationException e) { + LOG.trace(" `-> not merged span annotation [{}]: {}", sourceFS.getAddress(), + e.getMessage()); + localContext.messages.add(LogMessage.error(this, "%s", e.getMessage())); + } } - - return messages; } /** @@ -412,25 +460,26 @@ private void clearAnnotations(SourceDocument aDocument, CAS aCas) throws UIMAExc aCas.setDocumentText(backup.getDocumentText()); transferSegmentation(aDocument.getProject(), aCas, backup); + XmlNodeUtils.transferXmlDocumentStructure(aCas, backup); } /** * If tokens and/or sentences are not editable, then they are not part of the curation process * and we transfer them from the template CAS. */ - private void transferSegmentation(Project aProject, CAS aCas, CAS backup) + private void transferSegmentation(Project aProject, CAS aTarget, CAS aSource) { if (!schemaService.isTokenLayerEditable(aProject)) { // Transfer token boundaries - for (var t : selectTokens(backup)) { - aCas.addFsToIndexes(createToken(aCas, t.getBegin(), t.getEnd())); + for (var t : selectTokens(aSource)) { + aTarget.addFsToIndexes(createToken(aTarget, t.getBegin(), t.getEnd())); } } if (!schemaService.isSentenceLayerEditable(aProject)) { // Transfer sentence boundaries - for (var s : selectSentences(backup)) { - aCas.addFsToIndexes(createSentence(aCas, s.getBegin(), s.getEnd())); + for (var s : selectSentences(aSource)) { + aTarget.addFsToIndexes(createSentence(aTarget, s.getBegin(), s.getEnd())); } } } @@ -498,4 +547,11 @@ public CasMergeOperationResult mergeSpanAnnotation(SourceDocument aDoc, String a return CasMergeSpan.mergeSpanAnnotation(context, aDoc, aSrcUser, aLayer, aTargetCas, aSourceAnnotation, aLayer.isAllowStacking()); } + + private static class PerCasMergeContext + { + private int updated = 0; + private int created = 0; + private Set messages = new LinkedHashSet(); + } } diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkFeature.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkFeature.java index 7a36df55b44..13075d7b975 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkFeature.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkFeature.java @@ -26,7 +26,6 @@ import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toCollection; -import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -35,8 +34,6 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.jcas.tcas.Annotation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; @@ -49,8 +46,6 @@ public class CasMergeLinkFeature { - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - public static CasMergeOperationResult mergeSlotFeature(CasMergeContext aContext, SourceDocument aDocument, String aUsername, AnnotationLayer aAnnotationLayer, CAS aTargetCas, AnnotationFS aSourceFs, String aSourceFeature, int aSourceSlotIndex) diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeOperationResult.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeOperationResult.java index 1ae3e894353..f893b00978d 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeOperationResult.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeOperationResult.java @@ -33,12 +33,12 @@ public static enum ResultState } private final ResultState state; - private final int resultFSAddress; + private final int targetFSAddress; - public CasMergeOperationResult(ResultState aState, int aResultAddress) + public CasMergeOperationResult(ResultState aState, int aTargetAddress) { state = aState; - resultFSAddress = aResultAddress; + targetFSAddress = aTargetAddress; } public ResultState getState() @@ -46,8 +46,8 @@ public ResultState getState() return state; } - public int getResultFSAddress() + public int getTargetFSAddress() { - return resultFSAddress; + return targetFSAddress; } } diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategy.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategy.java index 37cacf8f517..81fdbd1c43a 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategy.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategy.java @@ -43,9 +43,7 @@ public class DefaultMergeStrategy public List chooseConfigurationsToMerge(DiffResult aDiff, ConfigurationSet aCfgs, AnnotationLayer aLayer) { - var stacked = aCfgs.getConfigurations().stream() // - .filter(Configuration::isStacked) // - .findAny().isPresent(); + var stacked = aCfgs.containsStackedConfigurations(); if (stacked) { LOG.trace(" `-> Not merging stacked annotation ({})", getClass().getSimpleName()); diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/DefaultMergeStrategyFactory.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategyFactory.java similarity index 93% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/DefaultMergeStrategyFactory.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategyFactory.java index 398bae28277..7a9657fb312 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/DefaultMergeStrategyFactory.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/DefaultMergeStrategyFactory.java @@ -15,14 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import org.apache.wicket.Component; import org.apache.wicket.markup.html.panel.EmptyPanel; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.inception.curation.config.CurationServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.curation.merge.strategy.DefaultMergeStrategy; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; /** diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategy.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategy.java index e6e734cffc0..053536ee675 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategy.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategy.java @@ -39,15 +39,11 @@ public class MergeIncompleteStrategy { private static final Logger LOG = LoggerFactory.getLogger(CasMerge.class); - public static final String BEAN_NAME = "incompleteAgreementNonStacked"; - @Override public List chooseConfigurationsToMerge(DiffResult aDiff, ConfigurationSet aCfgs, AnnotationLayer aLayer) { - var stacked = aCfgs.getConfigurations().stream() // - .filter(Configuration::isStacked) // - .findAny().isPresent(); + var stacked = aCfgs.containsStackedConfigurations(); if (stacked) { LOG.trace(" `-> Not merging stacked annotation ({})", getClass().getSimpleName()); diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeIncompleteStrategyFactory.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategyFactory.java similarity index 93% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeIncompleteStrategyFactory.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategyFactory.java index 09caf896187..09d1e19942b 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeIncompleteStrategyFactory.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeIncompleteStrategyFactory.java @@ -15,14 +15,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import org.apache.wicket.Component; import org.apache.wicket.markup.html.panel.EmptyPanel; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.inception.curation.config.CurationServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeIncompleteStrategy; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; /** diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory.java similarity index 92% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory.java index 7b1e4a8b149..a71f63bfab0 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory.java @@ -15,13 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import org.apache.wicket.Component; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategy; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPoint.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPoint.java similarity index 94% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPoint.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPoint.java index 79a656159f9..28ddfa3b290 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPoint.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPoint.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.support.extensionpoint.ExtensionPoint; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPointImpl.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPointImpl.java similarity index 96% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPointImpl.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPointImpl.java index eb480db5696..c8dc32d8926 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactoryExtensionPointImpl.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactoryExtensionPointImpl.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import java.util.List; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory_ImplBase.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory_ImplBase.java similarity index 97% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory_ImplBase.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory_ImplBase.java index 64ba203b0b7..016d91e80cc 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/MergeStrategyFactory_ImplBase.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/MergeStrategyFactory_ImplBase.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.fromJsonString; import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toJsonString; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategy.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategy.java index 8cae333d87b..a776bc0d1b6 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategy.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategy.java @@ -22,7 +22,6 @@ import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; -import java.util.Collections; import java.util.List; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -40,8 +39,6 @@ public class ThresholdBasedMergeStrategy { private static final Logger LOG = LoggerFactory.getLogger(CasMerge.class); - public static final String BEAN_NAME = "thresholdBased"; - /** * Number of users who have to annotate an item for it to be considered. If less than the given * number of users have voted, the item is considered Incomplete. @@ -60,12 +57,11 @@ public class ThresholdBasedMergeStrategy private final int topRanks; - public ThresholdBasedMergeStrategy(int aUserThreshold, double aConfidenceThreshold, - int aTopRanks) + private ThresholdBasedMergeStrategy(Builder builder) { - userThreshold = aUserThreshold; - confidenceThreshold = aConfidenceThreshold; - topRanks = aTopRanks; + this.userThreshold = builder.userThreshold; + this.confidenceThreshold = builder.confidenceThreshold; + this.topRanks = builder.topRanks; } @Override @@ -73,6 +69,9 @@ public List chooseConfigurationsToMerge(DiffResult aDiff, Configu AnnotationLayer aLayer) { var topRanksToConsider = aLayer.isAllowStacking() ? topRanks : 1; + if (topRanksToConsider == 0) { + topRanksToConsider = Integer.MAX_VALUE; + } var cfgsAboveUserThreshold = aCfgs.getConfigurations().stream() // .filter(cfg -> cfg.getCasGroupIds().size() >= userThreshold) // @@ -85,6 +84,11 @@ public List chooseConfigurationsToMerge(DiffResult aDiff, Configu return emptyList(); } + var totalAnnotators = cfgsAboveUserThreshold.stream() // + .flatMap(cfg -> cfg.getCasGroupIds().stream()) // + .distinct() // + .count(); + var totalVotes = cfgsAboveUserThreshold.stream() // .mapToDouble(cfg -> cfg.getCasGroupIds().size()) // .sum(); @@ -103,7 +107,7 @@ public List chooseConfigurationsToMerge(DiffResult aDiff, Configu if (topRanksToConsider == 1 && result.size() > 1) { // If we request only one result but there is more than one, then it is a tie. If only // a single result is requested, then ties are considered a dispute. - return Collections.emptyList(); + return emptyList(); } return result; @@ -137,4 +141,43 @@ public String toString() .append("userThreshold", userThreshold)// .append("confidenceThreshold", confidenceThreshold).toString(); } + + public static Builder builder() + { + return new Builder(); + } + + public static final class Builder + { + private int userThreshold; + private double confidenceThreshold; + private int topRanks; + + private Builder() + { + } + + public Builder withUserThreshold(int aUserThreshold) + { + userThreshold = aUserThreshold; + return this; + } + + public Builder withConfidenceThreshold(double aConfidenceThreshold) + { + confidenceThreshold = aConfidenceThreshold; + return this; + } + + public Builder withTopRanks(int aTopRanks) + { + topRanks = aTopRanks; + return this; + } + + public ThresholdBasedMergeStrategy build() + { + return new ThresholdBasedMergeStrategy(this); + } + } } diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactory.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactory.java similarity index 94% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactory.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactory.java index e626dac8b6c..7c941a1c976 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactory.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactory.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; public interface ThresholdBasedMergeStrategyFactory extends MergeStrategyFactory diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactoryImpl.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactoryImpl.java similarity index 84% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactoryImpl.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactoryImpl.java index 0b802a8794c..02d0f0848d7 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyFactoryImpl.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyFactoryImpl.java @@ -15,14 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import org.apache.wicket.Component; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.inception.curation.config.CurationServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategy; -import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategy; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; /** @@ -58,8 +56,11 @@ protected ThresholdBasedMergeStrategyTraits createTraits() @Override public MergeStrategy makeStrategy(ThresholdBasedMergeStrategyTraits aTraits) { - return new ThresholdBasedMergeStrategy(aTraits.getUserThreshold(), - aTraits.getConfidenceThreshold(), aTraits.getTopRanks()); + return ThresholdBasedMergeStrategy.builder() // + .withUserThreshold(aTraits.getUserThreshold()) // + .withConfidenceThreshold(aTraits.getConfidenceThreshold()) // + .withTopRanks(aTraits.getTopRanks()) // + .build(); } @Override diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraits.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraits.java similarity index 94% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraits.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraits.java index 33a8563d46d..36415110264 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraits.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraits.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import java.io.Serializable; @@ -58,7 +58,7 @@ public void setConfidenceThreshold(double aConfidenceThreshold) public int getTopRanks() { - return topRanks < 1 ? 1 : topRanks; + return topRanks < 0 ? 0 : topRanks; } public void setTopRanks(int aTopRanks) diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.html b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.html similarity index 73% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.html rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.html index 1b9b37ccd16..66d6f02430d 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.html +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.html @@ -51,14 +51,15 @@

+ Note that this setting only affects annotations on layers that allow stacking annotations. + For non-stacking other layers, an implicit setting of 1 is used here.
+ When set to 0, all labels are merged.
When set to 1, only the single most-voted label is merged. If there is a tie on the most-voted - label, then nothing is merged. When set to 2 or higher, the respective n most-voted - labels are pre-merged. If there is any tie within the n most-voted labels, then all labels that - still meet the lowest score of the tie are merged as well. For example, if set to 2 and three - annotators voted for label X and another two anotators voted for Y and Z + label, then nothing is merged.
+ When set to 2 or higher, the respective n most-voted labels are pre-merged. + If there is any tie within the n most-voted labels, then all labels that still meet the lowest score of the tie are merged as well.
+ For example, if set to 2 and three annotators voted for label X and another two anotators voted for Y and Z respectively, then Y and Z have a tie at the second rank, so both of them are merged. - Note that this setting only affects annotations on layers that allow stacking annotations. For other layers, - an implicit setting of 1 is used here.
@@ -72,11 +73,11 @@ %
- The confidence for a label is calculated by counting the number of annotators that provided a given label - dividing it by by the the total number annotators that annotated a given - position (votes(label) / all_votes). The user threshold is applied before - counting votes to calculate confidence. The confidence interacts with the number of valid labels - you expect. E.g. if you expect that there could be four valid labels (and therefore set the top-voted + The confidence for a label is calculated by dividing the number of annotators that chose a given label + by the number annotators that annotated a given position (votes(label) / all_votes). + The user threshold is applied before counting votes to calculate confidence. + The confidence interacts with the number of valid labels you expect. + E.g. if you expect that there could be four valid labels (and therefore set the top-voted parameter to 4), then the best confidence that a single label can have is 25% (= 100% / 4).
diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.java similarity index 94% rename from inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.java rename to inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.java index 72d4bdac418..e58f10e51b8 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/ThresholdBasedMergeStrategyTraitsEditor.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTraitsEditor.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.curation.merge; +package de.tudarmstadt.ukp.inception.curation.merge.strategy; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.form.Form; @@ -64,12 +64,14 @@ protected void onSubmit() form.add(new LambdaAjaxLink("presetUnanimousVote", this::actionPresetUnanimousVote)); - form.add(new NumberTextField<>("topRanks", Integer.class).setMinimum(1)); + form.add(new NumberTextField<>("topRanks", Integer.class).setMinimum(0)); form.add(new NumberTextField<>("userThreshold", Integer.class).setMinimum(1)); form.add(new NumberTextField<>("confidenceThreshold", Double.class) // - .setMinimum(0.0d).setMaximum(100.0d).setStep(0.1d) // + .setMinimum(0.0d) // + .setMaximum(100.0d) // + .setStep(0.1d) // .setModel(LambdaModelAdapter.of( // () -> traits.getConfidenceThreshold() * 100.0d, (v) -> traits.setConfidenceThreshold(v / 100.0d)))); diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeService.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeService.java index e6cfc372ba4..2d694964870 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeService.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeService.java @@ -46,6 +46,8 @@ public interface CurationMergeService * the merge strategy * @param aLayers * the layers to be merged + * @param aClearTargetCas + * whether to clear the target CAS before merging * @return any messages generated during the merge process. * @throws UIMAException * if there was an UIMA-level problem diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeServiceImpl.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeServiceImpl.java index eb0c980bcd0..993dfae3a8c 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeServiceImpl.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationMergeServiceImpl.java @@ -18,7 +18,7 @@ package de.tudarmstadt.ukp.inception.curation.service; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.getDiffAdapters; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.DiffAdapterRegistry.getDiffAdapters; import static java.lang.invoke.MethodHandles.lookup; import static java.util.stream.Collectors.toList; import static org.slf4j.LoggerFactory.getLogger; diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationService.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationService.java index 04bb9bcc41f..09a279ce7fb 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationService.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationService.java @@ -18,8 +18,8 @@ package de.tudarmstadt.ukp.inception.curation.service; import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactory; import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategy; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactory; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; public interface CurationService diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationServiceImpl.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationServiceImpl.java index cca6c6a7d24..3a0ed514c7a 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationServiceImpl.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/service/CurationServiceImpl.java @@ -24,9 +24,9 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.curation.config.CurationServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactoryExtensionPoint; import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategy; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactoryExtensionPoint; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; @@ -69,7 +69,7 @@ public CurationWorkflow readOrCreateCurationWorkflow(Project aProject) { CurationWorkflow result; try { - String query = "FROM CurationWorkflow WHERE project = :project"; + var query = "FROM CurationWorkflow WHERE project = :project"; result = entityManager.createQuery(query, CurationWorkflow.class) // .setParameter("project", aProject) // @@ -91,12 +91,12 @@ public MergeStrategy getDefaultMergeStrategy(Project aProject) return getMergeStrategy(readOrCreateCurationWorkflow(aProject)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({ "unchecked" }) @Override @Transactional public MergeStrategy getMergeStrategy(CurationWorkflow aCurationWorkflow) { - MergeStrategyFactory factory = getMergeStrategyFactory(aCurationWorkflow); + var factory = getMergeStrategyFactory(aCurationWorkflow); return factory.makeStrategy(factory.readTraits(aCurationWorkflow)); } diff --git a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/settings/MergeStrategyPanel.java b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/settings/MergeStrategyPanel.java index fe715a073e2..31bc4c9665e 100644 --- a/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/settings/MergeStrategyPanel.java +++ b/inception/inception-curation/src/main/java/de/tudarmstadt/ukp/inception/curation/settings/MergeStrategyPanel.java @@ -34,8 +34,8 @@ import org.apache.wicket.model.IModel; import org.apache.wicket.spring.injection.annot.SpringBean; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.MergeStrategyFactoryExtensionPoint; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.MergeStrategyFactoryExtensionPoint; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; import de.tudarmstadt.ukp.inception.support.lambda.LambdaModelAdapter; diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/export/CurationWorkflowExporterTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/export/CurationWorkflowExporterTest.java index b115899ecd6..441500de3a4 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/export/CurationWorkflowExporterTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/export/CurationWorkflowExporterTest.java @@ -39,9 +39,9 @@ import de.tudarmstadt.ukp.clarin.webanno.api.export.ProjectImportRequest; import de.tudarmstadt.ukp.clarin.webanno.export.model.ExportedProject; import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.inception.curation.merge.ThresholdBasedMergeStrategyFactory; -import de.tudarmstadt.ukp.inception.curation.merge.ThresholdBasedMergeStrategyFactoryImpl; -import de.tudarmstadt.ukp.inception.curation.merge.ThresholdBasedMergeStrategyTraits; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategyFactory; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategyFactoryImpl; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategyTraits; import de.tudarmstadt.ukp.inception.curation.model.CurationWorkflow; import de.tudarmstadt.ukp.inception.curation.service.CurationService; @@ -82,7 +82,8 @@ public void thatExportingWorks() throws Exception // Export the project var exportRequest = new FullProjectExportRequest(sourceProject, null, false); - var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test"); + var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test", + exportRequest.getFilenamePrefix()); var exportedProject = new ExportedProject(); var stage = mock(ZipOutputStream.class); diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkTest.java index ecf94226f06..0e5073969d4 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeLinkTest.java @@ -17,33 +17,42 @@ */ package de.tudarmstadt.ukp.inception.curation.merge; +import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; +import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.ANY_OVERLAP; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.INCLUDE; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.HOST_TYPE; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.LINKS_FEATURE; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.TARGET_FEATURE; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.makeLinkFS; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.makeLinkHostFS; +import static de.tudarmstadt.ukp.inception.schema.api.feature.MaterializedLink.toMaterializedLinks; +import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toJsonString; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.JCasFactory.createJCas; import static org.apache.uima.fit.util.FSUtil.getFeature; +import static org.apache.uima.fit.util.FSUtil.setFeature; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.lang.invoke.MethodHandles; import java.util.List; +import java.util.Map; -import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.JCas; import org.apache.uima.jcas.tcas.Annotation; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureTraits; +import de.tudarmstadt.ukp.inception.curation.merge.strategy.ThresholdBasedMergeStrategy; import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; -import de.tudarmstadt.ukp.inception.support.json.JSONUtil; +import de.tudarmstadt.ukp.inception.schema.api.feature.MaterializedLink; public class CasMergeLinkTest extends CasMergeTestBase @@ -52,7 +61,8 @@ public class CasMergeLinkTest private static final String DUMMY_USER = "dummyTargetUser"; - private JCas sourceCas; + private JCas sourceCas1; + private JCas sourceCas2; private JCas targetCas; @Override @@ -60,7 +70,8 @@ public class CasMergeLinkTest public void setup() throws Exception { super.setup(); - sourceCas = createJCas(); + sourceCas1 = createJCas(); + sourceCas2 = createJCas(); targetCas = createJCas(); } @@ -69,11 +80,11 @@ public void thatLinkIsCopiedFromSourceToTarget() throws Exception { // Set up source CAS var role = "slot1"; - var sourceFs = makeLinkHostFS(sourceCas, 0, 0, makeLinkFS(sourceCas, role, 0, 0)); + var sourceFs = makeLinkHostFS(sourceCas1, 0, 0, makeLinkFS(sourceCas1, role, 0, 0)); // Set up target CAS var target = makeLinkHostFS(targetCas, 0, 0); - var targetFiller = new Token(targetCas, 0, 0); + var targetFiller = new NamedEntity(targetCas, 0, 0); targetFiller.addToIndexes(); // Perform merge @@ -94,12 +105,12 @@ public void thatLinkIsAttachedToCorrectStackedTargetWithoutLabel() throws Except // Set up source CAS var role = "slot1"; - var sourceFs1 = makeLinkHostFS(sourceCas, 0, 0, makeLinkFS(sourceCas, role, 0, 0)); - var sourceFs2 = makeLinkHostFS(sourceCas, 0, 0, makeLinkFS(sourceCas, role, 1, 1)); + var sourceFs1 = makeLinkHostFS(sourceCas1, 0, 0, makeLinkFS(sourceCas1, role, 0, 0)); + var sourceFs2 = makeLinkHostFS(sourceCas1, 0, 0, makeLinkFS(sourceCas1, role, 1, 1)); // Set up target CAS var target1 = makeLinkHostFS(targetCas, 0, 0); - var targetFiller1 = new Token(targetCas, 0, 0); + var targetFiller1 = new NamedEntity(targetCas, 0, 0); targetFiller1.addToIndexes(); // Perform merge @@ -114,7 +125,7 @@ public void thatLinkIsAttachedToCorrectStackedTargetWithoutLabel() throws Except // Add stacked target to target CAS var target2 = makeLinkHostFS(targetCas, 0, 0); - var targetFiller2 = new Token(targetCas, 1, 1); + var targetFiller2 = new NamedEntity(targetCas, 1, 1); targetFiller2.addToIndexes(); // Perform another merge @@ -135,15 +146,15 @@ public void thatLinkIsAttachedToCorrectStackedTargetWithLabel() throws Exception // Set up source CAS var role = "slot1"; - var sourceFs1 = makeLinkHostFS(sourceCas, 0, 0, makeLinkFS(sourceCas, role, 0, 0)); - FSUtil.setFeature(sourceFs1, "f1", "foo"); - var sourceFs2 = makeLinkHostFS(sourceCas, 0, 0, makeLinkFS(sourceCas, role, 1, 1)); - FSUtil.setFeature(sourceFs2, "f1", "bar"); + var sourceFs1 = makeLinkHostFS(sourceCas1, 0, 0, makeLinkFS(sourceCas1, role, 0, 0)); + setFeature(sourceFs1, "f1", "foo"); + var sourceFs2 = makeLinkHostFS(sourceCas1, 0, 0, makeLinkFS(sourceCas1, role, 1, 1)); + setFeature(sourceFs2, "f1", "bar"); // Set up target CAS var target1 = makeLinkHostFS(targetCas, 0, 0); - FSUtil.setFeature(target1, "f1", "foo"); - var targetFiller1 = new Token(targetCas, 0, 0); + setFeature(target1, "f1", "foo"); + var targetFiller1 = new NamedEntity(targetCas, 0, 0); targetFiller1.addToIndexes(); // Perform merge @@ -158,8 +169,8 @@ public void thatLinkIsAttachedToCorrectStackedTargetWithLabel() throws Exception // Add stacked target to target CAS var target2 = makeLinkHostFS(targetCas, 0, 0); - FSUtil.setFeature(target2, "f1", "bar"); - var targetFiller2 = new Token(targetCas, 1, 1); + setFeature(target2, "f1", "bar"); + var targetFiller2 = new NamedEntity(targetCas, 1, 1); targetFiller2.addToIndexes(); // Perform another merge @@ -178,12 +189,12 @@ public void thatSecondLinkWithSameTargetIsRejectedWhenRolesAreDisabled() throws { var traits = new LinkFeatureTraits(); traits.setEnableRoleLabels(false); - slotFeature.setTraits(JSONUtil.toJsonString(traits)); + slotFeature.setTraits(toJsonString(traits)); // Set up source CAS - var sourceFs = buildAnnotation(sourceCas.getCas(), HOST_TYPE) // + var sourceFs = buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // .at(0, 0) // - .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas, null, 0, 0))) + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, null, 0, 0))) .buildAndAddToIndexes(); // Set up target CAS @@ -211,9 +222,9 @@ public void thatSecondLinkWithSameTargetButDifferentRoleIsAddedWhenRolesAreEnabl throws Exception { // Set up source CAS - var sourceFs = buildAnnotation(sourceCas.getCas(), HOST_TYPE) // + var sourceFs = buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // .at(0, 0) // - .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas, "role1", 0, 0))) + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role1", 0, 0))) .buildAndAddToIndexes(); // Set up target CAS @@ -242,9 +253,9 @@ public void thatSecondLinkWithSameTargetAndSameRoleIsRejectedWhenRolesAreEnabled throws Exception { // Set up source CAS - var sourceFs = buildAnnotation(sourceCas.getCas(), HOST_TYPE) // + var sourceFs = buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // .at(0, 0) // - .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas, "role1", 0, 0))) + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role1", 0, 0))) .buildAndAddToIndexes(); // Set up target CAS @@ -267,4 +278,107 @@ public void thatSecondLinkWithSameTargetAndSameRoleIsRejectedWhenRolesAreEnabled .containsExactly( // new LinkWithRoleModel("role1", null, targetFiller.getAddress())); } + + @Nested + class SingleUserThesholdBasedMergeStrategyTests + { + @BeforeEach + void setup() throws Exception + { + slotLayer.setOverlapMode(ANY_OVERLAP); + var traits = new LinkFeatureTraits(); + traits.setDiffMode(INCLUDE); + slotHostDiffAdapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES, + INCLUDE); + + slotFeature.setTraits(toJsonString(traits)); + sut.setMergeStrategy(ThresholdBasedMergeStrategy.builder() // + .withUserThreshold(1) // + .withConfidenceThreshold(0) // + .withTopRanks(0) // + .build()); + } + + @Test + public void thatStackedLinkHostsWithDifferentTargetsAreMerged() throws Exception + { + buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // + .at(0, 0) // + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role1", 1, 1))) + .buildAndAddToIndexes(); + + buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // + .at(0, 0) // + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role2", 1, 1))) + .buildAndAddToIndexes(); + + var casMap = Map.of("source", sourceCas1.getCas()); + var diff = doDiff(diffAdapters, casMap).toResult(); + sut.mergeCas(diff, document, DUMMY_USER, targetCas.getCas(), casMap); + + var targetHosts = targetCas.select(HOST_TYPE).asList(); + assertThat(targetHosts) // + .as("Links by host in target CAS") // + .hasSize(2) // + .extracting(host -> toMaterializedLinks(host, LINKS_FEATURE, "role", "target")) // + .containsExactlyInAnyOrder( // + asList(new MaterializedLink(LINKS_FEATURE, "role1", + NamedEntity._TypeName, 1, 1)), // + asList(new MaterializedLink(LINKS_FEATURE, "role2", + NamedEntity._TypeName, 1, 1))); + } + } + + @Nested + class DualUserThesholdBasedMergeStrategyTests + { + @BeforeEach + void setup() throws Exception + { + slotLayer.setOverlapMode(ANY_OVERLAP); + var traits = new LinkFeatureTraits(); + traits.setDiffMode(INCLUDE); + slotHostDiffAdapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES, + INCLUDE); + + slotFeature.setTraits(toJsonString(traits)); + sut.setMergeStrategy(ThresholdBasedMergeStrategy.builder() // + .withUserThreshold(2) // + .withConfidenceThreshold(0) // + .withTopRanks(0) // + .build()); + } + + @Test + public void thatMatchingStackedLinksAreMerged() throws Exception + { + buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // + .at(0, 0) // + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role1", 1, 1))) + .buildAndAddToIndexes(); + + buildAnnotation(sourceCas1.getCas(), HOST_TYPE) // + .at(0, 0) // + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas1, "role2", 1, 1))) + .buildAndAddToIndexes(); + + buildAnnotation(sourceCas2.getCas(), HOST_TYPE) // + .at(0, 0) // + .withFeature(LINKS_FEATURE, asList(makeLinkFS(sourceCas2, "role1", 1, 1))) + .buildAndAddToIndexes(); + + var casMap = Map.of("source1", sourceCas1.getCas(), "source2", sourceCas2.getCas()); + var diff = doDiff(diffAdapters, casMap).toResult(); + sut.mergeCas(diff, document, DUMMY_USER, targetCas.getCas(), casMap); + + var targetHosts = targetCas.select(HOST_TYPE).asList(); + assertThat(targetHosts) // + .as("Links by host in target CAS") // + .hasSize(1) // + .extracting(host -> toMaterializedLinks(host, LINKS_FEATURE, "role", "target")) // + .containsExactlyInAnyOrder( // + asList(new MaterializedLink(LINKS_FEATURE, "role1", + NamedEntity._TypeName, 1, 1))); + } + } } diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRelationTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRelationTest.java index 06bd4f93fab..ee6451dd530 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRelationTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRelationTest.java @@ -19,9 +19,9 @@ import static de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS._FeatName_PosValue; import static de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token._FeatName_pos; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_SOURCE; +import static de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport.FEAT_REL_TARGET; import static de.tudarmstadt.ukp.inception.curation.merge.CasMergeOperationResult.ResultState.CREATED; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_SOURCE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; import static org.apache.uima.fit.factory.CasFactory.createCas; import static org.apache.uima.fit.util.FSUtil.getFeature; diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRemergeTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRemergeTest.java index fb64f66f6cf..eda948e9c5c 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRemergeTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeRemergeTest.java @@ -23,12 +23,14 @@ import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiffSummaryState.calculateState; import static de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS._FeatName_PosValue; import static de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token._FeatName_pos; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.EXCLUDE; import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.HOST_TYPE; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.createMultiLinkWithRoleTestTypeSystem; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.makeLinkFS; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.makeLinkHostFS; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.createCasCopy; import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.JCasFactory.createJCas; import static org.apache.uima.fit.factory.JCasFactory.createText; @@ -93,8 +95,12 @@ public void thatIncompleteAnnotationIsNotMerged() throws Exception .extracting(set -> set.getPosition()) // .usingRecursiveFieldByFieldElementComparator() // .containsExactly( // - new SpanPosition(null, null, POS.class.getName(), 0, 4, "word", null, null, - -1, -1, null, null)); + SpanPosition.builder() // + .withType(POS.class.getName()) // + .withBegin(0) // + .withEnd(4) // + .withText("word") // + .build()); assertThat(select(curatorCas, POS.class)).isEmpty(); assertThat(calculateState(result)).isEqualTo(INCOMPLETE); @@ -118,23 +124,26 @@ public void thatIncompleteAnnotationIsMerged() throws Exception casByUser.put("user1", user1); casByUser.put("user2", user2); - var curatorCas = createText( - casByUser.values().stream().findFirst().get().getDocumentText()); + var curatorCas = createCasCopy(user1); var result = doDiff(diffAdapters, casByUser).toResult(); sut.setMergeStrategy(new MergeIncompleteStrategy()); - sut.clearAndMergeCas(result, document, DUMMY_USER, curatorCas.getCas(), casByUser); + sut.clearAndMergeCas(result, document, DUMMY_USER, curatorCas, casByUser); assertThat(result.getDifferingConfigurationSets()).isEmpty(); assertThat(result.getIncompleteConfigurationSets().values()) .extracting(set -> set.getPosition()) // .usingRecursiveFieldByFieldElementComparator()// - .containsExactly(// - new SpanPosition(null, null, POS.class.getName(), 0, 4, "word", null, null, - -1, -1, null, null)); - - assertThat(select(curatorCas, POS.class)).hasSize(1); + .containsExactly( // + SpanPosition.builder() // + .withType(POS.class.getName()) // + .withBegin(0) // + .withEnd(4) // + .withText("word") // + .build()); + + assertThat(curatorCas.select(POS.class).asList()).hasSize(1); assertThat(calculateState(result)).isEqualTo(INCOMPLETE); } @@ -260,7 +269,7 @@ public void multiLinkMultiHostTest() throws Exception curatorCas.setDocumentText(casByUser.values().stream().findFirst().get().getDocumentText()); var adapter = new SpanDiffAdapter(HOST_TYPE); - adapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES); + adapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES, EXCLUDE); var result = doDiff(asList(adapter), casByUser).toResult(); diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeSuiteTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeSuiteTest.java index 05a94276323..696ca0ca663 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeSuiteTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeSuiteTest.java @@ -19,18 +19,19 @@ import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.loadWebAnnoTsv3; +import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.createCasCopy; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.AnalysisEngineFactory.createEngineDescription; -import static org.apache.uima.fit.factory.CasFactory.createText; import static org.apache.uima.fit.pipeline.SimplePipeline.runPipeline; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.File; import java.io.FilenameFilter; import java.util.HashMap; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.RegexFileFilter; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.uima.cas.CAS; @@ -63,8 +64,7 @@ public void runTest(File aReferenceFolder) throws Exception casByUser.put(inputFile.getName(), loadWebAnnoTsv3(inputFile).getCas()); } - var curatorCas = createText( - casByUser.values().stream().findFirst().get().getDocumentText()); + var curatorCas = createCasCopy(casByUser.values().stream().findFirst().get()); var result = doDiff(diffAdapters, casByUser).toResult(); @@ -82,6 +82,7 @@ private void writeAndAssertEquals(CAS curatorCas, File aReferenceFolder) throws dmd.setDocumentId("curator"); runPipeline(curatorCas, createEngineDescription( // WebannoTsv3XWriter.class, // + WebannoTsv3XWriter.PARAM_USE_DOCUMENT_ID, true, // WebannoTsv3XWriter.PARAM_TARGET_LOCATION, targetFolder, // WebannoTsv3XWriter.PARAM_OVERWRITE, true)); @@ -90,8 +91,8 @@ private void writeAndAssertEquals(CAS curatorCas, File aReferenceFolder) throws var actualFile = new File(targetFolder, "curator.tsv"); - var reference = FileUtils.readFileToString(referenceFile, "UTF-8"); - var actual = FileUtils.readFileToString(actualFile, "UTF-8"); + var reference = contentOf(referenceFile, UTF_8); + var actual = contentOf(actualFile, UTF_8); assertThat(actual).isEqualToNormalizingNewlines(reference); } diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeTestBase.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeTestBase.java index f4df3e36345..67c59076e47 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeTestBase.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CasMergeTestBase.java @@ -20,24 +20,22 @@ import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationDiffAdapter.DEPENDENCY_DIFF_ADAPTER; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter.NER_DIFF_ADAPTER; import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter.POS_DIFF_ADAPTER; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter.SENTENCE_DIFF_ADAPTER; -import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanDiffAdapter.TOKEN_DIFF_ADAPTER; import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.CHARACTERS; import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.NO_OVERLAP; import static de.tudarmstadt.ukp.clarin.webanno.model.OverlapMode.OVERLAP_ONLY; +import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureDiffMode.EXCLUDE; import static de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureMultiplicityMode.ONE_TARGET_MULTIPLE_ROLES; import static de.tudarmstadt.ukp.inception.curation.merge.CurationTestUtils.HOST_TYPE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.RELATION_TYPE; import static java.util.Arrays.asList; +import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import java.util.ArrayList; import java.util.List; -import org.apache.uima.cas.CAS; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -63,12 +61,12 @@ import de.tudarmstadt.ukp.inception.annotation.feature.number.NumberFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistryImpl; -import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistryImpl; @ExtendWith(MockitoExtension.class) public class CasMergeTestBase @@ -103,7 +101,9 @@ public class CasMergeTestBase protected AnnotationFeature multiValSpanF1; protected AnnotationFeature multiValSpanF2; protected SourceDocument document; + protected List diffAdapters; + protected SpanDiffAdapter slotHostDiffAdapter; protected static final RelationDiffAdapter MULTIVALREL_DIFF_ADAPTER = new RelationDiffAdapter( "webanno.custom.Multivalrel", "Dependent", "Governor", "rel1", "rel2"); @@ -113,12 +113,13 @@ public class CasMergeTestBase @BeforeEach public void setup() throws Exception { - var slotHostDiffAdapter = new SpanDiffAdapter(HOST_TYPE); - slotHostDiffAdapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES); + slotHostDiffAdapter = new SpanDiffAdapter(HOST_TYPE); + slotHostDiffAdapter.addLinkFeature("links", "role", "target", ONE_TARGET_MULTIPLE_ROLES, + EXCLUDE); diffAdapters = new ArrayList<>(); - diffAdapters.add(TOKEN_DIFF_ADAPTER); - diffAdapters.add(SENTENCE_DIFF_ADAPTER); + // diffAdapters.add(TOKEN_DIFF_ADAPTER); + // diffAdapters.add(SENTENCE_DIFF_ADAPTER); diffAdapters.add(POS_DIFF_ADAPTER); diffAdapters.add(NER_DIFF_ADAPTER); diffAdapters.add(DEPENDENCY_DIFF_ADAPTER); @@ -156,7 +157,7 @@ public void setup() throws Exception posFeature = new AnnotationFeature(); posFeature.setName("PosValue"); posFeature.setEnabled(true); - posFeature.setType(CAS.TYPE_NAME_STRING); + posFeature.setType(TYPE_NAME_STRING); posFeature.setUiName("PosValue"); posFeature.setLayer(posLayer); posFeature.setProject(project); @@ -166,7 +167,7 @@ public void setup() throws Exception posCoarseFeature = new AnnotationFeature(); posCoarseFeature.setName("coarseValue"); posCoarseFeature.setEnabled(true); - posCoarseFeature.setType(CAS.TYPE_NAME_STRING); + posCoarseFeature.setType(TYPE_NAME_STRING); posCoarseFeature.setUiName("coarseValue"); posCoarseFeature.setLayer(posLayer); posCoarseFeature.setProject(project); @@ -179,7 +180,7 @@ public void setup() throws Exception neFeature = new AnnotationFeature(); neFeature.setName("value"); neFeature.setEnabled(true); - neFeature.setType(CAS.TYPE_NAME_STRING); + neFeature.setType(TYPE_NAME_STRING); neFeature.setUiName("value"); neFeature.setLayer(neLayer); neFeature.setProject(project); @@ -189,22 +190,22 @@ public void setup() throws Exception neIdentifierFeature = new AnnotationFeature(); neIdentifierFeature.setName("identifier"); neIdentifierFeature.setEnabled(true); - neIdentifierFeature.setType(CAS.TYPE_NAME_STRING); + neIdentifierFeature.setType(TYPE_NAME_STRING); neIdentifierFeature.setUiName("identifier"); neIdentifierFeature.setLayer(neLayer); neIdentifierFeature.setProject(project); neIdentifierFeature.setVisible(true); neIdentifierFeature.setCuratable(true); - depLayer = new AnnotationLayer(Dependency.class.getName(), "Dependency", RELATION_TYPE, - project, true, SINGLE_TOKEN, OVERLAP_ONLY); + depLayer = new AnnotationLayer(Dependency.class.getName(), "Dependency", + RelationLayerSupport.TYPE, project, true, SINGLE_TOKEN, OVERLAP_ONLY); depLayer.setAttachType(tokenLayer); depLayer.setAttachFeature(tokenPosFeature); depFeature = new AnnotationFeature(); depFeature.setName("DependencyType"); depFeature.setEnabled(true); - depFeature.setType(CAS.TYPE_NAME_STRING); + depFeature.setType(TYPE_NAME_STRING); depFeature.setUiName("Relation"); depFeature.setLayer(depLayer); depFeature.setProject(project); @@ -214,7 +215,7 @@ public void setup() throws Exception depFlavorFeature = new AnnotationFeature(); depFlavorFeature.setName("flavor"); depFlavorFeature.setEnabled(true); - depFlavorFeature.setType(CAS.TYPE_NAME_STRING); + depFlavorFeature.setType(TYPE_NAME_STRING); depFlavorFeature.setUiName("flavor"); depFlavorFeature.setLayer(depLayer); depFlavorFeature.setProject(project); @@ -242,7 +243,7 @@ public void setup() throws Exception stringFeature = new AnnotationFeature(); stringFeature.setName("f1"); stringFeature.setEnabled(true); - stringFeature.setType(CAS.TYPE_NAME_STRING); + stringFeature.setType(TYPE_NAME_STRING); stringFeature.setUiName("f1"); stringFeature.setLayer(slotLayer); stringFeature.setProject(project); @@ -255,7 +256,7 @@ public void setup() throws Exception multiValSpanF1 = new AnnotationFeature(); multiValSpanF1.setName("f1"); multiValSpanF1.setEnabled(true); - multiValSpanF1.setType(CAS.TYPE_NAME_STRING); + multiValSpanF1.setType(TYPE_NAME_STRING); multiValSpanF1.setUiName("f1"); multiValSpanF1.setLayer(multiValSpan); multiValSpanF1.setProject(project); @@ -265,7 +266,7 @@ public void setup() throws Exception multiValSpanF2 = new AnnotationFeature(); multiValSpanF2.setName("f2"); multiValSpanF2.setEnabled(true); - multiValSpanF2.setType(CAS.TYPE_NAME_STRING); + multiValSpanF2.setType(TYPE_NAME_STRING); multiValSpanF2.setUiName("f2"); multiValSpanF2.setLayer(multiValSpan); multiValSpanF2.setProject(project); @@ -273,13 +274,13 @@ public void setup() throws Exception multiValSpanF2.setCuratable(true); multiValRel = new AnnotationLayer("webanno.custom.Multivalrel", "Multivalrel", - RELATION_TYPE, project, true, SINGLE_TOKEN, OVERLAP_ONLY); + RelationLayerSupport.TYPE, project, true, SINGLE_TOKEN, OVERLAP_ONLY); multiValRel.setAttachType(multiValSpan); multiValRelRel1 = new AnnotationFeature(); multiValRelRel1.setName("rel1"); multiValRelRel1.setEnabled(true); - multiValRelRel1.setType(CAS.TYPE_NAME_STRING); + multiValRelRel1.setType(TYPE_NAME_STRING); multiValRelRel1.setUiName("rel1"); multiValRelRel1.setLayer(multiValSpan); multiValRelRel1.setProject(project); @@ -289,7 +290,7 @@ public void setup() throws Exception multiValRelRel2 = new AnnotationFeature(); multiValRelRel2.setName("rel2"); multiValRelRel2.setEnabled(true); - multiValRelRel2.setType(CAS.TYPE_NAME_STRING); + multiValRelRel2.setType(TYPE_NAME_STRING); multiValRelRel2.setUiName("rel2"); multiValRelRel2.setLayer(multiValSpan); multiValRelRel2.setProject(project); diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CurationTestUtils.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CurationTestUtils.java index f8259ac9343..d0dd64fc519 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CurationTestUtils.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/CurationTestUtils.java @@ -20,8 +20,10 @@ import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.RELATION_TYPE; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static de.tudarmstadt.ukp.inception.support.uima.FeatureStructureBuilder.buildFS; import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.CollectionReaderFactory.createReader; +import static org.apache.uima.fit.factory.TypeSystemDescriptionFactory.createTypeSystemDescription; import static org.apache.uima.util.CasCreationUtils.mergeTypeSystems; import java.io.File; @@ -37,7 +39,6 @@ import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.factory.JCasFactory; -import org.apache.uima.fit.factory.TypeSystemDescriptionFactory; import org.apache.uima.jcas.JCas; import org.apache.uima.resource.metadata.TypeSystemDescription; import org.apache.uima.resource.metadata.impl.TypeSystemDescription_impl; @@ -46,7 +47,7 @@ import de.tudarmstadt.ukp.clarin.webanno.tsv.WebannoTsv2Reader; import de.tudarmstadt.ukp.clarin.webanno.tsv.WebannoTsv3XReader; -import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; @@ -138,7 +139,7 @@ public static CAS readWebAnnoTSV(String aPath, TypeSystemDescription aType) WebannoTsv2Reader.PARAM_SOURCE_LOCATION, "src/test/resources/" + aPath); CAS cas; if (aType != null) { - var builtInTypes = TypeSystemDescriptionFactory.createTypeSystemDescription(); + var builtInTypes = createTypeSystemDescription(); List allTypes = new ArrayList<>(); allTypes.add(builtInTypes); allTypes.add(aType); @@ -161,8 +162,7 @@ public static CAS readXMI(String aPath, TypeSystemDescription aType) XmiReader.PARAM_SOURCE_LOCATION, "src/test/resources/" + aPath); CAS jcas; if (aType != null) { - TypeSystemDescription builtInTypes = TypeSystemDescriptionFactory - .createTypeSystemDescription(); + TypeSystemDescription builtInTypes = createTypeSystemDescription(); List allTypes = new ArrayList<>(); allTypes.add(builtInTypes); allTypes.add(aType); @@ -193,7 +193,7 @@ public static TypeSystemDescription createMultiLinkWithRoleTestTypeSytem() throw hostTD.addFeature(LINKS_FEATURE, "", CAS.TYPE_NAME_FS_ARRAY, linkTD.getName(), false); typeSystems.add(tsd); - typeSystems.add(TypeSystemDescriptionFactory.createTypeSystemDescription()); + typeSystems.add(createTypeSystemDescription()); return mergeTypeSystems(typeSystems); } @@ -218,7 +218,7 @@ public static TypeSystemDescription createMultiLinkWithRoleTestTypeSystem(String } typeSystems.add(tsd); - typeSystems.add(TypeSystemDescriptionFactory.createTypeSystemDescription()); + typeSystems.add(createTypeSystemDescription()); return mergeTypeSystems(typeSystems); } @@ -271,15 +271,12 @@ public static AnnotationFS makeLinkHostFS(JCas aCas, int aBegin, int aEnd, Featu public static FeatureStructure makeLinkFS(JCas aCas, String aRole, int aTargetBegin, int aTargetEnd) { - var token1 = new Token(aCas, aTargetBegin, aTargetEnd); - token1.addToIndexes(); + var token = new NamedEntity(aCas, aTargetBegin, aTargetEnd); + token.addToIndexes(); - var linkType = aCas.getTypeSystem().getType(LINK_TYPE); - var linkA1 = aCas.getCas().createFS(linkType); - linkA1.setStringValue(linkType.getFeatureByBaseName(ROLE_FEATURE), aRole); - linkA1.setFeatureValue(linkType.getFeatureByBaseName(TARGET_FEATURE), token1); - aCas.getCas().addFsToIndexes(linkA1); - - return linkA1; + return buildFS(aCas.getCas(), LINK_TYPE) // + .withFeature(ROLE_FEATURE, aRole) // + .withFeature(TARGET_FEATURE, token) // + .buildAndAddToIndexes(); } } diff --git a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTest.java b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTest.java index 6d2e3821576..d8291f6eb10 100644 --- a/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTest.java +++ b/inception/inception-curation/src/test/java/de/tudarmstadt/ukp/inception/curation/merge/strategy/ThresholdBasedMergeStrategyTest.java @@ -198,23 +198,26 @@ void testWithPointFiveConfidenceWithMultipleResults() private List calculate(int aUserThreshold, double aConfidenceThreshold, int aTopRanks, OverlapMode aOverlapMode, Configuration... aConfigurations) { - ConfigurationSet cfgSet = new ConfigurationSet(position); - for (Configuration cfg : aConfigurations) { + var cfgSet = new ConfigurationSet(position); + for (var cfg : aConfigurations) { cfgSet.addConfiguration(cfg); } - AnnotationLayer layer = new AnnotationLayer(); + var layer = new AnnotationLayer(); layer.setOverlapMode(aOverlapMode); - ThresholdBasedMergeStrategy sut = new ThresholdBasedMergeStrategy(aUserThreshold, - aConfidenceThreshold, aTopRanks); + var sut = ThresholdBasedMergeStrategy.builder() // + .withUserThreshold(aUserThreshold) // + .withConfidenceThreshold(aConfidenceThreshold) // + .withTopRanks(aTopRanks) // + .build(); return sut.chooseConfigurationsToMerge(null, cfgSet, layer); } private Configuration makeConfiguration(String... aAnnototors) { - Configuration cfg = new Configuration(position); - for (String annotator : aAnnototors) { + var cfg = new Configuration(position); + for (var annotator : aAnnototors) { cfg.add(annotator, new AID(0)); } return cfg; diff --git a/inception/inception-curation/src/test/resources/log4j2-test.xml b/inception/inception-curation/src/test/resources/log4j2-test.xml index ab063afc67a..3e6abb8335d 100644 --- a/inception/inception-curation/src/test/resources/log4j2-test.xml +++ b/inception/inception-curation/src/test/resources/log4j2-test.xml @@ -11,7 +11,8 @@ - - + + + diff --git a/inception/inception-dependencies/pom.xml b/inception/inception-dependencies/pom.xml index ba05d0f9cde..d8aff28069f 100644 --- a/inception/inception-dependencies/pom.xml +++ b/inception/inception-dependencies/pom.xml @@ -37,6 +37,14 @@ pom import
+ + + org.apache.logging.log4j + log4j-bom + ${log4j2.version} + pom + import + org.springframework @@ -390,7 +398,7 @@ org.mozilla rhino-runtime - 1.7.14 + 1.7.15 @@ -579,6 +587,20 @@ ${jena.version} pom import + + + org.junit + * + + + org.junit.jupiter + * + + + org.junitplatform + * + + org.apache.jena @@ -774,6 +796,16 @@ ${opensaml.version} pom import + + + com.fasterxml.jackson + * + + + com.fasterxml.jackson.datatype + * + + @@ -1128,6 +1160,16 @@ ${rdf4j.version} pom import + + + com.fasterxml.jackson + * + + + com.fasterxml.jackson.datatype + * + + org.eclipse.rdf4j @@ -1229,6 +1271,25 @@ + + org.opensearch + opensearch-common + ${opensearch.version} + + + org.apache.lucene + lucene-spatial + + + org.apache.lucene + lucene-spatial-extras + + + org.apache.lucene + lucene-spatial3d + + + org.opensearch opensearch-core @@ -1372,7 +1433,7 @@ org.apache.ant ant - 1.10.14 + ${ant-version} @@ -1402,28 +1463,58 @@ import - - org.apache.uima - uimafit-bom - ${uimafit.version} - pom - import - - - - org.apache.logging.log4j - log4j-bom - ${log4j2.version} - pom - import - - org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import + + + org.apache.maven.plugin-tools + * + + + org.seleniumhq.selenium + * + + + org.jetbrains.kotlinx + * + + + org.flywaydb + * + + + org.apache.logging.log4j + * + + + org.infinispan + * + + + org.junit.jupiter + * + + + org.junit.platform + * + + + org.junit.vintage + * + + + com.fasterxml.jackson.core + * + + + org.slf4j + * + + diff --git a/inception/inception-diag/pom.xml b/inception/inception-diag/pom.xml index 87419df9a5c..fe63c4eb010 100644 --- a/inception/inception-diag/pom.xml +++ b/inception/inception-diag/pom.xml @@ -56,6 +56,10 @@ de.tudarmstadt.ukp.inception.app inception-annotation-storage-api + + de.tudarmstadt.ukp.inception.app + inception-documents-api + de.tudarmstadt.ukp.inception.app inception-model @@ -64,6 +68,10 @@ de.tudarmstadt.ukp.inception.app inception-support + + de.tudarmstadt.ukp.inception.app + inception-io-xml + de.tudarmstadt.ukp.inception.app inception-api-annotation diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/CasDoctor.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/CasDoctor.java index 052fe103d66..0f9cafd6a8c 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/CasDoctor.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/CasDoctor.java @@ -34,10 +34,8 @@ import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.event.EventListener; -import de.tudarmstadt.ukp.clarin.webanno.diag.checks.Check; import de.tudarmstadt.ukp.clarin.webanno.diag.config.CasDoctorProperties; -import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.SettingsUtil; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -101,10 +99,10 @@ public boolean isFatalChecks() return fatalChecks; } - public void repair(Project aProject, CAS aCas) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas) { - List messages = new ArrayList<>(); - repair(aProject, aCas, messages); + var messages = new ArrayList(); + repair(aDocument, aDataOwner, aCas, messages); if (LOG.isWarnEnabled() && !messages.isEmpty()) { messages.forEach(s -> LOG.warn("{}", s)); } @@ -115,17 +113,18 @@ public boolean isRepairsActive() return !activeRepairs.isEmpty(); } - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { // APPLY REPAIRS - long tStart = currentTimeMillis(); + var tStart = currentTimeMillis(); for (String repairId : activeRepairs) { try { - Repair repair = repairsRegistry.getExtension(repairId).orElseThrow( + var repair = repairsRegistry.getExtension(repairId).orElseThrow( () -> new NoSuchElementException("Unknown repair [" + repairId + "]")); - long tStartTask = currentTimeMillis(); + var tStartTask = currentTimeMillis(); LOG.info("CasDoctor repair [" + repair.getId() + "] running..."); - repair.repair(aProject, aCas, aMessages); + repair.repair(aDocument, aDataOwner, aCas, aMessages); LOG.info("CasDoctor repair [" + repair.getId() + "] completed in " + (currentTimeMillis() - tStartTask) + "ms"); } @@ -140,44 +139,46 @@ public void repair(Project aProject, CAS aCas, List aMessages) // POST-CONDITION: CAS must be consistent // Ensure that the repairs actually fixed the CAS - analyze(aProject, aCas, aMessages, true); + analyze(aDocument, aDataOwner, aCas, aMessages, true); } - public boolean analyze(Project aProject, CAS aCas) throws CasDoctorException + public boolean analyze(SourceDocument aDocument, String aDataOwner, CAS aCas) + throws CasDoctorException { - List messages = new ArrayList<>(); - boolean result = analyze(aProject, aCas, messages); + var messages = new ArrayList(); + var result = analyze(aDocument, aDataOwner, aCas, messages); if (LOG.isDebugEnabled()) { messages.forEach(s -> LOG.debug("{}", s)); } return result; } - public boolean analyze(Project aProject, CAS aCas, List aMessages) + public boolean analyze(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) throws CasDoctorException { - return analyze(aProject, aCas, aMessages, isFatalChecks()); + return analyze(aDocument, aDataOwner, aCas, aMessages, isFatalChecks()); } - public boolean analyze(Project aProject, CAS aCas, List aMessages, - boolean aFatalChecks) + public boolean analyze(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages, boolean aFatalChecks) throws CasDoctorException { if (activeChecks.isEmpty()) { return true; } - long tStart = currentTimeMillis(); + var tStart = currentTimeMillis(); - boolean ok = true; - for (String checkId : activeChecks) { + var ok = true; + for (var checkId : activeChecks) { try { - Check check = checksRegistry.getExtension(checkId).orElseThrow( + var check = checksRegistry.getExtension(checkId).orElseThrow( () -> new NoSuchElementException("Unknown check [" + checkId + "]")); - long tStartTask = currentTimeMillis(); + var tStartTask = currentTimeMillis(); LOG.debug("CasDoctor analysis [" + check.getId() + "] running..."); - ok &= check.check(aProject, aCas, aMessages); + ok &= check.check(aDocument, aDataOwner, aCas, aMessages); LOG.debug("CasDoctor analysis [" + check.getId() + "] completed in " + (currentTimeMillis() - tStartTask) + "ms"); } @@ -194,7 +195,7 @@ public boolean analyze(Project aProject, CAS aCas, List aMessages, throw new CasDoctorException(aMessages); } - long duration = currentTimeMillis() - tStart; + var duration = currentTimeMillis() - tStart; LOG.debug("CasDoctor completed {} checks in {}ms", activeChecks.size(), duration); serverTiming("CasDoctor", "CasDoctor (analyze)", duration); @@ -215,7 +216,7 @@ public void setActiveRepairs(String... aActiveRepairs) public void onApplicationStartedEvent(ApplicationStartedEvent aEvent) { // When under development, automatically enable all checks. - String version = SettingsUtil.getVersionProperties().getProperty(SettingsUtil.PROP_VERSION); + var version = SettingsUtil.getVersionProperties().getProperty(SettingsUtil.PROP_VERSION); if ("unknown".equals(version) || version.contains("-SNAPSHOT") || version.contains("-beta-")) { if (disableAutoScan) { @@ -228,11 +229,11 @@ public void onApplicationStartedEvent(ApplicationStartedEvent aEvent) } } - for (String checkId : activeChecks) { + for (var checkId : activeChecks) { LOG.info("Check activated: " + checkId); } - for (String repairId : activeRepairs) { + for (var repairId : activeRepairs) { LOG.info("Repair activated: " + repairId); } } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheck.java index c6ca92380f3..ac3180f8ed0 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheck.java @@ -30,7 +30,7 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.text.TrimUtils; @@ -46,13 +46,14 @@ public AllAnnotationsStartAndEndWithCharactersCheck(AnnotationSchemaService aAnn } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { if (annotationService == null) { return true; } - var allAnnoLayers = annotationService.listAnnotationLayer(aProject); + var allAnnoLayers = annotationService.listAnnotationLayer(aDocument.getProject()); if (isEmpty(allAnnoLayers)) { return true; } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheck.java index 211786c0263..34d4298ee32 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheck.java @@ -26,11 +26,9 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; -import org.apache.uima.cas.text.AnnotationFS; import org.springframework.util.CollectionUtils; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -46,20 +44,21 @@ public AllAnnotationsStartAndEndWithinSentencesCheck(AnnotationSchemaService aAn } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { if (annotationService == null) { return true; } - List allAnnoLayers = annotationService.listAnnotationLayer(aProject); + var allAnnoLayers = annotationService.listAnnotationLayer(aDocument.getProject()); allAnnoLayers.removeIf(layer -> Sentence._TypeName.equals(layer.getName())); if (CollectionUtils.isEmpty(allAnnoLayers)) { return true; } boolean ok = true; - for (AnnotationLayer layer : allAnnoLayers) { + for (var layer : allAnnoLayers) { Type type; try { type = getType(aCas, layer.getName()); @@ -75,7 +74,7 @@ public boolean check(Project aProject, CAS aCas, List aMessages) continue; } - for (AnnotationFS ann : select(aCas, type)) { + for (var ann : select(aCas, type)) { var startsOutside = aCas.select(Sentence._TypeName) .covering(ann.getBegin(), ann.getBegin()).isEmpty(); var endsOutside = aCas.select(Sentence._TypeName) diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllFeatureStructuresIndexedCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllFeatureStructuresIndexedCheck.java index ea0822da05b..a63c3a10de6 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllFeatureStructuresIndexedCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllFeatureStructuresIndexedCheck.java @@ -26,14 +26,15 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.FeatureStructure; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public class AllFeatureStructuresIndexedCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { Map nonIndexed = getNonIndexedFSesWithOwner(aCas); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/CASMetadataTypeIsPresentCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/CASMetadataTypeIsPresentCheck.java index 91b28953e8e..51f4473dfb9 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/CASMetadataTypeIsPresentCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/CASMetadataTypeIsPresentCheck.java @@ -22,7 +22,7 @@ import org.apache.uima.cas.CAS; import de.tudarmstadt.ukp.clarin.webanno.api.type.CASMetadata; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; /** @@ -33,7 +33,8 @@ public class CASMetadataTypeIsPresentCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { if (aCas.getTypeSystem().getType(CASMetadata._TypeName) == null) { aMessages.add(LogMessage.info(this, "CAS needs upgrade to support CASMetadata which is " diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/Check.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/Check.java index d6ca956ca67..e25f582230c 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/Check.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/Check.java @@ -21,14 +21,15 @@ import org.apache.uima.cas.CAS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public interface Check extends Extension { - boolean check(Project aProject, CAS aCas, List aMessages); + boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages); @Override default String getId() diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DanglingRelationsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DanglingRelationsCheck.java index 163f76959ea..81b30dbebb2 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DanglingRelationsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DanglingRelationsCheck.java @@ -21,15 +21,15 @@ import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; import static de.tudarmstadt.ukp.inception.support.logging.LogLevel.INFO; +import java.util.HashMap; import java.util.List; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -50,23 +50,27 @@ public DanglingRelationsCheck(AnnotationSchemaService aAnnotationService) } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - boolean ok = true; + var ok = true; - for (AnnotationFS fs : aCas.getAnnotationIndex()) { - Type t = fs.getType(); + var adapterCache = new HashMap(); - Feature sourceFeat = t.getFeatureByBaseName(FEAT_REL_SOURCE); - Feature targetFeat = t.getFeatureByBaseName(FEAT_REL_TARGET); + for (var fs : aCas.getAnnotationIndex()) { + var t = fs.getType(); + + var sourceFeat = t.getFeatureByBaseName(FEAT_REL_SOURCE); + var targetFeat = t.getFeatureByBaseName(FEAT_REL_TARGET); // Is this a relation? if (!(sourceFeat != null && targetFeat != null)) { continue; } - RelationAdapter relationAdapter = (RelationAdapter) annotationService - .findAdapter(aProject, fs); + var relationAdapter = adapterCache.computeIfAbsent(t, + _t -> (RelationAdapter) annotationService.findAdapter(aDocument.getProject(), + fs)); Feature relationSourceAttachFeature = null; Feature relationTargetAttachFeature = null; @@ -77,8 +81,8 @@ public boolean check(Project aProject, CAS aCas, List aMessages) .getFeatureByBaseName(relationAdapter.getAttachFeatureName()); } - FeatureStructure source = fs.getFeatureValue(sourceFeat); - FeatureStructure target = fs.getFeatureValue(targetFeat); + var source = fs.getFeatureValue(sourceFeat); + var target = fs.getFeatureValue(targetFeat); // Here we get the annotations that the relation is pointing to in the UI if (source != null && relationSourceAttachFeature != null) { @@ -89,9 +93,9 @@ public boolean check(Project aProject, CAS aCas, List aMessages) target = (AnnotationFS) target.getFeatureValue(relationTargetAttachFeature); } - // Does it have null endpoints? + // Does it have null end-points? if (source == null || target == null) { - StringBuilder message = new StringBuilder(); + var message = new StringBuilder(); message.append("Relation [" + relationAdapter.getLayer().getName() + "] with id [" + ICasUtil.getAddr(fs) + "] has loose ends."); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DocumentTextStartsWithBomCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DocumentTextStartsWithBomCheck.java index a01b346f854..88a59bba897 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DocumentTextStartsWithBomCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/DocumentTextStartsWithBomCheck.java @@ -21,14 +21,15 @@ import org.apache.uima.cas.CAS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public class DocumentTextStartsWithBomCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { // BOM for UTF-16BE: FE FF // BOM for UTF-16LE: FF FE diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/FeatureAttachedSpanAnnotationsTrulyAttachedCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/FeatureAttachedSpanAnnotationsTrulyAttachedCheck.java index 2f7aff12fe3..0a74438230b 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/FeatureAttachedSpanAnnotationsTrulyAttachedCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/FeatureAttachedSpanAnnotationsTrulyAttachedCheck.java @@ -28,7 +28,7 @@ import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -48,11 +48,12 @@ public FeatureAttachedSpanAnnotationsTrulyAttachedCheck( } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - boolean ok = true; - int count = 0; - for (var layer : annotationService.listAnnotationLayer(aProject)) { + var ok = true; + var count = 0; + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!(SpanLayerSupport.TYPE.equals(layer.getType()) && layer.getAttachFeature() != null)) { continue; diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/LinksReachableThroughChainsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/LinksReachableThroughChainsCheck.java index 742e78cc274..3483236188c 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/LinksReachableThroughChainsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/LinksReachableThroughChainsCheck.java @@ -24,13 +24,11 @@ import java.util.List; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.FSUtil; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -47,10 +45,11 @@ public LinksReachableThroughChainsCheck(AnnotationSchemaService aAnnotationServi } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { boolean ok = true; - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!WebAnnoConst.CHAIN_TYPE.equals(layer.getType())) { continue; } @@ -72,8 +71,8 @@ public boolean check(Project aProject, CAS aCas, List aMessages) var chains = aCas.select(chainType).asList(); var links = new ArrayList<>(select(aCas, linkType)); - for (FeatureStructure chain : chains) { - AnnotationFS link = FSUtil.getFeature(chain, "first", AnnotationFS.class); + for (var chain : chains) { + var link = FSUtil.getFeature(chain, "first", AnnotationFS.class); while (link != null) { links.remove(link); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheck.java index 0974c11dd00..5506e93acbd 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheck.java @@ -24,14 +24,15 @@ import org.apache.uima.cas.CAS; import org.apache.uima.jcas.tcas.Annotation; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public class NegativeSizeAnnotationsCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { boolean ok = true; diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheck.java index 0ebe7fc9c95..bcb00af9780 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheck.java @@ -34,8 +34,7 @@ import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -52,19 +51,20 @@ public NoMultipleIncomingRelationsCheck(AnnotationSchemaService aAnnotationServi } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { if (annotationService == null) { return true; } - List allAnnoLayers = annotationService.listAnnotationLayer(aProject); + var allAnnoLayers = annotationService.listAnnotationLayer(aDocument.getProject()); if (CollectionUtils.isEmpty(allAnnoLayers)) { return true; } boolean ok = true; - for (AnnotationLayer layer : allAnnoLayers) { + for (var layer : allAnnoLayers) { if (!RELATION_TYPE.equals(layer.getType())) { continue; @@ -89,14 +89,13 @@ public boolean check(Project aProject, CAS aCas, List aMessages) // to provide a better debugging output. Map incoming = new HashMap<>(); - for (AnnotationFS rel : select(aCas, type)) { + for (var rel : select(aCas, type)) { - AnnotationFS source = getFeature(rel, FEAT_REL_SOURCE, AnnotationFS.class); - AnnotationFS target = getFeature(rel, FEAT_REL_TARGET, AnnotationFS.class); + var source = getFeature(rel, FEAT_REL_SOURCE, AnnotationFS.class); + var target = getFeature(rel, FEAT_REL_TARGET, AnnotationFS.class); - AnnotationFS existingSource = incoming.get(target); + var existingSource = incoming.get(target); if (existingSource != null) { - // Debug output should include sentence number to make the orientation // easier Optional sentenceNumber = Optional.empty(); @@ -118,7 +117,6 @@ public boolean check(Project aProject, CAS aCas, List aMessages) target.getCoveredText())); } else { - aMessages.add(LogMessage.warn(this, "Relation [%s] -> [%s] points to span that already has an " + "incoming relation [%s] -> [%s].", diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoZeroSizeTokensAndSentencesCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoZeroSizeTokensAndSentencesCheck.java index 28cc1d80ed3..104054ccc02 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoZeroSizeTokensAndSentencesCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoZeroSizeTokensAndSentencesCheck.java @@ -25,7 +25,7 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.text.AnnotationFS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -33,7 +33,8 @@ public class NoZeroSizeTokensAndSentencesCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { boolean ok = true; diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/RelationOffsetsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/RelationOffsetsCheck.java index b0cef8c9b29..d94c2e47dd5 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/RelationOffsetsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/RelationOffsetsCheck.java @@ -28,8 +28,7 @@ import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -51,11 +50,12 @@ public RelationOffsetsCheck(AnnotationSchemaService aAnnotationService) } @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { boolean ok = true; - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!RELATION_TYPE.equals(layer.getType())) { continue; } @@ -70,9 +70,8 @@ public boolean check(Project aProject, CAS aCas, List aMessages) continue; } - for (AnnotationFS rel : select(aCas, type)) { - AnnotationFS target = getFeature(rel, WebAnnoConst.FEAT_REL_TARGET, - AnnotationFS.class); + for (var rel : select(aCas, type)) { + var target = getFeature(rel, WebAnnoConst.FEAT_REL_TARGET, AnnotationFS.class); if ((rel.getBegin() != target.getBegin()) || (rel.getEnd() != target.getEnd())) { aMessages.add(new LogMessage(this, LogLevel.ERROR, "Relation offsets [%d,%d] to not match target offsets [%d,%d]", diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheck.java index 0134f4c8877..86082fbbe4b 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheck.java @@ -25,7 +25,7 @@ import org.apache.uima.cas.CAS; import org.apache.uima.jcas.tcas.Annotation; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -34,7 +34,8 @@ public class TokensAndSententencedDoNotOverlapCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { return checkTokens(aCas, aMessages) && checkSentences(aCas, aMessages); } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UniqueDocumentAnnotationCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UniqueDocumentAnnotationCheck.java index df6bbcd118a..be98577b75e 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UniqueDocumentAnnotationCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UniqueDocumentAnnotationCheck.java @@ -22,7 +22,7 @@ import org.apache.uima.cas.CAS; import org.apache.uima.jcas.tcas.DocumentAnnotation; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; /** @@ -32,7 +32,8 @@ public class UniqueDocumentAnnotationCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { if (aCas.select(DocumentAnnotation.class).count() > 1) { aMessages.add(LogMessage.error(this, "There is more than one document annotation!")); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UnreachableAnnotationsCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UnreachableAnnotationsCheck.java index 4ed9b0785d7..e8e599b4fb2 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UnreachableAnnotationsCheck.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/UnreachableAnnotationsCheck.java @@ -34,7 +34,7 @@ import org.apache.uima.cas.impl.CASImpl; import org.apache.uima.resource.ResourceInitializationException; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; @@ -42,7 +42,8 @@ public class UnreachableAnnotationsCheck implements Check { @Override - public boolean check(Project aProject, CAS aCas, List aMessages) + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { var casImpl = (CASImpl) getRealCas(aCas); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/XmlStructurePresentInCurationCasCheck.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/XmlStructurePresentInCurationCasCheck.java new file mode 100644 index 00000000000..0e6663327ab --- /dev/null +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/XmlStructurePresentInCurationCasCheck.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.diag.checks; + +import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.CURATION_USER; +import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; + +import java.io.IOException; +import java.util.List; + +import org.apache.uima.cas.CAS; +import org.dkpro.core.api.xml.type.XmlDocument; + +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.inception.documents.api.DocumentService; +import de.tudarmstadt.ukp.inception.support.logging.LogMessage; + +public class XmlStructurePresentInCurationCasCheck + implements Check +{ + private final DocumentService documentService; + + public XmlStructurePresentInCurationCasCheck(DocumentService aDocumentService) + { + documentService = aDocumentService; + } + + @Override + public boolean check(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) + { + if (!CURATION_USER.equals(aDataOwner)) { + // We want to check only the curation CAS + return true; + } + + if (!aCas.select(XmlDocument.class).isEmpty()) { + // Document structure already exists + return true; + } + + try { + var initialCas = documentService.createOrReadInitialCas(aDocument); + + if (initialCas.select(XmlDocument.class).isEmpty()) { + // Initial CAS also does not contain a document structure, so we are good + return true; + } + + // If we get here, the curation CAS does not contain a document structure + // but the initial CAS did, so the structure is missing from the curation CAS. + aMessages.add(LogMessage.error(this, + "XML document structure that is present in the initial CAS has not been " + + "copied over to the curation CAS")); + return false; + } + catch (IOException e) { + aMessages.add(LogMessage.error(this, "Unable to obtain initial CAS: %s", + getRootCauseMessage(e))); + return false; + } + } +} diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/config/CasDoctorAutoConfiguration.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/config/CasDoctorAutoConfiguration.java index 3b4a4aab827..eae0e1f07ea 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/config/CasDoctorAutoConfiguration.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/config/CasDoctorAutoConfiguration.java @@ -46,6 +46,7 @@ import de.tudarmstadt.ukp.clarin.webanno.diag.checks.TokensAndSententencedDoNotOverlapCheck; import de.tudarmstadt.ukp.clarin.webanno.diag.checks.UniqueDocumentAnnotationCheck; import de.tudarmstadt.ukp.clarin.webanno.diag.checks.UnreachableAnnotationsCheck; +import de.tudarmstadt.ukp.clarin.webanno.diag.checks.XmlStructurePresentInCurationCasCheck; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.CoverAllTextInSentencesRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.ReattachFeatureAttachedSpanAnnotationsRepair; @@ -57,9 +58,11 @@ import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.RemoveDanglingRelationsRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.RemoveZeroSizeTokensAndSentencesRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair; +import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.ReplaceXmlStructureInCurationCasRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.SwitchBeginAndEndOnNegativeSizedAnnotationsRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.TrimAnnotationsRepair; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.UpgradeCasRepair; +import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @Configuration @@ -265,4 +268,18 @@ public RemoveBomRepair removeBomRepair() { return new RemoveBomRepair(); } + + @Bean + public XmlStructurePresentInCurationCasCheck xmlStructurePresentInCurationCasCheck( + DocumentService aDocumentService) + { + return new XmlStructurePresentInCurationCasCheck(aDocumentService); + } + + @Bean + public ReplaceXmlStructureInCurationCasRepair replaceXmlStructureInCurationCasRepair( + DocumentService aDocumentService) + { + return new ReplaceXmlStructureInCurationCasRepair(aDocumentService); + } } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepair.java index 52dd95269e7..3e50859593e 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepair.java @@ -25,7 +25,7 @@ import org.apache.uima.cas.Type; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -34,7 +34,8 @@ public class CoverAllTextInSentencesRepair implements Repair { @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { int prevSentenceEnd = 0; for (Sentence sentence : aCas.select(Sentence.class)) { diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair.java index 649839b68bf..d950e01c250 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair.java @@ -27,12 +27,10 @@ import java.util.List; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -50,35 +48,36 @@ public ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair( } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!(SpanLayerSupport.TYPE.equals(layer.getType()) && layer.getAttachFeature() != null)) { continue; } - Type attachType = getType(aCas, layer.getAttachType().getName()); - String attachFeature = layer.getAttachFeature().getName(); + var attachType = getType(aCas, layer.getAttachType().getName()); + var attachFeature = layer.getAttachFeature().getName(); - int count = 0; + var count = 0; // Go over the layer that has an attach feature (e.g. Token) and make sure that it is // filled // anno -> e.g. Lemma // attach -> e.g. Token - for (AnnotationFS anno : select(aCas, getType(aCas, layer.getName()))) { + for (var anno : select(aCas, getType(aCas, layer.getName()))) { // Here we fetch all annotations of the layer we attach to at the relevant position, // e.g. Token - List attachables = selectCovered(attachType, anno); + var attachables = selectCovered(attachType, anno); if (attachables.size() > 1) { aMessages.add(LogMessage.error(this, "There is more than one attachable annotation for [%s] on layer [%s].", layer.getName(), attachType.getName())); } - for (AnnotationFS attach : attachables) { - AnnotationFS existing = getFeature(attach, attachFeature, AnnotationFS.class); + for (var attach : attachables) { + var existing = getFeature(attach, attachFeature, AnnotationFS.class); // So there is an annotation to which we could attach and it does not yet have // an annotation attached, so we attach to it. @@ -102,17 +101,15 @@ public void repair(Project aProject, CAS aCas, List aMessages) // // attach -> e.g. Token // candidates -> e.g. Lemma - List toDelete = new ArrayList<>(); - for (AnnotationFS attach : select(aCas, attachType)) { - List candidates = selectCovered(getType(aCas, layer.getName()), - attach); + var toDelete = new ArrayList(); + for (var attach : select(aCas, attachType)) { + var candidates = selectCovered(getType(aCas, layer.getName()), attach); if (!candidates.isEmpty()) { // One of the candidates should already be attached - AnnotationFS attachedCandidate = getFeature(attach, attachFeature, - AnnotationFS.class); + var attachedCandidate = getFeature(attach, attachFeature, AnnotationFS.class); - for (AnnotationFS candidate : candidates) { + for (var candidate : candidates) { if (!candidate.equals(attachedCandidate)) { toDelete.add(candidate); } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsRepair.java index 36a3177a0bb..af594ae9235 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReattachFeatureAttachedSpanAnnotationsRepair.java @@ -26,12 +26,10 @@ import java.util.List; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -48,30 +46,31 @@ public ReattachFeatureAttachedSpanAnnotationsRepair(AnnotationSchemaService aAnn } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!(SpanLayerSupport.TYPE.equals(layer.getType()) && layer.getAttachFeature() != null)) { continue; } - Type attachType = getType(aCas, layer.getAttachType().getName()); - String attachFeature = layer.getAttachFeature().getName(); + var attachType = getType(aCas, layer.getAttachType().getName()); + var attachFeature = layer.getAttachFeature().getName(); - int count = 0; - int nonNullCount = 0; + var count = 0; + var nonNullCount = 0; // Go over the layer that has an attach feature (e.g. Token) and make sure that it is // filled // anno -> e.g. Lemma // attach -> e.g. Token // Here we iterate over the attached layer, e.g. Lemma - for (AnnotationFS anno : select(aCas, getType(aCas, layer.getName()))) { + for (var anno : select(aCas, getType(aCas, layer.getName()))) { // Here we fetch all annotations of the layer we attach to at the relevant position, // e.g. Token - for (AnnotationFS attach : selectCovered(attachType, anno)) { - AnnotationFS existing = getFeature(attach, attachFeature, AnnotationFS.class); + for (var attach : selectCovered(attachType, anno)) { + var existing = getFeature(attach, attachFeature, AnnotationFS.class); if (existing == null) { setFeature(attach, layer.getAttachFeature().getName(), anno); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReindexFeatureAttachedSpanAnnotationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReindexFeatureAttachedSpanAnnotationsRepair.java index 000869bc800..82adf868c52 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReindexFeatureAttachedSpanAnnotationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReindexFeatureAttachedSpanAnnotationsRepair.java @@ -30,8 +30,7 @@ import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -53,25 +52,25 @@ public ReindexFeatureAttachedSpanAnnotationsRepair(AnnotationSchemaService aAnno } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { Map nonIndexed = getNonIndexedFSesWithOwner(aCas); - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!(SpanLayerSupport.TYPE.equals(layer.getType()) && layer.getAttachFeature() != null)) { continue; } - int count = 0; + var count = 0; // Go over the layer that has an attach feature (e.g. Token) and make sure that it is // filled // attach -> e.g. Token // anno -> e.g. Lemma - for (AnnotationFS attach : select(aCas, - getType(aCas, layer.getAttachType().getName()))) { - AnnotationFS anno = getFeature(attach, layer.getAttachFeature().getName(), + for (var attach : select(aCas, getType(aCas, layer.getAttachType().getName()))) { + var anno = getFeature(attach, layer.getAttachFeature().getName(), AnnotationFS.class); if (anno != null && nonIndexed.containsKey(anno)) { aCas.addFsToIndexes(anno); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RelationOffsetsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RelationOffsetsRepair.java index 57a2ac6b2c7..655eea59db9 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RelationOffsetsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RelationOffsetsRepair.java @@ -30,8 +30,7 @@ import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -54,10 +53,11 @@ public RelationOffsetsRepair(AnnotationSchemaService aAnnotationService) } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - List fixedRels = new ArrayList<>(); - for (AnnotationLayer layer : annotationService.listAnnotationLayer(aProject)) { + var fixedRels = new ArrayList(); + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!WebAnnoConst.RELATION_TYPE.equals(layer.getType())) { continue; } @@ -72,9 +72,8 @@ public void repair(Project aProject, CAS aCas, List aMessages) continue; } - for (AnnotationFS rel : select(aCas, type)) { - AnnotationFS target = getFeature(rel, WebAnnoConst.FEAT_REL_TARGET, - AnnotationFS.class); + for (var rel : select(aCas, type)) { + var target = getFeature(rel, WebAnnoConst.FEAT_REL_TARGET, AnnotationFS.class); if ((rel.getBegin() != target.getBegin()) || (rel.getEnd() != target.getEnd())) { fixedRels.add(rel); setFeature(rel, CAS.FEATURE_BASE_NAME_BEGIN, target.getBegin()); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepair.java index 6ef8630ebdd..db84b4518b6 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepair.java @@ -27,7 +27,7 @@ import org.apache.uima.jcas.tcas.Annotation; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @Safe(false) @@ -35,7 +35,8 @@ public class RemoveBomRepair implements Repair { @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { var cas = getRealCas(aCas); var text = cas.getDocumentText(); diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingChainLinksRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingChainLinksRepair.java index 9dc5599ca25..10c4b616ada 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingChainLinksRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingChainLinksRepair.java @@ -28,7 +28,7 @@ import org.apache.uima.jcas.tcas.Annotation; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.logging.LogLevel; @@ -46,9 +46,10 @@ public RemoveDanglingChainLinksRepair(AnnotationSchemaService aAnnotationService } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - for (var layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { if (!WebAnnoConst.CHAIN_TYPE.equals(layer.getType())) { continue; } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingFeatureAttachedSpanAnnotationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingFeatureAttachedSpanAnnotationsRepair.java index 611d5cad021..3ab8aa0c5aa 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingFeatureAttachedSpanAnnotationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingFeatureAttachedSpanAnnotationsRepair.java @@ -30,7 +30,7 @@ import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -48,11 +48,12 @@ public RemoveDanglingFeatureAttachedSpanAnnotationsRepair( } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { var nonIndexed = getNonIndexedFSesWithOwner(aCas); - for (var layer : annotationService.listAnnotationLayer(aProject)) { + for (var layer : annotationService.listAnnotationLayer(aDocument.getProject())) { var count = 0; if (!(SpanLayerSupport.TYPE.equals(layer.getType()) diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepair.java index 2783d1cbeb6..7ce1d292074 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepair.java @@ -30,7 +30,7 @@ import org.apache.uima.cas.text.AnnotationFS; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; @@ -58,7 +58,8 @@ public RemoveDanglingRelationsRepair(AnnotationSchemaService aAnnotationService) } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { var nonIndexed = getNonIndexedFSes(aCas); @@ -72,7 +73,7 @@ public void repair(Project aProject, CAS aCas, List aMessages) TypeAdapter adapter = null; try { adapter = adapterCache.computeIfAbsent(t.getName(), - $ -> annotationService.findAdapter(aProject, fs)); + $ -> annotationService.findAdapter(aDocument.getProject(), fs)); } catch (NoResultException e) { // Ignore diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveZeroSizeTokensAndSentencesRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveZeroSizeTokensAndSentencesRepair.java index a4d92709874..cdac9859516 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveZeroSizeTokensAndSentencesRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveZeroSizeTokensAndSentencesRepair.java @@ -27,7 +27,7 @@ import org.apache.uima.fit.util.FSUtil; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; @@ -36,7 +36,8 @@ public class RemoveZeroSizeTokensAndSentencesRepair implements Repair { @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { for (AnnotationFS s : selectSentences(aCas)) { if (s.getBegin() >= s.getEnd()) { diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/Repair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/Repair.java index 1e635fdce46..16ac753a8b8 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/Repair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/Repair.java @@ -25,14 +25,14 @@ import org.apache.uima.cas.CAS; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public interface Repair extends Extension { - void repair(Project aProject, CAS aCas, List aMessages); + void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, List aMessages); @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReplaceXmlStructureInCurationCasRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReplaceXmlStructureInCurationCasRepair.java new file mode 100644 index 00000000000..9280495f090 --- /dev/null +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/ReplaceXmlStructureInCurationCasRepair.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.diag.repairs; + +import static de.tudarmstadt.ukp.inception.io.xml.dkprocore.XmlNodeUtils.removeXmlDocumentStructure; +import static de.tudarmstadt.ukp.inception.io.xml.dkprocore.XmlNodeUtils.transferXmlDocumentStructure; +import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.CURATION_USER; +import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; + +import java.io.IOException; +import java.util.List; + +import org.apache.uima.cas.CAS; +import org.dkpro.core.api.xml.type.XmlDocument; + +import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.inception.documents.api.DocumentService; +import de.tudarmstadt.ukp.inception.support.logging.LogMessage; + +@Safe(false) +public class ReplaceXmlStructureInCurationCasRepair + implements Repair +{ + private final DocumentService documentService; + + public ReplaceXmlStructureInCurationCasRepair(DocumentService aDocumentService) + { + documentService = aDocumentService; + } + + @Override + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) + { + if (!CURATION_USER.equals(aDataOwner)) { + // We want to repair only the curation CAS + return; + } + + try { + var initialCas = documentService.createOrReadInitialCas(aDocument); + + if (initialCas.select(XmlDocument.class).isEmpty()) { + // Initial CAS also does not contain a document structure, so we are good + return; + } + + var deleted = removeXmlDocumentStructure(aCas); + var added = transferXmlDocumentStructure(aCas, initialCas); + + var operation = "replaced"; + if (deleted == 0) { + operation = "added"; + } + + aMessages.add(LogMessage.error(this, + "XML document structure has been %s using the structure from the initial CAS (nodes: %d removed, %d added)", + operation, deleted, added)); + } + catch (IOException e) { + aMessages.add(LogMessage.error(this, "Unable to obtain initial CAS: %s", + getRootCauseMessage(e))); + } + } +} diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepair.java index 16d29fcc338..b5305798601 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepair.java @@ -25,7 +25,7 @@ import org.apache.uima.jcas.tcas.Annotation; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @Safe(false) @@ -33,7 +33,8 @@ public class SwitchBeginAndEndOnNegativeSizedAnnotationsRepair implements Repair { @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { for (Annotation ann : aCas.select(Annotation.class)) { if (ann.getBegin() > ann.getEnd()) { diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/TrimAnnotationsRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/TrimAnnotationsRepair.java index 32fe9568c80..50997b01e7a 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/TrimAnnotationsRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/TrimAnnotationsRepair.java @@ -31,7 +31,7 @@ import org.apache.uima.cas.Type; import org.apache.uima.jcas.tcas.Annotation; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.text.TrimUtils; @@ -47,9 +47,10 @@ public TrimAnnotationsRepair(AnnotationSchemaService aAnnotationService) } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { - var allAnnoLayers = annotationService.listAnnotationLayer(aProject); + var allAnnoLayers = annotationService.listAnnotationLayer(aDocument.getProject()); if (isEmpty(allAnnoLayers)) { return; } diff --git a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/UpgradeCasRepair.java b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/UpgradeCasRepair.java index 0fb9004ec13..1e9e64d51dc 100644 --- a/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/UpgradeCasRepair.java +++ b/inception/inception-diag/src/main/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/UpgradeCasRepair.java @@ -33,7 +33,7 @@ import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair.Safe; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; import de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil; @@ -56,7 +56,8 @@ public UpgradeCasRepair(AnnotationSchemaService aAnnotationService) } @Override - public void repair(Project aProject, CAS aCas, List aMessages) + public void repair(SourceDocument aDocument, String aDataOwner, CAS aCas, + List aMessages) { try { var casImpl = (CASImpl) getRealCas(aCas); @@ -65,7 +66,7 @@ public void repair(Project aProject, CAS aCas, List aMessages) var bytesBefore = size(serializeCASComplete(casImpl)); int bytesAfter; - annotationService.upgradeCas(aCas, aProject); + annotationService.upgradeCas(aCas, aDocument.getProject()); aMessages.add(LogMessage.info(this, "CAS upgraded.")); var annotationCountsAfter = countFeatureStructures(casImpl); diff --git a/inception/inception-diag/src/main/resources/META-INF/asciidoc/user-guide/casdoctor.adoc b/inception/inception-diag/src/main/resources/META-INF/asciidoc/user-guide/casdoctor.adoc index 7e3e40cb096..ea194b541b4 100644 --- a/inception/inception-diag/src/main/resources/META-INF/asciidoc/user-guide/casdoctor.adoc +++ b/inception/inception-diag/src/main/resources/META-INF/asciidoc/user-guide/casdoctor.adoc @@ -224,21 +224,29 @@ Removing them is harmless and reduces memory and disk space usage. [[check_AllAnnotationsStartAndEndWithCharactersCheck]] === All annotations start and end with characters [horizontal] -ID:: `check_AllAnnotationsStartAndEndWithCharactersCheck` +ID:: `AllAnnotationsStartAndEndWithCharactersCheck` Related repairs:: <> -Checks if all annotations start and end with a character (i.e. not a whitespace). Annotations that start or end with a -whitespace character can cause problems during rendering. Trimming whitespace at the begin and end is typically as -harmless procedure. +Checks if all annotations start and end with a character (i.e. not a whitespace). Annotations that start or end with a whitespace character can cause problems during rendering. +Trimming whitespace at the begin and end is typically as harmless procedure. [[check_DocumentTextStartsWithBomCheck]] === Document text starts with Byte Order Mark [horizontal] -ID:: `check_DocumentTextStartsWithBomCheck` +ID:: `DocumentTextStartsWithBomCheck` Related repairs:: <> Checks if the document text starts with a Byte Order Mark (BOM). +[[check_XmlStructurePresentInCurationCasCheck]] +=== XML structure is present in curation CAS +[horizontal] +ID:: `XmlStructurePresentInCurationCasCheck` +Related repairs:: <> + +Checks if an XML structure that may have been extracted from the source document is present in the curation CAS. +If it is not present, this check will fail. + [[sect_repairs]] == Repairs @@ -251,9 +259,8 @@ ID:: `ReattachFeatureAttachedSpanAnnotationsRepair` This repair action attempts to attach spans that should be attached to another span, but are not. E.g. it tries to set the `pos` feature of tokens to the POS annotation for that respective token. -The action is not performed if there are multiple stacked annotations to choose from. Stacked -attached annotations would be an indication of a bug because attached layers are not allowed to -stack. +The action is not performed if there are multiple stacked annotations to choose from. +Stacked attached annotations would be an indication of a bug because attached layers are not allowed to stack. This is a safe repair action as it does not delete anything. @@ -265,10 +272,10 @@ This is a safe repair action as it does not delete anything. ID:: `ReattachFeatureAttachedSpanAnnotationsAndDeleteExtrasRepair` This is a destructive variant of <>. In -addition to re-attaching unattached annotations, it also removes all extra candidates that cannot -be attached. For example, if there are two unattached Lemma annotations at the position of a Token -annotation, then one will be attached and the other will be deleted. Which one is attached and -which one is deleted is undefined. +addition to re-attaching unattached annotations, it also removes all extra candidates that cannot be attached. +For example, if there are two unattached Lemma annotations at the position of a Token +annotation, then one will be attached and the other will be deleted. +Which one is attached and which one is deleted is undefined. [[repair_ReindexFeatureAttachedSpanAnnotationsRepair]] @@ -277,8 +284,8 @@ which one is deleted is undefined. [horizontal] ID:: `ReindexFeatureAttachedSpanAnnotationsRepair` -This repair locates annotations that are reachable via a attach feature but which are not actually -indexed in the CAS. Such annotations are then added back to the CAS indexes. +This repair locates annotations that are reachable via a attach feature but which are not actually indexed in the CAS. +Such annotations are then added back to the CAS indexes. This is a safe repair action as it does not delete anything. @@ -289,9 +296,8 @@ This is a safe repair action as it does not delete anything. [horizontal] ID:: `RelationOffsetsRepair` -Fixes that the offsets of relations match the target of the relation. This mirrors the DKPro -Core convention that the offsets of a dependency relation must match the offsets of the -dependent. +Fixes that the offsets of relations match the target of the relation. +This mirrors the DKPro Core convention that the offsets of a dependency relation must match the offsets of the dependent. [[repair_RemoveDanglingChainLinksRepair]] @@ -408,3 +414,11 @@ NOTE: Run the checks again after applying this repair as certain annotations can ID:: `RemoveBomRepair` This repair removes the Byte Order Mark at the start of the document and adjusts all annotation offsets accordingly. + +[[repair_ReplaceXmlStructureInCurationCasRepair]] +=== Relace XML structure in the curation CAS + +[horizontal] +ID:: `ReplaceXmlStructureInCurationCasRepair` + +This repair ensures the XML document structure that may have been extracted from the source document is also present in the curation CAS. Any potentially existing XML document structure int he curation CAS will be removed and replaced with the structure from the source document. diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsIndexedCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsIndexedCheckTest.java index d4f44b531c6..067f777bb4b 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsIndexedCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsIndexedCheckTest.java @@ -79,7 +79,7 @@ public void testFail() throws Exception .toArray(String[]::new)); // A project is not required for this check - var result = cd.analyze(null, cas, messages); + var result = cd.analyze(null, null, cas, messages); messages.forEach($ -> LOG.debug("{}", $)); @@ -126,7 +126,7 @@ public void testOK() throws Exception .toArray(String[]::new)); // A project is not required for this check - var result = cd.analyze(null, cas, messages); + var result = cd.analyze(null, null, cas, messages); messages.forEach($ -> LOG.debug("{}", $)); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheckTest.java index 1bb1f383a82..e82a52cfa0c 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithCharactersCheckTest.java @@ -39,6 +39,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -60,13 +61,18 @@ static class Config AllAnnotationsStartAndEndWithCharactersCheck sut; Project project; + SourceDocument document; + String dataOwner; JCas jCas; List layers; @BeforeEach void setup() throws Exception { - project = new Project(); + project = Project.builder().build(); + document = SourceDocument.builder() // + .withProject(project) // + .build(); jCas = JCasFactory.createJCas(); var namedEntityLayer = new AnnotationLayer(); @@ -88,7 +94,7 @@ void test() var messages = new ArrayList(); - var result = sut.check(project, jCas.getCas(), messages); + var result = sut.check(document, dataOwner, jCas.getCas(), messages); assertThat(result).isFalse(); assertThat(messages).hasSize(1); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheckTest.java index 9446139fc17..3ffb33afb7b 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/AllAnnotationsStartAndEndWithinSentencesCheckTest.java @@ -38,6 +38,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -59,16 +60,21 @@ static class Config AllAnnotationsStartAndEndWithinSentencesCheck sut; Project project; + SourceDocument document; + String dataOwner; JCas jCas; List layers; @BeforeEach void setup() throws Exception { - project = new Project(); + project = Project.builder().build(); + document = SourceDocument.builder() // + .withProject(project) // + .build(); jCas = JCasFactory.createJCas(); - AnnotationLayer namedEntityLayer = new AnnotationLayer(); + var namedEntityLayer = new AnnotationLayer(); namedEntityLayer.setName(NamedEntity._TypeName); layers = asList(namedEntityLayer); } @@ -88,7 +94,7 @@ void test() var messages = new ArrayList(); - var result = sut.check(project, jCas.getCas(), messages); + var result = sut.check(document, dataOwner, jCas.getCas(), messages); assertThat(result).isFalse(); assertThat(messages).hasSize(1); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheckTest.java index 2a582df5794..d7f06c9f435 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NegativeSizeAnnotationsCheckTest.java @@ -28,20 +28,21 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; class NegativeSizeAnnotationsCheckTest { NegativeSizeAnnotationsCheck sut; - Project project; + SourceDocument document; + String dataOwner; JCas jCas; @BeforeEach void setup() throws Exception { sut = new NegativeSizeAnnotationsCheck(); - project = new Project(); + document = SourceDocument.builder().build(); jCas = JCasFactory.createJCas(); } @@ -56,7 +57,7 @@ void test() var messages = new ArrayList(); - var result = sut.check(project, jCas.getCas(), messages); + var result = sut.check(document, dataOwner, jCas.getCas(), messages); assertThat(result).isFalse(); assertThat(messages).hasSize(1); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheckTest.java index c9ed2d9ef9f..ef337efa2a4 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/NoMultipleIncomingRelationsCheckTest.java @@ -17,16 +17,15 @@ */ package de.tudarmstadt.ukp.clarin.webanno.diag.checks; +import static java.util.Arrays.asList; +import static org.apache.uima.fit.factory.JCasFactory.createJCas; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.apache.uima.fit.factory.JCasFactory; -import org.apache.uima.jcas.JCas; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -37,11 +36,14 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.coref.type.CoreferenceChain; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; +import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @ExtendWith(SpringExtension.class) @@ -59,42 +61,54 @@ static class Config @Autowired NoMultipleIncomingRelationsCheck sut; + Project project; + SourceDocument document; + String dataOwner; + + @BeforeEach + void setup() throws Exception + { + project = Project.builder().build(); + document = SourceDocument.builder() // + .withProject(project) // + .build(); + } + @Test public void testFail() throws Exception { - AnnotationLayer relationLayer = new AnnotationLayer(); + var relationLayer = new AnnotationLayer(); relationLayer.setName(Dependency.class.getName()); - relationLayer.setType(WebAnnoConst.RELATION_TYPE); - when(annotationService.listAnnotationLayer(Mockito.isNull())) - .thenReturn(Arrays.asList(relationLayer)); + relationLayer.setType(RelationLayerSupport.TYPE); + when(annotationService.listAnnotationLayer(project)).thenReturn(asList(relationLayer)); - JCas jcas = JCasFactory.createJCas(); + var jcas = createJCas(); jcas.setDocumentText("This is a test."); - Token spanThis = new Token(jcas, 0, 4); + var spanThis = new Token(jcas, 0, 4); spanThis.addToIndexes(); - Token spanIs = new Token(jcas, 5, 7); + var spanIs = new Token(jcas, 5, 7); spanIs.addToIndexes(); - Token spanA = new Token(jcas, 8, 9); + var spanA = new Token(jcas, 8, 9); spanA.addToIndexes(); - Dependency dep1 = new Dependency(jcas, 0, 7); + var dep1 = new Dependency(jcas, 0, 7); dep1.setGovernor(spanThis); dep1.setDependent(spanIs); dep1.addToIndexes(); - Dependency dep2 = new Dependency(jcas, 0, 9); + var dep2 = new Dependency(jcas, 0, 9); dep2.setGovernor(spanA); dep2.setDependent(spanIs); dep2.addToIndexes(); - List messages = new ArrayList<>(); + var messages = new ArrayList(); - boolean result = sut.check(null, jcas.getCas(), messages); + var result = sut.check(document, dataOwner, jcas.getCas(), messages); messages.forEach(System.out::println); @@ -111,39 +125,38 @@ public void testFail() throws Exception @Test public void testOK() throws Exception { - AnnotationLayer relationLayer = new AnnotationLayer(); + var relationLayer = new AnnotationLayer(); relationLayer.setName(Dependency.class.getName()); + relationLayer.setType(RelationLayerSupport.TYPE); + when(annotationService.listAnnotationLayer(Mockito.isNull())) + .thenReturn(asList(relationLayer)); - relationLayer.setType(WebAnnoConst.RELATION_TYPE); - Mockito.when(annotationService.listAnnotationLayer(Mockito.isNull())) - .thenReturn(Arrays.asList(relationLayer)); - - JCas jcas = JCasFactory.createJCas(); + var jcas = createJCas(); jcas.setDocumentText("This is a test."); - Token spanThis = new Token(jcas, 0, 4); + var spanThis = new Token(jcas, 0, 4); spanThis.addToIndexes(); - Token spanIs = new Token(jcas, 6, 8); + var spanIs = new Token(jcas, 6, 8); spanIs.addToIndexes(); - Token spanA = new Token(jcas, 9, 10); + var spanA = new Token(jcas, 9, 10); spanA.addToIndexes(); - Dependency dep1 = new Dependency(jcas, 0, 8); + var dep1 = new Dependency(jcas, 0, 8); dep1.setGovernor(spanThis); dep1.setDependent(spanIs); dep1.addToIndexes(); - Dependency dep2 = new Dependency(jcas, 6, 10); + var dep2 = new Dependency(jcas, 6, 10); dep2.setGovernor(spanIs); dep2.setDependent(spanA); dep2.addToIndexes(); - List messages = new ArrayList<>(); + var messages = new ArrayList(); - boolean result = sut.check(null, jcas.getCas(), messages); + var result = sut.check(document, dataOwner, jcas.getCas(), messages); messages.forEach(System.out::println); @@ -154,39 +167,38 @@ public void testOK() throws Exception public void testOkBecauseCoref() throws Exception { - AnnotationLayer relationLayer = new AnnotationLayer(); + var relationLayer = new AnnotationLayer(); relationLayer.setName(CoreferenceChain.class.getName()); + relationLayer.setType(ChainLayerSupport.TYPE); + when(annotationService.listAnnotationLayer(Mockito.isNull())) + .thenReturn(asList(relationLayer)); - relationLayer.setType(WebAnnoConst.CHAIN_TYPE); - Mockito.when(annotationService.listAnnotationLayer(Mockito.isNull())) - .thenReturn(Arrays.asList(relationLayer)); - - JCas jcas = JCasFactory.createJCas(); + var jcas = createJCas(); jcas.setDocumentText("This is a test."); - Token spanThis = new Token(jcas, 0, 4); + var spanThis = new Token(jcas, 0, 4); spanThis.addToIndexes(); - Token spanIs = new Token(jcas, 6, 8); + var spanIs = new Token(jcas, 6, 8); spanIs.addToIndexes(); - Token spanA = new Token(jcas, 9, 10); + var spanA = new Token(jcas, 9, 10); spanA.addToIndexes(); - Dependency dep1 = new Dependency(jcas, 0, 8); + var dep1 = new Dependency(jcas, 0, 8); dep1.setGovernor(spanThis); dep1.setDependent(spanIs); dep1.addToIndexes(); - Dependency dep2 = new Dependency(jcas, 0, 10); + var dep2 = new Dependency(jcas, 0, 10); dep2.setGovernor(spanA); dep2.setDependent(spanIs); dep2.addToIndexes(); - List messages = new ArrayList<>(); + var messages = new ArrayList(); - boolean result = sut.check(null, jcas.getCas(), messages); + var result = sut.check(document, dataOwner, jcas.getCas(), messages); messages.forEach(System.out::println); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheckTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheckTest.java index 14a74c93d3f..9e35f9360b6 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheckTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/checks/TokensAndSententencedDoNotOverlapCheckTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -36,14 +36,15 @@ class TokensAndSententencedDoNotOverlapCheckTest { TokensAndSententencedDoNotOverlapCheck sut; - Project project; + SourceDocument document; + String dataOwner; JCas jCas; @BeforeEach void setup() throws Exception { sut = new TokensAndSententencedDoNotOverlapCheck(); - project = new Project(); + document = SourceDocument.builder().build(); jCas = JCasFactory.createJCas(); } @@ -60,7 +61,7 @@ void thatOverlappingSentencesFailCheck() var messages = new ArrayList(); - var result = sut.check(project, jCas.getCas(), messages); + var result = sut.check(document, dataOwner, jCas.getCas(), messages); assertThat(result).isFalse(); assertThat(messages).hasSize(1); @@ -81,7 +82,7 @@ void thatOverlappingTokensFailCheck() var messages = new ArrayList(); - var result = sut.check(project, jCas.getCas(), messages); + var result = sut.check(document, dataOwner, jCas.getCas(), messages); assertThat(result).isFalse(); assertThat(messages).hasSize(1); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepairTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepairTest.java index 365b627bb6c..2352c0ea4ae 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepairTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/CoverAllTextInSentencesRepairTest.java @@ -29,21 +29,22 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; class CoverAllTextInSentencesRepairTest { CoverAllTextInSentencesRepair sut; - Project project; + SourceDocument document; + String dataOwner; JCas jCas; @BeforeEach void setup() throws Exception { sut = new CoverAllTextInSentencesRepair(); - project = new Project(); + document = SourceDocument.builder().build(); jCas = JCasFactory.createJCas(); } @@ -59,7 +60,7 @@ void thatNewSentenceIsAddedOnText() var messages = new ArrayList(); - sut.repair(project, jCas.getCas(), messages); + sut.repair(document, dataOwner, jCas.getCas(), messages); assertThat(jCas.select(Sentence.class).asList()) // .extracting(Annotation::getBegin, Annotation::getEnd) @@ -81,7 +82,7 @@ void thatNoNewSentenceIsAddedOnBlank() var messages = new ArrayList(); - sut.repair(project, jCas.getCas(), messages); + sut.repair(document, dataOwner, jCas.getCas(), messages); assertThat(jCas.select(Sentence.class).asList()) // .extracting(Annotation::getBegin, Annotation::getEnd) @@ -101,7 +102,7 @@ void thatNewSentenceIsAddedAtDocumentStart() var messages = new ArrayList(); - sut.repair(project, jCas.getCas(), messages); + sut.repair(document, dataOwner, jCas.getCas(), messages); assertThat(jCas.select(Sentence.class).asList()) // .extracting(Annotation::getBegin, Annotation::getEnd) @@ -122,7 +123,7 @@ void thatNewSentenceIsAddedAtDocumentEnd() var messages = new ArrayList(); - sut.repair(project, jCas.getCas(), messages); + sut.repair(document, dataOwner, jCas.getCas(), messages); assertThat(jCas.select(Sentence.class).asList()) // .extracting(Annotation::getBegin, Annotation::getEnd) diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepairTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepairTest.java index 4d4aab9d4df..c6e2207b29d 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepairTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveBomRepairTest.java @@ -59,7 +59,7 @@ void test() throws Exception } var messages = new ArrayList(); - sut.repair(null, cas, messages); + sut.repair(null, null, cas, messages); assertThat(annotations).hasSizeGreaterThan(annotationCount); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepairTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepairTest.java index 2ad73144b15..798e5ca1203 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepairTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/RemoveDanglingRelationsRepairTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import org.apache.uima.fit.factory.JCasFactory; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -37,6 +38,8 @@ import de.tudarmstadt.ukp.clarin.webanno.diag.ChecksRegistryImpl; import de.tudarmstadt.ukp.clarin.webanno.diag.RepairsRegistryImpl; import de.tudarmstadt.ukp.clarin.webanno.diag.checks.AllFeatureStructuresIndexedCheck; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; @@ -49,6 +52,19 @@ public class RemoveDanglingRelationsRepairTest private @Mock ConstraintsService constraintsService; private @Mock AnnotationSchemaService schemaService; + Project project; + SourceDocument document; + String dataOwner; + + @BeforeEach + void setup() throws Exception + { + project = Project.builder().build(); + document = SourceDocument.builder() // + .withProject(project) // + .build(); + } + @Test public void test() throws Exception { @@ -84,10 +100,10 @@ public void test() throws Exception .toArray(String[]::new)); // A project is not required for this check - var result = cd.analyze(null, jcas.getCas(), messages); + var result = cd.analyze(null, null, jcas.getCas(), messages); // A project is not required for this repair - cd.repair(null, jcas.getCas(), messages); + cd.repair(document, dataOwner, jcas.getCas(), messages); assertFalse(result); diff --git a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepairTest.java b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepairTest.java index 20d34658544..a7319fc3a69 100644 --- a/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepairTest.java +++ b/inception/inception-diag/src/test/java/de/tudarmstadt/ukp/clarin/webanno/diag/repairs/SwitchBeginAndEndOnNegativeSizedAnnotationsRepairTest.java @@ -29,21 +29,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; class SwitchBeginAndEndOnNegativeSizedAnnotationsRepairTest { SwitchBeginAndEndOnNegativeSizedAnnotationsRepair sut; - Project project; JCas jCas; @BeforeEach void setup() throws Exception { sut = new SwitchBeginAndEndOnNegativeSizedAnnotationsRepair(); - project = new Project(); jCas = JCasFactory.createJCas(); } @@ -58,7 +55,7 @@ void test() var messages = new ArrayList(); - sut.repair(project, jCas.getCas(), messages); + sut.repair(null, null, jCas.getCas(), messages); assertThat(jCas.select(Token.class).asList()) // .extracting(Annotation::getBegin, Annotation::getEnd) diff --git a/inception/inception-diam-editor/src/main/java/de/tudarmstadt/ukp/inception/diam/sidebar/DiamAnnotationBrowser.java b/inception/inception-diam-editor/src/main/java/de/tudarmstadt/ukp/inception/diam/sidebar/DiamAnnotationBrowser.java index e0a17a0ad30..9ea8f11be42 100644 --- a/inception/inception-diam-editor/src/main/java/de/tudarmstadt/ukp/inception/diam/sidebar/DiamAnnotationBrowser.java +++ b/inception/inception-diam-editor/src/main/java/de/tudarmstadt/ukp/inception/diam/sidebar/DiamAnnotationBrowser.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.inception.diam.sidebar; +import static de.tudarmstadt.ukp.clarin.webanno.security.WicketSecurityUtils.getCsrfTokenFromSession; import static de.tudarmstadt.ukp.inception.diam.sidebar.preferences.DiamSidebarManagerPrefs.KEY_DIAM_SIDEBAR_MANAGER_PREFS; import static de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig.WS_ENDPOINT; import static java.lang.String.format; @@ -93,6 +94,7 @@ protected void onConfigure() Map properties = Map.of( // "ajaxEndpointUrl", diamBehavior.getCallbackUrl(), // "wsEndpointUrl", constructEndpointUrl(), // + "csrfToken", getCsrfTokenFromSession(), // "topicChannel", viewport.getTopic(), // "pinnedGroups", managerPrefs.getPinnedGroups(), // "userPreferencesKey", userPreferencesKey); diff --git a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLabelList.svelte b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLabelList.svelte index fffece6ffd6..28f3a5f59c5 100644 --- a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLabelList.svelte +++ b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLabelList.svelte @@ -100,6 +100,11 @@ } function scrollTo(ann: Annotation) { + if (ann instanceof Span) { + ajaxClient.scrollTo({ id: ann.vid, offset: ann.offsets[0] }); + return; + } + ajaxClient.scrollTo({ id: ann.vid }); } @@ -200,50 +205,51 @@ + {groupedAnnotations[group.label].length} {group.label || "No label"}
    {#if groupedAnnotations[group.label]} - {#each groupedAnnotations[group.label] as ann} - -
  • mouseOverAnnotation(ev, ann)} - on:mouseout={ev => mouseOutAnnotation(ev, ann)} - > -
    +
  • mouseOverAnnotation(ev, ann)} + on:mouseout={ev => mouseOutAnnotation(ev, ann)} > - {#if ann instanceof Span} -
    - {:else if ann instanceof Relation} -
    - {/if} -
    - -
    scrollTo(ann)} - > -
    - +
    + {#if ann instanceof Span} +
    + {:else if ann instanceof Relation} +
    + {/if}
    + +
    scrollTo(ann)} + > +
    + +
    - {#if ann instanceof Span} - - {:else if ann instanceof Relation} - - {/if} -
    -
  • - {/each} + {#if ann instanceof Span} + + {:else if ann instanceof Relation} + + {/if} + + + {/each} {:else}
  • No occurrences diff --git a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLayerList.svelte b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLayerList.svelte index 8578b7c88af..eaedeb2d1be 100644 --- a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLayerList.svelte +++ b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByLayerList.svelte @@ -102,8 +102,8 @@ } } - function scrollTo(ann: Annotation) { - ajaxClient.scrollTo({ id: ann.vid }); + function scrollToSpan(span: Span) { + ajaxClient.scrollTo({ id: span.vid, offset: span.offsets[0] }); } function mouseOverAnnotation(event: MouseEvent, annotation: Annotation) { @@ -204,105 +204,106 @@ + {groupedAnnotations[group.layer.name].length} {group.layer.name} {group.layer.type}
      {#if groupedAnnotations[group.layer.name]} - {#each groupedAnnotations[group.layer.name] as ann} - - {#if ann instanceof Span} -
    • mouseOverAnnotation(ev, ann)} - on:mouseout={ev => mouseOutAnnotation(ev, ann)} - > - - -
      scrollTo(ann)}> -
      - + {#each groupedAnnotations[group.layer.name] as ann} + + {#if ann instanceof Span} +
    • mouseOverAnnotation(ev, ann)} + on:mouseout={ev => mouseOutAnnotation(ev, ann)} + > + + +
      scrollToSpan(ann)}> +
      + +
      +
      - - -
    • + - {@const relations = groupedRelations[`${ann.vid}`]} - {#if relations} - {#each relations as relation} - {@const target = relation.arguments[1].target} - -
    • - mouseOverAnnotation(ev, relation)} - on:mouseout={(ev) => - mouseOutAnnotation(ev, relation)} - > -
      +
    • + mouseOverAnnotation(ev, relation)} + on:mouseout={(ev) => + mouseOutAnnotation(ev, relation)} > - - - - -
      scrollTo(target)} - > -
      - +
      + +
      + + +
      scrollToSpan(target)} + > +
      + +
      + +
      - - +
    • + {/each} + {/if} + {:else if ann instanceof Relation && group.layer.type === "relation"} +
    • mouseOverAnnotation(ev, ann)} + on:mouseout={ev => mouseOutAnnotation(ev, ann)} + > + + +
      +
      +
      -
    • - {/each} - {/if} - {:else if ann instanceof Relation && group.layer.type === "relation"} -
    • mouseOverAnnotation(ev, ann)} - on:mouseout={ev => mouseOutAnnotation(ev, ann)} - > - - -
      -
      - -
      -
      scrollTo(ann.arguments[0].target)}> -
      - +
      scrollToSpan(ann.arguments[0].target)}> +
      + +
      +
      - -
      -
      scrollTo(ann.arguments[1].target)}> -
      - +
      scrollToSpan(ann.arguments[1].target)}> +
      + +
      +
      -
      -
      -
    • - {/if} - {/each} + + {/if} + {/each} {:else}
    • No occurrences diff --git a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByPositionList.svelte b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByPositionList.svelte index 4dbd02f19e1..48cd0897c4c 100644 --- a/inception/inception-diam-editor/src/main/ts/src/AnnotationsByPositionList.svelte +++ b/inception/inception-diam-editor/src/main/ts/src/AnnotationsByPositionList.svelte @@ -59,7 +59,7 @@ } function scrollToRelation(relation: Relation) { - ajaxClient.scrollTo({ id: relation.vid }); + ajaxClient.scrollTo({ id: relation.vid, offset: relation.arguments[0].target.offsets[0] }); } function mouseOverAnnotation(event: MouseEvent, annotation: Annotation) { @@ -103,7 +103,7 @@ class="flex-grow-1 my-1 mx-2 overflow-hidden" on:click={() => scrollToSpan(firstSpan)} > -
      +
      {#each spans as span} @@ -144,7 +144,7 @@ class="flex-grow-1 my-1 mx-2 overflow-hidden" on:click={() => scrollToRelation(relation)} > -
      +
      text.length || rawTextAfter.length > textAfter.length) { + textAfter = ' ' + textAfter + } + textAfter = textAfter.trimEnd() + } + else { + textAfter = '' + } + } {#if text.length === 0} @@ -40,10 +57,7 @@ {text.substring(0, 50)} {:else} - {text} - {#if textAfter.length > 0} - {textAfter} - {/if} + {text}{#if textAfter.length > 0}{textAfter}{/if} {/if} diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/DiamAjaxBehavior.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/DiamAjaxBehavior.java index 5aaa872f7bd..e83558a2563 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/DiamAjaxBehavior.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/DiamAjaxBehavior.java @@ -21,24 +21,22 @@ import java.util.ArrayList; import java.util.List; -import org.apache.wicket.Component; -import org.apache.wicket.MarkupContainer; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; -import org.apache.wicket.util.visit.IVisit; -import org.apache.wicket.util.visit.IVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.inception.diam.editor.actions.EditorAjaxRequestHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.EditorAjaxRequestHandlerExtensionPoint; +import de.tudarmstadt.ukp.inception.editor.ContextMenuLookup; import de.tudarmstadt.ukp.inception.support.http.ServerTimingWatch; import de.tudarmstadt.ukp.inception.support.wicket.ContextMenu; public class DiamAjaxBehavior extends AbstractDefaultAjaxBehavior + implements ContextMenuLookup { private static final long serialVersionUID = -7681019566646236763L; @@ -83,8 +81,6 @@ protected void onBind() @Override protected void respond(AjaxRequestTarget aTarget) { - LOG.trace("AJAX request received"); - var request = RequestCycle.get().getRequest(); var priorityHandler = priorityHandlers.stream() // @@ -104,6 +100,7 @@ protected void respond(AjaxRequestTarget aTarget) private void call(AjaxRequestTarget aTarget, EditorAjaxRequestHandler aHandler) { + LOG.trace("AJAX request received for {}", aHandler.getClass().getName()); var request = RequestCycle.get().getRequest(); try (var watch = new ServerTimingWatch("diam", "diam (" + aHandler.getCommand() + ")")) { aHandler.handle(this, aTarget, request); @@ -111,25 +108,7 @@ private void call(AjaxRequestTarget aTarget, EditorAjaxRequestHandler aHandler) } } - public ContextMenu findContextMenu() - { - var component = getComponent(); - if (component instanceof MarkupContainer container) { - final Component[] result = new Component[1]; // Array to hold the result - container.visitChildren(ContextMenu.class, new IVisitor() - { - @Override - public void component(Component aComponent, IVisit visit) - { - result[0] = aComponent; - visit.stop(); - } - }); - return (ContextMenu) result[0]; - } - return null; - } - + @Override public ContextMenu getContextMenu() { return contextMenu; diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/CreateRelationAnnotationHandler.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/CreateRelationAnnotationHandler.java index 4f6589fafe4..f2ed7cb434b 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/CreateRelationAnnotationHandler.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/CreateRelationAnnotationHandler.java @@ -18,8 +18,10 @@ package de.tudarmstadt.ukp.inception.diam.editor.actions; import static de.tudarmstadt.ukp.inception.support.uima.ICasUtil.selectAnnotationByAddr; +import static java.util.Arrays.asList; import java.io.IOException; +import java.util.List; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.feedback.IFeedback; @@ -31,12 +33,15 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.NotEditableException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainAdapter; import de.tudarmstadt.ukp.inception.annotation.layer.relation.CreateRelationAnnotationRequest; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.diam.editor.DiamAjaxBehavior; import de.tudarmstadt.ukp.inception.diam.editor.config.DiamAutoConfig; import de.tudarmstadt.ukp.inception.diam.model.ajax.DefaultAjaxResponse; +import de.tudarmstadt.ukp.inception.editor.ContextMenuLookup; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.selection.Selection; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; @@ -57,6 +62,9 @@ public class CreateRelationAnnotationHandler extends EditorAjaxRequestHandlerBase { + private static final List SEGMENTATION_TYPES = asList(Sentence._TypeName, + Token._TypeName); + public static final String COMMAND = "arcOpenDialog"; private final AnnotationSchemaService schemaService; @@ -102,7 +110,7 @@ private void actionArc(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget, actionArc(aBehavior, aTarget, originSpan, targetSpan, clientX, clientY); } - public void actionArc(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget, VID originSpan, + public void actionArc(ContextMenuLookup aBehavior, AjaxRequestTarget aTarget, VID originSpan, VID targetSpan, int aClientX, int aClientY) throws NotEditableException, IOException, AnnotationException, IllegalPlacementException { @@ -155,7 +163,7 @@ private void createChainLink(ChainAdapter chainAdapter, AjaxRequestTarget aTarge } - private void createRelationAnnotation(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget, + private void createRelationAnnotation(ContextMenuLookup aBehavior, AjaxRequestTarget aTarget, AnnotationLayer originLayer, VID aOriginSpan, VID aTargetSpan, int aClientX, int aClientY) throws AnnotationException, IllegalPlacementException, IOException @@ -201,6 +209,11 @@ private void createRelationAnnotation(AjaxRequestTarget aTarget, AnnotationLayer var originFs = selectAnnotationByAddr(cas, origin.getId()); var targetFs = selectAnnotationByAddr(cas, target.getId()); + if (SEGMENTATION_TYPES.contains(originFs.getType().getName()) + || SEGMENTATION_TYPES.contains(targetFs.getType().getName())) { + throw new IllegalPlacementException("Cannot create relations on segmentation units."); + } + if (!schemaProperties.isCrossLayerRelationsEnabled() && !originFs.getType().equals(targetFs.getType())) { throw new IllegalPlacementException( diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/EditorAjaxRequestHandler.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/EditorAjaxRequestHandler.java index 3822ddfdebe..84d443915e3 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/EditorAjaxRequestHandler.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/EditorAjaxRequestHandler.java @@ -30,6 +30,7 @@ public interface EditorAjaxRequestHandler { int PRIO_CONTEXT_MENU = -10; int PRIO_RENDER_HANDLER = 0; + int PRIO_NAVIGATE_HANDLER = 50; int PRIO_SLOT_FILLER_HANDLER = 100; int PRIO_UNARM_SLOT_HANDLER = 180; int PRIO_EXTENSION_HANDLER = 190; diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/LinkToContextMenuItem.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/LinkToContextMenuItem.java new file mode 100644 index 00000000000..62efd6c0d24 --- /dev/null +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/LinkToContextMenuItem.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.diam.editor.actions; + +import static de.tudarmstadt.ukp.inception.support.spring.ApplicationContextProvider.getApplicationContext; + +import java.io.IOException; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.wicketstuff.jquery.ui.widget.menu.IMenuItem; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemExtension; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaMenuItem; + +public class LinkToContextMenuItem + implements ContextMenuItemExtension +{ + @Override + public boolean accepts(AnnotationPageBase aPage) + { + var state = aPage.getModelObject(); + + return state.getSelection().isSpan(); + } + + @Override + public IMenuItem createMenuItem(VID aVid, int aClientX, int aClientY) + { + return new LambdaMenuItem("Link to ...", $ -> actionLinkTo($, aVid, aClientX, aClientY)); + } + + /* + * This is a static method because it needs to be serializable! + */ + private static void actionLinkTo(AjaxRequestTarget aTarget, VID paramId, int aClientX, + int aClientY) + throws IOException, AnnotationException + { + var page = (AnnotationPageBase) aTarget.getPage(); + + var maybeContextMenuLookup = page.getContextMenuLookup(); + if (!maybeContextMenuLookup.isPresent()) { + return; + } + + page.ensureIsEditable(); + + var state = page.getModelObject(); + + if (!state.getSelection().isSpan()) { + return; + } + + // Need to fetch this here since the handler is not serializable + var createRelationAnnotationHandler = getApplicationContext() + .getBean(CreateRelationAnnotationHandler.class); + + createRelationAnnotationHandler.actionArc(maybeContextMenuLookup.get(), aTarget, + state.getSelection().getAnnotation(), paramId, aClientX, aClientY); + } +} diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ScrollToHandler.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ScrollToHandler.java index b6eac6cfd9f..122b5dfb236 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ScrollToHandler.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ScrollToHandler.java @@ -17,7 +17,9 @@ */ package de.tudarmstadt.ukp.inception.diam.editor.actions; -import static de.tudarmstadt.ukp.inception.diam.editor.actions.CreateSpanAnnotationHandler.getRangeFromRequest; +import static de.tudarmstadt.ukp.inception.rendering.model.Range.rangeClippedToDocument; + +import java.io.IOException; import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -29,15 +31,18 @@ import de.tudarmstadt.ukp.inception.diam.editor.DiamAjaxBehavior; import de.tudarmstadt.ukp.inception.diam.editor.config.DiamAutoConfig; import de.tudarmstadt.ukp.inception.diam.model.ajax.DefaultAjaxResponse; +import de.tudarmstadt.ukp.inception.diam.model.compact.CompactRangeList; import de.tudarmstadt.ukp.inception.rendering.model.Range; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; +import de.tudarmstadt.ukp.inception.support.json.JSONUtil; /** *

      * This class is exposed as a Spring Component via {@link DiamAutoConfig#scrollToHandler}. *

      */ -@Order(EditorAjaxRequestHandler.PRIO_ANNOTATION_HANDLER) +@Order(EditorAjaxRequestHandler.PRIO_NAVIGATE_HANDLER) public class ScrollToHandler extends EditorAjaxRequestHandlerBase { @@ -54,24 +59,12 @@ public DefaultAjaxResponse handle(DiamAjaxBehavior aBehavior, AjaxRequestTarget Request aRequest) { try { - AnnotationPageBase page = getPage(); - CAS cas = page.getEditorCas(); - - IRequestParameters requestParameters = aRequest.getRequestParameters(); + var page = getPage(); + var cas = page.getEditorCas(); - if (!requestParameters.getParameterValue(PARAM_OFFSETS).isEmpty()) { - Range offsets = getRangeFromRequest(getAnnotatorState(), requestParameters, cas); - page.getAnnotationActionHandler().actionJump(aTarget, offsets.getBegin(), - offsets.getEnd()); - } - else { - VID vid = VID.parseOptional( - requestParameters.getParameterValue(PARAM_ID).toOptionalString()); + var requestParameters = aRequest.getRequestParameters(); - if (vid.isSet() && !vid.isSynthetic()) { - page.getAnnotationActionHandler().actionJump(aTarget, vid); - } - } + scrollTo(aTarget, page, cas, requestParameters); return new DefaultAjaxResponse(getAction(aRequest)); } @@ -79,4 +72,41 @@ public DefaultAjaxResponse handle(DiamAjaxBehavior aBehavior, AjaxRequestTarget return handleError("Unable to scroll to annotation", e); } } + + private void scrollTo(AjaxRequestTarget aTarget, AnnotationPageBase page, CAS cas, + IRequestParameters requestParameters) + throws IOException, AnnotationException + { + var vid = VID + .parseOptional(requestParameters.getParameterValue(PARAM_ID).toOptionalString()); + + if (vid.isSet() && !vid.isSynthetic()) { + page.getAnnotationActionHandler().actionJump(aTarget, vid); + return; + } + + if (!requestParameters.getParameterValue(PARAM_OFFSETS).isEmpty()) { + var offsets = getRangeFromRequest(requestParameters, cas); + page.getAnnotationActionHandler().actionJump(aTarget, offsets.getBegin(), + offsets.getEnd()); + return; + } + } + + /** + * Extract offset information from the current request. These are either offsets of an existing + * selected annotations or offsets contained in the request for the creation of a new + * annotation. + */ + private Range getRangeFromRequest(IRequestParameters request, CAS aCas) throws IOException + { + var offsets = request.getParameterValue(PARAM_OFFSETS).toString(); + + var offsetLists = JSONUtil.fromJsonString(CompactRangeList.class, offsets); + + var begin = offsetLists.get(0).getBegin(); + var end = offsetLists.get(offsetLists.size() - 1).getEnd(); + + return rangeClippedToDocument(aCas, begin, end); + } } diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ShowContextMenuHandler.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ShowContextMenuHandler.java index 93c0446fea2..91ffc5dadb3 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ShowContextMenuHandler.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/actions/ShowContextMenuHandler.java @@ -17,22 +17,16 @@ */ package de.tudarmstadt.ukp.inception.diam.editor.actions; -import static de.tudarmstadt.ukp.inception.support.spring.ApplicationContextProvider.getApplicationContext; - -import java.io.IOException; import java.io.Serializable; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.request.Request; import org.springframework.core.annotation.Order; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemRegistry; import de.tudarmstadt.ukp.inception.diam.editor.DiamAjaxBehavior; import de.tudarmstadt.ukp.inception.diam.model.ajax.AjaxResponse; import de.tudarmstadt.ukp.inception.diam.model.ajax.DefaultAjaxResponse; -import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtensionRegistry; -import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; -import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; -import de.tudarmstadt.ukp.inception.support.lambda.LambdaMenuItem; @Order(EditorAjaxRequestHandler.PRIO_CONTEXT_MENU) public class ShowContextMenuHandler @@ -41,6 +35,13 @@ public class ShowContextMenuHandler { private static final long serialVersionUID = 2566256640285857435L; + private final ContextMenuItemRegistry contextMenuItemRegistry; + + public ShowContextMenuHandler(ContextMenuItemRegistry aContextMenuItemRegistry) + { + contextMenuItemRegistry = aContextMenuItemRegistry; + } + @Override public String getCommand() { @@ -67,25 +68,16 @@ public AjaxResponse handle(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget var clientX = cm.getClientX().getAsInt(); var clientY = cm.getClientY().getAsInt(); - // Need to fetch this here since the handler is not serializable - var extensionRegistry = getApplicationContext() - .getBean(AnnotationEditorExtensionRegistry.class); - try { - var items = cm.getItemList(); items.clear(); - var state = getAnnotatorState(); + var vid = getVid(aRequest); - if (state.getSelection().isSpan()) { - var vid = getVid(aRequest); - items.add(new LambdaMenuItem("Link to ...", - $ -> actionLinkTo(aBehavior, $, vid, clientX, clientY))); + for (var ext : contextMenuItemRegistry.getExtensions(getPage())) { + items.add(ext.createMenuItem(vid, clientX, clientY)); } - extensionRegistry.generateContextMenuItems(items); - if (!items.isEmpty()) { cm.onOpen(aTarget); } @@ -96,25 +88,4 @@ public AjaxResponse handle(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget return new DefaultAjaxResponse(getAction(aRequest)); } - - private void actionLinkTo(DiamAjaxBehavior aBehavior, AjaxRequestTarget aTarget, VID paramId, - int aClientX, int aClientY) - throws IOException, AnnotationException - { - var page = getPage(); - page.ensureIsEditable(); - - var state = getAnnotatorState(); - - if (!state.getSelection().isSpan()) { - return; - } - - // Need to fetch this here since the handler is not serializable - var createRelationAnnotationHandler = getApplicationContext() - .getBean(CreateRelationAnnotationHandler.class); - - createRelationAnnotationHandler.actionArc(aBehavior, aTarget, - state.getSelection().getAnnotation(), paramId, aClientX, aClientY); - } } diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/config/DiamAutoConfig.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/config/DiamAutoConfig.java index 4a72c4c0ebf..ecdc8a66e44 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/config/DiamAutoConfig.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/config/DiamAutoConfig.java @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Lazy; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.annotation.menu.ContextMenuItemRegistry; import de.tudarmstadt.ukp.inception.diam.editor.actions.CreateRelationAnnotationHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.CreateSpanAnnotationHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.DeleteAnnotationHandler; @@ -36,6 +37,7 @@ import de.tudarmstadt.ukp.inception.diam.editor.actions.FillSlotWithNewAnnotationHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.ImplicitUnarmSlotHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.LazyDetailsHandler; +import de.tudarmstadt.ukp.inception.diam.editor.actions.LinkToContextMenuItem; import de.tudarmstadt.ukp.inception.diam.editor.actions.LoadAnnotationsHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.LoadPreferences; import de.tudarmstadt.ukp.inception.diam.editor.actions.MoveSpanAnnotationHandler; @@ -109,9 +111,10 @@ public CreateRelationAnnotationHandler createRelationAnnotationHandler( } @Bean - public ShowContextMenuHandler ShowContextMenuHandler() + public ShowContextMenuHandler showContextMenuHandler( + ContextMenuItemRegistry aContextMenuItemRegistry) { - return new ShowContextMenuHandler(); + return new ShowContextMenuHandler(aContextMenuItemRegistry); } @Bean @@ -186,4 +189,10 @@ public SavePreferences savePreferences(UserDao aUserService, return new SavePreferences(aUserService, aPreferencesService, aClientSiderUserPreferencesProviderRegistry); } + + @Bean + public LinkToContextMenuItem linkToContextMenuItem() + { + return new LinkToContextMenuItem(); + } } diff --git a/inception/inception-diam/src/main/ts/src/diam/DiamAjaxImpl.ts b/inception/inception-diam/src/main/ts/src/diam/DiamAjaxImpl.ts index 9d9d6b3765f..509c7458cb3 100644 --- a/inception/inception-diam/src/main/ts/src/diam/DiamAjaxImpl.ts +++ b/inception/inception-diam/src/main/ts/src/diam/DiamAjaxImpl.ts @@ -16,7 +16,7 @@ * limitations under the License. */ import { Annotation, DiamAjax, Offsets, VID, LazyDetailGroup } from '@inception-project/inception-js-api' -import { DiamLoadAnnotationsOptions, DiamSelectAnnotationOptions } from '@inception-project/inception-js-api/src/diam/DiamAjax' +import { DiamAjaxConnectOptions, DiamLoadAnnotationsOptions, DiamSelectAnnotationOptions } from '@inception-project/inception-js-api/src/diam/DiamAjax' declare const Wicket: any @@ -31,9 +31,16 @@ const TRANSPORT_BUFFER: any = (document as any).DIAM_TRANSPORT_BUFFER export class DiamAjaxImpl implements DiamAjax { private ajaxEndpoint: string + private csrfToken: string - constructor (ajaxEndpoint: string) { - this.ajaxEndpoint = ajaxEndpoint + constructor (options: string | DiamAjaxConnectOptions) { + if (options instanceof String || typeof options === 'string') { + this.ajaxEndpoint = options as string + } + else { + this.ajaxEndpoint = options.url + this.csrfToken = options.csrfToken + } } /** @@ -76,7 +83,15 @@ export class DiamAjaxImpl implements DiamAjax { }) } - scrollTo (args: { id?: VID, offset?: Offsets }): void { + scrollTo (args: { id?: VID, offset?: Offsets, offsets?: Array }): void { + let effectiveOffsets: Array | undefined + if (args.offset) { + effectiveOffsets = [args.offset] + } + else { + effectiveOffsets = args.offsets + } + DiamAjaxImpl.performAjaxCall(() => { Wicket.Ajax.ajax({ m: 'POST', @@ -84,7 +99,7 @@ export class DiamAjaxImpl implements DiamAjax { ep: { action: 'scrollTo', id: args.id, - offset: args.offset + offsets: JSON.stringify(effectiveOffsets) } }) }) diff --git a/inception/inception-diam/src/main/ts/src/diam/DiamClientFactoryImpl.ts b/inception/inception-diam/src/main/ts/src/diam/DiamClientFactoryImpl.ts index b4eb091cce5..011e2707d06 100644 --- a/inception/inception-diam/src/main/ts/src/diam/DiamClientFactoryImpl.ts +++ b/inception/inception-diam/src/main/ts/src/diam/DiamClientFactoryImpl.ts @@ -17,14 +17,14 @@ */ import { DiamWebsocketImpl } from './DiamWebsocketImpl' import { DiamAjaxImpl } from './DiamAjaxImpl' -import { DiamClientFactory } from '@inception-project/inception-js-api' +import { DiamAjaxConnectOptions, DiamClientFactory } from '@inception-project/inception-js-api' export class DiamClientFactoryImpl implements DiamClientFactory { createWebsocketClient () : DiamWebsocketImpl { return new DiamWebsocketImpl() } - createAjaxClient (ajaxEndpoint: string) : DiamAjaxImpl { - return new DiamAjaxImpl(ajaxEndpoint) + createAjaxClient (options: string | DiamAjaxConnectOptions) : DiamAjaxImpl { + return new DiamAjaxImpl(options) } } diff --git a/inception/inception-diam/src/main/ts/src/diam/DiamWebsocketImpl.ts b/inception/inception-diam/src/main/ts/src/diam/DiamWebsocketImpl.ts index 1a25ede260c..60e6049b08b 100644 --- a/inception/inception-diam/src/main/ts/src/diam/DiamWebsocketImpl.ts +++ b/inception/inception-diam/src/main/ts/src/diam/DiamWebsocketImpl.ts @@ -16,7 +16,7 @@ * limitations under the License. */ import { Client, Stomp, StompSubscription, IFrame, frameCallbackType } from '@stomp/stompjs' -import { DiamWebsocket } from '@inception-project/inception-js-api' +import { DiamWebsocket, DiamWebsocketConnectOptions } from '@inception-project/inception-js-api' import * as jsonpatch from 'fast-json-patch' /** @@ -35,18 +35,26 @@ export class DiamWebsocketImpl implements DiamWebsocket { public onConnect: frameCallbackType - connect (aWsEndpoint: string) { + connect (options: string | DiamWebsocketConnectOptions) { if (this.stompClient) { console.log('Already connected') return } + const wsEndpoint = new URL((options instanceof String || typeof options === 'string') ? + options as string : options.url) + const protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') - const wsEndpoint = new URL(aWsEndpoint) wsEndpoint.protocol = protocol this.stompClient = Stomp.over(() => new WebSocket(wsEndpoint.toString())) this.stompClient.reconnectDelay = 5000 + + if (typeof options !== 'string' && options?.csrfToken) { + this.stompClient.connectHeaders = { + 'X-CSRF-TOKEN': options.csrfToken + } + } this.stompClient.onConnect = frame => { this.stompClient.subscribe('/user/queue/errors', this.handleProtocolError) diff --git a/inception/inception-diam/src/test/java/de/tudarmstadt/ukp/inception/diam/service/DiamWebsocketController_ViewportRoutingTest.java b/inception/inception-diam/src/test/java/de/tudarmstadt/ukp/inception/diam/service/DiamWebsocketController_ViewportRoutingTest.java index 369699bb596..8d023aa632f 100644 --- a/inception/inception-diam/src/test/java/de/tudarmstadt/ukp/inception/diam/service/DiamWebsocketController_ViewportRoutingTest.java +++ b/inception/inception-diam/src/test/java/de/tudarmstadt/ukp/inception/diam/service/DiamWebsocketController_ViewportRoutingTest.java @@ -18,6 +18,7 @@ package de.tudarmstadt.ukp.inception.diam.service; import static de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel.ANNOTATOR; +import static de.tudarmstadt.ukp.clarin.webanno.security.model.Role.ROLE_USER; import static de.tudarmstadt.ukp.inception.diam.service.DiamWebsocketController.FORMAT_LEGACY; import static de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig.WS_ENDPOINT; import static java.nio.charset.StandardCharsets.UTF_8; @@ -27,6 +28,7 @@ import static org.apache.tomcat.websocket.Constants.WS_AUTHENTICATION_PASSWORD; import static org.apache.tomcat.websocket.Constants.WS_AUTHENTICATION_USER_NAME; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import java.io.File; @@ -34,10 +36,10 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -54,21 +56,21 @@ import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.stomp.StompHeaders; import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.UserDetailsManager; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.messaging.WebSocketStompClient; @@ -80,12 +82,10 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.project.config.ProjectServiceAutoConfiguration; -import de.tudarmstadt.ukp.clarin.webanno.security.ExtensiblePermissionEvaluator; import de.tudarmstadt.ukp.clarin.webanno.security.InceptionDaoAuthenticationProvider; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.config.InceptionSecurityAutoConfiguration; import de.tudarmstadt.ukp.clarin.webanno.security.config.SecurityAutoConfiguration; -import de.tudarmstadt.ukp.clarin.webanno.security.model.Role; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.text.config.TextFormatsAutoConfiguration; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; @@ -111,7 +111,6 @@ import de.tudarmstadt.ukp.inception.support.logging.Logging; import de.tudarmstadt.ukp.inception.support.spring.ApplicationContextProvider; import de.tudarmstadt.ukp.inception.websocket.config.WebsocketAutoConfiguration; -import de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig; import de.tudarmstadt.ukp.inception.websocket.config.WebsocketSecurityConfig; import de.tudarmstadt.ukp.inception.websocket.config.stomp.LambdaStompFrameHandler; import de.tudarmstadt.ukp.inception.websocket.config.stomp.LoggingStompSessionHandlerAdapter; @@ -132,6 +131,7 @@ InceptionSecurityAutoConfiguration.class, // SecurityAutoConfiguration.class, // WebsocketAutoConfiguration.class, // + WebsocketSecurityConfig.class, // ProjectServiceAutoConfiguration.class, // DocumentServiceAutoConfiguration.class, // CasStorageServiceAutoConfiguration.class, // @@ -139,6 +139,7 @@ AnnotationSchemaServiceAutoConfiguration.class, // AnnotationAutoConfiguration.class, // TextFormatsAutoConfiguration.class, // + DocumentServiceAutoConfiguration.class, // DocumentImportExportServiceAutoConfiguration.class }) @EntityScan({ // "de.tudarmstadt.ukp.inception.preferences.model", // @@ -155,6 +156,7 @@ public class DiamWebsocketController_ViewportRoutingTest private WebSocketStompClient stompClient; private @LocalServerPort int port; private String websocketUrl; + private WebSocketHttpHeaders headers; private @Autowired DiamWebsocketController sut; @@ -164,23 +166,27 @@ public class DiamWebsocketController_ViewportRoutingTest private @Autowired EntityManager entityManager; private @Autowired UserDao userService; - // temporarily store data for test project private static @TempDir File repositoryDir; + private static User user; private static Project testProject; - private static SourceDocument testDocument; + private static SourceDocument testDoc; private static AnnotationDocument testAnnotationDocument; @BeforeEach public void setup() throws Exception { - // create websocket client websocketUrl = "ws://localhost:" + port + WS_ENDPOINT; - StandardWebSocketClient wsClient = new StandardWebSocketClient(); + var wsClient = new StandardWebSocketClient(); wsClient.setUserProperties(Map.of( // WS_AUTHENTICATION_USER_NAME, USER, // WS_AUTHENTICATION_PASSWORD, PASS)); + + headers = new WebSocketHttpHeaders(); + headers.add("Authorization", + "Basic " + Base64.getEncoder().encodeToString((USER + ":" + PASS).getBytes())); + stompClient = new WebSocketStompClient(wsClient); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); @@ -196,7 +202,7 @@ void setupOnce() throws Exception repositoryProperties.setPath(repositoryDir); MDC.put(Logging.KEY_REPOSITORY_PATH, repositoryProperties.getPath().toString()); - user = new User(USER, Role.ROLE_USER); + user = new User(USER, ROLE_USER); user.setPassword(PASS); userService.create(user); @@ -204,10 +210,10 @@ void setupOnce() throws Exception projectService.createProject(testProject); projectService.assignRole(testProject, user, ANNOTATOR); - testDocument = new SourceDocument("test", testProject, "text"); - documentService.createSourceDocument(testDocument); + testDoc = new SourceDocument("testDoc", testProject, "text"); + documentService.createSourceDocument(testDoc); - testAnnotationDocument = new AnnotationDocument(USER, testDocument); + testAnnotationDocument = new AnnotationDocument(USER, testDoc); documentService.createOrUpdateAnnotationDocument(testAnnotationDocument); try (var session = CasStorageSession.open()) { @@ -237,19 +243,18 @@ public void thatViewportBasedMessageRoutingWorks() throws Exception var sessionHandler1 = new SessionHandler(subscriptionDone, initDone, vpd1); var sessionHandler2 = new SessionHandler(subscriptionDone, initDone, vpd2); - // try { - var session1 = stompClient.connect(websocketUrl, sessionHandler1).get(1000, SECONDS); - var session2 = stompClient.connect(websocketUrl, sessionHandler2).get(1000, SECONDS); - // } - // catch (Exception e) { - // Thread.sleep(Duration.of(3, ChronoUnit.HOURS).toMillis()); - // } + var session1 = stompClient // + .connectAsync(websocketUrl, headers, sessionHandler1) // + .get(1000, SECONDS); + var session2 = stompClient // + .connectAsync(websocketUrl, headers, sessionHandler2) // + .get(1000, SECONDS); try { - subscriptionDone.await(5, TimeUnit.SECONDS); + subscriptionDone.await(5, SECONDS); assertThat(subscriptionDone.getCount()).isEqualTo(0); - initDone.await(5, TimeUnit.SECONDS); + initDone.await(5, SECONDS); assertThat(initDone.getCount()).isEqualTo(0); sut.sendUpdate(testAnnotationDocument, 12, 15); @@ -277,7 +282,7 @@ public void thatViewportBasedMessageRoutingWorks() throws Exception } } - private static class SessionHandler + private class SessionHandler extends LoggingStompSessionHandlerAdapter { private final CountDownLatch subscriptionDoneLatch; @@ -321,29 +326,26 @@ public List getRecieved() } } - @Configuration - public static class WebsocketSecurityTestConfig - extends WebsocketSecurityConfig + @SpringBootConfiguration + public static class WebsocketBrokerTestConfig { - @Autowired - public WebsocketSecurityTestConfig(ApplicationContext aContext, - ExtensiblePermissionEvaluator aPermissionEvaluator) + @Bean + public ChannelInterceptor csrfChannelInterceptor() { - super(aContext, aPermissionEvaluator); + // Disable CSRF + return new ChannelInterceptor() + { + }; } - } - @SpringBootConfiguration - public static class WebsocketBrokerTestConfig - { @Bean public ApplicationContextProvider applicationContextProvider() { return new ApplicationContextProvider(); } - @Bean(name = "authenticationProvider") - public DaoAuthenticationProvider internalAuthenticationProvider(PasswordEncoder aEncoder, + @Bean + public DaoAuthenticationProvider authenticationProvider(PasswordEncoder aEncoder, @Lazy UserDetailsManager aUserDetailsManager) { var authProvider = new InceptionDaoAuthenticationProvider(); @@ -352,6 +354,20 @@ public DaoAuthenticationProvider internalAuthenticationProvider(PasswordEncoder return authProvider; } + @Order(100) + @Bean + public SecurityFilterChain wsFilterChain(HttpSecurity aHttp) throws Exception + { + aHttp.securityMatcher(WS_ENDPOINT); + aHttp.authorizeHttpRequests(rules -> rules // + .requestMatchers("/**").authenticated() // + .anyRequest().denyAll()); + aHttp.sessionManagement(session -> session // + .sessionCreationPolicy(STATELESS)); + aHttp.httpBasic(withDefaults()); + return aHttp.build(); + } + @Primary @Bean public PreRenderer testPreRenderer() @@ -375,19 +391,5 @@ public void render(VDocument aResponse, RenderRequest aRequest) } }; } - - @Order(100) - @Bean - public SecurityFilterChain wsFilterChain(HttpSecurity aHttp) throws Exception - { - aHttp.securityMatcher(WebsocketConfig.WS_ENDPOINT); - aHttp.authorizeHttpRequests() // - .requestMatchers("/**").authenticated() // - .anyRequest().denyAll(); - aHttp.sessionManagement() // - .sessionCreationPolicy(STATELESS); - aHttp.httpBasic(); - return aHttp.build(); - } } } diff --git a/inception/inception-diam/src/test/resources/log4j2-test.xml b/inception/inception-diam/src/test/resources/log4j2-test.xml index 17ef451253c..590c931d7d2 100644 --- a/inception/inception-diam/src/test/resources/log4j2-test.xml +++ b/inception/inception-diam/src/test/resources/log4j2-test.xml @@ -14,6 +14,7 @@ + diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide.adoc index f2daf89cbbf..8dbb0ef6938 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide.adoc @@ -14,6 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +:include-dir: admin-guide/ +:imagesdir: admin-guide/ + = {product-name} Administrator Guide The {product-name} Team diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/upgrade_notes.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/upgrade_notes.adoc index 5133fa73164..51098c403c1 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/upgrade_notes.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/admin-guide/upgrade_notes.adoc @@ -22,6 +22,15 @@ NOTE: It is a good idea to back up your installation and data before an upgrade. you can actually re-create a working installation by restoring your backup. A backup which cannot be successfully restored is worthless. +== INCEpTION 34.0 + +=== Database schema changes + +This version makes various changes to the database schema in order to improve compatibiltiy with +different database systems. This entails that after running this version, you cannot easily downgrade +to an older version of {product-name} anymore. If you need to downgrade, you need to restore a +backup from before the upgrade or manually revert the database schema changes. + == INCEpTION 33.0 === MariaDB upgrade in the Docker Compose file diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/developer-guide.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/developer-guide.adoc index 375553b7a7f..a8cdb43d17e 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/developer-guide.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/developer-guide.adoc @@ -14,6 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +:include-dir: developer-guide/ +:imagesdir: developer-guide/ + = {product-name} Developer Guide The {product-name} Team diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc index 1edbda9e377..4c73c7f8f1c 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide.adoc @@ -14,6 +14,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +:include-dir: user-guide/ +:imagesdir: user-guide/ + = {product-name} User Guide The {product-name} Team @@ -129,6 +132,16 @@ include::{include-dir}projects_documents.adoc[leveloffset=+2] include::{include-dir}projects_layers.adoc[leveloffset=+2] +include::{include-dir}projects_layers_feature.adoc[leveloffset=+3] + +include::{include-dir}projects_layers_feature_string.adoc[leveloffset=+4] + +include::{include-dir}projects_layers_feature_number.adoc[leveloffset=+4] + +include::{include-dir}projects_layers_feature_boolean.adoc[leveloffset=+4] + +include::{include-dir}projects_layers_feature_link.adoc[leveloffset=+4] + include::{include-dir}projects_layers_feature_lookup.adoc[leveloffset=+4] include::{include-dir}projects_annotation.adoc[leveloffset=+2] diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/getting-started.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/getting-started.adoc index c69108cacd4..7ded11c3af5 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/getting-started.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/getting-started.adoc @@ -50,7 +50,7 @@ The following picture gives you a first impression on how annotated texts look l In this example, text xref:layers_and_features_in_getting_started[spans] have been annotated as whether they refer to a person (PER), location (LOC), organization (ORG) or any other (OTH). [.right] -image::getting_started_example_for_annotations.png[width=400] +image::images/getting_started_example_for_annotations.png[width=400] {product-name}'s key features are: First, before you annotate, you need a corpus to be annotated (*Corpus Creation*). You might have one already and import it or create it in {product-name}. @@ -125,7 +125,7 @@ After a moment, a splash screen will display. It shows that the application is loading. [.right] -image::getting_started_starting_the_jar_I.png[align="center",width=400] +image::images/getting_started_starting_the_jar_I.png[align="center",width=400] NOTE: *In case {product-name} does not start:* If double-clicking the JAR file does not start {product-name}, you might need to make the file executable first. Right-click on the JAR file and navigate through the settings and permissions. @@ -134,7 +134,7 @@ There, you can mark it as executable. Once the initialization is complete, a dialog appears. Here, you can open the application in your default browser or shut it down again: -image::getting_started_starting_the_jar_II.png[align="center"] +image::images/getting_started_starting_the_jar_II.png[align="center"] *Step 2b - Open via terminal:* If you prefer the command line, you may enter this command instead of double-clicking. Make sure that instead of "`x.xx.x`", you enter the version you downloaded: @@ -152,7 +152,7 @@ You need to enter this password into two separate fields. Only if the same password has been entered into both fields, it will be accepted and saved. After the password has been set, you will be redirected to the regular login screen where you can log in using the username **admin** and the password you have just set. -image::getting_started_set_password.png[align="center"] +image::images/getting_started_set_password.png[align="center"] *You have finished the installation.* @@ -167,7 +167,7 @@ We created several example projects for you to play with. You find them in the section https://inception-project.github.io/example-projects/[Example Projects^] on our website. [.right] -image::getting_started_download_example_project.png[width=400] +image::images/getting_started_download_example_project.png[width=400] *Step 1 - Download:* For this guide, we use the _Interactive Concept Linking_ project. @@ -188,7 +188,7 @@ Also see <> in the User Guide. {product-name}, click on the _Import project_ button on the top left (next to _Create new project_) and browse for the example project you have downloaded in Step 1. Finally, click _Import_. The project has now been added and you can use it to follow the explanations of the next section. -image::getting_started_import_project.png[align="center"] +image::images/getting_started_import_project.png[align="center"] [[sect_intro_settings]] == Project Settings @@ -216,14 +216,14 @@ There are more tabs but we focus on the most important ones to get started. You reach the settings after logging in when you click on the name of a project and then on _Settings_ on the left. If you have not imported the example project yet, we propose to follow the instruction in <> first. -image::getting_started_settings.png[align="center"] +image::images/getting_started_settings.png[align="center"] [[documents_in_getting_started]] === Documents Here, you may upload your files to be annotated. Make sure that the format selected in the dropdown on the right is the same as the one of the file to be uploaded. -image::getting_started_documents.png[align="center"] +image::images/getting_started_documents.png[align="center"] NOTE: *Formats:* For details on the different formats {product-name} provides for importing and exporting single documents as well as whole projects, you may check the main documentation, xref:sect_formats[Appendix A: Formats]. @@ -241,13 +241,13 @@ You can only add users to a project from the dropdown at the left if they exist Click on the *administration* button in the very top right corner and select section *Users* on the left. For *user roles* (for an _instance_ of {product-name}) see the <> in the main documentation. + -image::getting_started_create_users.png[align="center"] +image::images/getting_started_create_users.png[align="center"] + * *Giving rights to users:* After selecting a user from the dropdown in the project settings section *Users*, you can check and uncheck the user's rights on the right side. User rights count for that _project_ only and are different from user roles which count for the whole {product-name} _instance_. Any combination of rights is possible and the user will always have the sum of all rights given. + -image::getting_started_users.png[align="center"] +image::images/getting_started_users.png[align="center"] + [[User_rights]] @@ -281,7 +281,7 @@ In this section, you may create custom layers and modify them later. Built-in layers should not be changed. In case you do not want to work on built-in layers only but wish to create custom layers designed for your individual task, we recommend reading the documentation for details on <>. -image::getting_started_layers.png[align="center"] +image::images/getting_started_layers.png[align="center"] [[box_layers_and_features_in_getting_started]] NOTE: *Layers and Features:* There are different "`aspects`" or "`categories`" you might want to annotate. @@ -339,7 +339,7 @@ Let's have "`CAT`" for the name and "`This tag is to be used for every mention o Click the save-button and the tag has now been added to your set. As another example, create a new tag for the name "`DOG`" and description "`This tag is to be used for every mention of a dog and only for mentions of dogs.`". + -image::getting_started_tagset_create.png[align="center"] +image::images/getting_started_tagset_create.png[align="center"] + [[link_to_a_layer_and_feature]] * In order to use the tagset, it is necessary to *link it to a layer and feature*. @@ -353,7 +353,7 @@ When you click on it, the panel _Feature details_ opens. In this panel, scroll down to _Tagset_ and choose your tagset (to stick with our example: _Example_Tagset_) from the dropdown and click _Save_. The tagset which was selected before is not linked to the layer any more but the new one is. + -image::getting_started_tagset_link.png[align="center"] +image::images/getting_started_tagset_link.png[align="center"] + * From now on, you can select your tags for annotating. Navigate to the annotation page (click _INCEpTION_ on the top left -> _Annotation_ and choose the document _pets2.txt_). @@ -361,7 +361,7 @@ On the layer dropdown on the right, choose the layer _Named entity_. When you double-click on any part in the text, for example "`Socke`" in line one, and click on the dropdown _value_ on the right, you find the tags "`DOG`" and "`CAT`" to choose from. (For details on how to annotate, see <>). + -image::getting_started_tagset_use.png[align="center"] +image::images/getting_started_tagset_use.png[align="center"] + * You might want to link Named Entity tags again to the _Named entity_ Layer and _value_ feature in order to use them like they were before our little experiment. @@ -381,7 +381,7 @@ The latter will be empty at first. It will not be filled here in the settings but at the knowledge base page ( -> _Dashboard_, -> _Knowledge base_; also see the part xref:knowledge_bases_in_getting_started_in_structrue[Knowledge Base] in <>). In order to import or create a knowledge base, just click the _Create_ button and {product-name} will lead you. -image::getting_started_kbs.png[align="center"] +image::images/getting_started_kbs.png[align="center"] NOTE: *Knowledge Bases* are data bases for knowledge. Let's assume, the mention "`Paris`" is to be annotated. @@ -397,7 +397,7 @@ Using many little knowledge bases in one project will slow down the performance * Via the Dashboard (click the Dashboard-button at the top centre), you get to the *knowledge base page*. This is a page different from the one in the project settings where you can modify and work on your knowledge bases. + -image::getting_started_kb_page.png[align="center"] +image::images/getting_started_kb_page.png[align="center"] + * *For details* on knowledge bases, see our main documentation on <>s, or our https://www.youtube.com/watch?v=wp4AN3p23mQ&list=PL5Hz5pttaj96SlXHGRZf8KzlYvpVHIoL-&index=3&t=0s../[tutorial video “Overview“^] mentioning knowledge bases. @@ -413,14 +413,14 @@ For details on how to _use_ recommenders, see our main documentation on xref:sec For details on _how to create and adjust_ them, see xref:sect_projects_recommendation[Recommenders] in the Projects section. Or check the https://www.youtube.com/watch?v=Xz3Hs8Lyoeg&list=PL5Hz5pttaj96SlXHGRZf8KzlYvpVHIoL-&index=3/[tutorial video “Recommender Basics”^]. -image::getting_started_recommenders.png[align="center"] +image::images/getting_started_recommenders.png[align="center"] === Guidelines In this section, you may import files with annotation guidelines. There is no automatic correction or warning from {product-name} if guidelines are violated but it is a short way for every user in the project to read and check the team guidelines while working. On the annotation page (→ _Dashboard_ → _Annotation_ → open any document), annotators can quickly look them up by clicking on the guidelines button on the top which looks like a book (this button only appears if at least one guideline was imported). -image::getting_started_guidelines.png[align="center"] +image::images/getting_started_guidelines.png[align="center"] [[export_in_getting_started]] === Export @@ -465,7 +465,7 @@ Mark the document as finished. Differences are highlighted. You can accept an annotation by clicking on it. -image::getting_started_curation.png[align="center"] +image::images/getting_started_curation.png[align="center"] * As a curator, you can also create new annotations on this page. It works exactly like on the Annotation page. @@ -486,16 +486,13 @@ NOTE: *Agreement:* The annotations of different annotators usually do not match This aspect of difference / similarity is called agreement. For agreement, some common measures are provided. -image::getting_started_agreement.png[align="center"] +image::images/getting_started_agreement.png[align="center"] === Workload Here you can check the overall progress of your project; see which user is working on or has finished which document; and toggle for each user the status of each document between *Done / In Progress* or between *New / Locked*. For details, see <> in the main documentation. -image::getting_started_monitoring.png[align="center"] - -=== Evaluation -The evaluation page shows a learning curve diagram of each recommender (see xref:recommenders_in_getting_started[Recommender]). +image::images/getting_started_monitoring.png[align="center"] === Settings Here, you can organize, manage and adjust all the details of your project. @@ -527,7 +524,7 @@ Here, you can see all the projects which you have access to. Right now, this will be only the example project. Choose the example project by clicking on its name and you will be on the *Dashboard* of this project. -image::getting_started_open_a_project.png[align="center"] +image::images/getting_started_open_a_project.png[align="center"] NOTE: *Instructions to Example Projects:* In case of the example project, on the dashboard you also find instructions how to use it. @@ -549,7 +546,7 @@ Then, use the mouse to select a word in the annotation area, e.g. _in my home_ i When you release the mouse button, the annotation will immediately be created and you can edit its details in the right sidebar (see next paragraph). These "`details`" are the features we mentioned before. -image::getting_started_first_annotation.png[align="center"] +image::images/getting_started_first_annotation.png[align="center"] *_Note:_* All annotations will be saved automatically without clicking an extra save-button. @@ -570,7 +567,7 @@ In order to learn how to adjust and create them for your purpose, see section << In the *Annotation* panel, you see the details of a selected annotation. They are called features. -image::getting_started_annotation_panel.png[align="center"] +image::images/getting_started_annotation_panel.png[align="center"] It shows the layer the annotation is made in (field _Layer_; here: _Named entity_) and what part of the text has been annotated (field _Text_; here _in my home_). Below, you can see and modify what has been entered for each of the so-called *Features*. @@ -589,9 +586,9 @@ functionalities are available to you is determined by the project settings. The opened by clicking on one of the sidebar icons and they can be closed by clicking on the arrow icon at the top. -image::getting_started_Sidebar_closed.png[align="center"] +image::images/getting_started_Sidebar_closed.png[align="center"] -image::getting_started_Sidebar_open.png[align="center"] +image::images/getting_started_Sidebar_open.png[align="center"] There are several features you might want to check the main documentation for. Especially the *Recommender* section of the sidebar (the black speech bubble) is worth a look in case you use recommenders (see xref:recommenders_in_getting_started[Recommenders in the section Project Settings]). diff --git a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/workflow.adoc b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/workflow.adoc index 245fd2f86c7..7a2a2ecc166 100644 --- a/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/workflow.adoc +++ b/inception/inception-doc/src/main/resources/META-INF/asciidoc/user-guide/workflow.adoc @@ -2,7 +2,7 @@ The following image shows an exemplary workflow of an annotation project with {product-name}. -image::progress_workflow.jpg[align="center"] +image::images/progress_workflow.jpg[align="center"] First, the projects need to be set up. In more detail, this means that users are to be added, guidelines need to be provided, documents have to be uploaded, tagsets need to be defined and uploaded, diff --git a/inception/inception-doc/src/test/java/de/tudarmstadt/ukp/inception/doc/GenerateDocumentation.java b/inception/inception-doc/src/test/java/de/tudarmstadt/ukp/inception/doc/GenerateDocumentation.java index 3218fa6e3f0..d8653b8ffeb 100644 --- a/inception/inception-doc/src/test/java/de/tudarmstadt/ukp/inception/doc/GenerateDocumentation.java +++ b/inception/inception-doc/src/test/java/de/tudarmstadt/ukp/inception/doc/GenerateDocumentation.java @@ -17,6 +17,14 @@ */ package de.tudarmstadt.ukp.inception.doc; +import static java.nio.file.Files.createDirectories; +import static java.nio.file.Files.createDirectory; +import static java.nio.file.Files.createSymbolicLink; +import static java.nio.file.Files.exists; +import static org.apache.commons.io.FileUtils.copyFile; +import static org.apache.commons.io.FileUtils.deleteQuietly; +import static org.apache.commons.io.FileUtils.listFiles; + import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -24,14 +32,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Set; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.TrueFileFilter; import org.asciidoctor.Asciidoctor; import org.asciidoctor.Attributes; import org.asciidoctor.Options; -import org.asciidoctor.OptionsBuilder; import org.asciidoctor.Placement; import org.asciidoctor.SafeMode; @@ -43,21 +49,19 @@ public class GenerateDocumentation private static List getAsciiDocs(Path dir) throws IOException { return Files.list(dir).filter(Files::isDirectory) // - .filter(p -> Files.exists(p.resolve("pom.xml"))) // + .filter(p -> exists(p.resolve("pom.xml"))) // .map(p -> p.resolve(asciiDocPath)) // .filter(Files::isDirectory) // - .collect(Collectors.toList()); + .toList(); } private static void buildDoc(String type, Path outputDir) throws IOException { - Attributes attributes = Attributes.builder() - .attribute("source-dir", getInceptionDir() + "/") + var attributes = Attributes.builder().attribute("source-dir", getInceptionDir() + "/") .attribute("include-dir", outputDir.resolve("asciidoc").resolve(type).toString() + "/") .attribute("imagesdir", - outputDir.resolve("asciidoc").resolve(type).resolve("images").toString() - + "/") + outputDir.resolve("asciidoc").resolve(type).toString() + "/") .docType("book") // .attribute("toclevels", "8") // .setAnchors(true) // @@ -69,51 +73,88 @@ private static void buildDoc(String type, Path outputDir) throws IOException .tableOfContents(Placement.LEFT) // .experimental(true) // .build(); - OptionsBuilder options = Options.builder() // + var options = Options.builder() // .toDir(outputDir.toFile()) // .safe(SafeMode.UNSAFE) // .attributes(attributes); - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + + var asciidoctor = Asciidoctor.Factory.create(); asciidoctor.requireLibrary("asciidoctor-diagram"); - File f = new File(outputDir.resolve("asciidoc").resolve(type).toString() + ".adoc"); - Files.createDirectories(f.getParentFile().toPath()); + + var f = new File(outputDir.resolve("asciidoc").resolve(type).toString() + ".adoc"); + createDirectories(f.getParentFile().toPath()); asciidoctor.convertFile(f, options.build()); } public static void main(String... args) throws Exception { - - Path inceptionDir = getInceptionDir(); - Path outputDir = Paths.get(System.getProperty("user.dir")).resolve("target") + var inceptionDir = getInceptionDir(); + var outputDir = Paths.get(System.getProperty("user.dir")).resolve("target") .resolve("doc-out"); - List modules = new ArrayList<>(getAsciiDocs(inceptionDir)); + deleteQuietly(outputDir.toFile()); - FileUtils.deleteQuietly(outputDir.toFile()); - Files.createDirectory(outputDir); + linkDocFiles(inceptionDir, outputDir); - for (Path module : modules) { + buildDoc("user-guide", outputDir); + buildDoc("developer-guide", outputDir); + buildDoc("admin-guide", outputDir); + + System.out.printf("Documentation written to: %s\n", outputDir); + } + + private static void copyDocFiles(Path inceptionDir, Path outputDir) throws IOException + { + createDirectory(outputDir); + var modules = new ArrayList<>(getAsciiDocs(inceptionDir)); + for (var module : modules) { System.out.printf("Including module: %s\n", module); - for (File f : FileUtils.listFiles(module.toFile(), TrueFileFilter.INSTANCE, + for (var f : listFiles(module.toFile(), TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)) { - Path p = f.toPath(); - Path targetPath = f.toPath().subpath(module.toAbsolutePath().getNameCount(), + var p = f.toPath(); + var targetPath = f.toPath().subpath(module.toAbsolutePath().getNameCount(), p.toAbsolutePath().getNameCount()); - FileUtils.copyFile(f, outputDir.resolve("asciidoc").resolve(targetPath).toFile()); + copyFile(f, outputDir.resolve("asciidoc").resolve(targetPath).toFile()); } } + } - buildDoc("user-guide", outputDir); - buildDoc("developer-guide", outputDir); - buildDoc("admin-guide", outputDir); + private static void linkDocFiles(Path inceptionDir, Path outputDir) throws IOException + { + // Create the output directory if it doesn't exist + createDirectory(outputDir); - System.out.printf("Documentation written to: %s\n", outputDir); + // Get the list of module directories that contain AsciiDoc files + var modules = new ArrayList<>(getAsciiDocs(inceptionDir)); + + for (var module : modules) { + System.out.printf("Including module: %s\n", module); + + // List all files in the current module + for (var f : listFiles(module.toFile(), TrueFileFilter.INSTANCE, + TrueFileFilter.INSTANCE)) { + if (Set.of(".DS_Store").contains(f.getName())) { + continue; + } + + var p = f.toPath(); + var targetPath = f.toPath().subpath(module.toAbsolutePath().getNameCount(), + p.toAbsolutePath().getNameCount()); + var linkPath = outputDir.resolve("asciidoc").resolve(targetPath); + + // Create the parent directories for the linkPath if they don't exist + createDirectories(linkPath.getParent()); + + // Create a symbolic link pointing to the original file + createSymbolicLink(linkPath, p.toAbsolutePath()); + } + } } private static Path getInceptionDir() { - Path userDir = Paths.get(System.getProperty("user.dir")); + var userDir = Paths.get(System.getProperty("user.dir")); return runningFromIntelliJ() ? userDir.resolve("inception") : userDir.getParent(); } diff --git a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/FullProjectExportRequest.java b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/FullProjectExportRequest.java index cf7a0348ef0..3f4e6b97827 100644 --- a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/FullProjectExportRequest.java +++ b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/FullProjectExportRequest.java @@ -26,6 +26,7 @@ public class FullProjectExportRequest { private static final long serialVersionUID = -7010995651575991241L; + private static final String FILENAME_PREFIX = "project"; public static final String FORMAT_AUTO = "AUTO"; private String format; @@ -93,14 +94,9 @@ public boolean isIncludeInProgress() } @Override - public String getFilenameTag() + public String getFilenamePrefix() { - return filenameTag; - } - - public void setFilenameTag(String aFilenameTag) - { - filenameTag = aFilenameTag; + return FILENAME_PREFIX; } @Override diff --git a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportRequest_ImplBase.java b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportRequest_ImplBase.java index 9bcb2d6321d..bcf5953739d 100644 --- a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportRequest_ImplBase.java +++ b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportRequest_ImplBase.java @@ -45,5 +45,5 @@ public Project getProject() public abstract String getTitle(); - public abstract String getFilenameTag(); + public abstract String getFilenamePrefix(); } diff --git a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportTaskMonitor.java b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportTaskMonitor.java index 9bd4774712c..f617af13551 100644 --- a/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportTaskMonitor.java +++ b/inception/inception-export-api/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/export/ProjectExportTaskMonitor.java @@ -37,6 +37,7 @@ public class ProjectExportTaskMonitor private final ProjectExportTaskHandle handle; private final long projectId; private final String title; + private final String filenamePrefix; private long createTime; private long startTime = -1; @@ -49,11 +50,12 @@ public class ProjectExportTaskMonitor private boolean destroyed = false; public ProjectExportTaskMonitor(Project aProject, ProjectExportTaskHandle aHandle, - String aTitle) + String aTitle, String aFilenamePrefix) { projectId = aProject.getId(); handle = aHandle; title = aTitle; + filenamePrefix = aFilenamePrefix; } public long getProjectId() @@ -143,6 +145,11 @@ public synchronized File getExportedFile() return exportedFile; } + public String getExportedFilenamePrefix() + { + return filenamePrefix; + } + public synchronized void setExportedFile(File aExportedFile) { exportedFile = aExportedFile; diff --git a/inception/inception-export/src/main/java/de/tudarmstadt/ukp/inception/export/DocumentImportExportServiceImpl.java b/inception/inception-export/src/main/java/de/tudarmstadt/ukp/inception/export/DocumentImportExportServiceImpl.java index 2fb672975db..1cad67806fc 100644 --- a/inception/inception-export/src/main/java/de/tudarmstadt/ukp/inception/export/DocumentImportExportServiceImpl.java +++ b/inception/inception-export/src/main/java/de/tudarmstadt/ukp/inception/export/DocumentImportExportServiceImpl.java @@ -21,6 +21,7 @@ import static de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasAccessMode.UNMANAGED_ACCESS; import static de.tudarmstadt.ukp.inception.project.api.ProjectService.withProjectLogger; import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.CURATION_USER; +import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.INITIAL_CAS_PSEUDO_USER; import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.exists; import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.getRealCas; import static java.util.Arrays.asList; @@ -363,11 +364,10 @@ private void runCasDoctorOnImport(SourceDocument aDocument, FormatSupport aForma var casDoctor = new CasDoctor(checksRegistry, repairsRegistry); casDoctor.setActiveChecks( checksRegistry.getExtensions().stream().map(c -> c.getId()).toArray(String[]::new)); - casDoctor.analyze(aDocument.getProject(), aCas, messages, true); + casDoctor.analyze(aDocument, INITIAL_CAS_PSEUDO_USER, aCas, messages, true); } - private void splitTokens(CAS cas, FormatSupport aFormat) - throws IOException + private void splitTokens(CAS cas, FormatSupport aFormat) throws IOException { var tokenType = getType(cas, Token.class); @@ -396,8 +396,7 @@ private void checkTokenQuota(CAS cas, FormatSupport aFormat) throws IOException } } - private void splitSenencesIfNecssary(CAS cas, FormatSupport aFormat) - throws IOException + private void splitSenencesIfNecssary(CAS cas, FormatSupport aFormat) throws IOException { var sentenceType = getType(cas, Sentence.class); diff --git a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java index aecfd6ec2fb..8ce93bb98b4 100644 --- a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java +++ b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectLogExporterTest.java @@ -93,7 +93,8 @@ void thatExportingAndImportingAgainWorks() throws Exception // Export the project var exportRequest = new FullProjectExportRequest(sourceProject, null, false); - var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test"); + var monitor = new ProjectExportTaskMonitor(sourceProject, null, "test", + exportRequest.getFilenamePrefix()); var exportedProject = new ExportedProject(); try (var zos = new ZipOutputStream(new FileOutputStream(exportFile))) { diff --git a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectPermissionsExporterTest.java b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectPermissionsExporterTest.java index 7c04e2b05ab..305863554d2 100644 --- a/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectPermissionsExporterTest.java +++ b/inception/inception-export/src/test/java/de/tudarmstadt/ukp/inception/export/exporters/ProjectPermissionsExporterTest.java @@ -214,7 +214,8 @@ public void thatProjectSpecificPermissionsAreNotCreatedIfUserAlreadyExisted() th private void exportProject() throws Exception { var exportRequest = new FullProjectExportRequest(project, null, false); - var monitor = new ProjectExportTaskMonitor(project, null, "test"); + var monitor = new ProjectExportTaskMonitor(project, null, "test", + exportRequest.getFilenamePrefix()); var stage = mock(ZipOutputStream.class); sut.exportData(exportRequest, monitor, exportedProject, stage); } diff --git a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditor.java b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditor.java index 8e0e8f8564b..5d0168493b1 100644 --- a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditor.java +++ b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditor.java @@ -18,8 +18,6 @@ package de.tudarmstadt.ukp.inception.externaleditor; import static de.tudarmstadt.ukp.inception.externaleditor.config.ExternalEditorLoader.PLUGINS_EDITOR_BASE_URL; -import static de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig.WS_ENDPOINT; -import static java.lang.String.format; import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.substringAfter; @@ -27,8 +25,6 @@ import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; -import org.apache.wicket.request.Url; -import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; @@ -102,11 +98,10 @@ public static String getUrlForPluginAsset(ServletContext aContext, @Override protected AnnotationEditorProperties getProperties() { - var props = new AnnotationEditorProperties(); var pluginDesc = getDescription(); + + var props = super.getProperties(); props.setEditorFactory(pluginDesc.getFactory()); - props.setDiamAjaxCallbackUrl(getDiamBehavior().getCallbackUrl().toString()); - props.setDiamWsUrl(constructWsEndpointUrl()); props.setStylesheetSources(pluginDesc.getStylesheets().stream() // .map(this::getUrlForPluginAsset) // .collect(toList())); @@ -114,17 +109,7 @@ protected AnnotationEditorProperties getProperties() .map(this::getUrlForPluginAsset) // .collect(toList())); props.setSectionElements(pluginDesc.getSectionElements()); - getFactory().getUserPreferencesKey() - .ifPresent(key -> props.setUserPreferencesKey(key.getClientSideKey())); - props.setEditorFactoryId(getFactory().getBeanName()); return props; } - - private String constructWsEndpointUrl() - { - Url endPointUrl = Url.parse(format("%s%s", context.getContextPath(), WS_ENDPOINT)); - endPointUrl.setProtocol("ws"); - return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); - } } diff --git a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditorBase.java b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditorBase.java index 5e7707096ba..2284a5f836a 100644 --- a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditorBase.java +++ b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/ExternalAnnotationEditorBase.java @@ -17,8 +17,10 @@ */ package de.tudarmstadt.ukp.inception.externaleditor; +import static de.tudarmstadt.ukp.clarin.webanno.security.WicketSecurityUtils.getCsrfTokenFromSession; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; import static de.tudarmstadt.ukp.inception.support.wicket.WicketUtil.wrapInTryCatch; +import static de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig.WS_ENDPOINT; import static java.lang.String.format; import static java.lang.invoke.MethodHandles.lookup; import static org.apache.wicket.markup.head.JavaScriptHeaderItem.forReference; @@ -26,12 +28,15 @@ import static org.slf4j.LoggerFactory.getLogger; import java.io.IOException; +import java.util.Optional; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.model.IModel; +import org.apache.wicket.request.Url; +import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; import org.wicketstuff.event.annotation.OnEvent; @@ -45,6 +50,7 @@ import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtensionRegistry; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorFactory; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorRegistry; +import de.tudarmstadt.ukp.inception.editor.ContextMenuLookup; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.editor.view.DocumentViewExtensionPoint; import de.tudarmstadt.ukp.inception.externaleditor.command.CommandQueue; @@ -54,6 +60,7 @@ import de.tudarmstadt.ukp.inception.externaleditor.command.ScrollToCommand; import de.tudarmstadt.ukp.inception.externaleditor.model.AnnotationEditorProperties; import de.tudarmstadt.ukp.inception.externaleditor.resources.ExternalEditorJavascriptResourceReference; +import de.tudarmstadt.ukp.inception.preferences.ClientSideUserPreferencesProvider; import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.selection.ScrollToEvent; @@ -134,6 +141,12 @@ public DiamAjaxBehavior getDiamBehavior() return diamBehavior; } + @Override + public Optional getContextMenuLookup() + { + return Optional.ofNullable(diamBehavior); + } + protected abstract Component makeView(); @Override @@ -186,7 +199,28 @@ protected EditorCommand renderCommand() return new LoadAnnotationsCommand(); } - protected abstract AnnotationEditorProperties getProperties(); + protected AnnotationEditorProperties getProperties() + { + var props = new AnnotationEditorProperties(); + props.setEditorFactoryId(getFactory().getBeanName()); + props.setDiamAjaxCallbackUrl(getDiamBehavior().getCallbackUrl().toString()); + props.setDiamWsUrl(constructWsEndpointUrl()); + props.setCsrfToken(getCsrfTokenFromSession()); + + if (getFactory() instanceof ClientSideUserPreferencesProvider factory) { + factory.getUserPreferencesKey() + .ifPresent(key -> props.setUserPreferencesKey(key.getClientSideKey())); + } + + return props; + } + + private String constructWsEndpointUrl() + { + Url endPointUrl = Url.parse(format("%s%s", context.getContextPath(), WS_ENDPOINT)); + endPointUrl.setProtocol("ws"); + return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); + } private String getPropertiesAsJson() { diff --git a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/model/AnnotationEditorProperties.java b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/model/AnnotationEditorProperties.java index edf61d9223a..b9f55b4e3b4 100644 --- a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/model/AnnotationEditorProperties.java +++ b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/model/AnnotationEditorProperties.java @@ -25,6 +25,7 @@ public class AnnotationEditorProperties private String editorFactory; private String diamAjaxCallbackUrl; private String diamWsUrl; + private String csrfToken; private String userPreferencesKey; private List scriptSources; private List stylesheetSources; @@ -120,4 +121,14 @@ public void setSectionElements(List aSectionElements) { sectionElements = aSectionElements; } + + public String getCsrfToken() + { + return csrfToken; + } + + public void setCsrfToken(String aCsrfToken) + { + csrfToken = aCsrfToken; + } } diff --git a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/xhtml/XHtmlXmlDocumentViewControllerImpl.java b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/xhtml/XHtmlXmlDocumentViewControllerImpl.java index 477b0d603fb..e06dbf1d75b 100644 --- a/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/xhtml/XHtmlXmlDocumentViewControllerImpl.java +++ b/inception/inception-external-editor/src/main/java/de/tudarmstadt/ukp/inception/externaleditor/xhtml/XHtmlXmlDocumentViewControllerImpl.java @@ -19,6 +19,7 @@ import static java.lang.invoke.MethodHandles.lookup; import static java.util.Optional.ofNullable; +import static javax.xml.XMLConstants.DEFAULT_NS_PREFIX; import static org.slf4j.LoggerFactory.getLogger; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.MediaType.IMAGE_GIF; @@ -26,14 +27,11 @@ import static org.springframework.http.MediaType.IMAGE_PNG; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.StringWriter; import java.security.Principal; import java.util.Locale; import java.util.Optional; -import javax.xml.XMLConstants; - import org.apache.commons.lang3.StringUtils; import org.apache.uima.cas.CAS; import org.dkpro.core.api.xml.type.XmlDocument; @@ -64,7 +62,6 @@ import de.tudarmstadt.ukp.inception.externaleditor.policy.DefaultHtmlDocumentPolicy; import de.tudarmstadt.ukp.inception.externaleditor.policy.SafetyNetDocumentPolicy; import de.tudarmstadt.ukp.inception.externaleditor.xml.XmlCas2SaxEvents; -import de.tudarmstadt.ukp.inception.io.xml.dkprocore.Cas2SaxEvents; import de.tudarmstadt.ukp.inception.support.wicket.ServletContextUtils; import jakarta.servlet.ServletContext; @@ -87,6 +84,7 @@ public class XHtmlXmlDocumentViewControllerImpl private static final String BODY = "body"; private static final String HEAD = "head"; private static final String P = "p"; + private static final String SPAN = "span"; private final DocumentService documentService; private final DocumentStorageService documentStorageService; @@ -160,12 +158,8 @@ public ResponseEntity getDocument(@PathVariable("projectId") long aProje // If the CAS contains an actual HTML structure, then we send that. Mind that we do // not inject format-specific CSS then! if (casContainsHtml) { - var xml = maybeXmlDocument.get(); startXHtmlDocument(rawHandler); - - var serializer = new XmlCas2SaxEvents(xml, finalHandler); - serializer.process(xml.getRoot()); - + renderXmlContent(finalHandler, maybeXmlDocument.get()); endXHtmlDocument(rawHandler); return toResponse(out); } @@ -173,31 +167,35 @@ public ResponseEntity getDocument(@PathVariable("projectId") long aProje startXHtmlDocument(rawHandler); rawHandler.startElement(null, null, HTML, null); - renderHead(doc, rawHandler); - rawHandler.startElement(null, null, BODY, null); if (maybeXmlDocument.isEmpty()) { // Gracefully handle the case that the CAS does not contain any XML structure at all // and show only the document text in this case. + var atts = new AttributesImpl(); + atts.addAttribute("", "", "class", "CDATA", "i7n-plain-text-document"); + rawHandler.startElement(null, null, BODY, atts); renderTextContent(cas, finalHandler); + rawHandler.endElement(null, null, BODY); } else { + rawHandler.startElement(null, null, BODY, null); + var formatPolicy = formatRegistry.getFormatPolicy(doc); var defaultNamespace = formatPolicy.flatMap(policy -> policy.getDefaultNamespace()); if (defaultNamespace.isPresent()) { - finalHandler.startPrefixMapping(XMLConstants.DEFAULT_NS_PREFIX, - defaultNamespace.get()); + finalHandler.startPrefixMapping(DEFAULT_NS_PREFIX, defaultNamespace.get()); } - renderXmlContent(doc, finalHandler, aEditor, maybeXmlDocument.get()); + renderXmlContent(finalHandler, maybeXmlDocument.get()); if (defaultNamespace.isPresent()) { - finalHandler.endPrefixMapping(XMLConstants.DEFAULT_NS_PREFIX); + finalHandler.endPrefixMapping(DEFAULT_NS_PREFIX); } + + rawHandler.endElement(null, null, BODY); } - rawHandler.endElement(null, null, BODY); rawHandler.endElement(null, null, HTML); @@ -229,27 +227,34 @@ private void renderHead(SourceDocument doc, ContentHandler ch) throws SAXExcepti ch.endElement(null, null, HEAD); } - private void renderXmlContent(SourceDocument doc, ContentHandler ch, Optional aEditor, - XmlDocument aXmlDocument) - throws IOException, SAXException + private void renderXmlContent(ContentHandler ch, XmlDocument aXmlDocument) throws SAXException { - Cas2SaxEvents serializer = new XmlCas2SaxEvents(aXmlDocument, ch); + var serializer = new XmlCas2SaxEvents(aXmlDocument, ch); serializer.process(aXmlDocument.getRoot()); } private void renderTextContent(CAS cas, ContentHandler ch) throws SAXException { + var lineAttribs = new AttributesImpl(); + lineAttribs.addAttribute("", "", "class", "CDATA", "data-i7n-tracking"); + var text = cas.getDocumentText().toCharArray(); ch.startElement(null, null, P, null); + ch.startElement(null, null, SPAN, lineAttribs); + var lineBreakSequenceLength = 0; for (int i = 0; i < text.length; i++) { if (text[i] == '\n') { lineBreakSequenceLength++; + ch.endElement(null, null, SPAN); + ch.startElement(null, null, SPAN, lineAttribs); } else if (text[i] != '\r') { if (lineBreakSequenceLength > 1) { + ch.endElement(null, null, SPAN); ch.endElement(null, null, P); ch.startElement(null, null, P, null); + ch.startElement(null, null, SPAN, lineAttribs); } lineBreakSequenceLength = 0; @@ -257,6 +262,8 @@ else if (text[i] != '\r') { ch.characters(text, i, 1); } + + ch.endElement(null, null, SPAN); ch.endElement(null, null, P); } diff --git a/inception/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchAutoConfiguration.java b/inception/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchAutoConfiguration.java index ccbdafe2253..9d01de468ce 100644 --- a/inception/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchAutoConfiguration.java +++ b/inception/inception-external-search-core/src/main/java/de/tudarmstadt/ukp/inception/externalsearch/config/ExternalSearchAutoConfiguration.java @@ -40,7 +40,6 @@ public class ExternalSearchAutoConfiguration { @Bean - @Autowired public ExternalSearchService externalSearchService(ExternalSearchProviderRegistry aRegistry) { return new ExternalSearchServiceImpl(aRegistry); @@ -55,7 +54,7 @@ public ExternalSearchProviderRegistry externalSearchProviderRegistry( @Bean public DocumentRepositoryExporter documentRepositoryExporter( - @Autowired ExternalSearchService aSearchService) + ExternalSearchService aSearchService) { return new DocumentRepositoryExporter(aSearchService); } diff --git a/inception/inception-external-search-core/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/exporter/DocumentRepositoryExporterTest.java b/inception/inception-external-search-core/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/exporter/DocumentRepositoryExporterTest.java index 34b33b25ee6..9413eab815f 100644 --- a/inception/inception-external-search-core/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/exporter/DocumentRepositoryExporterTest.java +++ b/inception/inception-external-search-core/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/exporter/DocumentRepositoryExporterTest.java @@ -79,7 +79,8 @@ private ArgumentCaptor runExportImportAndFetchDocumentReposi { // Export the project var exportRequest = new FullProjectExportRequest(project, null, false); - var monitor = new ProjectExportTaskMonitor(project, null, "test"); + var monitor = new ProjectExportTaskMonitor(project, null, "test", + exportRequest.getFilenamePrefix()); var exportedProject = new ExportedProject(); var file = mock(ZipOutputStream.class); diff --git a/inception/inception-external-search-opensearch/pom.xml b/inception/inception-external-search-opensearch/pom.xml index 3284fe92e4d..16e326e8fac 100644 --- a/inception/inception-external-search-opensearch/pom.xml +++ b/inception/inception-external-search-opensearch/pom.xml @@ -147,5 +147,10 @@ inception-model test + + org.opensearch + opensearch-common + test + diff --git a/inception/inception-external-search-opensearch/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/opensearch/OpenSearchProviderTest.java b/inception/inception-external-search-opensearch/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/opensearch/OpenSearchProviderTest.java index 4dbae5dcc5e..e165c708f92 100644 --- a/inception/inception-external-search-opensearch/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/opensearch/OpenSearchProviderTest.java +++ b/inception/inception-external-search-opensearch/src/test/java/de/tudarmstadt/ukp/inception/externalsearch/opensearch/OpenSearchProviderTest.java @@ -24,12 +24,18 @@ import java.util.List; import org.codelibs.opensearch.runner.OpenSearchRunner; +import org.codelibs.opensearch.runner.OpenSearchRunnerException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opensearch.LegacyESVersion; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.client.Requests; +import org.opensearch.cluster.health.ClusterHealthStatus; +import org.opensearch.common.Priority; import org.opensearch.common.settings.Settings; import org.opensearch.common.settings.Settings.Builder; +import org.opensearch.common.unit.TimeValue; import de.tudarmstadt.ukp.inception.externalsearch.ExternalSearchResult; import de.tudarmstadt.ukp.inception.externalsearch.model.DocumentRepository; @@ -52,7 +58,25 @@ public class OpenSearchProviderTest @BeforeEach public void setup() { - osRunner = new OpenSearchRunner(); + osRunner = new OpenSearchRunner() + { + @Override + public ClusterHealthStatus ensureYellow(final String... indices) + { + final ClusterHealthResponse actionGet = client().admin().cluster() + .health(Requests.clusterHealthRequest(indices) + .waitForNoRelocatingShards(true).waitForYellowStatus() + .waitForEvents(Priority.LANGUID)) + .actionGet(TimeValue.timeValueSeconds(60)); + if (actionGet.isTimedOut()) { + throw new OpenSearchRunnerException("ensureYellow timed out, cluster state:\n" + + "\n" + client().admin().cluster().prepareState().get().getState() + + "\n" + client().admin().cluster().preparePendingClusterTasks().get(), + actionGet); + } + return actionGet.getStatus(); + } + }; osRunner.onBuild(new OpenSearchRunner.Builder() { @Override diff --git a/inception/inception-feature-lookup/src/main/java/de/tudarmstadt/ukp/inception/feature/lookup/LookupFeatureSupport.java b/inception/inception-feature-lookup/src/main/java/de/tudarmstadt/ukp/inception/feature/lookup/LookupFeatureSupport.java index 3a8dfe736a3..05cdd4090dc 100644 --- a/inception/inception-feature-lookup/src/main/java/de/tudarmstadt/ukp/inception/feature/lookup/LookupFeatureSupport.java +++ b/inception/inception-feature-lookup/src/main/java/de/tudarmstadt/ukp/inception/feature/lookup/LookupFeatureSupport.java @@ -20,6 +20,7 @@ import static java.util.Arrays.asList; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import java.lang.invoke.MethodHandles; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -61,7 +62,7 @@ public class LookupFeatureSupport public static final String STRING = "string"; public static final String TYPE_STRING_LOOKUP = PREFIX + STRING; - private static final Logger LOG = LoggerFactory.getLogger(LookupFeatureSupport.class); + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final LookupCache labelCache; private final LookupServiceProperties properties; @@ -94,13 +95,15 @@ public Optional getFeatureType(AnnotationFeature aFeature) } return Optional.of(new FeatureType(aFeature.getType(), - aFeature.getType().substring(PREFIX.length()), featureSupportId)); + "Lookup (" + aFeature.getType().substring(PREFIX.length()) + ")", + featureSupportId)); } @Override public List getSupportedFeatureTypes(AnnotationLayer aAnnotationLayer) { - return asList(new FeatureType(TYPE_STRING_LOOKUP, "Lookup", featureSupportId)); + return asList( + new FeatureType(TYPE_STRING_LOOKUP, "Lookup (" + STRING + ")", featureSupportId)); } @Override diff --git a/inception/inception-guidelines/src/main/java/de/tudarmstadt/ukp/inception/guidelines/GuidelinesDialogContent.html b/inception/inception-guidelines/src/main/java/de/tudarmstadt/ukp/inception/guidelines/GuidelinesDialogContent.html index d6f81a8c436..9c2001950b9 100644 --- a/inception/inception-guidelines/src/main/java/de/tudarmstadt/ukp/inception/guidelines/GuidelinesDialogContent.html +++ b/inception/inception-guidelines/src/main/java/de/tudarmstadt/ukp/inception/guidelines/GuidelinesDialogContent.html @@ -21,22 +21,19 @@
      - - {#if sectionSelector} + {#if sectionSelector}
      @@ -39,6 +39,14 @@
      --> +
      + + +
      +
      + + +
      diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts index 26bd754c2c9..d255635f189 100644 --- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts +++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/ApacheAnnotatorVisualizer.ts @@ -39,7 +39,7 @@ export class ApacheAnnotatorVisualizer { private tracker: ViewportTracker private showInlineLabels = false private showEmptyHighlights = false - private observer: IntersectionObserver + private sectionSelector: string private sectionAnnotationVisualizer: SectionAnnotationVisualizer private sectionAnnotationCreator: SectionAnnotationCreator @@ -47,8 +47,12 @@ export class ApacheAnnotatorVisualizer { private data? : AnnotatedText - private removeTransientMarkers: (() => void)[] = [] - private removeTransientMarkersTimeout: number | undefined = undefined + private scrolling = false + private lastScrollTop: number | undefined = undefined + private removeScrollMarkers: (() => void)[] = [] + private removeScrollMarkersTimeout: number | undefined = undefined + private removePingMarkers: (() => void)[] = [] + private removePingMarkersTimeout: number | undefined = undefined private alpha = '55' @@ -129,6 +133,11 @@ export class ApacheAnnotatorVisualizer { } loadAnnotations (): void { + // scrollTo uses a timeout to work around the problem that the browser does not always properly + // scroll to the target element. We want to avoid loading annotations while scrolling is still + // in progress. Once scrolling is complete, we should get triggered by the ViewportTracker. + if (this.scrolling) return + const options: DiamLoadAnnotationsOptions = { range: this.tracker.currentRange, includeText: false, @@ -146,7 +155,8 @@ export class ApacheAnnotatorVisualizer { } private renderAnnotations (doc: AnnotatedText): void { - const startTime = new Date().getTime() + console.log(`Client-side starting`) + const startTime = performance.now() this.clearHighlights() this.resizer.hide() @@ -181,8 +191,8 @@ export class ApacheAnnotatorVisualizer { this.renderSelectedRelationEndpointHighlights(doc) } - const endTime = new Date().getTime() - console.log(`Client-side rendering took ${Math.abs(endTime - startTime)}ms`) + const endTime = performance.now() + console.log(`Client-side rendering took ${endTime - startTime}ms`) } private renderVerticalSelectionMarker (doc: AnnotatedText) { @@ -262,12 +272,17 @@ export class ApacheAnnotatorVisualizer { * Some highlights may only contain whitepace. This method removes such highlights. */ private removeWhitepaceOnlyHighlights (selector: string = '.iaa-highlighted') { - this.root.querySelectorAll(selector).forEach(e => { + const start = performance.now(); + const candidates = this.root.querySelectorAll(selector) + console.log(`Found ${candidates.length} elements matching [${selector}] to remove whitespace-only highlights`) + candidates.forEach(e => { if (!e.classList.contains('iaa-zero-width') && !e.textContent?.trim()) { e.after(...e.childNodes) e.remove() } }) + const end = performance.now(); + console.log(`Removing whitespace only highlights took ${end - start}ms`) } private postProcessHighlights () { @@ -368,14 +383,14 @@ export class ApacheAnnotatorVisualizer { if (viewportBegin <= begin && end <= viewportEnd) { // Quick and easy if the annotation fits entirely into the visible viewport - const startTime = new Date().getTime() + const startTime = performance.now() this.renderHighlight(span, begin, end, attributes) - const endTime = new Date().getTime() - console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms`) + const endTime = performance.now() + // console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms`) } else { // Try optimizing for long spans to improve rendering performance let fragmentCount = 0 - const startTime = new Date().getTime() + const startTime = performance.now() const coreBegin = Math.max(begin, viewportBegin) const coreEnd = Math.min(end, viewportEnd) @@ -394,8 +409,8 @@ export class ApacheAnnotatorVisualizer { this.renderHighlight(span, end, end, attributes) fragmentCount++ } - const endTime = new Date().getTime() - console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms (${fragmentCount} fragments)`) + const endTime = performance.now() + // console.debug(`Rendering span with size ${end - begin} took ${Math.abs(endTime - startTime)}ms (${fragmentCount} fragments)`) } } @@ -414,21 +429,56 @@ export class ApacheAnnotatorVisualizer { this.toCleanUp.add(highlightText(range, 'mark', attributes)) } + private clearScrollMarkers () { + if (this.removeScrollMarkersTimeout) { + window.cancelIdleCallback(this.removeScrollMarkersTimeout) + this.removeScrollMarkersTimeout = undefined + this.removeScrollMarkers.forEach(remove => remove()) + this.removeScrollMarkers = [] + } + } + + private renderPingMarkers(pingRanges?: Offsets[]) { + if (!pingRanges) return + + console.log('Rendering ping markers') + + for (const pingOffset of pingRanges || []) { + const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1]) + if (pingRange) { + this.removePingMarkers.push(this.safeHighlightText(pingRange, 'mark', { class: 'iaa-ping-marker' })) + } + } + + this.removeWhitepaceOnlyHighlights('.iaa-ping-marker') + this.removeSpuriousZeroWidthHighlights() + + if (this.removePingMarkers.length > 0) { + this.removePingMarkersTimeout = window.setTimeout(() => this.clearPingMarkers(), 2000) + } + } + + private clearPingMarkers () { + console.log('Clearing ping markers'); + + if (this.removePingMarkersTimeout) { + window.clearTimeout(this.removePingMarkersTimeout) + this.removePingMarkersTimeout = undefined + this.removePingMarkers.forEach(remove => remove()) + this.removePingMarkers = [] + } + } + scrollTo (args: { offset: number, position?: string, pingRanges?: Offsets[] }): void { const range = offsetToRange(this.root, args.offset, args.offset) if (!range) return - window.clearTimeout(this.removeTransientMarkersTimeout) - this.removeTransientMarkers.forEach(remove => remove()) - this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120 + this.clearScrollMarkers() + this.clearPingMarkers() + // Add scroll marker const removeScrollMarker = highlightText(range, 'mark', { id: 'iaa-scroll-marker' }) - this.removeTransientMarkers = [removeScrollMarker] - for (const pingOffset of args.pingRanges || []) { - const pingRange = offsetToRange(this.root, pingOffset[0], pingOffset[1]) - if (!pingRange) continue - this.removeTransientMarkers.push(highlightText(pingRange, 'mark', { class: 'iaa-ping-marker' })) - } + this.removeScrollMarkers = [removeScrollMarker] if (!this.showEmptyHighlights) { this.removeWhitepaceOnlyHighlights('.iaa-ping-marker') @@ -461,17 +511,38 @@ export class ApacheAnnotatorVisualizer { // markers are still there. var scrollIntoViewFunc = () => { finalScrollTarget.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' }) - if (this.removeTransientMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100) + if (this.removePingMarkers.length > 0) window.setTimeout(scrollIntoViewFunc, 100) + + if (this.root instanceof HTMLElement) { + if (this.root.scrollTop === this.lastScrollTop) { + this.scrollToComplete(args.pingRanges) + } + else { + this.lastScrollTop = this.root.scrollTop + } + } } + this.scrolling = true + this.sectionAnnotationVisualizer.suspend() + this.sectionAnnotationCreator.suspend() window.setTimeout(scrollIntoViewFunc, 100) } - this.removeTransientMarkersTimeout = window.setTimeout(() => { - this.removeTransientMarkers.forEach(remove => remove()) - this.removeTransientMarkers = [] - this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120 - }, 2000) + this.removeScrollMarkersTimeout = window.requestIdleCallback(() => this.scrollToComplete(args.pingRanges), { timeout: 2000 }) + } + + private scrollToComplete(pingRanges?: Offsets[]) { + console.log('Scrolling complete') + + this.clearScrollMarkers() + // this.renderPingMarkers(pingRanges) + this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120 + + this.scrolling = false + this.sectionAnnotationCreator.resume() + this.sectionAnnotationVisualizer.resume() + this.lastScrollTop = undefined } private clearHighlights (): void { @@ -481,20 +552,19 @@ export class ApacheAnnotatorVisualizer { return } - const startTime = new Date().getTime() + const startTime = performance.now() const highlightCount = this.toCleanUp.size this.toCleanUp.forEach(cleanup => cleanup()) this.toCleanUp.clear() this.root.normalize() // https://github.com/apache/incubator-annotator/issues/120 - const endTime = new Date().getTime() + const endTime = performance.now() console.log(`Cleaning up ${highlightCount} annotations and normalizing DOM took ${Math.abs(endTime - startTime)}ms`) } destroy (): void { - if (this.observer) { - this.observer.disconnect() - } - + this.sectionAnnotationCreator.destroy() + this.sectionAnnotationVisualizer.destroy() + this.tracker.disconnect() this.clearHighlights() } } diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts index 4484e070397..4071b737219 100644 --- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts +++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationCreator.ts @@ -17,12 +17,17 @@ */ import './SectionAnnotationCreator.scss' import { AnnotatedText, calculateEndOffset, calculateStartOffset, DiamAjax } from "@inception-project/inception-js-api" +import { getScrollY } from './SectionAnnotationVisualizer' export class SectionAnnotationCreator { private sectionSelector: string private ajax: DiamAjax private root: Element + private observer: IntersectionObserver + private observerDebounceTimeout: number | undefined + private suspended = false + private _previewFrame: HTMLIFrameElement | undefined private previewRenderTimeout: number | undefined private previewScrollTimeout: number | undefined @@ -35,12 +40,23 @@ export class SectionAnnotationCreator { if (this.sectionSelector) { this.initializeElementTracking() this.initializeSectionTypeAttributes() - - // on scrolling the window, we need to ensure that the panels stay visible - this.root.addEventListener('scroll', () => this.ensureVisibility()) } } + public suspend() { + this.suspended = true + } + + public resume() { + this.suspended = false + } + + public destroy() { + this.observer.disconnect() + this.root.querySelectorAll('.iaa-section-control').forEach(e => e.remove()) + this.hidePreviewFrame() + } + private initializeSectionTypeAttributes() { this.root.querySelectorAll(this.sectionSelector).forEach((e, i) => { e.setAttribute('data-iaa-section-type', e.localName) @@ -65,9 +81,10 @@ export class SectionAnnotationCreator { } private ensureVisibility() { - const rootRect = this.root.getBoundingClientRect() - const scrollY = (this.root.scrollTop || 0) - rootRect.top + const scrollY = getScrollY(this.root) const panels = Array.from(this.root.querySelectorAll('.iaa-section-control') || []) + + const panelsTops = new Map() for (const panel of (panels as HTMLElement[])) { const sectionId = panel.getAttribute('data-iaa-applies-to') const section = this.root.querySelector(`[id="${sectionId}"]`) @@ -76,30 +93,42 @@ export class SectionAnnotationCreator { continue } const sectionRect = section.getBoundingClientRect() - panel.style.top = `${sectionRect.top + scrollY}px` + panelsTops.set(panel, sectionRect.top) + } + + // Update the position of the panels all at once to avoid layout thrashing + for (const [panel, top] of panelsTops) { + panel.style.top = `${top + scrollY}px` } } private handleIntersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void { - const rootRect = this.root.getBoundingClientRect() - const scrollY = (this.root.scrollTop || 0) - rootRect.top - - for (const entry of entries) { - const sectionId = entry.target.id - const sectionRect = entry.boundingClientRect - let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement - - if (entry.isIntersecting && !panel) { - panel = this.createControl() - panel.setAttribute('data-iaa-applies-to', sectionId) - this.root.appendChild(panel) - panel.style.top = `${sectionRect.top + scrollY}px` - } + if (this.observerDebounceTimeout) { + window.cancelIdleCallback(this.observerDebounceTimeout) + this.observerDebounceTimeout = undefined + } - if (!entry.isIntersecting && panel) { - panel.remove() + this.observerDebounceTimeout = window.requestIdleCallback(() => { + const rootRect = this.root.getBoundingClientRect() + const scrollY = (this.root.scrollTop || 0) - rootRect.top + + for (const entry of entries) { + const sectionId = entry.target.id + const sectionRect = entry.boundingClientRect + let panel = this.root.querySelector(`.iaa-section-control[data-iaa-applies-to="${sectionId}"]`) as HTMLElement + + if (entry.isIntersecting && !panel) { + panel = this.createControl() + panel.setAttribute('data-iaa-applies-to', sectionId) + panel.style.top = `${sectionRect.top + scrollY}px` + this.root.appendChild(panel) + } + + if (!entry.isIntersecting && panel) { + panel.remove() + } } - } + }, { timeout: 100 }) } private createControl(): HTMLElement { diff --git a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts index f0d97029dc1..5208dbd1174 100644 --- a/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts +++ b/inception/inception-html-apache-annotator-editor/src/main/ts/src/apache-annotator/SectionAnnotationVisualizer.ts @@ -15,6 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { ApacheAnnotatorEditor } from './ApacheAnnotatorEditor' +import { ApacheAnnotatorVisualizer } from './ApacheAnnotatorVisualizer' import './SectionAnnotationVisualizer.scss' import { AnnotatedText, bgToFgColor, DiamAjax, Span, VID } from "@inception-project/inception-js-api" @@ -22,6 +24,7 @@ export class SectionAnnotationVisualizer { private sectionSelector: string private ajax: DiamAjax private root: Element + private suspended = false public constructor(root: Element, ajax: DiamAjax, sectionSelector: string) { this.root = root @@ -31,15 +34,33 @@ export class SectionAnnotationVisualizer { if (this.sectionSelector) { const root = this.root.closest('.i7n-wrapper') || this.root // on scrolling the window, we need to ensure that the panels stay visible - root.addEventListener('scroll', () => this.ensurePanelVisibility()) + root.addEventListener('scroll', () => { + if (!this.suspended) { + this.ensurePanelVisibility('scroll') + } + }) } } + public suspend() { + this.suspended = true + } + + public resume() { + this.suspended = false + this.ensurePanelVisibility('resume') + } + + public destroy() { + this.clear() + } + + render(doc: AnnotatedText) { if (this.sectionSelector) { this.clear() this.renderSectionGroups(doc) - this.ensurePanelVisibility() + this.ensurePanelVisibility('render') } } @@ -50,63 +71,111 @@ export class SectionAnnotationVisualizer { } } - private ensurePanelVisibility() { - const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || []) + private ensurePanelVisibility(reason: string) { + performance.mark('start-ensure-panel-visibility') + performance.mark('start-ensure-panel-visibility-init') + const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || []) const root = this.root.closest('.i7n-wrapper') || this.root - // const root = this.root - const rootRect = root.getBoundingClientRect() - const scrollY = (root.scrollTop || 0) - rootRect.top + // const rootRect = root.getBoundingClientRect() + // const scrollY = (root.scrollTop || 0) - rootRect.top + const scrollY = getScrollY(root) + let rootTop = getTop(root) + let lastSectionPanelBottom = rootTop + performance.mark('end-ensure-panel-visibility-init') + performance.measure('SectionAnnotationVisualizer.ensurePanelVisibility.init', 'start-ensure-panel-visibility-init', 'end-ensure-panel-visibility-init') - let lastSectionPanelBottom = rootRect.top + performance.mark('start-get-section-spacers') + const sectionSpacersMap = new Map() + const spacerRectMap = new Map() + const sectionRectMap = new Map() + const spacers = this.root.querySelectorAll('.iaa-visible-annotations-panel-spacer') + spacers.forEach(spacer => { + const sectionId = spacer.getAttribute('data-iaa-applies-to') + if (sectionId) { + var section = this.root.ownerDocument.getElementById(sectionId) + if (section) { + sectionSpacersMap.set(sectionId, spacer) + spacerRectMap.set(sectionId, spacer.getBoundingClientRect()) + sectionRectMap.set(sectionId, section.getBoundingClientRect()) + } + } + }); + performance.mark('end-get-section-spacers') + performance.measure('SectionAnnotationVisualizer.getSectionSpacers', 'start-get-section-spacers', 'end-get-section-spacers') + + performance.mark('start-render-section-panels') for (const panel of (panels as HTMLElement[])) { const sectionId = panel.getAttribute('data-iaa-applies-to') - const spacer = this.root.querySelector(`.iaa-visible-annotations-panel-spacer[data-iaa-applies-to="${sectionId}"]`) + if (!sectionId) { + console.warn(`Panel has no 'data-iaa-applies-to' attribute`, panel) + continue + } + + const spacer = sectionSpacersMap.get(sectionId) if (!spacer) { console.warn(`No spacer found for section [${sectionId}]`) continue } - const section = this.root.querySelector(`[id="${sectionId}"]`) + + const section = this.root.ownerDocument.getElementById(sectionId) if (!section) { console.warn(`Cannot find element for section [${sectionId}]`) continue } - const sectionRect = section.getBoundingClientRect() - const spacerRect = spacer.getBoundingClientRect() // Dimensions same as panel - - const sectionLeavingViewport = sectionRect.bottom - spacerRect.height < rootRect.top - // console.log(`Leaving viewport = ${sectionLeavingViewport}`) - if (sectionLeavingViewport) { - const hiddenUnderHigherLevelPanel = lastSectionPanelBottom && (sectionRect.bottom + rootRect.top - spacerRect.height) < lastSectionPanelBottom - if (hiddenUnderHigherLevelPanel) { - // If there is already a higher-level panel stacked then we snap the panel back to its - // spacer immediately - panel.style.top = `${spacerRect.top + scrollY}px` + performance.mark(`start-render-section-panel-${sectionId}`) + try { + // Fit the panels to the spacers + const sectionRect = sectionRectMap.get(sectionId) + const spacerRect = spacerRectMap.get(sectionId) // Dimensions same as panel + + const sectionLeavingViewport = sectionRect.bottom - spacerRect.height < rootTop + // console.log(`Leaving viewport = ${sectionLeavingViewport}`) + if (sectionLeavingViewport) { + const hiddenUnderHigherLevelPanel = lastSectionPanelBottom && (sectionRect.bottom + rootTop - spacerRect.height) < lastSectionPanelBottom + if (hiddenUnderHigherLevelPanel) { + // If there is already a higher-level panel stacked then we snap the panel back to its + // spacer immediately + panel.style.position = 'fixed' + panel.style.top = `${spacerRect.top}px` + } + else { + // Otherwise, we move the panel along with the bottom of the section + panel.style.position = 'fixed' + panel.style.top = `${sectionRect.bottom - spacerRect.height}px` + } + continue } - else { - // Otherwise, we move the panel along with the bottom of the section - panel.style.top = `${sectionRect.bottom + scrollY - spacerRect.height}px` + + const shouldKeepPanelVisibleAtTop = spacerRect.top < lastSectionPanelBottom && !(sectionRect.bottom < lastSectionPanelBottom) + if (shouldKeepPanelVisibleAtTop) { + // Keep the panel at the top of the viewport if the spacer is above the viewport + // and the section is still visible + panel.style.position = 'fixed' + panel.style.top = `${lastSectionPanelBottom}px` + lastSectionPanelBottom += spacerRect.height + continue } - continue - } - const shouldKeepPanelVisibleAtTop = spacerRect.top < lastSectionPanelBottom && !(sectionRect.bottom < lastSectionPanelBottom) - if (shouldKeepPanelVisibleAtTop) { - // Keep the panel at the top of the viewport if the spacer is above the viewport - // and the section is still visible - panel.style.top = `${scrollY + lastSectionPanelBottom}px` - lastSectionPanelBottom = panel.getBoundingClientRect().bottom - continue + // Otherwise, keep the panel at the same position as the spacer + panel.style.position = 'absolute' + panel.style.top = `${spacerRect.top + scrollY}px` + } + finally { + performance.mark(`end-render-section-panel-${sectionId}`) + performance.measure(`SectionAnnotationVisualizer.renderSectionPanel-${sectionId}`, `start-render-section-panel-${sectionId}`, `end-render-section-panel-${sectionId}`) } - - // Otherwise, keep the panel at the same position as the spacer - panel.style.top = `${spacerRect.top + scrollY}px` } + performance.mark('end-render-section-panels') + performance.measure('SectionAnnotationVisualizer.renderSectionPanels', 'start-render-section-panels', 'end-render-section-panels') if (root instanceof HTMLElement) { - root.style.scrollPaddingTop = `${lastSectionPanelBottom - rootRect.top}px` + root.style.scrollPaddingTop = `${lastSectionPanelBottom - rootTop}px` } + + performance.mark('end-ensure-panel-visibility') + performance.measure(`SectionAnnotationVisualizer.ensurePanelVisibility (${reason})`, 'start-ensure-panel-visibility', 'end-ensure-panel-visibility') } private renderSectionGroups(doc: AnnotatedText) { @@ -126,6 +195,7 @@ export class SectionAnnotationVisualizer { } // Create an annotation panel for each section + performance.mark('start-create-annotation-panels') const annotationPanelsBySectionElement = new Map() const annotationPanelsByVid = new Map() for (const [vid, sectionElement] of sectionElements) { @@ -144,8 +214,11 @@ export class SectionAnnotationVisualizer { annotationPanelsByVid.set(vid, panel) } + performance.mark('end-create-annotation-panels') + performance.measure('SectionAnnotationVisualizer.createAnnotationPanels', 'start-create-annotation-panels', 'end-create-annotation-panels') - // Render the annotations for each section + // Render the section panels + performance.mark('start-render-section-panels') for (const vid of highlightsByVid.keys()) { const panel = annotationPanelsByVid.get(vid) if (!panel) continue @@ -155,27 +228,44 @@ export class SectionAnnotationVisualizer { panel.appendChild(this.createAnnotationPanelItem(span, selectedAnnotationVids)) } } + performance.mark('end-render-section-panels') + performance.measure('SectionAnnotationVisualizer.renderSectionPanels', 'start-render-section-panels', 'end-render-section-panels') - // Fit the panels to the sections + // Prepare the spacers without changing the DOM so layout due to getBoundingClientRect() is not + // triggered repeatedly + performance.mark('start-create-spacers') + const toProcess: {panel: HTMLElement, spacer: HTMLElement, section: HTMLElement}[] = [] const panels = Array.from(this.root.parentNode?.querySelectorAll('.iaa-visible-annotations-panel') || []) for (const panel of (panels as HTMLElement[])) { const appliesTo = panel.getAttribute('data-iaa-applies-to') if (!appliesTo) continue - const sectionElement = document.getElementById(appliesTo) as HTMLElement - if (!sectionElement) continue + const section = document.getElementById(appliesTo) as HTMLElement + if (!section) continue - const vspace = panel.getBoundingClientRect().height + performance.mark(`start-create-spacer-${appliesTo}`) + // The spacer reserves space for the panel in the document layout. The actual panel + // will then float over the spacer when possible but be adjusted such that it remains + // visible even if the spacer starts moving out of the screen const spacer = document.createElement('div') - spacer.setAttribute('data-iaa-applies-to', sectionElement.id) + spacer.setAttribute('data-iaa-applies-to', section.id) spacer.classList.add('iaa-visible-annotations-panel-spacer') - spacer.style.height = `${vspace}px` - sectionElement.parentElement?.insertBefore(spacer, sectionElement) - - const spacerRect = spacer.getBoundingClientRect() - const scrollY = spacer.ownerDocument.defaultView?.scrollY || 0 - panel.style.top = `${spacerRect.top + scrollY}px` + spacer.style.height = `${panel.getBoundingClientRect().height}px` + toProcess.push({panel, spacer, section}); + performance.mark(`end-create-spacer-${appliesTo}`) + performance.measure(`SectionAnnotationVisualizer.createSpacer-${appliesTo}`, `start-create-spacer-${appliesTo}`, `end-create-spacer-${appliesTo}`) + } + performance.mark('end-create-spacers') + performance.measure('SectionAnnotationVisualizer.createSpacers', 'start-create-spacers', 'end-create-spacers') + + // Add the spacers to the DOM all at once without triggering a re-layout in between + performance.mark('start-insert-spacers') + for (const parts of toProcess) { + const section = parts.section + section.parentElement?.insertBefore(parts.spacer, section) } + performance.mark('end-insert-spacers') + performance.measure('SectionAnnotationVisualizer.insertSpacers', 'start-insert-spacers', 'end-insert-spacers') } private groupHighlightsByVid(spans: NodeListOf) { @@ -266,4 +356,18 @@ export class SectionAnnotationVisualizer { console.error('Parent element of root element not found - cannot add visible annotations panel') } } -} \ No newline at end of file +} + +export function getTop(root) { + if (root instanceof HTMLElement) { + return (root.offsetTop || 0) + } else { + const rootRect = root.getBoundingClientRect() + return rootRect.top + } +} + +export function getScrollY(root) { + return (root.scrollTop || 0) - getTop(root) +} + diff --git a/inception/inception-html-editor/pom.xml b/inception/inception-html-editor/pom.xml index c72bc00d1dd..460cd3188b8 100644 --- a/inception/inception-html-editor/pom.xml +++ b/inception/inception-html-editor/pom.xml @@ -58,10 +58,6 @@ de.tudarmstadt.ukp.inception.app inception-support - - de.tudarmstadt.ukp.inception.app - inception-diam - de.tudarmstadt.ukp.inception.app inception-external-editor diff --git a/inception/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/annotatorjs/AnnotatorJsHtmlAnnotationEditor.java b/inception/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/annotatorjs/AnnotatorJsHtmlAnnotationEditor.java index c6568785a5f..0087dd0df91 100644 --- a/inception/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/annotatorjs/AnnotatorJsHtmlAnnotationEditor.java +++ b/inception/inception-html-editor/src/main/java/de/tudarmstadt/ukp/inception/annotatorjs/AnnotatorJsHtmlAnnotationEditor.java @@ -66,11 +66,10 @@ protected Component makeView() @Override protected AnnotationEditorProperties getProperties() { - var props = new AnnotationEditorProperties(); + var props = super.getProperties(); // The factory is the JS call. Cf. the "globalName" in build.js and the factory method // defined in main.ts props.setEditorFactory("AnnotatorJsEditor.factory()"); - props.setDiamAjaxCallbackUrl(getDiamBehavior().getCallbackUrl().toString()); props.setStylesheetSources( asList(referenceToUrl(servletContext, AnnotatorJsCssResourceReference.get()))); props.setScriptSources(asList( diff --git a/inception/inception-html-recogito-editor/src/main/java/de/tudarmstadt/ukp/inception/recogitojseditor/RecogitoHtmlAnnotationEditor.java b/inception/inception-html-recogito-editor/src/main/java/de/tudarmstadt/ukp/inception/recogitojseditor/RecogitoHtmlAnnotationEditor.java index 19126ba1684..70edefd527f 100644 --- a/inception/inception-html-recogito-editor/src/main/java/de/tudarmstadt/ukp/inception/recogitojseditor/RecogitoHtmlAnnotationEditor.java +++ b/inception/inception-html-recogito-editor/src/main/java/de/tudarmstadt/ukp/inception/recogitojseditor/RecogitoHtmlAnnotationEditor.java @@ -32,7 +32,6 @@ import de.tudarmstadt.ukp.inception.editor.view.DocumentViewFactory; import de.tudarmstadt.ukp.inception.externaleditor.ExternalAnnotationEditorBase; import de.tudarmstadt.ukp.inception.externaleditor.model.AnnotationEditorProperties; -import de.tudarmstadt.ukp.inception.preferences.ClientSideUserPreferencesProvider; import de.tudarmstadt.ukp.inception.recogitojseditor.resources.RecogitoJsCssResourceReference; import de.tudarmstadt.ukp.inception.recogitojseditor.resources.RecogitoJsJavascriptResourceReference; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; @@ -67,16 +66,10 @@ protected Component makeView() @Override protected AnnotationEditorProperties getProperties() { - AnnotationEditorProperties props = new AnnotationEditorProperties(); + var props = super.getProperties(); // The factory is the JS call. Cf. the "globalName" in build.js and the factory method // defined in main.ts props.setEditorFactory("RecogitoEditor.factory()"); - props.setEditorFactoryId(getFactory().getBeanName()); - if (getFactory() instanceof ClientSideUserPreferencesProvider) { - ((ClientSideUserPreferencesProvider) getFactory()).getUserPreferencesKey() - .ifPresent(key -> props.setUserPreferencesKey(key.getClientSideKey())); - } - props.setDiamAjaxCallbackUrl(getDiamBehavior().getCallbackUrl().toString()); props.setStylesheetSources( asList(referenceToUrl(servletContext, RecogitoJsCssResourceReference.get()))); props.setScriptSources(asList( diff --git a/inception/inception-imls-azureai-openai/pom.xml b/inception/inception-imls-azureai-openai/pom.xml index bc0855ac86f..51e6a19764f 100644 --- a/inception/inception-imls-azureai-openai/pom.xml +++ b/inception/inception-imls-azureai-openai/pom.xml @@ -29,7 +29,7 @@ de.tudarmstadt.ukp.inception.app - inception-imls-support-llm + inception-imls-llm-support de.tudarmstadt.ukp.inception.app @@ -43,10 +43,6 @@ de.tudarmstadt.ukp.inception.app inception-recommendation-api - - de.tudarmstadt.ukp.inception.app - inception-api-render - de.tudarmstadt.ukp.inception.app inception-api-annotation @@ -59,6 +55,10 @@ de.tudarmstadt.ukp.inception.app inception-support + + de.tudarmstadt.ukp.inception.app + inception-schema-api + org.apache.commons @@ -102,10 +102,6 @@ jdk-serializable-functional - - com.fasterxml.jackson.core - jackson-core - com.fasterxml.jackson.core jackson-annotations diff --git a/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommender.java b/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommender.java deleted file mode 100644 index 1f6a7027b61..00000000000 --- a/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommender.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Technische Universität Darmstadt under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The Technische Universität Darmstadt - * licenses this file to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.tudarmstadt.ukp.inception.recommendation.imls.azureaiopenai; - -import static de.tudarmstadt.ukp.inception.recommendation.imls.support.llm.prompt.PromptContextGenerator.VAR_EXAMPLES; -import static de.tudarmstadt.ukp.inception.recommendation.imls.support.llm.prompt.PromptContextGenerator.getPromptContextGenerator; -import static de.tudarmstadt.ukp.inception.recommendation.imls.support.llm.response.ResponseExtractor.getResponseExtractor; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.Map; - -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.uima.cas.CAS; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.NonTrainableRecommenderEngineImplBase; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; -import de.tudarmstadt.ukp.inception.recommendation.imls.azureaiopenai.client.AzureAiOpenAiClient; -import de.tudarmstadt.ukp.inception.recommendation.imls.azureaiopenai.client.ChatCompletionRequest; -import de.tudarmstadt.ukp.inception.recommendation.imls.support.llm.prompt.JinjaPromptRenderer; -import de.tudarmstadt.ukp.inception.rendering.model.Range; -import de.tudarmstadt.ukp.inception.security.client.auth.apikey.ApiKeyAuthenticationTraits; -import de.tudarmstadt.ukp.inception.support.logging.LogMessage; - -public class AzureAiOpenAiRecommender - extends NonTrainableRecommenderEngineImplBase -{ - private static final int MAX_FEW_SHOT_EXAMPLES = 10; - - private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final AzureAiOpenAiRecommenderTraits traits; - - private final AzureAiOpenAiClient client; - private final JinjaPromptRenderer promptRenderer; - - public AzureAiOpenAiRecommender(Recommender aRecommender, - AzureAiOpenAiRecommenderTraits aTraits, AzureAiOpenAiClient aClient) - { - super(aRecommender); - - traits = aTraits; - client = aClient; - promptRenderer = new JinjaPromptRenderer(); - } - - @Override - public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) - throws RecommendationException - { - var responseExtractor = getResponseExtractor(traits.getExtractionMode()); - var examples = responseExtractor.generate(this, aCas, MAX_FEW_SHOT_EXAMPLES); - var globalBindings = Map.of(VAR_EXAMPLES, examples); - - getPromptContextGenerator(traits.getPromptingMode()) - .generate(this, aCas, aBegin, aEnd, globalBindings).forEach(promptContext -> { - try { - var prompt = promptRenderer.render(traits.getPrompt(), promptContext); - var response = query(prompt); - responseExtractor.extract(this, aCas, promptContext, response); - } - catch (IOException e) { - aContext.log(LogMessage.warn(getRecommender().getName(), - "Azure AI OpenAI failed to respond: %s", - ExceptionUtils.getRootCauseMessage(e))); - LOG.error("Azure AI OpenAI failed to respond: {}", - ExceptionUtils.getRootCauseMessage(e)); - } - }); - - return new Range(aBegin, aEnd); - } - - private String query(String aPrompt) throws IOException - { - LOG.trace("Querying Azure AI OpenAI: [{}]", aPrompt); - var request = ChatCompletionRequest.builder() // - .withApiKey(((ApiKeyAuthenticationTraits) traits.getAuthentication()).getApiKey()) // - .withPrompt(aPrompt) // - .withFormat(traits.getFormat()) // - .build(); - var response = client.generate(traits.getUrl(), request).trim(); - LOG.trace("Azure AI OpenAI responds: [{}]", response); - return response; - } -} diff --git a/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommenderTraitsEditor.html b/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommenderTraitsEditor.html deleted file mode 100644 index eca569803ad..00000000000 --- a/inception/inception-imls-azureai-openai/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/azureaiopenai/AzureAiOpenAiRecommenderTraitsEditor.html +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - -
      - -
      - -
      -
      -
      - -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      -
      - -
      -
      -
      - -
      - -
      -
      -
      -