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