/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE and NOTICE files at the root of the source * tree and available online at * * http://www.dspace.org/license/ */ package org.dspace.rdf.negotiation; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.dspace.rdf.RDFUtil; import org.dspace.services.factory.DSpaceServicesFactory; /** * * @author Pascal-Nicolas Becker (dspace -at- pascal -hyphen- becker -dot- de) */ public class Negotiator { // Serialiazation codes public static final int UNSPECIFIED = -1; public static final int WILDCARD = 0; public static final int HTML = 1; public static final int RDFXML = 2; public static final int TURTLE = 3; public static final int N3 = 4; public static final String DEFAULT_LANG="html"; private static final Logger log = Logger.getLogger(Negotiator.class); public static int negotiate(String acceptHeader) { if (acceptHeader == null) return UNSPECIFIED; String[] mediaRangeSpecs = acceptHeader.split(","); ArrayList<MediaRange> requestedMediaRanges = new ArrayList<>(); for (String mediaRangeSpec : mediaRangeSpecs) { try { requestedMediaRanges.add(new MediaRange(mediaRangeSpec)); } catch (IllegalArgumentException | IllegalStateException ex) { log.warn("Couldn't parse part of an AcceptHeader, ignoring it.\n" + ex.getMessage(), ex); } } if (requestedMediaRanges.isEmpty()) { return UNSPECIFIED; } Collections.sort(requestedMediaRanges, getMediaRangeComparator()); Collections.reverse(requestedMediaRanges); if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder("Parsed Accept header '" + acceptHeader + "':\n"); for (Iterator<MediaRange> it = requestedMediaRanges.iterator(); it.hasNext(); ) { MediaRange mr = it.next(); sb.append(mr.getType()).append("/").append(mr.getSubtype()); sb.append(" has a qvalue of ").append(Double.toString(mr.getQvalue())); sb.append("\n"); } log.debug(sb.toString()); } boolean wildcard = false; boolean html = false; boolean rdf = false; boolean n3 = false; boolean turtle = false; Iterator<MediaRange> it = requestedMediaRanges.iterator(); MediaRange lookahead = it.hasNext() ? it.next() : null; while (lookahead != null) { double qvalue = lookahead.getQvalue(); String type = lookahead.getType(); String subtype = lookahead.getSubtype(); lookahead = it.hasNext() ? it.next() : null; if (qvalue <= 0.0) { // a quality of 0.0 means that the defined media range should // not to be send => don't parse it. continue; } if ("*".equals(type)) { wildcard = true; } if (("text".equals(type) && "html".equals(subtype)) || ("application".equals(type) && "xhtml+xml".equals(subtype))) { html = true; } if ("application".equals(type) && "rdf+xml".equals(subtype)) { rdf = true; } if (("text".equals(type) && "n3".equals(subtype)) || ("text".equals(type) && "rdf+n3".equals(subtype)) || ("application".equals(type) && "n3".equals(subtype))) { n3 = true; } if (("text".equals(type) && "turtle".equals(subtype)) || ("application".equals(type) && "turtle".equals(subtype)) || ("application".equals(type) && "x-turtle".equals(subtype)) || ("application".equals(type) && "rdf+turtle".equals(subtype))) { turtle = true; } if (lookahead != null && qvalue != lookahead.qvalue && (wildcard || html || rdf || n3 || turtle)) { // we've looked over all media range with the same precedence // and found one, we can serve break; } } if (html) { return HTML; } if (wildcard) { return WILDCARD; } else if (turtle) { return TURTLE; } else if (n3) { return N3; } else if (rdf) { return RDFXML; } return UNSPECIFIED; } /** * Method to get a comparator to compare media ranges regarding their * content negotiation precedence. Following RFC 2616 a media range is * higher prioritized then another media range if the first one has a higher * quality value then the second. If both quality values are equal, the * media range that is more specific should be used. * * <p>Note: this comparator imposes orderings that are inconsistent with * equals! Caution should be exercised when using it to order a sorted set * or a sorted map. Take a look at the java.util.Comparator for further * information.</p> * @return A comparator that imposes orderings that are inconsistent with equals! */ public static Comparator<MediaRange> getMediaRangeComparator() { return new Comparator<MediaRange>() { @Override public int compare(MediaRange mr1, MediaRange mr2) { if (Double.compare(mr1.qvalue, mr2.getQvalue()) != 0) { return Double.compare(mr1.qvalue, mr2.getQvalue()); } if (mr1.typeIsWildcard() && mr2.typeIsWildcard()) return 0; if (mr1.typeIsWildcard() && !mr2.typeIsWildcard()) return -1; if (!mr1.typeIsWildcard() && mr2.typeIsWildcard()) return 1; if (mr1.subtypeIsWildcard() && mr2.subtypeIsWildcard()) return 0; if (mr1.subtypeIsWildcard() && !mr2.subtypeIsWildcard()) return -1; if (!mr1.subtypeIsWildcard() && mr2.subtypeIsWildcard()) return 1; // if the quality of two media ranges is equal and both don't // use an asterisk either as type or subtype, they are equal in // the sense of content negotiation precedence. return 0; } }; } public static boolean sendRedirect(HttpServletResponse response, String handle, String extraPathInfo, int serialization, boolean redirectHTML) throws IOException { if (extraPathInfo == null) extraPathInfo = ""; StringBuilder urlBuilder = new StringBuilder(); String lang = null; switch (serialization) { case (Negotiator.UNSPECIFIED): case (Negotiator.WILDCARD): { lang = DEFAULT_LANG; break; } case (Negotiator.HTML): { lang = "html"; break; } case (Negotiator.RDFXML): { lang = "rdf"; break; } case (Negotiator.TURTLE): { lang = "turtle"; break; } case (Negotiator.N3): { lang = "n3"; break; } default: { lang = DEFAULT_LANG; break; } } assert (lang != null); if (StringUtils.isEmpty(handle)) { log.warn("Handle is empty, set it to Site Handle."); handle = DSpaceServicesFactory.getInstance().getConfigurationService() .getProperty("handle.prefix") + "/0"; } // don't redirect if HTML is requested and content negotiation is done // in a ServletFilter, as the ServletFilter should just let the request // pass. if ("html".equals(lang) && !redirectHTML) { return false; } // as we do content negotiation and we'll redirect the request, we // should send a vary caching so browsers can adopt their caching strategy response.setHeader("Vary", "Accept"); // if html is requested we have to forward to the repositories webui. if ("html".equals(lang)) { urlBuilder.append(DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.url")); if (!handle.equals(DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("handle.prefix") + "/0")) { urlBuilder.append("/handle/"); urlBuilder.append(handle).append("/").append(extraPathInfo); } String url = urlBuilder.toString(); log.debug("Will forward to '" + url + "'."); response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setHeader("Location", url); response.flushBuffer(); return true; } // currently we cannot serve statistics as rdf if("statistics".equals(extraPathInfo)) { log.info("Cannot send statistics as RDF yet. => 406 Not Acceptable."); response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); response.flushBuffer(); return true; } // load the URI of the dspace-rdf module. urlBuilder.append(DSpaceServicesFactory.getInstance() .getConfigurationService() .getProperty(RDFUtil.CONTEXT_PATH_KEY)); if (urlBuilder.length() == 0) { log.error("Cannot load URL of dspace-rdf module. " + "=> 500 Internal Server Error"); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.flushBuffer(); return true; } // and build the uri to the DataProviderServlet urlBuilder.append("/handle/").append(handle); urlBuilder.append("/").append(lang); String url = urlBuilder.toString(); log.debug("Will forward to '" + url + "'."); response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setHeader("Location", url); response.flushBuffer(); return true; } }