diff --git a/inception/inception-active-learning/pom.xml b/inception/inception-active-learning/pom.xml index 7909d448328..4dbf7740403 100644 --- a/inception/inception-active-learning/pom.xml +++ b/inception/inception-active-learning/pom.xml @@ -20,7 +20,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 27.0-SNAPSHOT + 29.0-SNAPSHOT .. inception-active-learning @@ -58,7 +58,7 @@ de.tudarmstadt.ukp.inception.app inception-support - + de.tudarmstadt.ukp.inception.app inception-recommendation @@ -116,6 +116,10 @@ org.apache.wicket wicket-spring + + org.apache.wicket + wicket-extensions + org.wicketstuff wicketstuff-annotationeventdispatcher @@ -128,6 +132,10 @@ de.agilecoders.wicket wicket-bootstrap-extensions + + org.danekja + jdk-serializable-functional + diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningService.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningService.java index 55119513bf3..4cd7e362f4c 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningService.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningService.java @@ -22,6 +22,7 @@ import java.util.Optional; 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.active.learning.ActiveLearningServiceImpl.ActiveLearningUserState; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecord; @@ -34,14 +35,14 @@ public interface ActiveLearningService { /** - * @param aUser + * @param aDataOwner * annotator user to get suggestions for * @param aLayer * layer to get suggestions for * @return all suggestions for the given layer and user as a flat list (i.e. not grouped by * documents, but grouped by alternatives). */ - List> getSuggestions(User aUser, AnnotationLayer aLayer); + List> getSuggestions(User aDataOwner, AnnotationLayer aLayer); /** * @param aRecord @@ -56,28 +57,27 @@ public interface ActiveLearningService * @return if the are any records of type {@link LearningRecordType#SKIPPED} in the history of * the given layer for the given user. * - * @param aUser + * @param aDataOwner * annotator user to check suggestions for * @param aLayer * layer to check suggestions for */ - boolean hasSkippedSuggestions(User aUser, AnnotationLayer aLayer); + boolean hasSkippedSuggestions(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer); - void hideRejectedOrSkippedAnnotations(User aUser, AnnotationLayer aLayer, + void hideRejectedOrSkippedAnnotations(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer, boolean aFilterSkippedRecommendation, List> aSuggestionGroups); - Optional> generateNextSuggestion(User aUser, + Optional> generateNextSuggestion(String aSessionOwner, User aDataOwner, ActiveLearningUserState aAlState); - void writeLearningRecordInDatabaseAndEventLog(User aUser, AnnotationLayer aLayer, - SpanSuggestion aSuggestion, LearningRecordType aUserAction, String aAnnotationValue); - - void acceptSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion, + void acceptSpanSuggestion(SourceDocument aDocument, User aDataOwner, SpanSuggestion aSuggestion, Object aValue) throws IOException, AnnotationException; - void rejectSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion); + void rejectSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer, + SpanSuggestion aSuggestion); - void skipSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion); + void skipSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer, + SpanSuggestion aSuggestion); } diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningServiceImpl.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningServiceImpl.java index 444f777a666..5a9dffbc25f 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningServiceImpl.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/ActiveLearningServiceImpl.java @@ -17,10 +17,6 @@ */ package de.tudarmstadt.ukp.inception.active.learning; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_REJECTED; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_SKIPPED; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_TRANSIENT_ACCEPTED; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_TRANSIENT_CORRECTED; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.AL_SIDEBAR; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordType.ACCEPTED; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordType.CORRECTED; @@ -34,9 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -import org.apache.uima.cas.CAS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,7 +38,6 @@ import org.springframework.transaction.annotation.Transactional; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; -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; @@ -52,20 +45,18 @@ import de.tudarmstadt.ukp.inception.active.learning.config.ActiveLearningAutoConfiguration; import de.tudarmstadt.ukp.inception.active.learning.event.ActiveLearningRecommendationEvent; import de.tudarmstadt.ukp.inception.active.learning.strategy.ActiveLearningStrategy; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecord; -import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordType; import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Preferences; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup.Delta; import de.tudarmstadt.ukp.inception.schema.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.adapter.AnnotationException; -import de.tudarmstadt.ukp.inception.schema.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.feature.FeatureSupportRegistry; /** @@ -89,14 +80,14 @@ public class ActiveLearningServiceImpl @Autowired public ActiveLearningServiceImpl(DocumentService aDocumentService, - RecommendationService aRecommendationService, UserDao aUserDao, + RecommendationService aRecommendationService, UserDao aUserService, LearningRecordService aLearningHistoryService, AnnotationSchemaService aSchemaService, ApplicationEventPublisher aApplicationEventPublisher, FeatureSupportRegistry aFeatureSupportRegistry) { documentService = aDocumentService; recommendationService = aRecommendationService; - userService = aUserDao; + userService = aUserService; learningHistoryService = aLearningHistoryService; applicationEventPublisher = aApplicationEventPublisher; schemaService = aSchemaService; @@ -123,11 +114,10 @@ public List> getSuggestions(User aUser, Annotati @Override public boolean isSuggestionVisible(LearningRecord aRecord) { - User user = userService.get(aRecord.getUser()); - List> suggestions = getSuggestions(user, - aRecord.getLayer()); - for (SuggestionGroup listOfAO : suggestions) { - if (listOfAO.stream().anyMatch(suggestion -> suggestion.getDocumentName() + var aSessionOwner = userService.get(aRecord.getUser()); + var suggestionGroups = getSuggestions(aSessionOwner, aRecord.getLayer()); + for (var suggestionGroup : suggestionGroups) { + if (suggestionGroup.stream().anyMatch(suggestion -> suggestion.getDocumentName() .equals(aRecord.getSourceDocument().getName()) && suggestion.getFeature().equals(aRecord.getAnnotationFeature().getName()) && suggestion.labelEquals(aRecord.getAnnotation()) @@ -141,170 +131,169 @@ public boolean isSuggestionVisible(LearningRecord aRecord) } @Override - public boolean hasSkippedSuggestions(User aUser, AnnotationLayer aLayer) + public boolean hasSkippedSuggestions(String aSessionOwner, User aDataOwner, + AnnotationLayer aLayer) { - return learningHistoryService.hasSkippedSuggestions(aUser, aLayer); + return learningHistoryService.hasSkippedSuggestions(aSessionOwner, aDataOwner, aLayer); } @Override - public void hideRejectedOrSkippedAnnotations(User aUser, AnnotationLayer aLayer, - boolean filterSkippedRecommendation, + public void hideRejectedOrSkippedAnnotations(String aSessionOwner, User aDataOwner, + AnnotationLayer aLayer, boolean filterSkippedRecommendation, List> aSuggestionGroups) { - List records = learningHistoryService.listRecords(aUser.getUsername(), - aLayer); + var records = learningHistoryService.listLearningRecords(aSessionOwner, + aDataOwner.getUsername(), aLayer); - for (SuggestionGroup group : aSuggestionGroups) { - for (SpanSuggestion s : group) { + for (var suggestionGroup : aSuggestionGroups) { + for (var suggestion : suggestionGroup) { // If a suggestion is already invisible, we don't need to check if it needs hiding. // Mind that this code does not unhide the suggestion immediately if a user // deletes a skip learning record - it will only get unhidden after the next // prediction run (unless the learning-record-deletion code does an explicit // unhiding). - if (!s.isVisible()) { + if (!suggestion.isVisible()) { continue; } - records.stream() - .filter(r -> r.getSourceDocument().getName().equals(s.getDocumentName()) - && r.getOffsetBegin() == s.getBegin() - && r.getOffsetEnd() == s.getEnd() - && s.labelEquals(r.getAnnotation())) - .forEach(record -> { - if (REJECTED.equals(record.getUserAction())) { - s.hide(FLAG_REJECTED); - } - else if (filterSkippedRecommendation - && SKIPPED.equals(record.getUserAction())) { - s.hide(FLAG_SKIPPED); - } - }); - + records.stream().filter( + r -> r.getSourceDocument().getName().equals(suggestion.getDocumentName()) + && r.getOffsetBegin() == suggestion.getBegin() + && r.getOffsetEnd() == suggestion.getEnd() + && suggestion.labelEquals(r.getAnnotation())) + .forEach(record -> suggestion.hideSuggestion(record.getUserAction())); } } } @Override - public Optional> generateNextSuggestion(User aUser, - ActiveLearningUserState alState) + public Optional> generateNextSuggestion(String aSessionOwner, + User aDataOwner, ActiveLearningUserState alState) { // Fetch the next suggestion to present to the user (if there is any) long startTimer = System.currentTimeMillis(); - List> suggestions = alState.getSuggestions(); + var suggestionGroups = alState.getSuggestions(); long getRecommendationsFromRecommendationService = System.currentTimeMillis(); - log.trace("Getting recommendations from recommender system costs {} ms.", + log.trace("Getting recommendations from recommender system took {} ms.", (getRecommendationsFromRecommendationService - startTimer)); // remove duplicate recommendations - suggestions = suggestions.stream() // + suggestionGroups = suggestionGroups.stream() // .map(it -> removeDuplicateRecommendations(it)) // - .collect(Collectors.toList()); + .collect(toList()); long removeDuplicateRecommendation = System.currentTimeMillis(); - log.trace("Removing duplicate recommendations costs {} ms.", + log.trace("Removing duplicate recommendations took {} ms.", (removeDuplicateRecommendation - getRecommendationsFromRecommendationService)); // hide rejected recommendations - hideRejectedOrSkippedAnnotations(aUser, alState.getLayer(), true, suggestions); + hideRejectedOrSkippedAnnotations(aSessionOwner, aDataOwner, alState.getLayer(), true, + suggestionGroups); long removeRejectedSkippedRecommendation = System.currentTimeMillis(); - log.trace("Removing rejected or skipped ones costs {} ms.", + log.trace("Removing rejected or skipped ones took {} ms.", (removeRejectedSkippedRecommendation - removeDuplicateRecommendation)); - Preferences pref = recommendationService.getPreferences(aUser, + var pref = recommendationService.getPreferences(aDataOwner, alState.getLayer().getProject()); - return alState.getStrategy().generateNextSuggestion(pref, suggestions); + return alState.getStrategy().generateNextSuggestion(pref, suggestionGroups); } @Override @Transactional - public void writeLearningRecordInDatabaseAndEventLog(User aUser, AnnotationLayer aLayer, - SpanSuggestion aSuggestion, LearningRecordType aUserAction, String aAnnotationValue) + public void acceptSpanSuggestion(SourceDocument aDocument, User aDataOwner, + SpanSuggestion aSuggestion, Object aValue) + throws IOException, AnnotationException { + // Upsert an annotation based on the suggestion + var layer = schemaService.getLayer(aDocument.getProject(), aSuggestion.getLayerId()) + .orElseThrow(() -> new IllegalArgumentException( + "No such layer: [" + aSuggestion.getLayerId() + "]")); + var feature = schemaService.getFeature(aSuggestion.getFeature(), layer); + var adapter = (SpanAdapter) schemaService.getAdapter(layer); - AnnotationFeature feat = schemaService.getFeature(aSuggestion.getFeature(), aLayer); - SourceDocument sourceDoc = documentService.getSourceDocument(aLayer.getProject(), + // Load CAS in which to create the annotation. This might be different from the one that + // is currently viewed by the user, e.g. if the user switched to another document after + // the suggestion has been loaded into the sidebar. + var sessionOwner = userService.getCurrentUsername(); + var dataOwner = aDataOwner.getUsername(); + var cas = documentService.readAnnotationCas(aDocument, dataOwner); + var document = documentService.getSourceDocument(feature.getProject(), aSuggestion.getDocumentName()); - List alternativeSuggestions = recommendationService - .getPredictions(aUser, aLayer.getProject()) - .getPredictionsByTokenAndFeature(aSuggestion.getDocumentName(), aLayer, - aSuggestion.getBegin(), aSuggestion.getEnd(), aSuggestion.getFeature()); + // Create AnnotationFeature and FeatureSupport + var featureSupport = featureSupportRegistry.findExtension(feature).orElseThrow(); + var label = (String) featureSupport.unwrapFeatureValue(feature, cas, aValue); - // Log the action to the learning record - learningHistoryService.logSpanRecord(sourceDoc, aUser.getUsername(), aSuggestion, - aAnnotationValue, aLayer, feat, aUserAction, AL_SIDEBAR); + // Clone of the original suggestion with the selected by the user + var suggestionWithUserSelectedLabel = aSuggestion.toBuilder().withLabel(label).build(); // If the action was a correction (i.e. suggestion label != annotation value) then generate // a rejection for the original value - we do not want the original value to re-appear - if (aUserAction == CORRECTED) { - learningHistoryService.logSpanRecord(sourceDoc, aUser.getUsername(), aSuggestion, - aSuggestion.getLabel(), aLayer, feat, REJECTED, AL_SIDEBAR); + var action = aSuggestion.labelEquals(label) ? ACCEPTED : CORRECTED; + if (action == CORRECTED) { + recommendationService.correctSuggestion(sessionOwner, aDocument, dataOwner, cas, + adapter, feature, aSuggestion, suggestionWithUserSelectedLabel, AL_SIDEBAR); } + else { + recommendationService.acceptSuggestion(sessionOwner, aDocument, dataOwner, cas, adapter, + feature, suggestionWithUserSelectedLabel, AL_SIDEBAR); + } + + // Save CAS after annotation has been created + documentService.writeAnnotationCas(cas, aDocument, aDataOwner, true); // Send an application event indicating if the user has accepted/skipped/corrected/rejected // the suggestion - applicationEventPublisher.publishEvent(new ActiveLearningRecommendationEvent(this, - sourceDoc, aSuggestion, aUser.getUsername(), aLayer, aSuggestion.getFeature(), - aUserAction, alternativeSuggestions)); + var alternativeSuggestions = recommendationService + .getPredictions(aDataOwner, feature.getProject()) + .getPredictionsByTokenAndFeature(suggestionWithUserSelectedLabel.getDocumentName(), + feature.getLayer(), suggestionWithUserSelectedLabel.getBegin(), + suggestionWithUserSelectedLabel.getEnd(), + suggestionWithUserSelectedLabel.getFeature()); + applicationEventPublisher.publishEvent(new ActiveLearningRecommendationEvent(this, document, + suggestionWithUserSelectedLabel, dataOwner, feature.getLayer(), + suggestionWithUserSelectedLabel.getFeature(), action, alternativeSuggestions)); } @Override @Transactional - public void acceptSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion, - Object aValue) - throws IOException, AnnotationException + public void rejectSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer, + SpanSuggestion aSuggestion) { - // There is always a current recommendation when we get here because if there is none, the - // button to accept the recommendation is not visible. - SpanSuggestion suggestion = aSuggestion; - - // Create AnnotationFeature and FeatureSupport - AnnotationFeature feat = schemaService.getFeature(suggestion.getFeature(), aLayer); - FeatureSupport featureSupport = featureSupportRegistry.findExtension(feat).orElseThrow(); - - // Load CAS in which to create the annotation. This might be different from the one that - // is currently viewed by the user, e.g. if the user switched to another document after - // the suggestion has been loaded into the sidebar. - SourceDocument sourceDoc = documentService.getSourceDocument(aLayer.getProject(), - suggestion.getDocumentName()); - String username = aUser.getUsername(); - CAS cas = documentService.readAnnotationCas(sourceDoc, username); - - // Upsert an annotation based on the suggestion - String value = (String) featureSupport.unwrapFeatureValue(feat, cas, aValue); - AnnotationLayer layer = schemaService.getLayer(aLayer.getProject(), suggestion.getLayerId()) - .orElseThrow(() -> new IllegalArgumentException( - "No such layer: [" + suggestion.getLayerId() + "]")); - - // Log the action to the learning record and immediately hide the suggestion - boolean areLabelsEqual = suggestion.labelEquals(value); - writeLearningRecordInDatabaseAndEventLog(aUser, aLayer, suggestion, - (areLabelsEqual) ? ACCEPTED : CORRECTED, value); - suggestion.hide((areLabelsEqual) ? FLAG_TRANSIENT_ACCEPTED : FLAG_TRANSIENT_CORRECTED); - - // Request clearing selection and when onFeatureValueUpdated is triggered as a callback - // from the update event created by upsertSpanFeature. - AnnotationFeature feature = schemaService.getFeature(suggestion.getFeature(), layer); - recommendationService.upsertSpanFeature(schemaService, sourceDoc, username, cas, layer, - feature, value, suggestion.getBegin(), suggestion.getEnd()); + var document = documentService.getSourceDocument(aLayer.getProject(), + aSuggestion.getDocumentName()); + recommendationService.rejectSuggestion(aSessionOwner, document, aDataOwner.getUsername(), + aSuggestion, AL_SIDEBAR); - // Save CAS after annotation has been created - documentService.writeAnnotationCas(cas, sourceDoc, aUser, true); + // Send an application event indicating if the user has accepted/skipped/corrected/rejected + // the suggestion + var alternativeSuggestions = recommendationService + .getPredictions(aDataOwner, aLayer.getProject()) + .getPredictionsByTokenAndFeature(aSuggestion.getDocumentName(), aLayer, + aSuggestion.getBegin(), aSuggestion.getEnd(), aSuggestion.getFeature()); + applicationEventPublisher.publishEvent(new ActiveLearningRecommendationEvent(this, document, + aSuggestion, aDataOwner.getUsername(), aLayer, aSuggestion.getFeature(), REJECTED, + alternativeSuggestions)); } @Override @Transactional - public void rejectSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion) + public void skipSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer, + SpanSuggestion aSuggestion) { - writeLearningRecordInDatabaseAndEventLog(aUser, aLayer, aSuggestion, REJECTED, - aSuggestion.getLabel()); - } + var document = documentService.getSourceDocument(aLayer.getProject(), + aSuggestion.getDocumentName()); + recommendationService.skipSuggestion(aSessionOwner, document, aDataOwner.getUsername(), + aSuggestion, AL_SIDEBAR); - @Override - @Transactional - public void skipSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggestion aSuggestion) - { - writeLearningRecordInDatabaseAndEventLog(aUser, aLayer, aSuggestion, SKIPPED, - aSuggestion.getLabel()); + // Send an application event indicating if the user has accepted/skipped/corrected/rejected + // the suggestion + var alternativeSuggestions = recommendationService + .getPredictions(aDataOwner, aLayer.getProject()) + .getPredictionsByTokenAndFeature(aSuggestion.getDocumentName(), aLayer, + aSuggestion.getBegin(), aSuggestion.getEnd(), aSuggestion.getFeature()); + applicationEventPublisher.publishEvent(new ActiveLearningRecommendationEvent(this, document, + aSuggestion, aDataOwner.getUsername(), aLayer, aSuggestion.getFeature(), SKIPPED, + alternativeSuggestions)); } private static SuggestionGroup removeDuplicateRecommendations( diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/event/ActiveLearningRecommendationEvent.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/event/ActiveLearningRecommendationEvent.java index c21696f830d..5395cb22ef2 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/event/ActiveLearningRecommendationEvent.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/event/ActiveLearningRecommendationEvent.java @@ -34,21 +34,21 @@ public class ActiveLearningRecommendationEvent private final SourceDocument document; private final SpanSuggestion currentRecommendation; - private final String user; + private final String dataOwner; private final AnnotationLayer layer; private final String annotationFeature; private final LearningRecordType action; private final List allRecommendations; public ActiveLearningRecommendationEvent(Object aSource, SourceDocument aDocument, - SpanSuggestion aCurrentRecommendation, String aUser, AnnotationLayer aLayer, + SpanSuggestion aCurrentRecommendation, String aDataOwner, AnnotationLayer aLayer, String aAnnotationFeature, LearningRecordType aAction, List aAllRecommendations) { super(aSource); document = aDocument; currentRecommendation = aCurrentRecommendation; - user = aUser; + dataOwner = aDataOwner; layer = aLayer; annotationFeature = aAnnotationFeature; action = aAction; @@ -67,7 +67,7 @@ public SpanSuggestion getCurrentRecommendation() public String getUser() { - return user; + return dataOwner; } public AnnotationLayer getLayer() diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.java index 9f9adbb2eec..c5b026063d0 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.java @@ -69,7 +69,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.keybindings.KeyBindingsPanel; -import de.tudarmstadt.ukp.clarin.webanno.api.event.AfterDocumentResetEvent; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; @@ -96,6 +95,7 @@ import de.tudarmstadt.ukp.inception.active.learning.event.ActiveLearningSessionStartedEvent; import de.tudarmstadt.ukp.inception.active.learning.event.ActiveLearningSuggestionOfferedEvent; import de.tudarmstadt.ukp.inception.active.learning.strategy.UncertaintySamplingStrategy; +import de.tudarmstadt.ukp.inception.annotation.events.DocumentOpenedEvent; import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationCreatedEvent; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanCreatedEvent; @@ -171,7 +171,7 @@ public class ActiveLearningSidebar private @SpringBean LearningRecordService learningRecordService; private @SpringBean DocumentService documentService; private @SpringBean ApplicationEventPublisherHolder applicationEventPublisherHolder; - private @SpringBean UserDao userDao; + private @SpringBean UserDao userService; private @SpringBean FeatureSupportRegistry featureSupportRegistry; private IModel> learningRecords; @@ -309,7 +309,7 @@ private void actionStartSession(AjaxRequestTarget aTarget, Form form) recommendationService.setPredictForAllDocuments(userName, project, true); recommendationService.triggerPrediction(userName, "ActionStartActiveLearningSession", - state.getDocument()); + state.getDocument(), state.getUser().getUsername()); // Start new session alState.setSessionActive(true); @@ -423,19 +423,23 @@ private Label createNoRecommendationLabel() { Label noRecommendation = new Label(CID_NO_RECOMMENDATION_LABEL, "There are no further suggestions."); - noRecommendation.add(visibleWhen(alStateModel.map(alState -> alState.isSessionActive() - && !alState.getSuggestion().isPresent() && !activeLearningService - .hasSkippedSuggestions(getModelObject().getUser(), alState.getLayer())))); + noRecommendation.add(visibleWhen(alStateModel + .map(alState -> alState.isSessionActive() && !alState.getSuggestion().isPresent() + && !activeLearningService.hasSkippedSuggestions( + userService.getCurrentUsername(), getModelObject().getUser(), + alState.getLayer())))); noRecommendation.setOutputMarkupPlaceholderTag(true); return noRecommendation; } private Form clearSkippedRecommendationForm() { - Form form = new Form<>(CID_LEARN_FROM_SKIPPED_RECOMMENDATION_FORM); - form.add(visibleWhen(alStateModel.map(alState -> alState.isSessionActive() - && !alState.getSuggestion().isPresent() && activeLearningService - .hasSkippedSuggestions(getModelObject().getUser(), alState.getLayer())))); + var form = new Form(CID_LEARN_FROM_SKIPPED_RECOMMENDATION_FORM); + form.add(visibleWhen(alStateModel + .map(alState -> alState.isSessionActive() && !alState.getSuggestion().isPresent() + && activeLearningService.hasSkippedSuggestions( + userService.getCurrentUsername(), getModelObject().getUser(), + alState.getLayer())))); form.setOutputMarkupPlaceholderTag(true); form.add(new Label(CID_ONLY_SKIPPED_RECOMMENDATION_LABEL, "There are only skipped suggestions. Do you want to learn these again?")); @@ -447,8 +451,8 @@ private Form clearSkippedRecommendationForm() private void actionClearSkippedRecommendations(AjaxRequestTarget aTarget, Form aForm) throws IOException { - learningRecordService.deleteSkippedSuggestions(getModelObject().getUser(), - alStateModel.getObject().getLayer()); + learningRecordService.deleteSkippedSuggestions(userService.getCurrentUsername(), + getModelObject().getUser(), alStateModel.getObject().getLayer()); // The history records caused suggestions to disappear. Since visibility is only fully // recalculated when new predictions come in, we need to update the visibility explicitly @@ -466,7 +470,7 @@ private Form createRecommendationOperationForm() { recommendationForm = new Form(CID_RECOMMENDATION_FORM); recommendationForm.add(visibleWhen(() -> { - ActiveLearningUserState alState = alStateModel.getObject(); + var alState = alStateModel.getObject(); return alState.isSessionActive() && alState.getSuggestion().isPresent(); })); recommendationForm.setOutputMarkupPlaceholderTag(true); @@ -552,48 +556,64 @@ private void refreshActiveLearningFeatureEditor(IPartialPageRequestHandler aTarg aTarget.add(alMainContainer); } - private FeatureEditor createFeatureEditor(SpanSuggestion aCurrentRecommendation) + private FeatureEditor createFeatureEditor(SpanSuggestion aSuggestion) { - AnnotatorState state = ActiveLearningSidebar.this.getModelObject(); - ActiveLearningUserState alState = alStateModel.getObject(); + var state = ActiveLearningSidebar.this.getModelObject(); + var alState = alStateModel.getObject(); // Obtain the feature state which serves as a model to the editor - AnnotationFeature feat = annotationService.getFeature(aCurrentRecommendation.getFeature(), - alState.getLayer()); + var feat = annotationService.getFeature(aSuggestion.getFeature(), alState.getLayer()); FeatureSupport featureSupport = featureSupportRegistry.findExtension(feat).orElseThrow(); // We get away with passing "null" here instead of the CAS because we currently have no // recommenders for any feature types that actually need the CAS (i.e. link feature types // and the likes). - Object wrappedFeatureValue = featureSupport.wrapFeatureValue(feat, null, - aCurrentRecommendation.getLabel()); - FeatureState featureState = new FeatureState(aCurrentRecommendation.getVID(), feat, + var wrappedFeatureValue = featureSupport.wrapFeatureValue(feat, null, + aSuggestion.getLabel()); + var featureState = new FeatureState(aSuggestion.getVID(), feat, (Serializable) wrappedFeatureValue); // Populate the tagset moving the tags with recommended labels to the top - List tagList = annotationService.listTagsReorderable(feat.getTagset()); - List reorderedTagList = new ArrayList<>(); + var tagList = annotationService.listTagsReorderable(feat.getTagset()); + featureState.tagset = orderTagList(aSuggestion, tagList); + + // Finally, create the editor + var featureEditor = featureSupport.createEditor(CID_EDITOR, alMainContainer, + this.getActionHandler(), this.getModel(), Model.of(featureState)); + featureEditor.setOutputMarkupPlaceholderTag(true); + featureEditor.add(visibleWhen(() -> alStateModel.getObject().getLayer() != null + && alState.getSuggestion().isPresent())); + + // We do not want key bindings in the active learning sidebar + featureEditor.visitChildren(KeyBindingsPanel.class, + (c, v) -> c.setVisibilityAllowed(false)); + + return featureEditor; + } + + private List orderTagList(SpanSuggestion aSuggestion, + List tagList) + { + var state = ActiveLearningSidebar.this.getModelObject(); + var reorderedTagList = new ArrayList(); + if (tagList.size() > 0) { var predictions = recommendationService.getPredictions(state.getUser(), state.getProject()); // get all the predictions - List allRecommendations = predictions.getPredictionsByTokenAndFeature( - aCurrentRecommendation.getDocumentName(), alState.getLayer(), - aCurrentRecommendation.getBegin(), aCurrentRecommendation.getEnd(), - aCurrentRecommendation.getFeature()); + var alternativeSuggestions = predictions.getAlternativeSuggestions(aSuggestion); // Get all the label of the predictions (e.g. "NN"). var allLabels = new LinkedHashMap(); - allRecommendations.stream() // + alternativeSuggestions.stream() // .filter(AnnotationSuggestion::isVisible) // // We filter for recommendations from the same recommender as the current // suggestion to assess comes from because scores may not be comparable // across recommenders - .filter(rec -> rec.getRecommenderId() == aCurrentRecommendation - .getRecommenderId()) + .filter(rec -> rec.getRecommenderId() == aSuggestion.getRecommenderId()) .forEachOrdered(rec -> { - if (Objects.equals(rec.getLabel(), aCurrentRecommendation.getLabel())) { - allLabels.put(rec.getLabel(), aCurrentRecommendation); + if (Objects.equals(rec.getLabel(), aSuggestion.getLabel())) { + allLabels.put(rec.getLabel(), aSuggestion); } else { var existingSuggestion = allLabels.get(rec.getLabel()); @@ -604,9 +624,9 @@ private FeatureEditor createFeatureEditor(SpanSuggestion aCurrentRecommendation) } }); - for (ReorderableTag tag : tagList) { + for (var tag : tagList) { // add the tags which contain the prediction-labels to the beginning of a tagset - SpanSuggestion suggestion = allLabels.get(tag.getName()); + var suggestion = allLabels.get(tag.getName()); if (suggestion != null) { tag.setReordered(true); tag.setScore(format("%.3f", suggestion.getScore())); @@ -620,20 +640,8 @@ private FeatureEditor createFeatureEditor(SpanSuggestion aCurrentRecommendation) // add the rest tags to the tagset after these reorderedTagList.addAll(tagList); } - featureState.tagset = reorderedTagList; - - // Finally, create the editor - FeatureEditor featureEditor = featureSupport.createEditor(CID_EDITOR, alMainContainer, - this.getActionHandler(), this.getModel(), Model.of(featureState)); - featureEditor.setOutputMarkupPlaceholderTag(true); - featureEditor.add(visibleWhen(() -> alStateModel.getObject().getLayer() != null - && alState.getSuggestion().isPresent())); - - // We do not want key bindings in the active learning sidebar - featureEditor.visitChildren(KeyBindingsPanel.class, - (c, v) -> c.setVisibilityAllowed(false)); - return featureEditor; + return reorderedTagList; } /** @@ -653,50 +661,59 @@ private void actionAnnotate(AjaxRequestTarget aTarget, Form aForm) { LOG.trace("actionAnnotate()"); - AnnotatorState state = getModelObject(); - ActiveLearningUserState alState = alStateModel.getObject(); + getAnnotationPage().ensureIsEditable(); + + var state = getModelObject(); + var alState = alStateModel.getObject(); // There is always a current recommendation when we get here because if there is none, the // button to accept the recommendation is not visible. - SpanSuggestion suggestion = alState.getSuggestion().get(); + var suggestion = alState.getSuggestion().get(); // Request clearing selection and when onFeatureValueUpdated is triggered as a callback // from the update event created by acceptSuggestion/upsertSpanFeature. requestClearningSelectionAndJumpingToSuggestion(); - activeLearningService.acceptSpanSuggestion(state.getUser(), alState.getLayer(), suggestion, + var document = documentService.getSourceDocument(state.getProject(), + suggestion.getDocumentName()); + activeLearningService.acceptSpanSuggestion(document, state.getUser(), suggestion, editor.getModelObject().value); // If the currently displayed document is the same one where the annotation was created, // then update timestamp in state to avoid concurrent modification errors - SourceDocument sourceDoc = documentService.getSourceDocument(state.getProject(), - suggestion.getDocumentName()); - if (Objects.equals(state.getDocument().getId(), sourceDoc.getId())) { - documentService.getAnnotationCasTimestamp(sourceDoc, state.getUser().getUsername()) + if (Objects.equals(state.getDocument().getId(), document.getId())) { + documentService.getAnnotationCasTimestamp(document, state.getUser().getUsername()) .ifPresent(state::setAnnotationDocumentTimestamp); } } - private void actionSkip(AjaxRequestTarget aTarget) + private void actionSkip(AjaxRequestTarget aTarget) throws AnnotationException + { LOG.trace("actionSkip()"); + getAnnotationPage().ensureIsEditable(); + + var sessionOwner = userService.getCurrentUsername(); + alStateModel.getObject().getSuggestion().ifPresent(suggestion -> { requestClearningSelectionAndJumpingToSuggestion(); - activeLearningService.skipSpanSuggestion(getModelObject().getUser(), + activeLearningService.skipSpanSuggestion(sessionOwner, getModelObject().getUser(), alStateModel.getObject().getLayer(), suggestion); moveToNextSuggestion(aTarget); }); } - private void actionReject(AjaxRequestTarget aTarget) + private void actionReject(AjaxRequestTarget aTarget) throws AnnotationException { LOG.trace("actionReject()"); + getAnnotationPage().ensureIsEditable(); + alStateModel.getObject().getSuggestion().ifPresent(suggestion -> { requestClearningSelectionAndJumpingToSuggestion(); - activeLearningService.rejectSpanSuggestion(getModelObject().getUser(), - alStateModel.getObject().getLayer(), suggestion); + activeLearningService.rejectSpanSuggestion(userService.getCurrentUsername(), + getModelObject().getUser(), alStateModel.getObject().getLayer(), suggestion); moveToNextSuggestion(aTarget); }); } @@ -708,14 +725,16 @@ private void moveToNextSuggestion(AjaxRequestTarget aTarget) // Ensure that predictions are switched annotationPage.actionRefreshDocument(aTarget); - ActiveLearningUserState alState = alStateModel.getObject(); - AnnotatorState state = getModelObject(); - Project project = state.getProject(); - User user = state.getUser(); + var alState = alStateModel.getObject(); + var state = getModelObject(); + var project = state.getProject(); + var dataOwner = state.getUser(); + var sessionOwner = userService.getCurrentUser(); // Generate the next recommendation but remember the current one Optional prevSuggestion = alState.getSuggestion(); - alState.setCurrentDifference(activeLearningService.generateNextSuggestion(user, alState)); + alState.setCurrentDifference(activeLearningService + .generateNextSuggestion(sessionOwner.getUsername(), dataOwner, alState)); // If there is no new suggestion, nothing left to do here if (!alState.getSuggestion().isPresent()) { @@ -737,10 +756,9 @@ private void moveToNextSuggestion(AjaxRequestTarget aTarget) } // If there is a suggestion, open it in the sidebar and take the main editor to its location - SpanSuggestion suggestion = alState.getSuggestion().get(); - SourceDocument sourceDocument = documentService.getSourceDocument(project, - suggestion.getDocumentName()); - loadSuggestionInActiveLearningSidebar(aTarget, suggestion, sourceDocument); + var suggestion = alState.getSuggestion().get(); + var document = documentService.getSourceDocument(project, suggestion.getDocumentName()); + loadSuggestionInActiveLearningSidebar(aTarget, suggestion, document); if (shouldBeClearningSelectionAndJumpingToSuggestion()) { clearSelectedAnnotationAndJumpToSuggestion(aTarget); @@ -750,14 +768,11 @@ private void moveToNextSuggestion(AjaxRequestTarget aTarget) LOG.trace("Not clearing and jumping"); } - List alternativeSuggestions = recommendationService - .getPredictions(user, project) - .getPredictionsByTokenAndFeature(suggestion.getDocumentName(), alState.getLayer(), - suggestion.getBegin(), suggestion.getEnd(), suggestion.getFeature()); - + var alternativeSuggestions = recommendationService.getPredictions(dataOwner, project) + .getAlternativeSuggestions(suggestion); applicationEventPublisherHolder.get() - .publishEvent(new ActiveLearningSuggestionOfferedEvent(this, sourceDocument, - suggestion, user.getUsername(), alState.getLayer(), suggestion.getFeature(), + .publishEvent(new ActiveLearningSuggestionOfferedEvent(this, document, suggestion, + dataOwner.getUsername(), alState.getLayer(), suggestion.getFeature(), alternativeSuggestions)); } @@ -968,19 +983,22 @@ private List getMatchingSuggestion( private List listLearningRecords() { - return learningRecordService.listRecords(getModelObject().getUser().getUsername(), - alStateModel.getObject().getLayer(), 50); + var sessionOwner = userService.getCurrentUsername(); + return learningRecordService.listLearningRecords(sessionOwner, + getModelObject().getUser().getUsername(), alStateModel.getObject().getLayer(), 50); } private void actionRemoveHistoryItem(AjaxRequestTarget aTarget, LearningRecord aRecord) - throws IOException + throws IOException, AnnotationException { + getAnnotationPage().ensureIsEditable(); + aTarget.add(alMainContainer); - ActiveLearningUserState alState = alStateModel.getObject(); + var alState = alStateModel.getObject(); annotationPage.actionRefreshDocument(aTarget); - learningRecordService.delete(aRecord); + learningRecordService.deleteLearningRecord(aRecord); // The history records caused suggestions to disappear. Since visibility is only fully // recalculated when new predictions come in, we need to update the visibility explicitly @@ -1033,7 +1051,7 @@ private void openHistoryItemRemovalConfirmationDialog(AjaxRequestTarget aTarget, } deleteAnnotationByHistory(_t, aRecord); }); - + dialogContent.setCancelAction(_t -> { if (alStateModel.getObject().getSuggestion().isPresent()) { setActiveLearningHighlight(alStateModel.getObject().getSuggestion().get()); @@ -1071,7 +1089,7 @@ public void onSpanCreated(SpanCreatedEvent aEvent) } // Does event match the current active learning configuration? - if (!aEvent.getUser().equals(getModelObject().getUser().getUsername()) + if (!aEvent.getDocumentOwner().equals(getModelObject().getUser().getUsername()) || !aEvent.getLayer().equals(alState.getLayer())) { return; } @@ -1090,7 +1108,7 @@ public void onRelationCreated(RelationCreatedEvent aEvent) } // Does event match the current active learning configuration? - if (!aEvent.getUser().equals(getModelObject().getUser().getUsername()) + if (!aEvent.getDocumentOwner().equals(getModelObject().getUser().getUsername()) || !aEvent.getLayer().equals(alState.getLayer())) { return; } @@ -1119,7 +1137,7 @@ public void onFeatureValueUpdated(FeatureValueUpdatedEvent aEvent) } // Does event match the current active learning configuration? - if (!aEvent.getUser().equals(getModelObject().getUser().getUsername()) + if (!aEvent.getDocumentOwner().equals(getModelObject().getUser().getUsername()) || !aEvent.getFeature().getLayer().equals(alState.getLayer()) || !aEvent.getFeature().getName() .equals(alState.getSuggestion().get().getFeature())) { @@ -1148,7 +1166,7 @@ public void onAnnotationDeleted(SpanDeletedEvent aEvent) } // Does event match the current active learning configuration? - if (!aEvent.getUser().equals(getModelObject().getUser().getUsername()) + if (!aEvent.getDocumentOwner().equals(getModelObject().getUser().getUsername()) || !aEvent.getLayer().equals(alState.getLayer())) { return; } @@ -1163,9 +1181,9 @@ private void reactToAnnotationsBeingCreatedOrDeleted(AjaxRequestTarget aTarget, LOG.trace("reactToAnnotationsBeingCreatedOrDeleted()"); try { - User user = getModelObject().getUser(); - Predictions predictions = recommendationService.getPredictions(user, - aLayer.getProject()); + var sessionOwner = userService.getCurrentUsername(); + var dataOwner = getModelObject().getUser(); + var predictions = recommendationService.getPredictions(dataOwner, aLayer.getProject()); if (predictions == null) { return; @@ -1176,12 +1194,11 @@ private void reactToAnnotationsBeingCreatedOrDeleted(AjaxRequestTarget aTarget, annotationPage.actionRefreshDocument(aTarget); // Update visibility in case the that was created/deleted overlaps with any suggestions - CAS cas = documentService.readAnnotationCas(aDocument, user.getUsername()); - SuggestionDocumentGroup group = SuggestionDocumentGroup.groupsOfType( - SpanSuggestion.class, + var cas = documentService.readAnnotationCas(aDocument, dataOwner.getUsername()); + var group = SuggestionDocumentGroup.groupsOfType(SpanSuggestion.class, predictions.getPredictionsByDocument(aDocument.getName())); - recommendationService.calculateSpanSuggestionVisibility(aDocument, cas, - user.getUsername(), aLayer, group, 0, cas.getDocumentText().length()); + recommendationService.calculateSpanSuggestionVisibility(sessionOwner, aDocument, cas, + dataOwner.getUsername(), aLayer, group, 0, cas.getDocumentText().length()); moveToNextSuggestion(aTarget); } @@ -1208,8 +1225,9 @@ public void onRecommendationRejectEvent(AjaxRecommendationRejectedEvent aEvent) && eventState.getProject().equals(annotatorState.getProject())) { var doc = eventState.getDocument(); var vid = VID.parse(aEvent.getVid().getExtensionPayload()); - var prediction = predictions.getPredictionByVID(doc, vid) - .filter(f -> f instanceof SpanSuggestion).map(f -> (SpanSuggestion) f); + var prediction = predictions.getPredictionByVID(doc, vid) // + .filter(f -> f instanceof SpanSuggestion) // + .map(f -> (SpanSuggestion) f); if (!prediction.isPresent()) { LOG.error("Could not find prediction in [{}] with id [{}]", doc, vid); @@ -1218,17 +1236,14 @@ public void onRecommendationRejectEvent(AjaxRecommendationRejectedEvent aEvent) } var rejectedRecommendation = prediction.get(); - applicationEventPublisherHolder.get().publishEvent( - new ActiveLearningRecommendationEvent(this, eventState.getDocument(), - rejectedRecommendation, annotatorState.getUser().getUsername(), + var alternativeSuggestions = predictions + .getAlternativeSuggestions(rejectedRecommendation); + applicationEventPublisherHolder.get() + .publishEvent(new ActiveLearningRecommendationEvent(this, + eventState.getDocument(), rejectedRecommendation, + annotatorState.getUser().getUsername(), eventState.getSelectedAnnotationLayer(), - rejectedRecommendation.getFeature(), REJECTED, - predictions.getPredictionsByTokenAndFeature( - rejectedRecommendation.getDocumentName(), - eventState.getSelectedAnnotationLayer(), - rejectedRecommendation.getBegin(), - rejectedRecommendation.getEnd(), - rejectedRecommendation.getFeature()))); + rejectedRecommendation.getFeature(), REJECTED, alternativeSuggestions)); if (doc.equals(annotatorState.getDocument()) && vid.getLayerId() == alStateModel.getObject().getLayer().getId() && prediction @@ -1236,6 +1251,7 @@ public void onRecommendationRejectEvent(AjaxRecommendationRejectedEvent aEvent) requestClearningSelectionAndJumpingToSuggestion(); moveToNextSuggestion(aEvent.getTarget()); } + aEvent.getTarget().add(alMainContainer); } } @@ -1253,9 +1269,8 @@ public void onRecommendationAcceptEvent(AjaxRecommendationAcceptedEvent aEvent) var state = getModelObject(); var predictions = recommendationService.getPredictions(state.getUser(), state.getProject()); - var eventState = aEvent.getAnnotatorState(); var doc = state.getDocument(); - var vid = VID.parse(aEvent.getVid().getExtensionPayload()); + var vid = VID.parse(aEvent.getSuggestionVid().getExtensionPayload()); var oRecommendation = predictions.getPredictionByVID(doc, vid) // .filter(f -> f instanceof SpanSuggestion) // @@ -1269,19 +1284,18 @@ public void onRecommendationAcceptEvent(AjaxRecommendationAcceptedEvent aEvent) var acceptedSuggestion = oRecommendation.get(); - applicationEventPublisherHolder.get().publishEvent(new ActiveLearningRecommendationEvent( - this, eventState.getDocument(), acceptedSuggestion, state.getUser().getUsername(), - eventState.getSelectedAnnotationLayer(), acceptedSuggestion.getFeature(), ACCEPTED, - predictions.getPredictionsByTokenAndFeature(acceptedSuggestion.getDocumentName(), - eventState.getSelectedAnnotationLayer(), acceptedSuggestion.getBegin(), - acceptedSuggestion.getEnd(), acceptedSuggestion.getFeature()))); + var alternativeSuggestions = predictions.getAlternativeSuggestions(acceptedSuggestion); + applicationEventPublisherHolder.get() + .publishEvent(new ActiveLearningRecommendationEvent(this, aEvent.getDocument(), + acceptedSuggestion, state.getUser().getUsername(), aEvent.getLayer(), + acceptedSuggestion.getFeature(), ACCEPTED, alternativeSuggestions)); // If the annotation that the user accepted is the one that is currently displayed in // the annotation sidebar, then we have to go and pick a new one - ActiveLearningUserState alState = alStateModel.getObject(); + var alState = alStateModel.getObject(); if (alState.isSessionActive() && alState.getSuggestion().isPresent() - && eventState.getUser().equals(state.getUser()) - && eventState.getProject().equals(state.getProject())) { + && aEvent.getDataOwner().equals(state.getUser()) + && aEvent.getProject().equals(state.getProject())) { SpanSuggestion suggestion = alState.getSuggestion().get(); if (acceptedSuggestion.getPosition().equals(suggestion.getPosition()) && vid.getLayerId() == suggestion.getLayerId() @@ -1353,7 +1367,7 @@ private void refreshAvailableSuggestions() } @OnEvent - public void onDocumentReset(AfterDocumentResetEvent aEvent) + public void onDocumentOpenedEvent(DocumentOpenedEvent aEvent) { // If active learning is not active, update the sidebar in case the session auto-terminated ActiveLearningUserState alState = alStateModel.getObject(); diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebarIcon.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebarIcon.java index 1574b1089e2..51c19214910 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebarIcon.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebarIcon.java @@ -48,9 +48,9 @@ public class ActiveLearningSidebarIcon public ActiveLearningSidebarIcon(String aId, IModel aState) { super(aId, aState); - + setOutputMarkupId(true); - + queue(new Icon("icon", FontAwesome5IconType.robot_s)); queue(new Icon("badge", LoadableDetachableModel.of(this::getStateIcon)) .add(new ClassAttributeModifier() @@ -99,14 +99,16 @@ private IconType getStateIcon() return FontAwesome5IconType.stop_circle_s; } - + @OnEvent - public void sessionStarted(ActiveLearningSessionStartedEvent aEvent) { + public void sessionStarted(ActiveLearningSessionStartedEvent aEvent) + { aEvent.getRequestTarget().add(this); } @OnEvent - public void sessionStarted(ActiveLearningSessionCompletedEvent aEvent) { + public void sessionStarted(ActiveLearningSessionCompletedEvent aEvent) + { aEvent.getRequestTarget().add(this); } } diff --git a/inception/inception-agreement/pom.xml b/inception/inception-agreement/pom.xml index 485da6c2098..6f4d888a263 100644 --- a/inception/inception-agreement/pom.xml +++ b/inception/inception-agreement/pom.xml @@ -20,7 +20,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 27.0-SNAPSHOT + 29.0-SNAPSHOT inception-agreement INCEpTION - Core - Agreement @@ -114,6 +114,10 @@ de.agilecoders.wicket wicket-bootstrap-core + + com.googlecode.wicket-jquery-ui + wicket-jquery-ui-core + org.danekja jdk-serializable-functional diff --git a/inception/inception-annotation-storage/pom.xml b/inception/inception-annotation-storage/pom.xml index 4b42db786d1..a66f8c98e93 100644 --- a/inception/inception-annotation-storage/pom.xml +++ b/inception/inception-annotation-storage/pom.xml @@ -22,7 +22,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 27.0-SNAPSHOT + 29.0-SNAPSHOT inception-annotation-storage INCEpTION - Core - Annotation Storage diff --git a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java index 3963f9e55ec..46e3928d004 100644 --- a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java +++ b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImpl.java @@ -1062,6 +1062,10 @@ private void realWriteCas(SourceDocument aDocument, String aUserName, CAS aCas) { analyze(aDocument.getProject(), aDocument.getName(), aDocument.getId(), aUserName, aCas); + log.debug("Writing annotation document [{}] ({}) for user [{}] in project [{}] ({})", + aDocument.getName(), aDocument.getId(), aUserName, aDocument.getProject().getName(), + aDocument.getProject().getId()); + driver.writeCas(aDocument, aUserName, aCas); } } diff --git a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStorageProperties.java b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStorageProperties.java index 4e3a71c5139..58f90af135c 100644 --- a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStorageProperties.java +++ b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStorageProperties.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.annotation.storage.config; +import java.time.Duration; + public interface CasStorageProperties { boolean isTraceAccess(); @@ -24,4 +26,6 @@ public interface CasStorageProperties boolean isParanoidCasSerialization(); boolean isCompressedCasSerialization(); + + Duration getFileSystemTimestampAccuracy(); } diff --git a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStoragePropertiesImpl.java b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStoragePropertiesImpl.java index c10031d926b..97e051cf506 100644 --- a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStoragePropertiesImpl.java +++ b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/config/CasStoragePropertiesImpl.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.annotation.storage.config; +import java.time.Duration; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; @@ -34,6 +36,7 @@ public class CasStoragePropertiesImpl private boolean compressedCasSerialization = true; private boolean paranoidCasSerialization = false; private boolean traceAccess = false; + private Duration fileSystemTimestampAccuracy = Duration.ofMillis(0); @ManagedAttribute public void setTraceAccess(boolean aTraceAccess) @@ -71,4 +74,17 @@ public boolean isCompressedCasSerialization() { return compressedCasSerialization; } + + @ManagedAttribute + public void setFileSystemTimestampAccuracy(Duration aFileSystemTimestampAccuracy) + { + fileSystemTimestampAccuracy = aFileSystemTimestampAccuracy; + } + + @Override + @ManagedAttribute + public Duration getFileSystemTimestampAccuracy() + { + return fileSystemTimestampAccuracy; + } } diff --git a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/driver/filesystem/FileSystemCasStorageDriver.java b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/driver/filesystem/FileSystemCasStorageDriver.java index d06fc5edcc9..1125bd45266 100644 --- a/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/driver/filesystem/FileSystemCasStorageDriver.java +++ b/inception/inception-annotation-storage/src/main/java/de/tudarmstadt/ukp/inception/annotation/storage/driver/filesystem/FileSystemCasStorageDriver.java @@ -463,7 +463,8 @@ public Optional verifyCasTimestamp(SourceDocument aDocument, String aUser, } long diskLastModified = casFile.lastModified(); - if (diskLastModified != aExpectedTimeStamp) { + if (Math.abs(diskLastModified - aExpectedTimeStamp) > casStorageProperties + .getFileSystemTimestampAccuracy().toMillis()) { StringBuilder lastWriteMsg = new StringBuilder(); if (metadataCache != null) { InternalMetadata meta = metadataCache.get(casFile); @@ -485,7 +486,8 @@ public Optional verifyCasTimestamp(SourceDocument aDocument, String aUser, + " (expected: " + formatTimestamp(aExpectedTimeStamp) + " actual on storage: " + formatTimestamp(diskLastModified) + ", delta: " + formatDurationHMS(Math.abs(diskLastModified - aExpectedTimeStamp)) + ")" - + lastWriteMsg); + + lastWriteMsg + ", accuracy: " + + casStorageProperties.getFileSystemTimestampAccuracy().toMillis() + "ms"); } diff --git a/inception/inception-annotation-storage/src/test/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImplTest.java b/inception/inception-annotation-storage/src/test/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImplTest.java index d555ba1a380..0420d4d48f1 100644 --- a/inception/inception-annotation-storage/src/test/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImplTest.java +++ b/inception/inception-annotation-storage/src/test/java/de/tudarmstadt/ukp/inception/annotation/storage/CasStorageServiceImplTest.java @@ -345,7 +345,7 @@ public void testHighConcurrencyIncludingDeletion() throws Exception log.info("---- Starting all threads ----"); tasks.forEach(Thread::start); - log.info("---- Wait for primary threads to complete ----"); + log.info("---- Waiting for primary threads to complete ----"); boolean done = false; while (!done) { long running = primaryTasks.stream().filter(Thread::isAlive).count(); @@ -357,7 +357,7 @@ public void testHighConcurrencyIncludingDeletion() throws Exception deleteCounter, deleteInitialCounter); } - log.info("---- Wait for threads secondary threads to wrap up ----"); + log.info("---- Waiting for secondary threads to wrap up ----"); rwTasksCompleted.set(true); for (Thread thread : secondaryTasks) { thread.join(); diff --git a/inception/inception-api-annotation/pom.xml b/inception/inception-api-annotation/pom.xml index 5c484bb3ece..3bd9420fe42 100644 --- a/inception/inception-api-annotation/pom.xml +++ b/inception/inception-api-annotation/pom.xml @@ -20,7 +20,7 @@ de.tudarmstadt.ukp.inception.app inception-app - 27.0-SNAPSHOT + 29.0-SNAPSHOT inception-api-annotation INCEpTION - Core - Annotation API @@ -180,6 +180,10 @@ de.agilecoders.wicket wicket-bootstrap-core + + org.danekja + jdk-serializable-functional + diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationEditorState.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationEditorState.java index 6cbf310f47f..c99d561954a 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationEditorState.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationEditorState.java @@ -21,14 +21,21 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import de.tudarmstadt.ukp.inception.preferences.Key; + @JsonIgnoreProperties(ignoreUnknown = true) public class AnnotationEditorState implements Serializable { private static final long serialVersionUID = -1637731874872789592L; + public static final Key KEY_EDITOR_STATE = new Key<>( + AnnotationEditorState.class, "annotation/editor"); + private String defaultEditor; + private boolean preferencesAccessAllowed = true; + public String getDefaultEditor() { return defaultEditor; @@ -38,4 +45,14 @@ public void setDefaultEditor(String aEditorId) { defaultEditor = aEditorId; } + + public boolean isPreferencesAccessAllowed() + { + return preferencesAccessAllowed; + } + + public void setPreferencesAccessAllowed(boolean aPreferencesAccessAllowed) + { + preferencesAccessAllowed = aPreferencesAccessAllowed; + } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java index d177e49856c..74a025e5160 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java @@ -26,18 +26,13 @@ import static java.util.stream.Collectors.joining; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; -import org.apache.commons.lang3.tuple.Pair; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.FeatureStructure; -import org.apache.uima.cas.SelectFSs; -import org.apache.uima.cas.Type; -import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.jcas.cas.TOP; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.feedback.IFeedback; import org.apache.wicket.model.IModel; @@ -51,6 +46,7 @@ import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.string.StringValue; import org.apache.wicket.util.string.StringValueConversionException; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.wicketstuff.urlfragment.UrlFragment; @@ -62,19 +58,15 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.ValidationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.paging.NoPagingStrategy; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.preferences.UserPreferencesService; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.model.ValidationMode; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; -import de.tudarmstadt.ukp.clarin.webanno.support.logging.LogMessage; import de.tudarmstadt.ukp.clarin.webanno.support.uima.ICasUtil; import de.tudarmstadt.ukp.clarin.webanno.support.wicket.DecoratedObject; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; -import de.tudarmstadt.ukp.inception.preferences.Key; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.rendering.vmodel.VRange; @@ -89,8 +81,7 @@ public abstract class AnnotationPageBase { private static final long serialVersionUID = -1133219266479577443L; - public static final Key KEY_EDITOR_STATE = new Key<>( - AnnotationEditorState.class, "annotation/editor"); + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); public static final String PAGE_PARAM_DOCUMENT = "d"; public static final String PAGE_PARAM_USER = "u"; @@ -238,7 +229,13 @@ protected void updateUrlFragment(AjaxRequestTarget aTarget) return; } - aTarget.registerRespondListener(new UrlFragmentUpdateListener()); + // Update URL for current document + try { + aTarget.registerRespondListener(new UrlFragmentUpdateListener()); + } + catch (Exception e) { + LOG.debug("Unable to request URL fragment update anymore", e); + } } /** @@ -354,9 +351,9 @@ protected void validateRequiredFeatures(AjaxRequestTarget aTarget, CAS aCas, TypeAdapter aAdapter) throws ValidationException, IOException, AnnotationException { - CAS editorCas = aCas; - AnnotationLayer layer = aAdapter.getLayer(); - List features = annotationService.listAnnotationFeature(layer); + var editorCas = aCas; + var layer = aAdapter.getLayer(); + var features = aAdapter.listFeatures(); // If no feature is required, then we can skip the whole procedure if (features.stream().allMatch((f) -> !f.isRequired())) { @@ -364,16 +361,16 @@ protected void validateRequiredFeatures(AjaxRequestTarget aTarget, CAS aCas, } // Check each feature structure of this layer - Type layerType = aAdapter.getAnnotationType(editorCas); - Type annotationFsType = editorCas.getAnnotationType(); - try (SelectFSs fses = editorCas.select(layerType)) { - for (FeatureStructure fs : fses) { - for (AnnotationFeature f : features) { + var layerType = aAdapter.getAnnotationType(editorCas); + var annotationFsType = editorCas.getAnnotationType(); + try (var fses = editorCas.select(layerType)) { + for (var fs : fses) { + for (var f : features) { if (ValidationUtils.isRequiredFeatureMissing(f, fs)) { // If it is an annotation, then we jump to it if it has required empty // features if (editorCas.getTypeSystem().subsumes(annotationFsType, layerType)) { - getAnnotationActionHandler().actionSelectAndJump(aTarget, new VID(fs)); + getAnnotationActionHandler().actionSelectAndJump(aTarget, VID.of(fs)); } // Inform the user @@ -389,8 +386,8 @@ protected void validateRequiredFeatures(AjaxRequestTarget aTarget, CAS aCas, public void actionValidateDocument(AjaxRequestTarget aTarget, CAS aCas) throws ValidationException, IOException, AnnotationException { - AnnotatorState state = getModelObject(); - for (AnnotationLayer layer : annotationService.listAnnotationLayer(state.getProject())) { + var state = getModelObject(); + for (var layer : annotationService.listAnnotationLayer(state.getProject())) { if (!layer.isEnabled()) { // No validation for disabled layers since there is nothing the annotator could do // about fixing annotations on disabled layers. @@ -402,21 +399,20 @@ public void actionValidateDocument(AjaxRequestTarget aTarget, CAS aCas) continue; } - TypeAdapter adapter = annotationService.getAdapter(layer); + var adapter = annotationService.getAdapter(layer); validateRequiredFeatures(aTarget, aCas, adapter); - List> messages = adapter.validate(aCas); + var messages = adapter.validate(aCas); if (!messages.isEmpty()) { - LogMessage message = messages.get(0).getLeft(); - AnnotationFS fs = messages.get(0).getRight(); + var message = messages.get(0).getLeft(); + var fs = messages.get(0).getRight(); - getAnnotationActionHandler().actionSelectAndJump(aTarget, new VID(fs)); + getAnnotationActionHandler().actionSelectAndJump(aTarget, VID.of(fs)); // Inform the user - throw new ValidationException( - "Annotation with ID [" + ICasUtil.getAddr(fs) + "] on layer [" - + layer.getUiName() + "] is invalid: " + message.getMessage()); + throw new ValidationException("Annotation with ID [" + VID.of(fs) + "] on layer [" + + layer.getUiName() + "] is invalid: " + message.getMessage()); } } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/AnnotationPreferencesDialogContent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/AnnotationPreferencesDialogContent.java index 32404e1755c..0e907c15e78 100755 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/AnnotationPreferencesDialogContent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/AnnotationPreferencesDialogContent.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.preferences; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationEditorState.KEY_EDITOR_STATE; import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.CURATION; import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.CHAIN_TYPE; @@ -55,7 +56,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationEditorState; -import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.support.lambda.AjaxCallback; @@ -130,8 +130,8 @@ public AnnotationPreferencesDialogContent(String aId, IModel aMo fontZoomField.setMaximum(AnnotationPreference.FONT_ZOOM_MAX); form.add(fontZoomField); - AnnotationEditorState state = preferencesService.loadDefaultTraitsForProject( - AnnotationPageBase.KEY_EDITOR_STATE, stateModel.getObject().getProject()); + AnnotationEditorState state = preferencesService + .loadDefaultTraitsForProject(KEY_EDITOR_STATE, stateModel.getObject().getProject()); DropDownChoice> editor = new DropDownChoice<>("editor"); editor.setChoiceRenderer(new ChoiceRenderer<>("value")); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/UserPreferencesActionBarExtension.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/UserPreferencesActionBarExtension.java index 24374f68147..44b08e146a6 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/UserPreferencesActionBarExtension.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/preferences/UserPreferencesActionBarExtension.java @@ -17,18 +17,41 @@ */ package de.tudarmstadt.ukp.clarin.webanno.api.annotation.preferences; +import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationEditorState.KEY_EDITOR_STATE; +import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.CURATION; + import org.apache.wicket.markup.html.panel.Panel; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.actionbar.ActionBarExtension; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationEditorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.inception.preferences.PreferencesService; @Order(10000) @Component public class UserPreferencesActionBarExtension implements ActionBarExtension { + private final PreferencesService preferencesService; + + public UserPreferencesActionBarExtension(PreferencesService aPreferencesService) + { + super(); + preferencesService = aPreferencesService; + } + + @Override + public boolean accepts(AnnotationPageBase aPage) + { + AnnotationEditorState editorState = preferencesService + .loadDefaultTraitsForProject(KEY_EDITOR_STATE, aPage.getProject()); + + return editorState.isPreferencesAccessAllowed() + || aPage.getModelObject().getMode() == CURATION; + } + @Override public Panel createActionBarItem(String aId, AnnotationPageBase aPage) { diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/LabelRenderer.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/LabelRenderer.java index 6edb1f22e32..3c574729f36 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/LabelRenderer.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/rendering/LabelRenderer.java @@ -51,6 +51,10 @@ public void render(VDocument aVDoc, RenderRequest aRequest) { for (AnnotationLayer layer : aVDoc.getAnnotationLayers()) { for (VObject vobj : aVDoc.objects(layer.getId())) { + if (vobj.getLabelHint() != null) { + // Label hint was already set earlier - do not overwrite it! + continue; + } vobj.setLabelHint(getUiLabelText(vobj)); } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/util/WebAnnoCasUtil.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/util/WebAnnoCasUtil.java index 4f4c797a625..f572f813681 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/util/WebAnnoCasUtil.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/util/WebAnnoCasUtil.java @@ -45,7 +45,6 @@ import org.apache.uima.cas.Type; import org.apache.uima.cas.impl.CASCompleteSerializer; import org.apache.uima.cas.impl.CASImpl; -import org.apache.uima.cas.impl.FixedBinaryCasSerDes; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.cas.text.AnnotationIndex; import org.apache.uima.fit.util.CasUtil; @@ -73,7 +72,6 @@ public class WebAnnoCasUtil public static CAS createCas(TypeSystemDescription aTSD) throws ResourceInitializationException { CAS cas = CasCreationUtils.createCas(aTSD, null, null); - FixedBinaryCasSerDes.inject(cas); if (ENFORCE_CAS_THREAD_LOCK) { cas = (CAS) Proxy.newProxyInstance(cas.getClass().getClassLoader(), @@ -100,7 +98,6 @@ public static CAS createCas() throws ResourceInitializationException public static CAS createCasCopy(CAS aOriginal) throws UIMAException { CAS copy = CasCreationUtils.createCas((TypeSystemDescription) null, null, null); - FixedBinaryCasSerDes.inject(copy); CASCompleteSerializer serializer = serializeCASComplete((CASImpl) getRealCas(aOriginal)); deserializeCASComplete(serializer, (CASImpl) getRealCas(copy)); return copy; diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationCreatedEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationCreatedEvent.java index 94cdbaa79a6..3b811198e24 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationCreatedEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationCreatedEvent.java @@ -28,7 +28,7 @@ public interface AnnotationCreatedEvent { SourceDocument getDocument(); - String getUser(); + String getDocumentOwner(); AnnotationLayer getLayer(); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationDeletedEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationDeletedEvent.java index 56e8da2b465..d7d7b23d217 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationDeletedEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationDeletedEvent.java @@ -28,7 +28,7 @@ public interface AnnotationDeletedEvent { SourceDocument getDocument(); - String getUser(); + String getDocumentOwner(); AnnotationLayer getLayer(); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationEvent.java index 812673e416f..c2eec6109ec 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/AnnotationEvent.java @@ -34,23 +34,25 @@ public abstract class AnnotationEvent private final Project project; private final SourceDocument document; - private final String user; + private final String documentOwner; private final AnnotationLayer layer; - public AnnotationEvent(Object aSource, Project aProject, String aUser, AnnotationLayer aLayer) + public AnnotationEvent(Object aSource, Project aProject, String aDocumentOwner, + AnnotationLayer aLayer) { - this(aSource, aProject, null, aUser, aLayer); + this(aSource, aProject, null, aDocumentOwner, aLayer); } - public AnnotationEvent(Object aSource, SourceDocument aDocument, String aUser, + public AnnotationEvent(Object aSource, SourceDocument aDocument, String aDocumentOwner, AnnotationLayer aLayer) { - this(aSource, aDocument != null ? aDocument.getProject() : null, aDocument, aUser, aLayer); + this(aSource, aDocument != null ? aDocument.getProject() : null, aDocument, aDocumentOwner, + aLayer); Validate.notNull(getProject(), "Project must be specified"); Validate.notNull(getDocument(), "Document must be specified"); Validate.notNull(getLayer(), "Layer must be specified"); - Validate.notNull(getUser(), "User must be specified"); + Validate.notNull(getDocumentOwner(), "User must be specified"); } public AnnotationEvent(Object aSource, SourceDocument aDocument, String aUser) @@ -59,16 +61,16 @@ public AnnotationEvent(Object aSource, SourceDocument aDocument, String aUser) Validate.notNull(getProject(), "Project must be specified"); Validate.notNull(getDocument(), "Document must be specified"); - Validate.notNull(getUser(), "User must be specified"); + Validate.notNull(getDocumentOwner(), "Document owner must be specified"); } private AnnotationEvent(Object aSource, Project aProject, SourceDocument aDocument, - String aUser, AnnotationLayer aLayer) + String aDocumentOwner, AnnotationLayer aLayer) { super(aSource); project = aProject; document = aDocument; - user = aUser; + documentOwner = aDocumentOwner; layer = aLayer; } @@ -82,9 +84,9 @@ public SourceDocument getDocument() return document; } - public String getUser() + public String getDocumentOwner() { - return user; + return documentOwner; } public AnnotationLayer getLayer() diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BeforeDocumentOpenedEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BeforeDocumentOpenedEvent.java index f8198f63123..9161db21c16 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BeforeDocumentOpenedEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BeforeDocumentOpenedEvent.java @@ -35,19 +35,19 @@ public class BeforeDocumentOpenedEvent private final CAS cas; private final SourceDocument document; // user who owns/annotates the opened document - private final String annotator; + private final String documentOwner; // user who opened the document - private final String opener; + private final String sessionOwner; private final boolean editable; public BeforeDocumentOpenedEvent(Object aSource, CAS aCas, SourceDocument aDocument, - String aAnnotator, String aOpener, boolean aEditable) + String aDocumentOwner, String aSessionOwner, boolean aEditable) { super(aSource); cas = aCas; document = aDocument; - annotator = aAnnotator; - opener = aOpener; + documentOwner = aDocumentOwner; + sessionOwner = aSessionOwner; editable = aEditable; } @@ -61,14 +61,14 @@ public SourceDocument getDocument() return document; } - public String getUser() + public String getSessionOwner() { - return opener; + return sessionOwner; } - public String getAnnotator() + public String getDocumentOwner() { - return annotator; + return documentOwner; } public boolean isEditable() diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BulkAnnotationEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BulkAnnotationEvent.java index 1dc4849248c..3b64b6f7e66 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BulkAnnotationEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/BulkAnnotationEvent.java @@ -34,21 +34,21 @@ public class BulkAnnotationEvent { private static final long serialVersionUID = -1187536069360130349L; - public BulkAnnotationEvent(Object aSource, Project aProject, String aUser, + public BulkAnnotationEvent(Object aSource, Project aProject, String aDocumentOwner, AnnotationLayer aLayer) { - super(aSource, aProject, aUser, aLayer); + super(aSource, aProject, aDocumentOwner, aLayer); } - public BulkAnnotationEvent(Object aSource, SourceDocument aDocument, String aUser, + public BulkAnnotationEvent(Object aSource, SourceDocument aDocument, String aDocumentOwner, AnnotationLayer aLayer) { - super(aSource, aDocument, aUser, aLayer); + super(aSource, aDocument, aDocumentOwner, aLayer); } - public BulkAnnotationEvent(Object aSource, SourceDocument aDocument, String aUser) + public BulkAnnotationEvent(Object aSource, SourceDocument aDocument, String aDocumentOwner) { - super(aSource, aDocument, aUser); + super(aSource, aDocument, aDocumentOwner); } @Override diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/DocumentOpenedEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/DocumentOpenedEvent.java index 52b10e8327f..dcdc03326ab 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/DocumentOpenedEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/DocumentOpenedEvent.java @@ -20,6 +20,7 @@ import org.apache.uima.cas.CAS; import org.springframework.context.ApplicationEvent; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.support.wicket.event.HybridApplicationUIEvent; @@ -32,18 +33,21 @@ public class DocumentOpenedEvent private final CAS cas; private final SourceDocument document; // user who owns/annotates the opened document - private final String annotator; + private final String documentOwner; // user who opened the document - private final String opener; + private final String sessionOwner; + private final AnnotationDocumentState stateBeforeOpening; public DocumentOpenedEvent(Object aSource, CAS aCas, SourceDocument aDocument, - String aAnnotator, String aOpener) + AnnotationDocumentState aStateBeforeOpening, String aDocumentOwner, + String aSessionOwner) { super(aSource); cas = aCas; document = aDocument; - annotator = aAnnotator; - opener = aOpener; + documentOwner = aDocumentOwner; + sessionOwner = aSessionOwner; + stateBeforeOpening = aStateBeforeOpening; } public CAS getCas() @@ -56,13 +60,18 @@ public SourceDocument getDocument() return document; } - public String getUser() + public String getSessionOwner() { - return opener; + return sessionOwner; } - public String getAnnotator() + public String getDocumentOwner() { - return annotator; + return documentOwner; + } + + public AnnotationDocumentState getStateBeforeOpening() + { + return stateBeforeOpening; } } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/FeatureValueUpdatedEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/FeatureValueUpdatedEvent.java index 7fbce1481bb..a6b3e7628db 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/FeatureValueUpdatedEvent.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/FeatureValueUpdatedEvent.java @@ -89,7 +89,7 @@ public String toString() builder.append("docID="); builder.append(getDocument()); builder.append(", user="); - builder.append(getUser()); + builder.append(getDocumentOwner()); builder.append(", "); } builder.append("addr="); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/RatingFeatureEditor.html b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/RatingFeatureEditor.html index d0053b527d7..4ebfecb3f72 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/RatingFeatureEditor.html +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/number/RatingFeatureEditor.html @@ -18,13 +18,17 @@ --> -
- -
- - - - +
+ +
+
+ + + +
diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/DynamicTextAreaFeatureEditor.html b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/DynamicTextAreaFeatureEditor.html index 9649b300f02..42a8df8f061 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/DynamicTextAreaFeatureEditor.html +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/DynamicTextAreaFeatureEditor.html @@ -26,7 +26,7 @@
- +