package edu.stanford.bmir.protege.web.client.primitive;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.*;
import com.google.inject.Inject;
import edu.stanford.bmir.protege.web.client.ui.anchor.AnchorClickedEvent;
import edu.stanford.bmir.protege.web.client.ui.anchor.AnchorClickedHandler;
import edu.stanford.bmir.protege.web.client.ui.library.common.EventStrategy;
import edu.stanford.bmir.protege.web.client.ui.library.suggest.EntitySuggestion;
import edu.stanford.bmir.protege.web.shared.DataFactory;
import edu.stanford.bmir.protege.web.shared.DirtyChangedEvent;
import edu.stanford.bmir.protege.web.shared.DirtyChangedHandler;
import edu.stanford.bmir.protege.web.shared.PrimitiveType;
import edu.stanford.bmir.protege.web.shared.entity.*;
import edu.stanford.bmir.protege.web.shared.project.ProjectId;
import org.semanticweb.owlapi.model.*;
import org.semanticweb.owlapi.vocab.OWL2Datatype;
import java.util.*;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Author: Matthew Horridge<br>
* Stanford University<br>
* Bio-Medical Informatics Research Group<br>
* Date: 03/12/2012
*
* An view for {@link OWLPrimitiveData} objects. The view supports auto-completion
* and has an option to allow the creation of new primitives.
*
* This is really a presenter for the editor view.
*/
public class PrimitiveDataEditorImpl extends Composite implements PrimitiveDataEditor, HasEnabled {
public static final double PLACEHOLDER_ICON_OPACITY = 0.3;
private final ProjectId projectId;
private final PrimitiveDataEditorSuggestOracle entitySuggestOracle;
private final LanguageEditor languageEditor;
private final PrimitiveDataParser primitiveDataParser;
private FreshEntitiesHandler freshEntitiesHandler;
private Optional<OWLPrimitiveData> currentData = Optional.absent();
private final Set<PrimitiveType> allowedTypes = Sets.newLinkedHashSet();
private boolean dirty = false;
private boolean enabled;
private PrimitiveDataEditorView view;
private String textPlaceholder = "";
private Optional<OWLPrimitiveData> primitiveDataPlaceholder = Optional.absent();
private Optional<EntitySuggestion> selectedSuggestion = Optional.absent();
@Inject
public PrimitiveDataEditorImpl(ProjectId projectId, PrimitiveDataEditorView editorView, LanguageEditor languageEditor, PrimitiveDataEditorSuggestOracle suggestOracle, PrimitiveDataParser parser, FreshEntitiesHandler freshEntitiesHandler) {
this.projectId = projectId;
this.languageEditor = languageEditor;
this.freshEntitiesHandler = freshEntitiesHandler;
this.primitiveDataParser = parser;
entitySuggestOracle = suggestOracle;
view = editorView;
view.asWidget().addStyleName("web-protege-form-layout-editor-input");
view.setMode(PrimitiveDataEditorView.Mode.SINGLE_LINE);
view.setSuggestOracle(entitySuggestOracle);
view.addSelectionHandler(new SelectionHandler<EntitySuggestion>() {
@Override
public void onSelection(SelectionEvent<EntitySuggestion> event) {
EntitySuggestion suggestion = event.getSelectedItem();
selectedSuggestion = Optional.of(suggestion);
setCurrentData(Optional.<OWLPrimitiveData>of(suggestion.getEntity()), EventStrategy.FIRE_EVENTS);
}
});
view.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
handleValueChanged();
}
});
languageEditor.addValueChangeHandler(new ValueChangeHandler<Optional<String>>() {
@Override
public void onValueChange(ValueChangeEvent<Optional<String>> event) {
handleLanguageChanged();
}
});
view.setAnchorVisible(false);
view.addAnchorClickedHandler(new AnchorClickedHandler() {
@Override
public void handleAnchorClicked(AnchorClickedEvent event) {
handleAnchorClick();
}
});
initWidget(view.asWidget());
}
@Override
public void setPlaceholder(String placeholder) {
this.textPlaceholder = placeholder;
view.setPlaceholder(placeholder);
}
@Override
public String getPlaceholder() {
return view.getPlaceholder();
}
@Override
public void setPrimitiveDataPlaceholder(Optional<OWLPrimitiveData> placeholder) {
primitiveDataPlaceholder = placeholder;
if(!primitiveDataPlaceholder.isPresent()) {
view.setPlaceholder(textPlaceholder);
}
else {
view.setPlaceholder(primitiveDataPlaceholder.get().getBrowserText());
setIconInsetStyleNameForEntityData(primitiveDataPlaceholder, PLACEHOLDER_ICON_OPACITY);
}
}
@Override
public Optional<OWLPrimitiveData> getPrimitiveDataPlaceholder() {
return primitiveDataPlaceholder;
}
@Override
public void setFreshEntitiesSuggestStrategy(FreshEntitySuggestStrategy suggestStrategy) {
entitySuggestOracle.setFreshEntityStrategy(suggestStrategy);
}
@Override
public Optional<EntitySuggestion> getSelectedSuggestion() {
return selectedSuggestion;
}
@Override
public void setFreshEntitiesHandler(FreshEntitiesHandler handler) {
checkNotNull(handler);
this.freshEntitiesHandler = handler;
}
@Override
public void setMode(PrimitiveDataEditorView.Mode mode) {
checkNotNull(mode);
view.setMode(mode);
}
@Override
public void setEntityLinkMode(EntityLinkMode entityLinkMode) {
// this.entityLinkMode = entityLinkMode;
}
@Override
public void setShowLinksForEntities(boolean showLinksForEntities) {
setEntityLinkMode(showLinksForEntities ? EntityLinkMode.SHOW_LINKS_FOR_ENTITIES : EntityLinkMode.DO_NOT_SHOW_LINKS_FOR_ENTITIES);
}
/**
* Returns true if the widget is enabled, false if not.
*/
@Override
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether this widget is enabled.
*
* @param enabled <code>true</code> to enable the widget, <code>false</code>
* to disable it
*/
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
view.setEnabled(enabled);
languageEditor.setEnabled(enabled);
}
@Override
public LanguageEditor getLanguageEditor() {
return languageEditor;
}
/**
* Gets this object's text.
*
* @return the object's text
*/
// @Override
public String getText() {
return getTrimmedText();
}
/**
* Adds a {@link com.google.gwt.event.dom.client.FocusEvent} handler.
*
* @param handler the focus handler
* @return {@link com.google.gwt.event.shared.HandlerRegistration} used to remove this handler
*/
@Override
public HandlerRegistration addFocusHandler(FocusHandler handler) {
return view.addFocusHandler(handler);
}
/**
* Adds a {@link com.google.gwt.event.dom.client.KeyUpEvent} handler.
*
* @param handler the key up handler
* @return {@link com.google.gwt.event.shared.HandlerRegistration} used to remove this handler
*/
@Override
public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
return view.addKeyUpHandler(handler);
}
@Override
public boolean isAnnotationPropertiesAllowed() {
return allowedTypes.contains(PrimitiveType.ANNOTATION_PROPERTY);
}
@Override
public void setAnnotationPropertiesAllowed(boolean annotationPropertiesAllowed) {
setAllowedType(PrimitiveType.ANNOTATION_PROPERTY, annotationPropertiesAllowed);
}
@Override
public boolean isDataPropertiesAllowed() {
return allowedTypes.contains(PrimitiveType.DATA_PROPERTY);
}
@Override
public void setDataPropertiesAllowed(boolean dataPropertiesAllowed) {
setAllowedType(PrimitiveType.DATA_PROPERTY, dataPropertiesAllowed);
}
@Override
public boolean isObjectPropertiesAllowed() {
return allowedTypes.contains(PrimitiveType.OBJECT_PROPERTY);
}
@Override
public void setObjectPropertiesAllowed(boolean objectPropertiesAllowed) {
setAllowedType(PrimitiveType.OBJECT_PROPERTY, objectPropertiesAllowed);
}
@Override
public boolean isClassesAllowed() {
return allowedTypes.contains(PrimitiveType.CLASS);
}
@Override
public void setClassesAllowed(boolean classesAllowed) {
setAllowedType(PrimitiveType.CLASS, classesAllowed);
}
@Override
public boolean isDatatypesAllowed() {
return allowedTypes.contains(PrimitiveType.DATA_TYPE);
}
@Override
public void setDatatypesAllowed(boolean datatypesAllowed) {
setAllowedType(PrimitiveType.DATA_TYPE, datatypesAllowed);
}
@Override
public boolean isNamedIndividualsAllowed() {
return allowedTypes.contains(PrimitiveType.NAMED_INDIVIDUAL);
}
@Override
public void setNamedIndividualsAllowed(boolean namedIndividualsAllowed) {
setAllowedType(PrimitiveType.NAMED_INDIVIDUAL, namedIndividualsAllowed);
}
@Override
public boolean isLiteralAllowed() {
return allowedTypes.contains(PrimitiveType.LITERAL);
}
@Override
public void setLiteralAllowed(boolean literalAllowed) {
setAllowedType(PrimitiveType.LITERAL, literalAllowed);
}
@Override
public boolean isIRIAllowed() {
return allowedTypes.contains(PrimitiveType.IRI);
}
@Override
public void setIRIAllowed(boolean iriAllowed) {
setAllowedType(PrimitiveType.IRI, iriAllowed);
}
@Override
public void setAllowedType(PrimitiveType type, boolean allowed) {
boolean revalidate;
if (allowed) {
revalidate = allowedTypes.add(type);
} else {
revalidate = allowedTypes.remove(type);
}
if (revalidate) {
if (type.getEntityType() != null) {
updateOracle();
}
validateCurrentEntityTypeAgainstAllowedTypes();
}
}
@Override
public void coerceToEntityType(EntityType<?> entityType) {
if(currentData.isPresent() && entityType.equals(currentData.get().getType().getEntityType())) {
return;
}
String text = getTrimmedText();
OWLEntity entity = freshEntitiesHandler.getFreshEntity(text, entityType);
OWLPrimitiveData coercedData = DataFactory.getOWLEntityData(entity, text);
setCurrentData(Optional.of(coercedData), EventStrategy.FIRE_EVENTS);
updateDisplayForCurrentData();
}
/**
* Adds a {@link com.google.gwt.event.logical.shared.ValueChangeEvent} handler.
*
* @param handler the handler
* @return the registration for the event
*/
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Optional<OWLPrimitiveData>> handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
@Override
public Optional<OWLPrimitiveData> getValue() {
return currentData;
}
@Override
public void setValue(OWLPrimitiveData object) {
checkNotNull(object);
setCurrentData(Optional.of(object), EventStrategy.DO_NOT_FIRE_EVENTS);
}
@Override
public void clearValue() {
view.setText("");
view.setPrimitiveDataStyleName(Optional.<String>absent());
languageEditor.setValue("");
setCurrentData(Optional.<OWLPrimitiveData>absent(), EventStrategy.DO_NOT_FIRE_EVENTS);
}
@Override
public boolean isWellFormed() {
return getTrimmedText().isEmpty() || currentData.isPresent();
}
/**
* Determines if this object is dirty.
*
* @return {@code true} if the object is dirty, otherwise {@code false}.
*/
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setAllowedTypes(Collection<PrimitiveType> primitiveTypes) {
if (primitiveTypes.equals(this.allowedTypes)) {
return;
}
this.allowedTypes.clear();
this.allowedTypes.addAll(primitiveTypes);
if (!allowedTypes.contains(PrimitiveType.LITERAL)) {
setMode(PrimitiveDataEditorView.Mode.SINGLE_LINE);
} else {
setMode(PrimitiveDataEditorView.Mode.MULTI_LINE);
}
updateOracle();
if (!isCurrentEntityTypeAllowed()) {
reparsePrimitiveData();
}
}
@Override
public HandlerRegistration addDirtyChangedHandler(DirtyChangedHandler handler) {
return addHandler(handler, DirtyChangedEvent.TYPE);
}
private void handleValueChanged() {
reparsePrimitiveData();
dirty = true;
}
private void handleAnchorClick() {
if (!currentData.isPresent()) {
return;
}
currentData.get().accept(new OWLPrimitiveDataVisitorAdapter<Void, RuntimeException>() {
@Override
public Void visit(IRIData data) throws RuntimeException {
Window.open(data.getObject().toString(), data.getBrowserText(), "");
return null;
}
});
}
private void showErrorLabel() {
String errorMessage = freshEntitiesHandler.getErrorMessage(view.getText());
SafeHtml errorMessageHTML = new SafeHtmlBuilder().appendHtmlConstant(errorMessage).toSafeHtml();
Set<EntityType<?>> suggestTypes;
if(freshEntitiesHandler.getFreshEntitiesPolicy() == FreshEntitiesPolicy.ALLOWED) {
suggestTypes = getMatchTypes();
}
else {
suggestTypes = Collections.emptySet();
}
view.showErrorMessage(errorMessageHTML, suggestTypes);
}
private void hideErrorLabel() {
view.clearErrorMessage();
}
private void reparsePrimitiveData() {
if (isCurrentDataRendered()) {
return;
}
final String trimmedText = getTrimmedText();
primitiveDataParser.parsePrimitiveData(trimmedText, languageEditor.getValue(), allowedTypes, new PrimitiveDataParserCallback() {
@Override
public void parsingFailure() {
setCurrentData(Optional.<OWLPrimitiveData>absent(), EventStrategy.FIRE_EVENTS);
showErrorLabel();
}
@Override
public void onSuccess(Optional<OWLPrimitiveData> result) {
hideErrorLabel();
setCurrentData(result, EventStrategy.FIRE_EVENTS);
}
});
}
private String getTrimmedText() {
return view.getText().trim();
}
private boolean isCurrentDataRendered() {
if (!currentData.isPresent()) {
return getTrimmedText().isEmpty() && !languageEditor.getValue().isPresent();
}
OWLPrimitiveData data = currentData.get();
String currentBrowserText = data.getBrowserText();
if (!currentBrowserText.equals(getTrimmedText())) {
return false;
}
if (!isCurrentEntityTypeAllowed()) {
return false;
}
if (data instanceof OWLLiteralData) {
final OWLLiteral literal = ((OWLLiteralData) data).getLiteral();
Optional<String> lang;
if (literal.hasLang()) {
lang = Optional.of(literal.getLang());
} else {
lang = Optional.absent();
}
if (!lang.equals(languageEditor.getValue())) {
return false;
}
}
return true;
}
private void updateDisplayForCurrentData() {
if(currentData.isPresent()) {
setIconInsetStyleNameForEntityData(currentData, 1.0);
validateCurrentEntityTypeAgainstAllowedTypes();
if (isExternalIRI()) {
view.setAnchorTitle("Open link in new window");
view.setAnchorVisible(true);
} else {
view.setAnchorVisible(false);
}
}
else {
setIconInsetStyleNameForEntityData(primitiveDataPlaceholder, PLACEHOLDER_ICON_OPACITY);
}
}
private boolean isExternalIRI() {
if (!currentData.isPresent()) {
return false;
}
OWLPrimitiveData data = currentData.get();
if (!(data instanceof IRIData)) {
return false;
}
IRI iri = (IRI) data.getObject();
if (!iri.isAbsolute()) {
return false;
}
if (!"http".equalsIgnoreCase(iri.getScheme())) {
return false;
}
return true;
}
private void setIconInsetStyleName(Optional<String> name) {
view.setPrimitiveDataStyleName(name);
}
////////////////////////////////////////////////////////////////////////////////////////
private void setIconInsetStyleNameForEntityData(final Optional<OWLPrimitiveData> data, double iconOpacity) {
if (!data.isPresent()) {
view.setTitle("");
hideErrorLabel();
setIconInsetStyleName(Optional.of("empty-icon-inset"));
return;
}
final OWLPrimitiveData entityData = data.get();
String styleName = entityData.accept(new OWLPrimitiveDataVisitorAdapter<String, RuntimeException>() {
@Override
protected String getDefaultReturnValue() {
view.setTitle("");
return "empty-icon-inset";
}
@Override
public String visit(OWLClassData data) throws RuntimeException {
setTooltip(data, "owl:Class");
return "class-icon-inset";
}
@Override
public String visit(OWLObjectPropertyData data) throws RuntimeException {
setTooltip(data, "owl:ObjectProperty");
return "object-property-icon-inset";
}
@Override
public String visit(OWLDataPropertyData data) throws RuntimeException {
setTooltip(data, "owl:DataProperty");
return "data-property-icon-inset";
}
@Override
public String visit(OWLAnnotationPropertyData data) throws RuntimeException {
setTooltip(data, "owl:AnnotationProperty");
return "annotation-property-icon-inset";
}
@Override
public String visit(OWLNamedIndividualData data) throws RuntimeException {
setTooltip(data, "owl:NamedIndividual");
return "individual-icon-inset";
}
@Override
public String visit(OWLDatatypeData data) throws RuntimeException {
setTooltip(data, "owl:Datatype");
return "datatype-icon-inset";
}
private void setTooltip(OWLEntityData data, String typeName) {
StringBuilder sb = new StringBuilder();
sb.append("\"");
sb.append(entityData.getBrowserText());
sb.append("\" is an ");
sb.append(typeName);
if (!DataFactory.isFreshEntity(data.getEntity())) {
sb.append("\n");
sb.append("<");
IRI iri = data.getEntity().getIRI();
sb.append(iri);
sb.append(">");
}
view.setTitle(sb.toString());
}
@Override
public String visit(OWLLiteralData data) throws RuntimeException {
String styleName = "literal-icon-inset";
OWLDatatype datatype = data.getLiteral().getDatatype();
if (datatype.isBuiltIn()) {
OWL2Datatype owl2Datatype = datatype.getBuiltInDatatype();
if (owl2Datatype.isNumeric()) {
styleName = "numeric-literal-icon-inset";
} else if (owl2Datatype.equals(OWL2Datatype.XSD_DATE_TIME)) {
styleName = "date-time-icon-inset";
}
}
String datatypeName = datatype.getIRI().getFragment();
if (datatypeName == null) {
datatypeName = datatype.getIRI().toString();
}
StringBuilder tooltip = new StringBuilder();
tooltip.append(entityData.getBrowserText());
char c = datatypeName.charAt(0);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
tooltip.append(" is an ");
} else {
tooltip.append(" is a ");
}
tooltip.append(datatypeName);
view.setTitle(tooltip.toString());
return styleName;
}
@Override
public String visit(IRIData data) throws RuntimeException {
if (data.isHTTPLink()) {
return "link-icon-inset";
} else {
return "iri-icon-inset";
}
}
});
setIconInsetStyleName(Optional.<String>of(styleName));
}
/**
* Checks that the current entity type is one of the allowed types.
*/
private void validateCurrentEntityTypeAgainstAllowedTypes() {
if (!currentData.isPresent()) {
hideErrorLabel();
// Allowed to be empty
return;
}
if (isCurrentEntityTypeAllowed()) {
hideErrorLabel();
return;
}
showErrorLabel();
}
private boolean isCurrentEntityTypeAllowed() {
return !currentData.isPresent() || allowedTypes.contains(currentData.get().getType());
}
private void handleLanguageChanged() {
reparsePrimitiveData();
}
private void updateOracle() {
Set<EntityType<?>> types = getMatchTypes();
entitySuggestOracle.setEntityTypes(types);
}
private Set<EntityType<?>> getMatchTypes() {
Set<EntityType<?>> types = new LinkedHashSet<EntityType<?>>();
for (PrimitiveType primitiveType : allowedTypes) {
EntityType<?> entityType = primitiveType.getEntityType();
if (entityType != null) {
types.add(entityType);
}
}
return types;
}
private void fireValueChangedEvent() {
ValueChangeEvent.fire(this, getValue());
}
private void setCurrentData(Optional<OWLPrimitiveData> nextCurrentData, EventStrategy eventStrategy) {
checkNotNull(nextCurrentData);
dirty = false;
if (currentData.equals(nextCurrentData)) {
return;
}
currentData = nextCurrentData;
if (nextCurrentData.isPresent()) {
OWLPrimitiveData data = nextCurrentData.get();
view.setText(data.getBrowserText());
if (data instanceof OWLLiteralData) {
String lang = ((OWLLiteralData) data).getLiteral().getLang();
languageEditor.setValue(lang);
}
if(selectedSuggestion.isPresent()) {
if(!selectedSuggestion.get().getReplacementString().equals(data.getBrowserText())) {
selectedSuggestion = Optional.absent();
}
}
}
else {
selectedSuggestion = Optional.absent();
}
updateDisplayForCurrentData();
if (eventStrategy == EventStrategy.FIRE_EVENTS) {
fireValueChangedEvent();
}
}
}