/* * Copyright 2015 * 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.detail; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.TypeUtil.getAdapter; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.findWindowStartCenteringOnSelection; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.getAddr; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.getFeature; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.getNextSentenceAddress; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.getSentenceNumber; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.isSame; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectAt; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectByAddr; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.setFeature; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.NoResultException; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.uima.UIMAException; import org.apache.uima.cas.CAS; import org.apache.uima.cas.CASRuntimeException; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; import org.apache.uima.jcas.JCas; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxCallListener; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.attributes.IAjaxCallListener; import org.apache.wicket.ajax.attributes.ThrottlingSettings; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.ajax.form.AjaxFormValidatingBehavior; import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.ChoiceRenderer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.RefreshingView; import org.apache.wicket.markup.repeater.util.ModelIteratorAdapter; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.time.Duration; import com.googlecode.wicket.kendo.ui.form.TextField; 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.WebAnnoConst; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter.ArcAdapter; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter.ChainAdapter; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter.SpanAdapter; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.adapter.TypeAdapter; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.FeatureState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.LinkWithRoleModel; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.Selection; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.TypeUtil; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.util.JavascriptUtils; import de.tudarmstadt.ukp.clarin.webanno.constraints.evaluator.Evaluator; import de.tudarmstadt.ukp.clarin.webanno.constraints.evaluator.PossibleValue; import de.tudarmstadt.ukp.clarin.webanno.constraints.evaluator.RulesIndicator; import de.tudarmstadt.ukp.clarin.webanno.constraints.evaluator.ValuesGenerator; import de.tudarmstadt.ukp.clarin.webanno.curation.storage.CurationDocumentService; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.Mode; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocumentState; import de.tudarmstadt.ukp.clarin.webanno.model.Tag; import de.tudarmstadt.ukp.clarin.webanno.model.TagSet; import de.tudarmstadt.ukp.clarin.webanno.support.DefaultFocusBehavior; import de.tudarmstadt.ukp.clarin.webanno.support.DefaultFocusBehavior2; import de.tudarmstadt.ukp.clarin.webanno.support.DescriptionTooltipBehavior; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.component.DeleteOrReplaceAnnotationModalPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.editor.BooleanFeatureEditor; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.editor.FeatureEditor; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.editor.LinkFeatureEditor; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.editor.NumberFeatureEditor; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.editor.TextFeatureEditor; import de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; /** * Annotation Detail Editor Panel. * */ public class AnnotationDetailEditorPanel extends Panel implements AnnotationActionHandler { private static final long serialVersionUID = 7324241992353693848L; private static final Logger LOG = LoggerFactory.getLogger(AnnotationDetailEditorPanel.class); private @SpringBean ProjectService projectService; private @SpringBean DocumentService documentService; private @SpringBean CurationDocumentService curationDocumentService; private @SpringBean AnnotationSchemaService annotationService; private AnnotationFeatureForm annotationFeatureForm; /** * Function to return tooltip using jquery * Docs for the JQuery tooltip widget that we configure below: * https://api.jqueryui.com/tooltip/ */ public static final String FUNCTION_FOR_TOOLTIP = "function() { return " + "'<div class=\"tooltip-title\">'+($(this).text() " + "? $(this).text() : 'no title')+'</div>" + "<div class=\"tooltip-content tooltip-pre\">'+($(this).attr('title') " + "? $(this).attr('title') : 'no description' )+'</div>' }"; public AnnotationDetailEditorPanel(String id, IModel<AnnotatorState> aModel) { super(id, aModel); setOutputMarkupId(true); annotationFeatureForm = new AnnotationFeatureForm("annotationFeatureForm", getModel()); annotationFeatureForm.setOutputMarkupId(true); annotationFeatureForm.add(new AjaxFormValidatingBehavior("submit") { private static final long serialVersionUID = -5642108496844056023L; @Override protected void onSubmit(AjaxRequestTarget aTarget) { try { actionAnnotate(aTarget); } catch (Exception e) { handleException(annotationFeatureForm, aTarget, e); } } }); add(annotationFeatureForm); } private boolean isAnnotationFinished() { AnnotatorState state = getModelObject(); if (state.getMode().equals(Mode.CURATION)) { return state.getDocument().getState().equals(SourceDocumentState.CURATION_FINISHED); } else { return documentService.isAnnotationFinished(state.getDocument(), state.getUser()); } } public class AnnotationFeatureForm extends Form<AnnotatorState> { private static final long serialVersionUID = 3635145598405490893L; // Add "featureEditorPanel" to AjaxRequestTargets instead of "featureEditorPanelContent" private WebMarkupContainer featureEditorPanel; private FeatureEditorPanelContent featureEditorPanelContent; private CheckBox forwardAnnotationCheck; private AjaxButton deleteButton; private AjaxButton reverseButton; private LayerSelector layerSelector; private String selectedTag = ""; private Label selectedAnnotationLayer; private TextField<String> forwardAnnotationText; private ModalWindow deleteModal; private Label selectedTextLabel; private List<AnnotationLayer> annotationLayers = new ArrayList<AnnotationLayer>(); public AnnotationFeatureForm(String id, IModel<AnnotatorState> aBModel) { super(id, new CompoundPropertyModel<AnnotatorState>(aBModel)); add(forwardAnnotationCheck = new CheckBox("forwardAnnotation") { private static final long serialVersionUID = 8908304272310098353L; { setOutputMarkupId(true); add(new AjaxFormComponentUpdatingBehavior("change") { private static final long serialVersionUID = 5179816588460867471L; @Override protected void onUpdate(AjaxRequestTarget aTarget) { updateForwardAnnotation(); if(AnnotationFeatureForm.this.getModelObject().isForwardAnnotation()){ aTarget.appendJavaScript(JavascriptUtils.getFocusScript(forwardAnnotationText)); selectedTag = ""; } } }); } @Override protected void onConfigure() { super.onConfigure(); setEnabled(isForwardable()); updateForwardAnnotation(); } }); add(new Label("noAnnotationWarning", "No annotation selected!") { private static final long serialVersionUID = -6046409838139863541L; @Override protected void onConfigure() { super.onConfigure(); setVisible(!AnnotationFeatureForm.this.getModelObject().getSelection() .getAnnotation().isSet()); } }); add(deleteButton = new AjaxButton("delete") { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); setVisible(state.getSelection().getAnnotation().isSet()); // Avoid deleting in read-only layers setEnabled(state.getSelectedAnnotationLayer() != null && !state.getSelectedAnnotationLayer().isReadonly()); } @Override public void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { try { AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); JCas jCas = getEditorCas(); AnnotationFS fs = selectByAddr(jCas, state.getSelection().getAnnotation().getId()); AnnotationLayer layer = state.getSelectedAnnotationLayer(); TypeAdapter adapter = getAdapter(annotationService, layer); if (adapter instanceof SpanAdapter && getAttachedRels(jCas, fs, layer).size() > 0) { deleteModal.setTitle("Are you sure you like to delete all attached relations to this span annotation?"); deleteModal.setContent(new DeleteOrReplaceAnnotationModalPanel( deleteModal.getContentId(), state, deleteModal, AnnotationDetailEditorPanel.this, state.getSelectedAnnotationLayer(), false)); deleteModal.show(aTarget); } else { actionDelete(aTarget); } } catch (Exception e) { handleException(this, aTarget, e); } } }); add(reverseButton = new AjaxButton("reverse") { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); setVisible(state.getSelection().isRelationAnno() && state.getSelection().getAnnotation().isSet() && state.getSelectedAnnotationLayer().getType() .equals(WebAnnoConst.RELATION_TYPE)); // Avoid reversing in read-only layers setEnabled(state.getSelectedAnnotationLayer() != null && !state.getSelectedAnnotationLayer().isReadonly()); } @Override public void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { aTarget.addChildren(getPage(), FeedbackPanel.class); try { actionReverse(aTarget); } catch (Exception e) { handleException(this, aTarget, e); } } }); reverseButton.setOutputMarkupPlaceholderTag(true); add(new AjaxButton("clear") { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); setVisible(AnnotationFeatureForm.this.getModelObject().getSelection() .getAnnotation().isSet()); } @Override public void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { aTarget.addChildren(getPage(), FeedbackPanel.class); try { actionClear(aTarget); } catch (Exception e) { handleException(this, aTarget, e); } } }); add(layerSelector = new LayerSelector("defaultAnnotationLayer", annotationLayers)); featureEditorPanel = new WebMarkupContainer("featureEditorsContainer") { private static final long serialVersionUID = 8908304272310098353L; @Override protected void onConfigure() { super.onConfigure(); setVisible(AnnotationFeatureForm.this.getModelObject().getSelection() .getAnnotation().isSet()); } }; // Add placeholder since wmc might start out invisible. Without the placeholder we // cannot make it visible in an AJAX call featureEditorPanel.setOutputMarkupPlaceholderTag(true); featureEditorPanel.setOutputMarkupId(true); featureEditorPanel.add(new Label("noFeaturesWarning", "No features available!") { private static final long serialVersionUID = 4398704672665066763L; @Override protected void onConfigure() { super.onConfigure(); setVisible(AnnotationFeatureForm.this.getModelObject().getFeatureStates() .isEmpty()); } }); featureEditorPanelContent = new FeatureEditorPanelContent("featureValues"); featureEditorPanel.add(featureEditorPanelContent); forwardAnnotationText = new TextField<String>("forwardAnno"); forwardAnnotationText.setOutputMarkupId(true); forwardAnnotationText.add(new AjaxFormComponentUpdatingBehavior("keyup") { private static final long serialVersionUID = 4554834769861958396L; @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); IAjaxCallListener listener = new AjaxCallListener() { private static final long serialVersionUID = -7968540662654079601L; @Override public CharSequence getPrecondition(Component component) { return "var keycode = Wicket.Event.keyCode(attrs.event);" + " return true;"; } }; attributes.getAjaxCallListeners().add(listener); attributes.getDynamicExtraParameters() .add("var eventKeycode = Wicket.Event.keyCode(attrs.event);" + "return {keycode: eventKeycode};"); attributes.setAllowDefault(true); } @Override protected void onUpdate(AjaxRequestTarget aTarget) { final Request request = RequestCycle.get().getRequest(); final String jsKeycode = request.getRequestParameters() .getParameterValue("keycode").toString(""); if (jsKeycode.equals("32")){ try { actionAnnotate(aTarget); selectedTag = ""; } catch (Exception e) { handleException(forwardAnnotationText, aTarget, e); } return; } if (jsKeycode.equals("13")){ selectedTag =""; return; } selectedTag = (forwardAnnotationText.getModelObject() == null ? "" : forwardAnnotationText.getModelObject().charAt(0)) + selectedTag; Map<String, String> bindTags = getBindTags(); if (!bindTags.isEmpty()) { List<FeatureState> featureStates = getModelObject().getFeatureStates(); featureStates.get(0).value = getKeyBindValue(selectedTag, bindTags); } aTarget.add(forwardAnnotationText); aTarget.add(featureEditorPanelContent.get(0)); } }); forwardAnnotationText.setOutputMarkupId(true); forwardAnnotationText.add(new AttributeAppender("style", "opacity:0", ";")); // forwardAnno.add(new AttributeAppender("style", "filter:alpha(opacity=0)", ";")); add(forwardAnnotationText); // the selected text for annotation selectedTextLabel = new Label("selectedText", PropertyModel.of(getModelObject(), "selection.text")); selectedTextLabel.setOutputMarkupId(true); featureEditorPanel.add(selectedTextLabel); featureEditorPanel.add(new Label("layerName","Layer"){ private static final long serialVersionUID = 6084341323607243784L; @Override protected void onConfigure() { super.onConfigure(); setVisible(AnnotationFeatureForm.this.getModelObject().getPreferences() .isRememberLayer()); } }); featureEditorPanel.setOutputMarkupId(true); // the annotation layer for the selected annotation selectedAnnotationLayer = new Label("selectedAnnotationLayer", new Model<String>()) { private static final long serialVersionUID = 4059460390544343324L; @Override protected void onConfigure() { super.onConfigure(); setVisible(AnnotationFeatureForm.this.getModelObject().getPreferences() .isRememberLayer()); } }; selectedAnnotationLayer.setOutputMarkupId(true); featureEditorPanel.add(selectedAnnotationLayer); add(featureEditorPanel); add(deleteModal = new ModalWindow("yesNoModal")); deleteModal.setOutputMarkupId(true); deleteModal.setInitialWidth(600); deleteModal.setInitialHeight(50); deleteModal.setResizable(true); deleteModal.setWidthUnit("px"); deleteModal.setHeightUnit("px"); deleteModal.setTitle("Are you sure you want to delete the existing annotation?"); } @Override protected void onConfigure() { super.onConfigure(); // Avoid reversing in read-only layers setEnabled(getModelObject().getDocument() != null && !isAnnotationFinished()); } private String getKeyBindValue(String aKey, Map<String, String> aBindTags){ // check if all the key pressed are the same character // if not, just check a Tag for the last char pressed if(aKey.isEmpty()){ return aBindTags.get(aBindTags.keySet().iterator().next()); } char prevC = aKey.charAt(0); for(char ch:aKey.toCharArray()){ if(ch!=prevC){ break; } } if (aBindTags.get(aKey)!=null){ return aBindTags.get(aKey); } // re-cycle suggestions if(aBindTags.containsKey(aKey.substring(0,1))){ selectedTag = aKey.substring(0,1); return aBindTags.get(aKey.substring(0,1)); } // set it to the first in the tag list , when arbitrary key is pressed return aBindTags.get(aBindTags.keySet().iterator().next()); } private Map<String, String> getBindTags() { AnnotationFeature f = annotationService .listAnnotationFeature(getModelObject().getSelectedAnnotationLayer()).get(0); TagSet tagSet = f.getTagset(); Map<Character, String> tagNames = new LinkedHashMap<>(); Map<String, String> bindTag2Key = new LinkedHashMap<>(); for (Tag tag : annotationService.listTags(tagSet)) { if (tagNames.containsKey(tag.getName().toLowerCase().charAt(0))) { String oldBinding = tagNames.get(tag.getName().toLowerCase().charAt(0)); String newBinding = oldBinding + tag.getName().toLowerCase().charAt(0); tagNames.put(tag.getName().toLowerCase().charAt(0), newBinding); bindTag2Key.put(newBinding, tag.getName()); } else { tagNames.put(tag.getName().toLowerCase().charAt(0), tag.getName().toLowerCase().substring(0, 1)); bindTag2Key.put(tag.getName().toLowerCase().substring(0, 1), tag.getName()); } } return bindTag2Key; } private void updateForwardAnnotation() { AnnotatorState state = getModelObject(); if (state.getSelectedAnnotationLayer() != null && !state.getSelectedAnnotationLayer().isLockToTokenOffset()) { state.setForwardAnnotation(false);// no forwarding for // sub-/multitoken annotation } else { state.setForwardAnnotation(state.isForwardAnnotation()); } } private void updateLayersDropdown() { LOG.trace(String.format("updateLayersDropdown()")); AnnotatorState state = getModelObject(); annotationLayers.clear(); AnnotationLayer l = null; for (AnnotationLayer layer : state.getAnnotationLayers()) { if (!layer.isEnabled() || layer.isReadonly() || layer.getName().equals(Token.class.getName())) { continue; } if (layer.getType().equals(WebAnnoConst.SPAN_TYPE)) { annotationLayers.add(layer); l = layer; } // manage chain type else if (layer.getType().equals(WebAnnoConst.CHAIN_TYPE)) { for (AnnotationFeature feature : annotationService.listAnnotationFeature(layer)) { if (!feature.isEnabled()) { continue; } if (feature.getName().equals(WebAnnoConst.COREFERENCE_TYPE_FEATURE)) { annotationLayers.add(layer); } } } // chain } if (state.getDefaultAnnotationLayer() != null) { state.setSelectedAnnotationLayer(state.getDefaultAnnotationLayer()); } else if (l != null) { state.setSelectedAnnotationLayer(l); } } private void updateRememberLayer() { LOG.trace(String.format("updateRememberLayer()")); AnnotatorState state = getModelObject(); if (state.getPreferences().isRememberLayer()) { if (state.getDefaultAnnotationLayer() == null) { state.setDefaultAnnotationLayer(state.getSelectedAnnotationLayer()); } } else if (!state.getSelection().isRelationAnno()) { state.setDefaultAnnotationLayer(state.getSelectedAnnotationLayer()); } // if no layer is selected in Settings if (state.getSelectedAnnotationLayer() != null) { selectedAnnotationLayer.setDefaultModelObject( state.getSelectedAnnotationLayer().getUiName()); } } public class LayerSelector extends DropDownChoice<AnnotationLayer> { private static final long serialVersionUID = 2233133653137312264L; public LayerSelector(String aId, List<? extends AnnotationLayer> aChoices) { super(aId, aChoices); setOutputMarkupId(true); setChoiceRenderer(new ChoiceRenderer<AnnotationLayer>("uiName")); add(new AjaxFormComponentUpdatingBehavior("change") { private static final long serialVersionUID = 5179816588460867471L; @Override protected void onUpdate(AjaxRequestTarget aTarget) { AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); // If "remember layer" is set, the we really just update the selected // layer... // we do not touch the selected annotation not the annotation detail panel if (state.getPreferences().isRememberLayer()) { state.setSelectedAnnotationLayer(getModelObject()); } // If "remember layer" is not set, then changing the layer means that we // want to change the type of the currently selected annotation else if (!state.getSelectedAnnotationLayer().equals(getModelObject()) && state.getSelection().getAnnotation().isSet()) { if (state.getSelection().isRelationAnno()) { try { actionClear(aTarget); } catch (Exception e) { handleException(LayerSelector.this, aTarget, e); } } else { AnnotationFeatureForm.this.deleteModal .setContent(new DeleteOrReplaceAnnotationModalPanel( AnnotationFeatureForm.this.deleteModal.getContentId(), state, AnnotationFeatureForm.this.deleteModal, AnnotationDetailEditorPanel.this, getModelObject(), true)); AnnotationFeatureForm.this.deleteModal.setWindowClosedCallback( new ModalWindow.WindowClosedCallback() { private static final long serialVersionUID = 4364820331676014559L; @Override public void onClose(AjaxRequestTarget target) { target.add(AnnotationFeatureForm.this); } }); AnnotationFeatureForm.this.deleteModal.show(aTarget); } } // If no annotation is selected, then prime the annotation detail panel for // the new type else { state.setSelectedAnnotationLayer(getModelObject()); AnnotationFeatureForm.this.selectedAnnotationLayer .setDefaultModelObject(getModelObject().getUiName()); aTarget.add(AnnotationFeatureForm.this.selectedAnnotationLayer); clearFeatureEditorModels(aTarget); } } }); } } public class FeatureEditorPanelContent extends RefreshingView<FeatureState> { private static final long serialVersionUID = -8359786805333207043L; public FeatureEditorPanelContent(String aId) { super(aId); setOutputMarkupId(true); // This strategy caches items as long as the panel exists. This is important to // allow the Kendo ComboBox datasources to be re-read when constraints change the // available tags. setItemReuseStrategy(new CachingReuseStrategy()); } @SuppressWarnings("rawtypes") @Override protected void populateItem(final Item<FeatureState> item) { LOG.trace(String.format("FeatureEditorPanelContent.populateItem(" + item.getModelObject().feature.getUiName() + ": " + item.getModelObject().value + ")")); // Feature editors that allow multiple values may want to update themselves, // e.g. to add another slot. item.setOutputMarkupId(true); final FeatureState featureState = item.getModelObject(); final FeatureEditor frag; switch (featureState.feature.getMultiValueMode()) { case NONE: { switch (featureState.feature.getType()) { case CAS.TYPE_NAME_INTEGER: { frag = new NumberFeatureEditor("editor", "numberFeatureEditor", AnnotationFeatureForm.this, item.getModel()); break; } case CAS.TYPE_NAME_FLOAT: { frag = new NumberFeatureEditor("editor", "numberFeatureEditor", AnnotationFeatureForm.this, item.getModel()); break; } case CAS.TYPE_NAME_BOOLEAN: { frag = new BooleanFeatureEditor("editor", "booleanFeatureEditor", AnnotationFeatureForm.this, item.getModel()); break; } case CAS.TYPE_NAME_STRING: { frag = new TextFeatureEditor("editor", "textFeatureEditor", AnnotationFeatureForm.this, item.getModel()); break; } default: throw new IllegalArgumentException( "Unsupported type [" + featureState.feature.getType() + "] on feature [" + featureState.feature.getName() + "]"); } break; } case ARRAY: { switch (featureState.feature.getLinkMode()) { case WITH_ROLE: { // If it is none of the primitive types, it must be a link feature frag = new LinkFeatureEditor("editor", "linkFeatureEditor", AnnotationDetailEditorPanel.this, item.getModel()); break; } default: throw new IllegalArgumentException( "Unsupported link mode [" + featureState.feature.getLinkMode() + "] on feature [" + featureState.feature.getName() + "]"); } break; } default: throw new IllegalArgumentException("Unsupported multi-value mode [" + featureState.feature.getMultiValueMode() + "] on feature [" + featureState.feature.getName() + "]"); } if (!featureState.feature.getLayer().isReadonly()) { AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); // Whenever it is updating an annotation, it updates automatically when a // component for the feature lost focus - but updating is for every component // edited LinkFeatureEditors must be excluded because the auto-update will break // the ability to add slots. Adding a slot is NOT an annotation action. if (state.getSelection().getAnnotation().isSet() && !(frag instanceof LinkFeatureEditor)) { addAnnotateActionBehavior(frag, "change"); } else if (!(frag instanceof LinkFeatureEditor)) { addRefreshFeaturePanelBehavior(frag, "change"); } // Put focus on hidden input field if we are in forward-mode if (state.isForwardAnnotation()) { AnnotationFeatureForm.this.forwardAnnotationText .add(new DefaultFocusBehavior2()); } // Put focus on first component if we select an existing annotation or create a // new one else if (item.getIndex() == 0 && SpanAnnotationResponse.is(state.getAction().getUserAction())) { frag.getFocusComponent().add(new DefaultFocusBehavior()); } // Restore/preserve focus when tabbing through the feature editors else if (state.getAction().getUserAction() == null) { AjaxRequestTarget target = RequestCycle.get() .find(AjaxRequestTarget.class); if (target != null && frag.getFocusComponent().getMarkupId() .equals(target.getLastFocusedElementId())) { target.focusComponent(frag.getFocusComponent()); } } // Add tooltip on label StringBuilder tooltipTitle = new StringBuilder(); tooltipTitle.append(featureState.feature.getUiName()); if (featureState.feature.getTagset() != null) { tooltipTitle.append(" ("); tooltipTitle.append(featureState.feature.getTagset().getName()); tooltipTitle.append(')'); } Component labelComponent = frag.getLabelComponent(); labelComponent.add(new AttributeAppender("style", "cursor: help", ";")); labelComponent.add(new DescriptionTooltipBehavior(tooltipTitle.toString(), featureState.feature.getDescription())); } else { frag.getFocusComponent().setEnabled(false); } // 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. frag.setOutputMarkupId(true); item.add(frag); } private void addRefreshFeaturePanelBehavior(final FeatureEditor aFrag, String aEvent) { aFrag.getFocusComponent().add(new AjaxFormComponentUpdatingBehavior(aEvent) { private static final long serialVersionUID = 5179816588460867471L; @Override protected void onUpdate(AjaxRequestTarget aTarget) { aTarget.add(featureEditorPanel); } }); } private void addAnnotateActionBehavior(final FeatureEditor aFrag, String aEvent) { aFrag.getFocusComponent().add(new AjaxFormComponentUpdatingBehavior(aEvent) { private static final long serialVersionUID = 5179816588460867471L; @Override protected void updateAjaxAttributes(AjaxRequestAttributes aAttributes) { super.updateAjaxAttributes(aAttributes); // When focus is on a feature editor and the user selects a new annotation, // there is a race condition between the saving the value of the feature // editor and the loading of the new annotation. Delay the feature editor // save to give preference to loading the new annotation. aAttributes.setThrottlingSettings(new ThrottlingSettings(getMarkupId(), Duration.milliseconds(250), true)); aAttributes.getAjaxCallListeners().add(new AjaxCallListener() { private static final long serialVersionUID = 1L; @Override public CharSequence getPrecondition(Component aComponent) { // If the panel refreshes because the user selects a new annotation, // the annotation editor panel is updated for the new annotation // first (before saving values) because of the delay set above. When // the delay is over, we can no longer save the value because the // old component is no longer there. We use the markup id of the // editor fragments to check if the old component is still there // (i.e. if the user has just tabbed to a new field) or if the old // component is gone (i.e. the user selected/created another // annotation). If the old component is no longer there, we abort // the delayed save action. return "return $('#" + aFrag.getMarkupId() + "').length > 0;"; } }); } @Override protected void onUpdate(AjaxRequestTarget aTarget) { try { AnnotatorState state = AnnotationFeatureForm.this.getModelObject(); if (state.getConstraints() != null) { // Make sure we update the feature editor panel because due to // constraints the contents may have to be re-rendered aTarget.add(AnnotationFeatureForm.this.featureEditorPanel); } actionAnnotate(aTarget); } catch (Exception e) { handleException(FeatureEditorPanelContent.this, aTarget, e); } } }); } @Override protected Iterator<IModel<FeatureState>> getItemModels() { List<FeatureState> featureStates = AnnotationFeatureForm.this.getModelObject() .getFeatureStates(); ModelIteratorAdapter<FeatureState> i = new ModelIteratorAdapter<FeatureState>( featureStates) { @Override protected IModel<FeatureState> model(FeatureState aObject) { return FeatureStateModel.of(AnnotationFeatureForm.this.getModel(), aObject); } }; return i; } } } private void createNewAnnotation(AjaxRequestTarget aTarget, TypeAdapter aAdapter, JCas aJCas) throws AnnotationException, UIMAException, ClassNotFoundException, IOException { AnnotatorState state = getModelObject(); if (state.getSelection().isRelationAnno()) { if (aAdapter instanceof SpanAdapter) { error("Layer [" + aAdapter.getLayer().getUiName() + "] does not support arc annotation."); aTarget.addChildren(getPage(), FeedbackPanel.class); return; } else if (aAdapter instanceof ArcAdapter) { createNewRelationAnnotation(aTarget, (ArcAdapter) aAdapter, aJCas); } else if (aAdapter instanceof ChainAdapter) { createNewChainLinkAnnotation(aTarget, (ChainAdapter) aAdapter, aJCas); } else { throw new IllegalStateException("I don't know how to use [" + aAdapter.getClass().getSimpleName() + "] in this situation."); } } else { if (aAdapter instanceof SpanAdapter) { createNewSpanAnnotation(aTarget, (SpanAdapter) aAdapter, aJCas); } else if (aAdapter instanceof ChainAdapter) { createNewChainElement(aTarget, (ChainAdapter) aAdapter, aJCas); } else { throw new IllegalStateException("I don't know how to use [" + aAdapter.getClass().getSimpleName() + "] in this situation."); } } } private void createNewRelationAnnotation(AjaxRequestTarget aTarget, ArcAdapter aAdapter, JCas aJCas) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { LOG.trace(String.format("createNewRelationAnnotation()")); AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); AnnotationFS originFs = selectByAddr(aJCas, selection.getOrigin()); AnnotationFS targetFs = selectByAddr(aJCas, selection.getTarget()); // Creating a relation AnnotationFS arc = aAdapter.add(originFs, targetFs, aJCas, state.getWindowBeginOffset(), state.getWindowEndOffset(), null, null); selection.setAnnotation(new VID(getAddr(arc))); if (selection.getAnnotation().isSet()) { selection.setText("[" + originFs.getCoveredText() + "] - [" + targetFs.getCoveredText() + "]"); } else { selection.setText(""); } selection.setBegin(originFs.getBegin()); } private void createNewSpanAnnotation(AjaxRequestTarget aTarget, SpanAdapter aAdapter, JCas aJCas) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { LOG.trace(String.format("createNewSpanAnnotation()")); AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); List<FeatureState> featureStates = state.getFeatureStates(); for (FeatureState featureState : featureStates) { Serializable spanValue = aAdapter.getSpan(aJCas, selection.getBegin(), selection.getEnd(), featureState.feature, null); if (spanValue != null) { // allow modification for forward annotation if (state.isForwardAnnotation()) { featureState.value = spanValue; featureStates.get(0).value = spanValue; annotationFeatureForm.selectedTag = annotationFeatureForm.getBindTags() .entrySet().stream().filter(e -> e.getValue().equals(spanValue)) .map(Map.Entry::getKey).findFirst().orElse(null); } else { actionClear(aTarget); throw new AnnotationException("Cannot create another annotation of layer [" + state.getSelectedAnnotationLayer().getUiName() + "] at this" + " location - stacking is not enabled for this layer."); } } } Integer annoId = aAdapter.add(aJCas, selection.getBegin(), selection.getEnd(), null, null); selection.setAnnotation(new VID(annoId)); AnnotationFS annoFs = WebAnnoCasUtil.selectByAddr(aJCas, annoId); selection.set(aJCas, annoFs.getBegin(), annoFs.getEnd()); } private void createNewChainElement(AjaxRequestTarget aTarget, ChainAdapter aAdapter, JCas aJCas) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { LOG.trace(String.format("createNewChainElement()")); AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); List<FeatureState> featureStates = state.getFeatureStates(); for (FeatureState featureState : featureStates) { Serializable spanValue = aAdapter.getSpan(aJCas, selection.getBegin(), selection.getEnd(), featureState.feature, null); if (spanValue != null) { // allow modification for forward annotation if (state.isForwardAnnotation()) { featureState.value = spanValue; featureStates.get(0).value = spanValue; annotationFeatureForm.selectedTag = annotationFeatureForm.getBindTags() .entrySet().stream().filter(e -> e.getValue().equals(spanValue)) .map(Map.Entry::getKey).findFirst().orElse(null); } } } selection.setAnnotation(new VID( aAdapter.addSpan(aJCas, selection.getBegin(), selection.getEnd(), null, null))); selection.setText( aJCas.getDocumentText().substring(selection.getBegin(), selection.getEnd())); } private void createNewChainLinkAnnotation(AjaxRequestTarget aTarget, ChainAdapter aAdapter, JCas aJCas) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { LOG.trace(String.format("createNewChainLinkAnnotation()")); AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); AnnotationFS originFs = selectByAddr(aJCas, selection.getOrigin()); AnnotationFS targetFs = selectByAddr(aJCas, selection.getTarget()); // Creating a new chain link selection.setAnnotation(new VID(aAdapter.addArc(aJCas, originFs, targetFs, null, null))); if (selection.getAnnotation().isSet()) { selection.setText(originFs.getCoveredText()); } else { selection.setText(""); } selection.setBegin(originFs.getBegin()); } @Override public void actionArcAnnotation(AjaxRequestTarget aTarget, JCas jCas, VID paramId, String aOriginType, int aOriginSpanId, String aTargetType, int aTargetSpanId) throws AnnotationException, UIMAException, ClassNotFoundException, IOException { assert jCas != null; AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); selection.setRelationAnno(true); selection.setAnnotation(paramId); selection.setOriginType(aOriginType); selection.setOrigin(aOriginSpanId); selection.setTargetType(aTargetType); selection.setTarget(aTargetSpanId); state.getAction().setAnnotate(selection.getAnnotation().isNotSet()); if (selection.getAnnotation().isNotSet()) { // Create new annotation actionAnnotate(aTarget); } else { // Edit existing annotation AnnotationFS originFs = selectByAddr(jCas, selection.getOrigin()); AnnotationFS targetFs = selectByAddr(jCas, selection.getTarget()); selection.setText("[" + originFs.getCoveredText() + "] - [" + targetFs.getCoveredText() + "]"); loadFeatureEditorModels(jCas, aTarget); // Ensure we re-render and update the highlight onChange(aTarget); } } @Override public void actionSpanAnnotation(AjaxRequestTarget aTarget, JCas jCas, int aBegin, int aEnd, VID paramId) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { assert jCas != null; AnnotatorState state = getModelObject(); if (state.isSlotArmed()) { if (paramId.isSet()) { // Fill slot with existing annotation setSlot(aTarget, jCas, paramId.getId()); } else if (!CAS.TYPE_NAME_ANNOTATION .equals(state.getArmedFeature().getType())) { // Fill slot with new annotation (only works if a concrete type is // set for the link feature! SpanAdapter adapter = (SpanAdapter) getAdapter(annotationService, annotationService.getLayer(state.getArmedFeature().getType(), state.getProject())); try { int id = adapter.add(jCas, aBegin, aEnd, null, null); setSlot(aTarget, jCas, id); } catch (Exception e) { handleException(this, aTarget, e); } } else { throw new AnnotationException("Unable to create annotation of type ["+ CAS.TYPE_NAME_ANNOTATION+"]. Please click an annotation in stead of selecting new text."); } } else { // Doing anything but filling an armed slot will unarm it clearArmedSlotModel(); state.clearArmedSlot(); Selection selection = state.getSelection(); selection.setRelationAnno(false); selection.setAnnotation(paramId); selection.set(jCas, aBegin, aEnd); state.getAction().setAnnotate(selection.getAnnotation().isNotSet()); if (selection.getAnnotation().isNotSet()) { // Create new annotation actionAnnotate(aTarget); } else { // Edit existing annotation loadFeatureEditorModels(jCas, aTarget); // Ensure we re-render and update the highlight onChange(aTarget); } } } @Override public void actionAnnotate(AjaxRequestTarget aTarget) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { // If there is no annotation yet, create one. During creation, the adapter // may notice that it would create a duplicate and return the address of // an existing annotation instead of a new one. JCas jCas = getEditorCas(); actionAnnotate(aTarget, jCas, false); } private void actionAnnotate(AjaxRequestTarget aTarget, JCas aJCas, boolean aIsForwarded) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { LOG.trace(String.format("actionAnnotate(isForwarded: %b)", aIsForwarded)); if (isAnnotationFinished()) { throw new AnnotationException("This document is already closed. Please ask your " + "project manager to re-open it via the Monitoring page"); } AnnotatorState state = getModelObject(); state.getAction().setAnnotate(true); // Note that refresh changes the selected layer if a relation is created. Then the layer // switches from the selected span layer to the relation layer that is attached to the span if (state.getSelection().isRelationAnno()) { LOG.trace(String .format("actionAnnotate() relation annotation - looking for attached layer")); // FIXME REC I think this whole section which meddles around with the selected annotation // layer should be moved out of there to the place where we originally set the annotation // layer...! long layerId = TypeUtil.getLayerId(state.getSelection().getOriginType()); AnnotationLayer spanLayer = annotationService.getLayer(layerId); if ( state.getPreferences().isRememberLayer() && state.getAction().isAnnotate() && !spanLayer.equals(state.getDefaultAnnotationLayer())) { throw new AnnotationException( "No relation annotation allowed ["+ spanLayer.getUiName() +"]"); } AnnotationLayer previousLayer = state.getSelectedAnnotationLayer(); // If we are creating a relation annotation, we have to set the current layer depending // on the type of relation that is permitted between the source/target span. This is // necessary because we have no separate UI control to set the relation annotation type. // It is possible because currently only a single relation layer is allowed to attach to // any given span layer. // If we drag an arc between POS annotations, then the relation must be a dependency // relation. // FIXME - Actually this case should be covered by the last case - the database lookup! if ( spanLayer.isBuiltIn() && spanLayer.getName().equals(POS.class.getName())) { AnnotationLayer depLayer = annotationService.getLayer(Dependency.class.getName(), state.getProject()); if (state.getAnnotationLayers().contains(depLayer)) { state.setSelectedAnnotationLayer(depLayer); } else { state.setSelectedAnnotationLayer(null); } } // If we drag an arc in a chain layer, then the arc is of the same layer as the span // Chain layers consist of arcs and spans else if (spanLayer.getType().equals(WebAnnoConst.CHAIN_TYPE)) { // one layer both for the span and arc annotation state.setSelectedAnnotationLayer(spanLayer); } // Otherwise, look up the possible relation layer(s) in the database. else { for (AnnotationLayer layer : annotationService.listAnnotationLayer(state .getProject())) { if (layer.getAttachType() != null && layer.getAttachType().equals(spanLayer)) { if (state.getAnnotationLayers().contains(layer)) { state.setSelectedAnnotationLayer(layer); } else { state.setSelectedAnnotationLayer(null); } break; } } } state.setDefaultAnnotationLayer(spanLayer); // If we switched layers, we need to initialize the feature editors for the new layer if (!ObjectUtils.equals(previousLayer, state.getSelectedAnnotationLayer())) { loadFeatureEditorModels(aJCas, aTarget); } } LOG.trace(String.format("actionAnnotate() selectedLayer: %s", state.getSelectedAnnotationLayer().getUiName())); LOG.trace(String.format("actionAnnotate() defaultLayer: %s", state.getDefaultAnnotationLayer().getUiName())); if (state.getSelectedAnnotationLayer() == null) { error("No layer is selected. First select a layer."); aTarget.addChildren(getPage(), FeedbackPanel.class); return; } if (state.getSelectedAnnotationLayer().isReadonly()) { error("Layer is not editable."); aTarget.addChildren(getPage(), FeedbackPanel.class); return; } // Verify if input is valid according to tagset LOG.trace(String.format("actionAnnotate() verifying feature values in editors")); List<FeatureState> featureStates = getModelObject().getFeatureStates(); for (int i = 0; i < featureStates.size(); i++) { AnnotationFeature feature = featureStates.get(i).feature; if (CAS.TYPE_NAME_STRING.equals(feature.getType())) { String value = (String) featureStates.get(i).value; // Check if tag is necessary, set, and correct if ( value != null && feature.getTagset() != null && !feature.getTagset().isCreateTag() && !annotationService.existsTag(value, feature.getTagset()) ) { error("[" + value + "] is not in the tag list. Please choose from the existing tags"); return; } } } // #186 - After filling a slot, the annotation detail panel is not updated aTarget.add(annotationFeatureForm.featureEditorPanel); TypeAdapter adapter = getAdapter(annotationService, state.getSelectedAnnotationLayer()); // If this is an annotation creation action, create the annotation if (state.getSelection().getAnnotation().isNotSet()) { // Load the feature editors with the remembered values (if any) loadFeatureEditorModels(aJCas, aTarget); createNewAnnotation(aTarget, adapter, aJCas); } // Update the features of the selected annotation from the values presently in the // feature editors writeFeatureEditorModelsToCas(adapter, aJCas); // Update progress information LOG.trace(String.format("actionAnnotate() updating progress information")); int sentenceNumber = getSentenceNumber(aJCas, state.getSelection().getBegin()); state.setFocusSentenceNumber(sentenceNumber); state.getDocument().setSentenceAccessed(sentenceNumber); // persist changes writeEditorCas(aJCas); // Remember the current feature values independently for spans and relations LOG.trace(String.format("actionAnnotate() remembering feature editor values")); state.rememberFeatures(); // Loading feature editor values from CAS loadFeatureEditorModels(aJCas, aTarget); // onAnnotate callback LOG.trace(String.format("onAnnotate()")); onAnnotate(aTarget); // Handle auto-forward if it is enabled if (state.isForwardAnnotation() && !aIsForwarded && featureStates.get(0).value != null) { if (state.getSelection().getEnd() >= state.getFirstVisibleSentenceEnd()) { autoScroll(aJCas, true); } LOG.info("BEGIN auto-forward annotation"); AnnotationFS nextToken = WebAnnoCasUtil.getNextToken(aJCas, state.getSelection().getBegin(), state.getSelection().getEnd()); if (nextToken != null) { if (getModelObject().getWindowEndOffset() > nextToken.getBegin()) { state.getSelection().clear(); state.getSelection().set(aJCas, nextToken.getBegin(), nextToken.getEnd()); actionAnnotate(aTarget, aJCas, true); } } LOG.trace(String.format("onAutoForward()")); onAutoForward(aTarget); LOG.info("END auto-forward annotation"); } // Perform auto-scroll if it is enabled else if (state.getPreferences().isScrollPage()) { autoScroll(aJCas, false); } annotationFeatureForm.forwardAnnotationText.setModelObject(null); LOG.trace(String.format("onChange()")); onChange(aTarget); if (state.isForwardAnnotation() && state.getFeatureStates().get(0).value != null) { aTarget.add(annotationFeatureForm); } // If we created a new annotation, then refresh the available annotation layers in the // detail panel. if (state.getSelection().getAnnotation().isNotSet()) { // This already happens in loadFeatureEditorModels() above - probably not needed // here again // annotationFeatureForm.updateLayersDropdown(); LOG.trace(String.format("actionAnnotate() setting selected layer (not sure why)")); if (annotationFeatureForm.annotationLayers.size() == 0) { state.setSelectedAnnotationLayer(new AnnotationLayer()); } else if (state.getSelectedAnnotationLayer() == null) { if (state.getRememberedSpanLayer() == null) { state.setSelectedAnnotationLayer(annotationFeatureForm.annotationLayers.get(0)); } else { state.setSelectedAnnotationLayer(state.getRememberedSpanLayer()); } } LOG.trace(String.format("actionAnnotate() selectedLayer: %s", state.getSelectedAnnotationLayer().getUiName())); // Actually not sure why we would want to clear these here - in fact, they should // still be around for the rendering phase of the feature editors... //clearFeatureEditorModels(aTarget); // This already happens in loadFeatureEditorModels() above - probably not needed // here again // annotationFeatureForm.updateRememberLayer(); } } public void actionDelete(AjaxRequestTarget aTarget) throws IOException, UIMAException, ClassNotFoundException, CASRuntimeException, AnnotationException { JCas jCas = getEditorCas(); AnnotatorState state = getModelObject(); AnnotationFS fs = selectByAddr(jCas, state.getSelection().getAnnotation().getId()); // TODO We assume here that the selected annotation layer corresponds to the type of the // FS to be deleted. It would be more robust if we could get the layer from the FS itself. AnnotationLayer layer = state.getSelectedAnnotationLayer(); TypeAdapter adapter = getAdapter(annotationService, layer); // == DELETE ATTACHED RELATIONS == // If the deleted FS is a span, we must delete all relations that // point to it directly or indirectly via the attachFeature. // // NOTE: It is important that this happens before UNATTACH SPANS since the attach feature // is no longer set after UNATTACH SPANS! if (adapter instanceof SpanAdapter) { for (AnnotationFS attachedFs : getAttachedRels(jCas, fs, layer)) { jCas.getCas().removeFsFromIndexes(attachedFs); info("The attached annotation for relation type [" + annotationService .getLayer(attachedFs.getType().getName(), state.getProject()).getUiName() + "] is deleted"); } } // == DELETE ATTACHED SPANS == // This case is currently not implemented because WebAnno currently does not allow to // create spans that attach to other spans. The only span type for which this is relevant // is the Token type which cannot be deleted. // == UNATTACH SPANS == // If the deleted FS is a span that is attached to another span, the // attachFeature in the other span must be set to null. Typical example: POS is deleted, so // the pos feature of Token must be set to null. This is a quick case, because we only need // to look at span annotations that have the same offsets as the FS to be deleted. if (adapter instanceof SpanAdapter && layer.getAttachType() != null) { Type spanType = CasUtil.getType(jCas.getCas(), layer.getAttachType().getName()); Feature attachFeature = spanType.getFeatureByBaseName(layer.getAttachFeature() .getName()); for (AnnotationFS attachedFs : selectAt(jCas.getCas(), spanType, fs.getBegin(), fs.getEnd())) { if (isSame(attachedFs.getFeatureValue(attachFeature), fs)) { attachedFs.setFeatureValue(attachFeature, null); LOG.debug("Unattached [" + attachFeature.getShortName() + "] on annotation [" + getAddr(attachedFs) + "]"); } } } // == CLEAN UP LINK FEATURES == // If the deleted FS is a span that is the target of a link feature, we must unset that // link and delete the slot if it is a multi-valued link. Here, we have to scan all // annotations from layers that have link features that could point to the FS // to be deleted: the link feature must be the type of the FS or it must be generic. if (adapter instanceof SpanAdapter) { for (AnnotationFeature linkFeature : annotationService.listAttachedLinkFeatures(layer)) { Type linkType = CasUtil.getType(jCas.getCas(), linkFeature.getLayer().getName()); for (AnnotationFS linkFS : CasUtil.select(jCas.getCas(), linkType)) { List<LinkWithRoleModel> links = getFeature(linkFS, linkFeature); Iterator<LinkWithRoleModel> i = links.iterator(); boolean modified = false; while (i.hasNext()) { LinkWithRoleModel link = i.next(); if (link.targetAddr == getAddr(fs)) { i.remove(); LOG.debug("Cleared slot [" + link.role + "] in feature [" + linkFeature.getName() + "] on annotation [" + getAddr(linkFS) + "]"); modified = true; } } if (modified) { setFeature(linkFS, linkFeature, links); } } } } // If the deleted FS is a relation, we don't have to do anything. Nothing can point to a // relation. if (adapter instanceof ArcAdapter) { // Do nothing ;) } // Actually delete annotation adapter.delete(jCas, state.getSelection().getAnnotation()); // Store CAS again writeEditorCas(jCas); // Update progress information int sentenceNumber = getSentenceNumber(jCas, state.getSelection().getBegin()); state.setFocusSentenceNumber(sentenceNumber); state.getDocument().setSentenceAccessed(sentenceNumber); // Auto-scroll if (state.getPreferences().isScrollPage()) { autoScroll(jCas, false); } state.rememberFeatures(); state.getAction().setAnnotate(false); info(generateMessage(state.getSelectedAnnotationLayer(), null, true)); state.getSelection().clear(); // after delete will follow annotation state.getAction().setAnnotate(true); aTarget.add(annotationFeatureForm); onChange(aTarget); onDelete(aTarget, fs); } private void actionReverse(AjaxRequestTarget aTarget) throws IOException, UIMAException, ClassNotFoundException, AnnotationException { JCas jCas = getEditorCas(); AnnotatorState state = getModelObject(); AnnotationFS idFs = selectByAddr(jCas, state.getSelection().getAnnotation().getId()); jCas.removeFsFromIndexes(idFs); AnnotationFS originFs = selectByAddr(jCas, state.getSelection().getOrigin()); AnnotationFS targetFs = selectByAddr(jCas, state.getSelection().getTarget()); List<FeatureState> featureStates = getModelObject().getFeatureStates(); TypeAdapter adapter = getAdapter(annotationService, state.getSelectedAnnotationLayer()); if (adapter instanceof ArcAdapter) { if (featureStates.size() == 0) { // If no features, still create arc #256 AnnotationFS arc = ((ArcAdapter) adapter).add(targetFs, originFs, jCas, state.getWindowBeginOffset(), state.getWindowEndOffset(), null, null); state.getSelection().setAnnotation(new VID(getAddr(arc))); } else { for (FeatureState featureState : featureStates) { AnnotationFS arc = ((ArcAdapter) adapter).add(targetFs, originFs, jCas, state.getWindowBeginOffset(), state.getWindowEndOffset(), featureState.feature, featureState.value); state.getSelection().setAnnotation(new VID(getAddr(arc))); } } } else { error("chains cannot be reversed"); return; } // persist changes writeEditorCas(jCas); int sentenceNumber = getSentenceNumber(jCas, originFs.getBegin()); state.setFocusSentenceNumber(sentenceNumber); state.getDocument().setSentenceAccessed(sentenceNumber); if (state.getPreferences().isScrollPage()) { autoScroll(jCas, false); } info("The arc has been reversed"); state.rememberFeatures(); // in case the user re-reverse it int temp = state.getSelection().getOrigin(); state.getSelection().setOrigin(state.getSelection().getTarget()); state.getSelection().setTarget(temp); onChange(aTarget); } public void actionClear(AjaxRequestTarget aTarget) throws IOException, UIMAException, ClassNotFoundException, AnnotationException { reset(aTarget); aTarget.add(annotationFeatureForm); onChange(aTarget); } public JCas getEditorCas() throws UIMAException, IOException, ClassNotFoundException { AnnotatorState state = getModelObject(); if (state.getMode().equals(Mode.ANNOTATION) || state.getMode().equals(Mode.AUTOMATION) || state.getMode().equals(Mode.CORRECTION)) { return documentService.readAnnotationCas(state.getDocument(), state.getUser()); } else { return curationDocumentService.readCurationCas(state.getDocument()); } } public void writeEditorCas(JCas aJCas) throws IOException { AnnotatorState state = getModelObject(); if (state.getMode().equals(Mode.ANNOTATION) || state.getMode().equals(Mode.AUTOMATION) || state.getMode().equals(Mode.CORRECTION)) { documentService.writeAnnotationCas(aJCas, state.getDocument(), state.getUser(), true); } else if (state.getMode().equals(Mode.CURATION)) { curationDocumentService.writeCurationCas(aJCas, state.getDocument(), state.getUser(), true); } } /** * Scroll the window of visible annotations. * @param aForward * instead of centering on the sentence that had the last editor, just scroll down * one sentence. This is for forward-annotation mode. */ private void autoScroll(JCas jCas, boolean aForward) { AnnotatorState state = getModelObject(); if (aForward) { // Fetch the first sentence on screen Sentence sentence = selectByAddr(jCas, Sentence.class, state.getFirstVisibleSentenceAddress()); // Find the following one int address = getNextSentenceAddress(jCas, sentence); // Move to it state.setFirstVisibleSentence(selectByAddr(jCas, Sentence.class, address)); } else { // Fetch the first sentence on screen Sentence sentence = selectByAddr(jCas, Sentence.class, state.getFirstVisibleSentenceAddress()); // Calculate the first sentence in the window in such a way that the annotation // currently selected is in the center of the window sentence = findWindowStartCenteringOnSelection(jCas, sentence, state.getSelection().getBegin(), state.getProject(), state.getDocument(), state.getPreferences().getWindowSize()); // Move to it state.setFirstVisibleSentence(sentence); } } @SuppressWarnings("unchecked") public void setSlot(AjaxRequestTarget aTarget, JCas aJCas, int aAnnotationId) { AnnotatorState state = getModelObject(); // Set an armed slot if (!state.getSelection().isRelationAnno() && state.isSlotArmed()) { List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) state.getFeatureState(state .getArmedFeature()).value; LinkWithRoleModel link = links.get(state.getArmedSlot()); link.targetAddr = aAnnotationId; link.label = selectByAddr(aJCas, aAnnotationId).getCoveredText(); state.clearArmedSlot(); } // Auto-commit if working on existing annotation if (state.getSelection().getAnnotation().isSet()) { try { actionAnnotate(aTarget, aJCas, false); } catch (Exception e) { handleException(this, aTarget, e); } } } public void loadFeatureEditorModels(AjaxRequestTarget aTarget) throws AnnotationException { try { JCas annotationCas = getEditorCas(); loadFeatureEditorModels(annotationCas, aTarget); } catch (AnnotationException e) { throw e; } catch (Exception e) { throw new AnnotationException(e); } } public void loadFeatureEditorModels(JCas aJCas, AjaxRequestTarget aTarget) throws AnnotationException { LOG.trace(String.format("loadFeatureEditorModels()")); try { AnnotatorState state = getModelObject(); Selection selection = state.getSelection(); if (!selection.isRelationAnno()) { annotationFeatureForm.updateLayersDropdown(); } if (selection.getAnnotation().isSet()) { // If an existing annotation was selected, take the feature editor model values from // there AnnotationFS annoFs = selectByAddr(aJCas, state.getSelection().getAnnotation() .getId()); // Try obtaining the layer from the feature structure AnnotationLayer layer; try { layer = TypeUtil.getLayer(annotationService, state.getProject(), annoFs); state.setSelectedAnnotationLayer(layer); LOG.trace(String.format( "loadFeatureEditorModels() selectedLayer set from selection: %s", state.getSelectedAnnotationLayer().getUiName())); } catch (NoResultException e) { clearFeatureEditorModels(aTarget); throw new IllegalStateException( "Unknown layer [" + annoFs.getType().getName() + "]", e); } // If remember layer is off, then the current layer follows the selected annotations // This is only relevant for span annotations because we only have these in the // dropdown - relation annotations are automatically determined based on the // selected span annotation if (!selection.isRelationAnno() && !state.getPreferences().isRememberLayer()) { state.setSelectedAnnotationLayer(layer); } loadFeatureEditorModelsCommon(aTarget, aJCas, layer, annoFs, null); } else { // If a new annotation is being created, populate the feature editors from the // remembered values (if any) if (selection.isRelationAnno()) { // Avoid creation of arcs on locked layers if (state.getSelectedAnnotationLayer() != null && state.getSelectedAnnotationLayer().isReadonly()) { state.setSelectedAnnotationLayer(new AnnotationLayer()); } else { loadFeatureEditorModelsCommon(aTarget, aJCas, state.getSelectedAnnotationLayer(), null, state.getRememberedArcFeatures()); } } else { loadFeatureEditorModelsCommon(aTarget, aJCas, state.getSelectedAnnotationLayer(), null, state.getRememberedSpanFeatures()); } } annotationFeatureForm.updateRememberLayer(); if (aTarget != null) { aTarget.add(annotationFeatureForm); } } catch (Exception e) { throw new AnnotationException(e); } } private void loadFeatureEditorModelsCommon(AjaxRequestTarget aTarget, JCas aJCas, AnnotationLayer aLayer, FeatureStructure aFS, Map<AnnotationFeature, Serializable> aRemembered) { clearFeatureEditorModels(aTarget); AnnotatorState state = AnnotationDetailEditorPanel.this.getModelObject(); // Populate from feature structure for (AnnotationFeature feature : annotationService.listAnnotationFeature(aLayer)) { if (!feature.isEnabled()) { continue; } Serializable value = null; if (aFS != null) { value = (Serializable) WebAnnoCasUtil.getFeature(aFS, feature); } else if (aRemembered != null) { value = aRemembered.get(feature); } FeatureState featureState = null; if (WebAnnoConst.CHAIN_TYPE.equals(feature.getLayer().getType())) { if (state.getSelection().isRelationAnno()) { if (feature.getLayer().isLinkedListBehavior() && WebAnnoConst.COREFERENCE_RELATION_FEATURE.equals(feature .getName())) { featureState = new FeatureState(feature, value); } } else { if (WebAnnoConst.COREFERENCE_TYPE_FEATURE.equals(feature.getName())) { featureState = new FeatureState(feature, value); } } } else { featureState = new FeatureState(feature, value); } if (featureState != null) { state.getFeatureStates().add(featureState); // verification to check whether constraints exist for this project or NOT if (state.getConstraints() != null && state.getSelection().getAnnotation().isSet()) { // indicator.setRulesExist(true); populateTagsBasedOnRules(aJCas, featureState); } else { // indicator.setRulesExist(false); featureState.tagset = annotationService.listTags(featureState.feature.getTagset()); } } } } private void writeFeatureEditorModelsToCas(TypeAdapter aAdapter, JCas aJCas) throws IOException { AnnotatorState state = getModelObject(); List<FeatureState> featureStates = state.getFeatureStates(); LOG.trace(String.format("writeFeatureEditorModelsToCas()")); List<AnnotationFeature> features = new ArrayList<AnnotationFeature>(); for (FeatureState featureState : featureStates) { features.add(featureState.feature); // For string features with extensible tagsets, extend the tagset if (CAS.TYPE_NAME_STRING.equals(featureState.feature.getType())) { String value = (String) featureState.value; if (value != null && featureState.feature.getTagset() != null && featureState.feature.getTagset().isCreateTag() && !annotationService.existsTag(value, featureState.feature.getTagset())) { Tag selectedTag = new Tag(); selectedTag.setName(value); selectedTag.setTagSet(featureState.feature.getTagset()); annotationService.createTag(selectedTag); } } LOG.trace(String.format("writeFeatureEditorModelsToCas() " + featureState.feature.getUiName() + " = " + featureState.value)); aAdapter.updateFeature(aJCas, featureState.feature, state.getSelection().getAnnotation().getId(), featureState.value); } // Generate info message if (state.getSelection().getAnnotation().isSet()) { String bratLabelText = TypeUtil.getUiLabelText(aAdapter, selectByAddr(aJCas, state.getSelection().getAnnotation().getId()), features); info(generateMessage(state.getSelectedAnnotationLayer(), bratLabelText, false)); } } protected void onChange(AjaxRequestTarget aTarget) { // Overriden in CurationPanel } protected void onAutoForward(AjaxRequestTarget aTarget) { // Overriden in CurationPanel } protected void onAnnotate(AjaxRequestTarget aTarget) { // Overriden in AutomationPage } protected void onDelete(AjaxRequestTarget aTarget, AnnotationFS aFs) { // Overriden in AutomationPage } @SuppressWarnings("unchecked") public IModel<AnnotatorState> getModel() { return (IModel<AnnotatorState>) getDefaultModel(); } public AnnotatorState getModelObject() { return (AnnotatorState) getDefaultModelObject(); } /** * Clear the values from the feature editors. */ private void clearFeatureEditorModels(AjaxRequestTarget aTarget) { LOG.trace(String.format("clearFeatureEditorModels()")); getModelObject().getFeatureStates().clear(); if (aTarget != null) { aTarget.add(annotationFeatureForm); } } /** * Adds and sorts tags based on Constraints rules */ private void populateTagsBasedOnRules(JCas aJCas, FeatureState aModel) { LOG.trace(String .format("populateTagsBasedOnRules(feature: " + aModel.feature.getUiName() + ")")); AnnotatorState state = getModelObject(); // Add values from rules String restrictionFeaturePath; switch (aModel.feature.getLinkMode()) { case WITH_ROLE: restrictionFeaturePath = aModel.feature.getName() + "." + aModel.feature.getLinkTypeRoleFeatureName(); break; case NONE: restrictionFeaturePath = aModel.feature.getName(); break; default: throw new IllegalArgumentException("Unsupported link mode [" + aModel.feature.getLinkMode() + "] on feature [" + aModel.feature.getName() + "]"); } aModel.indicator.reset(); // Fetch possible values from the constraint rules List<PossibleValue> possibleValues; try { FeatureStructure featureStructure = selectByAddr(aJCas, state.getSelection() .getAnnotation().getId()); Evaluator evaluator = new ValuesGenerator(); //Only show indicator if this feature can be affected by Constraint rules! aModel.indicator.setAffected(evaluator.isThisAffectedByConstraintRules( featureStructure, restrictionFeaturePath, state.getConstraints())); possibleValues = evaluator.generatePossibleValues( featureStructure, restrictionFeaturePath, state.getConstraints()); LOG.debug("Possible values for [" + featureStructure.getType().getName() + "] [" + restrictionFeaturePath + "]: " + possibleValues); } catch (Exception e) { error("Unable to evaluate constraints: " + ExceptionUtils.getRootCauseMessage(e)); LOG.error("Unable to evaluate constraints: " + e.getMessage(), e); possibleValues = new ArrayList<>(); } // Fetch actual tagset List<Tag> valuesFromTagset = annotationService.listTags(aModel.feature.getTagset()); // First add tags which are suggested by rules and exist in tagset List<Tag> tagset = compareSortAndAdd(possibleValues, valuesFromTagset, aModel.indicator); // Then add the remaining tags for (Tag remainingTag : valuesFromTagset) { if (!tagset.contains(remainingTag)) { tagset.add(remainingTag); } } // Record the possible values and the (re-ordered) tagset in the feature state aModel.possibleValues = possibleValues; aModel.tagset = tagset; } /* * Compares existing tagset with possible values resulted from rule evaluation Adds only which * exist in tagset and is suggested by rules. The remaining values from tagset are added * afterwards. */ private static List<Tag> compareSortAndAdd(List<PossibleValue> possibleValues, List<Tag> valuesFromTagset, RulesIndicator rulesIndicator) { //if no possible values, means didn't satisfy conditions if(possibleValues.isEmpty()) { rulesIndicator.didntMatchAnyRule(); } List<Tag> returnList = new ArrayList<Tag>(); // Sorting based on important flag // possibleValues.sort(null); // Comparing to check which values suggested by rules exists in existing // tagset and adding them first in list. for (PossibleValue value : possibleValues) { for (Tag tag : valuesFromTagset) { if (value.getValue().equalsIgnoreCase(tag.getName())) { //Matching values found in tagset and shown in dropdown rulesIndicator.rulesApplied(); // HACK BEGIN tag.setReordered(true); // HACK END //Avoid duplicate entries if(!returnList.contains(tag)){ returnList.add(tag); } } } } //If no matching tags found if(returnList.isEmpty()){ rulesIndicator.didntMatchAnyTag(); } return returnList; } public void reset(AjaxRequestTarget aTarget) { AnnotatorState state = getModelObject(); state.getSelection().clear(); state.getSelection().setBegin(0); state.getSelection().setEnd(0); clearFeatureEditorModels(aTarget); } /** * remove this model, if new annotation is to be created */ public void clearArmedSlotModel() { List<FeatureState> featureStates = getModelObject().getFeatureStates(); for (FeatureState featureState : featureStates) { if (StringUtils.isNotBlank(featureState.feature.getLinkTypeName())) { featureState.value = new ArrayList<>(); } } } private Set<AnnotationFS> getAttachedRels(JCas aJCas, AnnotationFS aFs, AnnotationLayer aLayer) throws UIMAException, ClassNotFoundException, IOException { Set<AnnotationFS> toBeDeleted = new HashSet<AnnotationFS>(); for (AnnotationLayer relationLayer : annotationService .listAttachedRelationLayers(aLayer)) { ArcAdapter relationAdapter = (ArcAdapter) getAdapter(annotationService, relationLayer); Type relationType = CasUtil.getType(aJCas.getCas(), relationLayer.getName()); Feature sourceFeature = relationType.getFeatureByBaseName(relationAdapter .getSourceFeatureName()); Feature targetFeature = relationType.getFeatureByBaseName(relationAdapter .getTargetFeatureName()); // This code is already prepared for the day that relations can go between // different layers and may have different attach features for the source and // target layers. Feature relationSourceAttachFeature = null; Feature relationTargetAttachFeature = null; if (relationAdapter.getAttachFeatureName() != null) { relationSourceAttachFeature = sourceFeature.getRange().getFeatureByBaseName( relationAdapter.getAttachFeatureName()); relationTargetAttachFeature = targetFeature.getRange().getFeatureByBaseName( relationAdapter.getAttachFeatureName()); } for (AnnotationFS relationFS : CasUtil.select(aJCas.getCas(), relationType)) { // Here we get the annotations that the relation is pointing to in the UI FeatureStructure sourceFS; if (relationSourceAttachFeature != null) { sourceFS = relationFS.getFeatureValue(sourceFeature).getFeatureValue( relationSourceAttachFeature); } else { sourceFS = relationFS.getFeatureValue(sourceFeature); } FeatureStructure targetFS; if (relationTargetAttachFeature != null) { targetFS = relationFS.getFeatureValue(targetFeature).getFeatureValue( relationTargetAttachFeature); } else { targetFS = relationFS.getFeatureValue(targetFeature); } if (isSame(sourceFS, aFs) || isSame(targetFS, aFs)) { toBeDeleted.add(relationFS); LOG.debug("Deleted relation [" + getAddr(relationFS) + "] from layer [" + relationLayer.getName() + "]"); } } } return toBeDeleted; } public AnnotationFeatureForm getAnnotationFeatureForm() { return annotationFeatureForm; } public Label getSelectedAnnotationLayer() { return annotationFeatureForm.selectedAnnotationLayer; } private boolean isForwardable() { AnnotatorState state = AnnotationDetailEditorPanel.this.getModelObject(); AnnotationLayer selectedLayer = state.getSelectedAnnotationLayer(); if (selectedLayer == null) { return false; } if (selectedLayer.getId() <= 0) { return false; } if (!selectedLayer.getType().equals(WebAnnoConst.SPAN_TYPE)) { return false; } if (!selectedLayer.isLockToTokenOffset()) { return false; } // no forward annotation for multifeature layers. if (annotationService.listAnnotationFeature(selectedLayer).size() > 1) { return false; } // if there are no features at all, no forward annotation if (annotationService.listAnnotationFeature(selectedLayer).isEmpty()) { return false; } // we allow forward annotation only for a feature with a tagset if (annotationService.listAnnotationFeature(selectedLayer).get(0).getTagset() == null) { return false; } // there should be at least one tag in the tagset TagSet tagSet = annotationService.listAnnotationFeature(selectedLayer).get(0).getTagset(); if (annotationService.listTags(tagSet).size() == 0) { return false; } return true; } public static void handleException(Component aComponent, AjaxRequestTarget aTarget, Exception aException) { try { throw aException; } catch (AnnotationException e) { if (aTarget != null) { aTarget.prependJavaScript("alert('Error: " + e.getMessage() + "')"); } else { aComponent.error("Error: " + e.getMessage()); } LOG.error("Error: " + ExceptionUtils.getRootCauseMessage(e), e); } catch (UIMAException e) { aComponent.error("Error: " + ExceptionUtils.getRootCauseMessage(e)); LOG.error("Error: " + ExceptionUtils.getRootCauseMessage(e), e); } catch (Exception e) { aComponent.error("Error: " + e.getMessage()); LOG.error("Error: " + e.getMessage(), e); } } private static String generateMessage(AnnotationLayer aLayer, String aLabel, boolean aDeleted) { String action = aDeleted ? "deleted" : "created/updated"; String msg = "The [" + aLayer.getUiName() + "] annotation has been " + action + "."; if (StringUtils.isNotBlank(aLabel)) { msg += " Label: [" + aLabel + "]"; } return msg; } }