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}