diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java index 428447e6402..4d981c547c1 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/annotation/page/AnnotationPageBase.java @@ -42,7 +42,6 @@ import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.RequestHandlerExecutor.ReplaceHandlerException; import org.apache.wicket.request.Url; -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; @@ -102,8 +101,8 @@ protected AnnotationPageBase(PageParameters aParameters) super(aParameters); var params = getPageParameters(); - StringValue documentParameter = params.get(PAGE_PARAM_DOCUMENT); - StringValue userParameter = params.get(PAGE_PARAM_DATA_OWNER); + var documentParameter = params.get(PAGE_PARAM_DOCUMENT); + var userParameter = params.get(PAGE_PARAM_DATA_OWNER); // If the page was accessed using an URL form ending in a document ID, let's move // the document ID into the fragment and redirect to the form without the document ID. @@ -111,22 +110,25 @@ protected AnnotationPageBase(PageParameters aParameters) // happily switch between documents using AJAX without having to worry about links with // a document ID potentially sending us back to a specific document. if (!documentParameter.isEmpty()) { - RequestCycle requestCycle = getRequestCycle(); - Url clientUrl = requestCycle.getRequest().getClientUrl(); - clientUrl.resolveRelative(Url.parse("./")); - List fragmentParams = new ArrayList<>(); + var requestCycle = getRequestCycle(); + + var fragmentParams = new ArrayList(); fragmentParams.add(format("%s=%s", PAGE_PARAM_DOCUMENT, documentParameter.toString())); params.remove(PAGE_PARAM_DOCUMENT); + if (!userParameter.isEmpty()) { - fragmentParams.add(format("%s=%s", PAGE_PARAM_DATA_OWNER, userParameter.toString())); + fragmentParams + .add(format("%s=%s", PAGE_PARAM_DATA_OWNER, userParameter.toString())); params.remove(PAGE_PARAM_DATA_OWNER); } - for (var namedParam : params.getAllNamed()) { - clientUrl.setQueryParameter(namedParam.getKey(), namedParam.getValue()); - } - clientUrl.setFragment("!" + fragmentParams.stream().collect(joining("&"))); - String url = requestCycle.getUrlRenderer().renderRelativeUrl(clientUrl); - throw new RedirectToUrlException(url.toString()); + + var url = Url.parse(requestCycle.urlFor(this.getClass(), params)); + url.setFragment("!" + fragmentParams.stream().collect(joining("&"))); + var finalUrl = requestCycle.getUrlRenderer().renderFullUrl(url); + LOG.trace( + "Pushing parameter for document [{}] and user [{}] into fragment: {} (URL redirect)", + documentParameter, userParameter, finalUrl); + throw new RedirectToUrlException(finalUrl.toString()); } } @@ -198,18 +200,18 @@ protected UrlParametersReceivingBehavior createUrlFragmentBehavior() protected void onParameterArrival(IRequestParameters aRequestParameters, AjaxRequestTarget aTarget) { - StringValue document = aRequestParameters.getParameterValue(PAGE_PARAM_DOCUMENT); - StringValue focus = aRequestParameters.getParameterValue(PAGE_PARAM_FOCUS); - StringValue user = aRequestParameters.getParameterValue(PAGE_PARAM_DATA_OWNER); + var document = aRequestParameters.getParameterValue(PAGE_PARAM_DOCUMENT); + var focus = aRequestParameters.getParameterValue(PAGE_PARAM_FOCUS); + var user = aRequestParameters.getParameterValue(PAGE_PARAM_DATA_OWNER); - // nothing changed, do not check for project, because inception always opens - // on a project if (document.isEmpty() && focus.isEmpty()) { return; } - SourceDocument previousDoc = getModelObject().getDocument(); - User aPreviousUser = getModelObject().getUser(); + LOG.trace("URL fragment update: {}@{} focus {}", user, document, focus); + + var previousDoc = getModelObject().getDocument(); + var aPreviousUser = getModelObject().getUser(); handleParameters(document, focus, user); updateDocumentView(aTarget, previousDoc, aPreviousUser, focus); @@ -220,6 +222,20 @@ protected void onParameterArrival(IRequestParameters aRequestParameters, protected abstract void handleParameters(StringValue aDocumentParameter, StringValue aFocusParameter, StringValue aUser); + /** + * Switch between documents. Note that the document and data owner to switch to are obtained + * from the {@link AnnotatorState}. The parameters indicate the the old document and data owner + * before the switch! + * + * @param aTarget + * a request target. + * @param aPreviousDocument + * the document before the switch. + * @param aPreviousUser + * the data owner before the switch. + * @param aFocusParameter + * the focus before the switch. + */ protected abstract void updateDocumentView(AjaxRequestTarget aTarget, SourceDocument aPreviousDocument, User aPreviousUser, StringValue aFocusParameter); @@ -512,14 +528,14 @@ private class UrlFragmentUpdateListener @Override public void onTargetRespond(AjaxRequestTarget aTarget) { - AnnotatorState state = getModelObject(); + var state = getModelObject(); if (state.getDocument() == null) { return; } - Long currentDocumentId = state.getDocument().getId(); - int currentFocusUnitIndex = state.getFocusUnitIndex(); + var currentDocumentId = state.getDocument().getId(); + var currentFocusUnitIndex = state.getFocusUnitIndex(); // Check if the relevant parameters have actually changed since the URL parameters were // last set - if this is not the case, then let's not set the parameters because that @@ -533,7 +549,7 @@ public void onTargetRespond(AjaxRequestTarget aTarget) urlFragmentLastDocumentId = currentDocumentId; urlFragmentLastFocusUnitIndex = currentFocusUnitIndex; - UrlFragment fragment = new UrlFragment(aTarget); + var fragment = new UrlFragment(aTarget); fragment.putParameter(PAGE_PARAM_DOCUMENT, currentDocumentId); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/PreparingToOpenDocumentEvent.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/PreparingToOpenDocumentEvent.java new file mode 100644 index 00000000000..f20643cf39f --- /dev/null +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/events/PreparingToOpenDocumentEvent.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.annotation.events; + +import org.springframework.context.ApplicationEvent; + +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.clarin.webanno.support.wicket.event.HybridApplicationUIEvent; +import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; + +/** + * Fire this event to give listeners a chance to look at or even modify the {@link AnnotatorState} + * before actually loading the data. + */ +public class PreparingToOpenDocumentEvent + extends ApplicationEvent + implements HybridApplicationUIEvent +{ + private static final long serialVersionUID = -5971290341142438144L; + + private final SourceDocument document; + // user who owns/annotates the opened document + private final String documentOwner; + // user who opened the document + private final String sessionOwner; + + public PreparingToOpenDocumentEvent(AnnotationPageBase aSource, SourceDocument aDocument, + String aDocumentOwner, String aSessionOwner) + { + super(aSource); + document = aDocument; + documentOwner = aDocumentOwner; + sessionOwner = aSessionOwner; + } + + public SourceDocument getDocument() + { + return document; + } + + public String getSessionOwner() + { + return sessionOwner; + } + + public String getDocumentOwner() + { + return documentOwner; + } + + @Override + public AnnotationPageBase getSource() + { + return (AnnotationPageBase) super.getSource(); + } +} diff --git a/inception/inception-documents/src/main/java/de/tudarmstadt/ukp/inception/documents/DocumentAccessImpl.java b/inception/inception-documents/src/main/java/de/tudarmstadt/ukp/inception/documents/DocumentAccessImpl.java index c6f5bb1223b..8cf910af2a5 100644 --- a/inception/inception-documents/src/main/java/de/tudarmstadt/ukp/inception/documents/DocumentAccessImpl.java +++ b/inception/inception-documents/src/main/java/de/tudarmstadt/ukp/inception/documents/DocumentAccessImpl.java @@ -22,8 +22,6 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel.MANAGER; import static org.apache.commons.collections4.CollectionUtils.containsAny; -import java.util.List; - import javax.persistence.NoResultException; import org.apache.commons.lang3.StringUtils; @@ -35,7 +33,6 @@ import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; -import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; @@ -82,38 +79,51 @@ public boolean canViewAnnotationDocument(String aSessionOwner, String aProjectId aSessionOwner, aProjectId, aDocumentId, aAnnotator); try { - User user = getUser(aSessionOwner); - Project project = getProject(aProjectId); + var user = getUser(aSessionOwner); + var project = getProject(aProjectId); - List permissionLevels = projectService.listRoles(project, user); + var permissionLevels = projectService.listRoles(project, user); // Does the user have the permission to access the project at all? if (permissionLevels.isEmpty()) { + log.trace("Access denied: User {} has no acccess to project {}", user, project); return false; } // Managers and curators can see anything if (containsAny(permissionLevels, MANAGER, CURATOR)) { + log.trace("Access granted: User {} can view annotations [{}] as MANGER or CURATOR", + user, aDocumentId); return true; } // Annotators can only see their own documents if (!aSessionOwner.equals(aAnnotator)) { + log.trace( + "Access denied: User {} tries to see annotations from [{}] but can only see own annotations", + user, aAnnotator); return false; } // Annotators cannot view blocked documents - SourceDocument doc = documentService.getSourceDocument(project.getId(), aDocumentId); + var doc = documentService.getSourceDocument(project.getId(), aDocumentId); if (documentService.existsAnnotationDocument(doc, aAnnotator)) { - AnnotationDocument aDoc = documentService.getAnnotationDocument(doc, aAnnotator); + var aDoc = documentService.getAnnotationDocument(doc, aAnnotator); if (aDoc.getState() == AnnotationDocumentState.IGNORE) { + log.trace("Access denied: Document {} is locked (IGNORE) for user {}", aDoc, + aAnnotator); return false; } } + log.trace( + "Access granted: canViewAnnotationDocument [aSessionOwner: {}] [project: {}] " + + "[document: {}] [annotator: {}]", + aSessionOwner, aProjectId, aDocumentId, aAnnotator); return true; } catch (NoResultException | AccessDeniedException e) { + log.trace("Access denied: prerequisites not met", e); // If any object does not exist, the user cannot view return false; } diff --git a/inception/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/config/EventLoggingPropertiesImpl.java b/inception/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/config/EventLoggingPropertiesImpl.java index b508f99ae6f..9a60621a847 100644 --- a/inception/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/config/EventLoggingPropertiesImpl.java +++ b/inception/inception-log/src/main/java/de/tudarmstadt/ukp/inception/log/config/EventLoggingPropertiesImpl.java @@ -24,6 +24,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.event.AfterCasWrittenEvent; import de.tudarmstadt.ukp.inception.annotation.events.BeforeDocumentOpenedEvent; +import de.tudarmstadt.ukp.inception.annotation.events.PreparingToOpenDocumentEvent; @ConfigurationProperties("event-logging") public class EventLoggingPropertiesImpl @@ -37,6 +38,7 @@ public class EventLoggingPropertiesImpl AvailabilityChangeEvent.class.getSimpleName(), // "RecommenderTaskNotificationEvent", // BeforeDocumentOpenedEvent.class.getSimpleName(), // + PreparingToOpenDocumentEvent.class.getSimpleName(), // "BrokerAvailabilityEvent", // "ShutdownDialogAvailableEvent"); diff --git a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectAccessImpl.java b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectAccessImpl.java index 510f7e77e4b..68be2a91a16 100644 --- a/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectAccessImpl.java +++ b/inception/inception-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/project/ProjectAccessImpl.java @@ -62,12 +62,25 @@ public boolean canAccessProject(String aUser, String aProjectId) log.trace("Permission check: canAccessProject [user: {}] [project: {}]", aUser, aProjectId); try { - User user = getUser(aUser); - Project project = getProject(aProjectId); + var user = getUser(aUser); + var project = getProject(aProjectId); - return userService.isAdministrator(user) || projectService.hasAnyRole(user, project); + if (userService.isAdministrator(user)) { + log.trace("Access granted: User {} can access project {} as administrator", user, + project); + return true; + } + + if (projectService.hasAnyRole(user, project)) { + log.trace("Access granted: User {} can access project {} as project member", user, + project); + return true; + } + + return false; } catch (NoResultException | AccessDeniedException e) { + log.trace("Access denied: prerequisites not met", e); // If any object does not exist, the user cannot view return false; } @@ -84,13 +97,25 @@ public boolean canManageProject(String aUser, String aProjectId) log.trace("Permission check: canManageProject [user: {}] [project: {}]", aUser, aProjectId); try { - User user = getUser(aUser); - Project project = getProject(aProjectId); + var user = getUser(aUser); + var project = getProject(aProjectId); - return userService.isAdministrator(user) - || projectService.hasRole(user, project, PermissionLevel.MANAGER); + if (userService.isAdministrator(user)) { + log.trace("Access granted: User {} can manage project {} as administrator", user, + project); + return true; + } + + if (projectService.hasRole(user, project, PermissionLevel.MANAGER)) { + log.trace("Access granted: User {} can manage project {} as manager", user, + project); + return true; + } + + return false; } catch (NoResultException | AccessDeniedException e) { + log.trace("Access denied: prerequisites not met", e); // If any object does not exist, the user cannot view return false; } 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 7646790ec12..9aae269f1e2 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 @@ -85,6 +85,7 @@ import de.tudarmstadt.ukp.inception.annotation.events.BeforeDocumentOpenedEvent; import de.tudarmstadt.ukp.inception.annotation.events.DocumentOpenedEvent; import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent; +import de.tudarmstadt.ukp.inception.annotation.events.PreparingToOpenDocumentEvent; import de.tudarmstadt.ukp.inception.documents.DocumentAccess; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorBase; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtensionRegistry; @@ -123,7 +124,7 @@ public class AnnotationPage private @SpringBean PreferencesService preferencesService; private @SpringBean DocumentAccess documentAccess; - private long currentprojectId; + private long currentProjectId; private WebMarkupContainer centerArea; private WebMarkupContainer actionBar; @@ -135,15 +136,22 @@ public AnnotationPage(final PageParameters aPageParameters) { super(aPageParameters); - LOG.debug("Setting up annotation page with parameters: {}", aPageParameters); - - AnnotatorState state = new AnnotatorStateImpl(Mode.ANNOTATION); + var state = new AnnotatorStateImpl(Mode.ANNOTATION); state.setUser(userRepository.getCurrentUser()); setModel(Model.of(state)); - StringValue document = aPageParameters.get(PAGE_PARAM_DOCUMENT); - StringValue focus = aPageParameters.get(PAGE_PARAM_FOCUS); - StringValue user = aPageParameters.get(PAGE_PARAM_DATA_OWNER); + // AnnotationPageBase will push the document and user parameters into the URL fragment so + // we can afterwards navigate between documents freely. When AnnotationPageBase + // does that, it restarts the request. So basically when we get here, PAGE_PARAM_DOCUMENT + // will always be `null`... PAGE_PARAM_DATA_OWNER may be non-null if it is set without a + // document being specified - but in that case it is pretty useless + // + // The actual loading of the documents will be handled by onParameterArrival in the + // UrlFragmentBehavior which will call handleParameters again, this time with the right + // information. + var document = aPageParameters.get(PAGE_PARAM_DOCUMENT); + var focus = aPageParameters.get(PAGE_PARAM_FOCUS); + var user = aPageParameters.get(PAGE_PARAM_DATA_OWNER); handleParameters(document, focus, user); @@ -420,32 +428,44 @@ public void actionLoadDocument(AjaxRequestTarget aTarget) protected void actionLoadDocument(AjaxRequestTarget aTarget, int aFocus) { - LOG.trace("BEGIN LOAD_DOCUMENT_ACTION at focus " + aFocus); - try { - AnnotatorState state = getModelObject(); + var sessionOwner = userRepository.getCurrentUser().getUsername(); + + var state = getModelObject(); if (state.getUser() == null) { state.setUser(userRepository.getCurrentUser()); } + + LOG.trace("Preparing to open document {}@{} {}", state.getUser(), state.getDocument(), + aFocus); state.reset(); + applicationEventPublisherHolder.get().publishEvent( + new PreparingToOpenDocumentEvent(this, getModelObject().getDocument(), + getModelObject().getUser().getUsername(), sessionOwner)); + + // INFO BOUNDARY --------------------------------------------------------------- + // PreparingToOpenDocumentEvent has the option to change the annotator state. + // No information from the annotator state read before this point may be + // used afterwards. Information has to be re-read from the annotator state to get + // the latest values. // Check if there is an annotation document entry in the database. If there is none, // create one. - AnnotationDocument annotationDocument = documentService + LOG.trace("Opening document {}@{} {}", state.getUser(), state.getDocument(), aFocus); + var annotationDocument = documentService .createOrGetAnnotationDocument(state.getDocument(), state.getUser()); var stateBeforeOpening = annotationDocument.getState(); // Read the CAS // Update the annotation document CAS - CAS editorCas = documentService.readAnnotationCas(annotationDocument, + var editorCas = documentService.readAnnotationCas(annotationDocument, FORCE_CAS_UPGRADE); - boolean editable = isEditable(); + var editable = isEditable(); applicationEventPublisherHolder.get() .publishEvent(new BeforeDocumentOpenedEvent(this, editorCas, getModelObject().getDocument(), - getModelObject().getUser().getUsername(), - userRepository.getCurrentUser().getUsername(), editable)); + getModelObject().getUser().getUsername(), sessionOwner, editable)); if (editable) { // After creating an new CAS or upgrading the CAS, we need to save it. If the @@ -467,9 +487,9 @@ protected void actionLoadDocument(AjaxRequestTarget aTarget, int aFocus) loadPreferences(); // if project is changed, reset some project specific settings - if (currentprojectId != state.getProject().getId()) { + if (currentProjectId != state.getProject().getId()) { state.clearRememberedFeatures(); - currentprojectId = state.getProject().getId(); + currentProjectId = state.getProject().getId(); } // Set the actual editor component. This has to happen *before* any AJAX refreshes are @@ -518,16 +538,16 @@ protected void actionLoadDocument(AjaxRequestTarget aTarget, int aFocus) WicketUtil.refreshPage(aTarget, getPage()); } - applicationEventPublisherHolder.get().publishEvent( - new DocumentOpenedEvent(this, editorCas, getModelObject().getDocument(), - stateBeforeOpening, getModelObject().getUser().getUsername(), - userRepository.getCurrentUser().getUsername())); + LOG.trace("Document opened {}@{} {}", state.getUser(), state.getDocument(), aFocus); + + applicationEventPublisherHolder.get() + .publishEvent(new DocumentOpenedEvent(this, editorCas, + getModelObject().getDocument(), stateBeforeOpening, + getModelObject().getUser().getUsername(), sessionOwner)); } catch (Exception e) { handleException(aTarget, e); } - - LOG.trace("END LOAD_DOCUMENT_ACTION"); } @Override @@ -555,21 +575,24 @@ public void actionRefreshDocument(AjaxRequestTarget aTarget) protected void handleParameters(StringValue aDocumentParameter, StringValue aFocusParameter, StringValue aUserParameter) { - User user = userRepository.getCurrentUser(); - requireAnyProjectRole(user); + var sessionOwner = userRepository.getCurrentUser(); + requireAnyProjectRole(sessionOwner); - AnnotatorState state = getModelObject(); - Project project = getProject(); - SourceDocument doc = getDocumentFromParameters(project, aDocumentParameter); + var state = getModelObject(); + var project = getProject(); + var doc = getDocumentFromParameters(project, aDocumentParameter); // If there is no change in the current document, then there is nothing to do. Mind // that document IDs are globally unique and a change in project does not happen unless // there is also a document change. + String dataOwner = state.getUser().getUsername(); if (doc != null && // doc.equals(state.getDocument()) && // aFocusParameter.toInt(0) == state.getFocusUnitIndex() && // - state.getUser().getUsername().equals(aUserParameter.toString()) // + dataOwner.equals(aUserParameter.toString()) // ) { + LOG.trace("Page parameters match page state ({}@{} {}) - nothing to do", dataOwner, + state.getDocument(), state.getFocusUnitIndex()); return; } @@ -584,46 +607,47 @@ protected void handleParameters(StringValue aDocumentParameter, StringValue aFoc // state.setUser(new User(CURATION_USER)); // } // else { - User requestedUser = userRepository.get(aUserParameter.toString()); + var requestedUser = userRepository.get(aUserParameter.toString()); if (requestedUser == null) { failWithDocumentNotFound("User not found [" + aUserParameter + "]"); } else { + LOG.trace("Changing data owner: {}", requestedUser); state.setUser(requestedUser); } // } } - if (doc != null && !documentAccess.canViewAnnotationDocument(user.getUsername(), + if (doc != null && !documentAccess.canViewAnnotationDocument(sessionOwner.getUsername(), String.valueOf(project.getId()), doc.getId(), state.getUser().getUsername())) { failWithDocumentNotFound("Access to document [" + aDocumentParameter + "] in project [" - + project.getName() + "] as denied"); + + project.getName() + "] is denied"); } // If we arrive here and the document is not null, then we have a change of document // or a change of focus (or both) if (doc != null && !doc.equals(state.getDocument())) { + LOG.trace("Changing document: {} (prev: {})", doc, state.getDocument()); state.setDocument(doc, getListOfDocs()); } } @Override protected void updateDocumentView(AjaxRequestTarget aTarget, SourceDocument aPreviousDocument, - User aPreviousUser, StringValue aFocusParameter) + User aPreviousDataOwner, StringValue aFocusParameter) { - // url is from external link, not just paging through documents, + // URL is from external link, not just paging through documents, // tabs may have changed depending on user rights if (aTarget != null && aPreviousDocument == null) { + LOG.trace( + "Refreshing left sidebar as this is the first document loaded on this page instance"); leftSidebar.refreshTabs(aTarget); } - SourceDocument currentDocument = getModelObject().getDocument(); - if (currentDocument == null) { - return; - } - - User currentUser = getModelObject().getUser(); - if (currentUser == null) { + var currentDocument = getModelObject().getDocument(); + var dataOwner = getModelObject().getUser(); + if (currentDocument == null || dataOwner == null) { + LOG.trace("No document open"); return; } @@ -640,15 +664,20 @@ protected void updateDocumentView(AjaxRequestTarget aTarget, SourceDocument aPre // that document IDs are globally unique and a change in project does not happen unless // there is also a document change. if (aPreviousDocument != null && aPreviousDocument.equals(currentDocument) && // - aPreviousUser != null && aPreviousUser.equals(currentUser) && // + aPreviousDataOwner != null && aPreviousDataOwner.equals(dataOwner) && // focus == getModelObject().getFocusUnitIndex() // ) { + LOG.trace("Document and data owner have not changed: {}@{}", dataOwner, + currentDocument); return; } // never had set a document or is a new one if (aPreviousDocument == null || !aPreviousDocument.equals(currentDocument) - || aPreviousUser == null || !aPreviousUser.equals(currentUser)) { + || aPreviousDataOwner == null || !aPreviousDataOwner.equals(dataOwner)) { + LOG.trace( + "Document or data owner have changed (old: {}@{}, new: {}@{}) - loading document", + aPreviousDataOwner, aPreviousDocument, dataOwner, currentDocument); actionLoadDocument(aTarget, focus); return; } diff --git a/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/page/ApplicationPageBase.java b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/page/ApplicationPageBase.java index d6bd0cf2e4d..309e6e952e1 100644 --- a/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/page/ApplicationPageBase.java +++ b/inception/inception-ui-core/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/core/page/ApplicationPageBase.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.clarin.webanno.ui.core.page; +import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; @@ -60,7 +61,7 @@ public abstract class ApplicationPageBase extends WebPage { - private final static Logger LOG = LoggerFactory.getLogger(ApplicationPageBase.class); + private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final long serialVersionUID = -1690130604031181803L; @@ -85,9 +86,13 @@ protected ApplicationPageBase() commonInit(); } - protected ApplicationPageBase(final PageParameters parameters) + protected ApplicationPageBase(final PageParameters aPageParameters) { - super(parameters); + super(aPageParameters); + + LOG.debug("Setting up page [{}] with parameters: {}", this.getClass().getName(), + aPageParameters); + commonInit(); } diff --git a/inception/inception-ui-curation/pom.xml b/inception/inception-ui-curation/pom.xml index 4bc8e45f25c..b1a30a05df3 100644 --- a/inception/inception-ui-curation/pom.xml +++ b/inception/inception-ui-curation/pom.xml @@ -142,6 +142,10 @@ org.springframework.security spring-security-core + + com.giffing.wicket.spring.boot.starter + wicket-spring-boot-context + javax.servlet diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/page/CurationPage.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/page/CurationPage.java index 30ed88f924b..7eeb137f30d 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/page/CurationPage.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/curation/page/CurationPage.java @@ -660,7 +660,7 @@ protected void handleParameters(StringValue aDocumentParameter, StringValue aFoc protected void updateDocumentView(AjaxRequestTarget aTarget, SourceDocument aPreviousDocument, User aPreviousUser, StringValue aFocusParameter) { - SourceDocument currentDocument = getModelObject().getDocument(); + var currentDocument = getModelObject().getDocument(); if (currentDocument == null) { return; } diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarApplicationInitializer.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarApplicationInitializer.java new file mode 100644 index 00000000000..053395cea0a --- /dev/null +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarApplicationInitializer.java @@ -0,0 +1,57 @@ +/* + * 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.ui.curation.sidebar; + +import static java.lang.invoke.MethodHandles.lookup; +import static org.slf4j.LoggerFactory.getLogger; + +import org.apache.wicket.Component; +import org.apache.wicket.protocol.http.WebApplication; +import org.slf4j.Logger; + +import com.giffing.wicket.spring.boot.context.extensions.WicketApplicationInitConfiguration; + +import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; + +public class CurationSidebarApplicationInitializer + implements WicketApplicationInitConfiguration +{ + private static final Logger LOG = getLogger(lookup().lookupClass()); + + @Override + public void init(WebApplication aWebApplication) + { + aWebApplication.getComponentInitializationListeners() + .add(this::addCurationSidebarBehaviorToAnnotationPage); + } + + private void addCurationSidebarBehaviorToAnnotationPage(Component aComponent) + { + if (!(aComponent instanceof AnnotationPage)) { + return; + } + + var annotationPage = (AnnotationPage) aComponent; + if (!annotationPage.getBehaviors(CurationSidebarBehavior.class).isEmpty()) { + LOG.trace("CurationSidebarBehavior is already installed"); + } + + LOG.trace("Installing CurationSidebarBehavior"); + annotationPage.add(new CurationSidebarBehavior()); + } +} diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarBehavior.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarBehavior.java index ff17617309a..cbaabb86e12 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarBehavior.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/CurationSidebarBehavior.java @@ -19,29 +19,31 @@ import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.CURATION_USER; import static de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase.setProjectPageParameter; +import static java.lang.invoke.MethodHandles.lookup; import static java.util.Arrays.asList; - -import java.lang.invoke.MethodHandles; +import static org.slf4j.LoggerFactory.getLogger; import org.apache.wicket.Component; import org.apache.wicket.RestartResponseException; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.event.IEvent; -import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import de.tudarmstadt.ukp.clarin.webanno.api.annotation.page.AnnotationPageBase; +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.ui.annotation.AnnotationPage; -import de.tudarmstadt.ukp.inception.annotation.events.BeforeDocumentOpenedEvent; +import de.tudarmstadt.ukp.inception.annotation.events.PreparingToOpenDocumentEvent; public class CurationSidebarBehavior extends Behavior { private static final long serialVersionUID = -6224298395673360592L; - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Logger LOG = getLogger(lookup().lookupClass()); private static final String STAY = "stay"; private static final String OFF = "off"; @@ -54,120 +56,131 @@ public class CurationSidebarBehavior private @SpringBean UserDao userService; @Override - public void onConfigure(Component aComponent) + public void onEvent(Component aComponent, IEvent aEvent) { - super.onConfigure(aComponent); - - var page = aComponent.getPage(); - if (!(page instanceof AnnotationPage)) { + if (!(aEvent.getPayload() instanceof PreparingToOpenDocumentEvent)) { + if (aEvent.getPayload() != null) { + LOG.trace("Event not relevant to curation sidebar: {} / {}", aEvent.getClass(), + aEvent.getPayload().getClass()); + } + else { + LOG.trace("Event not relevant to curation sidebar: {}", aEvent.getClass()); + } return; } - var annotationPage = (AnnotationPage) page; + var event = (PreparingToOpenDocumentEvent) aEvent.getPayload(); - if (annotationPage.getModelObject().getDocument() == null) { + var page = event.getSource(); + + if (!(page instanceof AnnotationPage)) { + // Only applies to the AnnotationPage - not to the CurationPage! + LOG.trace( + "Curation sidebar is not deployed on AnnotationPage but rather [{}] - ignoring event [{}]", + page.getClass(), event.getClass()); return; } - handleCurationSessionPageParameters(annotationPage); + var params = page.getPageParameters(); - handleWrongAnnotatorUserInState(annotationPage); - } + var sessionOwner = userService.getCurrentUsername(); + var doc = event.getDocument(); + var project = doc.getProject(); + var dataOwner = event.getDocumentOwner(); - @Override - public void onEvent(Component aComponent, IEvent aEvent) - { - if (aEvent.getPayload() instanceof BeforeDocumentOpenedEvent) { - var event = (BeforeDocumentOpenedEvent) aEvent.getPayload(); - var page = event.getRequestTarget().getPage(); + LOG.trace("Curation sidebar reacting to [{}]@{} being opened by [{}]", dataOwner, doc, + sessionOwner); - if (!(page instanceof AnnotationPage)) { - return; - } + handleSessionActivation(page, params, doc, sessionOwner); - var annotationPage = (AnnotationPage) page; + ensureDataOwnerMatchesCurationTarget(page, project, sessionOwner, dataOwner); + } - handleCurationSessionPageParameters(annotationPage); + private void ensureDataOwnerMatchesCurationTarget(AnnotationPageBase aPage, Project aProject, + String aSessionOwner, String aDataOwner) + { + if (!isSessionActive(aProject)) { + LOG.trace( + "No curation session active - no need to adjust data owner to curation target"); + return; + } - handleWrongAnnotatorUserInState(annotationPage); + if (!isViewingPotentialCurationTarget(aDataOwner)) { + return; } - } - private void handleWrongAnnotatorUserInState(AnnotationPage aPage) - { - if (isViewingPotentialCurationTarget(aPage) && isSessionActive(aPage)) { - var sessionOwner = userService.getCurrentUsername(); - var state = aPage.getModelObject(); - - // If curation is possible and the curation target user is different from the user set - // in the annotation state, then we need to update the state and reload. - var curationTarget = curationSidebarService.getCurationTargetUser(sessionOwner, - state.getProject().getId()); - if (!state.getUser().equals(curationTarget)) { - LOG.trace("Wrong user in state, setting and reloading"); - state.setUser(curationTarget); - aPage.actionLoadDocument(null); - RequestCycle.get().setResponsePage(aPage); - } + // If the curation target user is different from the data owner set in the annotation + // state, then we need to update the state and reload. + var curationTarget = curationSidebarService.getCurationTargetUser(aSessionOwner, + aProject.getId()); + + if (!aDataOwner.equals(curationTarget.getUsername())) { + LOG.trace("Data owner [{}] should match curation target {} - changing to {}", + curationTarget, aDataOwner, curationTarget); + aPage.getModelObject().setUser(curationTarget); + } + else { + LOG.trace("Data owner [{}] alredy matches curation target {}", curationTarget, + aDataOwner); } } - private void handleCurationSessionPageParameters(AnnotationPage aPage) + private void handleSessionActivation(AnnotationPageBase aPage, PageParameters aParams, + SourceDocument aDoc, String aSessionOwner) { - var params = aPage.getPageParameters(); - - var curationSessionParameterValue = params.get(PARAM_CURATION_SESSION); - var curationTargetOwnParameterValue = params.get(PARAM_CURATION_TARGET_OWN); - var project = aPage.getModelObject().getProject(); - var sessionOwner = userService.getCurrentUsername(); + var project = aDoc.getProject(); + var curationSessionParameterValue = aParams.get(PARAM_CURATION_SESSION); + if (curationSessionParameterValue.isEmpty()) { + return; + } switch (curationSessionParameterValue.toString(STAY)) { case ON: LOG.trace("Checking if to start curation session"); // Start a new session or switch to new curation target - if (!isSessionActive(aPage) || !curationTargetOwnParameterValue.isEmpty()) { - curationSidebarService.startSession(sessionOwner, project, + var curationTargetOwnParameterValue = aParams.get(PARAM_CURATION_TARGET_OWN); + if (!isSessionActive(project) || !curationTargetOwnParameterValue.isEmpty()) { + curationSidebarService.startSession(aSessionOwner, project, curationTargetOwnParameterValue.toBoolean(false)); } break; case OFF: LOG.trace("Checking if to stop curation session"); - if (isSessionActive(aPage)) { - curationSidebarService.closeSession(sessionOwner, project.getId()); + if (isSessionActive(project)) { + curationSidebarService.closeSession(aSessionOwner, project.getId()); } break; default: // Ignore + LOG.trace("No change in curation session state requested [{}]", + curationSessionParameterValue); } - if (!curationSessionParameterValue.isEmpty()) { - LOG.trace("Reloading page without session parameters"); - params.remove(PARAM_CURATION_TARGET_OWN); - params.remove(PARAM_CURATION_SESSION); - setProjectPageParameter(params, project); - params.set(AnnotationPage.PAGE_PARAM_DOCUMENT, - aPage.getModelObject().getDocument().getId()); - throw new RestartResponseException(aPage.getClass(), params); - } + LOG.trace("Removing session control parameters and reloading (redirect)"); + aParams.remove(PARAM_CURATION_TARGET_OWN); + aParams.remove(PARAM_CURATION_SESSION); + setProjectPageParameter(aParams, project); + aParams.set(AnnotationPage.PAGE_PARAM_DOCUMENT, aDoc.getId()); + // We need to do a redirect here to discard the arguments from the URL. + // This also discards the page state. + throw new RestartResponseException(aPage.getClass(), aParams); } - private boolean isViewingPotentialCurationTarget(AnnotationPage aPage) + private boolean isViewingPotentialCurationTarget(String aDataOwner) { // Curation sidebar is not allowed when viewing another users annotations var sessionOwner = userService.getCurrentUsername(); - var state = aPage.getModelObject(); - return asList(CURATION_USER, sessionOwner).contains(state.getUser().getUsername()); + var candidates = asList(CURATION_USER, sessionOwner); + var result = candidates.contains(aDataOwner); + if (!result) { + LOG.trace("Data ownwer [{}] is not in curation candidates {}", aDataOwner, candidates); + } + return result; } - private boolean isSessionActive(AnnotationPage aPage) + private boolean isSessionActive(Project aProject) { var sessionOwner = userService.getCurrentUsername(); - var project = aPage.getModelObject().getProject(); - if (project != null - && curationSidebarService.existsSession(sessionOwner, project.getId())) { - return true; - } - - return false; + return curationSidebarService.existsSession(sessionOwner, aProject.getId()); } } 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 a4b6b1832b4..189f3669ab0 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 @@ -18,9 +18,12 @@ package de.tudarmstadt.ukp.inception.ui.curation.sidebar; import static de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel.CURATOR; +import static java.lang.invoke.MethodHandles.lookup; +import static org.slf4j.LoggerFactory.getLogger; import org.apache.wicket.Component; import org.apache.wicket.model.IModel; +import org.slf4j.Logger; import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; @@ -41,6 +44,8 @@ public class CurationSidebarFactory extends AnnotationSidebarFactory_ImplBase { + private static final Logger LOG = getLogger(lookup().lookupClass()); + private final ProjectService projectService; private final UserDao userService; @@ -73,13 +78,7 @@ public AnnotationSidebar_ImplBase create(String aId, IModel aMod AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPage aAnnotationPage) { - var sidebar = new CurationSidebar(aId, aModel, aActionHandler, aCasProvider, - aAnnotationPage); - if (aAnnotationPage.getBehaviors(CurationSidebarBehavior.class).isEmpty()) { - aAnnotationPage.add(new CurationSidebarBehavior()); - } - - return sidebar; + return new CurationSidebar(aId, aModel, aActionHandler, aCasProvider, aAnnotationPage); } @Override diff --git a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java index 93c458fbaad..bb6dd7eb0b2 100644 --- a/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java +++ b/inception/inception-ui-curation/src/main/java/de/tudarmstadt/ukp/inception/ui/curation/sidebar/config/CurationSidebarAutoConfiguration.java @@ -34,6 +34,7 @@ import de.tudarmstadt.ukp.inception.schema.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.layer.LayerSupportRegistry; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationEditorExtension; +import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationSidebarApplicationInitializer; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationSidebarFactory; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationSidebarService; import de.tudarmstadt.ukp.inception.ui.curation.sidebar.CurationSidebarServiceImpl; @@ -82,4 +83,10 @@ public CurationSidebarRenderer curationSidebarRenderer(CurationSidebarService aC return new CurationSidebarRenderer(aCurationService, aLayerSupportRegistry, aDocumentService, aUserRepository, aAnnotationService); } + + @Bean + public CurationSidebarApplicationInitializer curationSidebarApplicationInitializer() + { + return new CurationSidebarApplicationInitializer(); + } }