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}