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.http.Constants.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.util.*;
019
020import org.apache.http.*;
021import org.apache.http.message.*;
022import org.apache.juneau.annotation.*;
023import org.apache.juneau.collections.*;
024import org.apache.juneau.internal.*;
025
026/**
027 * A parsed <c>Accept</c> or similar header value.
028 *
029 * <p>
030 * The returned media ranges are sorted such that the most acceptable media is available at ordinal position
031 * <js>'0'</js>, and the least acceptable at position n-1.
032 *
033 * <p>
034 * The syntax expected to be found in the referenced <c>value</c> complies with the syntax described in
035 * RFC2616, Section 14.1, as described below:
036 * <p class='bcode w800'>
037 *    Accept         = "Accept" ":"
038 *                      #( media-range [ accept-params ] )
039 *
040 *    media-range    = ( "*\/*"
041 *                      | ( type "/" "*" )
042 *                      | ( type "/" subtype )
043 *                      ) *( ";" parameter )
044 *    accept-params  = ";" "q" "=" qvalue *( accept-extension )
045 *    accept-extension = ";" token [ "=" ( token | quoted-string ) ]
046 * </p>
047 */
048@BeanIgnore
049public class MediaRanges {
050
051   private static final MediaRanges DEFAULT = new MediaRanges("");
052   private static final Cache<String,MediaRanges> CACHE = new Cache<>(NOCACHE, CACHE_MAX_SIZE);
053
054   private final MediaRange[] ranges;
055   private final String string;
056
057   /**
058    * Returns a parsed <c>Accept</c> header value.
059    *
060    * @param value The raw <c>Accept</c> header value.
061    * @return A parsed <c>Accept</c> header value.
062    */
063   public static MediaRanges of(String value) {
064      if (value == null || value.length() == 0)
065         return DEFAULT;
066
067      MediaRanges mr = CACHE.get(value);
068      if (mr == null)
069         mr = CACHE.put(value, new MediaRanges(value));
070      return mr;
071   }
072
073   /**
074    * Constructor.
075    *
076    * @param value The <c>Accept</c> header value.
077    */
078   public MediaRanges(String value) {
079      this(parse(value));
080   }
081
082   /**
083    * Constructor.
084    *
085    * @param e The parsed <c>Accept</c> header value.
086    */
087   public MediaRanges(HeaderElement[] e) {
088
089      List<MediaRange> l = AList.of();
090      for (HeaderElement e2 : e)
091         l.add(new MediaRange(e2));
092
093      l.sort(RANGE_COMPARATOR);
094      ranges = l.toArray(new MediaRange[l.size()]);
095
096      this.string = ranges.length == 1 ? ranges[0].toString() : StringUtils.join(l, ',');
097   }
098
099   /**
100    * Compares two MediaRanges for equality.
101    *
102    * <p>
103    * The values are first compared according to <c>qValue</c> values.
104    * Should those values be equal, the <c>type</c> is then lexicographically compared (case-insensitive) in
105    * ascending order, with the <js>"*"</js> type demoted last in that order.
106    * <c>MediaRanges</c> with the same type but different sub-types are compared - a more specific subtype is
107    * promoted over the 'wildcard' subtype.
108    * <c>MediaRanges</c> with the same types but with extensions are promoted over those same types with no
109    * extensions.
110    */
111   private static final Comparator<MediaRange> RANGE_COMPARATOR = new Comparator<MediaRange>() {
112      @Override
113      public int compare(MediaRange o1, MediaRange o2) {
114         // Compare q-values.
115         int qCompare = Float.compare(o2.getQValue(), o1.getQValue());
116         if (qCompare != 0)
117            return qCompare;
118
119         // Compare media-types.
120         // Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison.
121         int i = o2.toString().compareTo(o1.toString());
122         return i;
123      }
124   };
125
126   /**
127    * Given a list of media types, returns the best match for this <c>Accept</c> header.
128    *
129    * <p>
130    * Note that fuzzy matching is allowed on the media types where the <c>Accept</c> header may
131    * contain additional subtype parts.
132    * <br>For example, given identical q-values and an <c>Accept</c> value of <js>"text/json+activity"</js>,
133    * the media type <js>"text/json"</js> will match if <js>"text/json+activity"</js> or <js>"text/activity+json"</js>
134    * isn't found.
135    * <br>The purpose for this is to allow serializers to match when artifacts such as <c>id</c> properties are
136    * present in the header.
137    *
138    * <p>
139    * See {@doc https://www.w3.org/TR/activitypub/#retrieving-objects ActivityPub / Retrieving Objects}
140    *
141    * @param mediaTypes The media types to match against.
142    * @return The index into the array of the best match, or <c>-1</c> if no suitable matches could be found.
143    */
144   public int match(List<? extends MediaType> mediaTypes) {
145      if (string.isEmpty() || mediaTypes == null)
146         return -1;
147
148      int matchQuant = 0, matchIndex = -1;
149      float q = 0f;
150
151      // Media ranges are ordered by 'q'.
152      // So we only need to search until we've found a match.
153      for (MediaRange mr : ranges) {
154         float q2 = mr.getQValue();
155
156         if (q2 < q || q2 == 0)
157            break;
158
159         for (int i = 0; i < mediaTypes.size(); i++) {
160            MediaType mt = mediaTypes.get(i);
161            int matchQuant2 = mr.match(mt, false);
162
163            if (matchQuant2 > matchQuant) {
164               matchIndex = i;
165               matchQuant = matchQuant2;
166               q = q2;
167            }
168         }
169      }
170
171      return matchIndex;
172   }
173
174   /**
175    * Returns the {@link MediaRange} at the specified index.
176    *
177    * @param index The index position of the media range.
178    * @return The {@link MediaRange} at the specified index or <jk>null</jk> if the index is out of range.
179    */
180   public MediaRange getRange(int index) {
181      if (index < 0 || index >= ranges.length)
182         return null;
183      return ranges[index];
184   }
185
186   /**
187    * Convenience method for searching through all of the subtypes of all the media ranges in this header for the
188    * presence of a subtype fragment.
189    *
190    * <p>
191    * For example, given the header <js>"text/json+activity"</js>, calling
192    * <code>hasSubtypePart(<js>"activity"</js>)</code> returns <jk>true</jk>.
193    *
194    * @param part The media type subtype fragment.
195    * @return <jk>true</jk> if subtype fragment exists.
196    */
197   public boolean hasSubtypePart(String part) {
198
199      for (MediaRange mr : ranges)
200         if (mr.getQValue() > 0 && mr.getSubTypes().indexOf(part) >= 0)
201            return true;
202
203      return false;
204   }
205
206   /**
207    * Returns the media ranges that make up this object.
208    *
209    * @return The media ranges that make up this object.
210    */
211   public List<MediaRange> getRanges() {
212      return Collections.unmodifiableList(Arrays.asList(ranges));
213   }
214
215   private static HeaderElement[] parse(String value) {
216      return BasicHeaderValueParser.parseElements(emptyIfNull(trim(value)), null);
217   }
218
219   @Override /* Object */
220   public String toString() {
221      return string;
222   }
223}