diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualController.java new file mode 100644 index 000000000..2e632159f --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualController.java @@ -0,0 +1,120 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ExceptionResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; + +/** + * Handles requests for entity information. + */ +public class IndividualController extends FreemarkerHttpServlet { + private static final Log log = LogFactory + .getLog(IndividualController.class); + + private static final String TEMPLATE_HELP = "individual-help.ftl"; + + /** + * Use this map to decide which MIME type is suited for the "accept" header. + */ + public static final Map ACCEPTED_CONTENT_TYPES = initializeContentTypes(); + private static Map initializeContentTypes() { + HashMap map = new HashMap(); + map.put(HTML_MIMETYPE, 0.5f); + map.put(XHTML_MIMETYPE, 0.5f); + map.put("application/xml", 0.5f); + map.put(RDFXML_MIMETYPE, 1.0f); + map.put(N3_MIMETYPE, 1.0f); + map.put(TTL_MIMETYPE, 1.0f); + return Collections.unmodifiableMap(map); + } + + @Override + protected ResponseValues processRequest(VitroRequest vreq) { + try { + /* + * What type of request is this? + */ + IndividualRequestInfo requestInfo = analyzeTheRequest(vreq); + + switch (requestInfo.getType()) { + case RDF_REDIRECT: + /* + * If someone expects RDF by asking for the individual with an + * "accept" HTTP header, redirect them to the preferred URL. + */ + return new RedirectResponseValues(requestInfo.getRedirectUrl(), + HttpServletResponse.SC_SEE_OTHER); + case NO_INDIVIDUAL: + /* + * If we can't figure out what individual you want, or if there + * is no such individual, show an informative error page. + */ + return doNotFound(); + case BYTESTREAM_REDIRECT: + /* + * If the Individual requested is a FileBytestream, redirect + * them to the direct download URL, so they will get the correct + * filename, etc. + */ + return new RedirectResponseValues(requestInfo.getRedirectUrl(), + HttpServletResponse.SC_SEE_OTHER); + case LINKED_DATA: + /* + * If they are asking for RDF using the preferred URL, give it + * to them. + */ + return new IndividualRdfAssembler(vreq, + requestInfo.getIndividual(), requestInfo.getRdfFormat()) + .assembleRdf(); + default: + /* + * Otherwise, prepare an HTML response for the requested + * individual. + */ + return new IndividualResponseBuilder(vreq, + requestInfo.getIndividual()).assembleResponse(); + } + } catch (Throwable e) { + log.error(e, e); + return new ExceptionResponseValues(e); + } + } + + private IndividualRequestInfo analyzeTheRequest(VitroRequest vreq) { + return new IndividualRequestAnalyzer(vreq, + new IndividualRequestAnalysisContextImpl(vreq)).analyze(); + } + + private ResponseValues doNotFound() { + Map body = new HashMap(); + body.put("title", "Individual Not Found"); + body.put("errorMessage", "The individual was not found in the system."); + + return new TemplateResponseValues(TEMPLATE_HELP, body, + HttpServletResponse.SC_NOT_FOUND); + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doGet(request, response); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRdfAssembler.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRdfAssembler.java new file mode 100644 index 000000000..b77331e87 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRdfAssembler.java @@ -0,0 +1,208 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.hp.hpl.jena.datatypes.TypeMapper; +import com.hp.hpl.jena.datatypes.xsd.XSDDatatype; +import com.hp.hpl.jena.ontology.OntModel; +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.ResourceFactory; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.rdf.model.StmtIterator; +import com.hp.hpl.jena.shared.Lock; +import com.hp.hpl.jena.vocabulary.RDF; +import com.hp.hpl.jena.vocabulary.RDFS; + +import edu.cornell.mannlib.vitro.webapp.beans.DataPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectPropertyStatement; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RdfResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.utils.jena.ExtendedLinkedDataUtils; +import edu.cornell.mannlib.vitro.webapp.utils.jena.JenaOutputUtils; +import edu.cornell.mannlib.vitro.webapp.web.ContentType; + +/** + * TODO See where this can be improved. + */ +public class IndividualRdfAssembler { + private static final Log log = LogFactory + .getLog(IndividualRdfAssembler.class); + + private static final String RICH_EXPORT_ROOT = "/WEB-INF/rich-export/"; + private static final String PERSON_CLASS_URI = "http://xmlns.com/foaf/0.1/Person"; + private static final String INCLUDE_ALL = "all"; + + @SuppressWarnings("serial") + private static final Map namespaces = new HashMap() {{ + put("display", VitroVocabulary.DISPLAY); + put("vitro", VitroVocabulary.vitroURI); + put("vitroPublic", VitroVocabulary.VITRO_PUBLIC); + }}; + + private static final Property extendedLinkedDataProperty = ResourceFactory.createProperty(namespaces.get("vitro") + "extendedLinkedData"); + private static final Literal xsdTrue = ResourceFactory.createTypedLiteral("true", XSDDatatype.XSDboolean); + + private final VitroRequest vreq; + private final ServletContext ctx; + private final Individual individual; + private final ContentType rdfFormat; + + public IndividualRdfAssembler(VitroRequest vreq, Individual individual, + ContentType rdfFormat) { + this.vreq = vreq; + this.ctx = vreq.getSession().getServletContext(); + this.individual = individual; + this.rdfFormat = rdfFormat; + } + + /** + * @return + */ + public ResponseValues assembleRdf() { + OntModel ontModel = vreq.getJenaOntModel(); + + String[] includes = vreq.getParameterValues("include"); + Model newModel = getRDF(individual, ontModel, ModelFactory.createDefaultModel(), 0, includes); + JenaOutputUtils.setNameSpacePrefixes(newModel, vreq.getWebappDaoFactory()); + return new RdfResponseValues(rdfFormat, newModel); + } + + private Model getRDF(Individual entity, OntModel contextModel, Model newModel, int recurseDepth, String[] includes) { + + Resource subj = newModel.getResource(entity.getURI()); + + List dstates = entity.getDataPropertyStatements(); + TypeMapper typeMapper = TypeMapper.getInstance(); + for (DataPropertyStatement ds: dstates) { + Property dp = newModel.getProperty(ds.getDatapropURI()); + Literal lit = null; + if ((ds.getLanguage()) != null && (ds.getLanguage().length()>0)) { + lit = newModel.createLiteral(ds.getData(),ds.getLanguage()); + } else if ((ds.getDatatypeURI() != null) && (ds.getDatatypeURI().length()>0)) { + lit = newModel.createTypedLiteral(ds.getData(),typeMapper.getSafeTypeByName(ds.getDatatypeURI())); + } else { + lit = newModel.createLiteral(ds.getData()); + } + newModel.add(newModel.createStatement(subj, dp, lit)); + } + + if (recurseDepth < 5) { + List ostates = entity.getObjectPropertyStatements(); + + for (ObjectPropertyStatement os: ostates) { + Property prop = newModel.getProperty(os.getPropertyURI()); + Resource obj = newModel.getResource(os.getObjectURI()); + newModel.add(newModel.createStatement(subj, prop, obj)); + if ( includeInLinkedData(obj, contextModel)) { + newModel.add(getRDF(os.getObject(), contextModel, newModel, recurseDepth + 1, includes)); + } else { + contextModel.enterCriticalSection(Lock.READ); + try { + newModel.add(contextModel.listStatements(obj, RDFS.label, (RDFNode)null)); + } finally { + contextModel.leaveCriticalSection(); + } + } + } + } + + newModel = getLabelAndTypes(entity, contextModel, newModel ); + + // get all the statements not covered by the object property / datatype property code above + // note implication that extendedLinkedData individuals will only be evaluated for the + // recognized object properties. + contextModel.enterCriticalSection(Lock.READ); + try { + StmtIterator iter = contextModel.listStatements(subj, (Property) null, (RDFNode) null); + while (iter.hasNext()) { + Statement stmt = iter.next(); + if (!newModel.contains(stmt)) { + newModel.add(stmt); + } + } + } finally { + contextModel.leaveCriticalSection(); + } + + if (recurseDepth == 0 && includes != null && entity.isVClass(PERSON_CLASS_URI)) { + + for (String include : includes) { + + String rootDir = null; + if (INCLUDE_ALL.equals(include)) { + rootDir = RICH_EXPORT_ROOT; + } else { + rootDir = RICH_EXPORT_ROOT + include + "/"; + } + + long start = System.currentTimeMillis(); + Model extendedModel = ExtendedLinkedDataUtils.createModelFromQueries(ctx, rootDir, contextModel, entity.getURI()); + long elapsedTimeMillis = System.currentTimeMillis()-start; + log.info("Time to create rich export model: msecs = " + elapsedTimeMillis); + + newModel.add(extendedModel); + } + } + + return newModel; + } + + public static boolean includeInLinkedData(Resource object, Model contextModel) { + + boolean retval = false; + + contextModel.enterCriticalSection(Lock.READ); + + try { + StmtIterator iter = contextModel.listStatements(object, RDF.type, (RDFNode)null); + + while (iter.hasNext()) { + Statement stmt = iter.next(); + + if (stmt.getObject().isResource() && contextModel.contains(stmt.getObject().asResource(), extendedLinkedDataProperty, xsdTrue)) { + retval = true; + break; + } + } + } finally { + contextModel.leaveCriticalSection(); + } + + return retval; + } + + /* Get the properties that are difficult to get via a filtered WebappDaoFactory. */ + private Model getLabelAndTypes(Individual entity, Model ontModel, Model newModel){ + for( VClass vclass : entity.getVClasses()){ + newModel.add(newModel.getResource(entity.getURI()), RDF.type, newModel.getResource(vclass.getURI())); + } + + ontModel.enterCriticalSection(Lock.READ); + try { + newModel.add(ontModel.listStatements(ontModel.getResource(entity.getURI()), RDFS.label, (RDFNode)null)); + } finally { + ontModel.leaveCriticalSection(); + } + + return newModel; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContext.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContext.java new file mode 100644 index 000000000..f822360cc --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContext.java @@ -0,0 +1,43 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; + +/** + * Pull the fiddly-bits out of the IndividualRequestAnalyzer to make it easier + * to test. + */ +public interface IndividualRequestAnalysisContext { + + /** + * What is the default namespace for the application? + */ + String getDefaultNamespace(); + + /** + * Is there a namespace for this prefix? If not, return an empty string, but + * never null. + */ + String getNamespaceForPrefix(String prefix); + + /** + * Use the IndividualDao to get this individual. + * + * If the URI is null, or if no such Individual exists, return null. + */ + Individual getIndividualByURI(String individualUri); + + /** + * If there is a user with this netID, and if they have a profile, return + * that Individual. Otherwise, return null. + */ + Individual getIndividualByNetId(String netId); + + /** + * If this Individual represents a File Bytestream, get the Alias URL + * associated with it. Otherwise, return null. + */ + String getAliasUrlForBytestreamIndividual(Individual individual); + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContextImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContextImpl.java new file mode 100644 index 000000000..cf7a8c8d6 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalysisContextImpl.java @@ -0,0 +1,92 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import java.util.List; + +import javax.servlet.ServletContext; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.SelfEditingConfiguration; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.filestorage.model.FileInfo; +import edu.cornell.mannlib.vitro.webapp.utils.NamespaceMapper; +import edu.cornell.mannlib.vitro.webapp.utils.NamespaceMapperFactory; + +/** + * Implement all of the fiddly-bits that we need for analyzing the request for + * an individual, but that we do not want to do in unit tests. + */ +public class IndividualRequestAnalysisContextImpl implements + IndividualRequestAnalysisContext { + private final VitroRequest vreq; + private final ServletContext ctx; + private final WebappDaoFactory wadf; + private final IndividualDao iDao; + + public IndividualRequestAnalysisContextImpl(VitroRequest vreq) { + this.vreq = vreq; + this.ctx = vreq.getSession().getServletContext(); + this.wadf = vreq.getWebappDaoFactory(); + this.iDao = wadf.getIndividualDao(); + } + + @Override + public String getDefaultNamespace() { + return wadf.getDefaultNamespace(); + } + + @Override + public String getNamespaceForPrefix(String prefix) { + if (prefix == null) { + return ""; + } + + NamespaceMapper namespaceMapper = NamespaceMapperFactory + .getNamespaceMapper(ctx); + String ns = namespaceMapper.getNamespaceForPrefix(prefix); + + return (ns == null) ? "" : ns; + } + + @Override + public Individual getIndividualByURI(String individualUri) { + if (individualUri == null) { + return null; + } + return iDao.getIndividualByURI(individualUri); + } + + @Override + public Individual getIndividualByNetId(String netId) { + if (netId == null) { + return null; + } + + SelfEditingConfiguration sec = SelfEditingConfiguration.getBean(vreq); + List assocInds = sec.getAssociatedIndividuals(iDao, netId); + if (!assocInds.isEmpty()) { + return assocInds.get(0); + } else { + return null; + } + } + + @Override + public String getAliasUrlForBytestreamIndividual(Individual individual) { + if (individual == null) { + return null; + } + + FileInfo fileInfo = FileInfo.instanceFromBytestreamUri(wadf, + individual.getURI()); + if (fileInfo == null) { + return null; + } + + return fileInfo.getBytestreamAliasUrl(); + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzer.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzer.java new file mode 100644 index 000000000..2d5bad4a3 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzer.java @@ -0,0 +1,312 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.N3_MIMETYPE; +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.RDFXML_MIMETYPE; +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.TTL_MIMETYPE; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.web.ContentType; + +/** + * All sorts of requests are fielded by the IndividualController. Look at this + * request and figure out what type it is, and what data we need in order to + * respond. + */ +public class IndividualRequestAnalyzer { + private static final Log log = LogFactory + .getLog(IndividualRequestAnalyzer.class); + + + private static Pattern RDF_REQUEST = Pattern.compile("^/individual/([^/]+)/\\1\\.(rdf|n3|ttl)$"); + private static Pattern HTML_REQUEST = Pattern.compile("^/display/([^/]+)$"); + private static Pattern LINKED_DATA_URL = Pattern.compile("^/individual/([^/]+)$"); + private static Pattern NS_PREFIX_URL = Pattern.compile("^/individual/([^/]*)/([^/]+)$"); + + private final VitroRequest vreq; + private final IndividualRequestAnalysisContext analysisContext; + private final String url; + + public IndividualRequestAnalyzer(VitroRequest vreq, + IndividualRequestAnalysisContext analysisContext) { + this.vreq = vreq; + + // get URL without hostname or servlet context + this.url = vreq.getRequestURI().substring( + vreq.getContextPath().length()); + + this.analysisContext = analysisContext; + } + + public IndividualRequestInfo analyze() { + // If this is a request for RDF using an accept header, redirect it to + // the preferred URL. + String redirectUrl = checkForRedirect(); + if (redirectUrl != null) { + return IndividualRequestInfo.buildRdfRedirectInfo(redirectUrl); + } + + // Figure out what individual we are talking about. If we can't figure + // it out, complain. + Individual individual = getIndividualFromRequest(); + if (individual == null) { + return IndividualRequestInfo.buildNoIndividualInfo(); + } + + // If the requested individual is a FileBytestream, redirect to its + // "alias URL". + redirectUrl = getAliasUrlForBytestreamIndividual(individual); + if (redirectUrl != null) { + return IndividualRequestInfo.buildBytestreamRedirectInfo(redirectUrl); + } + + // Check to see whether Linked Data was requested. + ContentType rdfFormat = checkUrlForLinkedDataRequest(); + if (rdfFormat != null) { + return IndividualRequestInfo.buildLinkedDataInfo(individual, rdfFormat); + } + + // No redirect, no Linked Data; no problem. + return IndividualRequestInfo.buildDefaultInfo(individual); + } + + /* + * Following recipe 3 from + * "Best Practice Recipes for Publishing RDF Vocabularies." See + * http://www.w3.org/TR/swbp-vocab-pub/#recipe3. The basic idea is that a + * URI like http://vivo.cornell.edu/individual/n1234 identifies a real world + * individual. HTTP cannot send that as the response to a GET request + * because it can only send bytes and not things. The server sends a 303, to + * mean "you asked for something I cannot send you, but I can send you this + * other stream of bytes about that thing." In the case of a request like + * http://vivo.cornell.edu/individual/n1234/n1234.rdf or + * http://vivo.cornell.edu/individual/n1234?format=rdfxml, the request is + * for a set of bytes rather than an individual, so no 303 is needed. + */ + private static Pattern URI_PATTERN = Pattern + .compile("^/individual/([^/]*)$"); + + private String checkForRedirect() { + // A "format" parameter is special, and is dealt with elsewhere. + String formatParam = getRequestParameter("format", ""); + if (!formatParam.isEmpty()) { + return null; + } + + // Is it "/individual/" followed by a single group? + Matcher m = URI_PATTERN.matcher(url); + if (!m.matches() || m.groupCount() < 1) { + return null; + } + + // Then, use the "accept" header to decide how to redirect it. + ContentType c = checkAcceptHeaderForLinkedDataRequest(); + if (c != null) { + String mediaType = c.getMediaType(); + if (RDFXML_MIMETYPE.equals(mediaType)) { + return "/individual/" + m.group(1) + "/" + m.group(1) + ".rdf"; + } else if (N3_MIMETYPE.equals(mediaType)) { + return "/individual/" + m.group(1) + "/" + m.group(1) + ".n3"; + } else if (TTL_MIMETYPE.equals(mediaType)) { + return "/individual/" + m.group(1) + "/" + m.group(1) + ".ttl"; + } + } + // or redirect to the canonical URL for HTML representation. + return "/display/" + m.group(1); + } + + /** + * Check the accept header. This request will trigger a redirect with a 303 + * ("see also"), because the request is for an individual but the server can + * only provide a set of bytes. + */ + protected ContentType checkAcceptHeaderForLinkedDataRequest() { + String acceptHeader = vreq.getHeader("accept"); + if (acceptHeader == null) { + return null; + } + + try { + Map typesAndQ = ContentType + .getTypesAndQ(acceptHeader); + String ctStr = ContentType.getBestContentType(typesAndQ, + IndividualController.ACCEPTED_CONTENT_TYPES); + + if (RDFXML_MIMETYPE.equals(ctStr) || N3_MIMETYPE.equals(ctStr) + || TTL_MIMETYPE.equals(ctStr)) { + return new ContentType(ctStr); + } + } catch (Throwable th) { + log.error("Problem while checking accept header ", th); + } + return null; + } + + /** + * Gets the entity id from the request. Works for the following styles of + * URLs: + * + *
+	 *     /individual?uri=urlencodedURI
+	 *     /individual?netId=bdc34
+	 *     /individual?netid=bdc34
+	 *     /individual/localname         
+	 *     /display/localname
+	 *     /individual/localname/localname.rdf
+	 *     /individual/localname/localname.n3
+	 *     /individual/localname/localname.ttl
+	 *     /individual/nsprefix/localname
+	 * 
+ * + * @return null on failure. + */ + public Individual getIndividualFromRequest() { + try { + // Check for "uri" parameter. + String uri = getRequestParameter("uri", ""); + if (!uri.isEmpty()) { + return getIndividualByUri(uri); + } + + // Check for "netId" or "netid" parameter + String netId = getRequestParameter("netId", + getRequestParameter("netid", "")); + if (!netId.isEmpty()) { + return getIndividualByNetId(netId); + } + + // Check for just a local name + Matcher linkedDataMatch = LINKED_DATA_URL.matcher(url); + if (linkedDataMatch.matches() && linkedDataMatch.groupCount() == 1) { + return getIndividualByLocalname(linkedDataMatch.group(1)); + } + + // Check for the canonical HTML request. + Matcher htmlMatch = HTML_REQUEST.matcher(url); + if (htmlMatch.matches() && htmlMatch.groupCount() == 1) { + return getIndividualByLocalname(htmlMatch.group(1)); + } + + // Check for a request for RDF. + Matcher rdfMatch = RDF_REQUEST.matcher(url); + if (rdfMatch.matches() && rdfMatch.groupCount() == 2) { + return getIndividualByLocalname(rdfMatch.group(1)); + } + + // Does the URL look like a namespace prefix followed by a local + // name? + Matcher prefix_match = NS_PREFIX_URL.matcher(url); + if (prefix_match.matches() && prefix_match.groupCount() == 2) { + return getIndividualByPrefixAndLocalname(prefix_match.group(1), + prefix_match.group(2)); + } + + // Couldn't match it to anything. + return null; + } catch (Throwable e) { + log.error("Problems trying to find Individual", e); + return null; + } + } + + private String getAliasUrlForBytestreamIndividual(Individual individual) { + String aliasUrl = analysisContext.getAliasUrlForBytestreamIndividual(individual); + + if (individual.getURI().equals(aliasUrl)) { + // Avoid a tight loop; if the alias URL is equal to the URI, + // then don't recognize it as a FileBytestream. + return null; + } else { + return aliasUrl; + } + } + + /** + * @return null if this is not a linked data request, returns content type + * if it is a linked data request. + * + * These are Vitro-specific ways of requesting rdf, unrelated to + * semantic web standards. They do not trigger a redirect with a + * 303, because the request is for a set of bytes rather than an + * individual. + */ + protected ContentType checkUrlForLinkedDataRequest() { + /* + * Check for url param specifying format. Example: + * http://vivo.cornell.edu/individual/n23?format=rdfxml + */ + String formatParam = getRequestParameter("format", ""); + if (formatParam.contains("rdfxml")) { + return ContentType.RDFXML; + } + if (formatParam.contains("n3")) { + return ContentType.N3; + } + if (formatParam.contains("ttl")) { + return ContentType.TURTLE; + } + + /* + * Check for parts of URL that indicate request for RDF. Examples: + * http://vivo.cornell.edu/individual/n23/n23.rdf + * http://vivo.cornell.edu/individual/n23/n23.n3 + * http://vivo.cornell.edu/individual/n23/n23.ttl + */ + Matcher rdfMatch = RDF_REQUEST.matcher(url); + if (rdfMatch.matches() && rdfMatch.groupCount() == 2) { + String rdfType = rdfMatch.group(2); + if ("rdf".equals(rdfType)) { + return ContentType.RDFXML; + } + if ("n3".equals(rdfType)) { + return ContentType.N3; + } + if ("ttl".equals(rdfType)) { + return ContentType.TURTLE; + } + } + + return null; + } + + + + private String getRequestParameter(String key, String defaultValue) { + String value = vreq.getParameter(key); + if ((value == null) || value.isEmpty()) { + return defaultValue; + } else { + return value; + } + } + + private Individual getIndividualByUri(String uri) { + return analysisContext.getIndividualByURI(uri); + } + + private Individual getIndividualByLocalname(String localname) { + String defaultNamespace = analysisContext.getDefaultNamespace(); + String uri = defaultNamespace + localname; + return getIndividualByUri(uri); + } + + private Individual getIndividualByPrefixAndLocalname(String prefix, + String localName) { + String ns = analysisContext.getNamespaceForPrefix(prefix); + return getIndividualByUri(ns + localName); + } + + private Individual getIndividualByNetId(String netId) { + return analysisContext.getIndividualByNetId(netId); + } +} + diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestInfo.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestInfo.java new file mode 100644 index 000000000..05e8d785d --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestInfo.java @@ -0,0 +1,95 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.web.ContentType; + +/** + * All sorts of requests are fielded by the IndividualController. This is the + * essence of such a the request. + */ +public class IndividualRequestInfo { + public enum Type { + RDF_REDIRECT, // Redirect a requst for RDF to the preferred URL. + BYTESTREAM_REDIRECT, // Redirect a request for file contents. + NO_INDIVIDUAL, // The requested individual doesn't exist. + LINKED_DATA, // Requesting RDF for this individual. + DEFAULT // Requesting HTML response for this individual. + } + + public static IndividualRequestInfo buildRdfRedirectInfo(String redirectUrl) { + return new IndividualRequestInfo(Type.RDF_REDIRECT, redirectUrl, null, + null); + } + + public static IndividualRequestInfo buildBytestreamRedirectInfo( + String redirectUrl) { + return new IndividualRequestInfo(Type.BYTESTREAM_REDIRECT, redirectUrl, + null, null); + } + + public static IndividualRequestInfo buildNoIndividualInfo() { + return new IndividualRequestInfo(Type.NO_INDIVIDUAL, null, null, null); + } + + public static IndividualRequestInfo buildLinkedDataInfo( + Individual individual, ContentType rdfFormat) { + return new IndividualRequestInfo(Type.LINKED_DATA, null, individual, + rdfFormat); + } + + public static IndividualRequestInfo buildDefaultInfo(Individual individual) { + return new IndividualRequestInfo(Type.DEFAULT, null, individual, null); + } + + private final Type type; + private final String redirectUrl; + private final Individual individual; + private final ContentType rdfFormat; + + private IndividualRequestInfo(Type type, String redirectUrl, + Individual individual, ContentType rdfFormat) { + if (type == null) { + throw new NullPointerException("type may not be null."); + } + + if (((type == Type.RDF_REDIRECT) || (type == Type.BYTESTREAM_REDIRECT)) + && (redirectUrl == null)) { + throw new NullPointerException( + "redirectUrl may not be null if type is " + type + "."); + } + + if (((type == Type.LINKED_DATA) || (type == Type.DEFAULT)) + && (individual == null)) { + throw new NullPointerException( + "individual may not be null if type is " + type + "."); + } + + if ((type == Type.LINKED_DATA) && (rdfFormat == null)) { + throw new NullPointerException( + "rdfFormat may not be null if type is " + type + "."); + } + + this.type = type; + this.redirectUrl = redirectUrl; + this.individual = individual; + this.rdfFormat = rdfFormat; + } + + public Type getType() { + return this.type; + } + + public String getRedirectUrl() { + return this.redirectUrl; + } + + public Individual getIndividual() { + return this.individual; + } + + public ContentType getRdfFormat() { + return this.rdfFormat; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualResponseBuilder.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualResponseBuilder.java new file mode 100644 index 000000000..ee4e28960 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualResponseBuilder.java @@ -0,0 +1,207 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import java.util.HashMap; +import java.util.Map; + +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.ObjectProperty; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +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.dao.DisplayVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; +import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; +import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.web.beanswrappers.ReadOnlyBeansWrapper; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.individual.IndividualTemplateModel; +import edu.cornell.mannlib.vitro.webapp.web.templatemodels.individuallist.ListedIndividual; +import freemarker.ext.beans.BeansWrapper; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +/** + * We have determined that the request is for a normal Individual, and needs an + * HTML response. Assemble the information for that response. + * + * TODO clean this up. + */ +class IndividualResponseBuilder { + private static final Map namespaces = new HashMap() {{ + put("display", VitroVocabulary.DISPLAY); + put("vitro", VitroVocabulary.vitroURI); + put("vitroPublic", VitroVocabulary.VITRO_PUBLIC); + }}; + + private final VitroRequest vreq; + private final WebappDaoFactory wadf; + private final IndividualDao iDao; + private final ObjectPropertyDao opDao; + + private final Individual individual; + + public IndividualResponseBuilder(VitroRequest vreq, Individual individual) { + this.vreq = vreq; + this.wadf = vreq.getWebappDaoFactory(); + this.iDao = wadf.getIndividualDao(); + this.opDao = wadf.getObjectPropertyDao(); + + this.individual = individual; + } + + ResponseValues assembleResponse() throws TemplateModelException { + Map body = new HashMap(); + + body.put("title", individual.getName()); + body.put("relatedSubject", getRelatedSubject()); + body.put("namespaces", namespaces); + body.put("temporalVisualizationEnabled", getTemporalVisualizationFlag()); + body.put("verbosePropertySwitch", getVerbosePropertyValues()); + + IndividualTemplateModel itm = getIndividualTemplateModel(individual); + /* We need to expose non-getters in displaying the individual's property list, + * since it requires calls to methods with parameters. + * This is still safe, because we are only putting BaseTemplateModel objects + * into the data model: no real data can be modified. + */ + // body.put("individual", wrap(itm, BeansWrapper.EXPOSE_SAFE)); + body.put("individual", wrap(itm, new ReadOnlyBeansWrapper())); + + body.put("headContent", getRdfLinkTag(itm)); + + //If special values required for individuals like menu, include values in template values + body.putAll(getSpecialEditingValues()); + + String template = new IndividualTemplateLocator(vreq, individual).findTemplate(); + + return new TemplateResponseValues(template, body); + } + + /** + * Check if a "relatedSubjectUri" parameter has been supplied, and, if so, + * retrieve the related individual. + * + * Some individuals make little sense standing alone and should be displayed + * in the context of their relationship to another. + */ + private Map getRelatedSubject() { + Map map = null; + + String relatedSubjectUri = vreq.getParameter("relatedSubjectUri"); + if (relatedSubjectUri != null) { + Individual relatedSubjectInd = iDao.getIndividualByURI(relatedSubjectUri); + if (relatedSubjectInd != null) { + map = new HashMap(); + map.put("name", relatedSubjectInd.getName()); + + // TODO find out which of these values is the correct one + map.put("url", UrlBuilder.getIndividualProfileUrl(relatedSubjectInd, vreq)); + map.put("url", (new ListedIndividual(relatedSubjectInd, vreq)).getProfileUrl()); + + String relatingPredicateUri = vreq.getParameter("relatingPredicateUri"); + if (relatingPredicateUri != null) { + ObjectProperty relatingPredicateProp = opDao.getObjectPropertyByURI(relatingPredicateUri); + if (relatingPredicateProp != null) { + map.put("relatingPredicateDomainPublic", relatingPredicateProp.getDomainPublic()); + } + } + } + } + return map; + } + + private boolean getTemporalVisualizationFlag() { + String property = ConfigurationProperties.getBean(vreq).getProperty( + "visualization.temporal"); + return "enabled".equals(property); + } + + private Map getVerbosePropertyValues() { + Map map = null; + + if (PolicyHelper.isAuthorizedForActions(vreq, SimplePermission.SEE_VERBOSE_PROPERTY_INFORMATION.ACTIONS)) { + // Get current verbose property display value + String verbose = vreq.getParameter("verbose"); + Boolean verboseValue; + // If the form was submitted, get that value + if (verbose != null) { + verboseValue = "true".equals(verbose); + // If form not submitted, get the session value + } else { + Boolean verbosePropertyDisplayValueInSession = (Boolean) vreq.getSession().getAttribute("verbosePropertyDisplay"); + // True if session value is true, otherwise (session value is false or null) false + verboseValue = Boolean.TRUE.equals(verbosePropertyDisplayValueInSession); + } + vreq.getSession().setAttribute("verbosePropertyDisplay", verboseValue); + + map = new HashMap(); + map.put("currentValue", verboseValue); + + /* Factors contributing to switching from a form to an anchor element: + - Can't use GET with a query string on the action unless there is no form data, since + the form data is appended to the action with a "?", so there can't already be a query string + on it. + - The browser (at least Firefox) does not submit a form that has no form data. + - Some browsers might strip the query string off the form action of a POST - though + probably they shouldn't, because the HTML spec allows a full URI as a form action. + - Given these three, the only reliable solution is to dynamically create hidden inputs + for the query parameters. + - Much simpler is to just create an anchor element. This has the added advantage that the + browser doesn't ask to resend the form data when reloading the page. + */ + String url = vreq.getRequestURI() + "?verbose=" + !verboseValue; + // Append request query string, except for current verbose value, to url + String queryString = vreq.getQueryString(); + if (queryString != null) { + String[] params = queryString.split("&"); + for (String param : params) { + if (! param.startsWith("verbose=")) { + url += "&" + param; + } + } + } + map.put("url", url); + } else { + vreq.getSession().setAttribute("verbosePropertyDisplay", false); + } + + return map; + } + + private IndividualTemplateModel getIndividualTemplateModel( + Individual individual) { + individual.sortForDisplay(); + return new IndividualTemplateModel(individual, vreq); + } + + private TemplateModel wrap(Object obj, BeansWrapper wrapper) throws TemplateModelException { + return wrapper.wrap(obj); + } + + private String getRdfLinkTag(IndividualTemplateModel itm) { + String linkTag = null; + String linkedDataUrl = itm.getRdfUrl(); + if (linkedDataUrl != null) { + linkTag = " "; + } + return linkTag; + } + + //Get special values for cases such as Menu Management editing + private Map getSpecialEditingValues() { + Map map = new HashMap(); + + if(vreq.getAttribute(VitroRequest.SPECIAL_WRITE_MODEL) != null) { + map.put("reorderUrl", UrlBuilder.getUrl(DisplayVocabulary.REORDER_MENU_URL)); + } + + return map; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualTemplateLocator.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualTemplateLocator.java new file mode 100644 index 000000000..84e811051 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualTemplateLocator.java @@ -0,0 +1,118 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import java.util.List; + +import javax.servlet.ServletContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.beans.VClass; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; +import edu.cornell.mannlib.vitro.webapp.reasoner.SimpleReasoner; + +/** + * Figure out which Freemarker template to use when displaying this individual. + * + * If one of the classes of the individual (or one of the superclasses) has a + * custom template associated with it, use the first one that we find. + * Otherwise, use the default template. + * + * TODO examine the logic in this class. Is there anything we can get rid of? + */ +class IndividualTemplateLocator { + private static final Log log = LogFactory + .getLog(IndividualTemplateLocator.class); + + private static final String TEMPLATE_INDIVIDUAL_DEFAULT = "individual.ftl"; + + private final VitroRequest vreq; + private final ServletContext ctx; + + private final Individual individual; + + public IndividualTemplateLocator(VitroRequest vreq, Individual individual) { + this.vreq = vreq; + this.ctx = vreq.getSession().getServletContext(); + + this.individual = individual; + } + + // Determine whether the individual has a custom display template based on its class membership. + // If not, return the default individual template. + String findTemplate() { + + @SuppressWarnings("unused") + String vclassName = "unknown"; + String customTemplate = null; + + // First check vclass + if( individual.getVClass() != null ){ + vclassName = individual.getVClass().getName(); + List directClasses = individual.getVClasses(true); + for (VClass vclass : directClasses) { + customTemplate = vclass.getCustomDisplayView(); + if (customTemplate != null) { + if (customTemplate.length()>0) { + vclassName = vclass.getName(); // reset entity vclassname to name of class where a custom view; this call has side-effects + log.debug("Found direct class [" + vclass.getName() + "] with custom view " + customTemplate + "; resetting entity vclassName to this class"); + break; + } else { + customTemplate = null; + } + } + } + // If no custom template defined, check other vclasses + if (customTemplate == null) { + List inferredClasses = individual.getVClasses(false); + for (VClass vclass : inferredClasses) { + customTemplate = vclass.getCustomDisplayView(); + if (customTemplate != null) { + if (customTemplate.length()>0) { + // note that NOT changing entity vclassName here yet + log.debug("Found inferred class [" + vclass.getName() + "] with custom view " + customTemplate); + break; + } else { + customTemplate = null; + } + } + } + } + // If still no custom template defined, and inferencing is asynchronous (under RDB), check + // the superclasses of the vclass for a custom template specification. + SimpleReasoner simpleReasoner = (SimpleReasoner) ctx.getAttribute(SimpleReasoner.class.getName()); + if (customTemplate == null && simpleReasoner != null && simpleReasoner.isABoxReasoningAsynchronous()) { + log.debug("Checking superclasses for custom template specification because ABox reasoning is asynchronous"); + for (VClass directVClass : directClasses) { + VClassDao vcDao = vreq.getWebappDaoFactory().getVClassDao(); + List superClassUris = vcDao.getAllSuperClassURIs(directVClass.getURI()); + for (String uri : superClassUris) { + VClass vclass = vcDao.getVClassByURI(uri); + customTemplate = vclass.getCustomDisplayView(); + if (customTemplate != null) { + if (customTemplate.length()>0) { + // note that NOT changing entity vclassName here + log.debug("Found superclass [" + vclass.getName() + "] with custom view " + customTemplate); + break; + } else { + customTemplate = null; + } + } + } + } + } + } else if (individual.getVClassURI() != null) { + log.debug("Individual " + individual.getURI() + " with class URI " + + individual.getVClassURI() + ": no class found with that URI"); + } + + return customTemplate != null ? customTemplate : TEMPLATE_INDIVIDUAL_DEFAULT; + + } + + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzerTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzerTest.java new file mode 100644 index 000000000..4575901a0 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/individual/IndividualRequestAnalyzerTest.java @@ -0,0 +1,480 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.individual; + +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.N3_MIMETYPE; +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.RDFXML_MIMETYPE; +import static edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet.TTL_MIMETYPE; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import stubs.edu.cornell.mannlib.vitro.webapp.beans.IndividualStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.beans.Individual; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.web.ContentType; + +/** + * Are we able to figure out what sort of Individual request this is? + */ +public class IndividualRequestAnalyzerTest extends AbstractTestClass { + + /** + * Info about the application. + */ + private static final String URL_HOME_PAGE = "http://vivo.mydomain.edu"; + private static final String URL_INDIVIDUAL_PAGE = URL_HOME_PAGE + + "/individual"; + private static final String DEFAULT_NAMESPACE = URL_INDIVIDUAL_PAGE + "/"; + + /** + * Info about the individual that we're testing (mostly). + */ + private static final String ID_INDIVIDUAL_TEST = "testId"; + private static final String URI_INDIVIDUAL_TEST = DEFAULT_NAMESPACE + + ID_INDIVIDUAL_TEST; + private static final String NETID_USER_TEST = "joeUser"; + + /** + * Info about the file bytestream that appears in one test. + */ + /** The ID of an Individual that represents a FileBytestream object. */ + private static final String ID_FILE_BYTESTREAM = "bytesId"; + private static final String URI_FILE_BYTESTREAM = DEFAULT_NAMESPACE + + ID_FILE_BYTESTREAM; + private static final String BYTESTREAM_FILENAME = "imageFilename.jpg"; + private static final String URL_BYTESTREAM_ALIAS = URL_HOME_PAGE + "/file/" + + ID_FILE_BYTESTREAM + "/" + BYTESTREAM_FILENAME; + + /** + * Info about an individual that appears in a different namespace. + */ + private static final String SOME_PREFIX = "somePrefix"; + private static final String SOME_NAMESPACE = "http://some.namespace/"; + private static final String ID_INDIVIDUAL_FOREIGN = "foreignId"; + private static final String URI_INDIVIDUAL_FOREIGN = SOME_NAMESPACE + + ID_INDIVIDUAL_FOREIGN; + + private IndividualRequestAnalyzer analyzer; + private MyAnalysisContext analysisContext; + private HttpServletRequestStub req; + private VitroRequest vreq; + private IndividualRequestInfo requestInfo; + + private IndividualStub testIndividual; + private IndividualStub bytestreamIndividual; + private IndividualStub foreignIndividual; + + @Before + public void setup() { + req = new HttpServletRequestStub(); + analysisContext = new MyAnalysisContext(DEFAULT_NAMESPACE); + + testIndividual = new IndividualStub(URI_INDIVIDUAL_TEST); + analysisContext.addIndividual(testIndividual); + analysisContext.addProfilePage(NETID_USER_TEST, testIndividual); + + bytestreamIndividual = new IndividualStub(URI_FILE_BYTESTREAM); + analysisContext.addIndividual(bytestreamIndividual); + analysisContext.setAliasUrl(URI_FILE_BYTESTREAM, URL_BYTESTREAM_ALIAS); + + foreignIndividual = new IndividualStub(URI_INDIVIDUAL_FOREIGN); + analysisContext.addIndividual(foreignIndividual); + analysisContext.setNamespacePrefix(SOME_PREFIX, SOME_NAMESPACE); + } + + // ---------------------------------------------------------------------- + // Tests - locate by parameter + // ---------------------------------------------------------------------- + + /** /individual?uri=urlencodedURI */ + @Test + public void findIndividualByUriParameter() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("uri", URI_INDIVIDUAL_TEST); + analyzeIt(); + assertDefaultRequestInfo("find by URI parameter", URI_INDIVIDUAL_TEST); + } + + /** /individual?netId=bdc34 */ + @Test + public void findIndividualByNetIdParameter() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("netId", NETID_USER_TEST); + analyzeIt(); + assertDefaultRequestInfo("find by netId parameter", URI_INDIVIDUAL_TEST); + } + + /** /individual?netid=bdc34 */ + @Test + public void findIndividualByNetidParameter() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("netid", NETID_USER_TEST); + analyzeIt(); + assertDefaultRequestInfo("find by netid parameter", URI_INDIVIDUAL_TEST); + } + + // ---------------------------------------------------------------------- + // Tests - miscellaneous + // ---------------------------------------------------------------------- + + /** /display/localname */ + @Test + public void findIndividualByDisplayPath() { + req.setRequestUrl(url(URL_HOME_PAGE + "/display/" + ID_INDIVIDUAL_TEST)); + analyzeIt(); + assertDefaultRequestInfo("find by display path", URI_INDIVIDUAL_TEST); + } + + /** /individual/nsPrefix/localname */ + @Test + public void findByPrefixAndLocalname() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + SOME_PREFIX + "/" + + ID_INDIVIDUAL_FOREIGN)); + analyzeIt(); + assertDefaultRequestInfo("find by prefix and localname", + URI_INDIVIDUAL_FOREIGN); + } + + /** /individual/a/b/c fails. */ + @Test + public void unrecognizedPath() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + "this/that/theOther")); + analyzeIt(); + assertNoIndividualRequestInfo("unrecognized path"); + } + + /** /display/localname but no such individual */ + @Test + public void findNoSuchIndividualByDisplayPath() { + req.setRequestUrl(url(URL_HOME_PAGE + "/display/" + "bogusID")); + analyzeIt(); + assertNoIndividualRequestInfo("unrecognized ID"); + } + + // ---------------------------------------------------------------------- + // Tests - redirect a FileBytestream + // ---------------------------------------------------------------------- + + @Test + public void redirectAFileBytestreamIndividual() { + req.setRequestUrl(url(URL_HOME_PAGE + "/display/" + ID_FILE_BYTESTREAM)); + analyzeIt(); + assertBytestreamRedirectInfo("bytestream redirect", + URL_BYTESTREAM_ALIAS); + } + + // ---------------------------------------------------------------------- + // Tests - redirect from a Linked Data path + // ---------------------------------------------------------------------- + + /** /individual/localname, accept=RDF redirects to /individual/id/id.rdf */ + @Test + public void redirectFromLinkedDataPathAcceptRdf() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + req.setHeader("accept", RDFXML_MIMETYPE); + analyzeIt(); + assertRdfRedirectRequestInfo("by linked data path, accept RDF", + redirectUrlForRdfStream(ID_INDIVIDUAL_TEST, ".rdf")); + } + + /** /individual/localname, accept=N3 redirects to /individual/id/id.n3 */ + @Test + public void redirectFromLinkedDataPathAcceptN3() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + req.setHeader("accept", N3_MIMETYPE); + analyzeIt(); + assertRdfRedirectRequestInfo("by linked data path, accept N3", + redirectUrlForRdfStream(ID_INDIVIDUAL_TEST, ".n3")); + } + + /** /individual/localname, accept=TTL redirects to /individual/id/id.ttl */ + @Test + public void redirectFromLinkedDataPathAcceptTurtle() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + req.setHeader("accept", TTL_MIMETYPE); + analyzeIt(); + assertRdfRedirectRequestInfo("by linked data path, accept TTL", + redirectUrlForRdfStream(ID_INDIVIDUAL_TEST, ".ttl")); + } + + /** /individual/localname, no accept, redirects to /display/id */ + @Test + public void redirectFromLinkedDataPathNoAccept() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + analyzeIt(); + assertRdfRedirectRequestInfo("by linked data path with no accept", + "/display/" + ID_INDIVIDUAL_TEST); + } + + /** + * If the accept header is set to a recognized value, but not one of the + * onese that we like, treat the same as no accept. + */ + @Test + public void redirectFromLinkedDataPathAcceptStrange() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + req.setHeader("accept", "application/json"); + analyzeIt(); + assertRdfRedirectRequestInfo( + "by linked data path, accept a strange content type", + "/display/" + ID_INDIVIDUAL_TEST); + } + + /** + * If the accept header is set to an unrecognized value, treat the same as + * no accept. + */ + @Test + public void redirectFromLinkedDataPathAcceptGarbage() { + req.setRequestUrl(url(DEFAULT_NAMESPACE + ID_INDIVIDUAL_TEST)); + req.setHeader("accept", "a/b/c"); + analyzeIt(); + assertRdfRedirectRequestInfo( + "by linked data path, accept an unrecognized content type", + "/display/" + ID_INDIVIDUAL_TEST); + } + + // ---------------------------------------------------------------------- + // Tests - satisfy requests for RDF formats. + // ---------------------------------------------------------------------- + + @Test + public void getRdfByUriAndFormatParameters() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("uri", URI_INDIVIDUAL_TEST); + req.addParameter("format", "rdfxml"); + analyzeIt(); + assertLinkedDataRequestInfo("RDF by uri and format parameters", + URI_INDIVIDUAL_TEST, ContentType.RDFXML); + } + + @Test + public void getN3ByUriAndFormatParameters() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("uri", URI_INDIVIDUAL_TEST); + req.addParameter("format", "n3"); + analyzeIt(); + assertLinkedDataRequestInfo("N3 by uri and format parameters", + URI_INDIVIDUAL_TEST, ContentType.N3); + } + + @Test + public void getTurtleByUriAndFormatParameters() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("uri", URI_INDIVIDUAL_TEST); + req.addParameter("format", "ttl"); + analyzeIt(); + assertLinkedDataRequestInfo("Turtle by uri and format parameters", + URI_INDIVIDUAL_TEST, ContentType.TURTLE); + } + + @Test + public void unrecognizedFormatParameter() { + req.setRequestUrl(url(URL_INDIVIDUAL_PAGE)); + req.addParameter("uri", URI_INDIVIDUAL_TEST); + req.addParameter("format", "bogus"); + analyzeIt(); + assertDefaultRequestInfo("unrecognized format means HTML response", + URI_INDIVIDUAL_TEST); + } + + /** http://vivo.cornell.edu/individual/n23/n23.rdf */ + @Test + public void getRdfByStreamRequest() { + req.setRequestUrl(absoluteUrlForRdfStream(ID_INDIVIDUAL_TEST, ".rdf")); + analyzeIt(); + assertLinkedDataRequestInfo("RDF by stream request", + URI_INDIVIDUAL_TEST, ContentType.RDFXML); + } + + /** http://vivo.cornell.edu/individual/n23/n23.n3 */ + @Test + public void getN3ByStreamRequest() { + req.setRequestUrl(absoluteUrlForRdfStream(ID_INDIVIDUAL_TEST, ".n3")); + analyzeIt(); + assertLinkedDataRequestInfo("N3 by stream request", + URI_INDIVIDUAL_TEST, ContentType.N3); + } + + /** http://vivo.cornell.edu/individual/n23/n23.rdf */ + @Test + public void getTurtleByStreamRequest() { + req.setRequestUrl(absoluteUrlForRdfStream(ID_INDIVIDUAL_TEST, ".ttl")); + analyzeIt(); + assertLinkedDataRequestInfo("Turtle by stream request", + URI_INDIVIDUAL_TEST, ContentType.TURTLE); + } + + /** http://vivo.cornell.edu/individual/n23/n23.bogus is an error */ + @Test + public void unrecognizedFormatForRdfStreamRequest() { + req.setRequestUrl(absoluteUrlForRdfStream(ID_INDIVIDUAL_TEST, ".bogus")); + analyzeIt(); + assertNoIndividualRequestInfo("Unrecognized RDF stream request"); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + /** /individual/n23/n23.rdf, or the like */ + private String redirectUrlForRdfStream(String id, String extension) { + return "/individual/" + id + "/" + id + extension; + } + + /** http://vivo.mydomain.edu/individual/n23/n23.rdf, or the like */ + private URL absoluteUrlForRdfStream(String id, String extension) { + return url(DEFAULT_NAMESPACE + id + "/" + id + extension); + } + + private void analyzeIt() { + vreq = new VitroRequest(req); + analyzer = new IndividualRequestAnalyzer(vreq, analysisContext); + requestInfo = analyzer.analyze(); + } + + /** We should have a DEFAULT request with the expected Individual. */ + private void assertDefaultRequestInfo(String message, String individualUri) { + assertEquals(message + ": expecting DEFAULT request type", + IndividualRequestInfo.Type.DEFAULT, requestInfo.getType()); + assertNotNull(message + ": expected an individual", + requestInfo.getIndividual()); + assertEquals(message + ": expected individual", individualUri, + requestInfo.getIndividual().getURI()); + } + + /** We should have a RDF_REDIRECT request with the expected URL. */ + private void assertRdfRedirectRequestInfo(String message, String redirectUrl) { + assertEquals(message + ": expecting RDF_REDIRECT request type", + IndividualRequestInfo.Type.RDF_REDIRECT, requestInfo.getType()); + assertEquals(message + ": expected redirect URL", redirectUrl, + requestInfo.getRedirectUrl()); + } + + /** + * We should have a BYTESTREAM_REDIRECT request with the expected Individual + * and alias URL. + */ + private void assertBytestreamRedirectInfo(String message, String aliasUrl) { + assertEquals(message + ": expecting BYTESTREAM_REDIRECT request type", + IndividualRequestInfo.Type.BYTESTREAM_REDIRECT, + requestInfo.getType()); + assertEquals(message + ": expected alias URL", aliasUrl, + requestInfo.getRedirectUrl()); + } + + /** + * We should have a NO_INDIVIDUAL request. + */ + private void assertNoIndividualRequestInfo(String message) { + assertEquals(message + ": expecting NO_INDIVIDUAL request type", + IndividualRequestInfo.Type.NO_INDIVIDUAL, requestInfo.getType()); + } + + /** + * We should have a LINKED_DATA request, with the expected Individual and + * content type. + */ + private void assertLinkedDataRequestInfo(String message, + String individualUri, ContentType contentType) { + assertEquals(message + ": expecting LINKED_DATA request type", + IndividualRequestInfo.Type.LINKED_DATA, requestInfo.getType()); + assertNotNull(message + ": expected an individual", + requestInfo.getIndividual()); + assertEquals(message + ": expected individual", individualUri, + requestInfo.getIndividual().getURI()); + assertNotNull(message + ": expected a content type", + requestInfo.getRdfFormat()); + assertEquals(message + ": expected contentType", contentType, + requestInfo.getRdfFormat()); + } + + // ---------------------------------------------------------------------- + // Supporting classes + // ---------------------------------------------------------------------- + + private static class MyAnalysisContext implements + IndividualRequestAnalysisContext { + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private final String defaultNamespace; + private final Map individualsByUri = new HashMap(); + private final Map profilePages = new HashMap(); + private final Map namespacesByPrefix = new HashMap(); + private final Map aliasUrlsByIndividual = new HashMap(); + + public MyAnalysisContext(String defaultNamespace) { + this.defaultNamespace = defaultNamespace; + } + + public void addIndividual(Individual individual) { + individualsByUri.put(individual.getURI(), individual); + } + + public void addProfilePage(String netId, Individual individual) { + profilePages.put(netId, individual); + } + + public void setNamespacePrefix(String prefix, String namespace) { + namespacesByPrefix.put(prefix, namespace); + } + + public void setAliasUrl(String individualUri, String aliasUrl) { + aliasUrlsByIndividual.put(individualUri, aliasUrl); + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public String getDefaultNamespace() { + return defaultNamespace; + } + + @Override + public String getNamespaceForPrefix(String prefix) { + if (prefix == null) { + return ""; + } + String namespace = namespacesByPrefix.get(prefix); + return (namespace == null) ? "" : namespace; + } + + @Override + public Individual getIndividualByURI(String individualUri) { + if (individualUri == null) { + return null; + + } + return individualsByUri.get(individualUri); + } + + @Override + public Individual getIndividualByNetId(String netId) { + if (netId == null) { + return null; + } + return profilePages.get(netId); + } + + @Override + public String getAliasUrlForBytestreamIndividual(Individual individual) { + if (individual == null) { + return null; + } + return aliasUrlsByIndividual.get(individual.getURI()); + } + } +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/web/ContentTypeTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/web/ContentTypeTest.java index 0f46abf04..8424fd483 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/web/ContentTypeTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/web/ContentTypeTest.java @@ -8,7 +8,7 @@ import junit.framework.Assert; import org.junit.Test; -import edu.cornell.mannlib.vitro.webapp.controller.freemarker.IndividualController; +import edu.cornell.mannlib.vitro.webapp.controller.individual.IndividualController; public class ContentTypeTest { @@ -41,7 +41,7 @@ public class ContentTypeTest { Map clientAccepts = ContentType.getTypesAndQ( "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/rdf+xml;q=0.93,text/rdf+n3;q=0.5"); - Map serverTypes = IndividualController.getAcceptedContentTypes(); + Map serverTypes = IndividualController.ACCEPTED_CONTENT_TYPES; Assert.assertEquals("application/rdf+xml", ContentType.getBestContentType(clientAccepts, serverTypes)); } @@ -52,7 +52,7 @@ public class ContentTypeTest { Map clientAccepts = ContentType.getTypesAndQ( "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - Map serverTypes = IndividualController.getAcceptedContentTypes(); + Map serverTypes = IndividualController.ACCEPTED_CONTENT_TYPES; Assert.assertEquals("application/xhtml+xml", ContentType.getBestContentType(clientAccepts, serverTypes)); } diff --git a/webapp/web/WEB-INF/web.xml b/webapp/web/WEB-INF/web.xml index b52417ea9..1e8e7205b 100644 --- a/webapp/web/WEB-INF/web.xml +++ b/webapp/web/WEB-INF/web.xml @@ -986,7 +986,7 @@ individual - edu.cornell.mannlib.vitro.webapp.controller.freemarker.IndividualController + edu.cornell.mannlib.vitro.webapp.controller.individual.IndividualController diff --git a/webapp/web/templates/freemarker/body/individual/individual-help.ftl b/webapp/web/templates/freemarker/body/individual/individual-help.ftl index c7890db5f..6929787c8 100644 --- a/webapp/web/templates/freemarker/body/individual/individual-help.ftl +++ b/webapp/web/templates/freemarker/body/individual/individual-help.ftl @@ -2,6 +2,6 @@ <#-- Template for help on individual page --> -

Quick Notes on Using Individual:

+

Individual not found:

id is the id of the entity to query for. netid also works.

\ No newline at end of file