/* * Copyright 2012 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * 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; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectByAddr; import static org.apache.uima.fit.util.JCasUtil.select; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.persistence.NoResultException; import org.apache.commons.lang3.tuple.Pair; import org.apache.uima.jcas.JCas; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.NumberTextField; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.StringResourceModel; 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 org.springframework.security.core.context.SecurityContextHolder; import org.wicketstuff.annotation.mount.MountPath; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService; import de.tudarmstadt.ukp.clarin.webanno.api.ProjectType; import de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil; import de.tudarmstadt.ukp.clarin.webanno.api.SettingsService; import de.tudarmstadt.ukp.clarin.webanno.api.WebAnnoConst; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorStateImpl; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.brat.annotation.BratAnnotationEditor; import de.tudarmstadt.ukp.clarin.webanno.constraints.ConstraintsService; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentStateTransition; import de.tudarmstadt.ukp.clarin.webanno.model.Mode; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocumentStateTransition; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.clarin.webanno.support.dialog.ConfirmationDialog; import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.clarin.webanno.support.lambda.LambdaAjaxSubmitLink; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.AnnotationPreferencesModalPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.DocumentNamePanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.ExportModalPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.FinishImage; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.GuidelineModalPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.AnnotationDetailEditorPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.dialog.OpenDocumentDialog; import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItem; import de.tudarmstadt.ukp.clarin.webanno.ui.core.menu.MenuItemCondition; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import wicket.contrib.input.events.EventType; import wicket.contrib.input.events.InputBehavior; import wicket.contrib.input.events.key.KeyType; /** * A wicket page for the Brat Annotation/Visualization page. Included components for pagination, * annotation layer configuration, and Exporting document */ @MenuItem(icon = "images/categories.png", label = "Annotation", prio = 100) @MountPath(value = "/annotation.html", alt = "/annotate/${" + AnnotationPage.PAGE_PARAM_PROJECT_ID + "}/${" + AnnotationPage.PAGE_PARAM_DOCUMENT_ID + "}") @ProjectType(id = WebAnnoConst.PROJECT_TYPE_ANNOTATION, prio = 100) public class AnnotationPage extends AnnotationPageBase { private static final Logger LOG = LoggerFactory.getLogger(AnnotationPage.class); private static final long serialVersionUID = 1378872465851908515L; public static final String PAGE_PARAM_PROJECT_ID = "projectId"; public static final String PAGE_PARAM_DOCUMENT_ID = "documentId"; private @SpringBean DocumentService documentService; private @SpringBean ProjectService projectService; private @SpringBean ConstraintsService constraintsService; private @SpringBean SettingsService settingsService; private @SpringBean AnnotationSchemaService annotationService; private @SpringBean UserDao userRepository; private NumberTextField<Integer> gotoPageTextField; private long currentprojectId; // Open the dialog window on first load private boolean firstLoad = true; private ModalWindow openDocumentsModal; private FinishImage finishDocumentIcon; private ConfirmationDialog finishDocumentDialog; private LambdaAjaxLink finishDocumentLink; private AnnotationEditorBase annotationEditor; private AnnotationDetailEditorPanel detailEditor; public AnnotationPage() { super(); LOG.debug("Setting up annotation page without parameters"); commonInit(); } public AnnotationPage(final PageParameters aPageParameters) { super(aPageParameters); LOG.debug("Setting up annotation page with parameters: {}", aPageParameters); commonInit(); long projectId = aPageParameters.get("projectId").toLong(); Project project; try { project = projectService.getProject(projectId); } catch (NoResultException e) { error("Project [" + projectId + "] does not exist"); return; } long documentId = aPageParameters.get("documentId").toLong(); SourceDocument document; try { document = documentService.getSourceDocument(projectId, documentId); } catch (NoResultException e) { error("Document [" + documentId + "] does not exist in project [" + projectId + "]"); return; } String username = SecurityContextHolder.getContext().getAuthentication().getName(); User user = userRepository.get(username); if (!SecurityUtil.isAnnotator(project, projectService, user)) { error("You have no permission to access document [" + documentId + "] in project [" + projectId + "]"); return; } if (documentService.existsAnnotationDocument(document, user)) { AnnotationDocument adoc = documentService.getAnnotationDocument(document, user); if (AnnotationDocumentState.IGNORE.equals(adoc.getState())) { error("Document [" + documentId + "] in project [" + projectId + "] is locked for you"); return; } } firstLoad = false; getModelObject().setUser(user); getModelObject().setProject(project); getModelObject().setDocument(document, getListOfDocs()); actionLoadDocument(null); } private void commonInit() { setVersioned(false); setModel(Model.of(new AnnotatorStateImpl(Mode.ANNOTATION))); WebMarkupContainer sidebarCell = new WebMarkupContainer("sidebarCell") { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag aTag) { super.onComponentTag(aTag); AnnotatorState state = AnnotationPage.this.getModelObject(); aTag.put("width", state.getPreferences().getSidebarSize()+"%"); } }; sidebarCell.setOutputMarkupId(true); add(sidebarCell); WebMarkupContainer annotationViewCell = new WebMarkupContainer("annotationViewCell") { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag aTag) { super.onComponentTag(aTag); AnnotatorState state = AnnotationPage.this.getModelObject(); aTag.put("width", (100-state.getPreferences().getSidebarSize())+"%"); } }; annotationViewCell.setOutputMarkupId(true); add(annotationViewCell); sidebarCell.add(detailEditor = createDetailEditor()); annotationEditor = new BratAnnotationEditor("embedder1", getModel(), detailEditor, () -> { return getEditorCas(); }); annotationViewCell.add(annotationEditor); add(createDocumentInfoLabel()); add(getOrCreatePositionInfoLabel()); add(openDocumentsModal = new OpenDocumentDialog("openDocumentsModal", getModel(), getAllowedProjects()) { private static final long serialVersionUID = 5474030848589262638L; @Override public void onDocumentSelected(AjaxRequestTarget aTarget) { // Reload the page using AJAX. This does not add the project/document ID to the URL, // but being AJAX it flickers less. actionLoadDocument(aTarget); // Load the document and add the project/document ID to the URL. This causes a full // page reload. No AJAX. // PageParameters pageParameters = new PageParameters(); // pageParameters.set(PAGE_PARAM_PROJECT_ID, getModelObject().getProject().getId()); // pageParameters.set(PAGE_PARAM_DOCUMENT_ID, // getModelObject().getDocument().getId()); // setResponsePage(AnnotationPage.class, pageParameters); } }); add(new AnnotationPreferencesModalPanel("annotationLayersModalPanel", getModel(), detailEditor) { private static final long serialVersionUID = -4657965743173979437L; @Override protected void onChange(AjaxRequestTarget aTarget) { actionCompletePreferencesChange(aTarget); } }); add(new ExportModalPanel("exportModalPanel", getModel()){ private static final long serialVersionUID = -468896211970839443L; { setOutputMarkupId(true); setOutputMarkupPlaceholderTag(true); } @Override protected void onConfigure() { super.onConfigure(); AnnotatorState state = AnnotationPage.this.getModelObject(); setVisible(state.getProject() != null && (SecurityUtil.isAdmin(state.getProject(), projectService, state.getUser()) || !state.getProject().isDisableExport())); } }); Form<Void> gotoPageTextFieldForm = new Form<Void>("gotoPageTextFieldForm"); gotoPageTextField = new NumberTextField<Integer>("gotoPageText", Model.of(1), Integer.class); // FIXME minimum and maximum should be obtained from the annotator state gotoPageTextField.setMinimum(1); gotoPageTextField.setOutputMarkupId(true); gotoPageTextFieldForm.add(gotoPageTextField); gotoPageTextFieldForm.add(new LambdaAjaxSubmitLink("gotoPageLink", gotoPageTextFieldForm, this::actionGotoPage)); add(gotoPageTextFieldForm); add(new LambdaAjaxLink("showOpenDocumentModal", this::actionShowOpenDocumentDialog)); add(new LambdaAjaxLink("showPreviousDocument", t -> actionShowPreviousDocument(t)) .add(new InputBehavior(new KeyType[] { KeyType.Shift, KeyType.Page_up }, EventType.click))); add(new LambdaAjaxLink("showNextDocument", t -> actionShowNextDocument(t)) .add(new InputBehavior(new KeyType[] { KeyType.Shift, KeyType.Page_down }, EventType.click))); add(new LambdaAjaxLink("showNext", t -> actionShowNextPage(t)) .add(new InputBehavior(new KeyType[] { KeyType.Page_down }, EventType.click))); add(new LambdaAjaxLink("showPrevious", t -> actionShowPreviousPage(t)) .add(new InputBehavior(new KeyType[] { KeyType.Page_up }, EventType.click))); add(new LambdaAjaxLink("showFirst", t -> actionShowFirstPage(t)) .add(new InputBehavior(new KeyType[] { KeyType.Home }, EventType.click))); add(new LambdaAjaxLink("showLast", t -> actionShowLastPage(t)) .add(new InputBehavior(new KeyType[] { KeyType.End }, EventType.click))); add(new LambdaAjaxLink("toggleScriptDirection", this::actionToggleScriptDirection)); add(new GuidelineModalPanel("guidelineModalPanel", getModel())); add(createOrGetResetDocumentDialog()); add(createOrGetResetDocumentLink()); add(finishDocumentDialog = new ConfirmationDialog("finishDocumentDialog", new StringResourceModel("FinishDocumentDialog.title", this, null), new StringResourceModel("FinishDocumentDialog.text", this, null))); add(finishDocumentLink = new LambdaAjaxLink("showFinishDocumentDialog", this::actionFinishDocument) { private static final long serialVersionUID = 874573384012299998L; @Override protected void onConfigure() { super.onConfigure(); AnnotatorState state = AnnotationPage.this.getModelObject(); setEnabled(state.getDocument() != null && !documentService .isAnnotationFinished(state.getDocument(), state.getUser())); } }); finishDocumentIcon = new FinishImage("finishImage", getModel()); finishDocumentIcon.setOutputMarkupId(true); finishDocumentLink.add(finishDocumentIcon); } private IModel<List<Pair<Project, String>>> getAllowedProjects() { return new LoadableDetachableModel<List<Pair<Project, String>>>() { private static final long serialVersionUID = -2518743298741342852L; @Override protected List<Pair<Project, String>> load() { User user = userRepository.get( SecurityContextHolder.getContext().getAuthentication().getName()); List<Pair<Project, String>> allowedProject = new ArrayList<>(); for (Project project : projectService.listProjects()) { if (SecurityUtil.isAnnotator(project, projectService, user) && WebAnnoConst.PROJECT_TYPE_ANNOTATION.equals(project.getMode())) { allowedProject.add(Pair.of(project, null)); } } return allowedProject; } }; } private DocumentNamePanel createDocumentInfoLabel() { return new DocumentNamePanel("documentNamePanel", getModel()); } private AnnotationDetailEditorPanel createDetailEditor() { return new AnnotationDetailEditorPanel("annotationDetailEditorPanel", getModel()) { private static final long serialVersionUID = 2857345299480098279L; @Override protected void onChange(AjaxRequestTarget aTarget) { aTarget.addChildren(getPage(), FeedbackPanel.class); aTarget.add(getOrCreatePositionInfoLabel()); try { annotationEditor.render(aTarget, getEditorCas()); annotationEditor.setHighlight(aTarget, getModelObject().getSelection().getAnnotation()); } catch (Exception e) { LOG.info("Error reading CAS: {} " + e.getMessage(), e); error("Error reading CAS: " + e.getMessage()); } } @Override protected void onAutoForward(AjaxRequestTarget aTarget) { try { annotationEditor.render(aTarget, getEditorCas()); } catch (Exception e) { LOG.info("Error reading CAS: {} " + e.getMessage(), e); error("Error reading CAS " + e.getMessage()); return; } } }; } @Override protected List<SourceDocument> getListOfDocs() { AnnotatorState state = getModelObject(); return new ArrayList<>(documentService .listAnnotatableDocuments(state.getProject(), state.getUser()).keySet()); } /** * for the first time, open the <b>open document dialog</b> */ @Override public void renderHead(IHeaderResponse response) { super.renderHead(response); String jQueryString = ""; if (firstLoad) { jQueryString += "jQuery('#showOpenDocumentModal').trigger('click');"; firstLoad = false; } response.render(OnLoadHeaderItem.forScript(jQueryString)); } @Override protected JCas getEditorCas() throws IOException { AnnotatorState state = getModelObject(); if (state.getDocument() == null) { throw new IllegalStateException("Please open a document first!"); } SourceDocument aDocument = getModelObject().getDocument(); AnnotationDocument annotationDocument = documentService.getAnnotationDocument(aDocument, state.getUser()); // If there is no CAS yet for the annotation document, create one. return documentService.readAnnotationCas(annotationDocument); } private void actionShowOpenDocumentDialog(AjaxRequestTarget aTarget) { getModelObject().getSelection().clear(); openDocumentsModal.show(aTarget); } private void actionGotoPage(AjaxRequestTarget aTarget, Form<?> aForm) throws Exception { AnnotatorState state = getModelObject(); JCas jcas = getEditorCas(); List<Sentence> sentences = new ArrayList<>(select(jcas, Sentence.class)); int selectedSentence = gotoPageTextField.getModelObject(); selectedSentence = Math.min(selectedSentence, sentences.size()); gotoPageTextField.setModelObject(selectedSentence); state.setFirstVisibleSentence(sentences.get(selectedSentence - 1)); state.setFocusSentenceNumber(selectedSentence); actionRefreshDocument(aTarget, jcas); } private void actionToggleScriptDirection(AjaxRequestTarget aTarget) throws Exception { getModelObject().toggleScriptDirection(); annotationEditor.renderLater(aTarget); } private void actionCompletePreferencesChange(AjaxRequestTarget aTarget) { try { AnnotatorState state = getModelObject(); JCas jCas = getEditorCas(); // The number of visible sentences may have changed - let the state recalculate // the visible sentences Sentence sentence = selectByAddr(jCas, Sentence.class, state.getFirstVisibleSentenceAddress()); state.setFirstVisibleSentence(sentence); // Re-render the whole page because the width of the sidebar may have changed aTarget.add(AnnotationPage.this); } catch (Exception e) { LOG.info("Error reading CAS " + e.getMessage()); error("Error reading CAS " + e.getMessage()); return; } } private void actionFinishDocument(AjaxRequestTarget aTarget) { finishDocumentDialog.setConfirmAction((aCallbackTarget) -> { ensureRequiredFeatureValuesSet(aCallbackTarget, getEditorCas()); AnnotatorState state = getModelObject(); AnnotationDocument annotationDocument = documentService.getAnnotationDocument( state.getDocument(), state.getUser()); annotationDocument.setState(AnnotationDocumentStateTransition.transition( AnnotationDocumentStateTransition.ANNOTATION_IN_PROGRESS_TO_ANNOTATION_FINISHED)); // manually update state change!! No idea why it is not updated in the DB // without calling createAnnotationDocument(...) documentService.createAnnotationDocument(annotationDocument); aCallbackTarget.add(finishDocumentIcon); aCallbackTarget.add(finishDocumentLink); aCallbackTarget.add(detailEditor); aCallbackTarget.add(createOrGetResetDocumentLink()); }); finishDocumentDialog.show(aTarget); } @Override protected void actionLoadDocument(AjaxRequestTarget aTarget) { LOG.info("BEGIN LOAD_DOCUMENT_ACTION"); AnnotatorState state = getModelObject(); String username = SecurityContextHolder.getContext().getAuthentication().getName(); User user = userRepository.get(username); state.setUser(user); try { // Check if there is an annotation document entry in the database. If there is none, // create one. AnnotationDocument annotationDocument = documentService .createOrGetAnnotationDocument(state.getDocument(), user); // Read the CAS JCas editorCas = documentService.readAnnotationCas(annotationDocument); // Update the annotation document CAS documentService.upgradeCas(editorCas.getCas(), annotationDocument); // After creating an new CAS or upgrading the CAS, we need to save it documentService.writeAnnotationCas(editorCas.getCas().getJCas(), annotationDocument.getDocument(), user, false); // (Re)initialize brat model after potential creating / upgrading CAS state.clearAllSelections(); // Load constraints state.setConstraints(constraintsService.loadConstraints(state.getProject())); // Load user preferences PreferencesUtil.loadPreferences(username, settingsService, projectService, annotationService, state, state.getMode()); // Initialize the visible content state.setFirstVisibleSentence(WebAnnoCasUtil.getFirstSentence(editorCas)); // if project is changed, reset some project specific settings if (currentprojectId != state.getProject().getId()) { state.clearRememberedFeatures(); } currentprojectId = state.getProject().getId(); LOG.debug("Configured BratAnnotatorModel for user [" + state.getUser() + "] f:[" + state.getFirstVisibleSentenceNumber() + "] l:[" + state.getLastVisibleSentenceNumber() + "] s:[" + state.getFocusSentenceNumber() + "]"); gotoPageTextField.setModelObject(1); // Re-render the whole page because the font size if (aTarget != null) { aTarget.add(this); } // Update document state if (state.getDocument().getState().equals(SourceDocumentState.NEW)) { state.getDocument().setState(SourceDocumentStateTransition .transition(SourceDocumentStateTransition.NEW_TO_ANNOTATION_IN_PROGRESS)); documentService.createSourceDocument(state.getDocument()); } // Reset the editor detailEditor.reset(aTarget); // Populate the layer dropdown box detailEditor.loadFeatureEditorModels(editorCas, aTarget); } catch (Exception e) { handleException(aTarget, e); } LOG.info("END LOAD_DOCUMENT_ACTION"); } @Override protected void actionRefreshDocument(AjaxRequestTarget aTarget, JCas aEditorCas) { annotationEditor.render(aTarget, aEditorCas); gotoPageTextField.setModelObject(getModelObject().getFirstVisibleSentenceNumber()); aTarget.add(gotoPageTextField); aTarget.add(getOrCreatePositionInfoLabel()); } @MenuItemCondition public static boolean menuItemCondition(ProjectService aRepo, UserDao aUserRepo) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); User user = aUserRepo.get(username); return SecurityUtil.annotationEnabeled(aRepo, user, WebAnnoConst.PROJECT_TYPE_ANNOTATION); } }