Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions src/main/java/com/knowledgepixels/nanodash/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,10 @@ public static String getPageParametersAsString(PageParameters params) {
* @param selectItem the Select2Choice component to set the escape markup for
*/
public static void setSelect2ChoiceMinimalEscapeMarkup(Select2Choice<?> selectItem) {
selectItem.getSettings().setEscapeMarkup("function(markup) {" + "return markup" + ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" + ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" + ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" + ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" + ";}");
// The note of a to-be-minted value is not part of the value, so it is set in its own
// span and styled as an aside rather than as the term itself (issue #652).
String noteRegex = TO_BE_MINTED_NOTE.replace("(", "\\(").replace(")", "\\)");
selectItem.getSettings().setEscapeMarkup("function(markup) {" + "return markup" + ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" + ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" + ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" + ".replace(/^(.*) " + noteRegex + "$/, '<span class=\"term\">$1</span> <span class=\"mint-note\">" + TO_BE_MINTED_NOTE + "</span>')" + ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" + ";}");
}

/**
Expand Down Expand Up @@ -1123,6 +1126,44 @@ public static String getUriLabel(String uri) {
return uriLabel;
}

/**
* Whether a term typed into a choice field can be entered as a plain name for a resource that
* has no identifier yet (issue #652): it is not a URI already, and the IRI validator accepts
* it once a prefix is put in front of it -- the field's own, or the local one when it has
* none, in which case the nanopublication mints it under its own namespace.
*
* @param term the term typed into the field
* @return true if the term can be offered as a plain name
*/
public static boolean isPlainName(String term) {
if (term == null || term.isBlank()) return false;
if (isUriValue(term)) return false;
// Same rule as the validator: no colon, hash or whitespace, and well-formed as a URI once
// prefixed.
if (!term.matches("[^:#\\s]+")) return false;
return isWellFormedUri(LocalUri.PREFIX + term);
}

/**
* How a value that a nanopublication will mint under its own namespace is shown in a choice
* field: with the local prefix in front of it and marked as not being an identifier yet, e.g.
* "local:john (to be minted)" (issue #652). See
* {@link com.knowledgepixels.nanodash.template.TemplateContext#isToBeMinted(IRI, String)} for
* which values these are.
*
* @param value the plain name held for the placeholder
* @return the label to show for it
*/
public static String getToBeMintedLabel(String value) {
return LocalUri.PREFIX + value + " " + TO_BE_MINTED_NOTE;
}

/**
* The note appended to a to-be-minted value, set apart from the value itself by
* {@link #setSelect2ChoiceMinimalEscapeMarkup(Select2Choice)}.
*/
private static final String TO_BE_MINTED_NOTE = "(mint locally)";

/**
* Gets an ExternalLink with a URI label.
*
Expand All @@ -1142,7 +1183,29 @@ public static ExternalLink getUriLink(String markupId, String uri) {
* @return an ExternalLink with the URI label
*/
public static ExternalLink getUriLink(String markupId, IModel<String> model) {
return new ExternalLink(markupId, model, new UriLabelModel(model));
return new ExternalLink(markupId, new UriHrefModel(model), new UriLabelModel(model));
}

/**
* The href of a URI link: empty for anything that isn't a URI to link to, so that a local
* URI or a locally minted name (issue #652) isn't turned into a relative link. This mirrors
* what {@link #getUriLink(String, String)} does with local URIs.
*/
private static class UriHrefModel implements IModel<String> {

private IModel<String> uriModel;

public UriHrefModel(IModel<String> uriModel) {
this.uriModel = uriModel;
}

@Override
public String getObject() {
String uri = uriModel.getObject();
if (uri == null || isLocalURI(uri) || !isUriValue(uri)) return "";
return uri;
}

}

private static class UriLabelModel implements IModel<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,31 @@ public class AgentChoiceItem extends AbstractContextComponent {
private static final Logger logger = LoggerFactory.getLogger(AgentChoiceItem.class);

private String getChoiceLabel(String choiceId) {
IRI iri = vf.createIRI(choiceId);
IRI iri;
try {
iri = vf.createIRI(choiceId);
} catch (IllegalArgumentException ex) {
// A manually entered local name (issue #652) isn't a URI yet -- it is minted under
// the namespace of the nanopublication at publication time -- so there is no agent
// to look up, and the name itself is the best label we have.
return choiceId;
}
String name = User.getName(iri);
if (name != null) return name;
return choiceId;
}

/**
* Whether a manually entered term can be minted as a local identifier: a plain name that is
* not an ORCID, which is offered as the ORCID URI instead (issue #652).
*
* @param term the term typed into the field
* @return true if the term can be offered as a locally minted identifier
*/
private static boolean isLocalName(String term) {
return Utils.isPlainName(term) && !term.matches(ProfilePage.ORCID_PATTERN);
}

/**
* Constructor for AgentChoiceItem.
*
Expand Down Expand Up @@ -87,8 +106,14 @@ public AgentChoiceItem(String id, String parentId, final IRI iriP, boolean optio
@Override
public String getDisplayValue(String choiceId) {
if (choiceId == null || choiceId.isEmpty()) return "";
// A manually entered name is not an identifier yet, so it is shown as the local
// URI it will be minted into rather than as a bare word (issue #652).
if (context.isToBeMinted(iri, choiceId)) return Utils.getToBeMintedLabel(choiceId);
String label = getChoiceLabel(choiceId);
if (label == null || label.isBlank()) {
// No name to show for an agent that isn't a known user -- a manually entered URI
// (issue #652) -- and repeating the value as its own label would only render it
// twice.
if (label == null || label.isBlank() || label.equals(choiceId)) {
return choiceId;
}
return label + " (" + choiceId + ")";
Expand Down Expand Up @@ -116,7 +141,10 @@ public void query(String term, int page, Response<String> response) {
}
return;
}
if (term.startsWith("https://") || term.startsWith("http://")) {
// Any URI in an allowed scheme can be entered manually, not just http(s) ones
// (issue #652).
final String typedTerm = term;
if (Utils.isUriValue(term)) {
response.add(term);
} else if (term.matches(ProfilePage.ORCID_PATTERN)) {
response.add("https://orcid.org/" + term);
Expand Down Expand Up @@ -153,6 +181,14 @@ public void query(String term, int page, Response<String> response) {
response.add(iri.stringValue());
}
}

// Anything else the validator accepts is offered as a locally minted identifier
// (issue #652): typing "john-doe" mints it under the namespace of the
// nanopublication being published. It comes last, so that the known users the
// term matches keep the top of the list.
if (isLocalName(typedTerm) && !response.getResults().contains(typedTerm)) {
response.add(typedTerm);
}
}

@Override
Expand All @@ -165,7 +201,7 @@ public Collection<String> toChoices(Collection<String> ids) {
textfield.getSettings().getAjax(true).setDelay(500);
textfield.getSettings().setCloseOnSelect(true);
String placeholder = template.getLabel(iri);
if (placeholder == null) placeholder = "select user or paste ORCID/URL";
if (placeholder == null) placeholder = "select user or type name/ORCID/URI";
textfield.getSettings().setPlaceholder(placeholder);
Utils.setSelect2ChoiceMinimalEscapeMarkup(textfield);
textfield.getSettings().setAllowClear(true);
Expand Down Expand Up @@ -204,7 +240,9 @@ protected void onComponentTag(ComponentTag tag) {
try {
selectedIri = vf.createIRI(selectedValue);
} catch (IllegalArgumentException e) {
selectedIri = NanodashSession.get().getUserIri();
// A locally minted name (issue #652) identifies somebody other than the
// logged-in user, so it gets the generic icon rather than their picture.
selectedIri = null;
}
} else {
selectedIri = NanodashSession.get().getUserIri();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ public GuidedChoiceItem(String id, String parentId, final IRI iriP, boolean opti
@Override
public String getDisplayValue(String choiceId) {
if (choiceId == null || choiceId.isEmpty()) return "";
// A value that has no identifier yet is shown as the local URI it will be minted
// into rather than as a bare word (issue #652).
if (context.isToBeMinted(iri, choiceId)) return Utils.getToBeMintedLabel(choiceId);
String label = getChoiceLabel(choiceId);
if (label == null || label.isBlank()) {
return choiceId;
Expand All @@ -167,6 +170,7 @@ public void query(String term, int page, Response<String> response) {
response.addAll(possibleValues);
return;
}
final String typedTerm = term;
if (Utils.isUriValue(term)) {
if (prefix == null || term.startsWith(prefix)) {
response.add(term);
Expand All @@ -183,6 +187,15 @@ public void query(String term, int page, Response<String> response) {
for (String v : context.getTemplate().getPossibleValuesFromApi(iri, term, labelMap)) {
if (!alreadyAddedMap.containsKey(v)) response.add(v);
}

// A guided choice only suggests values, it doesn't limit them, so a plain name for
// a resource that has no identifier yet can be entered as well (issue #652): it is
// minted under the prefix of the field, or under the namespace of the
// nanopublication when the field has none. It comes last, so that the suggestions
// the term matches keep the top of the list.
if (Utils.isPlainName(typedTerm) && !response.getResults().contains(typedTerm)) {
response.add(typedTerm);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,29 @@ public boolean hasUnresolvedPrefix(IRI iri) {
return token != null && resolvePrefixBase(token) == null;
}

/**
* Whether the given value, held for the given placeholder, will be minted as a new IRI under
* the namespace of the nanopublication being published, rather than referring to an existing
* one (issue #652). This mirrors what {@link #processValue(Value)} does with a plain name that
* has no prefix in front of it, and lets the form show such a value as what it is rather than
* as a bare word.
*
* @param iri the placeholder IRI
* @param value the value currently held for that placeholder
* @return true if publishing would mint the value under the target namespace
*/
public boolean isToBeMinted(IRI iri, String value) {
// The same rule processValue applies: a plain name (no colon, hash or space) gets the
// target namespace put in front of it. The colon also rules out anything that is a URI.
if (value == null || !value.matches("[^:# ]+")) return false;
// A space-/namespace-dependent prefix mints the resource under the space or maintained
// resource instead, so it is not a local identifier of this nanopublication.
if (hasDynamicPrefix(iri)) return false;
if (template.isLocalResource(iri)) return true;
String prefix = getPrefix(iri);
return prefix == null || prefix.isEmpty();
}

private String resolvePrefixBase(String token) {
String base = DynamicPrefix.resolveFromContext(token, navigationContextId);
if (base != null && !base.isEmpty()) return base;
Expand Down
12 changes: 11 additions & 1 deletion src/main/webapp/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,8 @@ a.actionlink {
max-height: 28px;
}

.select2-selection__rendered > span.term {
.select2-selection__rendered > span.term,
.select2-selection__rendered > span.mint-note {
display: inline-block;
margin-top: 3px;
}
Expand All @@ -1002,6 +1003,15 @@ a.actionlink {
font-weight: bold;
}

/* The note on a value that the nanopublication will mint itself: an aside about the value, not
part of it, so it is set apart from the term next to it -- but only in weight and slant, since
it sits on the same line and shares the rules above with that term (issue #652). */
span.mint-note {
font-weight: normal;
font-style: italic;
vertical-align: middle;
}

.select2-dropdown.select2-dropdown--below {
min-width: 600px !important;
margin-top: -1px;
Expand Down
Loading