/* * 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.brat.annotation; import java.io.IOException; import java.util.Locale; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.jcas.JCas; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AbstractAjaxBehavior; import org.apache.wicket.markup.head.CssContentHeaderItem; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.model.IModel; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.cycle.AbstractRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import org.springframework.security.core.context.SecurityContextHolder; import com.googlecode.wicket.jquery.ui.resource.JQueryUIResourceReference; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.JCasProvider; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotationPreference; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.VID; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.brat.message.ArcAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.GetCollectionInformationResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.GetDocumentResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.LoadConfResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.SpanAnnotationResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.message.WhoamiResponse; import de.tudarmstadt.ukp.clarin.webanno.brat.render.BratRenderer; import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.Offsets; import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.OffsetsList; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratAjaxResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratAnnotatorUiResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratConfigurationResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratCssUiReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratCssVisReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratDispatcherResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratUtilResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratVisualizerResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.BratVisualizerUiResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.JQueryJsonResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.JQuerySvgDomResourceReference; import de.tudarmstadt.ukp.clarin.webanno.brat.resource.JQuerySvgResourceReference; import de.tudarmstadt.ukp.clarin.webanno.support.JSONUtil; /** * Brat annotator component. */ public class BratAnnotationEditor extends AnnotationEditorBase { private static final Logger LOG = LoggerFactory.getLogger(BratAnnotationEditor.class); private static final long serialVersionUID = -1537506294440056609L; private static final String PARAM_ACTION = "action"; private static final String PARAM_ARC_ID = "arcId"; private static final String PARAM_ID = "id"; private static final String PARAM_OFFSETS = "offsets"; private static final String PARAM_TARGET_SPAN_ID = "targetSpanId"; private static final String PARAM_ORIGIN_SPAN_ID = "originSpanId"; private static final String PARAM_TARGET_TYPE = "targetType"; private static final String PARAM_ORIGIN_TYPE = "originType"; private @SpringBean AnnotationSchemaService annotationService; private WebMarkupContainer vis; private AbstractAjaxBehavior requestHandler; public BratAnnotationEditor(String id, IModel<AnnotatorState> aModel, final AnnotationActionHandler aActionHandler, final JCasProvider aJCasProvider) { super(id, aModel, aActionHandler, aJCasProvider); vis = new WebMarkupContainer("vis"); vis.setOutputMarkupId(true); add(vis); requestHandler = new AbstractDefaultAjaxBehavior() { private static final long serialVersionUID = 1L; @Override protected void respond(AjaxRequestTarget aTarget) { long timerStart = System.currentTimeMillis(); // We always refresh the feedback panel - only doing this in the case were actually // something worth reporting occurs is too much of a hassel... aTarget.addChildren(getPage(), FeedbackPanel.class); final IRequestParameters request = getRequest().getPostParameters(); // Get action from the request String action = request.getParameterValue(PARAM_ACTION).toString(); LOG.info("AJAX-RPC CALLED: [{}]", action); // Parse annotation ID if present in request VID paramId; if (!request.getParameterValue(PARAM_ID).isEmpty() && !request.getParameterValue(PARAM_ARC_ID).isEmpty()) { throw new IllegalStateException( "[id] and [arcId] cannot be both set at the same time!"); } else if (!request.getParameterValue(PARAM_ID).isEmpty()) { paramId = VID.parseOptional(request.getParameterValue(PARAM_ID).toString()); } else { paramId = VID.parseOptional(request.getParameterValue(PARAM_ARC_ID).toString()); } // Record the action in the action context (which currently is persistent...) getModelObject().getAction().setUserAction(action); // Ensure that the user action is cleared *AFTER* rendering so that for AJAX // calls that do not go through this AjaxBehavior do not see an active user action. RequestCycle.get().getListeners().add(new AbstractRequestCycleListener() { @Override public void onEndRequest(RequestCycle aCycle) { BratAnnotationEditor.this.getModelObject().getAction().clearUserAction(); } }); // Load the CAS if necessary // Make sure we load the CAS only once here in case of an annotation action. boolean requiresCasLoading = SpanAnnotationResponse.is(action) || ArcAnnotationResponse.is(action) || GetDocumentResponse.is(action); JCas jCas = null; if (requiresCasLoading) { try { jCas = getJCasProvider().get(); } catch (Exception e) { LOG.error("Unable to load data", e); error("Unable to load data: " + ExceptionUtils.getRootCauseMessage(e)); return; } } // HACK: If an arc was clicked that represents a link feature, then open the // associated span annotation instead. if (paramId.isSlotSet() && ArcAnnotationResponse.is(action)) { action = SpanAnnotationResponse.COMMAND; paramId = new VID(paramId.getId()); } // Doing anything but a span annotation when a slot is armed will unarm it if (getModelObject().isSlotArmed() && !SpanAnnotationResponse.is(action)) { getModelObject().clearArmedSlot(); } Object result = null; try { if (WhoamiResponse.is(action)) { result = new WhoamiResponse( SecurityContextHolder.getContext().getAuthentication().getName()); } else if (SpanAnnotationResponse.is(action)) { Offsets offsets = getOffsetsFromRequest(request, jCas, paramId); getActionHandler().actionSpanAnnotation(aTarget, jCas, offsets.getBegin(), offsets.getEnd(), paramId); result = new SpanAnnotationResponse(); } else if (ArcAnnotationResponse.is(action)) { String originType = request.getParameterValue(PARAM_ORIGIN_TYPE).toString(); int originSpanId = request.getParameterValue(PARAM_ORIGIN_SPAN_ID).toInt(); String targetType = request.getParameterValue(PARAM_TARGET_TYPE).toString(); int targetSpanId = request.getParameterValue(PARAM_TARGET_SPAN_ID).toInt(); getActionHandler().actionArcAnnotation(aTarget, jCas, paramId, originType, originSpanId, targetType, targetSpanId); result = new ArcAnnotationResponse(); } else if (LoadConfResponse.is(action)) { result = new LoadConfResponse(); } else if (GetCollectionInformationResponse.is(action)) { GetCollectionInformationResponse info = new GetCollectionInformationResponse(); if (getModelObject().getProject() != null) { info.setEntityTypes(BratRenderer.buildEntityTypes( getModelObject().getAnnotationLayers(), annotationService)); } result = info; } else if (GetDocumentResponse.is(action)) { GetDocumentResponse response = new GetDocumentResponse(); if (getModelObject().getProject() != null) { BratRenderer.render(response, getModelObject(), jCas, annotationService); } result = response; } } catch (ClassNotFoundException e) { LOG.error("Invalid reader: " + e.getMessage(), e); error("Invalid reader: " + e.getMessage()); } catch (Exception e) { error("Error: " + e.getMessage()); LOG.error("Error: " + e.getMessage(), e); } // Serialize updated document to JSON if (result == null) { LOG.warn("AJAX-RPC: Action [{}] produced no result!", action); } else { String json = toJson(result); // Since we cannot pass the JSON directly to Brat, we attach it to the HTML // element into which BRAT renders the SVG. In our modified ajax.js, we pick it // up from there and then pass it on to BRAT to do the rendering. aTarget.prependJavaScript("Wicket.$('" + vis.getMarkupId() + "').temp = " + json + ";"); } LOG.info("AJAX-RPC DONE: [{}] completed in {}ms", action, (System.currentTimeMillis() - timerStart)); } }; add(requestHandler); } /** * Extract offset information from the current request. These are either offsets of an existing * selected annotations or offsets contained in the request for the creation of a new * annotation. */ private Offsets getOffsetsFromRequest(IRequestParameters request, JCas jCas, VID aVid) throws IOException { if (aVid.isNotSet()) { // Create new span annotation - in this case we get the offset information from the // request String offsets = request.getParameterValue(PARAM_OFFSETS).toString(); OffsetsList offsetLists = JSONUtil.getJsonConverter().getObjectMapper() .readValue(offsets, OffsetsList.class); int annotationBegin = getModelObject().getWindowBeginOffset() + offsetLists.get(0).getBegin(); int annotationEnd = getModelObject().getWindowBeginOffset() + offsetLists.get(offsetLists.size() - 1).getEnd(); return new Offsets(annotationBegin, annotationEnd); } else { // Edit existing span annotation - in this case we look up the offsets in the CAS // Let's not trust the client in this case. AnnotationFS fs = WebAnnoCasUtil.selectByAddr(jCas, aVid.getId()); return new Offsets(fs.getBegin(), fs.getEnd()); } } @Override protected void onConfigure() { super.onConfigure(); setVisible(getModelObject() != null && getModelObject().getProject() != null); } @Override public void renderHead(IHeaderResponse aResponse) { super.renderHead(aResponse); // CSS aResponse.render(CssHeaderItem.forReference(BratCssVisReference.get())); aResponse.render(CssHeaderItem.forReference(BratCssUiReference.get())); // Override CSS double textFontSize = getModelObject().getPreferences().getFontSize(); double spanFontSize = 10 * (textFontSize / (float) AnnotationPreference.FONT_SIZE_DEFAULT); double arcFontSize = 9 * (textFontSize / (float) AnnotationPreference.FONT_SIZE_DEFAULT); aResponse.render(CssContentHeaderItem.forCSS(String.format(Locale.US, ".span text { font-size: %.1fpx; }\n" + ".arcs text { font-size: %.1fpx; }\n" + "text { font-size: %.1fpx; }\n", spanFontSize, arcFontSize, textFontSize), "brat-font")); // Libraries aResponse.render(JavaScriptHeaderItem.forReference(JQueryUIResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(JQuerySvgResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(JQuerySvgDomResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(JQueryJsonResourceReference.get())); // BRAT helpers aResponse.render(JavaScriptHeaderItem.forReference(BratConfigurationResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(BratUtilResourceReference.get())); //aResponse.render(JavaScriptHeaderItem.forReference(BratAnnotationLogResourceReference.get())); // BRAT modules aResponse.render(JavaScriptHeaderItem.forReference(BratDispatcherResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(BratAjaxResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(BratVisualizerResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(BratVisualizerUiResourceReference.get())); aResponse.render(JavaScriptHeaderItem.forReference(BratAnnotatorUiResourceReference.get())); //aResponse.render(JavaScriptHeaderItem.forReference(BratUrlMonitorResourceReference.get())); StringBuilder script = new StringBuilder(); // REC 2014-10-18 - For a reason that I do not understand, the dispatcher cannot be a local // variable. If I put a "var" here, then communication fails with messages such as // "action 'openSpanDialog' returned result of action 'loadConf'" in the browsers's JS // console. script.append("(function() {"); script.append("var dispatcher = new Dispatcher();"); // Each visualizer talks to its own Wicket component instance script.append("dispatcher.ajaxUrl = '" + requestHandler.getCallbackUrl() + "'; "); // We attach the JSON send back from the server to this HTML element // because we cannot directly pass it from Wicket to the caller in ajax.js. script.append("dispatcher.wicketId = '" + vis.getMarkupId() + "'; "); script.append("var ajax = new Ajax(dispatcher);"); script.append("var visualizer = new Visualizer(dispatcher, '" + vis.getMarkupId() + "');"); script.append("var visualizerUI = new VisualizerUI(dispatcher, visualizer.svg);"); script.append("var annotatorUI = new AnnotatorUI(dispatcher, visualizer.svg);"); //script.append("var logger = new AnnotationLog(dispatcher);"); script.append("dispatcher.post('init');"); script.append("Wicket.$('" + vis.getMarkupId() + "').dispatcher = dispatcher;"); script.append("Wicket.$('" + vis.getMarkupId() + "').visualizer = visualizer;"); script.append("})();"); // Must be OnDomReader so that this is rendered before all other Javascript that is // appended to the same AJAX request which turns the annotator visible after a document // has been chosen. aResponse.render(OnDomReadyHeaderItem.forScript(script.toString())); // If the page is reloaded in the browser and a document was already open, we need // to render it. We use the "later" commands here to avoid polluting the Javascript // header items with document data and because loading times are not that critical // on a reload. if (getModelObject().getProject() != null) { bratInitRenderLater(aResponse); } } // private String bratInitCommand() // { // GetCollectionInformationResponse response = new GetCollectionInformationResponse(); // response.setEntityTypes(BratRenderer.buildEntityTypes(getModelObject() // .getAnnotationLayers(), annotationService)); // String json = toJson(response); // return "Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('collectionLoaded', [" + json // + "]);"; // } // public void bratInit(AjaxRequestTarget aTarget) // { // aTarget.appendJavaScript(bratInitCommand()); // } private String bratRenderCommand(JCas aJCas) { LOG.info("BEGIN bratRenderCommand"); GetDocumentResponse response = new GetDocumentResponse(); BratRenderer.render(response, getModelObject(), aJCas, annotationService); String json = toJson(response); LOG.info("END bratRenderCommand"); return "Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('renderData', [" + json + "]);"; } /** * This triggers the loading of the metadata (colors, types, etc.) * * @return the init script. */ private String bratInitLaterCommand() { return "Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('ajax', " + "[{action: 'getCollectionInformation',collection: '" + getCollection() + "'}, 'collectionLoaded', {collection: '" + getCollection() + "',keep: true}]);"; } /** * This one triggers the loading of the actual document data * * @return brat */ private String bratRenderLaterCommand() { return "Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('current', " + "['" + getCollection() + "', '1234', {}, true]);"; } /** * Reload {@link BratAnnotationEditor} when the Correction/Curation page is opened * * @param aResponse * the response. */ private void bratInitRenderLater(IHeaderResponse aResponse) { aResponse.render(OnLoadHeaderItem.forScript(bratInitLaterCommand())); aResponse.render(OnLoadHeaderItem.forScript(bratRenderLaterCommand())); } /** * Render content in a separate request. * * @param aTarget * the AJAX target. */ @Override public void renderLater(AjaxRequestTarget aTarget) { aTarget.appendJavaScript(bratRenderLaterCommand()); } /** * Render content as part of the current request. * * @param aTarget * the AJAX target. * @param aJCas * the CAS to render. */ @Override public void render(AjaxRequestTarget aTarget, JCas aJCas) { aTarget.appendJavaScript(bratRenderCommand(aJCas)); } /** * Render content as part of the current request. * * @param aTarget * the AJAX target. * @param aAnnotationId * the annotation ID to highlight. */ @Override public void setHighlight(AjaxRequestTarget aTarget, VID aAnnotationId) { if (!aAnnotationId.isSet()) { aTarget.appendJavaScript("Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('current', " + "['" + getCollection() + "', '1234', {edited:[]}, false]);"); } else { aTarget.appendJavaScript("Wicket.$('" + vis.getMarkupId() + "').dispatcher.post('current', " + "['" + getCollection() + "', '1234', {edited:[[\"" + aAnnotationId + "\"]]}, false]);"); } } private String getCollection() { if (getModelObject().getProject() != null) { return "#" + getModelObject().getProject().getName() + "/"; } else { return ""; } } private String toJson(Object result) { String json = "[]"; try { json = JSONUtil.toInterpretableJsonString(result); } catch (IOException e) { error("Unable to produce JSON response " + ":" + ExceptionUtils.getRootCauseMessage(e)); } return json; } }