Skip to content

Commit

Permalink
Merge pull request #1560 from inception-project/bugfix/1559-Problems-…
Browse files Browse the repository at this point in the history
…if-multiple-entities-have-the-same-label

#1559 - Problems if multiple entities have the same label
  • Loading branch information
reckart authored Dec 12, 2019
2 parents 45b1be3 + 0bd1498 commit eef5d77
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
</span>
<input style="width: 100%;" wicket:id="value" class="form-control" type="text"/>
</div>
<div class="col-sm-12" style="position: relative; height: 5em;" wicket:enclosure="description">
<div class="form-control" style="overflow-y: auto; width: unset; word-wrap: break-word; height: 5em; position: absolute; top: 0; left: 0px; right: 0px;" readonly>
<span class="small" wicket:id="description"/>
</div>
</div>
<div wicket:id="keyBindings" class="col-sm-12"/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,48 @@
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.wicket.event.Broadcast.BUBBLE;
import static org.apache.wicket.markup.head.JavaScriptHeaderItem.forReference;

import java.util.Collections;
import java.util.List;
import java.util.Locale;

import org.apache.commons.lang3.StringUtils;
import org.apache.uima.cas.CAS;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.feedback.IFeedback;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.string.StringValue;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.googlecode.wicket.jquery.core.JQueryBehavior;

import de.tudarmstadt.ukp.clarin.webanno.api.annotation.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupport;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.FeatureSupportRegistry;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.editor.FeatureEditor;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.editor.KendoChoiceDescriptionScriptReference;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.feature.event.FeatureEditorValueChangedEvent;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.keybindings.KeyBindingsPanel;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.FeatureState;
Expand All @@ -67,7 +83,8 @@ public class ConceptFeatureEditor

private static final long serialVersionUID = 7763348613632105600L;

private FormComponent focusComponent;
private AutoCompleteField focusComponent;
private Label description;
private IriInfoBadge iriBadge;
private ExternalLink openIriLink;

Expand All @@ -92,7 +109,7 @@ public ConceptFeatureEditor(String aId, MarkupContainer aItem, IModel<FeatureSta

add(new DisabledKBWarning("disabledKBWarning", Model.of(getModelObject().feature)));

add(focusComponent = new KnowledgeBaseItemAutoCompleteField(MID_VALUE, _query ->
add(focusComponent = new AutoCompleteField(MID_VALUE, _query ->
getCandidates(aStateModel, aHandler, _query)));

AnnotationFeature feat = getModelObject().feature;
Expand All @@ -103,6 +120,11 @@ public ConceptFeatureEditor(String aId, MarkupContainer aItem, IModel<FeatureSta
// editor is used in a "normal" context and not e.g. in the keybindings
// configuration panel
.add(visibleWhen(() -> getLabelComponent().isVisible())));

description = new Label("description", LoadableDetachableModel.of(this::descriptionValue));
description.setOutputMarkupPlaceholderTag(true);
description.add(visibleWhen(() -> getLabelComponent().isVisible()));
add(description);
}

@Override
Expand All @@ -112,7 +134,17 @@ public void renderHead(IHeaderResponse aResponse)

aResponse.render(forReference(KendoChoiceDescriptionScriptReference.get()));
}


private String descriptionValue()
{
return getModel().map(FeatureState::getValue)
.map(value -> (KBHandle) value)
.map(KBHandle::getDescription)
.map(value -> StringUtils.abbreviate(value, 130))
.orElse("no description")
.getObject();
}

private String iriTooltipValue()
{
return getModel().map(FeatureState::getValue)
Expand Down Expand Up @@ -174,10 +206,191 @@ private ConceptFeatureTraits readFeatureTraits(AnnotationFeature aAnnotationFeat
ConceptFeatureTraits traits = fs.readTraits(aAnnotationFeature);
return traits;
}

@Override
public void addFeatureUpdateBehavior()
{
focusComponent.add(new AjaxFormComponentUpdatingBehavior("change")
{
private static final long serialVersionUID = -8944946839865527412L;

@Override
protected void updateAjaxAttributes(AjaxRequestAttributes aAttributes)
{
super.updateAjaxAttributes(aAttributes);
aAttributes.getDynamicExtraParameters()
.add(focusComponent.getIdentifierDynamicAttributeScript());
addDelay(aAttributes, 250);
}

@Override
protected void onUpdate(AjaxRequestTarget aTarget)
{
aTarget.add(description);
send(focusComponent, BUBBLE,
new FeatureEditorValueChangedEvent(ConceptFeatureEditor.this, aTarget));
}
});
}

@Override
public FormComponent getFocusComponent()
{
return focusComponent;
}

/**
* Special version of the {@link KnowledgeBaseItemAutoCompleteField} for used in the concept
* feature editor.
*/
public static class AutoCompleteField
extends KnowledgeBaseItemAutoCompleteField
{
private static final long serialVersionUID = 5461442869971269291L;

private IConverter<KBHandle> converter;
private List<KBHandle> choiceCache;
private boolean allowChoiceCache = false;

public AutoCompleteField(String aId,
SerializableFunction<String, List<KBHandle>> aChoiceProvider)
{
super(aId, aChoiceProvider);
converter = newConverter();
}

@Override
public void onConfigure(JQueryBehavior aBehavior)
{
super.onConfigure(aBehavior);

// We need to explicitly trigger the change event on the input element in order to
// trigger the Wicket AJAX update (if there is one). If we do not do this, then Kendo
// will "forget" to trigger a change event if the label of the newly selected item is
// the same as the label of the previously selected item!!!
// Using the default select behavior of AutoCompleteTextField which is coupled to the
// onSelected(AjaxRequestTarget aTarget) callback does unfortunatle not work well
// because onSelected does not tell us when the auto-complete field is CLEARED!
aBehavior.setOption("select", String.join(" ",
"function (e) {",
" e.sender.element.trigger('change');",
"}"));
}

@Override
protected List<KBHandle> getChoices(String aInput)
{
if (!allowChoiceCache || choiceCache == null) {
choiceCache = super.getChoices(aInput);
}
return choiceCache;
}

@Override
public String[] getInputAsArray()
{
// If the web request includes the additional "identifier" parameter which is supposed
// to contain the IRI of the selected item instead of its label, then we use that as the
// value.
WebRequest request = getWebRequest();
IRequestParameters requestParameters = request.getRequestParameters();
StringValue identifier = requestParameters
.getParameterValue(getInputName() + ":identifier");

if (!identifier.isEmpty()) {
return new String[] { identifier.toString() };
}

return super.getInputAsArray();
}

/**
* When using this input component with an {@link AjaxFormChoiceComponentUpdatingBehavior},
* it is necessary to request the identifier of the selected item as an additional dynamic
* attribute, otherwise no distinction can be made between two items with the same label!
*/
public String getIdentifierDynamicAttributeScript()
{
return String.join(" ",
"var item = $(attrs.event.target).data('kendoAutoComplete').dataItem();",
"if (item) {",
" return [{",
" 'name': '" + getInputName() + ":identifier', ",
" 'value': $(attrs.event.target).data('kendoAutoComplete').dataItem().identifier",
" }]",
"}",
"return [];");
}

@Override
public <C> IConverter<C> getConverter(Class<C> aType)
{
if (aType != null && aType.isAssignableFrom(this.getType())) {
return (IConverter<C>) converter;
}

return super.getConverter(aType);
}

private IConverter<KBHandle> newConverter()
{
return new IConverter<KBHandle>() {

private static final long serialVersionUID = 1L;

@Override
public KBHandle convertToObject(String value, Locale locale)
{
if (value == null) {
return null;
}

if (value.equals(getModelValue())) {
return getModelObject();
}

// Check choices only here since fetching choices can take some time. If we
// already have choices from a previous query, then we use them instead of
// reloading all the choices. This avoids having to load the choices when
// opening the dropdown AND when selecting one of the items from it.
List<KBHandle> choices;
try {
allowChoiceCache = true;
choices = getChoices(value);
}
finally {
allowChoiceCache = false;
}

if (choices.isEmpty()) {
return null;
}

// Check if we can find a match by the identifier. The identifier is unique
// while the same label may appear on multiple items
for (KBHandle handle : choices) {
if (value.equals(handle.getIdentifier())) {
return handle;
}
}

// // Check labels if there was no match on the identifier
// for (KBHandle handle : choices) {
// if (value.equals(getRenderer().getText(handle))) {
// return handle;
// }
// }

// If there was no match at all, return null
return null;
}

@Override
public String convertToString(KBHandle value, Locale locale)
{
return getRenderer().getText(value);
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ public boolean accepts(AnnotationFeature aFeature)
}

@Override
public String renderFeatureValue(AnnotationFeature aFeature, String aLabel)
public String renderFeatureValue(AnnotationFeature aFeature, String aIdentifier)
{
String renderValue = null;
if (aLabel != null) {
return labelCache.get(new Key(aFeature, aLabel)).getUiLabel();
if (aIdentifier != null) {
return labelCache.get(new Key(aFeature, aIdentifier)).getUiLabel();
}
return renderValue;
}
Expand Down Expand Up @@ -202,7 +202,10 @@ public KBHandle wrapFeatureValue(AnnotationFeature aFeature, CAS aCAS, Object aV
{
if (aValue instanceof String) {
String identifier = (String) aValue;
return new KBHandle(identifier, renderFeatureValue(aFeature, identifier));
String label = renderFeatureValue(aFeature, identifier);
String description = labelCache.get(new Key(aFeature, identifier)).getDescription();

return new KBHandle(identifier, label, description);
}
else if (aValue instanceof KBHandle) {
return (KBHandle) aValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@

import de.tudarmstadt.ukp.inception.kb.graph.KBHandle;

/**
* Auto-complete field for accessing a knowledge base.
*/
public class KnowledgeBaseItemAutoCompleteField
extends AutoCompleteTextField<KBHandle>
{
Expand Down Expand Up @@ -76,7 +79,7 @@ protected List<KBHandle> getChoices(String aInput)
{
return choiceProvider.apply(aInput);
}

@Override
public void onConfigure(JQueryBehavior behavior)
{
Expand All @@ -94,6 +97,14 @@ public void onConfigure(JQueryBehavior behavior)
"function(e) {",
" e.sender.list.width(Math.max($(window).width()*0.3,300));",
"}"));

// Reset the values in the dropdown listbox to avoid that when opening the dropdown the next
// time ALL items with the same label as the selected item appear as selected
behavior.setOption("filtering", String.join(" ",
"function(e) {",
" e.sender.listView.value([]);",
"}"));

// Prevent scrolling action from closing the dropdown while the focus is on the input field
// Use one-third of the browser width but not less than 300 pixels. This is better than
// using the Kendo auto-sizing feature because that sometimes doesn't get the width right.
Expand Down Expand Up @@ -127,7 +138,7 @@ public String getText()
sb.append(" [${ data.rank }]");
sb.append(" </span>");
sb.append(" # } #");
sb.append(" ${ data.name }");
sb.append(" ${ data.uiLabel }");
sb.append(" </div>");
sb.append(" <div class=\"item-identifier\">");
sb.append(" ${ data.identifier }");
Expand All @@ -148,7 +159,6 @@ public String getText()
public List<String> getTextProperties()
{
List<String> properties = new ArrayList<>();
properties.add("name");
properties.add("identifier");
properties.add("description");
properties.add("rank");
Expand Down

0 comments on commit eef5d77

Please sign in to comment.