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.encoders; 018 019import static java.util.stream.Collectors.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 023 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.*; 028import org.apache.juneau.commons.reflect.*; 029import org.apache.juneau.cp.*; 030 031/** 032 * Represents the set of {@link Encoder encoders} keyed by codings. 033 * 034 * <h5 class='topic'>Description</h5> 035 * 036 * Maintains a set of encoders and the codings that they can handle. 037 * 038 * <p> 039 * The {@link #getEncoderMatch(String)} and {@link #getEncoder(String)} methods are then used to find appropriate 040 * encoders for specific <c>Accept-Encoding</c> and <c>Content-Encoding</c> header values. 041 * 042 * <h5 class='topic'>Match ordering</h5> 043 * 044 * Encoders are matched against <c>Accept-Encoding</c> strings in the order they exist in this group. 045 * 046 * <p> 047 * Encoders are tried in the order they appear in the set. The {@link Builder#add(Class...)}/{@link Builder#add(Encoder...)} 048 * methods prepend the values to the list to allow them the opportunity to override encoders already in the list. 049 * 050 * <p> 051 * For example, calling <code>groupBuilder.add(E1.<jk>class</jk>,E2.<jk>class</jk>).add(E3.<jk>class</jk>, 052 * E4.<jk>class</jk>)</code> will result in the order <c>E3, E4, E1, E2</c>. 053 * 054 * <h5 class='section'>Example:</h5> 055 * <p class='bjava'> 056 * <jc>// Create an encoder group with support for gzip compression.</jc> 057 * EncoderSet <jv>encoders</jv> = EncoderSet 058 * .<jsm>create</jsm>() 059 * .add(GzipEncoder.<jk>class</jk>) 060 * .build(); 061 * 062 * <jc>// Should return "gzip"</jc> 063 * String <jv>matchedCoding</jv> = <jv>encoders</jv>.findMatch(<js>"compress;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0"</js>); 064 * 065 * <jc>// Get the encoder</jc> 066 * Encoder <jv>encoder</jv> = <jv>encoders</jv>.getEncoder(<jv>matchedCoding</jv>); 067 * </p> 068 * 069 * <h5 class='section'>Notes:</h5><ul> 070 * <li class='note'>This class is thread safe and reusable. 071 * </ul> 072 * 073 * <h5 class='section'>See Also:</h5><ul> 074 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerEncoders">Encoders</a> 075 076 * </ul> 077 */ 078public class EncoderSet { 079 /** 080 * Builder class. 081 */ 082 public static class Builder extends BeanBuilder<EncoderSet> { 083 private static String toString(Object o) { 084 if (o == null) 085 return "null"; 086 if (o instanceof Class) 087 return "class:" + cns(o); 088 return "object:" + cns(o); 089 } 090 091 List<Object> entries; 092 093 Builder inheritFrom; 094 095 /** 096 * Constructor. 097 * 098 * @param beanStore The bean store to use for creating beans. 099 */ 100 protected Builder(BeanStore beanStore) { 101 super(EncoderSet.class, beanStore); 102 entries = list(); 103 } 104 105 /** 106 * Copy constructor. 107 * 108 * @param copyFrom The builder being copied. 109 */ 110 protected Builder(Builder copyFrom) { 111 super(copyFrom); 112 entries = copyOf(copyFrom.entries); 113 } 114 115 /** 116 * Registers the specified encoders with this group. 117 * 118 * <p> 119 * Entries are added in-order to the beginning of the list. 120 * 121 * @param values The encoders to add to this group. 122 * @return This object. 123 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 124 */ 125 public Builder add(Class<?>...values) { 126 List<Object> l = list(); 127 for (var v : values) 128 if (cns(v).equals("NoInherit")) 129 clear(); 130 for (var v : values) { 131 if (Encoder.class.isAssignableFrom(v)) { 132 l.add(v); 133 } else if (! cns(v).equals("NoInherit")) { 134 throw illegalArg("Invalid type passed to EncoderSet.Builder.add(): {0}", cn(v)); 135 } 136 } 137 entries.addAll(0, l); 138 return this; 139 } 140 141 /** 142 * Registers the specified encoders with this group. 143 * 144 * <p> 145 * Entries are added to the beginning of the list. 146 * 147 * @param values The encoders to add to this group. 148 * @return This object. 149 */ 150 public Builder add(Encoder...values) { 151 prependAll(entries, (Object[])values); 152 return this; 153 } 154 155 /** 156 * Clears out any existing encoders in this group. 157 * 158 * @return This object. 159 */ 160 public Builder clear() { 161 entries.clear(); 162 return this; 163 } 164 165 /** 166 * Makes a copy of this builder. 167 * 168 * @return A new copy of this builder. 169 */ 170 public Builder copy() { 171 return new Builder(this); 172 } 173 174 @Override /* Overridden from BeanBuilder */ 175 public Builder impl(Object value) { 176 super.impl(value); 177 return this; 178 } 179 180 /** 181 * Returns direct access to the {@link Encoder} objects and classes in this builder. 182 * 183 * <p> 184 * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such 185 * as re-ordering/adding/removing entries. 186 * 187 * <p> 188 * Note that it is up to the user to ensure that the list only contains {@link Encoder} objects and classes. 189 * 190 * @return The inner list of entries in this builder. 191 */ 192 public List<Object> inner() { 193 return entries; 194 } 195 196 /** 197 * Returns <jk>true</jk> if this builder is empty. 198 * 199 * @return <jk>true</jk> if this builder is empty. 200 */ 201 public boolean isEmpty() { return entries.isEmpty(); } 202 203 /** 204 * Sets the encoders in this group. 205 * 206 * <p> 207 * All encoders in this group are replaced with the specified values. 208 * 209 * <p> 210 * If {@link Inherit} is specified (or any other class whose simple name is <js>"Inherit"</js>, the existing values are preserved 211 * and inserted into the position in the values array. 212 * 213 * @param values The encoders to add to this group. 214 * @return This object. 215 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 216 */ 217 public Builder set(Class<?>...values) { 218 List<Object> l = list(); 219 for (var v : values) { 220 if (cns(v).equals("Inherit")) { 221 l.addAll(entries); 222 } else if (Encoder.class.isAssignableFrom(v)) { 223 l.add(v); 224 } else { 225 throw illegalArg("Invalid type passed to EncoderSet.Builder.set(): {0}", cn(v)); 226 } 227 } 228 entries = l; 229 return this; 230 } 231 232 @Override /* Overridden from Object */ 233 public String toString() { 234 return entries.stream().map(Builder::toString).collect(joining(",", "[", "]")); 235 } 236 237 @Override /* Overridden from BeanBuilder */ 238 public Builder type(Class<?> value) { 239 super.type(value); 240 return this; 241 } 242 243 @Override /* Overridden from BeanBuilder */ 244 protected EncoderSet buildDefault() { 245 return new EncoderSet(this); 246 } 247 } 248 249 /** 250 * An identifier that the previous encoders in this group should be inherited. 251 * <p> 252 * Used by {@link Builder#set(Class...)} 253 */ 254 public static abstract class Inherit extends Encoder {} 255 256 /** 257 * An identifier that the previous encoders in this group should not be inherited. 258 * <p> 259 * Used by {@link Builder#add(Class...)} 260 */ 261 public static abstract class NoInherit extends Encoder {} 262 263 /** 264 * Static creator. 265 * 266 * @return A new builder for this object. 267 */ 268 public static Builder create() { 269 return new Builder(BeanStore.INSTANCE); 270 } 271 272 /** 273 * Static creator. 274 * 275 * @param beanStore The bean store to use for creating beans. 276 * @return A new builder for this object. 277 */ 278 public static Builder create(BeanStore beanStore) { 279 return new Builder(beanStore); 280 } 281 282 private static Encoder instantiate(BeanStore bs, Object o) { 283 if (o instanceof Encoder o2) 284 return o2; 285 try { 286 return bs.createBean(Encoder.class).type((Class<?>)o).run(); 287 } catch (ExecutableException e) { 288 throw toRex(e); 289 } 290 } 291 292 // Maps Accept-Encoding headers to matching encoders. 293 private final ConcurrentHashMap<String,EncoderMatch> cache = new ConcurrentHashMap<>(); 294 private final List<String> encodings; 295 private final Encoder[] encodingsEncoders; 296 297 private final Encoder[] entries; 298 299 /** 300 * Constructor. 301 * 302 * @param builder The builder for this object. 303 */ 304 protected EncoderSet(Builder builder) { 305 entries = builder.entries.stream().map(x -> instantiate(builder.beanStore(), x)).toArray(Encoder[]::new); 306 307 List<String> lc = list(); 308 List<Encoder> l = list(); 309 for (var e : entries) { 310 for (var c : e.getCodings()) { 311 lc.add(c); 312 l.add(e); 313 } 314 } 315 316 this.encodings = u(lc); 317 this.encodingsEncoders = l.toArray(new Encoder[l.size()]); 318 } 319 320 /** 321 * Returns the encoder registered with the specified coding (e.g. <js>"gzip"</js>). 322 * 323 * @param encoding The coding string. 324 * @return The encoder, or <jk>null</jk> if encoder isn't registered with that coding. 325 */ 326 public Encoder getEncoder(String encoding) { 327 EncoderMatch em = getEncoderMatch(encoding); 328 return (em == null ? null : em.getEncoder()); 329 } 330 331 /** 332 * Returns the coding string for the matching encoder that can handle the specified <c>Accept-Encoding</c> 333 * or <c>Content-Encoding</c> header value. 334 * 335 * <p> 336 * Returns <jk>null</jk> if no encoders can handle it. 337 * 338 * <p> 339 * This method is fully compliant with the RFC2616/14.3 and 14.11 specifications. 340 * 341 * @param acceptEncoding The <c>Accept-Encoding</c> or <c>Content-Encoding</c> value. 342 * @return The coding value (e.g. <js>"gzip"</js>). 343 */ 344 public EncoderMatch getEncoderMatch(String acceptEncoding) { 345 EncoderMatch em = cache.get(acceptEncoding); 346 if (nn(em)) 347 return em; 348 349 var ae = StringRanges.of(acceptEncoding); 350 int match = ae.match(encodings); 351 352 if (match >= 0) { 353 em = new EncoderMatch(encodings.get(match), encodingsEncoders[match]); 354 cache.putIfAbsent(acceptEncoding, em); 355 } 356 357 return cache.get(acceptEncoding); 358 } 359 360 /** 361 * Returns the set of codings supported by all encoders in this group. 362 * 363 * @return An unmodifiable list of codings supported by all encoders in this group. Never <jk>null</jk>. 364 */ 365 public List<String> getSupportedEncodings() { return encodings; } 366}