001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.io.compress;
019
020import java.util.*;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.apache.hadoop.classification.InterfaceAudience;
025import org.apache.hadoop.classification.InterfaceStability;
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.Path;
028import org.apache.hadoop.util.ReflectionUtils;
029
030/**
031 * A factory that will find the correct codec for a given filename.
032 */
033@InterfaceAudience.Public
034@InterfaceStability.Evolving
035public class CompressionCodecFactory {
036
037  public static final Log LOG =
038    LogFactory.getLog(CompressionCodecFactory.class.getName());
039  
040  private static final ServiceLoader<CompressionCodec> CODEC_PROVIDERS =
041    ServiceLoader.load(CompressionCodec.class);
042
043  /**
044   * A map from the reversed filename suffixes to the codecs.
045   * This is probably overkill, because the maps should be small, but it 
046   * automatically supports finding the longest matching suffix. 
047   */
048  private SortedMap<String, CompressionCodec> codecs = null;
049
050    /**
051     * A map from the reversed filename suffixes to the codecs.
052     * This is probably overkill, because the maps should be small, but it
053     * automatically supports finding the longest matching suffix.
054     */
055    private Map<String, CompressionCodec> codecsByName = null;
056
057  /**
058   * A map from class names to the codecs
059   */
060  private HashMap<String, CompressionCodec> codecsByClassName = null;
061
062  private void addCodec(CompressionCodec codec) {
063    String suffix = codec.getDefaultExtension();
064    codecs.put(new StringBuilder(suffix).reverse().toString(), codec);
065    codecsByClassName.put(codec.getClass().getCanonicalName(), codec);
066
067    String codecName = codec.getClass().getSimpleName();
068    codecsByName.put(codecName.toLowerCase(), codec);
069    if (codecName.endsWith("Codec")) {
070      codecName = codecName.substring(0, codecName.length() - "Codec".length());
071      codecsByName.put(codecName.toLowerCase(), codec);
072    }
073  }
074
075  /**
076   * Print the extension map out as a string.
077   */
078  @Override
079  public String toString() {
080    StringBuilder buf = new StringBuilder();
081    Iterator<Map.Entry<String, CompressionCodec>> itr = 
082      codecs.entrySet().iterator();
083    buf.append("{ ");
084    if (itr.hasNext()) {
085      Map.Entry<String, CompressionCodec> entry = itr.next();
086      buf.append(entry.getKey());
087      buf.append(": ");
088      buf.append(entry.getValue().getClass().getName());
089      while (itr.hasNext()) {
090        entry = itr.next();
091        buf.append(", ");
092        buf.append(entry.getKey());
093        buf.append(": ");
094        buf.append(entry.getValue().getClass().getName());
095      }
096    }
097    buf.append(" }");
098    return buf.toString();
099  }
100
101  /**
102   * Get the list of codecs discovered via a Java ServiceLoader, or
103   * listed in the configuration. Codecs specified in configuration come
104   * later in the returned list, and are considered to override those
105   * from the ServiceLoader.
106   * @param conf the configuration to look in
107   * @return a list of the {@link CompressionCodec} classes
108   */
109  public static List<Class<? extends CompressionCodec>> getCodecClasses(Configuration conf) {
110    List<Class<? extends CompressionCodec>> result
111      = new ArrayList<Class<? extends CompressionCodec>>();
112    // Add codec classes discovered via service loading
113    synchronized (CODEC_PROVIDERS) {
114      // CODEC_PROVIDERS is a lazy collection. Synchronize so it is
115      // thread-safe. See HADOOP-8406.
116      for (CompressionCodec codec : CODEC_PROVIDERS) {
117        result.add(codec.getClass());
118      }
119    }
120    // Add codec classes from configuration
121    String codecsString = conf.get("io.compression.codecs");
122    if (codecsString != null) {
123      StringTokenizer codecSplit = new StringTokenizer(codecsString, ",");
124      while (codecSplit.hasMoreElements()) {
125        String codecSubstring = codecSplit.nextToken().trim();
126        if (codecSubstring.length() != 0) {
127          try {
128            Class<?> cls = conf.getClassByName(codecSubstring);
129            if (!CompressionCodec.class.isAssignableFrom(cls)) {
130              throw new IllegalArgumentException("Class " + codecSubstring +
131                                                 " is not a CompressionCodec");
132            }
133            result.add(cls.asSubclass(CompressionCodec.class));
134          } catch (ClassNotFoundException ex) {
135            throw new IllegalArgumentException("Compression codec " + 
136                                               codecSubstring + " not found.",
137                                               ex);
138          }
139        }
140      }
141    }
142    return result;
143  }
144  
145  /**
146   * Sets a list of codec classes in the configuration. In addition to any
147   * classes specified using this method, {@link CompressionCodec} classes on
148   * the classpath are discovered using a Java ServiceLoader.
149   * @param conf the configuration to modify
150   * @param classes the list of classes to set
151   */
152  public static void setCodecClasses(Configuration conf,
153                                     List<Class> classes) {
154    StringBuilder buf = new StringBuilder();
155    Iterator<Class> itr = classes.iterator();
156    if (itr.hasNext()) {
157      Class cls = itr.next();
158      buf.append(cls.getName());
159      while(itr.hasNext()) {
160        buf.append(',');
161        buf.append(itr.next().getName());
162      }
163    }
164    conf.set("io.compression.codecs", buf.toString());   
165  }
166  
167  /**
168   * Find the codecs specified in the config value io.compression.codecs 
169   * and register them. Defaults to gzip and deflate.
170   */
171  public CompressionCodecFactory(Configuration conf) {
172    codecs = new TreeMap<String, CompressionCodec>();
173    codecsByClassName = new HashMap<String, CompressionCodec>();
174    codecsByName = new HashMap<String, CompressionCodec>();
175    List<Class<? extends CompressionCodec>> codecClasses = getCodecClasses(conf);
176    if (codecClasses == null || codecClasses.isEmpty()) {
177      addCodec(new GzipCodec());
178      addCodec(new DefaultCodec());      
179    } else {
180      for (Class<? extends CompressionCodec> codecClass : codecClasses) {
181        addCodec(ReflectionUtils.newInstance(codecClass, conf));
182      }
183    }
184  }
185  
186  /**
187   * Find the relevant compression codec for the given file based on its
188   * filename suffix.
189   * @param file the filename to check
190   * @return the codec object
191   */
192  public CompressionCodec getCodec(Path file) {
193    CompressionCodec result = null;
194    if (codecs != null) {
195      String filename = file.getName();
196      String reversedFilename = new StringBuilder(filename).reverse().toString();
197      SortedMap<String, CompressionCodec> subMap = 
198        codecs.headMap(reversedFilename);
199      if (!subMap.isEmpty()) {
200        String potentialSuffix = subMap.lastKey();
201        if (reversedFilename.startsWith(potentialSuffix)) {
202          result = codecs.get(potentialSuffix);
203        }
204      }
205    }
206    return result;
207  }
208  
209  /**
210   * Find the relevant compression codec for the codec's canonical class name.
211   * @param classname the canonical class name of the codec
212   * @return the codec object
213   */
214  public CompressionCodec getCodecByClassName(String classname) {
215    if (codecsByClassName == null) {
216      return null;
217    }
218    return codecsByClassName.get(classname);
219  }
220
221    /**
222     * Find the relevant compression codec for the codec's canonical class name
223     * or by codec alias.
224     * <p/>
225     * Codec aliases are case insensitive.
226     * <p/>
227     * The code alias is the short class name (without the package name).
228     * If the short class name ends with 'Codec', then there are two aliases for
229     * the codec, the complete short class name and the short class name without
230     * the 'Codec' ending. For example for the 'GzipCodec' codec class name the
231     * alias are 'gzip' and 'gzipcodec'.
232     *
233     * @param codecName the canonical class name of the codec
234     * @return the codec object
235     */
236    public CompressionCodec getCodecByName(String codecName) {
237      if (codecsByClassName == null) {
238        return null;
239      }
240      CompressionCodec codec = getCodecByClassName(codecName);
241      if (codec == null) {
242        // trying to get the codec by name in case the name was specified instead a class
243        codec = codecsByName.get(codecName.toLowerCase());
244      }
245      return codec;
246    }
247
248    /**
249     * Find the relevant compression codec for the codec's canonical class name
250     * or by codec alias and returns its implemetation class.
251     * <p/>
252     * Codec aliases are case insensitive.
253     * <p/>
254     * The code alias is the short class name (without the package name).
255     * If the short class name ends with 'Codec', then there are two aliases for
256     * the codec, the complete short class name and the short class name without
257     * the 'Codec' ending. For example for the 'GzipCodec' codec class name the
258     * alias are 'gzip' and 'gzipcodec'.
259     *
260     * @param codecName the canonical class name of the codec
261     * @return the codec class
262     */
263    public Class<? extends CompressionCodec> getCodecClassByName(String codecName) {
264      CompressionCodec codec = getCodecByName(codecName);
265      if (codec == null) {
266        return null;
267      }
268      return codec.getClass();
269    }
270
271  /**
272   * Removes a suffix from a filename, if it has it.
273   * @param filename the filename to strip
274   * @param suffix the suffix to remove
275   * @return the shortened filename
276   */
277  public static String removeSuffix(String filename, String suffix) {
278    if (filename.endsWith(suffix)) {
279      return filename.substring(0, filename.length() - suffix.length());
280    }
281    return filename;
282  }
283  
284  /**
285   * A little test program.
286   * @param args
287   */
288  public static void main(String[] args) throws Exception {
289    Configuration conf = new Configuration();
290    CompressionCodecFactory factory = new CompressionCodecFactory(conf);
291    boolean encode = false;
292    for(int i=0; i < args.length; ++i) {
293      if ("-in".equals(args[i])) {
294        encode = true;
295      } else if ("-out".equals(args[i])) {
296        encode = false;
297      } else {
298        CompressionCodec codec = factory.getCodec(new Path(args[i]));
299        if (codec == null) {
300          System.out.println("Codec for " + args[i] + " not found.");
301        } else { 
302          if (encode) {
303            CompressionOutputStream out = null;
304            java.io.InputStream in = null;
305            try {
306              out = codec.createOutputStream(
307                  new java.io.FileOutputStream(args[i]));
308              byte[] buffer = new byte[100];
309              String inFilename = removeSuffix(args[i], 
310                  codec.getDefaultExtension());
311              in = new java.io.FileInputStream(inFilename);
312              int len = in.read(buffer);
313              while (len > 0) {
314                out.write(buffer, 0, len);
315                len = in.read(buffer);
316              }
317            } finally {
318              if(out != null) { out.close(); }
319              if(in  != null) { in.close(); }
320            }
321          } else {
322            CompressionInputStream in = null;
323            try {
324              in = codec.createInputStream(
325                  new java.io.FileInputStream(args[i]));
326              byte[] buffer = new byte[100];
327              int len = in.read(buffer);
328              while (len > 0) {
329                System.out.write(buffer, 0, len);
330                len = in.read(buffer);
331              }
332            } finally {
333              if(in != null) { in.close(); }
334            }
335          }
336        }
337      }
338    }
339  }
340}