diff --git a/src/org/linkeddatafragments/util/MIMEParse.java b/src/org/linkeddatafragments/util/MIMEParse.java new file mode 100644 index 0000000..d45d605 --- /dev/null +++ b/src/org/linkeddatafragments/util/MIMEParse.java @@ -0,0 +1,262 @@ +package org.linkeddatafragments.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang.math.NumberUtils; + +/** + * MIME-Type Parser + * + * This class provides basic functions for handling mime-types. It can handle + * matching mime-types against a list of media-ranges. See section 14.1 of the + * HTTP specification [RFC 2616] for a complete explanation. + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + * + * A port to Java of Joe Gregorio's MIME-Type Parser: + * + * http://code.google.com/p/mimeparse/ + * + * Ported by Tom Zellman . + * + */ +public final class MIMEParse +{ + + /** + * Parse results container + */ + protected static class ParseResults + { + String type; + + String subType; + + // !a dictionary of all the parameters for the media range + Map params; + + @Override + public String toString() + { + StringBuffer s = new StringBuffer("('" + type + "', '" + subType + + "', {"); + for (String k : params.keySet()) + s.append("'" + k + "':'" + params.get(k) + "',"); + return s.append("})").toString(); + } + } + + /** + * Carves up a mime-type and returns a ParseResults object + * + * For example, the media range 'application/xhtml;q=0.5' would get parsed + * into: + * + * ('application', 'xhtml', {'q', '0.5'}) + */ + protected static ParseResults parseMimeType(String mimeType) + { + String[] parts = StringUtils.split(mimeType, ";"); + ParseResults results = new ParseResults(); + results.params = new HashMap(); + + for (int i = 1; i < parts.length; ++i) + { + String p = parts[i]; + String[] subParts = StringUtils.split(p, '='); + if (subParts.length == 2) + results.params.put(subParts[0].trim(), subParts[1].trim()); + } + String fullType = parts[0].trim(); + + // Java URLConnection class sends an Accept header that includes a + // single "*" - Turn it into a legal wildcard. + if (fullType.equals("*")) + fullType = "*/*"; + String[] types = StringUtils.split(fullType, "/"); + results.type = types[0].trim(); + results.subType = types[1].trim(); + return results; + } + + /** + * Carves up a media range and returns a ParseResults. + * + * For example, the media range 'application/*;q=0.5' would get parsed into: + * + * ('application', '*', {'q', '0.5'}) + * + * In addition this function also guarantees that there is a value for 'q' + * in the params dictionary, filling it in with a proper default if + * necessary. + * + * @param range + */ + protected static ParseResults parseMediaRange(String range) + { + ParseResults results = parseMimeType(range); + String q = results.params.get("q"); + float f = NumberUtils.toFloat(q, 1); + if (StringUtils.isBlank(q) || f < 0 || f > 1) + results.params.put("q", "1"); + return results; + } + + /** + * Structure for holding a fitness/quality combo + */ + protected static class FitnessAndQuality implements + Comparable + { + int fitness; + + float quality; + + String mimeType; // optionally used + + public FitnessAndQuality(int fitness, float quality) + { + this.fitness = fitness; + this.quality = quality; + } + + public int compareTo(FitnessAndQuality o) + { + if (fitness == o.fitness) + { + if (quality == o.quality) + return 0; + else + return quality < o.quality ? -1 : 1; + } + else + return fitness < o.fitness ? -1 : 1; + } + } + + /** + * Find the best match for a given mimeType against a list of media_ranges + * that have already been parsed by MimeParse.parseMediaRange(). Returns a + * tuple of the fitness value and the value of the 'q' quality parameter of + * the best match, or (-1, 0) if no match was found. Just as for + * quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges. + * + * @param mimeType + * @param parsedRanges + */ + protected static FitnessAndQuality fitnessAndQualityParsed(String mimeType, + Collection parsedRanges) + { + int bestFitness = -1; + float bestFitQ = 0; + ParseResults target = parseMediaRange(mimeType); + + for (ParseResults range : parsedRanges) + { + if ((target.type.equals(range.type) || range.type.equals("*") || target.type + .equals("*")) + && (target.subType.equals(range.subType) + || range.subType.equals("*") || target.subType + .equals("*"))) + { + for (String k : target.params.keySet()) + { + int paramMatches = 0; + if (!k.equals("q") && range.params.containsKey(k) + && target.params.get(k).equals(range.params.get(k))) + { + paramMatches++; + } + int fitness = (range.type.equals(target.type)) ? 100 : 0; + fitness += (range.subType.equals(target.subType)) ? 10 : 0; + fitness += paramMatches; + if (fitness > bestFitness) + { + bestFitness = fitness; + bestFitQ = NumberUtils + .toFloat(range.params.get("q"), 0); + } + } + } + } + return new FitnessAndQuality(bestFitness, bestFitQ); + } + + /** + * Find the best match for a given mime-type against a list of ranges that + * have already been parsed by parseMediaRange(). Returns the 'q' quality + * parameter of the best match, 0 if no match was found. This function + * bahaves the same as quality() except that 'parsed_ranges' must be a list + * of parsed media ranges. + * + * @param mimeType + * @param parsedRanges + * @return + */ + protected static float qualityParsed(String mimeType, + Collection parsedRanges) + { + return fitnessAndQualityParsed(mimeType, parsedRanges).quality; + } + + /** + * Returns the quality 'q' of a mime-type when compared against the + * mediaRanges in ranges. For example: + * + * @param mimeType + * @param parsedRanges + */ + public static float quality(String mimeType, String ranges) + { + List results = new LinkedList(); + for (String r : StringUtils.split(ranges, ',')) + results.add(parseMediaRange(r)); + return qualityParsed(mimeType, results); + } + + /** + * Takes a list of supported mime-types and finds the best match for all the + * media-ranges listed in header. The value of header must be a string that + * conforms to the format of the HTTP Accept: header. The value of + * 'supported' is a list of mime-types. + * + * MimeParse.bestMatch(Arrays.asList(new String[]{"application/xbel+xml", + * "text/xml"}), "text/*;q=0.5,*; q=0.1") 'text/xml' + * + * @param supported + * @param header + * @return + */ + public static String bestMatch(Collection supported, String header) + { + List parseResults = new LinkedList(); + List weightedMatches = new LinkedList(); + for (String r : StringUtils.split(header, ',')) + parseResults.add(parseMediaRange(r)); + + for (String s : supported) + { + FitnessAndQuality fitnessAndQuality = fitnessAndQualityParsed(s, + parseResults); + fitnessAndQuality.mimeType = s; + weightedMatches.add(fitnessAndQuality); + } + Collections.sort(weightedMatches); + + FitnessAndQuality lastOne = weightedMatches + .get(weightedMatches.size() - 1); + return NumberUtils.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType + : ""; + } + + // hidden + private MIMEParse() + { + } +}