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.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
023
024import java.util.*;
025import java.util.concurrent.*;
026
027import org.apache.juneau.*;
028import org.apache.juneau.commons.reflect.*;
029import org.apache.juneau.cp.*;
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>// Should return "gzip"</jc>
063 *    String <jv>matchedCoding</jv> = <jv>encoders</jv>.findMatch(<js>"compress;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0"</js>);
064 *
065 *    <jc>// Get the encoder</jc>
066 *    Encoder <jv>encoder</jv> = <jv>encoders</jv>.getEncoder(<jv>matchedCoding</jv>);
067 * </p>
068 *
069 * <h5 class='section'>Notes:</h5><ul>
070 *    <li class='note'>This class is thread safe and reusable.
071 * </ul>
072 *
073 * <h5 class='section'>See Also:</h5><ul>
074 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerEncoders">Encoders</a>
075
076 * </ul>
077 */
078public class EncoderSet {
079   /**
080    * Builder class.
081    */
082   public static class Builder extends BeanBuilder<EncoderSet> {
083      private static String toString(Object o) {
084         if (o == null)
085            return "null";
086         if (o instanceof Class)
087            return "class:" + cns(o);
088         return "object:" + cns(o);
089      }
090
091      List<Object> entries;
092
093      Builder inheritFrom;
094
095      /**
096       * Constructor.
097       *
098       * @param beanStore The bean store to use for creating beans.
099       */
100      protected Builder(BeanStore beanStore) {
101         super(EncoderSet.class, beanStore);
102         entries = list();
103      }
104
105      /**
106       * Copy constructor.
107       *
108       * @param copyFrom The builder being copied.
109       */
110      protected Builder(Builder copyFrom) {
111         super(copyFrom);
112         entries = copyOf(copyFrom.entries);
113      }
114
115      /**
116       * Registers the specified encoders with this group.
117       *
118       * <p>
119       * Entries are added in-order to the beginning of the list.
120       *
121       * @param values The encoders to add to this group.
122       * @return This object.
123       * @throws IllegalArgumentException if any class does not extend from {@link Encoder}.
124       */
125      public Builder add(Class<?>...values) {
126         List<Object> l = list();
127         for (var v : values)
128            if (cns(v).equals("NoInherit"))
129               clear();
130         for (var v : values) {
131            if (Encoder.class.isAssignableFrom(v)) {
132               l.add(v);
133            } else if (! cns(v).equals("NoInherit")) {
134               throw illegalArg("Invalid type passed to EncoderSet.Builder.add(): {0}", cn(v));
135            }
136         }
137         entries.addAll(0, l);
138         return this;
139      }
140
141      /**
142       * Registers the specified encoders with this group.
143       *
144       * <p>
145       * Entries are added to the beginning of the list.
146       *
147       * @param values The encoders to add to this group.
148       * @return This object.
149       */
150      public Builder add(Encoder...values) {
151         prependAll(entries, (Object[])values);
152         return this;
153      }
154
155      /**
156       * Clears out any existing encoders in this group.
157       *
158       * @return This object.
159       */
160      public Builder clear() {
161         entries.clear();
162         return this;
163      }
164
165      /**
166       * Makes a copy of this builder.
167       *
168       * @return A new copy of this builder.
169       */
170      public Builder copy() {
171         return new Builder(this);
172      }
173
174      @Override /* Overridden from BeanBuilder */
175      public Builder impl(Object value) {
176         super.impl(value);
177         return this;
178      }
179
180      /**
181       * Returns direct access to the {@link Encoder} objects and classes in this builder.
182       *
183       * <p>
184       * Provided to allow for any extraneous modifications to the list not accomplishable via other methods on this builder such
185       * as re-ordering/adding/removing entries.
186       *
187       * <p>
188       * Note that it is up to the user to ensure that the list only contains {@link Encoder} objects and classes.
189       *
190       * @return The inner list of entries in this builder.
191       */
192      public List<Object> inner() {
193         return entries;
194      }
195
196      /**
197       * Returns <jk>true</jk> if this builder is empty.
198       *
199       * @return <jk>true</jk> if this builder is empty.
200       */
201      public boolean isEmpty() { return entries.isEmpty(); }
202
203      /**
204       * Sets the encoders in this group.
205       *
206       * <p>
207       * All encoders in this group are replaced with the specified values.
208       *
209       * <p>
210       * If {@link Inherit} is specified (or any other class whose simple name is <js>"Inherit"</js>, the existing values are preserved
211       * and inserted into the position in the values array.
212       *
213       * @param values The encoders to add to this group.
214       * @return This object.
215       * @throws IllegalArgumentException if any class does not extend from {@link Encoder}.
216       */
217      public Builder set(Class<?>...values) {
218         List<Object> l = list();
219         for (var v : values) {
220            if (cns(v).equals("Inherit")) {
221               l.addAll(entries);
222            } else if (Encoder.class.isAssignableFrom(v)) {
223               l.add(v);
224            } else {
225               throw illegalArg("Invalid type passed to EncoderSet.Builder.set(): {0}", cn(v));
226            }
227         }
228         entries = l;
229         return this;
230      }
231
232      @Override /* Overridden from Object */
233      public String toString() {
234         return entries.stream().map(Builder::toString).collect(joining(",", "[", "]"));
235      }
236
237      @Override /* Overridden from BeanBuilder */
238      public Builder type(Class<?> value) {
239         super.type(value);
240         return this;
241      }
242
243      @Override /* Overridden from BeanBuilder */
244      protected EncoderSet buildDefault() {
245         return new EncoderSet(this);
246      }
247   }
248
249   /**
250    * An identifier that the previous encoders in this group should be inherited.
251    * <p>
252    * Used by {@link Builder#set(Class...)}
253    */
254   public static abstract class Inherit extends Encoder {}
255
256   /**
257    * An identifier that the previous encoders in this group should not be inherited.
258    * <p>
259    * Used by {@link Builder#add(Class...)}
260    */
261   public static abstract class NoInherit extends Encoder {}
262
263   /**
264    * Static creator.
265    *
266    * @return A new builder for this object.
267    */
268   public static Builder create() {
269      return new Builder(BeanStore.INSTANCE);
270   }
271
272   /**
273    * Static creator.
274    *
275    * @param beanStore The bean store to use for creating beans.
276    * @return A new builder for this object.
277    */
278   public static Builder create(BeanStore beanStore) {
279      return new Builder(beanStore);
280   }
281
282   private static Encoder instantiate(BeanStore bs, Object o) {
283      if (o instanceof Encoder o2)
284         return o2;
285      try {
286         return bs.createBean(Encoder.class).type((Class<?>)o).run();
287      } catch (ExecutableException e) {
288         throw toRex(e);
289      }
290   }
291
292   // Maps Accept-Encoding headers to matching encoders.
293   private final ConcurrentHashMap<String,EncoderMatch> cache = new ConcurrentHashMap<>();
294   private final List<String> encodings;
295   private final Encoder[] encodingsEncoders;
296
297   private final Encoder[] entries;
298
299   /**
300    * Constructor.
301    *
302    * @param builder The builder for this object.
303    */
304   protected EncoderSet(Builder builder) {
305      entries = builder.entries.stream().map(x -> instantiate(builder.beanStore(), x)).toArray(Encoder[]::new);
306
307      List<String> lc = list();
308      List<Encoder> l = list();
309      for (var e : entries) {
310         for (var c : e.getCodings()) {
311            lc.add(c);
312            l.add(e);
313         }
314      }
315
316      this.encodings = u(lc);
317      this.encodingsEncoders = l.toArray(new Encoder[l.size()]);
318   }
319
320   /**
321    * Returns the encoder registered with the specified coding (e.g. <js>"gzip"</js>).
322    *
323    * @param encoding The coding string.
324    * @return The encoder, or <jk>null</jk> if encoder isn't registered with that coding.
325    */
326   public Encoder getEncoder(String encoding) {
327      EncoderMatch em = getEncoderMatch(encoding);
328      return (em == null ? null : em.getEncoder());
329   }
330
331   /**
332    * Returns the coding string for the matching encoder that can handle the specified <c>Accept-Encoding</c>
333    * or <c>Content-Encoding</c> header value.
334    *
335    * <p>
336    * Returns <jk>null</jk> if no encoders can handle it.
337    *
338    * <p>
339    * This method is fully compliant with the RFC2616/14.3 and 14.11 specifications.
340    *
341    * @param acceptEncoding The <c>Accept-Encoding</c> or <c>Content-Encoding</c> value.
342    * @return The coding value (e.g. <js>"gzip"</js>).
343    */
344   public EncoderMatch getEncoderMatch(String acceptEncoding) {
345      EncoderMatch em = cache.get(acceptEncoding);
346      if (nn(em))
347         return em;
348
349      var ae = StringRanges.of(acceptEncoding);
350      int match = ae.match(encodings);
351
352      if (match >= 0) {
353         em = new EncoderMatch(encodings.get(match), encodingsEncoders[match]);
354         cache.putIfAbsent(acceptEncoding, em);
355      }
356
357      return cache.get(acceptEncoding);
358   }
359
360   /**
361    * Returns the set of codings supported by all encoders in this group.
362    *
363    * @return An unmodifiable list of codings supported by all encoders in this group.  Never <jk>null</jk>.
364    */
365   public List<String> getSupportedEncodings() { return encodings; }
366}