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}