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.common.utils.ThrowableUtils.*; 021import static org.apache.juneau.common.utils.Utils.*; 022import static org.apache.juneau.internal.CollectionUtils.*; 023 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.*; 028import org.apache.juneau.common.utils.*; 029import org.apache.juneau.cp.*; 030import org.apache.juneau.internal.*; 031 032/** 033 * Represents the set of {@link Encoder encoders} keyed by codings. 034 * 035 * <h5 class='topic'>Description</h5> 036 * 037 * Maintains a set of encoders and the codings that they can handle. 038 * 039 * <p> 040 * The {@link #getEncoderMatch(String)} and {@link #getEncoder(String)} methods are then used to find appropriate 041 * encoders for specific <c>Accept-Encoding</c> and <c>Content-Encoding</c> header values. 042 * 043 * <h5 class='topic'>Match ordering</h5> 044 * 045 * Encoders are matched against <c>Accept-Encoding</c> strings in the order they exist in this group. 046 * 047 * <p> 048 * Encoders are tried in the order they appear in the set. The {@link Builder#add(Class...)}/{@link Builder#add(Encoder...)} 049 * methods prepend the values to the list to allow them the opportunity to override encoders already in the list. 050 * 051 * <p> 052 * For example, calling <code>groupBuilder.add(E1.<jk>class</jk>,E2.<jk>class</jk>).add(E3.<jk>class</jk>, 053 * E4.<jk>class</jk>)</code> will result in the order <c>E3, E4, E1, E2</c>. 054 * 055 * <h5 class='section'>Example:</h5> 056 * <p class='bjava'> 057 * <jc>// Create an encoder group with support for gzip compression.</jc> 058 * EncoderSet <jv>encoders</jv> = EncoderSet 059 * .<jsm>create</jsm>() 060 * .add(GzipEncoder.<jk>class</jk>) 061 * .build(); 062 * 063 * <jc>// Should return "gzip"</jc> 064 * String <jv>matchedCoding</jv> = <jv>encoders</jv>.findMatch(<js>"compress;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0"</js>); 065 * 066 * <jc>// Get the encoder</jc> 067 * Encoder <jv>encoder</jv> = <jv>encoders</jv>.getEncoder(<jv>matchedCoding</jv>); 068 * </p> 069 * 070 * <h5 class='section'>Notes:</h5><ul> 071 * <li class='note'>This class is thread safe and reusable. 072 * </ul> 073 * 074 * <h5 class='section'>See Also:</h5><ul> 075 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerEncoders">Encoders</a> 076 077 * </ul> 078 */ 079public class EncoderSet { 080 081 //----------------------------------------------------------------------------------------------------------------- 082 // Static 083 //----------------------------------------------------------------------------------------------------------------- 084 085 /** 086 * An identifier that the previous encoders in this group should be inherited. 087 * <p> 088 * Used by {@link Builder#set(Class...)} 089 */ 090 public static abstract class Inherit extends Encoder {} 091 092 /** 093 * An identifier that the previous encoders in this group should not be inherited. 094 * <p> 095 * Used by {@link Builder#add(Class...)} 096 */ 097 public static abstract class NoInherit extends Encoder {} 098 099 /** 100 * Static creator. 101 * 102 * @param beanStore The bean store to use for creating beans. 103 * @return A new builder for this object. 104 */ 105 public static Builder create(BeanStore beanStore) { 106 return new Builder(beanStore); 107 } 108 109 /** 110 * Static creator. 111 * 112 * @return A new builder for this object. 113 */ 114 public static Builder create() { 115 return new Builder(BeanStore.INSTANCE); 116 } 117 118 //----------------------------------------------------------------------------------------------------------------- 119 // Builder 120 //----------------------------------------------------------------------------------------------------------------- 121 122 /** 123 * Builder class. 124 */ 125 public static class Builder extends BeanBuilder<EncoderSet> { 126 List<Object> entries; 127 Builder inheritFrom; 128 129 /** 130 * Constructor. 131 * 132 * @param beanStore The bean store to use for creating beans. 133 */ 134 protected Builder(BeanStore beanStore) { 135 super(EncoderSet.class, beanStore); 136 entries = Utils.list(); 137 } 138 139 /** 140 * Copy constructor. 141 * 142 * @param copyFrom The builder being copied. 143 */ 144 protected Builder(Builder copyFrom) { 145 super(copyFrom); 146 entries = copyOf(copyFrom.entries); 147 } 148 149 @Override /* BeanBuilder */ 150 protected EncoderSet buildDefault() { 151 return new EncoderSet(this); 152 } 153 154 /** 155 * Makes a copy of this builder. 156 * 157 * @return A new copy of this builder. 158 */ 159 public Builder copy() { 160 return new Builder(this); 161 } 162 163 //------------------------------------------------------------------------------------------------------------- 164 // Properties 165 //------------------------------------------------------------------------------------------------------------- 166 167 /** 168 * Registers the specified encoders with this group. 169 * 170 * <p> 171 * Entries are added in-order to the beginning of the list. 172 * 173 * @param values The encoders to add to this group. 174 * @return This object. 175 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 176 */ 177 public Builder add(Class<?>...values) { 178 List<Object> l = Utils.list(); 179 for (Class<?> v : values) 180 if (v.getSimpleName().equals("NoInherit")) 181 clear(); 182 for (Class<?> v : values) { 183 if (Encoder.class.isAssignableFrom(v)) { 184 l.add(v); 185 } else if (! v.getSimpleName().equals("NoInherit")) { 186 throw new IllegalArgumentException("Invalid type passed to EncoderSet.Builder.add(): " + v.getName()); 187 } 188 } 189 entries.addAll(0, l); 190 return this; 191 } 192 193 /** 194 * Sets the encoders in this group. 195 * 196 * <p> 197 * All encoders in this group are replaced with the specified values. 198 * 199 * <p> 200 * If {@link Inherit} is specified (or any other class whose simple name is <js>"Inherit"</js>, the existing values are preserved 201 * and inserted into the position in the values array. 202 * 203 * @param values The encoders to add to this group. 204 * @return This object. 205 * @throws IllegalArgumentException if any class does not extend from {@link Encoder}. 206 */ 207 public Builder set(Class<?>...values) { 208 List<Object> l = Utils.list(); 209 for (Class<?> v : values) { 210 if (v.getSimpleName().equals("Inherit")) { 211 l.addAll(entries); 212 } else if (Encoder.class.isAssignableFrom(v)) { 213 l.add(v); 214 } else { 215 throw new IllegalArgumentException("Invalid type passed to EncoderSet.Builder.set(): " + v.getName()); 216 } 217 } 218 entries = l; 219 return this; 220 } 221 222 /** 223 * Registers the specified encoders with this group. 224 * 225 * <p> 226 * Entries are added to the beginning of the list. 227 * 228 * @param values The encoders to add to this group. 229 * @return This object. 230 */ 231 public Builder add(Encoder...values) { 232 prependAll(entries, (Object[])values); 233 return this; 234 } 235 236 /** 237 * Clears out any existing encoders in this group. 238 * 239 * @return This object. 240 */ 241 public Builder clear() { 242 entries.clear(); 243 return this; 244 } 245 246 /** 247 * Returns <jk>true</jk> if this builder is empty. 248 * 249 * @return <jk>true</jk> if this builder is empty. 250 */ 251 public boolean isEmpty() { 252 return entries.isEmpty(); 253 } 254 255 /** 256 * Returns direct access to the {@link Encoder} objects and classes in this builder. 257 * 258 * <p> 259 * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such 260 * as re-ordering/adding/removing entries. 261 * 262 * <p> 263 * Note that it is up to the user to ensure that the list only contains {@link Encoder} objects and classes. 264 * 265 * @return The inner list of entries in this builder. 266 */ 267 public List<Object> inner() { 268 return entries; 269 } 270 @Override /* Overridden from BeanBuilder */ 271 public Builder impl(Object value) { 272 super.impl(value); 273 return this; 274 } 275 276 @Override /* Overridden from BeanBuilder */ 277 public Builder type(Class<?> value) { 278 super.type(value); 279 return this; 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 = Utils.list(); 319 List<Encoder> l = Utils.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 = u(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}