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 java.util.*;
016import java.util.concurrent.*;
017
018import org.apache.juneau.collections.*;
019import org.apache.juneau.http.header.*;
020
021/**
022 * Represents the group of {@link Encoder encoders} keyed by codings.
023 *
024 * <h5 class='topic'>Description</h5>
025 *
026 * Maintains a set of encoders and the codings that they can handle.
027 *
028 * <p>
029 * The {@link #getEncoderMatch(String)} and {@link #getEncoder(String)} methods are then used to find appropriate
030 * encoders for specific <c>Accept-Encoding</c> and <c>Content-Encoding</c> header values.
031 *
032 * <h5 class='topic'>Match ordering</h5>
033 *
034 * Encoders are matched against <c>Accept-Encoding</c> strings in the order they exist in this group.
035 *
036 * <p>
037 * Adding new entries will cause the entries to be prepended to the group.
038 * This allows for previous encoders to be overridden through subsequent calls.
039 *
040 * <p>
041 * For example, calling <code>groupBuilder.append(E1.<jk>class</jk>,E2.<jk>class</jk>).append(E3.<jk>class</jk>,
042 * E4.<jk>class</jk>)</code> will result in the order <c>E3, E4, E1, E2</c>.
043 *
044 * <h5 class='section'>Example:</h5>
045 * <p class='bcode w800'>
046 *    <jc>// Create an encoder group with support for gzip compression.</jc>
047 *    EncoderGroup g = EncoderGroup.<jsm>create</jsm>().append(GzipEncoder.<jk>class</jk>).build();
048 *
049 *    <jc>// Should return "gzip"</jc>
050 *    String matchedCoding = g.findMatch(<js>"compress;q=1.0, gzip;q=0.8, identity;q=0.5, *;q=0"</js>);
051 *
052 *    <jc>// Get the encoder</jc>
053 *    IEncoder encoder = g.getEncoder(matchedCoding);
054 * </p>
055 */
056public final class EncoderGroup {
057
058   /**
059    * A default encoder group consisting of identity and G-Zip encoding.
060    */
061   public static final EncoderGroup DEFAULT = create().append(IdentityEncoder.class, GzipEncoder.class).build();
062
063   // Maps Accept-Encoding headers to matching encoders.
064   private final ConcurrentHashMap<String,EncoderMatch> cache = new ConcurrentHashMap<>();
065
066   private final List<String> encodings;
067   private final Encoder[] encodingsEncoders;
068   private final List<Encoder> encoders;
069
070   /**
071    * Instantiates a new clean-slate {@link EncoderGroupBuilder} object.
072    *
073    * <p>
074    * This is equivalent to simply calling <code><jk>new</jk> EncoderGroupBuilder()</code>.
075    *
076    * @return A new {@link EncoderGroupBuilder} object.
077    */
078   public static EncoderGroupBuilder create() {
079      return new EncoderGroupBuilder();
080   }
081
082   /**
083    * Returns a builder that's a copy of the settings on this encoder group.
084    *
085    * @return A new {@link EncoderGroupBuilder} initialized to this group.
086    */
087   public EncoderGroupBuilder builder() {
088      return new EncoderGroupBuilder(this);
089   }
090
091   /**
092    * Constructor
093    *
094    * @param encoders The encoders to add to this group.
095    */
096   public EncoderGroup(Encoder[] encoders) {
097      this.encoders = AList.unmodifiable(encoders);
098
099      AList<String> lc = AList.of();
100      AList<Encoder> l = AList.of();
101      for (Encoder e : encoders) {
102         for (String c: e.getCodings()) {
103            lc.add(c);
104            l.add(e);
105         }
106      }
107
108      this.encodings = lc.unmodifiable();
109      this.encodingsEncoders = l.asArrayOf(Encoder.class);
110   }
111
112   /**
113    * Returns the coding string for the matching encoder that can handle the specified <c>Accept-Encoding</c>
114    * or <c>Content-Encoding</c> header value.
115    *
116    * <p>
117    * Returns <jk>null</jk> if no encoders can handle it.
118    *
119    * <p>
120    * This method is fully compliant with the RFC2616/14.3 and 14.11 specifications.
121    *
122    * @param acceptEncoding The <c>Accept-Encoding</c> or <c>Content-Encoding</c> value.
123    * @return The coding value (e.g. <js>"gzip"</js>).
124    */
125   public EncoderMatch getEncoderMatch(String acceptEncoding) {
126      EncoderMatch em = cache.get(acceptEncoding);
127      if (em != null)
128         return em;
129
130      AcceptEncoding ae = AcceptEncoding.of(acceptEncoding);
131      int match = ae.match(encodings);
132
133      if (match >= 0) {
134         em = new EncoderMatch(encodings.get(match), encodingsEncoders[match]);
135         cache.putIfAbsent(acceptEncoding, em);
136      }
137
138      return cache.get(acceptEncoding);
139   }
140
141   /**
142    * Returns the encoder registered with the specified coding (e.g. <js>"gzip"</js>).
143    *
144    * @param encoding The coding string.
145    * @return The encoder, or <jk>null</jk> if encoder isn't registered with that coding.
146    */
147   public Encoder getEncoder(String encoding) {
148      EncoderMatch em = getEncoderMatch(encoding);
149      return (em == null ? null : em.getEncoder());
150   }
151
152   /**
153    * Returns the set of codings supported by all encoders in this group.
154    *
155    * @return An unmodifiable list of codings supported by all encoders in this group.  Never <jk>null</jk>.
156    */
157   public List<String> getSupportedEncodings() {
158      return encodings;
159   }
160
161   /**
162    * Returns the encoders in this group.
163    *
164    * @return An unmodifiable list of encoders in this group.
165    */
166   public List<Encoder> getEncoders() {
167      return encoders;
168   }
169}