package com.google.sitebricks.http.negotiate; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.sitebricks.headless.Request; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ContentNegotiator that supports comma separated and wildcard matches in Accept header style * for example, {@literal @}Accept("text/html, text/plain") will match an incoming request * with HTTP Accept header "text/*" and {@literal @}Accept("text/*") will match incoming * request with headers "Accept: text/html" or "Accept: text/plain" * * Notes: * Wildcard for subtypes such as "text/*, image/*" are supported but there is no * wildcard matching on the main media types. Negotiating on other HTTP request * headers where "/*" might be useful is currently undefined. * * */ public class WildcardNegotiator implements ContentNegotiator { // Lifted TOKEN, TYPE_PATTERN from com.google.gdata.util private static String TOKEN = "[\\p{ASCII}&&[^\\p{Cntrl} ;/=\\[\\]\\(\\)\\<\\>\\@\\,\\:\\\"\\?\\=]]+"; private static Pattern TYPE_PATTERN = Pattern.compile( "(" + TOKEN + ")" + // mediatype (G1) "/" + // separator "(" + TOKEN + ")" + // subtype (G2) "\\s*(.*)\\s*", Pattern.DOTALL); private HashMultimap<String, String> createMultimatch(List<String> matchlist) { HashMultimap<String, String> multimatch = HashMultimap.create(); for (String m : matchlist) { Matcher mediaType = TYPE_PATTERN.matcher(m); if (mediaType.matches()) { String type = mediaType.group(1).toLowerCase(); String subtype = mediaType.group(2).toLowerCase(); multimatch.put(type, subtype); } } return multimatch; } public boolean shouldCall(Map<String, String> negotiations, Request request) { Multimap<String, String> headers = request.headers(); for (Map.Entry<String, String> negotiate : negotiations.entrySet()) { Collection<String> collectionOfHeader = headers.get(negotiate.getKey()); if (null == collectionOfHeader) continue; Iterator<String> headerValues = collectionOfHeader.iterator(); boolean shouldFire = false; List<String> matches = Arrays.asList(negotiate.getValue().split(",[ ]*")); HashMultimap<String,String> mediaMatches = createMultimatch(matches); while (headerValues.hasNext()) { String value = headerValues.next(); List<String> values = Arrays.asList(value.split(",[ ]*")); HashMultimap<String,String> mediaValues = createMultimatch(values); if (!mediaMatches.isEmpty()) { Set<String> typeIntersection = Sets.intersection(mediaMatches.keySet(), mediaValues.keySet()); if (typeIntersection.isEmpty()) { shouldFire |= typeIntersection.isEmpty(); } else { for (String mediaType: typeIntersection) { Set<String> subtypeMatches = mediaMatches.get(mediaType); Set<String> subtypeValues = mediaValues.get(mediaType); shouldFire |= (subtypeMatches.contains("*") || subtypeValues.contains("*") || !Sets.intersection(subtypeMatches, subtypeValues).isEmpty()); } } } else { shouldFire |= !(Collections.disjoint(Arrays.asList(value.split(",[ ]*")), matches)); } } if (!shouldFire) { return false; } } return true; } } // TODO - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html // Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 // TODO - other headers with slashes (but not signifying media types)