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}