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