001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.http; 014 015import static org.apache.juneau.internal.CollectionUtils.*; 016 017import java.util.*; 018import java.util.Map.*; 019 020import org.apache.juneau.annotation.*; 021import org.apache.juneau.internal.*; 022 023/** 024 * Represents a single value in a comma-delimited header value that optionally contains a quality metric for 025 * comparison and extension parameters. 026 * 027 * <p> 028 * Similar in concept to {@link MediaTypeRange} except instead of media types (e.g. <js>"text/json"</js>), 029 * it's a simple type (e.g. <js>"iso-8601"</js>). 030 * 031 * <p> 032 * An example of a type range is a value in an <code>Accept-Encoding</code> header. 033 * 034 * <h5 class='section'>See Also:</h5> 035 * <ul class='doctree'> 036 * <li class='extlink'>{@doc RFC2616} 037 * </ul> 038 */ 039@BeanIgnore 040public final class StringRange implements Comparable<StringRange> { 041 042 private static final StringRange[] DEFAULT = new StringRange[]{new StringRange("*")}; 043 044 private final String type; 045 private final Float qValue; 046 private final Map<String,Set<String>> extensions; 047 048 /** 049 * Parses a header such as an <code>Accept-Encoding</code> header value into an array of type ranges. 050 * 051 * <p> 052 * The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in 053 * RFC2616, Section 14.1, as described below: 054 * <p class='bcode w800'> 055 * Accept-Encoding = "Accept-Encoding" ":" 056 * 1#( codings [ ";" "q" "=" qvalue ] ) 057 * codings = ( content-coding | "*" ) 058 * </p> 059 * 060 * <p> 061 * Examples of its use are: 062 * <p class='bcode w800'> 063 * Accept-Encoding: compress, gzip 064 * Accept-Encoding: 065 * Accept-Encoding: * 066 * Accept-Encoding: compress;q=0.5, gzip;q=1.0 067 * Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 068 * </p> 069 * 070 * @param value 071 * The value to parse. 072 * If <jk>null</jk> or empty, returns a single <code>TypeRange</code> is returned that represents all types. 073 * @return 074 * The type ranges described by the string. 075 * <br>The ranges are sorted such that the most acceptable type is available at ordinal position <js>'0'</js>, and 076 * the least acceptable at position n-1. 077 */ 078 public static StringRange[] parse(String value) { 079 080 if (value == null || value.length() == 0) 081 return DEFAULT; 082 083 if (value.indexOf(',') == -1) 084 return new StringRange[]{new StringRange(value)}; 085 086 Set<StringRange> ranges = new TreeSet<>(); 087 088 for (String r : StringUtils.split(value)) { 089 r = r.trim(); 090 091 if (r.isEmpty()) 092 continue; 093 094 ranges.add(new StringRange(r)); 095 } 096 097 return ranges.toArray(new StringRange[ranges.size()]); 098 } 099 100 private StringRange(String token) { 101 Builder b = new Builder(token); 102 this.type = b.type; 103 this.qValue = b.qValue; 104 this.extensions = unmodifiableMap(b.extensions); 105 } 106 107 static final class Builder { 108 String type; 109 Float qValue = 1f; 110 Map<String,Set<String>> extensions; 111 112 Builder(String token) { 113 114 token = token.trim(); 115 116 int i = token.indexOf(";q="); 117 118 if (i == -1) { 119 type = token; 120 return; 121 } 122 123 type = token.substring(0, i); 124 125 String[] tokens = token.substring(i+1).split(";"); 126 127 // Only the type of the range is specified 128 if (tokens.length > 0) { 129 boolean isInExtensions = false; 130 for (int j = 0; j < tokens.length; j++) { 131 String[] parm = tokens[j].split("="); 132 if (parm.length == 2) { 133 String k = parm[0], v = parm[1]; 134 if (isInExtensions) { 135 if (extensions == null) 136 extensions = new TreeMap<>(); 137 if (! extensions.containsKey(k)) 138 extensions.put(k, new TreeSet<String>()); 139 extensions.get(k).add(v); 140 } else if (k.equals("q")) { 141 qValue = new Float(v); 142 isInExtensions = true; 143 } 144 } 145 } 146 } 147 } 148 } 149 150 /** 151 * Returns the type enclosed by this type range. 152 * 153 * <h5 class='section'>Examples:</h5> 154 * <ul> 155 * <li><js>"compress"</js> 156 * <li><js>"gzip"</js> 157 * <li><js>"*"</js> 158 * </ul> 159 * 160 * @return The type of this type range, lowercased, never <jk>null</jk>. 161 */ 162 public String getType() { 163 return type; 164 } 165 166 /** 167 * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616. 168 * 169 * <p> 170 * The quality value is a float between <code>0.0</code> (unacceptable) and <code>1.0</code> (most acceptable). 171 * 172 * <p> 173 * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js> 174 * header, as opposed to <js>"accept-*"</js> header, its value will always be <js>"1"</js>. 175 * 176 * @return The 'q' value for this type, never <jk>null</jk>. 177 */ 178 public Float getQValue() { 179 return qValue; 180 } 181 182 /** 183 * Returns the optional set of custom extensions defined for this type. 184 * 185 * <p> 186 * Values are lowercase and never <jk>null</jk>. 187 * 188 * @return The optional list of extensions, never <jk>null</jk>. 189 */ 190 public Map<String,Set<String>> getExtensions() { 191 return extensions; 192 } 193 194 /** 195 * Provides a string representation of this media range, suitable for use as an <code>Accept</code> header value. 196 * 197 * <p> 198 * The literal text generated will be all lowercase. 199 * 200 * @return A media range suitable for use as an Accept header value, never <code>null</code>. 201 */ 202 @Override /* Object */ 203 public String toString() { 204 StringBuffer sb = new StringBuffer().append(type); 205 206 // '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue. 207 if (qValue.floatValue() == 1.0) { 208 if (! extensions.isEmpty()) { 209 sb.append(";q=").append(qValue); 210 for (Entry<String,Set<String>> e : extensions.entrySet()) { 211 String k = e.getKey(); 212 for (String v : e.getValue()) 213 sb.append(';').append(k).append('=').append(v); 214 } 215 } 216 } else { 217 sb.append(";q=").append(qValue); 218 for (Entry<String,Set<String>> e : extensions.entrySet()) { 219 String k = e.getKey(); 220 for (String v : e.getValue()) 221 sb.append(';').append(k).append('=').append(v); 222 } 223 } 224 return sb.toString(); 225 } 226 227 /** 228 * Returns <jk>true</jk> if the specified object is also a <code>MediaType</code>, and has the same qValue, type, 229 * parameters, and extensions. 230 * 231 * @return <jk>true</jk> if object is equivalent. 232 */ 233 @Override /* Object */ 234 public boolean equals(Object o) { 235 236 if (o == null || !(o instanceof StringRange)) 237 return false; 238 239 if (this == o) 240 return true; 241 242 StringRange o2 = (StringRange) o; 243 return qValue.equals(o2.qValue) 244 && type.equals(o2.type) 245 && extensions.equals(o2.extensions); 246 } 247 248 /** 249 * Returns a hash based on this instance's <code>media-type</code>. 250 * 251 * @return A hash based on this instance's <code>media-type</code>. 252 */ 253 @Override /* Object */ 254 public int hashCode() { 255 return type.hashCode(); 256 } 257 258 /** 259 * Compares two MediaRanges for equality. 260 * 261 * <p> 262 * The values are first compared according to <code>qValue</code> values. 263 * Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in 264 * ascending order, with the <js>"*"</js> type demoted last in that order. 265 * <code>TypeRanges</code> with the same types but with extensions are promoted over those same types with no 266 * extensions. 267 * 268 * @param o The range to compare to. Never <jk>null</jk>. 269 */ 270 @Override /* Comparable */ 271 public int compareTo(StringRange o) { 272 273 // Compare q-values. 274 int qCompare = Float.compare(o.qValue, qValue); 275 if (qCompare != 0) 276 return qCompare; 277 278 // Compare media-types. 279 // Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison. 280 int i = o.type.toString().compareTo(type.toString()); 281 return i; 282 } 283 284 /** 285 * Checks if the specified type matches this range. 286 * 287 * <p> 288 * The type will match this range if the range type string is the same or <js>"*"</js>. 289 * 290 * @param type The type to match against this range. 291 * @return <jk>true</jk> if the specified type matches this range. 292 */ 293 public boolean matches(String type) { 294 if (qValue == 0) 295 return false; 296 return this.type.equals(type) || this.type.equals("*"); 297 } 298}