diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..04905c55736 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,24 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +# means that files that GIT determines to be text files, will be +# converted from CRLF -> LF upon being added to the repo, and +# converted from LF -> LF or CRLF when checked out (depending on the platform, I think) +# * text=auto + +# We force LF for all text files because we have Checkstyle set up in such a way +# The "text" by itself says these files must be line-ending-conversion controlled on check-in / out +# The internal repo form for these is always lf, +# The eol=lf means on check-out do nothing, and on check-in, if the file has crlf, convert to lf +* text eol=lf +# *.sh text eol=lf + +# Make sure that these files are treated as binary so that newlines are preserved. +# overrides GIT's determination if a file is text or not +*.bin binary +*.dump binary +*.xcas binary +*.xmi binary +# next is probably the default +*.pdf binary +*.ser binary +*.png binary +*.jpg binary diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 964880a2873..8086cfe414b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,15 +16,19 @@ on: jobs: build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + jdk: [17] - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{ matrix.jdk }} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{ matrix.jdk }} distribution: 'temurin' cache: maven - name: Build with Maven diff --git a/inception/inception-active-learning/pom.xml b/inception/inception-active-learning/pom.xml index 3f9aeb1b55c..b6b2d34e487 100644 --- a/inception/inception-active-learning/pom.xml +++ b/inception/inception-active-learning/pom.xml @@ -66,6 +66,10 @@ de.tudarmstadt.ukp.inception.app inception-support + + de.tudarmstadt.ukp.inception.app + inception-preferences + de.tudarmstadt.ukp.inception.app inception-support-bootstrap @@ -97,6 +101,10 @@ commons-lang3 + + org.springframework + spring-core + org.springframework spring-beans diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/config/ActiveLearningAutoConfiguration.java b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/config/ActiveLearningAutoConfiguration.java index 60e555498ea..a9b0782030f 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/config/ActiveLearningAutoConfiguration.java +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/config/ActiveLearningAutoConfiguration.java @@ -31,6 +31,7 @@ import de.tudarmstadt.ukp.inception.active.learning.log.ActiveLearningSuggestionOfferedAdapter; import de.tudarmstadt.ukp.inception.active.learning.sidebar.ActiveLearningSidebarFactory; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; +import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; @@ -73,7 +74,8 @@ public ActiveLearningSuggestionOfferedAdapter activeLearningSuggestionOfferedAda @Bean public ActiveLearningSidebarFactory activeLearningSidebarFactory( - RecommendationService aRecommendationService) + RecommendationService aRecommendationService, PreferencesService aPreferencesService, + UserDao aUserService) { return new ActiveLearningSidebarFactory(aRecommendationService); } diff --git a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.html b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.html index 0f5acfb325c..6530f9edf7b 100644 --- a/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.html +++ b/inception/inception-active-learning/src/main/java/de/tudarmstadt/ukp/inception/active/learning/sidebar/ActiveLearningSidebar.html @@ -26,6 +26,10 @@
+ + + +
+ +
+
+ + +
+ +
+
+ +
+ +
+ + + + \ No newline at end of file diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.java b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.java similarity index 56% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.java rename to inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.java index b1b9b663fb2..c3d1184552e 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.java +++ b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.java @@ -15,12 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.processor; +package de.tudarmstadt.ukp.inception.processing.recommender; import static de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel.ANNOTATOR; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; +import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhenNot; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -33,19 +37,27 @@ import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.spring.injection.annot.SpringBean; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer_; +import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User_; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender_; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.TrainingCapability; -import de.tudarmstadt.ukp.inception.recommendation.tasks.BulkPredictionTask; +import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState; import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxButton; -import de.tudarmstadt.ukp.inception.ui.scheduling.TaskMonitorPanel; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; +import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; -public class BulkProcessingPanel +public class BulkRecommenderPanel extends GenericPanel { private static final long serialVersionUID = 3568501821432165745L; @@ -54,37 +66,60 @@ public class BulkProcessingPanel private @SpringBean RecommendationService recommendationService; private @SpringBean SchedulingService schedulingService; private @SpringBean UserDao userService; + private @SpringBean AnnotationSchemaService annotationSchemaService; - public BulkProcessingPanel(String aId, IModel aModel) + private CompoundPropertyModel formModel; + private FeatureEditorPanel processingMetadata; + + public BulkRecommenderPanel(String aId, IModel aModel) { super(aId, aModel); - queue(new Form("form", new CompoundPropertyModel<>(new FormData()))); + formModel = new CompoundPropertyModel<>(new FormData()); + queue(new Form("form", formModel)); queue(new DropDownChoice<>("user") // .setChoices(LoadableDetachableModel.of(this::listUsers)) // - .setChoiceRenderer(new ChoiceRenderer<>("uiName")) // + .setChoiceRenderer(new ChoiceRenderer<>(User_.UI_NAME)) // .setRequired(true)); queue(new DropDownChoice<>("recommender") // .setChoices(LoadableDetachableModel.of(this::listRecommenders)) // - .setChoiceRenderer(new ChoiceRenderer<>("name")) // + .setChoiceRenderer(new ChoiceRenderer<>(Recommender_.NAME)) // .setRequired(true)); - queue(new LambdaAjaxButton<>("startProcessing", this::actionStartProcessing)); + processingMetadata = new FeatureEditorPanel("processingMetadata"); + processingMetadata.setOutputMarkupPlaceholderTag(true); + queue(processingMetadata); + + var docMetaLayers = LoadableDetachableModel.of(this::listDocumentMetadataLayers); + queue(new DropDownChoice<>("processingMetadataLayer") // + .setNullValid(true) // + .setChoices(docMetaLayers) // + .setChoiceRenderer(new ChoiceRenderer<>(AnnotationLayer_.UI_NAME)) + .add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, _target -> { + processingMetadata.setModelObject(listFeatureStates()); + _target.add(processingMetadata); + })) // + .add(visibleWhenNot(docMetaLayers.map(List::isEmpty)))); - queue(new TaskMonitorPanel("runningProcesses").setPopupMode(false) - .setShowFinishedTasks(true)); + queue(new LambdaAjaxButton<>("startProcessing", this::actionStartProcessing)); } private void actionStartProcessing(AjaxRequestTarget aTarget, Form aForm) { + var metadata = new HashMap(); + for (var state : processingMetadata.getModelObject()) { + metadata.put(state.getFeature(), state.getValue()); + } + var formData = aForm.getModelObject(); schedulingService.enqueue(BulkPredictionTask.builder() // .withSessionOwner(userService.getCurrentUser()) // .withRecommender(formData.recommender) // .withTrigger("User request") // .withDataOwner(formData.user.getUsername()) // + .withProcessingMetadata(metadata) // .build()); } @@ -93,6 +128,13 @@ private List listUsers() return projectService.listProjectUsersWithPermissions(getModelObject(), ANNOTATOR); } + private List listDocumentMetadataLayers() + { + return annotationSchemaService.listAnnotationLayer(getModelObject()) // + .stream().filter(l -> DocumentMetadataLayerSupport.TYPE.equals(l.getType())) // + .toList(); + } + private List listRecommenders() { // We list all recommenders here - not only the enabled ones. Maybe we want to manually @@ -122,6 +164,33 @@ private List listRecommenders() return recommenders; } + private List listFeatureStates() + { + if (!formModel.map(d -> d.processingMetadataLayer).isPresent().getObject()) { + return Collections.emptyList(); + } + + var layer = formModel.map(d -> d.processingMetadataLayer).getObject(); + + var featureStates = new ArrayList(); + for (var feature : annotationSchemaService.listSupportedFeatures(layer)) { + if (!feature.isEnabled()) { + continue; + } + + if (feature.getLinkMode() != LinkMode.NONE) { + continue; + } + + var featureState = new FeatureState(null, feature, null); + featureStates.add(featureState); + featureState.tagset = annotationSchemaService + .listTagsReorderable(featureState.feature.getTagset()); + } + + return featureStates; + } + private static class FormData implements Serializable { @@ -129,5 +198,6 @@ private static class FormData private User user; private Recommender recommender; + private AnnotationLayer processingMetadataLayer; } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.properties b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.properties similarity index 95% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.properties rename to inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.properties index dc8a589aff6..8246c364352 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.properties +++ b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/BulkRecommenderPanel.properties @@ -18,3 +18,5 @@ startProcessing=Start processing... applyRecommenderDescription=Apply a recommender to all documents the chosen annotator has not started working \ on yet. Once the recommender has been applied, the documents are marked as finished. Only recommenders that \ do not require training can be used here. +processingMetadataLayer=Processing metadata layer + \ No newline at end of file diff --git a/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.html b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.html new file mode 100644 index 00000000000..1907054ac21 --- /dev/null +++ b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.html @@ -0,0 +1,25 @@ + + + + + +
+
+
+ \ No newline at end of file diff --git a/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.java b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.java new file mode 100644 index 00000000000..340693bb247 --- /dev/null +++ b/inception/inception-processing/src/main/java/de/tudarmstadt/ukp/inception/processing/recommender/FeatureEditorPanel.java @@ -0,0 +1,110 @@ +/* + * 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.processing.recommender; + +import static java.util.Collections.emptyList; + +import java.util.List; + +import org.apache.wicket.behavior.AttributeAppender; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.markup.html.panel.GenericPanel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.spring.injection.annot.SpringBean; + +import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureEditor; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.inception.support.wicket.DescriptionTooltipBehavior; + +public class FeatureEditorPanel + extends GenericPanel> +{ + private static final long serialVersionUID = -3186504694280146998L; + + private static final String CID_EDITOR = "editor"; + private static final String CID_FEATURE_EDITORS = "featureEditors"; + + private @SpringBean AnnotationSchemaService annotationService; + private @SpringBean FeatureSupportRegistry featureSupportRegistry; + + public FeatureEditorPanel(String aId) + { + this(aId, Model.ofList(emptyList())); + } + + public FeatureEditorPanel(String aId, IModel> aFeatureStates) + { + super(aId, aFeatureStates); + + setOutputMarkupId(true); + + queue(createFeaturesList(CID_FEATURE_EDITORS, aFeatureStates)); + } + + private ListView createFeaturesList(String aId, IModel> aFeatureStates) + { + return new ListView(aId, aFeatureStates) + { + private static final long serialVersionUID = -1139622234318691941L; + + @Override + protected void populateItem(ListItem item) + { + final FeatureState featureState = item.getModelObject(); + final FeatureEditor editor; + + // Look up a suitable editor and instantiate it + var featureSupport = featureSupportRegistry.findExtension(featureState.feature) + .orElseThrow(); + editor = featureSupport.createEditor(CID_EDITOR, this, null, null, item.getModel()); + + // Add tooltip on label + var tooltipTitle = new StringBuilder(); + tooltipTitle.append(featureState.feature.getUiName()); + if (featureState.feature.getTagset() != null) { + tooltipTitle.append(" ("); + tooltipTitle.append(featureState.feature.getTagset().getName()); + tooltipTitle.append(')'); + } + + var labelComponent = editor.getLabelComponent(); + labelComponent.add(new AttributeAppender("style", "cursor: help", ";")); + labelComponent.add(new DescriptionTooltipBehavior(tooltipTitle.toString(), + featureState.feature.getDescription())); + + // We need to enable the markup ID here because we use it during the AJAX behavior + // that automatically saves feature editors on change/blur. + // Check addAnnotateActionBehavior. + editor.setOutputMarkupPlaceholderTag(true); + + // Ensure that markup IDs of feature editor focus components remain constant + // across refreshes of the feature editor panel. This is required to restore the + // focus. + editor.getFocusComponent().setOutputMarkupId(true); + editor.getFocusComponent().setMarkupId(FeatureEditorPanel.this.getMarkupId() + + editor.getModelObject().feature.getId()); + + item.add(editor); + } + }; + } +} diff --git a/inception/inception-processing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/inception/inception-processing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..5f2df7f85c6 --- /dev/null +++ b/inception/inception-processing/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +de.tudarmstadt.ukp.inception.processing.config.ProcessingAutoConfiguration diff --git a/inception/inception-project-api/pom.xml b/inception/inception-project-api/pom.xml index e86306afe7e..c084cb39241 100644 --- a/inception/inception-project-api/pom.xml +++ b/inception/inception-project-api/pom.xml @@ -52,14 +52,9 @@ commons-lang3
- org.springframework spring-context - - org.springframework.security - spring-security-core - \ No newline at end of file diff --git a/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java index 4b9b3fda2db..ee57b216a7c 100644 --- a/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java +++ b/inception/inception-project-api/src/main/java/de/tudarmstadt/ukp/inception/project/api/ProjectService.java @@ -31,7 +31,6 @@ import org.apache.commons.lang3.Validate; import org.slf4j.MDC; -import org.springframework.security.access.prepost.PreAuthorize; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; @@ -66,7 +65,6 @@ public interface ProjectService * @deprecated Use {@link #assignRole(Project, User, PermissionLevel...)} instead. */ @Deprecated - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER', 'ROLE_REMOTE')") void createProjectPermission(ProjectPermission aPermission); /** @@ -349,7 +347,6 @@ List listProjectsWithUserHavingRole(User aUser, PermissionLevel aRole, * @throws IOException * if the project to be deleted is not available in the file system */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void removeProject(Project aProject) throws IOException; /** diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java index 2a983f943ce..3bedd42e0a0 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java @@ -127,6 +127,15 @@ Optional getEvaluatedRecommender(User aSessionOwner, void putIncomingPredictions(User aSessionOwner, Project aProject, Predictions aPredictions); + /** + * Replace the current predictions with the pending predictions if any are available. + * + * @param aSessionOwner + * the owner of the session. + * @param aProject + * the project. + * @return whether the current predictions where replaced or not. + */ boolean switchPredictions(String aSessionOwner, Project aProject); /** @@ -277,7 +286,13 @@ void calculateSuggestionVisibility(String aSess SourceDocument aDocument, CAS aCas, String aDataOwner, AnnotationLayer aLayer, Collection> aRecommendations, int aWindowBegin, int aWindowEnd); - void clearState(String aSessionOwner); + /** + * Discards all predictions in all states belonging to the owner. Flags are retained. + * + * @param aSessionOwner + * the user owning the session. + */ + void resetState(String aSessionOwner); void triggerPrediction(String aSessionOwner, String aEventName, SourceDocument aDocument, String aDocumentOwner); @@ -301,4 +316,17 @@ void setPredictForAllDocuments(String aSessionOwner, Project aProject, long countEnabledRecommenders(); Progress getProgressTowardsNextEvaluation(User aSessionOwner, Project aProject); + + boolean isSuspended(String aUser, Project aProject); + + void setSuspended(String aUser, Project aProject, boolean aState); + + /** + * @return if the curation sidebar mode is available. + * @deprecated This obviously shouldn't be here, but this is the easiest way to access this + * information from the recommender settings. Should be removed when the curation + * sidebar leaves experimental mode. + */ + @Deprecated + boolean isCurationSidebarEnabled(); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RecommenderGeneralSettings.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RecommenderGeneralSettings.java index 68a396e7f58..3f050fde284 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RecommenderGeneralSettings.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RecommenderGeneralSettings.java @@ -26,6 +26,10 @@ public class RecommenderGeneralSettings private boolean waitForRecommendersOnOpenDocument = false; + private boolean showRecommendationsWhenViewingOtherUser = true; + + private boolean showRecommendationsWhenViewingCurationUser = true; + public boolean isWaitForRecommendersOnOpenDocument() { return waitForRecommendersOnOpenDocument; @@ -35,4 +39,26 @@ public void setWaitForRecommendersOnOpenDocument(boolean aWaitForRecommendersOnO { waitForRecommendersOnOpenDocument = aWaitForRecommendersOnOpenDocument; } + + public boolean isShowRecommendationsWhenViewingOtherUser() + { + return showRecommendationsWhenViewingOtherUser; + } + + public void setShowRecommendationsWhenViewingOtherUser( + boolean aShowRecommendationsWhenViewingOtherUser) + { + showRecommendationsWhenViewingOtherUser = aShowRecommendationsWhenViewingOtherUser; + } + + public boolean isShowRecommendationsWhenViewingCurationUser() + { + return showRecommendationsWhenViewingCurationUser; + } + + public void setShowRecommendationsWhenViewingCurationUser( + boolean aShowRecommendationsWhenViewingCurationUser) + { + showRecommendationsWhenViewingCurationUser = aShowRecommendationsWhenViewingCurationUser; + } } diff --git a/inception/inception-recommendation/pom.xml b/inception/inception-recommendation/pom.xml index 94a0fee7ba0..3a2a6d4935e 100644 --- a/inception/inception-recommendation/pom.xml +++ b/inception/inception-recommendation/pom.xml @@ -117,10 +117,6 @@ de.tudarmstadt.ukp.inception.app inception-scheduling - - de.tudarmstadt.ukp.inception.app - inception-ui-scheduling - de.tudarmstadt.ukp.inception.app inception-log @@ -259,10 +255,6 @@ org.wicketstuff wicketstuff-annotationeventdispatcher - - org.wicketstuff - wicketstuff-input-events - diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index d97c5ed22e9..aff17475098 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -34,7 +34,6 @@ import org.apache.uima.cas.CAS; import org.apache.uima.jcas.tcas.Annotation; -import org.apache.wicket.Page; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.feedback.IFeedback; import org.slf4j.Logger; @@ -63,7 +62,6 @@ import de.tudarmstadt.ukp.inception.recommendation.api.event.AjaxRecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.event.PredictionsSwitchedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; @@ -209,10 +207,10 @@ private void actionAcceptPrediction(AnnotationActionHandler aActionHandler, private Optional getPrediction(AnnotatorState aState, VID aRecVid) { - Predictions predictions = recommendationService.getPredictions(aState.getUser(), + var predictions = recommendationService.getPredictions(aState.getUser(), aState.getProject()); - SourceDocument document = aState.getDocument(); - Optional prediction = predictions // + var document = aState.getDocument(); + var prediction = predictions // .getPredictionByVID(document, aRecVid); return prediction; } @@ -254,7 +252,7 @@ private void actionRejectRecommendation(AnnotationActionHandler aActionHandler, new AjaxRecommendationRejectedEvent(aTarget, aState, aVID)); // Trigger a re-rendering of the document - Page page = aTarget.getPage(); + var page = aTarget.getPage(); page.send(page, BREADTH, new SelectionChangedEvent(aTarget)); } @@ -264,7 +262,7 @@ public void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState) log.trace("renderRequested()"); // do not show predictions during curation or when viewing others' work - String sessionOwner = userService.getCurrentUsername(); + var sessionOwner = userService.getCurrentUsername(); if (!aState.getMode().equals(ANNOTATION)) { return; } @@ -273,8 +271,7 @@ public void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState) // at the moment. For another, even if we had it, it would be quite annoying to the user // if the UI kept updating itself without any the user expecting an update. The user does // expect an update when she makes some interaction, so we piggy-back on this expectation. - boolean switched = recommendationService.switchPredictions(sessionOwner, - aState.getProject()); + var switched = recommendationService.switchPredictions(sessionOwner, aState.getProject()); log.trace("switchPredictions() returned {}", switched); if (!switched) { @@ -324,14 +321,14 @@ public List lookupLazyDetails(SourceDocument aDocument, User a var detailGroups = new ArrayList(); for (var aFeature : annotationService.listAnnotationFeature(aLayer)) { if (aFeature.getLinkMode() == LinkMode.WITH_ROLE) { - return emptyList(); + continue; } var vid = VID.parse(aVid.getExtensionPayload()); var representative = predictions.getPredictionByVID(aDocument, vid); if (representative.isEmpty() || !representative.get().getFeature().equals(aFeature.getName())) { - return emptyList(); + continue; } var sao = representative.get(); @@ -343,7 +340,7 @@ public List lookupLazyDetails(SourceDocument aDocument, User a .findFirst(); if (group.isEmpty()) { - return emptyList(); + continue; } var pref = recommendationService.getPreferences(aUser, aDocument.getProject()); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java index 10fbfc65baa..051d8644dbe 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java @@ -20,7 +20,6 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -51,7 +50,6 @@ import de.tudarmstadt.ukp.inception.recommendation.log.RecommenderDeletedEventAdapter; import de.tudarmstadt.ukp.inception.recommendation.log.RecommenderEvaluationResultEventAdapter; import de.tudarmstadt.ukp.inception.recommendation.metrics.RecommendationMetricsImpl; -import de.tudarmstadt.ukp.inception.recommendation.processor.BulkProcessingPageMenuItem; import de.tudarmstadt.ukp.inception.recommendation.project.ProjectRecommendersMenuItem; import de.tudarmstadt.ukp.inception.recommendation.project.RecommenderProjectSettingsPanelFactory; import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; @@ -66,7 +64,6 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import jakarta.servlet.ServletContext; /** * Provides all back-end Spring beans for the recommendation functionality. @@ -147,7 +144,8 @@ public ProjectRecommendersMenuItem projectRecommendersMenuItem() havingValue = "true", matchIfMissing = true) @Bean public RecommendationSidebarFactory recommendationSidebarFactory( - RecommendationService aRecommendationService) + RecommendationService aRecommendationService, PreferencesService aPreferencesService, + UserDao aUserService) { return new RecommendationSidebarFactory(aRecommendationService); } @@ -190,12 +188,13 @@ public RecommendationEventFooterItem recommendationEventFooterItem() } @Bean - public RecommendationRenderer recommendationRenderer(AnnotationSchemaService aAnnotationService, + public RecommendationRenderer recommendationRenderer( RecommendationService aRecommendationService, - SuggestionSupportRegistry aSuggestionSupportRegistry) + SuggestionSupportRegistry aSuggestionSupportRegistry, + PreferencesService aPreferencesService, UserDao aUserService) { - return new RecommendationRenderer(aAnnotationService, aRecommendationService, - aSuggestionSupportRegistry); + return new RecommendationRenderer(aRecommendationService, aSuggestionSupportRegistry, + aPreferencesService, aUserService); } @Bean @@ -244,13 +243,4 @@ public SuggestionSupportRegistry layerRecommendtionSupportRegistry( { return new SuggestionSupportRegistryImpl(aExtensions); } - - @ConditionalOnWebApplication - @Bean - @ConditionalOnExpression("${websocket.enabled:true} and ${bulk-processing.enabled:false}") - public BulkProcessingPageMenuItem bulkProcessingPageMenuItem(UserDao aUserRepo, - ProjectService aProjectService, ServletContext aServletContext) - { - return new BulkProcessingPageMenuItem(aUserRepo, aProjectService, aServletContext); - } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersResumedEvent.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersResumedEvent.java new file mode 100644 index 00000000000..240abf5612f --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersResumedEvent.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.event; + +import org.springframework.context.ApplicationEvent; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.inception.support.wicket.event.HybridApplicationUIEvent; + +public class RecommendersResumedEvent + extends ApplicationEvent + implements HybridApplicationUIEvent +{ + private static final long serialVersionUID = -4736560772442881663L; + + private final Project project; + private final String sessionOwner; + + public RecommendersResumedEvent(Object source, Project aProject, String aSessionOwner) + { + super(source); + project = aProject; + sessionOwner = aSessionOwner; + } + + public Project getProject() + { + return project; + } + + public String getUser() + { + return sessionOwner; + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersSuspendedEvent.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersSuspendedEvent.java new file mode 100644 index 00000000000..c5ecb061ef3 --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/event/RecommendersSuspendedEvent.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.event; + +import org.springframework.context.ApplicationEvent; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.inception.support.wicket.event.HybridApplicationUIEvent; + +public class RecommendersSuspendedEvent + extends ApplicationEvent + implements HybridApplicationUIEvent +{ + private static final long serialVersionUID = -4736560772442881663L; + + private final Project project; + private final String sessionOwner; + + public RecommendersSuspendedEvent(Object source, Project aProject, String aSessionOwner) + { + super(source); + project = aProject; + sessionOwner = aSessionOwner; + } + + public Project getProject() + { + return project; + } + + public String getUser() + { + return sessionOwner; + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/footer/RecommendationEventFooterPanel.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/footer/RecommendationEventFooterPanel.java index 3902c10c3dd..512f9ca9c53 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/footer/RecommendationEventFooterPanel.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/footer/RecommendationEventFooterPanel.java @@ -42,7 +42,7 @@ import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase; import de.tudarmstadt.ukp.inception.support.svelte.SvelteBehavior; -import de.tudarmstadt.ukp.inception.websocket.feedback.FeedbackPanelExtensionBehavior; +import de.tudarmstadt.ukp.inception.ui.core.feedback.FeedbackPanelExtensionBehavior; import jakarta.servlet.ServletContext; public class RecommendationEventFooterPanel diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.html deleted file mode 100644 index 482af7ace86..00000000000 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPanel.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - -
-
-
-
- -
-
-

- -

-
-
- - -
-
- -
- -
-
-
-
-
-
-
-
- - \ No newline at end of file diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.html index 2918206dd3b..c63c547ccda 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.html +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.html @@ -25,33 +25,40 @@
diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.java index 31701787bf5..145bb01bcf6 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.java @@ -75,16 +75,22 @@ public RecommenderListPanel(String id, IModel aProject, overviewList.add(new LambdaAjaxFormComponentUpdatingBehavior("change", this::onChange)); add(overviewList); - LambdaAjaxLink lambdaAjaxLink = new LambdaAjaxLink(MID_CREATE_BUTTON, this::actionCreate); + var lambdaAjaxLink = new LambdaAjaxLink(MID_CREATE_BUTTON, this::actionCreate); lambdaAjaxLink.setVisible(showCreateButton); add(lambdaAjaxLink); - RecommenderGeneralSettings settings = preferencesService.loadDefaultTraitsForProject( + var settings = preferencesService.loadDefaultTraitsForProject( KEY_RECOMMENDER_GENERAL_SETTINGS, projectModel.getObject()); var form = new Form<>("form", CompoundPropertyModel.of(settings)); form.setOutputMarkupId(true); - form.add(new CheckBox("waitForRecommendersOnOpenDocument").setOutputMarkupId(true)); + form.add(new CheckBox("waitForRecommendersOnOpenDocument") // + .setOutputMarkupId(true)); + form.add(new CheckBox("showRecommendationsWhenViewingOtherUser") // + .setOutputMarkupId(true)); + form.add(new CheckBox("showRecommendationsWhenViewingCurationUser") // + .setOutputMarkupId(true) // + .setVisible(recommendationService.isCurationSidebarEnabled())); form.add(new LambdaAjaxButton<>("save", this::actionSaveSettings)); add(form); } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.properties b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.properties index a44aaad2b1b..65d6c2160a5 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.properties +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/project/RecommenderListPanel.properties @@ -15,4 +15,7 @@ # limitations under the License. recommenders=Recommenders -waitForRecommendersOnOpenDocument=Wait for suggestions from non-trainable recommenders when opening document \ No newline at end of file +settings=Settings +waitForRecommendersOnOpenDocument=Wait for suggestions from non-trainable recommenders when opening document +showRecommendationsWhenViewingOtherUser=Show suggestions when viewing annotations from another user +showRecommendationsWhenViewingCurationUser=Show suggestions when viewing curation annotations diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java index 662cd589a2c..9e19a9785b2 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java @@ -18,27 +18,29 @@ package de.tudarmstadt.ukp.inception.recommendation.render; import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; +import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.KEY_RECOMMENDER_GENERAL_SETTINGS; import static de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup.groupByType; import static java.util.function.Function.identity; import static java.util.stream.Collectors.groupingBy; import java.util.HashMap; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import org.springframework.core.annotation.Order; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; -import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderStep; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; -import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; /** *

@@ -52,17 +54,19 @@ public class RecommendationRenderer { public static final String ID = "RecommendationRenderer"; - private final AnnotationSchemaService annotationService; private final RecommendationService recommendationService; private final SuggestionSupportRegistry suggestionSupportRegistry; + private final PreferencesService preferencesService; + private final UserDao userService; - public RecommendationRenderer(AnnotationSchemaService aAnnotationService, - RecommendationService aRecommendationService, - SuggestionSupportRegistry aSuggestionSupportRegistry) + public RecommendationRenderer(RecommendationService aRecommendationService, + SuggestionSupportRegistry aSuggestionSupportRegistry, + PreferencesService aPreferencesService, UserDao aUserService) { - annotationService = aAnnotationService; recommendationService = aRecommendationService; suggestionSupportRegistry = aSuggestionSupportRegistry; + preferencesService = aPreferencesService; + userService = aUserService; } @Override @@ -74,7 +78,7 @@ public String getId() @Override public boolean accepts(RenderRequest aRequest) { - AnnotatorState state = aRequest.getState(); + var state = aRequest.getState(); if (aRequest.getCas() == null) { return false; @@ -85,6 +89,21 @@ public boolean accepts(RenderRequest aRequest) return false; } + var prefs = preferencesService.loadDefaultTraitsForProject(KEY_RECOMMENDER_GENERAL_SETTINGS, + aRequest.getProject()); + + // Do not show predictions when viewing annotations of another user + if (!prefs.isShowRecommendationsWhenViewingOtherUser() + && !Objects.equals(aRequest.getAnnotationUser(), aRequest.getSessionOwner())) { + return false; + } + + // Do not show predictions when viewing annotations of curation user + if (!prefs.isShowRecommendationsWhenViewingCurationUser() + && Objects.equals(aRequest.getAnnotationUser(), userService.getCurationUser())) { + return false; + } + return true; } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 2328f84083e..5f5efd803ab 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -60,6 +60,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.core.Ordered; @@ -112,6 +113,8 @@ import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.recommendation.event.RecommenderDeletedEvent; import de.tudarmstadt.ukp.inception.recommendation.event.RecommenderUpdatedEvent; +import de.tudarmstadt.ukp.inception.recommendation.event.RecommendersResumedEvent; +import de.tudarmstadt.ukp.inception.recommendation.event.RecommendersSuspendedEvent; import de.tudarmstadt.ukp.inception.recommendation.model.DirtySpot; import de.tudarmstadt.ukp.inception.recommendation.tasks.NonTrainableRecommenderActivationTask; import de.tudarmstadt.ukp.inception.recommendation.tasks.PredictionTask; @@ -175,6 +178,9 @@ public class RecommendationServiceImpl { }; + @Value("${curation.sidebar.enabled:false}") + private boolean curationSidebarEnabled; + @Autowired public RecommendationServiceImpl(PreferencesService aPreferencesService, SessionRegistry aSessionRegistry, UserDao aUserRepository, @@ -211,62 +217,71 @@ public RecommendationServiceImpl(PreferencesService aPreferencesService, aLayerRecommendtionSupportRegistry); } + @Deprecated @Override - public Predictions getPredictions(User aUser, Project aProject) + public boolean isCurationSidebarEnabled() { - var state = getState(aUser.getUsername(), aProject); + return curationSidebarEnabled; + } + + @Override + public Predictions getPredictions(User aSessionOwner, Project aProject) + { + var state = getState(aSessionOwner.getUsername(), aProject); return state.getActivePredictions(); } @Override - public Predictions getIncomingPredictions(User aUser, Project aProject) + public Predictions getIncomingPredictions(User aSessionOwner, Project aProject) { - var state = getState(aUser.getUsername(), aProject); + var state = getState(aSessionOwner.getUsername(), aProject); return state.getIncomingPredictions(); } @Override - public void putIncomingPredictions(User aUser, Project aProject, Predictions aPredictions) + public void putIncomingPredictions(User aSessionOwner, Project aProject, + Predictions aPredictions) { - var state = getState(aUser.getUsername(), aProject); + var state = getState(aSessionOwner.getUsername(), aProject); synchronized (state) { state.setIncomingPredictions(aPredictions); } } @Override - public boolean hasActiveRecommenders(String aUser, Project aProject) + public boolean hasActiveRecommenders(String aSessionOwner, Project aProject) { - var state = getState(aUser, aProject); + var state = getState(aSessionOwner, aProject); synchronized (state) { return !state.getActiveRecommenders().isEmpty(); } } @Override - public void setEvaluatedRecommenders(User aUser, AnnotationLayer aLayer, + public void setEvaluatedRecommenders(User aSessionOwner, AnnotationLayer aLayer, List aRecommenders) { - var state = getState(aUser.getUsername(), aLayer.getProject()); + var state = getState(aSessionOwner.getUsername(), aLayer.getProject()); synchronized (state) { state.setEvaluatedRecommenders(aLayer, aRecommenders); } } @Override - public List getEvaluatedRecommenders(User aUser, AnnotationLayer aLayer) + public List getEvaluatedRecommenders(User aSessionOwner, + AnnotationLayer aLayer) { - var state = getState(aUser.getUsername(), aLayer.getProject()); + var state = getState(aSessionOwner.getUsername(), aLayer.getProject()); synchronized (state) { return new ArrayList<>(state.getEvaluatedRecommenders().get(aLayer)); } } @Override - public Optional getEvaluatedRecommender(User aUser, + public Optional getEvaluatedRecommender(User aSessionOwner, Recommender aRecommender) { - var state = getState(aUser.getUsername(), aRecommender.getProject()); + var state = getState(aSessionOwner.getUsername(), aRecommender.getProject()); synchronized (state) { return state.getEvaluatedRecommenders().get(aRecommender.getLayer()).stream() .filter(r -> r.getRecommender().equals(aRecommender)).findAny(); @@ -274,18 +289,19 @@ public Optional getEvaluatedRecommender(User aUser, } @Override - public List getActiveRecommenders(User aUser, AnnotationLayer aLayer) + public List getActiveRecommenders(User aSessionOwner, + AnnotationLayer aLayer) { - var state = getState(aUser.getUsername(), aLayer.getProject()); + var state = getState(aSessionOwner.getUsername(), aLayer.getProject()); synchronized (state) { return new ArrayList<>(state.getActiveRecommenders().get(aLayer)); } } @Override - public List getActiveRecommenders(User aUser, Project aProject) + public List getActiveRecommenders(User aSessionOwner, Project aProject) { - var state = getState(aUser.getUsername(), aProject); + var state = getState(aSessionOwner.getUsername(), aProject); synchronized (state) { return new ArrayList<>(state.getActiveRecommenders().values()); } @@ -373,14 +389,14 @@ public Recommender getRecommender(AnnotationSuggestion aSuggestion) @Override @Transactional - public boolean existsRecommender(Project aProject, String aName) + public boolean existsRecommender(Project aProject, String aRecommender) { var cb = entityManager.getCriteriaBuilder(); var query = cb.createQuery(Long.class); var root = query.from(Recommender.class); query.select(cb.count(root)).where(cb.and( // - cb.equal(root.get(Recommender_.name), aName), // + cb.equal(root.get(Recommender_.name), aRecommender), // cb.equal(root.get(Recommender_.project), aProject))); long count = entityManager.createQuery(query).getSingleResult(); @@ -390,7 +406,7 @@ public boolean existsRecommender(Project aProject, String aName) @Override @Transactional - public Optional getRecommender(Project aProject, String aName) + public Optional getRecommender(Project aProject, String aRecommender) { String query = String.join("\n", // "FROM Recommender ", // @@ -398,7 +414,7 @@ public Optional getRecommender(Project aProject, String aName) "AND project = :project"); return entityManager.createQuery(query, Recommender.class) // - .setParameter("name", aName) // + .setParameter("name", aRecommender) // .setParameter("project", aProject) // .getResultStream() // .findFirst(); @@ -490,7 +506,7 @@ public void onDocumentOpened(DocumentOpenedEvent aEvent) else { // If the session owner has switched the data they are looking at, we need to // clear and rebuild the predictions. - clearState(sessionOwnerName); + resetState(sessionOwnerName); } } @@ -543,6 +559,10 @@ public void onDocumentOpened(DocumentOpenedEvent aEvent) private boolean nonTrainableRecommenderRunSync(SourceDocument doc, Predictions predictions, User aSessionOwner, String trigger, String aDataOwner) { + if (isSuspended(aSessionOwner.getUsername(), doc.getProject())) { + return false; + } + if (predictions != null && predictions.hasRunPredictionOnDocument(doc)) { LOG.trace("Not running sync prediction for non-trainable recommenders as we already " + "have predictions"); @@ -750,7 +770,7 @@ public void onAfterCasWritten(AfterCasWrittenEvent aEvent) @EventListener public void onRecommenderUpdated(RecommenderUpdatedEvent aEvent) { - clearState(aEvent.getRecommender().getProject()); + resetState(aEvent.getRecommender().getProject()); } @EventListener @@ -765,13 +785,13 @@ public void onRecommenderDelete(RecommenderDeletedEvent aEvent) @EventListener public void onDocumentCreated(AfterDocumentCreatedEvent aEvent) { - clearState(aEvent.getDocument().getProject()); + resetState(aEvent.getDocument().getProject()); } @EventListener public void onDocumentRemoval(BeforeDocumentRemovedEvent aEvent) { - clearState(aEvent.getDocument().getProject()); + resetState(aEvent.getDocument().getProject()); } @EventListener @@ -789,21 +809,25 @@ public void onAfterProjectRemoved(AfterProjectRemovedEvent aEvent) @EventListener public void onLayerConfigurationChangedEvent(LayerConfigurationChangedEvent aEvent) { - clearState(aEvent.getProject()); + resetState(aEvent.getProject()); } @Override - public void triggerPrediction(String aUsername, String aEventName, SourceDocument aDocument, + public void triggerPrediction(String aSessionOwner, String aEventName, SourceDocument aDocument, String aDataOwner) { - var user = userRepository.get(aUsername); + if (isSuspended(aSessionOwner, aDocument.getProject())) { + return; + } + + var sessionOwner = userRepository.get(aSessionOwner); - if (user == null) { + if (sessionOwner == null) { return; } schedulingService.enqueue(PredictionTask.builder() // - .withSessionOwner(user) // + .withSessionOwner(sessionOwner) // .withTrigger(aEventName) // .withCurrentDocument(aDocument) // .withDataOwner(aDataOwner) // @@ -814,6 +838,10 @@ public void triggerPrediction(String aUsername, String aEventName, SourceDocumen public void triggerTrainingAndPrediction(String aSessionOwner, Project aProject, String aEventName, SourceDocument aCurrentDocument, String aDataOwner) { + if (isSuspended(aSessionOwner, aProject)) { + return; + } + triggerTraining(aSessionOwner, aProject, aEventName, aCurrentDocument, aDataOwner, false, null); } @@ -822,6 +850,10 @@ public void triggerTrainingAndPrediction(String aSessionOwner, Project aProject, public void triggerSelectionTrainingAndPrediction(String aSessionOwner, Project aProject, String aEventName, SourceDocument aCurrentDocument, String aDataOwner) { + if (isSuspended(aSessionOwner, aProject)) { + return; + } + triggerTraining(aSessionOwner, aProject, aEventName, aCurrentDocument, aDataOwner, true, null); } @@ -830,6 +862,10 @@ private void triggerTraining(String aSessionOwner, Project aProject, String aEve SourceDocument aCurrentDocument, String aDataOwner, boolean aForceSelection, Set aDirties) { + if (isSuspended(aSessionOwner, aProject)) { + return; + } + var user = userRepository.get(aSessionOwner); // do not trigger training during when viewing others' work if (user == null || !user.equals(userRepository.getCurrentUser())) { @@ -885,10 +921,10 @@ private void triggerTraining(String aSessionOwner, Project aProject, String aEve } @Override - public List getLog(String aUser, Project aProject) + public List getLog(String aSessionOwner, Project aProject) { - var activePredictions = getState(aUser, aProject).getActivePredictions(); - var incomingPredictions = getState(aUser, aProject).getIncomingPredictions(); + var activePredictions = getState(aSessionOwner, aProject).getActivePredictions(); + var incomingPredictions = getState(aSessionOwner, aProject).getIncomingPredictions(); var messageSets = new ArrayList(); @@ -914,10 +950,37 @@ public boolean isPredictForAllDocuments(String aUser, Project aProject) } @Override - public void setPredictForAllDocuments(String aUser, Project aProject, + public void setPredictForAllDocuments(String aSessionOwner, Project aProject, boolean aPredictForAllDocuments) { - getState(aUser, aProject).setPredictForAllDocuments(aPredictForAllDocuments); + getState(aSessionOwner, aProject).setPredictForAllDocuments(aPredictForAllDocuments); + } + + @Override + public boolean isSuspended(String aSessionOwner, Project aProject) + { + return getState(aSessionOwner, aProject).isSuspended(); + } + + @Override + public void setSuspended(String aSessionOwner, Project aProject, boolean aState) + { + var suspended = isSuspended(aSessionOwner, aProject); + if (suspended == aState) { + return; + } + + getState(aSessionOwner, aProject).setSuspended(aState); + if (aState) { + applicationEventPublisher + .publishEvent(new RecommendersSuspendedEvent(this, aProject, aSessionOwner)); + ; + } + else { + applicationEventPublisher + .publishEvent(new RecommendersResumedEvent(this, aProject, aSessionOwner)); + ; + } } @EventListener @@ -950,14 +1013,14 @@ public void afterDocumentReset(AfterDocumentResetEvent aEvent) { var currentDocument = aEvent.getDocument().getDocument(); var currentUser = aEvent.getDocument().getUser(); - clearState(currentUser); + resetState(currentUser); deleteLearningRecords(currentDocument, currentUser); } @Override - public Preferences getPreferences(User aUser, Project aProject) + public Preferences getPreferences(User aSessionOwner, Project aProject) { - var state = getState(aUser.getUsername(), aProject); + var state = getState(aSessionOwner.getUsername(), aProject); return state.getPreferences(); } @@ -974,22 +1037,47 @@ public Optional> getRecommenderFactory(Recommende return Optional.ofNullable(recommenderFactoryRegistry.getFactory(aRecommender.getTool())); } - private RecommendationState getState(String aUsername, Project aProject) + private RecommendationState getState(String aSessionOwner, Project aProject) { synchronized (states) { - return states.computeIfAbsent(new RecommendationStateKey(aUsername, aProject), + return states.computeIfAbsent(new RecommendationStateKey(aSessionOwner, aProject), (v) -> new RecommendationState()); } } @Override - public void clearState(String aUsername) + public void resetState(String aSessionOwner) + { + Validate.notNull(aSessionOwner, "Username must be specified"); + + synchronized (states) { + states.entrySet().stream() // + .filter(e -> aSessionOwner.equals(e.getKey().getUser())) + .forEach(e -> e.getValue().reset()); + trainingTaskCounter.keySet().removeIf(key -> aSessionOwner.equals(key.getUser())); + } + } + + private void clearState(String aSessionOwner) + { + Validate.notNull(aSessionOwner, "Username must be specified"); + + synchronized (states) { + states.keySet().removeIf(key -> aSessionOwner.equals(key.getUser())); + trainingTaskCounter.keySet().removeIf(key -> aSessionOwner.equals(key.getUser())); + } + } + + private void resetState(Project aProject) { - Validate.notNull(aUsername, "Username must be specified"); + Validate.notNull(aProject, "Project must be specified"); synchronized (states) { - states.keySet().removeIf(key -> aUsername.equals(key.getUser())); - trainingTaskCounter.keySet().removeIf(key -> aUsername.equals(key.getUser())); + states.entrySet().stream() // + .filter(e -> Objects.equals(aProject.getId(), e.getKey().getProjectId())) + .forEach(e -> e.getValue().reset()); + trainingTaskCounter.keySet() + .removeIf(key -> Objects.equals(aProject.getId(), key.getProjectId())); } } @@ -1035,9 +1123,10 @@ public Optional getContext(String aSessionOwner, Recommender } @Override - public void putContext(User aUser, Recommender aRecommender, RecommenderContext aContext) + public void putContext(User aSessionOwner, Recommender aRecommender, + RecommenderContext aContext) { - var state = getState(aUser.getUsername(), aRecommender.getProject()); + var state = getState(aSessionOwner.getUsername(), aRecommender.getProject()); synchronized (state) { state.putContext(aRecommender, aContext); } @@ -1202,12 +1291,14 @@ public int hashCode() */ private class RecommendationState { + private boolean suspended; private Preferences preferences; + private boolean predictForAllDocuments; + private MultiValuedMap evaluatedRecommenders; private Map contexts; private Predictions activePredictions; private Predictions incomingPredictions; - private boolean predictForAllDocuments; private Map> learningRecords; private int predictionsSinceLastEvaluation; private int predictionsUntilNextEvaluation; @@ -1220,6 +1311,17 @@ public RecommendationState() learningRecords = new ConcurrentHashMap<>(); } + public void reset() + { + evaluatedRecommenders = new HashSetValuedHashMap<>(); + contexts = new ConcurrentHashMap<>(); + activePredictions = null; + incomingPredictions = null; + learningRecords = new ConcurrentHashMap<>(); + predictionsSinceLastEvaluation = 0; + predictionsUntilNextEvaluation = 0; + } + public Preferences getPreferences() { return preferences; @@ -1390,6 +1492,16 @@ public void setPredictForAllDocuments(boolean aPredictForAllDocuments) predictForAllDocuments = aPredictForAllDocuments; } + public boolean isSuspended() + { + return suspended; + } + + public void setSuspended(boolean aSuspended) + { + suspended = aSuspended; + } + public void logRecord(LearningRecord aRecord) { var records = learningRecords.computeIfAbsent(aRecord.getLayer(), diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPage.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.html similarity index 72% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPage.html rename to inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.html index b4350a9314f..e1f6c9c1552 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/processor/BulkProcessingPage.html +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.html @@ -17,19 +17,7 @@ limitations under the License. --> - - - - - - - -

- - + + + diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.java new file mode 100644 index 00000000000..103996bc33c --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/EvaluationProgressPanel.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.sidebar; + +import static org.apache.commons.lang3.StringUtils.repeat; + +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.panel.GenericPanel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.spring.injection.annot.SpringBean; +import org.wicketstuff.event.annotation.OnEvent; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.event.PredictionsSwitchedEvent; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Progress; + +public class EvaluationProgressPanel + extends GenericPanel +{ + private static final long serialVersionUID = -8498000053224985486L; + + private @SpringBean UserDao userService; + private @SpringBean RecommendationService recommendationService; + + public EvaluationProgressPanel(String aId, IModel aModel) + { + super(aId, aModel); + + setOutputMarkupId(true); + + var sessionOwner = userService.getCurrentUser(); + + add(new Label("progress", LoadableDetachableModel.of(() -> { + Progress p = recommendationService.getProgressTowardsNextEvaluation(sessionOwner, + getModelObject()); + return repeat(" ", p.getDone()) + + repeat(" ", p.getTodo()); + })).setEscapeModelStrings(false)); // SAFE - RENDERING ONLY SPECIFIC ICONS + } + + @OnEvent + public void onPredictionsSwitched(PredictionsSwitchedEvent aEvent) + { + aEvent.getRequestTarget().ifPresent(target -> target.add(this)); + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.html index 417406d31cc..383fede09aa 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.html +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.html @@ -42,11 +42,9 @@
-
- - +
+ +
@@ -79,9 +77,24 @@
-
+ +
+ + + -
+
+
+
+ + +
+
+
+
+
+
+
diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.java index 465ccdd8f64..4bfcfa6c398 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.java @@ -17,10 +17,14 @@ */ package de.tudarmstadt.ukp.inception.recommendation.sidebar; +import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.KEY_RECOMMENDER_GENERAL_SETTINGS; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; +import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhenNot; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.wicket.ajax.AjaxRequestTarget; @@ -32,6 +36,7 @@ import org.apache.wicket.markup.html.form.NumberTextField; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.model.util.ListModel; import org.apache.wicket.spring.injection.annot.SpringBean; @@ -41,16 +46,13 @@ import com.googlecode.wicket.kendo.ui.widget.tooltip.TooltipBehavior; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.Preferences; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequestedEvent; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -69,7 +71,9 @@ public class RecommendationSidebar private @SpringBean RecommendationService recommendationService; private @SpringBean AnnotationSchemaService annoService; private @SpringBean UserDao userRepository; + private @SpringBean PreferencesService preferencesService; + private IModel recommendersAvailable; private WebMarkupContainer warning; private StringResourceModel tipModel; private Form form; @@ -82,32 +86,53 @@ public RecommendationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); - IModel modelPreferences = LambdaModelAdapter.of( - () -> recommendationService.getPreferences(aModel.getObject().getUser(), + recommendersAvailable = LoadableDetachableModel.of(this::isRecommendersAvailable); + + var mainContainer = new WebMarkupContainer("mainContainer"); + mainContainer.add(visibleWhen(recommendersAvailable)); + add(mainContainer); + + var sessionOwner = userRepository.getCurrentUser(); + var modelPreferences = LambdaModelAdapter.of( + () -> recommendationService.getPreferences(sessionOwner, aModel.getObject().getProject()), - (v) -> recommendationService.setPreferences(aModel.getObject().getUser(), + (v) -> recommendationService.setPreferences(sessionOwner, aModel.getObject().getProject(), v)); warning = new WebMarkupContainer("warning"); warning.setOutputMarkupPlaceholderTag(true); add(warning); tipModel = new StringResourceModel("mismatch", this); - TooltipBehavior tip = new TooltipBehavior(tipModel); + var tip = new TooltipBehavior(tipModel); tip.setOption("width", Options.asString("300px")); warning.add(tip); - Label noRecommendersLabel = new Label("noRecommendersLabel", + var noRecommendersLabel = new Label("noRecommendersLabel", new StringResourceModel("noRecommenders")); var recommenders = recommendationService .listEnabledRecommenders(aModel.getObject().getProject()); noRecommendersLabel.add(visibleWhen(() -> recommenders.isEmpty())); add(noRecommendersLabel); + var notAvailableNotice = new WebMarkupContainer("notAvailableNotice"); + notAvailableNotice.add(visibleWhenNot(recommendersAvailable)); + add(notAvailableNotice); + add(new LambdaAjaxLink("showLog", this::actionShowLog) - .add(visibleWhen(() -> !recommenders.isEmpty()))); + .add(visibleWhenNot(recommenders::isEmpty))); add(new LambdaAjaxLink("retrain", this::actionRetrain) - .add(visibleWhen(() -> !recommenders.isEmpty()))); + .add(visibleWhenNot(recommenders::isEmpty))); + + var modelEnabled = LambdaModelAdapter.of( + () -> !recommendationService.isSuspended(sessionOwner.getUsername(), + aModel.getObject().getProject()), + (v) -> recommendationService.setSuspended(sessionOwner.getUsername(), + aModel.getObject().getProject(), !v)); + mainContainer.add(new CheckBox("enabled", modelEnabled).setOutputMarkupId(true) + .add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT))); + mainContainer.add( + new EvaluationProgressPanel("progress", aModel.map(AnnotatorState::getProject))); form = new Form<>("form", CompoundPropertyModel.of(modelPreferences)); form.setOutputMarkupId(true); @@ -123,7 +148,7 @@ public RecommendationSidebar(String aId, IModel aModel, .add(visibleWhen(() -> !form.getModelObject().isShowAllPredictions()))); form.add(new CheckBox("showAllPredictions").setOutputMarkupId(true) - .add(new LambdaAjaxFormComponentUpdatingBehavior("change", + .add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, _target -> _target.add(form)))); form.add(new LambdaAjaxButton<>("save", @@ -132,16 +157,19 @@ public RecommendationSidebar(String aId, IModel aModel, add(form); - // add(new LearningCurveChartPanel(LEARNING_CURVE, aModel) - // .add(visibleWhen(() -> !recommenders.isEmpty()))); - recommenderInfos = new RecommenderInfoPanel("recommenders", aModel); recommenderInfos.add(visibleWhen(() -> !recommenders.isEmpty())); - add(recommenderInfos); + mainContainer.add(recommenderInfos); logDialog = new LogDialog("logDialog"); add(logDialog); + } + @Override + protected void onDetach() + { + super.onDetach(); + recommendersAvailable.detach(); } @Override @@ -150,21 +178,42 @@ protected void onConfigure() // using onConfigure as last state in lifecycle to configure visibility super.onConfigure(); configureMismatched(); - boolean enabled = getModelObject().getUser().equals(userRepository.getCurrentUser()); + var enabled = getModelObject().getUser().equals(userRepository.getCurrentUser()); form.setEnabled(enabled); recommenderInfos.setEnabled(enabled); } + private boolean isRecommendersAvailable() + { + var state = getModelObject(); + var prefs = preferencesService.loadDefaultTraitsForProject(KEY_RECOMMENDER_GENERAL_SETTINGS, + state.getProject()); + + // Do not show predictions when viewing annotations of another user + if (!prefs.isShowRecommendationsWhenViewingOtherUser() + && !Objects.equals(state.getUser(), userRepository.getCurrentUser())) { + return false; + } + + // Do not show predictions when viewing annotations of curation user + if (!prefs.isShowRecommendationsWhenViewingCurationUser() + && Objects.equals(state.getUser(), userRepository.getCurationUser())) { + return false; + } + + return true; + } + protected void configureMismatched() { - List mismatchedRecommenders = findMismatchedRecommenders(); + var mismatchedRecommenders = findMismatchedRecommenders(); if (mismatchedRecommenders.isEmpty()) { warning.setVisible(false); return; } - String recommendersStr = mismatchedRecommenders.stream().collect(Collectors.joining(", ")); + var recommendersStr = mismatchedRecommenders.stream().collect(Collectors.joining(", ")); tipModel.setParameters(recommendersStr); warning.setVisible(true); } @@ -185,11 +234,11 @@ private void actionShowLog(AjaxRequestTarget aTarget) private void actionRetrain(AjaxRequestTarget aTarget) { - AnnotatorState state = getModelObject(); + var state = getModelObject(); var sessionOwner = userRepository.getCurrentUsername(); var dataOwner = state.getUser().getUsername(); - recommendationService.clearState(sessionOwner); + recommendationService.resetState(sessionOwner); recommendationService.triggerSelectionTrainingAndPrediction(sessionOwner, state.getProject(), "User request via sidebar", state.getDocument(), dataOwner); @@ -201,15 +250,14 @@ private void actionRetrain(AjaxRequestTarget aTarget) private List findMismatchedRecommenders() { - List mismatchedRecommenderNames = new ArrayList<>(); - Project project = getModelObject().getProject(); - for (AnnotationLayer layer : annoService.listAnnotationLayer(project)) { + var mismatchedRecommenderNames = new ArrayList(); + var project = getModelObject().getProject(); + for (var layer : annoService.listAnnotationLayer(project)) { if (!layer.isEnabled()) { continue; } - for (Recommender recommender : recommendationService.listEnabledRecommenders(layer)) { - RecommendationEngineFactory factory = recommendationService - .getRecommenderFactory(recommender).orElse(null); + for (var recommender : recommendationService.listEnabledRecommenders(layer)) { + var factory = recommendationService.getRecommenderFactory(recommender).orElse(null); // E.g. if the module providing a configured recommender has been disabled but the // recommender is still configured. diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.properties b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.properties index 67b51c71d23..c2acef3cb29 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.properties +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebar.properties @@ -25,3 +25,4 @@ retrain=Retrain showLog=Log noRecommenders=None of the layers have any recommenders configured. Please set the recommenders first in the Project Settings. progressTowardsEvaluation=Progress towards next evaluation +notAvailableNotice=Currently not available \ No newline at end of file diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java index 11e8b29c2f9..bef73eaf1f9 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommendationSidebarFactory.java @@ -20,9 +20,8 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; -import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; -import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; @@ -39,6 +38,7 @@ * {@link RecommenderServiceAutoConfiguration#recommendationSidebarFactory}. *

*/ +@Order(5000) public class RecommendationSidebarFactory extends AnnotationSidebarFactory_ImplBase { @@ -66,7 +66,7 @@ public String getDescription() @Override public Component createIcon(String aId, IModel aState) { - return new Icon(aId, FontAwesome5IconType.chart_line_s); + return new RecommenderSidebarIcon(aId, aState); } @Override diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.html index 0468196f670..ddbfcfb441d 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.html +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.html @@ -19,9 +19,6 @@
-
- -
diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.java index aee73d84e75..ab9fe8c16ca 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderInfoPanel.java @@ -20,7 +20,6 @@ import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.getDocumentTitle; import static java.util.stream.Collectors.groupingBy; -import static org.apache.commons.lang3.StringUtils.repeat; import java.io.IOException; import java.util.Collection; @@ -59,7 +58,6 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation; 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.Progress; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; @@ -98,17 +96,10 @@ public RecommenderInfoPanel(String aId, IModel aModel) .closeOnClick(); add(detailsDialog); - add(new Label("progress", LoadableDetachableModel.of(() -> { - Progress p = recommendationService.getProgressTowardsNextEvaluation(sessionOwner, - aModel.getObject().getProject()); - return repeat(" ", p.getDone()) - + repeat(" ", p.getTodo()); - })).setEscapeModelStrings(false)); // SAFE - RENDERING ONLY SPECIFIC ICONS - var recommenderContainer = new WebMarkupContainer("recommenderContainer"); add(recommenderContainer); - ListView searchResultGroups = new ListView("recommender") + var searchResultGroups = new ListView("recommender") { private static final long serialVersionUID = -631500052426449048L; @@ -193,9 +184,8 @@ protected void populateItem(ListItem item) .add(visibleWhen(() -> !resultsContainer.isVisible()))); } }; - IModel> recommenders = LoadableDetachableModel - .of(() -> recommendationService - .listEnabledRecommenders(aModel.getObject().getProject())); + var recommenders = LoadableDetachableModel.of(() -> recommendationService + .listEnabledRecommenders(aModel.getObject().getProject())); searchResultGroups.setModel(recommenders); recommenderContainer.add(visibleWhen(() -> !recommenders.getObject().isEmpty())); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.html b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.html new file mode 100644 index 00000000000..b11677a1e94 --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.html @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.java new file mode 100644 index 00000000000..4f1bc0c465e --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/RecommenderSidebarIcon.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.sidebar; + +import java.util.Set; + +import org.apache.wicket.ClassAttributeModifier; +import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.spring.injection.annot.SpringBean; +import org.wicketstuff.event.annotation.OnEvent; + +import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; +import de.agilecoders.wicket.core.markup.html.bootstrap.image.IconType; +import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.event.RecommendersResumedEvent; +import de.tudarmstadt.ukp.inception.recommendation.event.RecommendersSuspendedEvent; +import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; + +public class RecommenderSidebarIcon + extends Panel +{ + private static final long serialVersionUID = -1870047500327624860L; + + private @SpringBean RecommendationService recommendationService; + private @SpringBean UserDao userService; + + public RecommenderSidebarIcon(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() + { + private static final long serialVersionUID = 4534226094224688646L; + + @Override + protected Set update(Set aClasses) + { + if (isSessionActive()) { + aClasses.add("text-primary"); + aClasses.remove("text-muted"); + } + else { + aClasses.add("text-muted"); + aClasses.remove("text-primary"); + } + + return aClasses; + } + })); + } + + @SuppressWarnings("unchecked") + public IModel getModel() + { + return (IModel) getDefaultModel(); + } + + public AnnotatorState getModelObject() + { + return (AnnotatorState) getDefaultModelObject(); + } + + private boolean isSessionActive() + { + return !recommendationService.isSuspended(userService.getCurrentUsername(), + getModelObject().getProject()); + } + + private IconType getStateIcon() + { + if (isSessionActive()) { + return FontAwesome5IconType.play_circle_s; + } + + return FontAwesome5IconType.stop_circle_s; + } + + @OnEvent + public void sessionStarted(RecommendersSuspendedEvent aEvent) + { + aEvent.getRequestTarget().ifPresent(target -> target.add(this)); + } + + @OnEvent + public void sessionStarted(RecommendersResumedEvent aEvent) + { + aEvent.getRequestTarget().ifPresent(target -> target.add(this)); + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/tasks/TrainingTask.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/tasks/TrainingTask.java index 6d9b7f6ed4e..13de2456777 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/tasks/TrainingTask.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/tasks/TrainingTask.java @@ -244,10 +244,10 @@ private void schedulePredictionTask() schedulingService.enqueue(predictionTask); } - private void commitContext(User user, Recommender recommender, RecommenderContext ctx) + private void commitContext(User aSessionOwner, Recommender recommender, RecommenderContext ctx) { ctx.close(); - recommenderService.putContext(user, recommender, ctx); + recommenderService.putContext(aSessionOwner, recommender, ctx); } private void logTrainingOverallEnd(long overallStartTime) @@ -371,9 +371,10 @@ private void logTrainingSuccessful(LazyCasLoader casses, Recommender recommender private void logTrainingOverallStart() { - LOG.debug("[{}][{}]: Starting training for project {} triggered by [{}]...", getId(), - getSessionOwner().getUsername(), getProject(), getTrigger()); - info("Starting training triggered by [%s]...", getTrigger()); + LOG.debug( + "[{}][{}]: Starting training for project {} on data from [{}] triggered by [{}]...", + getId(), getSessionOwner().getUsername(), getProject(), dataOwner, getTrigger()); + info("Starting training on data from [%s] triggered by [%s]...", dataOwner, getTrigger()); } private void logTrainingRecommenderStart(LazyCasLoader aLoader, Recommender recommender, diff --git a/inception/inception-recommendation/src/main/resources/META-INF/asciidoc/user-guide/projects_recommendation.adoc b/inception/inception-recommendation/src/main/resources/META-INF/asciidoc/user-guide/projects_recommendation.adoc index 74fee984fc3..483cfd2da4c 100644 --- a/inception/inception-recommendation/src/main/resources/META-INF/asciidoc/user-guide/projects_recommendation.adoc +++ b/inception/inception-recommendation/src/main/resources/META-INF/asciidoc/user-guide/projects_recommendation.adoc @@ -41,6 +41,9 @@ performed some action such as creating an annotation. NOTE: Enable this option only if all of your non-trainable recommenders have a fast response time, as otherwise your users may complain about a long delay when opening documents. +The option **show suggestions when viewing annotations from another user** configures whether to display annotation +suggestions when viewing annotations from another user (e.g. as project manager, you can select to view annotations from +any annotator in the open document dialog). == Per-recommender settings diff --git a/inception/inception-scheduling/pom.xml b/inception/inception-scheduling/pom.xml index 2439fe835cb..629b532417c 100644 --- a/inception/inception-scheduling/pom.xml +++ b/inception/inception-scheduling/pom.xml @@ -46,6 +46,10 @@ de.tudarmstadt.ukp.inception.app inception-support + + de.tudarmstadt.ukp.inception.app + inception-websocket + @@ -64,10 +68,6 @@ org.springframework.boot spring-boot - - org.springframework - spring-messaging - org.springframework.security spring-security-core diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/NotifyingTaskMonitor.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/NotifyingTaskMonitor.java index 93adf3abf34..41c05f9f599 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/NotifyingTaskMonitor.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/NotifyingTaskMonitor.java @@ -17,25 +17,22 @@ */ package de.tudarmstadt.ukp.inception.scheduling; -import static de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController.BASE_TOPIC; -import static de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController.TASKS_TOPIC; - -import org.springframework.messaging.simp.SimpMessagingTemplate; - +import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController; import de.tudarmstadt.ukp.inception.scheduling.controller.model.MTaskStateUpdate; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; public class NotifyingTaskMonitor extends TaskMonitor { - private final SimpMessagingTemplate msgTemplate; + private final SchedulerWebsocketController schedulerWebsocketController; private MTaskStateUpdate lastUpdate; - public NotifyingTaskMonitor(TaskHandle aHandle, Task aTask, SimpMessagingTemplate aMsgTemplate) + public NotifyingTaskMonitor(TaskHandle aHandle, Task aTask, + SchedulerWebsocketController aSchedulerWebsocketController) { super(aHandle, aTask); - msgTemplate = aMsgTemplate; + schedulerWebsocketController = aSchedulerWebsocketController; } @Override @@ -89,7 +86,7 @@ protected void onDestroy() { var msg = new MTaskStateUpdate(this, true); if (getUser() != null) { - msgTemplate.convertAndSendToUser(getUser(), "/queue" + BASE_TOPIC + TASKS_TOPIC, msg); + schedulerWebsocketController.dispatch(msg); } } @@ -102,8 +99,7 @@ private void sendNotification() var msg = new MTaskStateUpdate(this); if (lastUpdate == null || !lastUpdate.equals(msg)) { if (getUser() != null) { - msgTemplate.convertAndSendToUser(getUser(), "/queue" + BASE_TOPIC + TASKS_TOPIC, - msg); + schedulerWebsocketController.dispatch(msg); } lastUpdate = msg; } diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/ProjectTask.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/ProjectTask.java new file mode 100644 index 00000000000..206a8e6a430 --- /dev/null +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/ProjectTask.java @@ -0,0 +1,29 @@ +/* + * 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.scheduling; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; + +/** + * Marks a task as belonging to a project. That implies that all project managers can perform + * actions on it and that it should be shown on the processing page in the project. + */ +public interface ProjectTask +{ + Project getProject(); +} diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java index b706a443a9b..89f3a6eb1a9 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/Task.java @@ -29,11 +29,11 @@ import org.slf4j.MDC; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.simp.SimpMessagingTemplate; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.documents.api.RepositoryProperties; +import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController; public abstract class Task implements Runnable, InitializingBean @@ -41,7 +41,7 @@ public abstract class Task private final static AtomicInteger nextId = new AtomicInteger(1); private @Autowired RepositoryProperties repositoryProperties; - private @Autowired(required = false) SimpMessagingTemplate msgTemplate; + private @Autowired(required = false) SchedulerWebsocketController schedulerController; private final TaskHandle handle; private final User sessionOwner; @@ -80,8 +80,8 @@ public void afterPropertiesSet() { // For tasks that have a parent task, we use a non-notifying monitor. Also, we do not report // such subtasks ia the SchedulerControllerImpl - they are internal. - if (msgTemplate != null && sessionOwner != null && parentTask == null) { - monitor = new NotifyingTaskMonitor(handle, this, msgTemplate); + if (schedulerController != null && sessionOwner != null && parentTask == null) { + monitor = new NotifyingTaskMonitor(handle, this, schedulerController); } else { monitor = new TaskMonitor(handle, this); diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/TaskMonitor.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/TaskMonitor.java index d2406ca7a85..26962a7fa0b 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/TaskMonitor.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/TaskMonitor.java @@ -26,6 +26,7 @@ import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -34,6 +35,7 @@ public class TaskMonitor private final Deque messages = new ConcurrentLinkedDeque<>(); private final TaskHandle handle; + private final Project project; private final String user; private final String title; private final String type; @@ -56,6 +58,7 @@ public TaskMonitor(TaskHandle aHandle, Task aTask) handle = aHandle; type = aTask.getType(); user = aTask.getUser().map(User::getUsername).orElse(null); + project = aTask.getProject(); title = aTask.getTitle(); createTime = System.currentTimeMillis(); cancellable = aTask.isCancellable(); @@ -76,6 +79,11 @@ public String getUser() return user; } + public Project getProject() + { + return project; + } + public String getTitle() { return title; diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/SchedulerWebsocketController.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/SchedulerWebsocketController.java index 2e2bf1e1f25..4d30425514f 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/SchedulerWebsocketController.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/SchedulerWebsocketController.java @@ -18,10 +18,43 @@ package de.tudarmstadt.ukp.inception.scheduling.controller; import static de.tudarmstadt.ukp.inception.security.config.InceptionSecurityWebUIApiAutoConfiguration.BASE_API_URL; +import static de.tudarmstadt.ukp.inception.websocket.config.WebSocketConstants.PARAM_PROJECT; +import static de.tudarmstadt.ukp.inception.websocket.config.WebSocketConstants.TOPIC_ELEMENT_PROJECT; + +import java.util.Properties; + +import org.springframework.util.PropertyPlaceholderHelper; + +import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.inception.scheduling.controller.model.MTaskStateUpdate; public interface SchedulerWebsocketController { String BASE_URL = BASE_API_URL + "/scheduler"; + String BASE_TOPIC = "/scheduler"; - String TASKS_TOPIC = "/tasks"; + String USER_TASKS_TOPIC = BASE_TOPIC + "/user"; + String PROJECT_TASKS_TOPIC_TEMPLATE = BASE_TOPIC + TOPIC_ELEMENT_PROJECT + "{" + PARAM_PROJECT + + "}"; + + void dispatch(MTaskStateUpdate update); + + static String getUserTaskUpdatesTopic() + { + return USER_TASKS_TOPIC; + } + + static String getProjectTaskUpdatesTopic(Project aProject) + { + return getProjectTaskUpdatesTopic(aProject.getId()); + } + + static String getProjectTaskUpdatesTopic(long aProjectId) + { + var properties = new Properties(); + properties.setProperty(PARAM_PROJECT, String.valueOf(aProjectId)); + var replacer = new PropertyPlaceholderHelper("{", "}", null, false); + var topic = replacer.replacePlaceholders(PROJECT_TASKS_TOPIC_TEMPLATE, properties); + return topic; + } } diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/model/MTaskStateUpdate.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/model/MTaskStateUpdate.java index b9e6472eaa8..8e6f5721136 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/model/MTaskStateUpdate.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/controller/model/MTaskStateUpdate.java @@ -29,14 +29,19 @@ public class MTaskStateUpdate { - private final long timestamp; private final int id; + private final String title; + private final long timestamp; private final String type; + + private final String username; + private final long projectId; + private final String projectName; + + private final TaskState state; + private final int progress; private final int maxProgress; - private final TaskState state; - private final String title; - private final int messageCount; @JsonInclude(Include.NON_DEFAULT) private final boolean cancellable; @@ -44,6 +49,8 @@ public class MTaskStateUpdate @JsonInclude(Include.NON_DEFAULT) private final boolean removed; + private final int messageCount; + @JsonInclude(Include.NON_EMPTY) private final LogMessage latestMessage; @@ -54,17 +61,41 @@ public MTaskStateUpdate(TaskMonitor aMonitor) public MTaskStateUpdate(TaskMonitor aMonitor, boolean aRemoved) { - timestamp = System.currentTimeMillis(); - title = aMonitor.getTitle(); id = aMonitor.getHandle().getId(); + title = aMonitor.getTitle(); + timestamp = System.currentTimeMillis(); type = aMonitor.getType(); + + username = aMonitor.getUser(); + + projectId = aMonitor.getProject() != null ? aMonitor.getProject().getId() : -1; + projectName = aMonitor.getProject() != null ? aMonitor.getProject().getName() : null; + + state = aMonitor.getState(); + progress = aMonitor.getProgress(); maxProgress = aMonitor.getMaxProgress(); - state = aMonitor.getState(); + cancellable = aMonitor.isCancellable(); + messageCount = aMonitor.getMessages().size(); latestMessage = aMonitor.getMessages().peekLast(); + removed = aRemoved; - cancellable = aMonitor.isCancellable(); + } + + public String getUsername() + { + return username; + } + + public long getProjectId() + { + return projectId; + } + + public String getProjectName() + { + return projectName; } public String getTitle() diff --git a/inception/inception-schema-api/pom.xml b/inception/inception-schema-api/pom.xml index 709e56334a2..9f1de19819e 100644 --- a/inception/inception-schema-api/pom.xml +++ b/inception/inception-schema-api/pom.xml @@ -64,10 +64,6 @@ org.springframework spring-context - - org.springframework.security - spring-security-core - org.slf4j diff --git a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/AnnotationSchemaService.java b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/AnnotationSchemaService.java index 6d56e8bb5cc..65b9fb11be4 100644 --- a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/AnnotationSchemaService.java +++ b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/AnnotationSchemaService.java @@ -28,7 +28,6 @@ import org.apache.uima.resource.ResourceInitializationException; import org.apache.uima.resource.metadata.TypeSystemDescription; import org.apache.wicket.validation.ValidationError; -import org.springframework.security.access.prepost.PreAuthorize; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasUpgradeMode; import de.tudarmstadt.ukp.clarin.webanno.api.type.CASMetadata; @@ -64,7 +63,6 @@ public interface AnnotationSchemaService * @param tag * the tag. */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void createTag(Tag tag); /** @@ -74,7 +72,6 @@ public interface AnnotationSchemaService * @param tag * the tag. */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void createTags(Tag... tag); void updateTagRanks(TagSet aTagSet, List aTags); @@ -87,7 +84,6 @@ public interface AnnotationSchemaService * @param tagset * the tagset. */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void createTagSet(TagSet tagset); /** @@ -99,7 +95,6 @@ public interface AnnotationSchemaService * @param type * the type. */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void createOrUpdateLayer(AnnotationLayer type); void createFeature(AnnotationFeature feature); @@ -444,7 +439,6 @@ public interface AnnotationSchemaService * @param tag * the tag. */ - @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')") void removeTag(Tag tag); /** diff --git a/inception/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/tasks/ReindexTask.java b/inception/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/tasks/ReindexTask.java index 6ad28256c6b..e53d6437a1d 100644 --- a/inception/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/tasks/ReindexTask.java +++ b/inception/inception-search-core/src/main/java/de/tudarmstadt/ukp/inception/search/scheduling/tasks/ReindexTask.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import de.tudarmstadt.ukp.inception.scheduling.MatchResult; +import de.tudarmstadt.ukp.inception.scheduling.ProjectTask; import de.tudarmstadt.ukp.inception.scheduling.Task; import de.tudarmstadt.ukp.inception.search.SearchService; import de.tudarmstadt.ukp.inception.search.model.Monitor; @@ -43,6 +44,7 @@ */ public class ReindexTask extends IndexingTask_ImplBase + implements ProjectTask { public static final String TYPE = "ReindexTask"; diff --git a/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/WicketSecurityUtils.java b/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/WicketSecurityUtils.java new file mode 100644 index 00000000000..15d3896a566 --- /dev/null +++ b/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/WicketSecurityUtils.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.security; + +import org.apache.wicket.protocol.http.servlet.ServletWebRequest; +import org.apache.wicket.request.cycle.RequestCycle; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class WicketSecurityUtils +{ + public static String getCsrfTokenFromSession() + { + var httpRequest = (HttpServletRequest) RequestCycle.get().getRequest() + .getContainerRequest(); + var httpResponse = (HttpServletResponse) RequestCycle.get().getResponse() + .getContainerResponse(); + + var csrfTokenRepository = new HttpSessionCsrfTokenRepository(); + var csrfToken = csrfTokenRepository.loadDeferredToken(httpRequest, httpResponse); + + if (csrfToken != null) { + return csrfToken.get().getToken(); + } + else { + return ""; + } + } + + /** + * Checks if auto-logout is enabled. For Winstone, we get a max session length of 0, so here it + * is disabled. + */ + public static int getAutoLogoutTime() + { + int duration = 0; + var request = RequestCycle.get().getRequest(); + if (request instanceof ServletWebRequest servletRequest) { + var session = servletRequest.getContainerRequest().getSession(); + if (session != null) { + duration = session.getMaxInactiveInterval(); + } + } + return duration; + } +} diff --git a/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/config/InceptionSecurityAutoConfiguration.java b/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/config/InceptionSecurityAutoConfiguration.java index fd0e7457ed0..9e73a635d73 100644 --- a/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/config/InceptionSecurityAutoConfiguration.java +++ b/inception/inception-security/src/main/java/de/tudarmstadt/ukp/clarin/webanno/security/config/InceptionSecurityAutoConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; import org.springframework.security.crypto.password.PasswordEncoder; @@ -42,6 +43,7 @@ import de.tudarmstadt.ukp.inception.support.deployment.DeploymentModeService; @EnableWebSecurity +@EnableMethodSecurity public class InceptionSecurityAutoConfiguration { @Bean diff --git a/inception/inception-support-bootstrap/src/main/java/de/tudarmstadt/ukp/inception/bootstrap/IconToggleBox.java b/inception/inception-support-bootstrap/src/main/java/de/tudarmstadt/ukp/inception/bootstrap/IconToggleBox.java index 661b8bf7f59..f997ef2b2d6 100644 --- a/inception/inception-support-bootstrap/src/main/java/de/tudarmstadt/ukp/inception/bootstrap/IconToggleBox.java +++ b/inception/inception-support-bootstrap/src/main/java/de/tudarmstadt/ukp/inception/bootstrap/IconToggleBox.java @@ -24,7 +24,7 @@ import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.FormComponentUpdatingBehavior; -import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.model.IModel; import de.agilecoders.wicket.core.markup.html.bootstrap.image.IconType; @@ -33,7 +33,7 @@ import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; public class IconToggleBox - extends Panel + extends GenericPanel { private static final long serialVersionUID = 4721646397508723919L; @@ -75,12 +75,14 @@ public MarkupContainer setDefaultModel(IModel aModel) return super.setDefaultModel(aModel); } + @Override public IconToggleBox setModel(IModel aModel) { this.setDefaultModel(aModel); return this; } + @Override @SuppressWarnings("unchecked") public IModel getModel() { diff --git a/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/wicket/WicketUtil.java b/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/wicket/WicketUtil.java index a7c6c316479..606bc74bc22 100644 --- a/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/wicket/WicketUtil.java +++ b/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/wicket/WicketUtil.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.support.wicket; +import static java.lang.String.format; + import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; @@ -29,8 +31,10 @@ import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; +import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.Response; +import org.apache.wicket.request.Url; import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.PageRequestHandlerTracker; import org.apache.wicket.request.cycle.RequestCycle; @@ -248,4 +252,19 @@ public void onBeginRequest(RequestCycle aCycle) aApplication.getRequestCycleSettings() .addResponseFilter(new WicketUtil.TimingResponseFilter()); } + + public static String constructEndpointUrl(String aUrl) + { + var contextPath = WebApplication.get().getServletContext().getContextPath(); + var endPointUrl = Url.parse(format("%s%s", contextPath, aUrl)); + return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); + } + + public static String constructWsEndpointUrl(String aUrl) + { + var contextPath = WebApplication.get().getServletContext().getContextPath(); + var endPointUrl = Url.parse(format("%s%s", contextPath, aUrl)); + endPointUrl.setProtocol("ws"); + return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); + } } diff --git a/inception/inception-ui-annotation/pom.xml b/inception/inception-ui-annotation/pom.xml index 212ca9b8475..904026c2d5f 100644 --- a/inception/inception-ui-annotation/pom.xml +++ b/inception/inception-ui-annotation/pom.xml @@ -64,10 +64,6 @@ de.tudarmstadt.ukp.inception.app inception-api-render - - de.tudarmstadt.ukp.inception.app - inception-documents - de.tudarmstadt.ukp.inception.app inception-support diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/AnnotationPage.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/AnnotationPage.java index 601ba3d925e..80adf72e237 100755 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/AnnotationPage.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/AnnotationPage.java @@ -82,7 +82,7 @@ import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent; import de.tudarmstadt.ukp.inception.annotation.events.PreparingToOpenDocumentEvent; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; -import de.tudarmstadt.ukp.inception.documents.DocumentAccess; +import de.tudarmstadt.ukp.inception.documents.api.DocumentAccess; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorBase; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtensionRegistry; @@ -354,7 +354,7 @@ private SidebarPanel createLeftSidebar() private WebMarkupContainer createRightSidebar() { - WebMarkupContainer rightSidebar = new WebMarkupContainer("rightSidebar"); + var rightSidebar = new WebMarkupContainer("rightSidebar"); rightSidebar.setOutputMarkupPlaceholderTag(true); // Override sidebar width from preferences rightSidebar.add(new AttributeModifier("style", @@ -372,7 +372,7 @@ private WebMarkupContainer createRightSidebar() @Override public List getListOfDocs() { - AnnotatorState state = getModelObject(); + var state = getModelObject(); return new ArrayList<>(documentService .listAnnotatableDocuments(state.getProject(), state.getUser()).keySet()); } @@ -403,7 +403,7 @@ public CAS getEditorCas() throws IOException public void writeEditorCas(CAS aCas) throws IOException, AnnotationException { ensureIsEditable(); - AnnotatorState state = getModelObject(); + var state = getModelObject(); documentService.writeAnnotationCas(aCas, state.getDocument(), state.getUser(), true); bumpAnnotationCasTimestamp(state); diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/actionbar/docnav/DocumentNavigator.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/actionbar/docnav/DocumentNavigator.java index e170952b46a..0bfc6c79cd9 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/actionbar/docnav/DocumentNavigator.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/actionbar/docnav/DocumentNavigator.java @@ -32,7 +32,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.actionbar.open.OpenDocumentDialog; -import de.tudarmstadt.ukp.inception.documents.DocumentAccess; +import de.tudarmstadt.ukp.inception.documents.api.DocumentAccess; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.inception.support.wicket.input.InputBehavior; diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/config/AnnotationUIAutoConfiguration.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/config/AnnotationUIAutoConfiguration.java index 998ee155577..e633498f375 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/config/AnnotationUIAutoConfiguration.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/config/AnnotationUIAutoConfiguration.java @@ -35,6 +35,7 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.actionbar.undo.actions.SpanAnnotationActionUndoSupport; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.actionbar.undo.actions.UndoableActionSupportRegistryImpl; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.actionbar.undo.actions.UndoableAnnotationActionSupport; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.layer.LayerVisibilitySidebarFactory; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import jakarta.servlet.ServletContext; @@ -91,4 +92,10 @@ public CloseSessionActionBarExtension closeSessionActionBarExtension() { return new CloseSessionActionBarExtension(); } + + @Bean + public LayerVisibilitySidebarFactory layerVisibilitySidebarFactory() + { + return new LayerVisibilitySidebarFactory(); + } } diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebarFactory_ImplBase.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebarFactory_ImplBase.java index 80e78c33d68..06f1d2e1a4b 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebarFactory_ImplBase.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebarFactory_ImplBase.java @@ -18,10 +18,9 @@ package de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar; import org.springframework.beans.factory.BeanNameAware; -import org.springframework.core.Ordered; public abstract class AnnotationSidebarFactory_ImplBase - implements BeanNameAware, Ordered, AnnotationSidebarFactory + implements BeanNameAware, AnnotationSidebarFactory { private String beanName; @@ -36,10 +35,4 @@ public String getBeanName() { return beanName; } - - @Override - public int getOrder() - { - return Ordered.LOWEST_PRECEDENCE; - } } diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebar_ImplBase.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebar_ImplBase.java index 58e012ef72f..11ddf6ebf43 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebar_ImplBase.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/AnnotationSidebar_ImplBase.java @@ -20,7 +20,7 @@ import java.io.IOException; import org.apache.wicket.ajax.AjaxRequestTarget; -import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.model.IModel; import org.apache.wicket.spring.injection.annot.SpringBean; @@ -32,7 +32,7 @@ import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; public abstract class AnnotationSidebar_ImplBase - extends Panel + extends GenericPanel { private static final long serialVersionUID = 8637373389151630602L; @@ -59,22 +59,14 @@ public AnnotationSidebar_ImplBase(final String aId, final IModel setOutputMarkupPlaceholderTag(true); } - public void setModel(IModel aModel) - { - setDefaultModel(aModel); - } - + @Override @SuppressWarnings("unchecked") public IModel getModel() { return (IModel) getDefaultModel(); } - public void setModelObject(AnnotatorState aModel) - { - setDefaultModelObject(aModel); - } - + @Override public AnnotatorState getModelObject() { return (AnnotatorState) getDefaultModelObject(); diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarPanel.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarPanel.java index b7f9250a9b5..7b64e3dc8bb 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarPanel.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarPanel.java @@ -101,16 +101,15 @@ public void refreshTabs(AjaxRequestTarget aTarget) private List makeTabs() { - List tabs = new ArrayList<>(); - for (AnnotationSidebarFactory factory : sidebarRegistry.getSidebarFactories()) { + var tabs = new ArrayList(); + for (var factory : sidebarRegistry.getSidebarFactories()) { if (!factory.applies(stateModel.getObject())) { continue; } - String factoryId = factory.getBeanName(); - SidebarTab tab = new SidebarTab(Model.of(factory.getDisplayName()), - factory.getBeanName()) + var factoryId = factory.getBeanName(); + var tab = new SidebarTab(Model.of(factory.getDisplayName()), factory.getBeanName()) { private static final long serialVersionUID = 2144644282070158783L; diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTab.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTab.java index 687400a4402..849b27f191d 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTab.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTab.java @@ -20,7 +20,6 @@ import org.apache.wicket.Component; import org.apache.wicket.extensions.markup.html.tabs.AbstractTab; import org.apache.wicket.model.IModel; -import org.springframework.context.ApplicationContext; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.support.spring.ApplicationContextProvider; @@ -49,13 +48,12 @@ public Component getIcon(String aId, IModel aState) // We need to get the methods and services directly in here so // that the lambda doesn't have a dependency on the non-serializable // AnnotationSidebarFactory class. - ApplicationContext ctx = ApplicationContextProvider.getApplicationContext(); + var ctx = ApplicationContextProvider.getApplicationContext(); return ctx.getBean(AnnotationSidebarRegistry.class).getSidebarFactory(factoryId) .createIcon(aId, aState); } catch (Exception e) { throw new RuntimeException(e); } - } } diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTabbedPanel.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTabbedPanel.java index dc6c330131b..ee867dba722 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTabbedPanel.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/SidebarTabbedPanel.java @@ -37,7 +37,6 @@ import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; -import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.preferences.Key; import de.tudarmstadt.ukp.inception.preferences.PreferencesService; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; @@ -69,7 +68,7 @@ public SidebarTabbedPanel(String aId, List aTabs, IModel aSta setOutputMarkupId(true); setVisible(!aTabs.isEmpty()); - LambdaAjaxLink showHideLink = new LambdaAjaxLink("showHideLink", this::showHideAction); + var showHideLink = new LambdaAjaxLink("showHideLink", this::showHideAction); showHideLink.add(new Icon("showHideIcon", LoadableDetachableModel.of(() -> isExpanded() ? chevron_left_s : chevron_right_s))); @@ -118,19 +117,19 @@ public TabbedPanel setSelectedTab(int aIndex) private void saveSidebarState() { - AnnotationSidebarState sidebarState = new AnnotationSidebarState(); + var sidebarState = new AnnotationSidebarState(); sidebarState.setSelectedTab(getTabs().get(getSelectedTab()).getFactoryId()); sidebarState.setExpanded(expanded); - User user = userService.getCurrentUser(); + var user = userService.getCurrentUser(); prefService.saveTraitsForUserAndProject(KEY_SIDEBAR_STATE, user, state.getObject().getProject(), sidebarState); } private void loadSidebarState() { - User user = userService.getCurrentUser(); - AnnotationSidebarState sidebarState = prefService.loadTraitsForUserAndProject( - KEY_SIDEBAR_STATE, user, state.getObject().getProject()); + var user = userService.getCurrentUser(); + var sidebarState = prefService.loadTraitsForUserAndProject(KEY_SIDEBAR_STATE, user, + state.getObject().getProject()); if (isNotBlank(sidebarState.getSelectedTab())) { var tabFactories = getTabs().stream().map(SidebarTab::getFactoryId) .collect(Collectors.toList()); @@ -145,8 +144,8 @@ private void loadSidebarState() @Override protected Component newTitle(String aTitleId, IModel aTitleModel, int aIndex) { - SidebarTab tab = getTabs().get(aIndex); - Component icon = tab.getIcon("icon", state); + var tab = getTabs().get(aIndex); + var icon = tab.getIcon("icon", state); icon.add(new AttributeModifier("title", aTitleModel)); return icon; } diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/docinfo/DocumentInfoSidebarFactory.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/docinfo/DocumentInfoSidebarFactory.java index b7dedaa58ac..acee3bcb1a0 100644 --- a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/docinfo/DocumentInfoSidebarFactory.java +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/docinfo/DocumentInfoSidebarFactory.java @@ -19,6 +19,7 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; +import org.springframework.core.annotation.Order; import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; @@ -31,6 +32,7 @@ import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; // This now mainly serves as example code - we do not actually use it +@Order(1) public class DocumentInfoSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.html b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.html new file mode 100644 index 00000000000..fda7b82fd29 --- /dev/null +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.html @@ -0,0 +1,37 @@ + + + + +
+
+
+ +
+
+
    +
  • + + +
  • +
+
+
+
+
+ diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.java new file mode 100644 index 00000000000..4f595a891fc --- /dev/null +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.layer; + +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; + +import java.io.IOException; +import java.util.List; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.spring.injection.annot.SpringBean; + +import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.preferences.UserPreferencesService; +import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.bootstrap.IconToggleBox; +import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.config.AnnotationSchemaProperties; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; + +public class LayerVisibilitySidebar + extends AnnotationSidebar_ImplBase +{ + private static final long serialVersionUID = 6127948490101336779L; + + private @SpringBean AnnotationSchemaService annotationService; + private @SpringBean AnnotationSchemaProperties annotationEditorProperties; + private @SpringBean UserPreferencesService userPreferencesService; + private @SpringBean UserDao userService; + + public LayerVisibilitySidebar(String aId, IModel aModel, + AnnotationActionHandler aActionHandler, CasProvider aCasProvider, + AnnotationPage aAnnotationPage) + { + super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); + + add(createLayerContainer("annotationLayers", LoadableDetachableModel.of(this::listLayers))); + } + + private List listLayers() + { + return annotationService.listSupportedLayers(getModelObject().getProject()).stream() // + .filter(AnnotationLayer::isEnabled) // + .filter(layer -> !annotationEditorProperties.isLayerBlocked(layer)) // + .toList(); + } + + private void actionToggleVisibility(AnnotationLayer aLayer, boolean aHidden, + AjaxRequestTarget aTarget) + throws IOException + { + getModelObject().getPreferences().setLayerVisible(aLayer, !aHidden); + + var sessionOwner = userService.getCurrentUsername(); + userPreferencesService.savePreferences(getModelObject(), sessionOwner); + userPreferencesService.loadPreferences(getModelObject(), sessionOwner); + + getAnnotationPage().actionRefreshDocument(aTarget); + } + + private ListView createLayerContainer(String aId, + IModel> aLayers) + { + return new ListView(aId, aLayers) + { + private static final long serialVersionUID = -4040731191748923013L; + + @Override + protected void populateItem(ListItem aItem) + { + var prefs = LayerVisibilitySidebar.this.getModelObject().getPreferences(); + var layer = aItem.getModelObject(); + var hiddenLayerIds = prefs.getHiddenAnnotationLayerIds(); + + var layerVisible = new IconToggleBox("visibleToggle") // + .setCheckedIcon(FontAwesome5IconType.eye_s) + .setCheckedTitle(Model.of("Visible")) + .setUncheckedIcon(FontAwesome5IconType.eye_slash_s) + .setUncheckedTitle(Model.of("Hidden")) + .setModel(Model.of(!hiddenLayerIds.contains(layer.getId()))); + layerVisible.add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, + _target -> actionToggleVisibility(layer, layerVisible.getModelObject(), + _target))); + aItem.add(layerVisible); + + aItem.add(new Label("name", layer.getUiName())); + } + }; + } +} diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.properties b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.properties new file mode 100644 index 00000000000..ebfb183fda5 --- /dev/null +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebar.properties @@ -0,0 +1,18 @@ +# Licensed to the Technische Universitt Darmstadt under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The Technische Universitt 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. +layers=Layers +layer=Layer +show=Show diff --git a/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebarFactory.java b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebarFactory.java new file mode 100644 index 00000000000..1ff683b3b6d --- /dev/null +++ b/inception/inception-ui-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/annotation/sidebar/layer/LayerVisibilitySidebarFactory.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.layer; + +import org.apache.wicket.Component; +import org.apache.wicket.model.IModel; +import org.springframework.core.annotation.Order; + +import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; +import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; +import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebarFactory_ImplBase; +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; + +@Order(2000) +public class LayerVisibilitySidebarFactory + extends AnnotationSidebarFactory_ImplBase +{ + @Override + public String getDisplayName() + { + return "Layers"; + } + + @Override + public String getDescription() + { + return "Allows showing/hiding layers"; + } + + @Override + public Component createIcon(String aId, IModel aState) + { + return new Icon(aId, FontAwesome5IconType.layer_group_s); + } + + @Override + public AnnotationSidebar_ImplBase create(String aId, IModel aModel, + AnnotationActionHandler aActionHandler, CasProvider aCasProvider, + AnnotationPage aAnnotationPage) + { + return new LayerVisibilitySidebar(aId, aModel, aActionHandler, aCasProvider, + aAnnotationPage); + } +} diff --git a/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/logout/LogoutPanel.java b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/logout/LogoutPanel.java index 559a0d379b0..af16e95e771 100644 --- a/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/logout/LogoutPanel.java +++ b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/logout/LogoutPanel.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.clarin.webanno.ui.core.logout; +import static de.tudarmstadt.ukp.clarin.webanno.security.WicketSecurityUtils.getAutoLogoutTime; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.enabledWhen; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -31,9 +32,6 @@ import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; -import org.apache.wicket.protocol.http.servlet.ServletWebRequest; -import org.apache.wicket.request.Request; -import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.flow.RedirectToUrlException; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; @@ -46,7 +44,6 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.core.login.LoginPage; import de.tudarmstadt.ukp.clarin.webanno.ui.core.users.ManageUsersPage; import de.tudarmstadt.ukp.inception.support.lambda.LambdaStatelessLink; -import jakarta.servlet.http.HttpSession; @StatelessComponent public class LogoutPanel @@ -130,7 +127,7 @@ public static void actionLogout(Component aOwner, } if (isNotBlank(aSecProperties.getAutoLogin())) { - PageParameters parameters = new PageParameters(); + var parameters = new PageParameters(); parameters.set(LoginPage.PARAM_SKIP_AUTO_LOGIN, true); aOwner.setResponsePage(LoginPage.class, parameters); return; @@ -138,21 +135,4 @@ public static void actionLogout(Component aOwner, aOwner.setResponsePage(aOwner.getApplication().getHomePage()); } - - /** - * Checks if auto-logout is enabled. For Winstone, we get a max session length of 0, so here it - * is disabled. - */ - private int getAutoLogoutTime() - { - int duration = 0; - Request request = RequestCycle.get().getRequest(); - if (request instanceof ServletWebRequest) { - HttpSession session = ((ServletWebRequest) request).getContainerRequest().getSession(); - if (session != null) { - duration = session.getMaxInactiveInterval(); - } - } - return duration; - } } diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtension.js b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtension.js similarity index 100% rename from inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtension.js rename to inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtension.js diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionBehavior.java b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionBehavior.java similarity index 98% rename from inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionBehavior.java rename to inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionBehavior.java index a2a8d5a6223..a548e240843 100644 --- a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionBehavior.java +++ b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionBehavior.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.websocket.feedback; +package de.tudarmstadt.ukp.inception.ui.core.feedback; import org.apache.wicket.Component; import org.apache.wicket.Page; diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionJavascriptReference.java b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionJavascriptReference.java similarity index 96% rename from inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionJavascriptReference.java rename to inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionJavascriptReference.java index e55fa3da271..d73bc2c4ef3 100644 --- a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/feedback/FeedbackPanelExtensionJavascriptReference.java +++ b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/inception/ui/core/feedback/FeedbackPanelExtensionJavascriptReference.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.websocket.feedback; +package de.tudarmstadt.ukp.inception.ui.core.feedback; import org.apache.wicket.request.resource.JavaScriptResourceReference; diff --git a/inception/inception-ui-curation/pom.xml b/inception/inception-ui-curation/pom.xml index 294c5f6c57e..7e89559df9f 100644 --- a/inception/inception-ui-curation/pom.xml +++ b/inception/inception-ui-curation/pom.xml @@ -64,10 +64,6 @@ de.tudarmstadt.ukp.inception.app inception-model-vdoc
- - de.tudarmstadt.ukp.inception.app - inception-documents - de.tudarmstadt.ukp.inception.app inception-project-api diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/actionbar/CurationDocumentNavigator.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/actionbar/CurationDocumentNavigator.java index 9a693d96872..0c3a1d5a97c 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/actionbar/CurationDocumentNavigator.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/actionbar/CurationDocumentNavigator.java @@ -31,7 +31,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.annotation.actionbar.export.ExportDocumentDialog; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; -import de.tudarmstadt.ukp.inception.documents.DocumentAccess; +import de.tudarmstadt.ukp.inception.documents.api.DocumentAccess; import de.tudarmstadt.ukp.inception.project.api.ProjectService; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.inception.support.wicket.input.InputBehavior; diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarFactory.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarFactory.java index aeed0f1ad51..ba4ad5e03c7 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarFactory.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarFactory.java @@ -24,6 +24,7 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; import org.slf4j.Logger; +import org.springframework.core.annotation.Order; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -41,6 +42,7 @@ * {@link CurationSidebarAutoConfiguration#curationSidebarFactory}. *

*/ +@Order(3000) public class CurationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/sidebar/ExternalSearchAnnotationSidebarFactory.java b/inception/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/sidebar/ExternalSearchAnnotationSidebarFactory.java index 3219d91babe..174509389cf 100644 --- a/inception/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/sidebar/ExternalSearchAnnotationSidebarFactory.java +++ b/inception/inception-ui-external-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/externalsearch/sidebar/ExternalSearchAnnotationSidebarFactory.java @@ -19,6 +19,7 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; +import org.springframework.core.annotation.Order; import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; @@ -39,6 +40,7 @@ * {@link ExternalSearchUIAutoConfiguration#externalSearchAnnotationSidebarFactory}. *

*/ +@Order(3600) public class ExternalSearchAnnotationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor_ImplBase.java b/inception/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor_ImplBase.java index c515ba66c59..ddd06a7295d 100644 --- a/inception/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor_ImplBase.java +++ b/inception/inception-ui-kb/src/main/java/de/tudarmstadt/ukp/inception/ui/kb/feature/ConceptFeatureEditor_ImplBase.java @@ -115,19 +115,19 @@ protected List getCandidates(IModel aStateModel, } // Extract exact match filter on the query - boolean labelFilter = false; - String trimmedInput = input.trim(); + var labelFilter = false; + var trimmedInput = input.trim(); if (trimmedInput.length() > 2 && trimmedInput.startsWith("\"") && trimmedInput.endsWith("\"")) { input = StringUtils.substring(trimmedInput, 1, -1).trim(); labelFilter = true; } - final String finalInput = input; + final var finalInput = input; List choices; try { - AnnotationFeature feat = getModelObject().feature; + var feat = getModelObject().feature; var traits = readFeatureTraits(feat); var repoId = traits.getRepositoryId(); @@ -181,8 +181,7 @@ protected List getCandidates(IModel aStateModel, } var result = choices.stream()// - .limit(entityLinkingProperties.getCandidateDisplayLimit()) - .collect(Collectors.toList()); + .limit(entityLinkingProperties.getCandidateDisplayLimit()).toList(); WicketUtil.serverTiming("getCandidates", currentTimeMillis() - startTime); diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.css b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.css index 5a3a42c8f56..11fea6d03ff 100644 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.css +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.css @@ -24,5 +24,6 @@ .document-table tr td:nth-child(5), .document-table tr th:nth-child(5) { white-space: nowrap;} .document-table tr td:nth-child(6), .document-table tr th:nth-child(6) { white-space: nowrap;} .document-table tr td:nth-child(7), .document-table tr th:nth-child(7) { white-space: nowrap;} +.document-table tr td:nth-child(8), .document-table tr th:nth-child(8) { white-space: nowrap;} .document-table col.s, .document-table tr.s , .document-table td.s { background-color: var(--primary); } .document-table .state-toggle { white-space: nowrap; cursor: pointer; } diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.java b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.java index 0c412b69a8a..6e19fcf29da 100644 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.java +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.java @@ -122,7 +122,9 @@ public SourceDocumentTable(String aId, IModel> aModel) columns.add(new LambdaColumn<>(new ResourceModel("DocumentFormat"), FORMAT, $ -> renderFormat($.getDocument().getFormat()))); columns.add(new LambdaColumn<>(new ResourceModel("DocumentSize"), - $ -> renderSize($.getDocument()))); + $ -> renderDocumentSize($.getDocument()))); + columns.add(new LambdaColumn<>(new ResourceModel("InitialCasSize"), + $ -> renderInitialCasSize($.getDocument()))); columns.add(new LambdaColumn<>(new ResourceModel("DocumentCreated"), CREATED, $ -> renderDate($.getDocument().getCreated()))); columns.add(new SourceDocumentTableDeleteActionColumn(this)); @@ -197,12 +199,24 @@ private String renderDate(Date aDate) return dateFormat.format(aDate); } - private String renderSize(SourceDocument aDocumnent) + private String renderDocumentSize(SourceDocument aDocumnent) { return FileUtils.byteCountToDisplaySize( FileUtils.sizeOf(documentService.getSourceDocumentFile(aDocumnent))); } + private String renderInitialCasSize(SourceDocument aDocument) + { + try { + return documentService.getInitialCasFileSize(aDocument) + .map(FileUtils::byteCountToDisplaySize).orElse("unknown"); + } + catch (IOException e) { + LOG.error("Unable to get size of INITIAL CAS file for {}", aDocument, e); + return "error"; + } + } + @Override public void renderHead(IHeaderResponse aResponse) { diff --git a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.properties b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.properties index c8f24d87065..862c64ffbb1 100644 --- a/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.properties +++ b/inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/documents/SourceDocumentTable.properties @@ -18,4 +18,5 @@ DocumentName=Name DocumentFormat=Format DocumentCreated=Created DocumentSize=Size +InitialCasSize=CAS Size nameFilter.placeholder=Filter by name... diff --git a/inception/inception-ui-scheduling/pom.xml b/inception/inception-ui-scheduling/pom.xml index cd2d7e35752..cbc114c16aa 100644 --- a/inception/inception-ui-scheduling/pom.xml +++ b/inception/inception-ui-scheduling/pom.xml @@ -45,6 +45,14 @@ de.tudarmstadt.ukp.inception.app inception-security
+ + de.tudarmstadt.ukp.inception.app + inception-model + + + de.tudarmstadt.ukp.inception.app + inception-project + @@ -75,20 +83,12 @@ org.springframework.security spring-security-core - - org.springframework.security - spring-security-web - org.apache.wicket wicket-core - - org.apache.wicket - wicket-request - org.apache.wicket wicket-spring diff --git a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorFooterItem.java b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorFooterItem.java index ba9bd60bd5b..a76636732ee 100644 --- a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorFooterItem.java +++ b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorFooterItem.java @@ -29,6 +29,8 @@ public class TaskMonitorFooterItem @Override public Component create(String aId) { - return new TaskMonitorPanel(aId).setShowFinishedTasks(true).setPopupMode(true); + return new TaskMonitorPanel(aId) // + .setShowFinishedTasks(true) // + .setPopupMode(true); } } diff --git a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorPanel.java b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorPanel.java index e5de895f10d..e49269e62ef 100644 --- a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorPanel.java +++ b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/TaskMonitorPanel.java @@ -17,27 +17,25 @@ */ package de.tudarmstadt.ukp.inception.ui.scheduling; +import static de.tudarmstadt.ukp.clarin.webanno.security.WicketSecurityUtils.getCsrfTokenFromSession; +import static de.tudarmstadt.ukp.inception.support.wicket.WicketUtil.constructEndpointUrl; +import static de.tudarmstadt.ukp.inception.support.wicket.WicketUtil.constructWsEndpointUrl; import static de.tudarmstadt.ukp.inception.websocket.config.WebsocketConfig.WS_ENDPOINT; -import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.isBlank; import java.util.Map; -import org.apache.commons.lang3.StringUtils; import org.apache.wicket.authorization.Action; import org.apache.wicket.authroles.authorization.strategies.role.annotations.AuthorizeAction; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.model.Model; -import org.apache.wicket.request.Url; -import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; -import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerController; import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController; import de.tudarmstadt.ukp.inception.support.svelte.SvelteBehavior; import jakarta.servlet.ServletContext; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; @AuthorizeAction(action = Action.RENDER, roles = "ROLE_USER") public class TaskMonitorPanel @@ -46,15 +44,44 @@ public class TaskMonitorPanel private static final long serialVersionUID = -9006607500867612027L; private @SpringBean ServletContext servletContext; + private @SpringBean SchedulerWebsocketController schedulerWebsocketController; + private final String taskStatusTopic; + private final String taskUpdatesTopic; private boolean popupMode = true; private boolean showFinishedTasks = true; private String typePattern = ""; + /** + * Create a monitoring panel that subscribes to all events for the current user. + * + * @param aId + * The non-null id of this component + */ public TaskMonitorPanel(String aId) { super(aId); setOutputMarkupPlaceholderTag(true); + taskStatusTopic = "/app" + SchedulerWebsocketController.getUserTaskUpdatesTopic(); + taskUpdatesTopic = "/user/queue" + SchedulerWebsocketController.getUserTaskUpdatesTopic(); + } + + /** + * Create a monitoring panel that subscribes to all events for the given project. + * + * @param aId + * The non-null id of this component + * @param aProject + * The project to monitor. + */ + public TaskMonitorPanel(String aId, Project aProject) + { + super(aId); + setOutputMarkupPlaceholderTag(true); + taskStatusTopic = "/app" + + SchedulerWebsocketController.getProjectTaskUpdatesTopic(aProject); + taskUpdatesTopic = "/topic" + + SchedulerWebsocketController.getProjectTaskUpdatesTopic(aProject); } public TaskMonitorPanel setPopupMode(boolean aPopupMode) @@ -71,7 +98,7 @@ public TaskMonitorPanel setShowFinishedTasks(boolean aKeepRemovedTasks) public TaskMonitorPanel setTypePattern(String aTypePattern) { - if (StringUtils.isBlank(aTypePattern)) { + if (isBlank(aTypePattern)) { typePattern = ""; } else { @@ -91,42 +118,11 @@ protected void onConfigure() "popupMode", popupMode, // "showFinishedTasks", showFinishedTasks, // "typePattern", typePattern, // - "endpointUrl", constructEndpointUrl(), // - "wsEndpointUrl", constructWsEndpointUrl(), // - "topicChannel", SchedulerWebsocketController.BASE_TOPIC))); + "endpointUrl", constructEndpointUrl(SchedulerController.BASE_URL), // + "wsEndpointUrl", constructWsEndpointUrl(WS_ENDPOINT), // + "taskStatusTopic", taskStatusTopic, // + "taskUpdatesTopic", taskUpdatesTopic))); add(new SvelteBehavior()); } - - private String constructEndpointUrl() - { - Url endPointUrl = Url.parse( - format("%s%s", servletContext.getContextPath(), SchedulerController.BASE_URL)); - return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); - } - - private String constructWsEndpointUrl() - { - Url endPointUrl = Url.parse(format("%s%s", servletContext.getContextPath(), WS_ENDPOINT)); - endPointUrl.setProtocol("ws"); - return RequestCycle.get().getUrlRenderer().renderFullUrl(endPointUrl); - } - - public String getCsrfTokenFromSession() - { - var httpRequest = (HttpServletRequest) RequestCycle.get().getRequest() - .getContainerRequest(); - var httpResponse = (HttpServletResponse) RequestCycle.get().getResponse() - .getContainerResponse(); - - var csrfTokenRepository = new HttpSessionCsrfTokenRepository(); - var csrfToken = csrfTokenRepository.loadDeferredToken(httpRequest, httpResponse); - - if (csrfToken != null) { - return csrfToken.get().getToken(); - } - else { - return ""; - } - } } diff --git a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/config/SchedulingUiAutoConfiguration.java b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/config/SchedulingUiAutoConfiguration.java index a7ac8824d3a..568436feebe 100644 --- a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/config/SchedulingUiAutoConfiguration.java +++ b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/config/SchedulingUiAutoConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import de.tudarmstadt.ukp.clarin.webanno.project.ProjectAccess; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerController; @@ -40,8 +41,9 @@ public TaskMonitorFooterItem taskMonitorFooterItem() } @Bean - SchedulerController schedulerController(SchedulingService aSchedulingService, UserDao aUserDao) + SchedulerController schedulerController(SchedulingService aSchedulingService, UserDao aUserDao, + ProjectAccess aProjectAccess) { - return new SchedulerControllerImpl(aSchedulingService, aUserDao); + return new SchedulerControllerImpl(aSchedulingService, aUserDao, aProjectAccess); } } diff --git a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerControllerImpl.java b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerControllerImpl.java index 2b79c8612e0..0d61b6a0f02 100644 --- a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerControllerImpl.java +++ b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerControllerImpl.java @@ -26,8 +26,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import de.tudarmstadt.ukp.clarin.webanno.project.ProjectAccess; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.scheduling.ProjectTask; import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; +import de.tudarmstadt.ukp.inception.scheduling.Task; import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerController; @ConditionalOnWebApplication @@ -38,11 +42,14 @@ public class SchedulerControllerImpl { private final SchedulingService schedulingService; private final UserDao userService; + private final ProjectAccess projectAccess; - public SchedulerControllerImpl(SchedulingService aSchedulingService, UserDao aUserDao) + public SchedulerControllerImpl(SchedulingService aSchedulingService, UserDao aUserDao, + ProjectAccess aProjectAccess) { schedulingService = aSchedulingService; userService = aUserDao; + projectAccess = aProjectAccess; } @PostMapping(// @@ -51,10 +58,10 @@ public SchedulerControllerImpl(SchedulingService aSchedulingService, UserDao aUs produces = APPLICATION_JSON_VALUE) public void cancelTask(@PathVariable(PARAM_TASK_ID) int aTaskId) { - var user = userService.getCurrentUser(); + var sessionOwner = userService.getCurrentUser(); schedulingService.stopAllTasksMatching( - t -> t.getId() == aTaskId && t.getUser().filter(user::equals).isPresent()); + t -> t.getId() == aTaskId && canPerformActionOnTask(t, sessionOwner)); } @PostMapping(// @@ -63,9 +70,18 @@ public void cancelTask(@PathVariable(PARAM_TASK_ID) int aTaskId) produces = APPLICATION_JSON_VALUE) public void acknowledgeResult(@PathVariable(PARAM_TASK_ID) int aTaskId) { - var user = userService.getCurrentUser(); + var sessionOwner = userService.getCurrentUser(); schedulingService.stopAllTasksMatching( - t -> t.getId() == aTaskId && t.getUser().filter(user::equals).isPresent()); + t -> t.getId() == aTaskId && canPerformActionOnTask(t, sessionOwner)); + } + + private boolean canPerformActionOnTask(Task aTask, User aUser) + { + if (aTask instanceof ProjectTask) { + return projectAccess.canManageProject(String.valueOf(aTask.getProject().getId())); + } + + return aTask.getUser().filter(aUser::equals).isPresent(); } } diff --git a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerWebsocketControllerImpl.java b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerWebsocketControllerImpl.java index 4ad6654b1c2..619e093fadc 100644 --- a/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerWebsocketControllerImpl.java +++ b/inception/inception-ui-scheduling/src/main/java/de/tudarmstadt/ukp/inception/ui/scheduling/controller/SchedulerWebsocketControllerImpl.java @@ -17,22 +17,31 @@ */ package de.tudarmstadt.ukp.inception.ui.scheduling.controller; +import static de.tudarmstadt.ukp.inception.websocket.config.WebSocketConstants.PARAM_PROJECT; + +import java.io.IOException; import java.security.Principal; import java.util.List; import java.util.Objects; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SendToUser; import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import de.tudarmstadt.ukp.inception.scheduling.ProjectTask; import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; import de.tudarmstadt.ukp.inception.scheduling.controller.SchedulerWebsocketController; import de.tudarmstadt.ukp.inception.scheduling.controller.model.MTaskStateUpdate; +import jakarta.servlet.ServletContext; @Controller @RequestMapping(SchedulerWebsocketController.BASE_URL) @@ -41,15 +50,19 @@ public class SchedulerWebsocketControllerImpl implements SchedulerWebsocketController { private final SchedulingService schedulingService; + private final SimpMessagingTemplate msgTemplate; @Autowired - public SchedulerWebsocketControllerImpl(SchedulingService aSchedulingService) + public SchedulerWebsocketControllerImpl(SchedulingService aSchedulingService, + ServletContext aServletContext, SimpMessagingTemplate aMsgTemplate) { + msgTemplate = aMsgTemplate; schedulingService = aSchedulingService; } - @SubscribeMapping(SchedulerWebsocketController.BASE_TOPIC + "/tasks") - public List getCurrentTaskStates(Principal user) throws AccessDeniedException + @SubscribeMapping(SchedulerWebsocketController.USER_TASKS_TOPIC) + public List onSubscribeToUserTaskUpdates(Principal user) + throws AccessDeniedException { return schedulingService.getAllTasks().stream() // .filter(t -> t.getParentTask() == null) // @@ -61,6 +74,44 @@ public List getCurrentTaskStates(Principal user) throws Access .toList(); } + @SubscribeMapping(PROJECT_TASKS_TOPIC_TEMPLATE) + public List onSubscribeToProjectTaskUpdates( + SimpMessageHeaderAccessor aHeaderAccessor, Principal aPrincipal, // + @DestinationVariable(PARAM_PROJECT) long aProjectId) + throws IOException + { + return schedulingService.getAllTasks().stream() // + .filter(t -> t.getParentTask() == null) // + .filter(ProjectTask.class::isInstance) // + .map(t -> t.getMonitor()) // + .filter(Objects::nonNull) // + .filter(t -> t.getProject() != null) // + .filter(t -> Objects.equals(t.getProject().getId(), aProjectId)) // + .map(MTaskStateUpdate::new) // + .toList(); + } + + @Override + public void dispatch(MTaskStateUpdate aUpdate) + { + if (aUpdate.getUsername() != null) { + msgTemplate.convertAndSendToUser(aUpdate.getUsername(), + "/queue" + SchedulerWebsocketController.USER_TASKS_TOPIC, aUpdate); + } + + if (aUpdate.getProjectId() > 0) { + var topic = SchedulerWebsocketController + .getProjectTaskUpdatesTopic(aUpdate.getProjectId()); + msgTemplate.convertAndSend("/topic" + topic, aUpdate); + } + } + + @SendTo(PROJECT_TASKS_TOPIC_TEMPLATE) + public MTaskStateUpdate send(MTaskStateUpdate aUpdate) + { + return aUpdate; + } + @MessageExceptionHandler @SendToUser("/queue/errors") public String handleException(Throwable exception) diff --git a/inception/inception-ui-scheduling/src/main/ts/src/TaskMonitorPanel.svelte b/inception/inception-ui-scheduling/src/main/ts/src/TaskMonitorPanel.svelte index 480df9010f8..8671d88ddab 100644 --- a/inception/inception-ui-scheduling/src/main/ts/src/TaskMonitorPanel.svelte +++ b/inception/inception-ui-scheduling/src/main/ts/src/TaskMonitorPanel.svelte @@ -24,7 +24,8 @@ export let csrfToken: string export let endpointUrl: string // should this be full http://... url export let wsEndpointUrl: string // should this be full ws://... url - export let topicChannel: string + export let taskStatusTopic: string + export let taskUpdatesTopic: string export let tasks: MTaskStateUpdate[] = [] export let connected = false export let popupMode = true @@ -56,7 +57,7 @@ ); }); stompClient.subscribe( - "/app" + topicChannel + "/tasks", + taskStatusTopic, function (msg) { tasks = JSON.parse(msg.body) || [] if (!showFinishedTasks) { @@ -65,7 +66,7 @@ } ); stompClient.subscribe( - "/user/queue" + topicChannel + "/tasks", + taskUpdatesTopic, function (msg) { var msgBody = JSON.parse(msg.body) as MTaskStateUpdate; diff --git a/inception/inception-ui-search/pom.xml b/inception/inception-ui-search/pom.xml index 139e1b9370a..67310ea1ed2 100644 --- a/inception/inception-ui-search/pom.xml +++ b/inception/inception-ui-search/pom.xml @@ -87,6 +87,10 @@ commons-collections4 + + org.springframework + spring-core + org.springframework spring-context @@ -95,6 +99,10 @@ org.springframework.boot spring-boot-autoconfigure + + org.springframework.security + spring-security-core + org.apache.uima diff --git a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java index 2dd3c21de06..963308c7081 100644 --- a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java +++ b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebar.java @@ -31,16 +31,13 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; import org.apache.wicket.AttributeModifier; @@ -74,17 +71,16 @@ import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; import org.wicketstuff.event.annotation.OnEvent; import de.agilecoders.wicket.core.markup.html.bootstrap.navigation.BootstrapPagingNavigator.Size; import de.agilecoders.wicket.core.markup.html.bootstrap.navigation.ajax.BootstrapAjaxPagingNavigator; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; -import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -96,10 +92,10 @@ import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.options.DeleteAnnotationsOptions; import de.tudarmstadt.ukp.inception.app.ui.search.sidebar.options.SearchOptions; import de.tudarmstadt.ukp.inception.bootstrap.IconToggleBox; +import de.tudarmstadt.ukp.inception.documents.api.DocumentAccess; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; -import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState; import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderAnnotationsEvent; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.rendering.vmodel.VRange; @@ -111,7 +107,6 @@ import de.tudarmstadt.ukp.inception.search.SearchService; import de.tudarmstadt.ukp.inception.search.config.SearchProperties; import de.tudarmstadt.ukp.inception.search.event.SearchQueryEvent; -import de.tudarmstadt.ukp.inception.search.model.Progress; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxButton; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; @@ -149,9 +144,10 @@ public class SearchAnnotationSidebar private static final long serialVersionUID = -3358207848681467993L; - private static final Logger LOG = LoggerFactory.getLogger(SearchAnnotationSidebar.class); + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private @SpringBean DocumentService documentService; + private @SpringBean DocumentAccess documentAccess; private @SpringBean AnnotationSchemaService annotationService; private @SpringBean SearchService searchService; private @SpringBean UserDao userRepository; @@ -159,8 +155,6 @@ public class SearchAnnotationSidebar private @SpringBean SearchProperties searchProperties; private @SpringBean WorkloadManagementService workloadService; - private User currentUser; - private final WebMarkupContainer mainContainer; private final WebMarkupContainer resultsGroupContainer; private final WebMarkupContainer resultsTable; @@ -199,7 +193,6 @@ public SearchAnnotationSidebar(String aId, IModel aModel, { super(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); - currentUser = userRepository.getCurrentUser(); resultsProvider = new SearchResultsProviderWrapper( new SearchResultsProvider(searchService, groupedResults), searchOptions.bind("isLowLevelPaging")); @@ -245,7 +238,7 @@ public SearchAnnotationSidebar(String aId, IModel aModel, @Override protected void populateItem(Item item) { - ResultsGroup result = item.getModelObject(); + var result = item.getModelObject(); item.add(new Label(MID_GROUP_TITLE, LoadableDetachableModel.of(() -> groupSizeLabelValue(result)))); item.add(createGroupLevelSelectionCheckBox(MID_SELECT_ALL_IN_GROUP, @@ -320,8 +313,8 @@ private Label createNumberOfResults(String aId) var label = new Label(aId); label.setOutputMarkupId(true); label.setDefaultModel(LoadableDetachableModel.of(() -> { - long first = searchResultGroups.getFirstItemOffset(); - long total = searchResultGroups.getItemCount(); + var first = searchResultGroups.getFirstItemOffset(); + var total = searchResultGroups.getItemCount(); return format("%d-%d / %d", first + 1, min(first + searchResultGroups.getItemsPerPage(), total), total); })); @@ -351,18 +344,19 @@ protected void onAjaxEvent(AjaxRequestTarget aTarget) private Form createSearchForm(String aId) { - Form searchForm = new Form<>(aId); + var searchForm = new Form(aId); searchForm.add(new TextArea<>("queryInput", targetQuery)); - LambdaAjaxButton searchButton = new LambdaAjaxButton<>("search", - this::actionSearch); + + var searchButton = new LambdaAjaxButton<>("search", this::actionSearch); searchForm.add(searchButton); searchForm.setDefaultButton(searchButton); + return searchForm; } private Form createSearchOptionsForm(String aId) { - Form searchOptionsForm = new Form<>(aId, searchOptions); + var searchOptionsForm = new Form<>(aId, searchOptions); searchOptionsForm.add(createLayerDropDownChoice("groupingLayer", annotationService.listAnnotationLayer(getModelObject().getProject()))); @@ -384,8 +378,7 @@ protected void onConfigure() { super.onConfigure(); - setChangeAnnotationsElementsEnabled( - !getModelObject().isUserViewingOthersWork(userRepository.getCurrentUsername())); + setChangeAnnotationsElementsEnabled(getAnnotationPage().isEditable()); } @Override @@ -393,7 +386,6 @@ public void renderHead(IHeaderResponse aResponse) { super.renderHead(aResponse); - // CSS aResponse.render(CssHeaderItem.forReference(SearchAnnotationSidebarCssReference.get())); } @@ -409,7 +401,7 @@ private void setChangeAnnotationsElementsEnabled(boolean aEnabled) private String groupSizeLabelValue(ResultsGroup aResultsGroup) { - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.append(aResultsGroup.getGroupKey() + " (" + aResultsGroup.getResults().size()); if (!resultsProvider.applyLowLevelPaging()) { sb.append("/" + resultsProvider.groupSize(aResultsGroup.getGroupKey())); @@ -420,8 +412,7 @@ private String groupSizeLabelValue(ResultsGroup aResultsGroup) private DropDownChoice createResultsPerPageSelection(String aId) { - List choices = Arrays.stream(searchProperties.getPageSizes()).boxed() - .collect(Collectors.toList()); + var choices = Arrays.stream(searchProperties.getPageSizes()).boxed().toList(); var dropdown = new DropDownChoice<>(aId, choices); dropdown.add(new LambdaAjaxFormComponentUpdatingBehavior()); @@ -431,7 +422,7 @@ private DropDownChoice createResultsPerPageSelection(String aId) private DropDownChoice createLayerDropDownChoice(String aId, List aChoices) { - DropDownChoice layerChoice = new DropDownChoice<>(aId, aChoices, + var layerChoice = new DropDownChoice(aId, aChoices, new ChoiceRenderer<>("uiName")); layerChoice.add(new AjaxFormComponentUpdatingBehavior("change") @@ -454,7 +445,7 @@ protected void onUpdate(AjaxRequestTarget aTarget) private CheckBox createLowLevelPagingCheckBox() { - CheckBox checkbox = new CheckBox("lowLevelPaging"); + var checkbox = new CheckBox("lowLevelPaging"); checkbox.setOutputMarkupId(true); checkbox.add(enabledWhen(() -> searchOptions.getObject().getGroupingLayer() == null && searchOptions.getObject().getGroupingFeature() == null)); @@ -466,7 +457,7 @@ private CheckBox createLowLevelPagingCheckBox() private AjaxCheckBox createGroupLevelSelectionCheckBox(String aId, String aGroupKey) { - AjaxCheckBox selectAllCheckBox = new AjaxCheckBox(aId, Model.of(true)) + return new AjaxCheckBox(aId, Model.of(true)) { private static final long serialVersionUID = 2431702654443882657L; @@ -503,7 +494,6 @@ protected void onConfigure() .allMatch(SearchResult::isSelectedForAnnotation)); } }; - return selectAllCheckBox; } private void actionSearch(AjaxRequestTarget aTarget, Form aForm) @@ -524,8 +514,8 @@ private IResourceStream exportSearchResults() @Override public InputStream getInputStream() throws ResourceStreamNotFoundException { - SearchResultsExporter exporter = new SearchResultsExporter(); try { + var exporter = new SearchResultsExporter(); return exporter.generateCsv(resultsProvider.getAllResults()); } catch (Exception e) { @@ -558,12 +548,12 @@ private void executeSearchResultsGroupedQuery(AjaxRequestTarget aTarget) return; } - AnnotatorState state = getModelObject(); - Project project = state.getProject(); + var state = getModelObject(); + var project = state.getProject(); - Optional maybeProgress = searchService.getIndexProgress(project); + var maybeProgress = searchService.getIndexProgress(project); if (maybeProgress.isPresent()) { - Progress p = maybeProgress.get(); + var p = maybeProgress.get(); info("Indexing in progress... cannot perform query at this time. " + p.percent() + "% (" + p.getDone() + "/" + p.getTotal() + ")"); aTarget.addChildren(getPage(), IFeedback.class); @@ -579,7 +569,7 @@ private void executeSearchResultsGroupedQuery(AjaxRequestTarget aTarget) } try { - SourceDocument limitToDocument = state.getDocument(); + var limitToDocument = state.getDocument(); if (workloadService.getWorkloadManagerExtension(project).isDocumentRandomAccessAllowed( project) && !searchOptions.getObject().isLimitedToCurrentDocument()) { limitToDocument = null; @@ -587,7 +577,7 @@ private void executeSearchResultsGroupedQuery(AjaxRequestTarget aTarget) applicationEventPublisher.get().publishEvent(new SearchQueryEvent(this, project, state.getUser().getUsername(), targetQuery.getObject(), limitToDocument)); - SearchOptions opt = searchOptions.getObject(); + var opt = searchOptions.getObject(); resultsProvider.initializeQuery(getModelObject().getUser(), project, targetQuery.getObject(), limitToDocument, opt.getGroupingLayer(), opt.getGroupingFeature()); @@ -617,110 +607,118 @@ public void actionApplyToSelectedResults(AjaxRequestTarget aTarget, Operation aC aTarget.addChildren(getPage(), IFeedback.class); if (VID.NONE_ID.equals(getModelObject().getSelection().getAnnotation())) { error("No annotation selected. Please select an annotation first"); + getAnnotationPage().actionRefreshDocument(aTarget); + return; } - else { - AnnotationLayer layer = getModelObject().getSelectedAnnotationLayer(); - try { - SpanAdapter adapter = (SpanAdapter) annotationService.getAdapter(layer); - adapter.silenceEvents(); - - // Group the results by document such that we can process one CAS at a time - Map> resultsByDocument = groupedResults.getObject() - .allResultsGroups().stream() - // the grouping can be based on some other strategy than the document, so - // we re-group here - .flatMap(group -> group.getResults().stream()) - .collect(groupingBy(SearchResult::getDocumentId)); - - BulkOperationResult bulkResult = new BulkOperationResult(); - - AnnotatorState state = getModelObject(); - for (Entry> resultsGroup : resultsByDocument.entrySet()) { - long documentId = resultsGroup.getKey(); - SourceDocument sourceDoc = documentService - .getSourceDocument(state.getProject().getId(), documentId); - - AnnotationDocument annoDoc = documentService - .createOrGetAnnotationDocument(sourceDoc, currentUser); - - switch (annoDoc.getState()) { - case FINISHED: // fall-through - case IGNORE: - // Skip processing any documents which are finished or ignored - continue; - default: - // Do nothing - } - // Holder for lazily-loaded CAS - Optional cas = Optional.empty(); + var sessionOwner = userRepository.getCurrentUser(); + var dataOwner = getAnnotationPage().getModelObject().getUser(); + var layer = getModelObject().getSelectedAnnotationLayer(); + try { + var adapter = (SpanAdapter) annotationService.getAdapter(layer); + adapter.silenceEvents(); + + // Group the results by document such that we can process one CAS at a time + var resultsByDocument = groupedResults.getObject().allResultsGroups().stream() + // the grouping can be based on some other strategy than the document, so + // we re-group here + .flatMap(group -> group.getResults().stream()) + .collect(groupingBy(SearchResult::getDocumentId)); + + var bulkResult = new BulkOperationResult(); + + var state = getModelObject(); + for (var resultsGroup : resultsByDocument.entrySet()) { + var documentId = resultsGroup.getKey(); + var sourceDoc = documentService.getSourceDocument(state.getProject().getId(), + documentId); + + if (!canAccessDocument(sessionOwner, sourceDoc, dataOwner)) { + continue; + } - // Apply bulk operations to all hits from this document - for (SearchResult result : resultsGroup.getValue()) { - if (result.isReadOnly() || !result.isSelectedForAnnotation()) { - continue; - } + var annoDoc = documentService.createOrGetAnnotationDocument(sourceDoc, dataOwner); - if (!cas.isPresent()) { - // Lazily load annotated document - cas = Optional.of(documentService.readAnnotationCas(sourceDoc, - currentUser.getUsername(), AUTO_CAS_UPGRADE)); - } + // Holder for lazily-loaded CAS + Optional cas = Optional.empty(); - aConsumer.apply(sourceDoc, cas.get(), adapter, result, bulkResult); + // Apply bulk operations to all hits from this document + for (var result : resultsGroup.getValue()) { + if (result.isReadOnly() || !result.isSelectedForAnnotation()) { + continue; } - // Persist annotated document - if (cas.isPresent()) { - writeJCasAndUpdateTimeStamp(sourceDoc, cas.get()); + if (!cas.isPresent()) { + // Lazily load annotated document + cas = Optional + .of(documentService.readAnnotationCas(annoDoc, AUTO_CAS_UPGRADE)); } - } - if (bulkResult.created > 0) { - success("Created annotations: " + bulkResult.created); - } - if (bulkResult.updated > 0) { - success("Updated annotations: " + bulkResult.updated); - } - if (bulkResult.deleted > 0) { - success("Deleted annotations: " + bulkResult.deleted); - } - if (bulkResult.conflict > 0) { - warn("Annotations skipped due to conflicts: " + bulkResult.conflict); + aConsumer.apply(sourceDoc, cas.get(), adapter, result, bulkResult); } - if (bulkResult.created == 0 && bulkResult.updated == 0 && bulkResult.deleted == 0) { - info("No changes"); + // Persist annotated document + if (cas.isPresent()) { + writeJCasAndUpdateTimeStamp(sourceDoc, cas.get()); } + } - applicationEventPublisher.get().publishEvent(new BulkAnnotationEvent(this, - getModelObject().getProject(), currentUser.getUsername(), layer)); + if (bulkResult.created > 0) { + success("Created annotations: " + bulkResult.created); } - catch (ClassCastException e) { - error("Can only create SPAN annotations for search results."); - LOG.error("Can only create SPAN annotations for search results", e); + if (bulkResult.updated > 0) { + success("Updated annotations: " + bulkResult.updated); } - catch (Exception e) { - error("Unable to apply action to search results: " + e.getMessage()); - LOG.error("Unable to apply action to search results: ", e); + if (bulkResult.deleted > 0) { + success("Deleted annotations: " + bulkResult.deleted); } + if (bulkResult.conflict > 0) { + warn("Annotations skipped due to conflicts: " + bulkResult.conflict); + } + + if (bulkResult.created == 0 && bulkResult.updated == 0 && bulkResult.deleted == 0) { + info("No changes"); + } + + applicationEventPublisher.get().publishEvent(new BulkAnnotationEvent(this, + getModelObject().getProject(), dataOwner.getUsername(), layer)); + } + catch (ClassCastException e) { + error("Can only create SPAN annotations for search results."); + LOG.error("Can only create SPAN annotations for search results", e); + } + catch (Exception e) { + error("Unable to apply action to search results: " + e.getMessage()); + LOG.error("Unable to apply action to search results: ", e); } getAnnotationPage().actionRefreshDocument(aTarget); } + private boolean canAccessDocument(User sessionOwner, SourceDocument sourceDoc, User dataOwner) + { + try { + documentAccess.assertCanEditAnnotationDocument(sessionOwner, sourceDoc, + dataOwner.getUsername()); + return true; + } + catch (AccessDeniedException e) { + return false; + } + } + private void createAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, SpanAdapter aAdapter, SearchResult aSearchResult, BulkOperationResult aBulkResult) throws AnnotationException { - AnnotatorState state = getModelObject(); - AnnotationLayer layer = aAdapter.getLayer(); + var state = getModelObject(); + var layer = aAdapter.getLayer(); - Type type = CasUtil.getAnnotationType(aCas, aAdapter.getAnnotationTypeName()); - AnnotationFS annoFS = selectAt(aCas, type, aSearchResult.getOffsetStart(), + var type = CasUtil.getAnnotationType(aCas, aAdapter.getAnnotationTypeName()); + var annoFS = selectAt(aCas, type, aSearchResult.getOffsetStart(), aSearchResult.getOffsetEnd()).stream().findFirst().orElse(null); - boolean overrideExisting = createOptions.getObject().isOverrideExistingAnnotations(); + var overrideExisting = createOptions.getObject().isOverrideExistingAnnotations(); // if there is already an annotation of the same type at the target location // and we don't want to override it and stacking is not enabled, do nothing. @@ -728,11 +726,11 @@ private void createAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, return; } - boolean match = false; + var match = false; // create a new annotation if not already there or if stacking is enabled and the // new annotation has different features than the existing one - for (AnnotationFS eannoFS : selectAt(aCas, type, aSearchResult.getOffsetStart(), + for (var eannoFS : selectAt(aCas, type, aSearchResult.getOffsetStart(), aSearchResult.getOffsetEnd())) { if (overrideExisting) { setFeatureValues(aDocument, aCas, aAdapter, state, eannoFS); @@ -745,7 +743,7 @@ else if (featureValuesMatchCurrentState(eannoFS)) { if (annoFS == null || (!match && !overrideExisting)) { try { - annoFS = aAdapter.add(aDocument, currentUser.getUsername(), aCas, + annoFS = aAdapter.add(aDocument, state.getUser().getUsername(), aCas, aSearchResult.getOffsetStart(), aSearchResult.getOffsetEnd()); aBulkResult.created++; } @@ -763,18 +761,19 @@ private void setFeatureValues(SourceDocument aDocument, CAS aCas, SpanAdapter aA AnnotatorState state, AnnotationFS annoFS) throws AnnotationException { - int addr = ICasUtil.getAddr(annoFS); - List featureStates = state.getFeatureStates(); - for (FeatureState featureState : featureStates) { - Object featureValue = featureState.value; - AnnotationFeature feature = featureState.feature; + var addr = ICasUtil.getAddr(annoFS); + for (var featureState : state.getFeatureStates()) { + var featureValue = featureState.value; + var feature = featureState.feature; + // Ignore slot features - cf. https://github.com/inception-project/inception/issues/2505 if (feature.getLinkMode() != LinkMode.NONE) { continue; } + if (featureValue != null) { - aAdapter.setFeatureValue(aDocument, currentUser.getUsername(), aCas, addr, feature, - featureValue); + aAdapter.setFeatureValue(aDocument, state.getUser().getUsername(), aCas, addr, + feature, featureValue); } } } @@ -782,13 +781,14 @@ private void setFeatureValues(SourceDocument aDocument, CAS aCas, SpanAdapter aA private void deleteAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, SpanAdapter aAdapter, SearchResult aSearchResult, BulkOperationResult aBulkResult) { - Type type = CasUtil.getAnnotationType(aCas, aAdapter.getAnnotationTypeName()); + var dataOwner = getAnnotationPage().getModelObject().getUser(); + var type = CasUtil.getAnnotationType(aCas, aAdapter.getAnnotationTypeName()); - for (AnnotationFS annoFS : selectAt(aCas, type, aSearchResult.getOffsetStart(), + for (var annoFS : selectAt(aCas, type, aSearchResult.getOffsetStart(), aSearchResult.getOffsetEnd())) { if ((annoFS != null && featureValuesMatchCurrentState(annoFS)) || !deleteOptions.getObject().isDeleteOnlyMatchingFeatureValues()) { - aAdapter.delete(aDocument, currentUser.getUsername(), aCas, new VID(annoFS)); + aAdapter.delete(aDocument, dataOwner.getUsername(), aCas, VID.of(annoFS)); aBulkResult.deleted++; } } @@ -797,30 +797,32 @@ private void deleteAnnotationAtSearchResult(SourceDocument aDocument, CAS aCas, private void writeJCasAndUpdateTimeStamp(SourceDocument aSourceDoc, CAS aCas) throws IOException, AnnotationException { - AnnotatorState state = getModelObject(); + var state = getModelObject(); if (Objects.equals(state.getDocument().getId(), aSourceDoc.getId())) { // Updating the currently open document is done through the page in order to notify the // mechanism to detect concurrent modifications. getAnnotationPage().writeEditorCas(aCas); + return; } - else { - documentService.writeAnnotationCas(aCas, aSourceDoc, currentUser, true); - } + + documentService.writeAnnotationCas(aCas, aSourceDoc, state.getUser().getUsername(), true); } private boolean featureValuesMatchCurrentState(AnnotationFS aAnnotationFS) { - SpanAdapter aAdapter = (SpanAdapter) annotationService + var aAdapter = (SpanAdapter) annotationService .getAdapter(getModelObject().getSelectedAnnotationLayer()); - for (FeatureState state : getModelObject().getFeatureStates()) { - Object featureValue = state.value; - AnnotationFeature feature = state.feature; + for (var state : getModelObject().getFeatureStates()) { + var featureValue = state.value; + var feature = state.feature; + // Ignore slot features - cf. https://github.com/inception-project/inception/issues/2505 if (feature.getLinkMode() != LinkMode.NONE) { continue; } - Object valueAtFS = aAdapter.getFeatureValue(feature, aAnnotationFS); + + var valueAtFS = aAdapter.getFeatureValue(feature, aAnnotationFS); if (!Objects.equals(valueAtFS, featureValue)) { return false; } @@ -838,31 +840,29 @@ public SearchResultGroup(String aId, String aMarkupId, MarkupContainer aMarkupPr { super(aId, aMarkupId, aMarkupProvider, aModel); - ListView statementList = new ListView("results") + var statementList = new ListView("results") { private static final long serialVersionUID = 5811425707843441458L; @Override protected void populateItem(ListItem aItem) { - Project currentProject = SearchAnnotationSidebar.this.getModel().getObject() + var currentProject = SearchAnnotationSidebar.this.getModel().getObject() .getProject(); - SearchResult result = aItem.getModelObject(); - - LambdaAjaxLink lambdaAjaxLink = new LambdaAjaxLink("showSelectedDocument", - t -> { - selectedResult = aItem.getModelObject(); - actionShowSelectedDocument(t, - documentService.getSourceDocument(currentProject, - selectedResult.getDocumentTitle()), - selectedResult.getOffsetStart(), - selectedResult.getOffsetEnd()); - // Need to re-render because we want to highlight the match - getAnnotationPage().actionRefreshDocument(t); - }); + var result = aItem.getModelObject(); + + var lambdaAjaxLink = new LambdaAjaxLink("showSelectedDocument", t -> { + selectedResult = aItem.getModelObject(); + actionShowSelectedDocument(t, + documentService.getSourceDocument(currentProject, + selectedResult.getDocumentTitle()), + selectedResult.getOffsetStart(), selectedResult.getOffsetEnd()); + // Need to re-render because we want to highlight the match + getAnnotationPage().actionRefreshDocument(t); + }); aItem.add(lambdaAjaxLink); - AjaxCheckBox selected = new AjaxCheckBox("selected", + var selected = new AjaxCheckBox("selected", Model.of(result.isSelectedForAnnotation())) { private static final long serialVersionUID = -6955396602403459129L; @@ -870,7 +870,7 @@ protected void populateItem(ListItem aItem) @Override protected void onUpdate(AjaxRequestTarget target) { - SearchResult modelObject = aItem.getModelObject(); + var modelObject = aItem.getModelObject(); modelObject.setSelectedForAnnotation(getModelObject()); if (!getModelObject()) { // not all results in the document are selected, so set document diff --git a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java index 6076ef1cdd6..9ed206e2b0e 100644 --- a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java +++ b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/SearchAnnotationSidebarFactory.java @@ -19,6 +19,7 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; +import org.springframework.core.annotation.Order; import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; @@ -36,6 +37,7 @@ * {@link SearchServiceUIAutoConfiguration#searchAnnotationSidebarFactory}. *

*/ +@Order(3500) public class SearchAnnotationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/StatisticsAnnotationSidebarFactory.java b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/StatisticsAnnotationSidebarFactory.java index a0be99f7f42..fea108d8865 100644 --- a/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/StatisticsAnnotationSidebarFactory.java +++ b/inception/inception-ui-search/src/main/java/de/tudarmstadt/ukp/inception/app/ui/search/sidebar/StatisticsAnnotationSidebarFactory.java @@ -19,6 +19,7 @@ import org.apache.wicket.Component; import org.apache.wicket.model.IModel; +import org.springframework.core.annotation.Order; import de.agilecoders.wicket.core.markup.html.bootstrap.image.Icon; import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome5IconType; @@ -36,6 +37,7 @@ * {@link StatsServiceUIAutoConfiguration#statisticsAnnotationSidebarFactory}. *

*/ +@Order(11000) public class StatisticsAnnotationSidebarFactory extends AnnotationSidebarFactory_ImplBase { diff --git a/inception/inception-websocket/pom.xml b/inception/inception-websocket/pom.xml index 8928dff5a88..903b118ce60 100644 --- a/inception/inception-websocket/pom.xml +++ b/inception/inception-websocket/pom.xml @@ -25,43 +25,15 @@ inception-websocket INCEpTION - Websocket - - de.tudarmstadt.ukp.inception.app - inception-documents-api - - - de.tudarmstadt.ukp.inception.app - inception-project-api - de.tudarmstadt.ukp.inception.app inception-security - - de.tudarmstadt.ukp.inception.app - inception-support-bootstrap - de.tudarmstadt.ukp.inception.app inception-ui-core - - de.tudarmstadt.ukp.inception.app - inception-model - - - de.tudarmstadt.ukp.inception.app - inception-log - - - org.apache.wicket - wicket-core - - - org.apache.wicket - wicket-util - com.giffing.wicket.spring.boot.starter wicket-spring-boot-starter @@ -75,7 +47,7 @@ org.springframework - spring-core + spring-messaging org.springframework @@ -85,10 +57,6 @@ org.springframework spring-context - - org.springframework - spring-messaging - org.springframework spring-websocket @@ -118,130 +86,5 @@ org.slf4j slf4j-api - - - jakarta.persistence - jakarta.persistence-api - - - - org.apache.uima - uimaj-core - test - - - org.apache.tomcat.embed - tomcat-embed-websocket - test - - - org.springframework.security - spring-security-crypto - test - - - org.junit.jupiter - junit-jupiter-api - test - - - org.springframework.boot - spring-boot-starter-data-jpa - test - - - org.springframework.boot - spring-boot-test - test - - - org.springframework.boot - spring-boot-starter-web - test - - - org.springframework.boot - spring-boot-test-autoconfigure - test - - - org.springframework - spring-test - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.security - spring-security-web - test - - - org.hsqldb - hsqldb - test - - - de.tudarmstadt.ukp.inception.app - inception-diag - test - - - de.tudarmstadt.ukp.inception.app - inception-support - test - - - de.tudarmstadt.ukp.inception.app - inception-project - test - - - de.tudarmstadt.ukp.inception.app - inception-documents - test - - - de.tudarmstadt.ukp.inception.app - inception-api-annotation - test - - - de.tudarmstadt.ukp.inception.app - inception-schema - test - - - de.tudarmstadt.ukp.inception.app - inception-annotation-storage - test - - - de.tudarmstadt.ukp.inception.app - inception-export - test - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - org.springframework:spring-core - org.springframework.boot:spring-boot-starter-web - org.springframework.boot:spring-boot-test-autoconfigure - - - - - - diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketAutoConfiguration.java b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketAutoConfiguration.java index b2e91f0f2c9..d0c394b3b58 100644 --- a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketAutoConfiguration.java +++ b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketAutoConfiguration.java @@ -17,7 +17,6 @@ */ package de.tudarmstadt.ukp.inception.websocket.config; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -27,12 +26,9 @@ import com.giffing.wicket.spring.boot.starter.configuration.extensions.core.csrf.CsrfAttacksPreventionProperties; -import de.tudarmstadt.ukp.inception.log.config.EventLoggingAutoConfiguration; - @ConditionalOnWebApplication @Configuration @EnableWebSocketMessageBroker -@AutoConfigureAfter(EventLoggingAutoConfiguration.class) @ConditionalOnProperty(prefix = "websocket", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(CsrfAttacksPreventionProperties.class) public class WebsocketAutoConfiguration diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketConfig.java b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketConfig.java index 5df487a4c73..742bbad8619 100644 --- a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketConfig.java +++ b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketConfig.java @@ -64,9 +64,10 @@ public void configureMessageBroker(MessageBrokerRegistry aRegistry) // broker will send to destinations with this prefix, queue is custom for user-specific // channels. client will subscribe to /queue/{subtopic} where subtopic is a specific topic // that controller or service will address messages to - aRegistry.enableSimpleBroker("/queue", "/topic"); + aRegistry.enableSimpleBroker("/queue/", "/topic/"); // clients should send messages to channels pre-fixed with this - aRegistry.setApplicationDestinationPrefixes("/app"); + aRegistry.setApplicationDestinationPrefixes("/app/"); + aRegistry.setUserDestinationPrefix("/user/"); // messages to clients are by default not ordered, need to explicitly set order here aRegistry.setPreservePublishOrder(true); } diff --git a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketSecurityConfig.java b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketSecurityConfig.java index 740871bfd24..8edea57ea09 100644 --- a/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketSecurityConfig.java +++ b/inception/inception-websocket/src/main/java/de/tudarmstadt/ukp/inception/websocket/config/WebsocketSecurityConfig.java @@ -74,8 +74,11 @@ protected void configureInbound(MessageSecurityMetadataSourceRegistry aSecurityR .simpTypeMatchers(DISCONNECT).permitAll() // messages other than MESSAGE,SUBSCRIBE are allowed for authenticated users .nullDestMatcher().authenticated() // + .simpSubscribeDestMatchers("/*/errors*").hasRole("USER") .simpSubscribeDestMatchers("/*/loggedEvents").hasRole("ADMIN") - .simpSubscribeDestMatchers("/*/scheduler").hasRole("USER") + .simpSubscribeDestMatchers("/*/scheduler/user").hasRole("USER") + .simpSubscribeDestMatchers("/*/scheduler" + TOPIC_ELEMENT_PROJECT + "{" + PARAM_PROJECT + "}") + .access("@projectAccess.canManageProject(#" + PARAM_PROJECT + ")") .simpSubscribeDestMatchers("/*" + NS_PROJECT + "/{" + PARAM_PROJECT + "}/exports") .access("@projectAccess.canManageProject(#" + PARAM_PROJECT + ")") .simpSubscribeDestMatchers(annotationEditorTopic) diff --git a/inception/installEclipseSettings.sh b/inception/installEclipseSettings.sh index 181640b7b45..33324870e95 100755 --- a/inception/installEclipseSettings.sh +++ b/inception/installEclipseSettings.sh @@ -84,6 +84,7 @@ installPrefs inception-kb installPrefs inception-kb-fact-linking installPrefs inception-layer-docmetadata installPrefs inception-log +installPrefs inception-log-ui installPrefs inception-model installPrefs inception-model-export installPrefs inception-pdf-editor diff --git a/inception/pom.xml b/inception/pom.xml index 87191113492..5c39c24465b 100644 --- a/inception/pom.xml +++ b/inception/pom.xml @@ -188,6 +188,7 @@ inception-telemetry inception-ui-core inception-log + inception-log-ui inception-scheduling inception-js-api inception-annotation-storage @@ -215,6 +216,7 @@ inception-ui-tagsets inception-sharing inception-preferences + inception-processing inception-guidelines inception-schema