Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<body>
<wicket:panel>
<a wicket:id="np" class="source" rel="cite-as" vocab="https://www.w3.org/ns/iana/link-relations/relation#">^</a>
<a wicket:id="np" class="source">^</a>
</wicket:panel>
</body>

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/page/ExplorePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,21 @@ public String getMountPath() {
return MOUNT_PATH;
}

/**
* {@inheritDoc}
*/
@Override
protected String getCiteAsIri() {
return citeAsIri;
}

private Nanopub publishedNanopub = null;

// Set when the explored id resolves to a nanopublication, i.e. when this is that
// nanopublication's own page. Only then does the page announce what to cite it as; a page
// about a term that merely mentions nanopublications announces nothing (issue #716).
private String citeAsIri = null;

/**
* Constructor for ExplorePage.
*
Expand Down Expand Up @@ -196,6 +209,7 @@ private void initPage() {
boolean isNanopubId = (np != null);
if (isNanopubId) {
tempRef = np.getUri().stringValue();
citeAsIri = tempRef;
}
if (!isNanopubId && tempRef.matches("^(.*[^A-Za-z0-9-_])?RA[A-Za-z0-9-_]{43}[^A-Za-z0-9-_].*$")) {
np = Utils.getAsNanopub(tempRef.replaceFirst("^(.*[^A-Za-z0-9-_])?(RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$2"));
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/page/NanodashPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,20 @@ protected RdfSource getRdfSource() {
return null;
}

/**
* The IRI this page should be cited as, or null where it should not be cited as anything.
* <p>
* A page returns one only when it <em>is</em> that resource's own page -- the one its IRI
* resolves to. A page that merely links to a nanopublication is not announcing itself as
* that nanopublication, and a listing that links to many is announcing itself as none of
* them (issue #716).
*
* @return the IRI to be cited as, or null
*/
protected String getCiteAsIri() {
return null;
}

/**
* Answers a client that asked for RDF in its {@code Accept} header with a 303 to the
* download page in the matching format, and lets everyone else have the HTML (issue
Expand Down Expand Up @@ -420,6 +434,31 @@ private void renderRdfLinks(IHeaderResponse response) {
response.render(StringHeaderItem.forString("<script type=\"application/ld+json\">\n" + jsonLd + "\n</script>\n"));
}

// The vocabulary the cite-as relation is a term of, scoped to the one link that uses it:
// with it in scope every token of that link's rel resolves against it, which is why the
// link carries the relation and nothing else (issues #633, #716).
private static final String LINK_RELATIONS_VOCAB = "https://www.w3.org/ns/iana/link-relations/relation#";

/**
* Renders the {@code cite-as} link (<a href="https://datatracker.ietf.org/doc/html/rfc8574">RFC
* 8574</a>) that says which IRI this page is to be cited as, for pages that are a resource's
* own page. Annotation tools read it to anchor to the resource rather than to whichever URL
* the reader arrived by (issue #633).
* <p>
* The {@code vocab} attribute lets an RDFa parser resolve the relation, as dokieli's does.
* Rendered into the head rather than onto the visible IRI link: a {@code rel} is a list of
* tokens and the visible link already carries {@code noopener noreferrer}, which the vocab
* would turn into link-relation triples of their own.
*
* @param response the header response to render into
*/
private void renderCiteAs(IHeaderResponse response) {
String citeAsIri = getCiteAsIri();
if (citeAsIri == null || citeAsIri.isBlank()) return;
response.render(MetaDataHeaderItem.forLinkTag("cite-as", citeAsIri)
.addTagAttribute("vocab", LINK_RELATIONS_VOCAB));
}

/**
* Renders the description, canonical URL, Open Graph and Twitter card tags that
* search engines and link previews read (issue #704).
Expand Down Expand Up @@ -470,6 +509,7 @@ public void renderHead(IHeaderResponse response) {
super.renderHead(response);
renderPageMetadata(response);
renderRdfLinks(response);
renderCiteAs(response);
response.render(CssHeaderItem.forUrl(getStyleSheetUrl()));
response.render(JavaScriptHeaderItem.forReference(getApplication().getJavaScriptLibrarySettings().getJQueryReference()));
response.render(JavaScriptReferenceHeaderItem.forReference(nanodashJs));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.knowledgepixels.nanodash.page;

import com.knowledgepixels.nanodash.WicketApplication;
import org.apache.wicket.util.tester.WicketTester;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* The {@code cite-as} link of RFC 8574 says which IRI the page being read is to be cited as,
* so it belongs on a resource's own page and nowhere else: a page that merely links to
* nanopublications is not announcing itself as one of them (issues #633, #716).
*/
class CiteAsLinkTest {

private WicketTester tester;

@BeforeEach
void setUp() {
tester = new WicketTester(new WicketApplication());
}

private String render(Class<? extends NanodashPage> pageClass) {
tester.startPage(pageClass);
return tester.getLastResponse().getDocument();
}

@Test
void aResourcesOwnPageSaysWhatToCiteItAs() {
String document = render(CiteAsSubjectPage.class);
assertTrue(document.contains("rel=\"cite-as\""), document);
assertTrue(document.contains("href=\"" + CiteAsSubjectPage.SUBJECT_IRI + "\""), document);
}

// Without the vocabulary in scope an RDFa parser has no IRI to resolve the relation to,
// and dokieli -- the reason the link is here at all -- reads it through one.
@Test
void theRelationIsResolvableByAnRdfaParser() {
String document = render(CiteAsSubjectPage.class);
assertTrue(document.contains("vocab=\"https://www.w3.org/ns/iana/link-relations/relation#\""), document);
}

// The link carries the relation and nothing else. Every token of a rel resolves against
// the vocabulary in scope, so a noopener alongside it would assert a link relation of its
// own about the page -- which is why this is a link of its own rather than an attribute on
// the visible IRI link, where noopener and noreferrer already sit.
@Test
void theCiteAsLinkCarriesNoOtherRelation() {
String document = render(CiteAsSubjectPage.class);
int relIndex = document.indexOf("rel=\"cite-as\"");
assertTrue(relIndex > -1, document);
int lineStart = document.lastIndexOf("<", relIndex);
int lineEnd = document.indexOf(">", relIndex);
String tag = document.substring(lineStart, lineEnd + 1);
assertFalse(tag.contains("noopener"), tag);
assertFalse(tag.contains("noreferrer"), tag);
}

// A page with no subject of its own claims nothing, which is the case every listing and
// every page that merely links to a nanopublication falls into.
@Test
void aPageThatIsNotAResourcesOwnPageClaimsNothing() {
String document = render(ErrorPage.class);
assertFalse(document.contains("cite-as"), document);
}

@Test
void exactlyOneCiteAsIsClaimed() {
String document = render(CiteAsSubjectPage.class);
assertEquals(1, document.split("rel=\"cite-as\"", -1).length - 1, document);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<head>
<wicket:head>
<title wicket:id="pagetitle">Cite-as | nanodash</title>
</wicket:head>
</head>
<body>
<wicket:extend>
<div class="row-section"><div class="col-12"><p>cite-as test page</p></div></div>
</wicket:extend>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.knowledgepixels.nanodash.page;

import com.knowledgepixels.nanodash.component.TitleBar;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.EmptyPanel;
import org.apache.wicket.request.mapper.parameter.PageParameters;

/**
* A stand-in for a page that is a resource's own page, used to render the {@code cite-as}
* link without reaching for a nanopublication over the network.
*/
public class CiteAsSubjectPage extends NanodashPage {

/** The IRI this page claims to be cited as. */
public static final String SUBJECT_IRI = "https://w3id.org/np/RAAbCdEfGhIjKlMnOpQrStUvWxYz0123456789-_AbCdE";

/**
* @param parameters the page parameters, unused
*/
public CiteAsSubjectPage(final PageParameters parameters) {
super(parameters);
try {
add(new TitleBar("titlebar", this));
} catch (Exception ex) {
add(new EmptyPanel("titlebar"));
}
add(new Label("pagetitle", "Cite-as | nanodash"));
}

/**
* {@inheritDoc}
*/
@Override
public String getMountPath() {
return "/cite-as-subject";
}

/**
* {@inheritDoc}
*/
@Override
protected String getCiteAsIri() {
return SUBJECT_IRI;
}

}