001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.http;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.juneau.commons.bean.*;
027
028/**
029 * Represents a single value in a comma-delimited header value that optionally contains a quality metric for
030 * comparison and extension parameters.
031 *
032 * <p>
033 * Similar in concept to {@link MediaRanges} except instead of media types (e.g. <js>"text/json"</js>),
034 * it's a simple type (e.g. <js>"iso-8601"</js>).
035 *
036 * <p>
037 * An example of a type range is a value in an <c>Accept-Encoding</c> header.
038 *
039 * <h5 class='section'>See Also:</h5><ul>
040 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common</a>
041 *    <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
042
043 * </ul>
044 */
045@BeanIgnore
046public class StringRange {
047
048   private static HeaderElement parse(String value) {
049      HeaderElement[] elements = HeaderValueParser.readElements(emptyIfNull(trim(value)));
050      return (elements.length > 0 ? elements[0] : new HeaderElement("*", NameValuePair.EMPTY_ARRAY));
051   }
052
053   private final NameValuePair[] extensions;
054   private final Float qValue;
055   private final String name;
056
057   private final String string;
058
059   /**
060    * Constructor.
061    *
062    * @param e The parsed string range element.
063    */
064   @SuppressWarnings({
065      "javabugs:S2259" // The header element must not be null, per the Javadoc contract.
066   })
067   public StringRange(HeaderElement e) {
068      Float qValue2 = 1f;
069
070      // The media type consists of everything up to the q parameter.
071      // The q parameter and stuff after is part of the range.
072      List<NameValuePair> extensions2 = list();
073      for (var p : e.getParameters()) {
074         if (eq(p.getName(), "q")) {
075            qValue2 = Float.parseFloat(p.getValue());
076         } else {
077            extensions2.add(new BasicNameValuePair(p.getName(), p.getValue()));
078         }
079      }
080
081      this.qValue = qValue2;
082      this.extensions = extensions2.toArray(NameValuePair.EMPTY_ARRAY);
083      this.name = e.getName();
084
085      var sb = new StringBuilder();
086      sb.append(name);
087
088      // '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue.
089      if (Float.compare(qValue2.floatValue(), 1f) == 0) {
090         if (this.extensions.length > 0) {
091            sb.append(";q=").append(qValue2);
092            extensions2.forEach(x -> sb.append(';').append(x.getName()).append('=').append(x.getValue()));
093         }
094      } else {
095         sb.append(";q=").append(qValue2);
096         extensions2.forEach(x -> sb.append(';').append(x.getName()).append('=').append(x.getValue()));
097      }
098      string = sb.toString();
099   }
100
101   /**
102    * Constructor.
103    *
104    * @param value
105    *    The raw string range string.
106    *    <br>A value of <jk>null</jk> gets interpreted as matching anything (e.g. <js>"*"</js>).
107    */
108   public StringRange(String value) {
109      this(parse(value));
110   }
111
112   /**
113    * Returns <jk>true</jk> if the specified object is also a <c>StringRange</c>, and has the same qValue, type,
114    * parameters, and extensions.
115    *
116    * @return <jk>true</jk> if object is equivalent.
117    */
118   @Override /* Overridden from Object */
119   public boolean equals(Object o) {
120      return (o instanceof StringRange o2) && eq(this, o2, (x, y) -> eq(x.string, y.string));
121   }
122
123   /**
124    * Performs an action on the optional set of custom extensions defined for this type.
125    *
126    * @param action The action to perform.
127    * @return This object.
128    */
129   public StringRange forEachExtension(Consumer<NameValuePair> action) {
130      for (var p : extensions)
131         action.accept(p);
132      return this;
133   }
134
135   /**
136    * Returns the optional set of custom extensions defined for this type.
137    *
138    * <p>
139    * Values are lowercase and never <jk>null</jk>.
140    *
141    * @return The optional list of extensions, never <jk>null</jk>.
142    */
143   public List<NameValuePair> getExtensions() { return l(extensions); }
144
145   /**
146    * Returns the name of this string range.
147    *
148    * <p>
149    * This is the primary value minus the quality or other parameters.
150    *
151    * @return The name of this string range.
152    */
153   public String getName() { return name; }
154
155   /**
156    * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616.
157    *
158    * <p>
159    * The quality value is a float between <c>0.0</c> (unacceptable) and <c>1.0</c> (most acceptable).
160    *
161    * <p>
162    * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js>
163    * header, as opposed to <js>"accept-*"</js> header, its value will always be <js>"1"</js>.
164    *
165    * @return The 'q' value for this type, never <jk>null</jk>.
166    */
167   public Float getQValue() { return qValue; }
168
169   /**
170    * Returns a hash based on this instance's <c>media-type</c>.
171    *
172    * @return A hash based on this instance's <c>media-type</c>.
173    */
174   @Override /* Overridden from Object */
175   public int hashCode() {
176      return string.hashCode();
177   }
178
179   /**
180    * Performs a match of this string range against the specified name.
181    *
182    * @param name The name being compared against.
183    * @return
184    *    0 = no match, 100 = perfect match, 50 = meta-match.
185    */
186   public int match(String name) {
187      if (qValue == 0)
188         return 0;
189      if (eq(this.name, name))
190         return 100;
191      if (eq(this.name, "*"))
192         return 50;
193      return 0;
194   }
195
196   @Override /* Overridden from Object */
197   public String toString() {
198      return string;
199   }
200}