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.marshall.encoders; 018 019import static java.util.stream.Collectors.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.commons.http.*; 028import org.apache.juneau.commons.inject.*; 029import org.apache.juneau.commons.reflect.*; 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>// Get the encoder for "gzip"</jc> 063 * Encoder <jv>encoder</jv> = <jv>encoders</jv>.getEncoder(<js>"compress;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0"</js>); 064 * </p> 065 * 066 * <h5 class='section'>Notes:</h5><ul> 067 * <li class='note'>This class is thread safe and reusable. 068 * </ul> 069 * 070 * <h5 class='section'>See Also:</h5><ul> 071 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MarshallEncoders">Encoders</a> 072 073 * </ul> 074 */ 075public class EncoderSet { 076 077 /** 078 * Builder class. 079 */ 080 public static class Builder implements BeanStoreOverridable<Builder> { 081 private static String toString(Object o) { 082 if (o == null) 083 return "null"; 084 if (o instanceof Class) 085 return "class:" + cns(o); 086 return "object:" + cns(o); 087 } 088 089 private BeanStore beanStore; 090 private EncoderSet impl; 091 092 List<Object> entries; 093 094 /** 095 * Constructor. 096 * 097 * @param beanStore The bean store to use for creating beans. 098 */ 099 protected Builder(BeanStore beanStore) { 100 this.beanStore = beanStore; 101 entries = list(); 102 } 103 104 /** 105 * Copy constructor. 106 * 107 * @param copyFrom The builder being copied. 108 */ 109 protected Builder(Builder copyFrom) { 110 this.beanStore = copyFrom.beanStore; 111 this.impl = copyFrom.impl; 112 entries = cp(copyFrom.entries); 113 } 114 115 /** 116 * Returns the bean store used by this builder. 117 * 118 * @return The bean store used by this builder. 119 */ 120 public BeanStore beanStore() { 121 return beanStore; 122 } 123 124 /** 125 * Installs the supplied {@link BeanStore} as the {@code overridingParent} of this builder's bean store. 126 * 127 * <p> 128 * Wraps the current {@link #beanStore()} in a fresh {@link BasicBeanStore} whose {@code overridingParent} 129 * is the supplied store, so test-time overrides win over the builder's regular bean lookups during 130 * construction-time reflective injection. Passing {@code null} is a no-op. 131 * 132 * @param store The override layer. Can be <jk>null</jk> (no-op). 133 * @return This object. 134 * @since 10.0.0 135 */ 136 @Override 137 @SuppressWarnings({ 138 "resource" // BasicBeanStore lifecycle is owned by the enclosing builder; closed externally when the builder is disposed. 139 }) 140 public Builder overridingBeanStore(BeanStore store) { 141 if (store != null) 142 this.beanStore = new BasicBeanStore(this.beanStore, store); 143 return this; 144 } 145 146 /** 147 * Registers the specified encoders with this group. 148 * 149 * <p> 150 * Entries are added in-order to the beginning of the list. 151 * 152 * @param values The encoders to add to this group. 153 * @return This object. 154 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 155 */ 156 public Builder add(Class<?>...values) { 157 List<Object> l = list(); 158 for (var v : values) 159 if (eq(cns(v), "NoInherit")) 160 clear(); 161 for (var v : values) { 162 if (Encoder.class.isAssignableFrom(v)) { 163 l.add(v); 164 } else if (neq(cns(v), "NoInherit")) { 165 throw iaex("Invalid type passed to EncoderSet.Builder.add(): %s", cn(v)); 166 } 167 } 168 entries.addAll(0, l); 169 return this; 170 } 171 172 /** 173 * Registers the specified encoders with this group. 174 * 175 * <p> 176 * Entries are added to the beginning of the list. 177 * 178 * @param values The encoders to add to this group. 179 * @return This object. 180 */ 181 public Builder add(Encoder...values) { 182 prependAll(entries, (Object[])values); 183 return this; 184 } 185 186 /** 187 * Clears out any existing encoders in this group. 188 * 189 * @return This object. 190 */ 191 public Builder clear() { 192 entries.clear(); 193 return this; 194 } 195 196 /** 197 * Makes a copy of this builder. 198 * 199 * @return A new copy of this builder. 200 */ 201 public Builder copy() { 202 return new Builder(this); 203 } 204 205 /** 206 * Overrides the bean returned by the {@link #build()} method with a pre-built instance. 207 * 208 * @param value The pre-built instance. 209 * @return This object. 210 */ 211 public Builder impl(Object value) { 212 impl = (EncoderSet) value; 213 return this; 214 } 215 216 /** 217 * Returns direct access to the {@link Encoder} objects and classes in this builder. 218 * 219 * <p> 220 * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such 221 * as re-ordering/adding/removing entries. 222 * 223 * <p> 224 * Note that it is up to the user to ensure that the list only contains {@link Encoder} objects and classes. 225 * 226 * @return The inner list of entries in this builder. 227 */ 228 public List<Object> inner() { 229 return entries; 230 } 231 232 /** 233 * Returns <jk>true</jk> if this builder is empty. 234 * 235 * @return <jk>true</jk> if this builder is empty. 236 */ 237 public boolean isEmpty() { return entries.isEmpty(); } 238 239 /** 240 * Sets the encoders in this group. 241 * 242 * <p> 243 * All encoders in this group are replaced with the specified values. 244 * 245 * <p> 246 * If {@link Inherit} is specified (or any other class whose simple name is <js>"Inherit"</js>, the existing values are preserved 247 * and inserted into the position in the values array. 248 * 249 * @param values The encoders to add to this group. 250 * @return This object. 251 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 252 */ 253 public Builder set(Class<?>...values) { 254 List<Object> l = list(); 255 for (var v : values) { 256 if (eq(cns(v), "Inherit")) { 257 l.addAll(entries); 258 } else if (Encoder.class.isAssignableFrom(v)) { 259 l.add(v); 260 } else { 261 throw iaex("Invalid type passed to EncoderSet.Builder.set(): %s", cn(v)); 262 } 263 } 264 entries = l; 265 return this; 266 } 267 268 @Override /* Overridden from Object */ 269 public String toString() { 270 return entries.stream().map(Builder::toString).collect(joining(",", "[", "]")); 271 } 272 273 /** 274 * Builds the encoder set. 275 * 276 * @return A new {@link EncoderSet}. 277 */ 278 public EncoderSet build() { 279 if (nn(impl)) 280 return impl; 281 return new EncoderSet(this); 282 } 283 } 284 285 /** 286 * An identifier that the previous encoders in this group should be inherited. 287 * <p> 288 * Used by {@link Builder#set(Class...)} 289 */ 290 public abstract static class Inherit extends Encoder {} 291 292 /** 293 * An identifier that the previous encoders in this group should not be inherited. 294 * <p> 295 * Used by {@link Builder#add(Class...)} 296 */ 297 public abstract static class NoInherit extends Encoder {} 298 299 /** 300 * Static creator. 301 * 302 * @return A new builder for this object. 303 */ 304 public static Builder create() { 305 return new Builder(BasicBeanStore.INSTANCE); 306 } 307 308 /** 309 * Static creator. 310 * 311 * @param beanStore The bean store to use for creating beans. 312 * @return A new builder for this object. 313 */ 314 public static Builder create(BeanStore beanStore) { 315 return new Builder(beanStore); 316 } 317 318 private static Encoder instantiate(BeanStore bs, Object o) { 319 if (o instanceof Encoder o2) 320 return o2; 321 try { 322 @SuppressWarnings({ 323 "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site 324 }) 325 var subType = (Class<? extends Encoder>) o; 326 return BeanInstantiator.of(Encoder.class, bs).type(subType).run(); 327 } catch (ExecutableException e) { 328 throw toRex(e); 329 } 330 } 331 332 // Maps Accept-Encoding headers to matching encoders. 333 private final ConcurrentHashMap<String,EncoderMatch> cache = new ConcurrentHashMap<>(); 334 private final List<String> encodings; 335 private final Encoder[] encodingsEncoders; 336 337 private final Encoder[] entries; 338 339 /** 340 * Constructor. 341 * 342 * @param builder The builder for this object. 343 */ 344 protected EncoderSet(Builder builder) { 345 entries = builder.entries.stream().map(x -> instantiate(builder.beanStore(), x)).toArray(Encoder[]::new); 346 347 List<String> lc = list(); 348 List<Encoder> l = list(); 349 for (var e : entries) { 350 for (var c : e.getCodings()) { 351 lc.add(c); 352 l.add(e); 353 } 354 } 355 356 this.encodings = u(lc); 357 this.encodingsEncoders = l.toArray(new Encoder[l.size()]); 358 } 359 360 /** 361 * Returns the encoder registered with the specified coding (e.g. <js>"gzip"</js>). 362 * 363 * @param encoding The coding string. 364 * @return The encoder, or <jk>null</jk> if encoder isn't registered with that coding. 365 */ 366 public Encoder getEncoder(String encoding) { 367 EncoderMatch em = getEncoderMatch(encoding); 368 return (em == null ? null : em.getEncoder()); 369 } 370 371 /** 372 * Returns the encoder match for the encoder that can handle the specified <c>Accept-Encoding</c> 373 * or <c>Content-Encoding</c> header value. 374 * 375 * <p> 376 * Returns <jk>null</jk> if no encoders can handle it. 377 * 378 * <p> 379 * This method is fully compliant with the RFC2616/14.3 and 14.11 specifications. 380 * 381 * @param acceptEncoding The <c>Accept-Encoding</c> or <c>Content-Encoding</c> value. 382 * @return The matching encoder, or <jk>null</jk> if no encoders can handle the specified value. 383 */ 384 public EncoderMatch getEncoderMatch(String acceptEncoding) { 385 EncoderMatch em = cache.get(acceptEncoding); 386 if (nn(em)) 387 return em; 388 389 var ae = StringRanges.of(acceptEncoding); 390 int match = ae.match(encodings); 391 392 if (match >= 0) { 393 em = new EncoderMatch(encodings.get(match), encodingsEncoders[match]); 394 cache.putIfAbsent(acceptEncoding, em); 395 } 396 397 return cache.get(acceptEncoding); 398 } 399 400 /** 401 * Returns the set of codings supported by all encoders in this group. 402 * 403 * @return An unmodifiable list of codings supported by all encoders in this group. Never <jk>null</jk>. 404 */ 405 public List<String> getSupportedEncodings() { return encodings; } 406}