Skip to content

Commit

Permalink
Merge branch 'main' into feature/3944-Ability-to-use-recommenders-as-…
Browse files Browse the repository at this point in the history
…curator

* main:
  #3945 - Centralize handling of CURATION_USER
  #3947 - Show more timing information in recommender log
  #3945 - Centralize handling of CURATION_USER
  #3934 - Cannot merge link feature with same target on curation page
  #3932 - Upgrade dependencies
  #3932 - Upgrade dependencies
  • Loading branch information
reckart committed Apr 15, 2023
2 parents de043d0 + 4cd75fc commit 599c274
Show file tree
Hide file tree
Showing 23 changed files with 1,234 additions and 823 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
Expand Down Expand Up @@ -158,7 +159,9 @@ private Object getValue(FeatureStructure fs, AnnotationFeature aFeature)
@Override
public <T> T getFeatureValue(AnnotationFeature aFeature, FeatureStructure aFs)
{
return (T) featureSupportRegistry.findExtension(aFeature).orElseThrow()
return (T) featureSupportRegistry.findExtension(aFeature) //
.orElseThrow(() -> new NoSuchElementException(
"Unsupported feature type [" + aFeature.getType() + "]"))
.getFeatureValue(aFeature, aFs);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public LinkWithRoleModel()
// No-args constructor
}

public LinkWithRoleModel(LinkWithRoleModel aOther)
{
role = aOther.role;
label = aOther.label;
targetAddr = aOther.targetAddr;
}

public LinkWithRoleModel(String aRole, String aLabel, int aTargetAddr)
{
role = aRole;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.isPrimitiveType;
import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectSentences;
import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectTokens;
import static de.tudarmstadt.ukp.clarin.webanno.model.MultiValueMode.ARRAY;
import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.FEAT_REL_SOURCE;
import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.FEAT_REL_TARGET;
import static de.tudarmstadt.ukp.clarin.webanno.support.uima.ICasUtil.getAddr;
Expand All @@ -38,13 +39,15 @@
import static org.apache.uima.fit.util.CasUtil.selectCovered;
import static org.apache.uima.fit.util.FSUtil.getFeature;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
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.stream.Collectors;

Expand Down Expand Up @@ -73,16 +76,15 @@
import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.span.SpanPosition;
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.MultiValueMode;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil;
import de.tudarmstadt.ukp.clarin.webanno.support.logging.LogMessage;
import de.tudarmstadt.ukp.clarin.webanno.support.uima.ICasUtil;
import de.tudarmstadt.ukp.dkpro.core.api.metadata.type.DocumentMetaData;
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.events.BulkAnnotationEvent;
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.span.SpanAdapter;
import de.tudarmstadt.ukp.inception.curation.merge.strategy.DefaultMergeStrategy;
Expand Down Expand Up @@ -110,14 +112,39 @@ public class CasMerge
private boolean silenceEvents = false;
private Map<AnnotationLayer, List<AnnotationFeature>> featureCache = new HashMap<>();
private LoadingCache<AnnotationLayer, TypeAdapter> adapterCache;
private LoadingCache<AnnotationFeature, LinkFeatureTraits> linkTraitsCache;

public CasMerge(AnnotationSchemaService aSchemaService,
ApplicationEventPublisher aEventPublisher)
{
schemaService = aSchemaService;
eventPublisher = aEventPublisher;

adapterCache = Caffeine.newBuilder().maximumSize(100).build(schemaService::getAdapter);
adapterCache = Caffeine.newBuilder() //
.maximumSize(100) //
.build(schemaService::getAdapter);
linkTraitsCache = Caffeine.newBuilder() //
.maximumSize(100) //
.build(this::readTraits);
}

// Would be better to use this from the LinkFeatureSupport - but I do not want to change the
// constructor at the moment to inject another dependency.
private LinkFeatureTraits readTraits(AnnotationFeature aFeature)
{
LinkFeatureTraits traits = null;
try {
traits = JSONUtil.fromJsonString(LinkFeatureTraits.class, aFeature.getTraits());
}
catch (IOException e) {
LOG.error("Unable to read traits", e);
}

if (traits == null) {
traits = new LinkFeatureTraits();
}

return traits;
}

public void setSilenceEvents(boolean aSilenceEvents)
Expand Down Expand Up @@ -654,56 +681,73 @@ public CasMergeOperationResult mergeSlotFeature(SourceDocument aDocument, String

List<AnnotationFS> candidateHosts = getCandidateAnnotations(aTargetCas, adapter, aSourceFs);

AnnotationFS targetFs;
if (candidateHosts.size() == 0) {
throw new UnfulfilledPrerequisitesException(
"The base annotation does not exist. Please add it first.");
"There is no suitable [" + adapter.getLayer().getUiName() + "] annotation at ["
+ aSourceFs.getBegin() + "," + aSourceFs.getEnd()
+ "] into which the link could be merged. Please add one first.");
}
AnnotationFS mergeFs = candidateHosts.get(0);
int liIndex = aSourceSlotIndex;

AnnotationFeature slotFeature = null;
LinkWithRoleModel linkRole = null;
f: for (AnnotationFeature feat : adapter.listFeatures()) {
if (!aSourceFeature.equals(feat.getName())) {
continue;
}
var slotFeature = adapter.listFeatures().stream() //
.filter(f -> aSourceFeature.equals(f.getName())) //
.findFirst() //
.orElseThrow(() -> new AnnotationException(
"Feature [" + aSourceFeature + "] not found"));

if (MultiValueMode.ARRAY.equals(feat.getMultiValueMode())
&& LinkMode.WITH_ROLE.equals(feat.getLinkMode())) {
List<LinkWithRoleModel> links = adapter.getFeatureValue(feat, aSourceFs);
for (int li = 0; li < links.size(); li++) {
LinkWithRoleModel link = links.get(li);
if (li == liIndex) {
slotFeature = feat;

List<AnnotationFS> targets = checkAndGetTargets(aTargetCas,
selectAnnotationByAddr(aSourceFs.getCAS(), link.targetAddr));
targetFs = targets.get(0);
link.targetAddr = getAddr(targetFs);
linkRole = link;
break f;
}
}
}
if (slotFeature.getMultiValueMode() != ARRAY) {
throw new AnnotationException("Feature [" + aSourceFeature + "] is not a slot feature");
}

List<LinkWithRoleModel> sourceLinks = adapter.getFeatureValue(slotFeature, aSourceFs);
List<AnnotationFS> 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));
newLink.targetAddr = getAddr(targets.get(0));

List<LinkWithRoleModel> links = adapter.getFeatureValue(slotFeature, mergeFs);
LinkWithRoleModel duplicateLink = null; //
for (LinkWithRoleModel lr : links) {
if (lr.targetAddr == linkRole.targetAddr) {
duplicateLink = lr;
break;
// Override an existing link if no roles are used. If roles are used, then the user may want
// to link the same target multiple times with different roles - hence we simply add.
switch (slotFeature.getLinkMode()) {
case WITH_ROLE:
var traits = linkTraitsCache.get(slotFeature);
if (traits.isEnableRoleLabels()) {
if (links.stream().noneMatch(l -> l.targetAddr == newLink.targetAddr
&& Objects.equals(l.role, newLink.role))) {
links.add(newLink);
}
}
else {
links.remove(existingLinkWithTarget(newLink, links));
links.add(newLink);
}
break;
default:
throw new AnnotationException("Feature [" + aSourceFeature + "] is not a slot feature");
}
links.add(linkRole);
links.remove(duplicateLink);

adapter.setFeatureValue(aDocument, aUsername, aTargetCas, mergeFs.getAddress(), slotFeature,
adapter.setFeatureValue(aDocument, aUsername, aTargetCas, getAddr(mergeFs), slotFeature,
links);

return new CasMergeOperationResult(CasMergeOperationResult.ResultState.UPDATED,
ICasUtil.getAddr(mergeFs));
getAddr(mergeFs));
}

private LinkWithRoleModel existingLinkWithTarget(LinkWithRoleModel aLink,
List<LinkWithRoleModel> aLinks)
{
for (LinkWithRoleModel lr : aLinks) {
if (lr.targetAddr == aLink.targetAddr) {
return lr;
}
}
return null;
}

private static List<AnnotationFS> checkAndGetTargets(CAS aCas, AnnotationFS aOldTarget)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.curation.merge;

import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.doDiff;
import static de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.LinkCompareBehavior.LINK_TARGET_AS_LABEL;
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.schema.feature.FeatureUtil.setLinkFeatureValue;
import static java.util.Arrays.asList;
import static org.apache.uima.fit.factory.JCasFactory.createJCas;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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.jcas.JCas;
import org.junit.jupiter.api.Test;

import de.tudarmstadt.ukp.clarin.webanno.curation.casdiff.CasDiff.DiffResult;

public class CasDiffLinkFeaturesTest
extends CasMergeTestBase
{
@Test
public void copyLinkToEmptyTest() throws Exception
{
// Set up target CAS
JCas targetCas = createJCas(createMultiLinkWithRoleTestTypeSystem("f1"));
Type type = targetCas.getTypeSystem().getType(HOST_TYPE);
Feature feature = type.getFeatureByBaseName("f1");
AnnotationFS mergeFs = makeLinkHostFS(targetCas, 0, 0, feature, "A");
FeatureStructure linkFs = makeLinkFS(targetCas, "slot1", 0, 0);
setLinkFeatureValue(mergeFs, type.getFeatureByBaseName("links"), asList(linkFs));

// Set up source CAS
JCas sourceCas = createJCas(createMultiLinkWithRoleTestTypeSystem("f1"));
makeLinkHostFS(sourceCas, 0, 0, feature, "A",
makeLinkFS(sourceCas, "slot1", 0, 0));

// Perform diff
Map<String, List<CAS>> casByUser = new LinkedHashMap<>();
casByUser.put("user1", asList(targetCas.getCas()));
casByUser.put("user2", asList(sourceCas.getCas()));
DiffResult diff = doDiff(diffAdapters, LINK_TARGET_AS_LABEL, casByUser).toResult();

assertThat(diff.getDifferingConfigurationSets()).isEmpty();
assertThat(diff.getIncompleteConfigurationSets()).isEmpty();
}

@Test
public void copyLinkToExistingButDiffLinkTest() throws Exception
{
// Set up target CAS
JCas targetCas = createJCas(createMultiLinkWithRoleTestTypeSystem("f1"));
Type type = targetCas.getTypeSystem().getType(HOST_TYPE);
Feature feature = type.getFeatureByBaseName("f1");
AnnotationFS mergeFs = makeLinkHostFS(targetCas, 0, 0, feature, "A",
makeLinkFS(targetCas, "slot1", 0, 0));
FeatureStructure linkFs = makeLinkFS(targetCas, "slot2", 0, 0);
setLinkFeatureValue(mergeFs, type.getFeatureByBaseName("links"), asList(linkFs));

// Set up source CAS
JCas sourceCas = createJCas(createMultiLinkWithRoleTestTypeSystem("f1"));
makeLinkHostFS(sourceCas, 0, 0, feature, "A",
makeLinkFS(sourceCas, "slot1", 0, 0));

// Perform diff
Map<String, List<CAS>> casByUser = new LinkedHashMap<>();
casByUser.put("user1", asList(targetCas.getCas()));
casByUser.put("user2", asList(sourceCas.getCas()));
DiffResult diff = doDiff(diffAdapters, LINK_TARGET_AS_LABEL, casByUser).toResult();

assertThat(diff.getDifferingConfigurationSets()).isEmpty();
assertThat(diff.getIncompleteConfigurationSets()).hasSize(2);
}
}

0 comments on commit 599c274

Please sign in to comment.