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(x -> toString(x)).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}