/* * 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.editor; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.form.AjaxButton; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.AbstractTextComponent; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.panel.FeedbackPanel; 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.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; import com.googlecode.wicket.jquery.core.Options; import com.googlecode.wicket.jquery.ui.widget.tooltip.TooltipBehavior; import com.googlecode.wicket.kendo.ui.KendoUIBehavior; import com.googlecode.wicket.kendo.ui.form.TextField; import com.googlecode.wicket.kendo.ui.form.combobox.ComboBoxBehavior; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; 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.constraints.evaluator.PossibleValue; import de.tudarmstadt.ukp.clarin.webanno.model.Tag; import de.tudarmstadt.ukp.clarin.webanno.support.DescriptionTooltipBehavior; import de.tudarmstadt.ukp.clarin.webanno.support.StyledComboBox; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.AnnotationDetailEditorPanel; public class LinkFeatureEditor extends FeatureEditor { private static final Logger LOG = LoggerFactory.getLogger(LinkFeatureEditor.class); private static final long serialVersionUID = 7469241620229001983L; private @SpringBean AnnotationSchemaService annotationService; private WebMarkupContainer content; // // For showing the status of Constraints rules kicking in. // private RulesIndicator indicator = new RulesIndicator(); @SuppressWarnings("rawtypes") private final AbstractTextComponent field; private boolean hideUnconstraintFeature; private IModel<AnnotatorState> stateModel; private AnnotationDetailEditorPanel owner; private String newRole; @SuppressWarnings("unchecked") public LinkFeatureEditor(String aId, String aMarkupId, AnnotationDetailEditorPanel aOwner, final IModel<FeatureState> aFeatureStateModel) { super(aId, aMarkupId, aOwner.getAnnotationFeatureForm(), CompoundPropertyModel.of(aFeatureStateModel)); stateModel = aOwner.getModel(); owner = aOwner; // Checks whether hide un-constraint feature is enabled or not hideUnconstraintFeature = getModelObject().feature.isHideUnconstraintFeature(); add(new Label("feature", getModelObject().feature.getUiName())); // Most of the content is inside this container such that we can refresh it independently // from the rest of the form content = new WebMarkupContainer("content"); content.setOutputMarkupId(true); add(content); content.add(new RefreshingView<LinkWithRoleModel>("slots", PropertyModel.of(getModel(), "value")) { private static final long serialVersionUID = 5475284956525780698L; @Override protected Iterator<IModel<LinkWithRoleModel>> getItemModels() { ModelIteratorAdapter<LinkWithRoleModel> i = new ModelIteratorAdapter<LinkWithRoleModel>( (List<LinkWithRoleModel>) LinkFeatureEditor.this.getModelObject().value) { @Override protected IModel<LinkWithRoleModel> model(LinkWithRoleModel aObject) { return Model.of(aObject); } }; return i; } @Override protected void populateItem(final Item<LinkWithRoleModel> aItem) { AnnotatorState state = stateModel.getObject(); aItem.setModel( new CompoundPropertyModel<LinkWithRoleModel>(aItem.getModelObject())); Label role = new Label("role"); aItem.add(role); final Label label; if (aItem.getModelObject().targetAddr == -1 && state.isArmedSlot(getModelObject().feature, aItem.getIndex())) { label = new Label("label", "<Select to fill>"); } else { label = new Label("label"); } label.add(new AjaxEventBehavior("click") { private static final long serialVersionUID = 7633309278417475424L; @Override protected void onEvent(AjaxRequestTarget aTarget) { actionToggleArmedState(aTarget, aItem); } }); label.add(new AttributeAppender("style", new Model<String>() { private static final long serialVersionUID = 1L; @Override public String getObject() { if (state.isArmedSlot(getModelObject().feature, aItem.getIndex())) { return "; background: orange"; } else { return ""; } } })); aItem.add(label); } }); if (getModelObject().feature.getTagset() != null) { field = new StyledComboBox<Tag>("newRole", PropertyModel.of(this, "newRole"), PropertyModel.of(getModel(), "tagset")) { private static final long serialVersionUID = 1L; @Override protected void onInitialize() { super.onInitialize(); // Ensure proper order of the initializing JS header items: first combo box // behavior (in super.onInitialize()), then tooltip. Options options = new Options(DescriptionTooltipBehavior.makeTooltipOptions()); options.set("content", AnnotationDetailEditorPanel.FUNCTION_FOR_TOOLTIP); add(new TooltipBehavior("#" + field.getMarkupId() + "_listbox *[title]", options) { private static final long serialVersionUID = -7207021885475073279L; @Override protected String $() { // REC: It takes a moment for the KendoDatasource to load the data and // for the Combobox to render the hidden dropdown. I did not find // a way to hook into this process and to get notified when the // data is available in the dropdown, so trying to handle this // with a slight delay hopeing that all is set up after 1 second. return "try {setTimeout(function () { " + super.$() + " }, 1000); } catch (err) {}; "; } }); } @Override protected void onConfigure() { super.onConfigure(); // If a slot is armed, then load the slot's role into the dropdown AnnotatorState state = stateModel.getObject(); if (state.isSlotArmed() && LinkFeatureEditor.this.getModelObject().feature .equals(state.getArmedFeature())) { List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this .getModelObject().value; setModelObject(links.get(state.getArmedSlot()).role); } else { setModelObject(""); } // Trigger a re-loading of the tagset from the server as constraints may have // changed the ordering AjaxRequestTarget target = RequestCycle.get().find(AjaxRequestTarget.class); if (target != null) { LOG.trace("onInitialize() requesting datasource re-reading"); target.appendJavaScript( String.format("var $w = %s; if ($w) { $w.dataSource.read(); }", KendoUIBehavior.widget(this, ComboBoxBehavior.METHOD))); } } }; // Ensure that markup IDs of feature editor focus components remain constant across // refreshs of the feature editor panel. This is required to restore the focus. field.setOutputMarkupId(true); field.setMarkupId(ID_PREFIX + getModelObject().feature.getId()); content.add(field); } else { content.add(field = new TextField<String>("newRole", PropertyModel.of(this, "newRole")) { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { super.onConfigure(); AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); FeatureState featureState = LinkFeatureEditor.this.getModelObject(); if (state.isSlotArmed() && featureState.feature.equals(state.getArmedFeature())) { List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) featureState.value; setModelObject(links.get(state.getArmedSlot()).role); } else { setModelObject(""); } } }); } // Shows whether constraints are triggered or not // also shows state of constraints use. Component constraintsInUseIndicator = new WebMarkupContainer("linkIndicator") { private static final long serialVersionUID = 4346767114287766710L; @Override public boolean isVisible() { return getModelObject().indicator.isAffected(); } }.add(new AttributeAppender("class", new Model<String>() { private static final long serialVersionUID = -7683195283137223296L; @Override public String getObject() { // adds symbol to indicator return getModelObject().indicator.getStatusSymbol(); } })).add(new AttributeAppender("style", new Model<String>() { private static final long serialVersionUID = -5255873539738210137L; @Override public String getObject() { // adds color to indicator return "; color: " + getModelObject().indicator.getStatusColor(); } })); add(constraintsInUseIndicator); // Add a new empty slot with the specified role content.add(new AjaxButton("add") { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); setVisible(!(state.isSlotArmed() && LinkFeatureEditor.this.getModelObject().feature .equals(state.getArmedFeature()))); // setEnabled(!(model.isSlotArmed() // && aModel.feature.equals(model.getArmedFeature()))); } @Override protected void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { actionAdd(aTarget); } }); // Allows user to update slot content.add(new AjaxButton("set") { private static final long serialVersionUID = 7923695373085126646L; @Override protected void onConfigure() { AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); setVisible(state.isSlotArmed() && LinkFeatureEditor.this.getModelObject().feature .equals(state.getArmedFeature())); // setEnabled(model.isSlotArmed() // && aModel.feature.equals(model.getArmedFeature())); } @Override protected void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { actionSet(aTarget); } }); // Add a new empty slot with the specified role content.add(new AjaxButton("del") { private static final long serialVersionUID = 1L; @Override protected void onConfigure() { AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); setVisible(state.isSlotArmed() && LinkFeatureEditor.this.getModelObject().feature .equals(state.getArmedFeature())); // setEnabled(model.isSlotArmed() // && aModel.feature.equals(model.getArmedFeature())); } @Override protected void onSubmit(AjaxRequestTarget aTarget, Form<?> aForm) { actionDel(aTarget); } }); } private void removeAutomaticallyAddedUnusedEntries() { // Remove unused (but auto-added) tags. @SuppressWarnings("unchecked") List<LinkWithRoleModel> list = (List<LinkWithRoleModel>) LinkFeatureEditor.this .getModelObject().value; Iterator<LinkWithRoleModel> existingLinks = list.iterator(); while (existingLinks.hasNext()) { LinkWithRoleModel link = existingLinks.next(); if (link.autoCreated && link.targetAddr == -1) { // remove it existingLinks.remove(); } } } private void autoAddImportantTags(List<Tag> aTagset, List<PossibleValue> aPossibleValues) { if (aTagset == null || aTagset.isEmpty() || aPossibleValues == null || aPossibleValues.isEmpty()) { return; } // Construct a quick index for tags Set<String> tagset = new HashSet<String>(); for (Tag t : aTagset) { tagset.add(t.getName()); } // Get links list and build role index @SuppressWarnings("unchecked") List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) getModelObject().value; Set<String> roles = new HashSet<String>(); for (LinkWithRoleModel l : links) { roles.add(l.role); } // Loop over values to see which of the tags are important and add them. for (PossibleValue value : aPossibleValues) { if (!value.isImportant() || !tagset.contains(value.getValue())) { continue; } // Check if there is already a slot with the given name if (roles.contains(value.getValue())) { continue; } // Add empty slot in UI with that name. LinkWithRoleModel m = new LinkWithRoleModel(); m.role = value.getValue(); // Marking so that can be ignored later. m.autoCreated = true; links.add(m); // NOT arming the slot here! } } @Override public Component getFocusComponent() { return field; } /** * Hides feature if "Hide un-constraint feature" is enabled and constraint rules are applied and * feature doesn't match any constraint rule */ @Override public void onConfigure() { // Update entries for important tags. removeAutomaticallyAddedUnusedEntries(); FeatureState featureState = getModelObject(); autoAddImportantTags(featureState.tagset, featureState.possibleValues); // if enabled and constraints rule execution returns anything other than green setVisible(!hideUnconstraintFeature || (getModelObject().indicator.isAffected() && getModelObject().indicator.getStatusColor().equals("green"))); } private void actionAdd(AjaxRequestTarget aTarget) { if (StringUtils.isBlank((String) field.getModelObject())) { error("Must set slot label before adding!"); aTarget.addChildren(getPage(), FeedbackPanel.class); } else { @SuppressWarnings("unchecked") List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this .getModelObject().value; AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); LinkWithRoleModel m = new LinkWithRoleModel(); m.role = (String) field.getModelObject(); links.add(m); state.setArmedSlot(LinkFeatureEditor.this.getModelObject().feature, links.size() - 1); // Need to re-render the whole form because a slot in another // link editor might get unarmed aTarget.add(owner.getAnnotationFeatureForm()); } } private void actionSet(AjaxRequestTarget aTarget) { @SuppressWarnings("unchecked") List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this .getModelObject().value; AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); // Update the slot LinkWithRoleModel m = links.get(state.getArmedSlot()); m.role = (String) field.getModelObject(); links.set(state.getArmedSlot(), m); // avoid reordering aTarget.add(content); // Commit change try { owner.actionAnnotate(aTarget); } catch (Exception e) { AnnotationDetailEditorPanel.handleException(this, aTarget, e); } } private void actionDel(AjaxRequestTarget aTarget) { @SuppressWarnings("unchecked") List<LinkWithRoleModel> links = (List<LinkWithRoleModel>) LinkFeatureEditor.this .getModelObject().value; AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); links.remove(state.getArmedSlot()); state.clearArmedSlot(); aTarget.add(content); // Auto-commit if working on existing annotation if (state.getSelection().getAnnotation().isSet()) { try { owner.actionAnnotate(aTarget); } catch (Exception e) { AnnotationDetailEditorPanel.handleException(this, aTarget, e); } } } private void actionToggleArmedState(AjaxRequestTarget aTarget, Item<LinkWithRoleModel> aItem) { AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); if (state.isArmedSlot(getModelObject().feature, aItem.getIndex())) { state.clearArmedSlot(); aTarget.add(content); } else { state.setArmedSlot(getModelObject().feature, aItem.getIndex()); // Need to re-render the whole form because a slot in another // link editor might get unarmed aTarget.add(owner.getAnnotationFeatureForm()); } } }