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;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.http.*;
027import org.apache.http.message.*;
028import org.apache.juneau.annotation.*;
029
030/**
031 * Represents a single value in a comma-delimited header value that optionally contains a quality metric for
032 * comparison and extension parameters.
033 *
034 * <p>
035 * Similar in concept to {@link MediaRanges} except instead of media types (e.g. <js>"text/json"</js>),
036 * it's a simple type (e.g. <js>"iso-8601"</js>).
037 *
038 * <p>
039 * An example of a type range is a value in an <c>Accept-Encoding</c> header.
040 *
041 * <h5 class='section'>See Also:</h5><ul>
042 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommonBasics">juneau-rest-common Basics</a>
043 *    <li class='extlink'><a class="doclink" href="https://www.w3.org/Protocols/rfc2616/rfc2616.html">Hypertext Transfer Protocol -- HTTP/1.1</a>
044
045 * </ul>
046 */
047@BeanIgnore
048public class StringRange {
049
050   private static HeaderElement parse(String value) {
051      HeaderElement[] elements = BasicHeaderValueParser.parseElements(emptyIfNull(trim(value)), null);
052      return (elements.length > 0 ? elements[0] : new BasicHeaderElement("*", ""));
053   }
054
055   private final NameValuePair[] extensions;
056   private final Float qValue;
057   private final String name;
058
059   private final String string;
060
061   /**
062    * Constructor.
063    *
064    * @param e The parsed string range element.
065    */
066   public StringRange(HeaderElement e) {
067      Float qValue = 1f;
068
069      // The media type consists of everything up to the q parameter.
070      // The q parameter and stuff after is part of the range.
071      List<NameValuePair> extensions = list();
072      for (var p : e.getParameters()) {
073         if (p.getName().equals("q")) {
074            qValue = Float.parseFloat(p.getValue());
075         } else {
076            extensions.add(new BasicNameValuePair(p.getName(), p.getValue()));
077         }
078      }
079
080      this.qValue = qValue;
081      this.extensions = extensions.toArray(new NameValuePair[extensions.size()]);
082      this.name = e.getName();
083
084      var sb = new StringBuffer();
085      sb.append(name);
086
087      // '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue.
088      if (Float.compare(qValue.floatValue(), 1f) == 0) {
089         if (this.extensions.length > 0) {
090            sb.append(";q=").append(qValue);
091            extensions.forEach(x -> sb.append(';').append(x.getName()).append('=').append(x.getValue()));
092         }
093      } else {
094         sb.append(";q=").append(qValue);
095         extensions.forEach(x -> sb.append(';').append(x.getName()).append('=').append(x.getValue()));
096      }
097      string = sb.toString();
098   }
099
100   /**
101    * Constructor.
102    *
103    * @param value
104    *    The raw string range string.
105    *    <br>A value of <jk>null</jk> gets interpreted as matching anything (e.g. <js>"*"</js>).
106    */
107   public StringRange(String value) {
108      this(parse(value));
109   }
110
111   /**
112    * Returns <jk>true</jk> if the specified object is also a <c>StringRange</c>, and has the same qValue, type,
113    * parameters, and extensions.
114    *
115    * @return <jk>true</jk> if object is equivalent.
116    */
117   @Override /* Overridden from Object */
118   public boolean equals(Object o) {
119      return (o instanceof StringRange o2) && eq(this, o2, (x, y) -> eq(x.string, y.string));
120   }
121
122   /**
123    * Performs an action on the optional set of custom extensions defined for this type.
124    *
125    * @param action The action to perform.
126    * @return This object.
127    */
128   public StringRange forEachExtension(Consumer<NameValuePair> action) {
129      for (var p : extensions)
130         action.accept(p);
131      return this;
132   }
133
134   /**
135    * Returns the optional set of custom extensions defined for this type.
136    *
137    * <p>
138    * Values are lowercase and never <jk>null</jk>.
139    *
140    * @return The optional list of extensions, never <jk>null</jk>.
141    */
142   public List<NameValuePair> getExtensions() { return l(extensions); }
143
144   /**
145    * Returns the name of this string range.
146    *
147    * <p>
148    * This is the primary value minus the quality or other parameters.
149    *
150    * @return The name of this string range.
151    */
152   public String getName() { return name; }
153
154   /**
155    * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616.
156    *
157    * <p>
158    * The quality value is a float between <c>0.0</c> (unacceptable) and <c>1.0</c> (most acceptable).
159    *
160    * <p>
161    * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js>
162    * header, as opposed to <js>"accept-*"</js> header, its value will always be <js>"1"</js>.
163    *
164    * @return The 'q' value for this type, never <jk>null</jk>.
165    */
166   public Float getQValue() { return qValue; }
167
168   /**
169    * Returns a hash based on this instance's <c>media-type</c>.
170    *
171    * @return A hash based on this instance's <c>media-type</c>.
172    */
173   @Override /* Overridden from Object */
174   public int hashCode() {
175      return string.hashCode();
176   }
177
178   /**
179    * Performs a match of this string range against the specified name.
180    *
181    * @param name The name being compared against.
182    * @return
183    *    0 = no match, 100 = perfect match, 50 = meta-match.
184    */
185   public int match(String name) {
186      if (qValue == 0)
187         return 0;
188      if (eq(this.name, name))
189         return 100;
190      if (eq(this.name, "*"))
191         return 50;
192      return 0;
193   }
194
195   @Override /* Overridden from Object */
196   public String toString() {
197      return string;
198   }
199}