diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java index e3dae8fc881..f749bb3b4de 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java @@ -233,9 +233,9 @@ public boolean suppressAutoFocus(AnnotationFeature aFeature) @Override public List lookupLazyDetails(AnnotationFeature aFeature, Object aValue) { - if (aValue instanceof String) { - var value = (String) aValue; + if (aValue instanceof String value) { var tag = schemaService.getTag(value, aFeature.getTagset()); + if (isNotBlank(value) && aFeature.getTagset() != null && tag.isEmpty()) { return asList(new VLazyDetailGroup(new VLazyDetail(value, "Tag not in tagset"))); } 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 bbb997c1fde..a5c45d5b326 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 @@ -252,8 +252,7 @@ public List render(VDocument aVDocument, AnnotationFS aFS, } @Override - public List lookupLazyDetails(CAS aCas, VID aVid, int aWindowBeginOffset, - int aWindowEndOffset) + public List lookupLazyDetails(CAS aCas, VID aVid) { if (!checkTypeSystem(aCas)) { return Collections.emptyList(); @@ -278,7 +277,7 @@ public List lookupLazyDetails(CAS aCas, VID aVid, int aWindowB renderYield(fs).ifPresent( yield -> group.addDetail(new VLazyDetail("Yield", abbreviate(yield, "...", 300)))); - var details = super.lookupLazyDetails(aCas, aVid, aWindowBeginOffset, aWindowEndOffset); + var details = super.lookupLazyDetails(aCas, aVid); if (!group.getDetails().isEmpty()) { details.add(0, group); } 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 62ce6cf4cd2..47dbb517f4c 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 @@ -99,8 +99,7 @@ public List selectAnnotationsInWindow(CAS aCas, int aWindowBegin, } @Override - public List lookupLazyDetails(CAS aCas, VID aVid, int aWindowBeginOffset, - int aWindowEndOffset) + public List lookupLazyDetails(CAS aCas, VID aVid) { if (!checkTypeSystem(aCas)) { return Collections.emptyList(); @@ -112,7 +111,7 @@ public List lookupLazyDetails(CAS aCas, VID aVid, int aWindowB group.addDetail( new VLazyDetail("Text", abbreviate(fs.getCoveredText(), MAX_HOVER_TEXT_LENGTH))); - var details = super.lookupLazyDetails(aCas, aVid, aWindowBeginOffset, aWindowEndOffset); + var details = super.lookupLazyDetails(aCas, aVid); if (!group.getDetails().isEmpty()) { details.add(0, group); } 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 c96b308ec4d..128ca153a4a 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 @@ -27,6 +27,7 @@ import com.googlecode.wicket.jquery.ui.widget.menu.IMenuItem; 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.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; @@ -79,8 +80,8 @@ default void generateContextMenuItems(List aItems) // Do nothing by default } - default List lookupLazyDetails(SourceDocument aDocument, User aUser, VID aVid, - AnnotationFeature aFeature) + default List lookupLazyDetails(SourceDocument aDocument, User aUser, CAS aCas, + VID aVid, AnnotationLayer aLayer) { return Collections.emptyList(); } 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 92192041bc6..b8a6e3d99a1 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 @@ -58,17 +58,14 @@ 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; 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.internal.AID; 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.curation.casdiff.span.SpanDiffAdapter; 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.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -930,9 +927,21 @@ public ConfigurationSet getConfigurationSet(Position aPosition) public Optional findConfiguration(String aRepresentativeCasGroupId, AID aAid) { - for (ConfigurationSet cfgSet : getConfigurationSets()) { - Optional cfg = cfgSet.findConfiguration(aRepresentativeCasGroupId, - aAid); + for (var cfgSet : getConfigurationSets()) { + var cfg = cfgSet.findConfiguration(aRepresentativeCasGroupId, aAid); + if (cfg.isPresent()) { + return cfg; + } + } + + return Optional.empty(); + } + + public Optional findConfiguration(String aRepresentativeCasGroupId, + FeatureStructure aFS) + { + for (var cfgSet : getConfigurationSets()) { + var cfg = cfgSet.findConfiguration(aRepresentativeCasGroupId, aFS); if (cfg.isPresent()) { return cfg; } @@ -1139,18 +1148,18 @@ public static List getDiffAdapters(AnnotationSchemaService schemaSe return emptyList(); } - Project project = aLayers.iterator().next().getProject(); + var project = aLayers.iterator().next().getProject(); var featuresByLayer = schemaService.listSupportedFeatures(project).stream() // .collect(groupingBy(AnnotationFeature::getLayer)); - List adapters = new ArrayList<>(); + var adapters = new ArrayList(); nextLayer: for (AnnotationLayer layer : aLayers) { if (!layer.isEnabled()) { continue nextLayer; } - Set labelFeatures = new LinkedHashSet<>(); + var labelFeatures = new LinkedHashSet(); nextFeature: for (var f : featuresByLayer.getOrDefault(layer, emptyList())) { if (!f.isEnabled() || !f.isCuratable()) { continue nextFeature; 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 d2aaf7dfb59..2d3035cae42 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 @@ -31,7 +31,6 @@ import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.selectSentences; import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.selectTokens; import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.apache.uima.fit.util.CasUtil.getType; import static org.apache.uima.fit.util.CasUtil.selectAt; @@ -53,7 +52,6 @@ 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 org.apache.uima.fit.util.CasUtil; import org.apache.uima.fit.util.FSUtil; @@ -68,7 +66,6 @@ import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.ConfigurationSet; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.DiffResult; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.internal.AID; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.relation.RelationPosition; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanPosition; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; @@ -207,9 +204,9 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume { silenceEvents = true; - int updated = 0; - int created = 0; - Set messages = new LinkedHashSet<>(); + var updated = 0; + var created = 0; + var messages = new LinkedHashSet(); // Remove any annotations from the target CAS - keep type system, sentences and tokens clearAnnotations(aTargetDocument.getProject(), aTargetCas); @@ -221,13 +218,12 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume // Set up a cache for resolving type to layer to avoid hammering the DB as we process each // position - Map type2layer = aDiff.getPositions().stream() - .map(Position::getType) // + var type2layer = aDiff.getPositions().stream().map(Position::getType) // .distinct() // .map(type -> schemaService.findLayer(aTargetDocument.getProject(), type)) .collect(toMap(AnnotationLayer::getName, identity())); - List layerNames = new ArrayList<>(type2layer.keySet()); + var layerNames = new ArrayList<>(type2layer.keySet()); // Move token layer to front if (layerNames.contains(Token.class.getName())) { @@ -245,14 +241,14 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume // 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) - for (String layerName : layerNames) { - List positions = aDiff.getPositions().stream() + for (var layerName : layerNames) { + var positions = 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) // - .collect(toList()); + .toList(); if (positions.isEmpty()) { continue; @@ -300,14 +296,14 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume } // After the spans are in place, we can merge the slot features - for (String layerName : layerNames) { - List positions = aDiff.getPositions().stream() + for (var layerName : layerNames) { + var positions = 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) // - .collect(Collectors.toList()); + .toList(); if (positions.isEmpty()) { continue; @@ -315,7 +311,7 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume LOG.debug("Processing {} slot positions on layer [{}]", positions.size(), layerName); - for (SpanPosition position : positions) { + for (var position : positions) { LOG.trace(" | processing {}", position); var layer = type2layer.get(position.getType()); var cfgs = aDiff.getConfigurationSet(position); @@ -328,9 +324,8 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume for (var cfgToMerge : cfgsToMerge) { try { - AnnotationFS sourceFS = (AnnotationFS) cfgToMerge - .getRepresentative(aCasMap); - AID sourceFsAid = cfgs.getConfigurations().get(0).getRepresentativeAID(); + var sourceFS = (AnnotationFS) cfgToMerge.getRepresentative(aCasMap); + var sourceFsAid = cfgs.getConfigurations().get(0).getRepresentativeAID(); mergeSlotFeature(aTargetDocument, aTargetUsername, type2layer.get(position.getType()), aTargetCas, sourceFS, sourceFsAid.feature, sourceFsAid.index); @@ -345,8 +340,8 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume } // Finally, we merge the relations - for (String layerName : layerNames) { - List positions = aDiff.getPositions().stream() + for (var layerName : layerNames) { + var positions = aDiff.getPositions().stream() .filter(pos -> layerName.equals(pos.getType())) .filter(pos -> pos instanceof RelationPosition) .map(pos -> (RelationPosition) pos) // @@ -420,7 +415,7 @@ public Set reMergeCas(DiffResult aDiff, SourceDocument aTargetDocume private void clearAnnotations(Project aProject, CAS aCas) throws UIMAException { // Copy the CAS - basically we do this just to keep the full type system information - CAS backup = WebAnnoCasUtil.createCasCopy(aCas); + var backup = WebAnnoCasUtil.createCasCopy(aCas); // Remove all annotations from the target CAS but we keep the type system! aCas.reset(); @@ -432,6 +427,7 @@ private void clearAnnotations(Project aProject, CAS aCas) throws UIMAException else { createDocumentMetadata(aCas); } + aCas.setDocumentLanguage(backup.getDocumentLanguage()); // DKPro Core Issue 435 aCas.setDocumentText(backup.getDocumentText()); @@ -446,14 +442,14 @@ private void transferSegmentation(Project aProject, CAS aCas, CAS backup) { if (!schemaService.isTokenLayerEditable(aProject)) { // Transfer token boundaries - for (AnnotationFS t : selectTokens(backup)) { + for (var t : selectTokens(backup)) { aCas.addFsToIndexes(createToken(aCas, t.getBegin(), t.getEnd())); } } if (!schemaService.isSentenceLayerEditable(aProject)) { // Transfer sentence boundaries - for (AnnotationFS s : selectSentences(backup)) { + for (var s : selectSentences(backup)) { aCas.addFsToIndexes(createSentence(aCas, s.getBegin(), s.getEnd())); } } @@ -461,7 +457,7 @@ private void transferSegmentation(Project aProject, CAS aCas, CAS backup) private static boolean existsEquivalentAt(CAS aCas, TypeAdapter aAdapter, AnnotationFS aFs) { - Type targetType = CasUtil.getType(aCas, aFs.getType().getName()); + var targetType = CasUtil.getType(aCas, aFs.getType().getName()); return selectAt(aCas, targetType, aFs.getBegin(), aFs.getEnd()).stream() // .filter(cand -> aAdapter.equivalents(aFs, cand, (_fs, _f) -> !shouldIgnoreFeatureOnMerge(_f))) // @@ -472,15 +468,14 @@ private static boolean existsEquivalentAt(CAS aCas, TypeAdapter aAdapter, Annota private static List selectCandidateRelationsAt(CAS aTargetCas, AnnotationFS aSourceFs, AnnotationFS aSourceOriginFs, AnnotationFS aSourceTargetFs) { - Type type = aSourceFs.getType(); - Type targetType = CasUtil.getType(aTargetCas, aSourceFs.getType().getName()); - Feature sourceFeat = type.getFeatureByBaseName(FEAT_REL_SOURCE); - Feature targetFeat = type.getFeatureByBaseName(FEAT_REL_TARGET); + var type = aSourceFs.getType(); + var targetType = CasUtil.getType(aTargetCas, aSourceFs.getType().getName()); + var sourceFeat = type.getFeatureByBaseName(FEAT_REL_SOURCE); + var targetFeat = type.getFeatureByBaseName(FEAT_REL_TARGET); return selectCovered(aTargetCas, targetType, aSourceFs.getBegin(), aSourceFs.getEnd()) - .stream() - .filter(fs -> fs.getFeatureValue(sourceFeat).equals(aSourceOriginFs) + .stream().filter(fs -> fs.getFeatureValue(sourceFeat).equals(aSourceOriginFs) && fs.getFeatureValue(targetFeat).equals(aSourceTargetFs)) - .collect(toList()); + .toList(); } private void copyFeatures(SourceDocument aDocument, String aUsername, TypeAdapter aAdapter, @@ -488,15 +483,15 @@ private void copyFeatures(SourceDocument aDocument, String aUsername, TypeAdapte throws AnnotationException { // Cache the feature list instead of hammering the database - List features = featureCache.computeIfAbsent(aAdapter.getLayer(), + var features = featureCache.computeIfAbsent(aAdapter.getLayer(), key -> schemaService.listSupportedFeatures(key)); - for (AnnotationFeature feature : features) { + for (var feature : features) { if (!feature.isCuratable()) { continue; } - Type sourceFsType = aAdapter.getAnnotationType(aSourceFs.getCAS()); - Feature sourceFeature = sourceFsType.getFeatureByBaseName(feature.getName()); + var sourceFsType = aAdapter.getAnnotationType(aSourceFs.getCAS()); + var sourceFeature = sourceFsType.getFeatureByBaseName(feature.getName()); if (sourceFeature == null) { throw new IllegalStateException("Target CAS type [" + sourceFsType.getName() @@ -507,7 +502,7 @@ private void copyFeatures(SourceDocument aDocument, String aUsername, TypeAdapte continue; } - Object value = aAdapter.getFeatureValue(feature, aSourceFs); + var value = aAdapter.getFeatureValue(feature, aSourceFs); try { aAdapter.setFeatureValue(aDocument, aUsername, aTargetFS.getCAS(), @@ -525,11 +520,11 @@ private void copyFeatures(SourceDocument aDocument, String aUsername, TypeAdapte private static List getCandidateAnnotations(CAS aTargetCas, TypeAdapter aAdapter, AnnotationFS aSource) { - Type targetType = CasUtil.getType(aTargetCas, aSource.getType().getName()); + var targetType = CasUtil.getType(aTargetCas, aSource.getType().getName()); return selectCovered(aTargetCas, targetType, aSource.getBegin(), aSource.getEnd()).stream() .filter(fs -> aAdapter.equivalents(fs, aSource, (_fs, _f) -> !shouldIgnoreFeatureOnMerge(_f))) - .collect(toList()); + .toList(); } public CasMergeOperationResult mergeSpanAnnotation(SourceDocument aDocument, String aUsername, @@ -548,7 +543,7 @@ public CasMergeOperationResult mergeSpanAnnotation(SourceDocument aDocument, Str } // a) if stacking allowed add this new annotation to the mergeview - Type targetType = CasUtil.getType(aTargetCas, adapter.getAnnotationTypeName()); + var targetType = CasUtil.getType(aTargetCas, adapter.getAnnotationTypeName()); var existingAnnos = selectAt(aTargetCas, targetType, aSourceFs.getBegin(), aSourceFs.getEnd()); if (existingAnnos.isEmpty() || aAllowStacking) { @@ -557,7 +552,7 @@ public CasMergeOperationResult mergeSpanAnnotation(SourceDocument aDocument, Str var mergedSpan = adapter.add(aDocument, aUsername, aTargetCas, aSourceFs.getBegin(), aSourceFs.getEnd()); - int mergedSpanAddr = -1; + var mergedSpanAddr = -1; try { copyFeatures(aDocument, aUsername, adapter, mergedSpan, aSourceFs); mergedSpanAddr = getAddr(mergedSpan); @@ -573,9 +568,9 @@ public CasMergeOperationResult mergeSpanAnnotation(SourceDocument aDocument, Str } // b) if stacking is not allowed, modify the existing annotation with this one else { - AnnotationFS annoToUpdate = existingAnnos.get(0); + var annoToUpdate = existingAnnos.get(0); copyFeatures(aDocument, aUsername, adapter, annoToUpdate, aSourceFs); - int mergedSpanAddr = getAddr(annoToUpdate); + var mergedSpanAddr = getAddr(annoToUpdate); return new CasMergeOperationResult(CasMergeOperationResult.ResultState.UPDATED, mergedSpanAddr); } @@ -586,7 +581,7 @@ public CasMergeOperationResult mergeRelationAnnotation(SourceDocument aDocument, AnnotationFS aSourceFs, boolean aAllowStacking) throws AnnotationException { - RelationAdapter relationAdapter = (RelationAdapter) adapterCache.get(aAnnotationLayer); + var relationAdapter = (RelationAdapter) adapterCache.get(aAnnotationLayer); if (silenceEvents) { relationAdapter.silenceEvents(); } @@ -596,12 +591,12 @@ public CasMergeOperationResult mergeRelationAnnotation(SourceDocument aDocument, "The annotation already exists in the target document."); } - AnnotationFS originFsClicked = getFeature(aSourceFs, relationAdapter.getSourceFeatureName(), + var originFsClicked = getFeature(aSourceFs, relationAdapter.getSourceFeatureName(), AnnotationFS.class); - AnnotationFS targetFsClicked = getFeature(aSourceFs, relationAdapter.getTargetFeatureName(), + var targetFsClicked = getFeature(aSourceFs, relationAdapter.getTargetFeatureName(), AnnotationFS.class); - SpanAdapter spanAdapter = (SpanAdapter) adapterCache.get(aAnnotationLayer.getAttachType()); + var spanAdapter = (SpanAdapter) adapterCache.get(aAnnotationLayer.getAttachType()); var candidateOrigins = getCandidateAnnotations(aTargetCas, spanAdapter, originFsClicked); var candidateTargets = getCandidateAnnotations(aTargetCas, spanAdapter, targetFsClicked); @@ -622,13 +617,13 @@ public CasMergeOperationResult mergeRelationAnnotation(SourceDocument aDocument, "Stacked targets exist in the target document. Cannot merge this relation."); } - AnnotationFS originFs = candidateOrigins.get(0); - AnnotationFS targetFs = candidateTargets.get(0); + var originFs = candidateOrigins.get(0); + var targetFs = candidateTargets.get(0); if (relationAdapter.getAttachFeatureName() != null) { - AnnotationFS originAttachAnnotation = FSUtil.getFeature(originFs, + var originAttachAnnotation = FSUtil.getFeature(originFs, relationAdapter.getAttachFeatureName(), AnnotationFS.class); - AnnotationFS targetAttachAnnotation = FSUtil.getFeature(targetFs, + var targetAttachAnnotation = FSUtil.getFeature(targetFs, relationAdapter.getAttachFeatureName(), AnnotationFS.class); if (originAttachAnnotation == null || targetAttachAnnotation == null) { @@ -637,11 +632,10 @@ public CasMergeOperationResult mergeRelationAnnotation(SourceDocument aDocument, } } - List existingAnnos = selectCandidateRelationsAt(aTargetCas, aSourceFs, - originFs, targetFs); + var existingAnnos = selectCandidateRelationsAt(aTargetCas, aSourceFs, originFs, targetFs); if (existingAnnos.isEmpty() || aAllowStacking) { - AnnotationFS mergedRelation = relationAdapter.add(aDocument, aUsername, originFs, - targetFs, aTargetCas); + var mergedRelation = relationAdapter.add(aDocument, aUsername, originFs, targetFs, + aTargetCas); try { copyFeatures(aDocument, aUsername, relationAdapter, mergedRelation, aSourceFs); } @@ -654,7 +648,7 @@ public CasMergeOperationResult mergeRelationAnnotation(SourceDocument aDocument, getAddr(mergedRelation)); } else { - AnnotationFS mergeTargetFS = existingAnnos.get(0); + var mergeTargetFS = existingAnnos.get(0); copyFeatures(aDocument, aUsername, relationAdapter, mergeTargetFS, aSourceFs); return new CasMergeOperationResult(CasMergeOperationResult.ResultState.UPDATED, getAddr(mergeTargetFS)); @@ -666,12 +660,12 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String String aSourceFeature, int aSourceSlotIndex) throws AnnotationException { - TypeAdapter adapter = adapterCache.get(aAnnotationLayer); + var adapter = adapterCache.get(aAnnotationLayer); if (silenceEvents) { adapter.silenceEvents(); } - List candidateHosts = getCandidateAnnotations(aTargetCas, adapter, aSourceFs); + var candidateHosts = getCandidateAnnotations(aTargetCas, adapter, aSourceFs); if (candidateHosts.size() == 0) { throw new UnfulfilledPrerequisitesException( @@ -679,7 +673,7 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String + aSourceFs.getBegin() + "," + aSourceFs.getEnd() + "] into which the link could be merged. Please add one first."); } - AnnotationFS mergeFs = candidateHosts.get(0); + var mergeFs = candidateHosts.get(0); int liIndex = aSourceSlotIndex; var slotFeature = adapter.listFeatures().stream() // @@ -693,14 +687,14 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String } List sourceLinks = adapter.getFeatureValue(slotFeature, aSourceFs); - List targets = checkAndGetTargets(aTargetCas, + var targets = checkAndGetTargets(aTargetCas, selectAnnotationByAddr(aSourceFs.getCAS(), sourceLinks.get(liIndex).targetAddr)); if (targets.isEmpty()) { throw new AnnotationException("No suitable merge target found"); } - LinkWithRoleModel newLink = new LinkWithRoleModel(sourceLinks.get(liIndex)); + var newLink = new LinkWithRoleModel(sourceLinks.get(liIndex)); newLink.targetAddr = getAddr(targets.get(0)); List links = adapter.getFeatureValue(slotFeature, mergeFs); @@ -720,7 +714,7 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String } } else { - LinkWithRoleModel existing = existingLinkWithTarget(newLink, links); + var existing = existingLinkWithTarget(newLink, links); if (existing != null && existing.equals(newLink)) { throw new AlreadyMergedException( "The slot has already been filled with this annotation in the target document."); @@ -743,7 +737,7 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String private LinkWithRoleModel existingLinkWithTarget(LinkWithRoleModel aLink, List aLinks) { - for (LinkWithRoleModel lr : aLinks) { + for (var lr : aLinks) { if (lr.targetAddr == aLink.targetAddr) { return lr; } @@ -754,14 +748,12 @@ private LinkWithRoleModel existingLinkWithTarget(LinkWithRoleModel aLink, private static List checkAndGetTargets(CAS aCas, AnnotationFS aOldTarget) throws UnfulfilledPrerequisitesException { - Type casType = CasUtil.getType(aCas, aOldTarget.getType().getName()); - List targets = selectCovered(aCas, casType, aOldTarget.getBegin(), - aOldTarget.getEnd()) - .stream() - .filter(fs -> AnnotationComparisonUtils.isEquivalentSpanAnnotation(fs, - aOldTarget, - (FeatureFilter) (_fs, _f) -> !shouldIgnoreFeatureOnMerge(_f))) - .collect(Collectors.toList()); + var casType = CasUtil.getType(aCas, aOldTarget.getType().getName()); + var targets = selectCovered(aCas, casType, aOldTarget.getBegin(), aOldTarget.getEnd()) + .stream() + .filter(fs -> AnnotationComparisonUtils.isEquivalentSpanAnnotation(fs, aOldTarget, + (FeatureFilter) (_fs, _f) -> !shouldIgnoreFeatureOnMerge(_f))) + .collect(Collectors.toList()); if (targets.size() == 0) { throw new UnfulfilledPrerequisitesException( diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupService.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupService.java index 3bdf35b449d..e9e1b8ee304 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupService.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupService.java @@ -20,12 +20,16 @@ import java.io.IOException; import java.util.List; +import org.apache.uima.cas.CAS; import org.apache.wicket.request.IRequestParameters; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; +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.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VLazyDetailGroup; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; public interface LazyDetailsLookupService @@ -35,4 +39,12 @@ List lookupLazyDetails(IRequestParameters request, VID paramId, int windowEndOffset) throws AnnotationException, IOException; + List lookupAnnotationLevelDetails(VID aVid, SourceDocument aDocument, + User aUser, AnnotationLayer aLayer, CAS aCas) + throws AnnotationException, IOException; + + List lookupFeatureLevelDetails(VID aVid, CAS aCas, + AnnotationFeature aFeature); + + List lookupLayerLevelDetails(VID aVid, CAS aCas, AnnotationLayer aLayer); } diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java index 492e5664c1a..3005010028a 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java @@ -35,7 +35,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -88,37 +87,59 @@ public List lookupLazyDetails(IRequestParameters request, VID a var cas = aCas.get(); - var detailGroups = new ArrayList(); - if (isSentence(cas, aVid)) { - var fs = selectFsByAddr(cas, aVid.getId()); - var id = FSUtil.getFeature(fs, Sentence._FeatName_id, String.class); - if (StringUtils.isNotBlank(id)) { - var group = new VLazyDetailGroup(); - group.addDetail(new VLazyDetail(Sentence._FeatName_id, id)); - detailGroups.add(group); - } + var detailGroups = lookLazyDetails(aVid, aDocument, aUser, layerParam, cas); + + return detailGroups.stream() // + .map(this::toExternalForm) // + .collect(toList()); + } + + private List lookLazyDetails(VID aVid, SourceDocument aDocument, User aUser, + StringValue aLayerParam, CAS aCas) + throws AnnotationException, IOException + { + if (isSentence(aCas, aVid)) { + return lookupSentenceLevelLazyDetails(aVid, aCas); } - else { - var layer = findLayer(aVid, cas, layerParam, aDocument.getProject()); - lookupLayerLevelDetails(aVid, cas, windowBeginOffset, windowEndOffset, layer) - .forEach(detailGroups::add); + var layer = findLayer(aVid, aCas, aLayerParam, aDocument.getProject()); + return lookupAnnotationLevelDetails(aVid, aDocument, aUser, layer, aCas); + } + + @Override + public List lookupAnnotationLevelDetails(VID aVid, SourceDocument aDocument, + User aUser, AnnotationLayer aLayer, CAS aCas) + throws AnnotationException, IOException + { + var detailGroups = new ArrayList(); - for (var feature : annotationService.listAnnotationFeature(layer)) { - lookupExtensionLevelDetails(aVid, aDocument, cas, aUser, feature) - .forEach(detailGroups::add); + lookupLayerLevelDetails(aVid, aCas, aLayer).forEach(detailGroups::add); - // FIXME: We would like to get feature-level lazy details for the annotation label - // provided by the extension or said otherwise, we want to e.g. get KB details for a - // concept feature suggestion... this worked when we used the "query", but now is - // broken! - lookupFeatureLevelDetail(aVid, cas, feature).forEach(detailGroups::add); + if (aVid.isSynthetic()) { + lookupExtensionLevelDetails(aVid, aDocument, aCas, aUser, aLayer) + .forEach(detailGroups::add); + } + else { + for (var feature : annotationService.listAnnotationFeature(aLayer)) { + lookupFeatureLevelDetails(aVid, aCas, feature).forEach(detailGroups::add); } } - return detailGroups.stream() // - .map(this::toExternalForm) // - .collect(toList()); + return detailGroups; + } + + private List lookupSentenceLevelLazyDetails(VID aVid, CAS cas) + { + var detailGroups = new ArrayList(); + var fs = selectFsByAddr(cas, aVid.getId()); + var id = FSUtil.getFeature(fs, Sentence._FeatName_id, String.class); + if (StringUtils.isNotBlank(id)) { + var group = new VLazyDetailGroup(); + group.addDetail(new VLazyDetail(Sentence._FeatName_id, id)); + detailGroups.add(group); + } + + return detailGroups; } private boolean isSentence(CAS aCas, VID aVid) @@ -154,7 +175,8 @@ private AnnotationLayer findLayer(VID aVid, CAS aCas, StringValue aLayerParam, P return annotationService.findLayer(project, fs); } - private List lookupFeatureLevelDetail(VID aVid, CAS aCas, + @Override + public List lookupFeatureLevelDetails(VID aVid, CAS aCas, AnnotationFeature aFeature) { if (aVid.isSynthetic()) { @@ -166,8 +188,9 @@ private List lookupFeatureLevelDetail(VID aVid, CAS aCas, return ext.lookupLazyDetails(aFeature, ext.getFeatureValue(aFeature, fs)); } - private List lookupLayerLevelDetails(VID aVid, CAS aCas, - int windowBeginOffset, int windowEndOffset, AnnotationLayer aLayer) + @Override + public List lookupLayerLevelDetails(VID aVid, CAS aCas, + AnnotationLayer aLayer) { if (aVid.isSynthetic()) { return emptyList(); @@ -175,28 +198,19 @@ private List lookupLayerLevelDetails(VID aVid, CAS aCas, return layerSupportRegistry.getLayerSupport(aLayer) .createRenderer(aLayer, () -> annotationService.listAnnotationFeature(aLayer)) - .lookupLazyDetails(aCas, aVid, windowBeginOffset, windowEndOffset); + .lookupLazyDetails(aCas, aVid); } private List lookupExtensionLevelDetails(VID aVid, SourceDocument aDocument, - CAS aCas, User aUser, AnnotationFeature aFeature) + CAS aCas, User aUser, AnnotationLayer aLayer) throws IOException { if (!aVid.isSynthetic()) { return emptyList(); } - if (aFeature.getLinkMode() == LinkMode.WITH_ROLE) { - return emptyList(); - } - - var result = new ArrayList(); var extension = extensionRegistry.getExtension(aVid.getExtensionId()); - var value = extension.getFeatureValue(aDocument, aUser, aCas, aVid, aFeature); - featureSupportRegistry.findExtension(aFeature).orElseThrow() - .lookupLazyDetails(aFeature, value).forEach(result::add); - extension.lookupLazyDetails(aDocument, aUser, aVid, aFeature).forEach(result::add); - return result; + return extension.lookupLazyDetails(aDocument, aUser, aCas, aVid, aLayer); } } diff --git a/inception/inception-js-api/src/main/ts/src/widget/AnnotationDetailPopOver.svelte b/inception/inception-js-api/src/main/ts/src/widget/AnnotationDetailPopOver.svelte index 6777ad6a10a..ad24cde078b 100644 --- a/inception/inception-js-api/src/main/ts/src/widget/AnnotationDetailPopOver.svelte +++ b/inception/inception-js-api/src/main/ts/src/widget/AnnotationDetailPopOver.svelte @@ -187,14 +187,17 @@
ID: {annotation?.vid}
{#if annotation} -
+
{#if annotation.comments} - {#each annotation.comments as comment} -
{comment.comment}
- {/each} +
+ {#each annotation.comments as comment} +
{comment.comment}
+ {/each} +
{/if} + {#if loading} -
+
Loading... @@ -202,17 +205,21 @@
{:else if detailGroups} - {#each detailGroups as detailGroup} - {#if detailGroup.title} -
{detailGroup.title}
- {/if} - {#each detailGroup.details as detail} -
- {detail.label}: - {detail.value} -
+
    + {#each detailGroups as detailGroup} +
  • + {#if detailGroup.title} +
    {detailGroup.title}
    + {/if} + {#each detailGroup.details as detail} +
    + {detail.label}: + {detail.value} +
    + {/each} +
  • {/each} - {/each} +
{/if}
{/if} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 7db2a572743..d97c5ed22e9 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -46,6 +46,8 @@ import de.tudarmstadt.ukp.clarin.webanno.brat.message.DoActionResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.RejectActionResponse; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -310,8 +312,8 @@ public V getFeatureValue(SourceDocument aDocument, User aUser, CAS aCas, VID } @Override - public List lookupLazyDetails(SourceDocument aDocument, User aUser, VID aVid, - AnnotationFeature aFeature) + public List lookupLazyDetails(SourceDocument aDocument, User aUser, CAS aCas, + VID aVid, AnnotationLayer aLayer) { var predictions = recommendationService.getPredictions(aUser, aDocument.getProject()); @@ -319,46 +321,56 @@ public List lookupLazyDetails(SourceDocument aDocument, User a return emptyList(); } - var vid = VID.parse(aVid.getExtensionPayload()); - var representative = predictions.getPredictionByVID(aDocument, vid); - if (representative.isEmpty() - || !representative.get().getFeature().equals(aFeature.getName())) { - return emptyList(); - } - - var sao = representative.get(); - var group = predictions - .getGroupedPredictions(AnnotationSuggestion.class, aDocument.getName(), - aFeature.getLayer(), sao.getWindowBegin(), sao.getWindowEnd()) - .stream() // - .filter(g -> g.contains(representative.get())) // - .findFirst(); + var detailGroups = new ArrayList(); + for (var aFeature : annotationService.listAnnotationFeature(aLayer)) { + if (aFeature.getLinkMode() == LinkMode.WITH_ROLE) { + return emptyList(); + } - if (group.isEmpty()) { - return emptyList(); - } + var vid = VID.parse(aVid.getExtensionPayload()); + var representative = predictions.getPredictionByVID(aDocument, vid); + if (representative.isEmpty() + || !representative.get().getFeature().equals(aFeature.getName())) { + return emptyList(); + } - var pref = recommendationService.getPreferences(aUser, aDocument.getProject()); - var label = defaultIfBlank(sao.getLabel(), null); - var sortedByScore = group.get().bestSuggestionsByFeatureAndLabel(pref, aFeature.getName(), - label); + var sao = representative.get(); + var group = predictions + .getGroupedPredictions(AnnotationSuggestion.class, aDocument.getName(), + aFeature.getLayer(), sao.getWindowBegin(), sao.getWindowEnd()) + .stream() // + .filter(g -> g.contains(representative.get())) // + .findFirst(); - var detailGroups = new ArrayList(); - for (var ao : sortedByScore) { - var detailGroup = new VLazyDetailGroup(ao.getRecommenderName()); - // detailGroup.addDetail(new VLazyDetail("Age", String.valueOf(ao.getAge()))); - if (ao.getScore() > 0.0d) { - detailGroup - .addDetail(new VLazyDetail("Score", String.format("%.2f", ao.getScore()))); - } - if (ao.getScoreExplanation().isPresent()) { - detailGroup - .addDetail(new VLazyDetail("Explanation", ao.getScoreExplanation().get())); + if (group.isEmpty()) { + return emptyList(); } - if (pref.isShowAllPredictions() && !ao.isVisible()) { - detailGroup.addDetail(new VLazyDetail("Hidden", ao.getReasonForHiding())); + + var pref = recommendationService.getPreferences(aUser, aDocument.getProject()); + var label = defaultIfBlank(sao.getLabel(), null); + var sortedByScore = group.get().bestSuggestionsByFeatureAndLabel(pref, + aFeature.getName(), label); + + var value = getFeatureValue(aDocument, aUser, aCas, aVid, aFeature); + featureSupportRegistry.findExtension(aFeature).orElseThrow() + .lookupLazyDetails(aFeature, value).forEach(detailGroups::add); + + for (var ao : sortedByScore) { + var detailGroup = new VLazyDetailGroup(ao.getRecommenderName()); + // detailGroup.addDetail(new VLazyDetail("Age", String.valueOf(ao.getAge()))); + if (ao.getScore() > 0.0d) { + detailGroup.addDetail( + new VLazyDetail("Score", String.format("%.2f", ao.getScore()))); + } + if (ao.getScoreExplanation().isPresent()) { + detailGroup.addDetail( + new VLazyDetail("Explanation", ao.getScoreExplanation().get())); + } + if (pref.isShowAllPredictions() && !ao.isVisible()) { + detailGroup.addDetail(new VLazyDetail("Hidden", ao.getReasonForHiding())); + } + detailGroups.add(detailGroup); } - detailGroups.add(detailGroup); } return detailGroups; diff --git a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/rendering/Renderer.java b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/rendering/Renderer.java index 551bb6c14b3..811b633c997 100644 --- a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/rendering/Renderer.java +++ b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/rendering/Renderer.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.NoSuchElementException; +import org.apache.commons.lang3.StringUtils; import org.apache.uima.cas.CAS; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; @@ -75,15 +76,15 @@ List render(VDocument aVDocument, AnnotationFS aFS, List renderLabelFeatureValues(TypeAdapter aAdapter, FeatureStructure aFs, List aFeatures) { - FeatureSupportRegistry fsr = getFeatureSupportRegistry(); - Map features = new LinkedHashMap<>(); + var fsr = getFeatureSupportRegistry(); + var features = new LinkedHashMap(); - for (AnnotationFeature feature : aFeatures) { + for (var feature : aFeatures) { if (!feature.isEnabled() || !feature.isVisible()) { continue; } - String label = defaultString(fsr.findExtension(feature) + var label = defaultString(fsr.findExtension(feature) .orElseThrow(() -> new NoSuchElementException( "No feature support found for feature " + feature)) .renderFeatureValue(feature, aFs)); @@ -97,7 +98,7 @@ default Map renderLabelFeatureValues(TypeAdapter aAdapter, Featu default void renderRequiredFeatureErrors(List aFeatures, FeatureStructure aFS, VDocument aResponse) { - for (AnnotationFeature f : aFeatures) { + for (var f : aFeatures) { if (!f.isEnabled()) { continue; } @@ -109,8 +110,7 @@ default void renderRequiredFeatureErrors(List aFeatures, } } - default List lookupLazyDetails(CAS aCas, VID aVid, int windowBeginOffset, - int windowEndOffset) + default List lookupLazyDetails(CAS aCas, VID aVid) { var fsr = getFeatureSupportRegistry(); @@ -130,12 +130,13 @@ default void generateLazyDetailsForFeaturesIncludedInHover(FeatureSupportRegistr continue; } - String text = defaultString( - fsr.findExtension(feature).orElseThrow().renderFeatureValue(feature, aFs)); + var label = fsr.findExtension(feature).orElseThrow().renderFeatureValue(feature, aFs); - var group = new VLazyDetailGroup(); - group.addDetail(new VLazyDetail(feature.getName(), text)); - details.add(group); + if (StringUtils.isNotBlank(label)) { + var group = new VLazyDetailGroup(); + group.addDetail(new VLazyDetail(feature.getName(), label)); + details.add(group); + } } } } diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationEditorExtension.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationEditorExtension.java index d974fbcd9ad..e807506ffa8 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationEditorExtension.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationEditorExtension.java @@ -17,11 +17,21 @@ */ package de.tudarmstadt.ukp.inception.ui.curation.sidebar; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.RELATION_TYPE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.SPAN_TYPE; +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.LinkCompareBehavior.LINK_ROLE_AS_LABEL; +import static de.tudarmstadt.ukp.clarin.webanno.model.MultiValueMode.NONE; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.uima.cas.CAS; import org.apache.uima.cas.text.AnnotationFS; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -31,24 +41,28 @@ 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.curation.casdiff.CasDiff; 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.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.curation.merge.CasMerge; -import de.tudarmstadt.ukp.inception.curation.merge.CasMergeOperationResult; import de.tudarmstadt.ukp.inception.diam.editor.actions.ScrollToHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.SelectAnnotationHandler; +import de.tudarmstadt.ukp.inception.diam.editor.lazydetails.LazyDetailsLookupService; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtension; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtensionImplBase; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VLazyDetail; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VLazyDetailGroup; 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.TypeAdapter; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.config.CurationSidebarAutoConfiguration; @@ -66,7 +80,7 @@ public class CurationEditorExtension { public static final String EXTENSION_ID = "cur"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final AnnotationSchemaService annotationService; private final DocumentService documentService; @@ -74,11 +88,13 @@ public class CurationEditorExtension private final UserDao userRepository; private final CurationSidebarService curationSidebarService; private final FeatureSupportRegistry featureSupportRegistry; + private final LazyDetailsLookupService detailsLookupService; public CurationEditorExtension(AnnotationSchemaService aAnnotationService, DocumentService aDocumentService, ApplicationEventPublisher aApplicationEventPublisher, UserDao aUserRepository, CurationSidebarService aCurationSidebarService, - FeatureSupportRegistry aFeatureSupportRegistry) + FeatureSupportRegistry aFeatureSupportRegistry, + LazyDetailsLookupService aDetailsLookupService) { annotationService = aAnnotationService; documentService = aDocumentService; @@ -86,6 +102,7 @@ public CurationEditorExtension(AnnotationSchemaService aAnnotationService, userRepository = aUserRepository; curationSidebarService = aCurationSidebarService; featureSupportRegistry = aFeatureSupportRegistry; + detailsLookupService = aDetailsLookupService; } @Override @@ -104,16 +121,16 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, return; } - CurationVID curationVid = CurationVID.parse(aParamId.getExtensionPayload()); + var curationVid = CurationVID.parse(aParamId.getExtensionPayload()); if (curationVid == null) { return; } - SourceDocument doc = aState.getDocument(); - String srcUser = curationVid.getUsername(); + var doc = aState.getDocument(); + var srcUser = curationVid.getUsername(); if (!documentService.existsAnnotationDocument(doc, srcUser)) { - log.error("Source CAS of [{}] for curation not found", srcUser); + LOG.error("Source CAS of [{}] for curation not found", srcUser); return; } @@ -128,10 +145,10 @@ public void handleAction(AnnotationActionHandler aPanel, AnnotatorState aState, } else if (ScrollToHandler.COMMAND.equals(aAction)) { // get user CAS and annotation (to be merged into curator's) - VID vid = VID.parse(curationVid.getExtensionPayload()); + var vid = VID.parse(curationVid.getExtensionPayload()); - CAS srcCas = documentService.readAnnotationCas(doc, srcUser); - AnnotationFS sourceAnnotation = ICasUtil.selectAnnotationByAddr(srcCas, vid.getId()); + var srcCas = documentService.readAnnotationCas(doc, srcUser); + var sourceAnnotation = ICasUtil.selectAnnotationByAddr(srcCas, vid.getId()); var page = (AnnotationPageBase) aTarget.getPage(); page.getAnnotationActionHandler().actionJump(aTarget, sourceAnnotation.getBegin(), @@ -157,7 +174,7 @@ public V getFeatureValue(SourceDocument aDocument, User aUser, CAS aCas, VID var srcUser = curationVid.getUsername(); if (!documentService.existsAnnotationDocument(aDocument, srcUser)) { - log.error("Source CAS of [{}] for curation not found", srcUser); + LOG.error("Source CAS of [{}] for curation not found", srcUser); return null; } @@ -177,22 +194,22 @@ private void mergeAnnotation(String aAction, AnnotationActionHandler aPanel, throws IOException, AnnotationException { // get user CAS and annotation (to be merged into curator's) - SourceDocument doc = aState.getDocument(); - String srcUser = aCurationVid.getUsername(); + var doc = aState.getDocument(); + var srcUser = aCurationVid.getUsername(); - VID vid = VID.parse(aCurationVid.getExtensionPayload()); + var vid = VID.parse(aCurationVid.getExtensionPayload()); - CAS srcCas = documentService.readAnnotationCas(doc, srcUser); - AnnotationFS sourceAnnotation = ICasUtil.selectAnnotationByAddr(srcCas, vid.getId()); - AnnotationLayer layer = annotationService.findLayer(aState.getProject(), sourceAnnotation); + var srcCas = documentService.readAnnotationCas(doc, srcUser); + var sourceAnnotation = ICasUtil.selectAnnotationByAddr(srcCas, vid.getId()); + var layer = annotationService.findLayer(aState.getProject(), sourceAnnotation); if (vid.isSlotSet()) { mergeSlot(aState, aTargetCas, vid, srcUser, sourceAnnotation, layer); } - else if (RELATION_TYPE.equals(layer.getType())) { + else if (RelationLayerSupport.TYPE.equals(layer.getType())) { mergeRelation(aState, aTargetCas, vid, srcUser, sourceAnnotation, layer); } - else if (SPAN_TYPE.equals(layer.getType())) { + else if (SpanLayerSupport.TYPE.equals(layer.getType())) { mergeSpan(aState, aTargetCas, vid, srcUser, sourceAnnotation, layer); } @@ -204,18 +221,18 @@ private void mergeSlot(AnnotatorState aState, CAS aTargetCas, VID aVid, String a AnnotationFS sourceAnnotation, AnnotationLayer layer) throws AnnotationException { - SourceDocument doc = aState.getDocument(); - CasMerge casMerge = new CasMerge(annotationService, applicationEventPublisher); + var doc = aState.getDocument(); + var casMerge = new CasMerge(annotationService, applicationEventPublisher); - TypeAdapter adapter = annotationService.getAdapter(layer); - AnnotationFeature feature = adapter.listFeatures().stream().sequential() - .skip(aVid.getAttribute()).findFirst().get(); + var adapter = annotationService.getAdapter(layer); + var feature = adapter.listFeatures().stream().sequential().skip(aVid.getAttribute()) + .findFirst().get(); - CasMergeOperationResult mergeResult = casMerge.mergeSlotFeature(doc, aSrcUser, layer, - aTargetCas, sourceAnnotation, feature.getName(), aVid.getSlot()); + var mergeResult = casMerge.mergeSlotFeature(doc, aSrcUser, layer, aTargetCas, + sourceAnnotation, feature.getName(), aVid.getSlot()); - // open created/updates FS in annotation detail editorpanel - AnnotationFS mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, + // open created/updates FS in annotation detail editor panel + var mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, mergeResult.getResultFSAddress()); aState.getSelection().selectSpan(mergedAnno); } @@ -224,13 +241,13 @@ private void mergeRelation(AnnotatorState aState, CAS aTargetCas, VID aVid, Stri AnnotationFS sourceAnnotation, AnnotationLayer layer) throws AnnotationException { - SourceDocument doc = aState.getDocument(); - CasMerge casMerge = new CasMerge(annotationService, applicationEventPublisher); - CasMergeOperationResult mergeResult = casMerge.mergeRelationAnnotation(doc, aSrcUser, layer, - aTargetCas, sourceAnnotation, layer.isAllowStacking()); + var doc = aState.getDocument(); + var casMerge = new CasMerge(annotationService, applicationEventPublisher); + var mergeResult = casMerge.mergeRelationAnnotation(doc, aSrcUser, layer, aTargetCas, + sourceAnnotation, layer.isAllowStacking()); - // open created/updates FS in annotation detail editorpanel - AnnotationFS mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, + // open created/updates FS in annotation detail editor panel + var mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, mergeResult.getResultFSAddress()); aState.getSelection().selectArc(mergedAnno); } @@ -239,14 +256,145 @@ private void mergeSpan(AnnotatorState aState, CAS aTargetCas, VID aVid, String a AnnotationFS sourceAnnotation, AnnotationLayer layer) throws AnnotationException { - SourceDocument doc = aState.getDocument(); - CasMerge casMerge = new CasMerge(annotationService, applicationEventPublisher); - CasMergeOperationResult mergeResult = casMerge.mergeSpanAnnotation(doc, aSrcUser, layer, - aTargetCas, sourceAnnotation, layer.isAllowStacking()); + var doc = aState.getDocument(); + var casMerge = new CasMerge(annotationService, applicationEventPublisher); + var mergeResult = casMerge.mergeSpanAnnotation(doc, aSrcUser, layer, aTargetCas, + sourceAnnotation, layer.isAllowStacking()); // open created/updates FS in annotation detail editor panel - AnnotationFS mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, + var mergedAnno = ICasUtil.selectAnnotationByAddr(aTargetCas, mergeResult.getResultFSAddress()); aState.getSelection().selectSpan(mergedAnno); } + + @Override + public List lookupLazyDetails(SourceDocument aDocument, User aUser, CAS aCas, + VID aVid, AnnotationLayer aLayer) + { + var detailGroups = new ArrayList(); + + try { + var curationVid = CurationVID.parse(aVid.getExtensionPayload()); + var vid = VID.parse(curationVid.getExtensionPayload()); + var srcUser = curationVid.getUsername(); + var srcCas = documentService.readAnnotationCas(aDocument, srcUser); + var features = annotationService.listEnabledFeatures(aLayer).stream() // + .filter(f -> f.isIncludeInHover()) // + .filter(f -> NONE == f.getMultiValueMode()) // + .toList(); + + // This is where we get the "show on hover" stuff... + detailsLookupService.lookupLayerLevelDetails(aVid, aCas, aLayer) + .forEach(detailGroups::add); + + // The curatable features need to be all the same across the users for the position + var curatableFeatures = features.stream().filter(f -> f.isCuratable()).toList(); + for (var feature : curatableFeatures) { + detailsLookupService.lookupFeatureLevelDetails(aVid, aCas, feature) + .forEach(detailGroups::add); + } + + // The non-curatable features (e.g. comments) may differ, so we need to collect them + var nonCuratableFeatures = features.stream().filter(f -> !f.isCuratable()).toList(); + lookupFeaturesAcrossAnnotators(aDocument, aUser, aCas, aLayer, vid, srcUser, srcCas, + nonCuratableFeatures).forEach(detailGroups::add); + } + catch (IOException e) { + LOG.error("Unable to load lazy details", e); + var detailGroup = new VLazyDetailGroup(); + detailGroup.addDetail(new VLazyDetail("Error", "Unable to load annotator details")); + detailGroups.add(detailGroup); + } + + return detailGroups; + } + + private List lookupFeaturesAcrossAnnotators(SourceDocument aDocument, + User aUser, CAS aCas, AnnotationLayer aLayer, VID vid, String srcUser, CAS srcCas, + List nonCuratableFeatures) + { + var sessionOwner = userRepository.getCurrentUsername(); + var selectedUsers = curationSidebarService.listUsersReadyForCuration(sessionOwner, + aDocument.getProject(), aDocument); + + if (selectedUsers.isEmpty()) { + return emptyList(); + } + + var casses = collectCasses(aDocument, aUser, aCas, selectedUsers); + + var srcAnnotation = ICasUtil.selectAnnotationByAddr(srcCas, vid.getId()); + var casDiff = createDiff(casses, aLayer, srcAnnotation.getBegin(), srcAnnotation.getEnd()); + + var maybeConfiguration = casDiff.toResult().findConfiguration(srcUser, srcAnnotation); + if (maybeConfiguration.isEmpty()) { + return emptyList(); + } + + var detailGroups = new ArrayList(); + + var configuration = maybeConfiguration.get(); + for (var user : configuration.getCasGroupIds()) { + var group = new VLazyDetailGroup(user); + var fs = configuration.getFs(user, casses); + for (var f : nonCuratableFeatures) { + featureSupportRegistry.findExtension(f).ifPresent(support -> { + var label = support.renderFeatureValue(f, fs); + if (StringUtils.isNotBlank(label)) { + group.addDetail(new VLazyDetail(f.getUiName(), label)); + } + }); + } + if (!group.getDetails().isEmpty()) { + detailGroups.add(group); + } + } + + return detailGroups; + + // for (var group : delegateDetailGroups) { + // if (isNotBlank(group.getTitle())) { + // var detailGroup = new VLazyDetailGroup(srcUser + ": " + group.getTitle()); + // group.getDetails().forEach(d -> detailGroup.addDetail(d)); + // detailGroups.add(detailGroup); + // } + // else { + // var detailGroup = new VLazyDetailGroup(); + // for (var detail : group.getDetails()) { + // detailGroup.addDetail(new VLazyDetail(srcUser + ": " + detail.getLabel(), + // detail.getValue())); + // } + // detailGroups.add(detailGroup); + // } + // } + } + + private Map collectCasses(SourceDocument aDocument, User aUser, CAS aCas, + List selectedUsers) + { + var casses = new LinkedHashMap(); + + // This is the CAS that the user can actively edit + casses.put(aUser.getUsername(), aCas); + + for (var user : selectedUsers) { + try { + var userCas = documentService.readAnnotationCas(aDocument, user.getUsername()); + casses.put(user.getUsername(), userCas); + } + catch (IOException e) { + LOG.error("Could not retrieve CAS for user [{}] and project {}", user.getUsername(), + aDocument.getProject(), e); + } + } + + return casses; + } + + private CasDiff createDiff(Map casses, AnnotationLayer aLayer, int aBegin, + int aEnd) + { + var adapters = getDiffAdapters(annotationService, asList(aLayer)); + return doDiff(adapters, LINK_ROLE_AS_LABEL, casses, aBegin, aEnd); + } } diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebar.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebar.java index e7ebb491c1b..473fe0c7361 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebar.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebar.java @@ -33,10 +33,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import org.apache.uima.UIMAException; -import org.apache.uima.cas.CAS; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.markup.html.WebMarkupContainer; @@ -46,7 +44,6 @@ import org.apache.wicket.markup.html.form.CheckGroup; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; -import org.apache.wicket.markup.html.form.IChoiceRenderer; import org.apache.wicket.markup.html.form.LambdaChoiceRenderer; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; @@ -62,8 +59,6 @@ import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; -import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; @@ -126,7 +121,7 @@ public CurationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); - AnnotatorState state = aModel.getObject(); + var state = aModel.getObject(); var notCuratableNotice = new WebMarkupContainer("notCuratableNotice"); notCuratableNotice.setOutputMarkupId(true); @@ -176,8 +171,8 @@ public CurationSidebar(String aId, IModel aModel, private boolean isViewingPotentialCurationTarget() { // Curation sidebar is not allowed when viewing another users annotations - String currentUsername = userRepository.getCurrentUsername(); - AnnotatorState state = getModelObject(); + var currentUsername = userRepository.getCurrentUsername(); + var state = getModelObject(); return asList(CURATION_USER, currentUsername).contains(state.getUser().getUsername()); } @@ -197,7 +192,7 @@ private void actionOpenMergeDialog(AjaxRequestTarget aTarget, Form aForm) private void actionMerge(AjaxRequestTarget aTarget, Form aForm) { - AnnotatorState state = getModelObject(); + var state = getModelObject(); if (aForm.getModelObject().isSaveSettingsAsDefault()) { curationService.createOrUpdateCurationWorkflow(curationWorkflowModel.getObject()); @@ -224,13 +219,13 @@ private void actionMerge(AjaxRequestTarget aTarget, Form aForm) private void doMerge(AnnotatorState aState, String aCurator, Collection aUsers) throws IOException, UIMAException { - SourceDocument doc = aState.getDocument(); - CAS aTargetCas = curationSidebarService + var doc = aState.getDocument(); + var aTargetCas = curationSidebarService .retrieveCurationCAS(aCurator, aState.getProject().getId(), doc) .orElseThrow(() -> new IllegalArgumentException( "No target CAS configured in curation state")); - Map userCases = documentService.readAllCasesSharedNoUpgrade(doc, aUsers); + var userCases = documentService.readAllCasesSharedNoUpgrade(doc, aUsers); // FIXME: should merging not overwrite the current users annos? (can result in // deleting the users annotations!!!), currently fixed by warn message to user @@ -252,7 +247,7 @@ private Form createSessionControlForm(String aId) form.setOutputMarkupId(true); - IChoiceRenderer targetChoiceRenderer = new LambdaChoiceRenderer<>( + var targetChoiceRenderer = new LambdaChoiceRenderer<>( aUsername -> CURATION_USER.equals(aUsername) ? "curation document" : "my document"); var curationTargets = new ArrayList(); @@ -325,8 +320,8 @@ private void actionStopSession(AjaxRequestTarget aTarget) private Form createUserSelection(String aId) { - String sessionOwner = userRepository.getCurrentUsername(); - Project project = getModelObject().getProject(); + var sessionOwner = userRepository.getCurrentUsername(); + var project = getModelObject().getProject(); var form = new Form(aId); form.setOutputMarkupPlaceholderTag(true); @@ -363,7 +358,7 @@ protected void populateItem(ListItem aItem) private IModel maybeAnonymizeUsername(ListItem aUserListItem) { - Project project = getModelObject().getProject(); + var project = getModelObject().getProject(); if (project.isAnonymousCuration() && !projectService.hasRole(userRepository.getCurrentUser(), project, MANAGER)) { return Model.of("Anonymized annotator " + (aUserListItem.getIndex() + 1)); diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarServiceImpl.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarServiceImpl.java index 0a0df62da91..62536704bde 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarServiceImpl.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarServiceImpl.java @@ -48,12 +48,10 @@ import org.apache.uima.cas.CAS; import org.springframework.context.event.EventListener; import org.springframework.security.core.session.SessionDestroyedEvent; -import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasStorageService; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; @@ -144,15 +142,15 @@ private CurationSession getSession(String aSessionOwner, long aProjectId) private CurationSession readSession(String aSessionOwner, long aProjectId) { - List settings = queryDBForSetting(aSessionOwner, aProjectId); + var settings = queryDBForSetting(aSessionOwner, aProjectId); CurationSession state; if (settings.isEmpty()) { state = new CurationSession(aSessionOwner); } else { - CurationSettings setting = settings.get(0); - Project project = projectService.getProject(aProjectId); + var setting = settings.get(0); + var project = projectService.getProject(aProjectId); List users = new ArrayList<>(); if (!setting.getSelectedUserNames().isEmpty()) { users = setting.getSelectedUserNames().stream() @@ -172,11 +170,11 @@ private List queryDBForSetting(String aSessionOwner, long aPro Validate.notBlank(aSessionOwner, "User must be specified"); Validate.notNull(aProjectId, "project must be specified"); - String query = "FROM " + CurationSettings.class.getName() // + var query = "FROM " + CurationSettings.class.getName() // + " o WHERE o.username = :username " // + "AND o.projectId = :projectId"; - List settings = entityManager // + var settings = entityManager // .createQuery(query, CurationSettings.class) // .setParameter("username", aSessionOwner) // .setParameter("projectId", aProjectId) // @@ -232,7 +230,7 @@ public List listUsersReadyForCuration(String aSessionOwner, Project aProje return new ArrayList<>(); } - List finishedUsers = listCuratableUsers(aDocument); + var finishedUsers = listCuratableUsers(aDocument); finishedUsers.retainAll(selectedUsers); return finishedUsers; } @@ -242,7 +240,7 @@ public List listUsersReadyForCuration(String aSessionOwner, Project aProje @Transactional public List listCuratableUsers(String aSessionOwner, SourceDocument aDocument) { - String curationTarget = getCurationTarget(aSessionOwner, aDocument.getProject().getId()); + var curationTarget = getCurationTarget(aSessionOwner, aDocument.getProject().getId()); return listCuratableUsers(aDocument).stream() .filter(user -> !user.getUsername().equals(aSessionOwner) || curationTarget.equals(CURATION_USER)) @@ -255,21 +253,19 @@ public List listCuratableUsers(SourceDocument aSourceDocument) { Validate.notNull(aSourceDocument, "Document must be specified"); - String query = String.join("\n", // + var query = String.join("\n", // "SELECT u FROM User u, AnnotationDocument d", // "WHERE u.username = d.user", // "AND d.document = :document", // "AND (d.state = :state or d.annotatorState = :ignore)", // "ORDER BY u.username ASC"); - List finishedUsers = new ArrayList<>(entityManager // + return new ArrayList<>(entityManager // .createQuery(query, User.class) // .setParameter("document", aSourceDocument) // .setParameter("state", AnnotationDocumentState.FINISHED) // .setParameter("ignore", AnnotationDocumentState.IGNORE) // .getResultList()); - - return finishedUsers; } @Transactional @@ -278,7 +274,7 @@ public Optional retrieveCurationCAS(String aSessionOwner, long aProjectId, SourceDocument aDoc) throws IOException { - String curationUser = getSession(aSessionOwner, aProjectId).getCurationTarget(); + var curationUser = getSession(aSessionOwner, aProjectId).getCurationTarget(); if (curationUser == null) { return Optional.empty(); } @@ -295,14 +291,14 @@ public void writeCurationCas(CAS aTargetCas, AnnotatorState aState, long aProjec { User curator; synchronized (sessions) { - String curatorName = getSession(aState.getUser().getUsername(), aProjectId) + var curatorName = getSession(aState.getUser().getUsername(), aProjectId) .getCurationTarget(); curator = userRegistry.getUserOrCurationUser(curatorName); } - SourceDocument doc = aState.getDocument(); - AnnotationDocument annoDoc = documentService.createOrGetAnnotationDocument(doc, curator); + var doc = aState.getDocument(); + var annoDoc = documentService.createOrGetAnnotationDocument(doc, curator); documentService.writeAnnotationCas(aTargetCas, annoDoc, true); casStorageService.getCasTimestamp(doc, curator.getUsername()) .ifPresent(aState::setAnnotationDocumentTimestamp); @@ -376,7 +372,7 @@ public void setDefaultSelectedUsersForDocument(String aSessionOwner, SourceDocum @Transactional public void onSessionDestroyed(SessionDestroyedEvent event) { - SessionInformation info = sessionRegistry.getSessionInformation(event.getId()); + var info = sessionRegistry.getSessionInformation(event.getId()); if (info == null) { return; @@ -407,14 +403,14 @@ public void onSessionDestroyed(SessionDestroyedEvent event) */ private void storeCurationSettings(User aSessionOwner) { - String aUsername = aSessionOwner.getUsername(); + var aUsername = aSessionOwner.getUsername(); - for (Project project : projectService.listAccessibleProjects(aSessionOwner)) { - Long projectId = project.getId(); + for (var project : projectService.listAccessibleProjects(aSessionOwner)) { + var projectId = project.getId(); Set usernames = null; if (sessions.containsKey(new CurationSessionKey(aUsername, projectId))) { - CurationSession state = sessions.get(new CurationSessionKey(aUsername, projectId)); + var state = sessions.get(new CurationSessionKey(aUsername, projectId)); // user does not exist anymore or is anonymous authentication if (state == null) { continue; @@ -428,7 +424,7 @@ private void storeCurationSettings(User aSessionOwner) // get setting from context and update values if it exists, else save new setting // to db - CurationSettings setting = entityManager.find(CurationSettings.class, + var setting = entityManager.find(CurationSettings.class, new CurationSettingsId(projectId, aUsername)); if (setting != null) { @@ -510,8 +506,8 @@ public User getCurationTargetUser(String aSessionOwner, long aProjectId) @Override public boolean isCurationFinished(AnnotatorState aState, String aSessionOwner) { - String username = aState.getUser().getUsername(); - SourceDocument sourceDoc = aState.getDocument(); + var username = aState.getUser().getUsername(); + var sourceDoc = aState.getDocument(); return (username.equals(aSessionOwner) && documentService.isAnnotationFinished(sourceDoc, aState.getUser())) || (username.equals(CURATION_USER) diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java index a531ad29af3..ec247cc75bf 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java @@ -29,6 +29,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasStorageService; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.diam.editor.lazydetails.LazyDetailsLookupService; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -63,11 +64,12 @@ public CurationEditorExtension curationEditorExtension( AnnotationSchemaService aAnnotationService, DocumentService aDocumentService, ApplicationEventPublisher aApplicationEventPublisher, UserDao aUserRepository, CurationSidebarService aCurationSidebarService, - FeatureSupportRegistry aFeatureSupportRegistry) + FeatureSupportRegistry aFeatureSupportRegistry, + LazyDetailsLookupService aDetailsLookupService) { return new CurationEditorExtension(aAnnotationService, aDocumentService, aApplicationEventPublisher, aUserRepository, aCurationSidebarService, - aFeatureSupportRegistry); + aFeatureSupportRegistry, aDetailsLookupService); } @Bean("curationSidebar") diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/render/CurationSidebarRenderer.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/render/CurationSidebarRenderer.java index cbaf1cab3ec..c987c863997 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/render/CurationSidebarRenderer.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/render/CurationSidebarRenderer.java @@ -22,20 +22,17 @@ import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkCompareBehavior.LINK_ROLE_AS_LABEL; import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.text.AnnotationFS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,18 +40,13 @@ import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.Configuration; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.ConfigurationSet; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.DiffResult; -import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.DiffAdapter; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.api.Position; import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.internal.AID; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; -import de.tudarmstadt.ukp.inception.rendering.Renderer; -import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderStep; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; @@ -62,9 +54,7 @@ import de.tudarmstadt.ukp.inception.rendering.vmodel.VCommentType; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; -import de.tudarmstadt.ukp.inception.rendering.vmodel.VObject; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupport; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationSidebarService; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.config.CurationSidebarAutoConfiguration; @@ -83,7 +73,7 @@ public class CurationSidebarRenderer private static final String COLOR = "#ccccff"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final CurationSidebarService curationService; private final LayerSupportRegistry layerSupportRegistry; @@ -111,7 +101,7 @@ public String getId() @Override public boolean accepts(RenderRequest aRequest) { - AnnotatorState state = aRequest.getState(); + var state = aRequest.getState(); // do not show predictions on the decicated curation page if (state != null && state.getMode() != ANNOTATION) { @@ -128,90 +118,66 @@ public boolean accepts(RenderRequest aRequest) @Override public void render(VDocument aVdoc, RenderRequest aRequest) { - String sessionOwner = userRepository.getCurrentUsername(); - if (!curationService.existsSession(sessionOwner, aRequest.getProject().getId())) { + var sessionOwner = userRepository.getCurrentUsername(); + var project = aRequest.getProject(); + + if (!curationService.existsSession(sessionOwner, project.getId())) { return; } - List selectedUsers = curationService.listUsersReadyForCuration(sessionOwner, - aRequest.getProject(), aRequest.getSourceDocument()); - + var selectedUsers = curationService.listUsersReadyForCuration(sessionOwner, project, + aRequest.getSourceDocument()); if (selectedUsers.isEmpty()) { return; } - Map casses = new LinkedHashMap<>(); - - // This is the CAS that the user can actively edit - casses.put(aRequest.getAnnotationUser().getUsername(), aRequest.getCas()); - - for (User user : selectedUsers) { - try { - CAS userCas = documentService.readAnnotationCas(aRequest.getSourceDocument(), - user.getUsername()); - casses.put(user.getUsername(), userCas); - } - catch (IOException e) { - log.error("Could not retrieve CAS for user [{}] and project {}", user.getUsername(), - aRequest.getProject(), e); - } - } - - List adapters = getDiffAdapters(annotationService, - aRequest.getVisibleLayers()); - CasDiff casDiff = doDiff(adapters, LINK_ROLE_AS_LABEL, casses, - aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); - DiffResult diff = casDiff.toResult(); + var casDiff = createDiff(aRequest, selectedUsers); + var diff = casDiff.toResult(); // Listing the features once is faster than repeatedly hitting the DB to list features for // every layer. - List supportedFeatures = annotationService - .listSupportedFeatures(aRequest.getProject()); - List allFeatures = annotationService - .listAnnotationFeature(aRequest.getProject()); + var supportedFeatures = annotationService.listSupportedFeatures(project); + var allFeatures = annotationService.listAnnotationFeature(project); // Set up a cache for resolving type to layer to avoid hammering the DB as we process each // position - Map type2layer = diff.getPositions().stream() - .map(Position::getType).distinct() - .map(type -> annotationService.findLayer(aRequest.getProject(), type)) + var type2layer = diff.getPositions().stream().map(Position::getType).distinct() + .map(type -> annotationService.findLayer(project, type)) .collect(toMap(AnnotationLayer::getName, identity())); - Set generatedCurationVids = new HashSet<>(); - boolean showAll = curationService.isShowAll(sessionOwner, aRequest.getProject().getId()); - String curationTarget = curationService.getCurationTarget(sessionOwner, - aRequest.getProject().getId()); - for (ConfigurationSet cfgSet : diff.getConfigurationSets()) { + var generatedCurationVids = new HashSet(); + var showAll = curationService.isShowAll(sessionOwner, project.getId()); + var curationTarget = curationService.getCurationTarget(sessionOwner, project.getId()); + for (var cfgSet : diff.getConfigurationSets()) { if (!showAll && cfgSet.getCasGroupIds().contains(curationTarget)) { // Hide configuration sets where the curator has already curated (likely) continue; } - AnnotationLayer layer = type2layer.get(cfgSet.getPosition().getType()); + var layer = type2layer.get(cfgSet.getPosition().getType()); - List layerSupportedFeatures = supportedFeatures.stream() // + var layerSupportedFeatures = supportedFeatures.stream() // .filter(feature -> feature.getLayer().equals(layer)) // - .collect(toList()); - List layerAllFeatures = allFeatures.stream() // + .toList(); + var layerAllFeatures = allFeatures.stream() // .filter(feature -> feature.getLayer().equals(layer)) // - .collect(toList()); + .toList(); - for (Configuration cfg : cfgSet.getConfigurations()) { - FeatureStructure fs = cfg.getRepresentative(casDiff.getCasMap()); - String user = cfg.getRepresentativeCasGroupId(); + for (var cfg : cfgSet.getConfigurations()) { + var fs = cfg.getRepresentative(casDiff.getCasMap()); + var user = cfg.getRepresentativeCasGroupId(); // We need to pass in *all* the annotation features here because we also to that in // other places where we create renderers - and the set of features must always be // the same because otherwise the IDs of armed slots would be inconsistent - LayerSupport layerSupport = layerSupportRegistry.getLayerSupport(layer); - Renderer renderer = layerSupport.createRenderer(layer, () -> layerAllFeatures); + var layerSupport = layerSupportRegistry.getLayerSupport(layer); + var renderer = layerSupport.createRenderer(layer, () -> layerAllFeatures); - List objects = renderer.render(aVdoc, (AnnotationFS) fs, - layerSupportedFeatures, aRequest.getWindowBeginOffset(), - aRequest.getWindowEndOffset()); + var objects = renderer.render(aVdoc, (AnnotationFS) fs, layerSupportedFeatures, + aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); - for (VObject object : objects) { - VID curationVid = new CurationVID(user, object.getVid()); + for (var object : objects) { + var curationVid = new CurationVID(user, object.getVid()); if (generatedCurationVids.contains(curationVid)) { continue; } @@ -223,27 +189,49 @@ public void render(VDocument aVdoc, RenderRequest aRequest) aVdoc.add(object); aVdoc.add(new VComment(object.getVid(), VCommentType.INFO, - "Users with this annotation:\n" + cfg.getCasGroupIds().stream() - .collect(Collectors.joining(", ")))); + "Annotators: " + cfg.getCasGroupIds().stream().collect(joining(", ")))); - if (object instanceof VArc) { - VArc arc = (VArc) object; + if (object instanceof VArc arc) { // Currently works for relations but not for slots arc.setSource(getCurationVid(aRequest.getAnnotationUser(), diff, cfg, arc.getSource())); arc.setTarget(getCurationVid(aRequest.getAnnotationUser(), diff, cfg, arc.getTarget())); - log.trace("Rendering curation vid: {} source: {} target: {}", arc.getVid(), + LOG.trace("Rendering curation vid: {} source: {} target: {}", arc.getVid(), arc.getSource(), arc.getTarget()); } else { - log.trace("Rendering curation vid: {}", object.getVid()); + LOG.trace("Rendering curation vid: {}", object.getVid()); } } } } } + private CasDiff createDiff(RenderRequest aRequest, List selectedUsers) + { + var casses = new LinkedHashMap(); + + // This is the CAS that the user can actively edit + casses.put(aRequest.getAnnotationUser().getUsername(), aRequest.getCas()); + + for (var user : selectedUsers) { + try { + var userCas = documentService.readAnnotationCas(aRequest.getSourceDocument(), + user.getUsername()); + casses.put(user.getUsername(), userCas); + } + catch (IOException e) { + LOG.error("Could not retrieve CAS for user [{}] and project {}", user.getUsername(), + aRequest.getProject(), e); + } + } + + var adapters = getDiffAdapters(annotationService, aRequest.getVisibleLayers()); + return doDiff(adapters, LINK_ROLE_AS_LABEL, casses, aRequest.getWindowBeginOffset(), + aRequest.getWindowEndOffset()); + } + /** * Find and return the rendered VID which is equivalent to the given VID. E.g. if the given VID * belongs to an already curated annotation, then locate the VID for the rendered annotation of