|
| 1 | +package org.linkeddatafragments.util; |
| 2 | + |
| 3 | +import java.util.Collection; |
| 4 | +import java.util.Collections; |
| 5 | +import java.util.HashMap; |
| 6 | +import java.util.LinkedList; |
| 7 | +import java.util.List; |
| 8 | +import java.util.Map; |
| 9 | + |
| 10 | +import org.apache.commons.lang3.StringUtils; |
| 11 | +import org.apache.commons.lang.math.NumberUtils; |
| 12 | + |
| 13 | +/** |
| 14 | + * MIME-Type Parser |
| 15 | + * |
| 16 | + * This class provides basic functions for handling mime-types. It can handle |
| 17 | + * matching mime-types against a list of media-ranges. See section 14.1 of the |
| 18 | + * HTTP specification [RFC 2616] for a complete explanation. |
| 19 | + * |
| 20 | + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 |
| 21 | + * |
| 22 | + * A port to Java of Joe Gregorio's MIME-Type Parser: |
| 23 | + * |
| 24 | + * http://code.google.com/p/mimeparse/ |
| 25 | + * |
| 26 | + * Ported by Tom Zellman <[email protected]>. |
| 27 | + * |
| 28 | + */ |
| 29 | +public final class MIMEParse |
| 30 | +{ |
| 31 | + |
| 32 | + /** |
| 33 | + * Parse results container |
| 34 | + */ |
| 35 | + protected static class ParseResults |
| 36 | + { |
| 37 | + String type; |
| 38 | + |
| 39 | + String subType; |
| 40 | + |
| 41 | + // !a dictionary of all the parameters for the media range |
| 42 | + Map<String, String> params; |
| 43 | + |
| 44 | + @Override |
| 45 | + public String toString() |
| 46 | + { |
| 47 | + StringBuffer s = new StringBuffer("('" + type + "', '" + subType |
| 48 | + + "', {"); |
| 49 | + for (String k : params.keySet()) |
| 50 | + s.append("'" + k + "':'" + params.get(k) + "',"); |
| 51 | + return s.append("})").toString(); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Carves up a mime-type and returns a ParseResults object |
| 57 | + * |
| 58 | + * For example, the media range 'application/xhtml;q=0.5' would get parsed |
| 59 | + * into: |
| 60 | + * |
| 61 | + * ('application', 'xhtml', {'q', '0.5'}) |
| 62 | + */ |
| 63 | + protected static ParseResults parseMimeType(String mimeType) |
| 64 | + { |
| 65 | + String[] parts = StringUtils.split(mimeType, ";"); |
| 66 | + ParseResults results = new ParseResults(); |
| 67 | + results.params = new HashMap<String, String>(); |
| 68 | + |
| 69 | + for (int i = 1; i < parts.length; ++i) |
| 70 | + { |
| 71 | + String p = parts[i]; |
| 72 | + String[] subParts = StringUtils.split(p, '='); |
| 73 | + if (subParts.length == 2) |
| 74 | + results.params.put(subParts[0].trim(), subParts[1].trim()); |
| 75 | + } |
| 76 | + String fullType = parts[0].trim(); |
| 77 | + |
| 78 | + // Java URLConnection class sends an Accept header that includes a |
| 79 | + // single "*" - Turn it into a legal wildcard. |
| 80 | + if (fullType.equals("*")) |
| 81 | + fullType = "*/*"; |
| 82 | + String[] types = StringUtils.split(fullType, "/"); |
| 83 | + results.type = types[0].trim(); |
| 84 | + results.subType = types[1].trim(); |
| 85 | + return results; |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * Carves up a media range and returns a ParseResults. |
| 90 | + * |
| 91 | + * For example, the media range 'application/*;q=0.5' would get parsed into: |
| 92 | + * |
| 93 | + * ('application', '*', {'q', '0.5'}) |
| 94 | + * |
| 95 | + * In addition this function also guarantees that there is a value for 'q' |
| 96 | + * in the params dictionary, filling it in with a proper default if |
| 97 | + * necessary. |
| 98 | + * |
| 99 | + * @param range |
| 100 | + */ |
| 101 | + protected static ParseResults parseMediaRange(String range) |
| 102 | + { |
| 103 | + ParseResults results = parseMimeType(range); |
| 104 | + String q = results.params.get("q"); |
| 105 | + float f = NumberUtils.toFloat(q, 1); |
| 106 | + if (StringUtils.isBlank(q) || f < 0 || f > 1) |
| 107 | + results.params.put("q", "1"); |
| 108 | + return results; |
| 109 | + } |
| 110 | + |
| 111 | + /** |
| 112 | + * Structure for holding a fitness/quality combo |
| 113 | + */ |
| 114 | + protected static class FitnessAndQuality implements |
| 115 | + Comparable<FitnessAndQuality> |
| 116 | + { |
| 117 | + int fitness; |
| 118 | + |
| 119 | + float quality; |
| 120 | + |
| 121 | + String mimeType; // optionally used |
| 122 | + |
| 123 | + public FitnessAndQuality(int fitness, float quality) |
| 124 | + { |
| 125 | + this.fitness = fitness; |
| 126 | + this.quality = quality; |
| 127 | + } |
| 128 | + |
| 129 | + public int compareTo(FitnessAndQuality o) |
| 130 | + { |
| 131 | + if (fitness == o.fitness) |
| 132 | + { |
| 133 | + if (quality == o.quality) |
| 134 | + return 0; |
| 135 | + else |
| 136 | + return quality < o.quality ? -1 : 1; |
| 137 | + } |
| 138 | + else |
| 139 | + return fitness < o.fitness ? -1 : 1; |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * Find the best match for a given mimeType against a list of media_ranges |
| 145 | + * that have already been parsed by MimeParse.parseMediaRange(). Returns a |
| 146 | + * tuple of the fitness value and the value of the 'q' quality parameter of |
| 147 | + * the best match, or (-1, 0) if no match was found. Just as for |
| 148 | + * quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges. |
| 149 | + * |
| 150 | + * @param mimeType |
| 151 | + * @param parsedRanges |
| 152 | + */ |
| 153 | + protected static FitnessAndQuality fitnessAndQualityParsed(String mimeType, |
| 154 | + Collection<ParseResults> parsedRanges) |
| 155 | + { |
| 156 | + int bestFitness = -1; |
| 157 | + float bestFitQ = 0; |
| 158 | + ParseResults target = parseMediaRange(mimeType); |
| 159 | + |
| 160 | + for (ParseResults range : parsedRanges) |
| 161 | + { |
| 162 | + if ((target.type.equals(range.type) || range.type.equals("*") || target.type |
| 163 | + .equals("*")) |
| 164 | + && (target.subType.equals(range.subType) |
| 165 | + || range.subType.equals("*") || target.subType |
| 166 | + .equals("*"))) |
| 167 | + { |
| 168 | + for (String k : target.params.keySet()) |
| 169 | + { |
| 170 | + int paramMatches = 0; |
| 171 | + if (!k.equals("q") && range.params.containsKey(k) |
| 172 | + && target.params.get(k).equals(range.params.get(k))) |
| 173 | + { |
| 174 | + paramMatches++; |
| 175 | + } |
| 176 | + int fitness = (range.type.equals(target.type)) ? 100 : 0; |
| 177 | + fitness += (range.subType.equals(target.subType)) ? 10 : 0; |
| 178 | + fitness += paramMatches; |
| 179 | + if (fitness > bestFitness) |
| 180 | + { |
| 181 | + bestFitness = fitness; |
| 182 | + bestFitQ = NumberUtils |
| 183 | + .toFloat(range.params.get("q"), 0); |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + } |
| 188 | + return new FitnessAndQuality(bestFitness, bestFitQ); |
| 189 | + } |
| 190 | + |
| 191 | + /** |
| 192 | + * Find the best match for a given mime-type against a list of ranges that |
| 193 | + * have already been parsed by parseMediaRange(). Returns the 'q' quality |
| 194 | + * parameter of the best match, 0 if no match was found. This function |
| 195 | + * bahaves the same as quality() except that 'parsed_ranges' must be a list |
| 196 | + * of parsed media ranges. |
| 197 | + * |
| 198 | + * @param mimeType |
| 199 | + * @param parsedRanges |
| 200 | + * @return |
| 201 | + */ |
| 202 | + protected static float qualityParsed(String mimeType, |
| 203 | + Collection<ParseResults> parsedRanges) |
| 204 | + { |
| 205 | + return fitnessAndQualityParsed(mimeType, parsedRanges).quality; |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Returns the quality 'q' of a mime-type when compared against the |
| 210 | + * mediaRanges in ranges. For example: |
| 211 | + * |
| 212 | + * @param mimeType |
| 213 | + * @param parsedRanges |
| 214 | + */ |
| 215 | + public static float quality(String mimeType, String ranges) |
| 216 | + { |
| 217 | + List<ParseResults> results = new LinkedList<ParseResults>(); |
| 218 | + for (String r : StringUtils.split(ranges, ',')) |
| 219 | + results.add(parseMediaRange(r)); |
| 220 | + return qualityParsed(mimeType, results); |
| 221 | + } |
| 222 | + |
| 223 | + /** |
| 224 | + * Takes a list of supported mime-types and finds the best match for all the |
| 225 | + * media-ranges listed in header. The value of header must be a string that |
| 226 | + * conforms to the format of the HTTP Accept: header. The value of |
| 227 | + * 'supported' is a list of mime-types. |
| 228 | + * |
| 229 | + * MimeParse.bestMatch(Arrays.asList(new String[]{"application/xbel+xml", |
| 230 | + * "text/xml"}), "text/*;q=0.5,*; q=0.1") 'text/xml' |
| 231 | + * |
| 232 | + * @param supported |
| 233 | + * @param header |
| 234 | + * @return |
| 235 | + */ |
| 236 | + public static String bestMatch(Collection<String> supported, String header) |
| 237 | + { |
| 238 | + List<ParseResults> parseResults = new LinkedList<ParseResults>(); |
| 239 | + List<FitnessAndQuality> weightedMatches = new LinkedList<FitnessAndQuality>(); |
| 240 | + for (String r : StringUtils.split(header, ',')) |
| 241 | + parseResults.add(parseMediaRange(r)); |
| 242 | + |
| 243 | + for (String s : supported) |
| 244 | + { |
| 245 | + FitnessAndQuality fitnessAndQuality = fitnessAndQualityParsed(s, |
| 246 | + parseResults); |
| 247 | + fitnessAndQuality.mimeType = s; |
| 248 | + weightedMatches.add(fitnessAndQuality); |
| 249 | + } |
| 250 | + Collections.sort(weightedMatches); |
| 251 | + |
| 252 | + FitnessAndQuality lastOne = weightedMatches |
| 253 | + .get(weightedMatches.size() - 1); |
| 254 | + return NumberUtils.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType |
| 255 | + : ""; |
| 256 | + } |
| 257 | + |
| 258 | + // hidden |
| 259 | + private MIMEParse() |
| 260 | + { |
| 261 | + } |
| 262 | +} |
0 commit comments