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