/*
* 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);
}
}