NIHVIVO-1633 Support collation toggling for all object properties. Basic functionality in place but not all queries working perfectly yet.

This commit is contained in:
rjy7 2011-01-19 20:46:36 +00:00
parent 43167c6c5c
commit b71400d74e
8 changed files with 129 additions and 102 deletions

View file

@ -2,21 +2,15 @@
package edu.cornell.mannlib.vitro.webapp.controller.freemarker; package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.beans.Portal;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues;
import freemarker.template.Configuration;
/** /**
* Freemarker controller and template sandbox. * Freemarker controller and template sandbox.

View file

@ -12,6 +12,7 @@ import java.util.TreeMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -26,7 +27,6 @@ import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
public class CollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateModel { public class CollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateModel {
private static final Log log = LogFactory.getLog(CollatedObjectPropertyTemplateModel.class); private static final Log log = LogFactory.getLog(CollatedObjectPropertyTemplateModel.class);
private static final String DEFAULT_CONFIG_FILE = "listViewConfig-default-collated.xml";
private static final Pattern SELECT_SUBCLASS_PATTERN = private static final Pattern SELECT_SUBCLASS_PATTERN =
// SELECT ?subclass // SELECT ?subclass
Pattern.compile("SELECT[^{]*\\?subclass\\b", Pattern.CASE_INSENSITIVE); Pattern.compile("SELECT[^{]*\\?subclass\\b", Pattern.CASE_INSENSITIVE);
@ -35,18 +35,41 @@ public class CollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateM
private static final Pattern ORDER_BY_SUBCLASS_PATTERN = private static final Pattern ORDER_BY_SUBCLASS_PATTERN =
Pattern.compile("ORDER\\s+BY\\s+(DESC\\s*\\(\\s*)?\\?subclass", Pattern.CASE_INSENSITIVE); Pattern.compile("ORDER\\s+BY\\s+(DESC\\s*\\(\\s*)?\\?subclass", Pattern.CASE_INSENSITIVE);
private static enum ConfigError {
NO_QUERY("Missing query specification"),
NO_SUBCLASS_SELECT("Query does not select a subclass variable"),
NO_SUBCLASS_ORDER_BY("Query does not sort first by subclass variable");
String message;
ConfigError(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public String toString() {
return getMessage();
}
}
private SortedMap<String, List<ObjectPropertyStatementTemplateModel>> subclasses; private SortedMap<String, List<ObjectPropertyStatementTemplateModel>> subclasses;
CollatedObjectPropertyTemplateModel(ObjectProperty op, Individual subject, CollatedObjectPropertyTemplateModel(ObjectProperty op, Individual subject,
VitroRequest vreq, EditingPolicyHelper policyHelper) VitroRequest vreq, EditingPolicyHelper policyHelper)
throws InvalidConfigurationException { throws InvalidCollatedPropertyConfigurationException {
super(op, subject, vreq, policyHelper); super(op, subject, vreq, policyHelper);
String invalidConfigMessage = checkConfiguration(); // RY It would be more efficient to check for these errors in the super constructor, so that we don't
if ( ! invalidConfigMessage.isEmpty() ) { // go through the rest of that constructor before throwing an error. In that case, the subclasses
throw new InvalidConfigurationException("Invalid configuration for collated property " + // could each have their own checkConfiguration() method.
op.getURI() + ":" + invalidConfigMessage + ". Creating uncollated display instead."); ConfigError configError = checkConfiguration();
if ( configError != null ) {
throw new InvalidCollatedPropertyConfigurationException("Invalid configuration for collated property " +
op.getURI() + ":" + configError + ". Creating uncollated display instead.");
} }
/* Get the data */ /* Get the data */
@ -78,22 +101,26 @@ public class CollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateM
} }
} }
private String checkConfiguration() { private ConfigError checkConfiguration() {
String queryString = getQueryString(); String queryString = getQueryString();
Matcher m;
if (StringUtils.isBlank(queryString)) {
return ConfigError.NO_QUERY;
}
Matcher m;
m = SELECT_SUBCLASS_PATTERN.matcher(queryString); m = SELECT_SUBCLASS_PATTERN.matcher(queryString);
if ( ! m.find() ) { if ( ! m.find() ) {
return("Query does not select a subclass variable."); return ConfigError.NO_SUBCLASS_SELECT;
} }
m = ORDER_BY_SUBCLASS_PATTERN.matcher(queryString); m = ORDER_BY_SUBCLASS_PATTERN.matcher(queryString);
if ( ! m.find() ) { if ( ! m.find() ) {
return("Query does not sort first by subclass variable."); return ConfigError.NO_SUBCLASS_ORDER_BY;
} }
return ""; return null;
} }
private Map<String, List<ObjectPropertyStatementTemplateModel>> collate(String subjectUri, String propertyUri, private Map<String, List<ObjectPropertyStatementTemplateModel>> collate(String subjectUri, String propertyUri,
@ -134,11 +161,6 @@ public class CollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateM
return subclassName; return subclassName;
} }
@Override
protected String getDefaultConfigFileName() {
return DEFAULT_CONFIG_FILE;
}
/* Access methods for templates */ /* Access methods for templates */
public Map<String, List<ObjectPropertyStatementTemplateModel>> getSubclasses() { public Map<String, List<ObjectPropertyStatementTemplateModel>> getSubclasses() {

View file

@ -41,16 +41,13 @@ public class ObjectPropertyStatementTemplateModel extends PropertyStatementTempl
/** /**
* This method handles the special case where we are creating a DataPropertyStatementTemplateModel * This method handles the special case where we are creating a DataPropertyStatementTemplateModel
* outside the GroupedPropertyList. Specifically, it allows vitro:primaryLink and vitro:additionalLink * outside the GroupedPropertyList. Specifically, it allows vitro:primaryLink and vitro:additionalLink
* to be treated like data property statements and thus have editing links. (In a future version, * to be treated like object property statements and thus have editing links. (In a future version,
* these properties will be replaced by vivo core ontology properties.) It could potentially be used * these properties will be replaced by vivo core ontology properties.) It could potentially be used
* for other properties outside the property list as well. * for other properties outside the property list as well.
*/ */
ObjectPropertyStatementTemplateModel(String subjectUri, String propertyUri, ObjectPropertyStatementTemplateModel(String subjectUri, String propertyUri,
VitroRequest vreq, EditingPolicyHelper policyHelper) { VitroRequest vreq, EditingPolicyHelper policyHelper) {
super(subjectUri, propertyUri, policyHelper); super(subjectUri, propertyUri, policyHelper);
ObjectPropertyStatementDao opsDao = vreq.getWebappDaoFactory().getObjectPropertyStatementDao();
} }
private void setEditAccess(EditingPolicyHelper policyHelper) { private void setEditAccess(EditingPolicyHelper policyHelper) {

View file

@ -69,6 +69,26 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
// ?subject ?property ?\w+ // ?subject ?property ?\w+
Pattern.compile("\\?" + KEY_SUBJECT + "\\s+\\?" + KEY_PROPERTY + "\\s+\\?(\\w+)"); Pattern.compile("\\?" + KEY_SUBJECT + "\\s+\\?" + KEY_PROPERTY + "\\s+\\?(\\w+)");
private static enum ConfigError {
NO_QUERY("Missing query specification"),
NO_TEMPLATE("Missing template specification"),
TEMPLATE_NOT_FOUND("Specified template does not exist");
String message;
ConfigError(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public String toString() {
return getMessage();
}
}
private PropertyListConfig config; private PropertyListConfig config;
private String objectKey; private String objectKey;
@ -76,7 +96,11 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
private boolean addAccess = false; private boolean addAccess = false;
ObjectPropertyTemplateModel(ObjectProperty op, Individual subject, VitroRequest vreq, EditingPolicyHelper policyHelper) { ObjectPropertyTemplateModel(ObjectProperty op, Individual subject, VitroRequest vreq, EditingPolicyHelper policyHelper) {
super(op, subject, policyHelper); super(op, subject, policyHelper);
log.debug("Creating template model for object property " + op.getURI());
setName(op.getDomainPublic()); setName(op.getDomainPublic());
// Get the config for this object property // Get the config for this object property
@ -140,7 +164,7 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
if (op.getCollateBySubclass()) { if (op.getCollateBySubclass()) {
try { try {
return new CollatedObjectPropertyTemplateModel(op, subject, vreq, policyHelper); return new CollatedObjectPropertyTemplateModel(op, subject, vreq, policyHelper);
} catch (InvalidConfigurationException e) { } catch (InvalidCollatedPropertyConfigurationException e) {
log.warn(e.getMessage()); log.warn(e.getMessage());
return new UncollatedObjectPropertyTemplateModel(op, subject, vreq, policyHelper); return new UncollatedObjectPropertyTemplateModel(op, subject, vreq, policyHelper);
} }
@ -225,12 +249,9 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
if (dateTimeEnd == null) { if (dateTimeEnd == null) {
// If the first statement has a null end datetime, all subsequent statements in the list also do, // If the first statement has a null end datetime, all subsequent statements in the list also do,
// so there is nothing to reorder. // so there is nothing to reorder.
// NB This assumption is FALSE if the query orders by subclass but the property is not collated. if (statements.indexOf(stmt) == 0) {
// This happens when a query is written with a subclass variable to support turning on collation break;
// in the back end. }
// if (statements.indexOf(stmt) == 0) {
// break;
// }
tempList.add(stmt); tempList.add(stmt);
iterator.remove(); iterator.remove();
} }
@ -244,12 +265,13 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
return objectKey; return objectKey;
} }
protected abstract String getDefaultConfigFileName();
private class PropertyListConfig { private class PropertyListConfig {
private static final String CONFIG_FILE_PATH = "/config/"; private static final String CONFIG_FILE_PATH = "/config/";
private static final String NODE_NAME_QUERY = "query"; private static final String DEFAULT_CONFIG_FILE_NAME = "listViewConfig-default.xml";
private static final String NODE_NAME_QUERY_BASE = "query-base";
private static final String NODE_NAME_QUERY_COLLATED = "query-collated";
private static final String NODE_NAME_TEMPLATE = "template"; private static final String NODE_NAME_TEMPLATE = "template";
private static final String NODE_NAME_POSTPROCESSOR = "postprocessor"; private static final String NODE_NAME_POSTPROCESSOR = "postprocessor";
@ -258,27 +280,26 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
private String templateName; private String templateName;
private String postprocessor; private String postprocessor;
PropertyListConfig(ObjectProperty op, VitroRequest vreq) throws Exception { PropertyListConfig(ObjectProperty op, VitroRequest vreq) {
// Get the custom config filename // Get the custom config filename
WebappDaoFactory wdf = vreq.getWebappDaoFactory(); String configFileName = vreq.getWebappDaoFactory().getObjectPropertyDao().getCustomListViewConfigFileName(op);
ObjectPropertyDao opDao = wdf.getObjectPropertyDao();
String configFileName = opDao.getCustomListViewConfigFileName(op);
if (configFileName == null) { // no custom config; use default config if (configFileName == null) { // no custom config; use default config
configFileName = getDefaultConfigFileName(); configFileName = DEFAULT_CONFIG_FILE_NAME;
} }
log.debug("Using list view config file " + configFileName + " for object property " + op.getURI()); log.debug("Using list view config file " + configFileName + " for object property " + op.getURI());
String configFilePath = getConfigFilePath(configFileName); String configFilePath = getConfigFilePath(configFileName);
try { try {
File config = new File(configFilePath); File config = new File(configFilePath);
if ( ! isDefaultConfig(configFileName) && ! config.exists() ) { if ( ! isDefaultConfig(configFileName) && ! config.exists() ) {
log.warn("Can't find config file " + configFilePath + " for object property " + op.getURI() + "\n" + log.warn("Can't find config file " + configFilePath + " for object property " + op.getURI() + "\n" +
". Using default config file instead."); ". Using default config file instead.");
configFilePath = getConfigFilePath(getDefaultConfigFileName()); configFilePath = getConfigFilePath(DEFAULT_CONFIG_FILE_NAME);
// Should we test for the existence of the default, and throw an error if it doesn't exist? // Should we test for the existence of the default, and throw an error if it doesn't exist?
} }
setValuesFromConfigFile(configFilePath); setValuesFromConfigFile(configFilePath, op);
} catch (Exception e) { } catch (Exception e) {
log.error("Error processing config file " + configFilePath + " for object property " + op.getURI(), e); log.error("Error processing config file " + configFilePath + " for object property " + op.getURI(), e);
@ -286,13 +307,18 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
} }
if ( ! isDefaultConfig(configFileName) ) { if ( ! isDefaultConfig(configFileName) ) {
String invalidConfigMessage = checkForInvalidConfig(vreq); ConfigError configError = checkForInvalidConfig(vreq);
if ( StringUtils.isNotEmpty(invalidConfigMessage) ) { // If the configuration contains an error, use the default configuration.
// Exception: a collated property with a missing collated query. This will
// be caught in CollatedObjectPropertyTemplateModel constructor and result
// in using an UncollatedObjectPropertyTemplateModel instead.
if ( configError != null &&
! (configError == ConfigError.NO_QUERY && op.getCollateBySubclass()) ) {
log.warn("Invalid list view config for object property " + op.getURI() + log.warn("Invalid list view config for object property " + op.getURI() +
" in " + configFilePath + ":\n" + " in " + configFilePath + ":\n" +
invalidConfigMessage + " Using default config instead."); configError + " Using default config instead.");
configFilePath = getConfigFilePath(getDefaultConfigFileName()); configFilePath = getConfigFilePath(DEFAULT_CONFIG_FILE_NAME);
setValuesFromConfigFile(configFilePath); setValuesFromConfigFile(configFilePath, op);
} }
} }
@ -300,31 +326,31 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
} }
private boolean isDefaultConfig(String configFileName) { private boolean isDefaultConfig(String configFileName) {
return configFileName.equals(getDefaultConfigFileName()); return configFileName.equals(DEFAULT_CONFIG_FILE_NAME);
} }
private String checkForInvalidConfig(VitroRequest vreq) { private ConfigError checkForInvalidConfig(VitroRequest vreq) {
String invalidConfigMessage = null; ConfigError error = null;
if ( StringUtils.isBlank(queryString)) { if ( StringUtils.isBlank(queryString)) {
invalidConfigMessage = "Missing query specification."; error = ConfigError.NO_QUERY;
} else if ( StringUtils.isBlank(templateName)) { } else if ( StringUtils.isBlank(templateName)) {
invalidConfigMessage = "Missing template specification."; error = ConfigError.NO_TEMPLATE;
} else { } else {
Configuration fmConfig = (Configuration) vreq.getAttribute("freemarkerConfig"); Configuration fmConfig = (Configuration) vreq.getAttribute("freemarkerConfig");
TemplateLoader tl = fmConfig.getTemplateLoader(); TemplateLoader tl = fmConfig.getTemplateLoader();
try { try {
if ( tl.findTemplateSource(templateName) == null ) { if ( tl.findTemplateSource(templateName) == null ) {
invalidConfigMessage = "Specified template " + templateName + " does not exist."; error = ConfigError.TEMPLATE_NOT_FOUND;
} }
} catch (IOException e) { } catch (IOException e) {
log.error("Error finding template " + templateName, e); log.error("Error finding template " + templateName, e);
} }
} }
return invalidConfigMessage; return error;
} }
private void setValuesFromConfigFile(String configFilePath) { private void setValuesFromConfigFile(String configFilePath, ObjectProperty op) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db; DocumentBuilder db;
@ -332,8 +358,11 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
try { try {
db = dbf.newDocumentBuilder(); db = dbf.newDocumentBuilder();
Document doc = db.parse(configFilePath); Document doc = db.parse(configFilePath);
// Required values // Required values
queryString = getConfigValue(doc, NODE_NAME_QUERY); String queryNodeName = op.getCollateBySubclass() ? NODE_NAME_QUERY_COLLATED : NODE_NAME_QUERY_BASE;
log.debug("Using query element " + queryNodeName + " for object property " + op.getURI());
queryString = getConfigValue(doc, queryNodeName);
templateName = getConfigValue(doc, NODE_NAME_TEMPLATE); templateName = getConfigValue(doc, NODE_NAME_TEMPLATE);
// Optional values // Optional values
@ -362,11 +391,11 @@ public abstract class ObjectPropertyTemplateModel extends PropertyTemplateModel
} }
} }
protected class InvalidConfigurationException extends Exception { protected class InvalidCollatedPropertyConfigurationException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
protected InvalidConfigurationException(String s) { protected InvalidCollatedPropertyConfigurationException(String s) {
super(s); super(s);
} }
} }

View file

@ -18,7 +18,6 @@ import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
public class UncollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateModel { public class UncollatedObjectPropertyTemplateModel extends ObjectPropertyTemplateModel {
private static final Log log = LogFactory.getLog(UncollatedObjectPropertyTemplateModel.class); private static final Log log = LogFactory.getLog(UncollatedObjectPropertyTemplateModel.class);
private static final String DEFAULT_CONFIG_FILE = "listViewConfig-default-uncollated.xml";
private List<ObjectPropertyStatementTemplateModel> statements; private List<ObjectPropertyStatementTemplateModel> statements;
@ -47,11 +46,6 @@ public class UncollatedObjectPropertyTemplateModel extends ObjectPropertyTemplat
postprocessStatementList(statements); postprocessStatementList(statements);
} }
@Override
protected String getDefaultConfigFileName() {
return DEFAULT_CONFIG_FILE;
}
/* Access methods for templates */ /* Access methods for templates */
public List<ObjectPropertyStatementTemplateModel> getStatements() { public List<ObjectPropertyStatementTemplateModel> getStatements() {

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<!-- Default list view config file for uncollated object properties
See guidelines in vitro/doc/list_view_configuration_guidelines.txt -->
<list-view-config>
<query>
PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt;
PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
SELECT ?object ?name ?moniker {
GRAPH ?g1 { ?subject ?property ?object }
OPTIONAL { GRAPH ?g2 { ?object rdfs:label ?name } }
OPTIONAL { GRAPH ?g3 { ?object vitro:moniker ?moniker } }
}
</query>
<postprocessor>edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.DefaultListViewDataPostProcessor</postprocessor>
<template>propStatement-default.ftl</template>
</list-view-config>

View file

@ -1,12 +1,23 @@
<?xml version="1.0" encoding="ISO-8859-1"?> <?xml version="1.0" encoding="ISO-8859-1"?>
<!-- $This file is distributed under the terms of the license in /doc/license.txt$ --> <!-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<!-- Default list view config file for collated object properties <!-- Default list view config file for uncollated object properties
See guidelines in vitro/doc/list_view_configuration_guidelines.txt --> See guidelines in vitro/doc/list_view_configuration_guidelines.txt -->
<list-view-config> <list-view-config>
<query> <query-base>
PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt;
PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
SELECT ?object ?name ?moniker {
GRAPH ?g1 { ?subject ?property ?object }
OPTIONAL { GRAPH ?g2 { ?object rdfs:label ?name } }
OPTIONAL { GRAPH ?g3 { ?object vitro:moniker ?moniker } }
}
</query-base>
<query-collated>
PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt; PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt;
PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt; PREFIX rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt;
@ -18,8 +29,8 @@
FILTER (?g4 != &lt;http://vitro.mannlib.cornell.edu/default/inferred-tbox&gt; &amp;&amp; FILTER (?g4 != &lt;http://vitro.mannlib.cornell.edu/default/inferred-tbox&gt; &amp;&amp;
?g4 != &lt;http://vitro.mannlib.cornell.edu/default/vitro-kb-inf&gt; ) ?g4 != &lt;http://vitro.mannlib.cornell.edu/default/vitro-kb-inf&gt; )
} }
} } ORDER BY ?subclass
</query> </query-collated>
<postprocessor>edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.DefaultListViewDataPostProcessor</postprocessor> <postprocessor>edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.DefaultListViewDataPostProcessor</postprocessor>

View file

@ -6,17 +6,20 @@
See guidelines in vitro/doc/list_view_configuration_guidelines.txt --> See guidelines in vitro/doc/list_view_configuration_guidelines.txt -->
<list-view-config> <list-view-config>
<query> <query-base>
PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt; PREFIX vitro: &lt;http://vitro.mannlib.cornell.edu/ns/vitro/0.7#&gt;
PREFIX afn: &lt;http://jena.hpl.hp.com/ARQ/function#&gt; PREFIX afn: &lt;http://jena.hpl.hp.com/ARQ/function#&gt;
SELECT ?link (afn:localname(?link) AS ?linkName) ?anchor ?url WHERE { SELECT ?link
(afn:localname(?link) AS ?linkName)
?anchor
?url WHERE {
GRAPH ?g1 { ?subject ?property ?link } GRAPH ?g1 { ?subject ?property ?link }
OPTIONAL { GRAPH ?g2 { ?link vitro:linkAnchor ?anchor } } OPTIONAL { GRAPH ?g2 { ?link vitro:linkAnchor ?anchor } }
OPTIONAL { GRAPH ?g3 { ?link vitro:linkURL ?url } } OPTIONAL { GRAPH ?g3 { ?link vitro:linkURL ?url } }
OPTIONAL { GRAPH ?g4 { ?link vitro:linkDisplayRank ?rank } } OPTIONAL { GRAPH ?g4 { ?link vitro:linkDisplayRank ?rank } }
} ORDER BY ?rank } ORDER BY ?rank
</query> </query-base>
<template>propStatement-vitroLink.ftl</template> <template>propStatement-vitroLink.ftl</template>
</list-view-config> </list-view-config>