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