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}