add compressors class for discovering compression formats
diff --git a/src/main/java/org/apache/commons/compress2/FormatDiscoverer.java b/src/main/java/org/apache/commons/compress2/FormatDiscoverer.java
new file mode 100644
index 0000000..a838012
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress2/FormatDiscoverer.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import org.apache.commons.compress2.Format;
+
+/**
+ * Loads formats defined as "services" and provides access to them.
+ *
+ * <p>Uses {@link java.util.ServiceLoader} under the covers but iterates over all formats found eagerly inside the
+ * constructor so errors are reported early.</p>
+ */
+public abstract class FormatDiscoverer<F extends Format> implements Iterable<F> {
+    private final ServiceLoader<F> formatLoader;
+    private Map<String, F> formats;
+
+    /**
+     * Loads services using the given class loader.
+     * @throws ServiceConfigurationError if an error occurs reading a service file or instantiating a format
+     */
+    protected FormatDiscoverer(ClassLoader cl, Class<F> clazz) throws ServiceConfigurationError {
+        this(ServiceLoader.load(clazz, cl));
+    }
+
+    private FormatDiscoverer(ServiceLoader<F> loader) {
+        formatLoader = loader;
+        fillMap();
+    }
+
+    /**
+     * Clears the cached formats and rebuilds it.
+     *
+     * @see ServiceLoader#reload
+     */
+    public void reload() {
+        formatLoader.reload();
+        fillMap();
+    }
+
+    /**
+     * Iterator over all known formats.
+     */
+    public Iterator<F> iterator() {
+        return formats.values().iterator();
+    }
+
+    /**
+     * Iterates over all known formats that support writing.
+     */
+    public Iterable<F> getFormatsWithWriteSupport() {
+        return filter(Format::supportsWriting);
+    }
+
+    /**
+     * Gets a format by its name.
+     * @param name the {@link Format#getName name} of the format.
+     * @return the Format instance if one is known by that name
+     */
+    public Optional<F> getFormatByName(String name) {
+        return Optional.ofNullable(formats.get(name));
+    }
+
+    private void fillMap() throws ServiceConfigurationError {
+        Set<F> ts = new TreeSet<F>(Format.AUTO_DETECTION_ORDER);
+        ts.addAll(asList(formatLoader));
+        formats = Collections.unmodifiableMap(ts.stream()
+            .collect(Collectors.toMap(Format::getName, Function.identity())));
+    }
+
+    protected Iterable<F> filter(final Predicate<F> p) {
+        return () -> StreamSupport.stream(Spliterators.spliterator(formats.values(),
+                                                                   Spliterator.NONNULL),
+                                          false)
+            .filter(p)
+            .iterator();
+    }
+
+    private static <T> List<T> asList(Iterable<T> i) {
+        List<T> l = new ArrayList<T>();
+        for (T t : i) {
+            l.add(t);
+        }
+        return l;
+    }
+}
diff --git a/src/main/java/org/apache/commons/compress2/archivers/Archivers.java b/src/main/java/org/apache/commons/compress2/archivers/Archivers.java
index 26f527c..014c2de 100644
--- a/src/main/java/org/apache/commons/compress2/archivers/Archivers.java
+++ b/src/main/java/org/apache/commons/compress2/archivers/Archivers.java
@@ -18,22 +18,9 @@
  */
 package org.apache.commons.compress2.archivers;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.Spliterator;
-import java.util.Spliterators;
-import java.util.List;
-import java.util.Map;
+import java.util.Optional;
 import java.util.ServiceConfigurationError;
-import java.util.ServiceLoader;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
-import org.apache.commons.compress2.Format;
+import org.apache.commons.compress2.FormatDiscoverer;
 
 /**
  * Loads ArchiveFormats defined as "services" from {@code
@@ -42,9 +29,7 @@
  * <p>Uses {@link java.util.ServiceLoader} under the covers but iterates over all formats found eagerly inside the
  * constructor so errors are reported early.</p>
  */
-public class Archivers implements Iterable<ArchiveFormat<? extends ArchiveEntry>> {
-    private final ServiceLoader<ArchiveFormat> formatLoader;
-    private Map<String, ArchiveFormat<? extends ArchiveEntry>> archivers;
+public class Archivers extends FormatDiscoverer<ArchiveFormat> {
 
     /**
      * Loads services using the current thread's context class loader.
@@ -59,90 +44,37 @@
      * @throws ServiceConfigurationError if an error occurs reading a service file or instantiating a format
      */
     public Archivers(ClassLoader cl) throws ServiceConfigurationError {
-        this(ServiceLoader.load(ArchiveFormat.class, cl));
-    }
-
-    private Archivers(ServiceLoader<ArchiveFormat> loader) {
-        formatLoader = loader;
-        fillMap();
-    }
-
-    /**
-     * Clears the cached formats and rebuilds it.
-     *
-     * @see ServiceLoader#reload
-     */
-    public void reload() {
-        formatLoader.reload();
-        fillMap();
-    }
-
-    /**
-     * Iterator over all known formats.
-     */
-    public Iterator<ArchiveFormat<? extends ArchiveEntry>> iterator() {
-        return archivers.values().iterator();
-    }
-
-    /**
-     * Iterates over all known formats that can write archives.
-     */
-    public Iterable<ArchiveFormat<? extends ArchiveEntry>> getFormatsWithWriteSupport() {
-        return filter(ArchiveFormat::supportsWriting);
+        super(cl, ArchiveFormat.class);
     }
 
     /**
      * Iterates over all known formats that can write archives to channels.
      */
-    public Iterable<ArchiveFormat<? extends ArchiveEntry>> getFormatsWithWriteSupportForNonSeekableChannels() {
+    public Iterable<ArchiveFormat> getFormatsWithWriteSupportForNonSeekableChannels() {
         return filter(ArchiveFormat::supportsWritingToNonSeekableChannels);
     }
 
     /**
      * Iterates over all known formats that can read archives from channels.
      */
-    public Iterable<ArchiveFormat<? extends ArchiveEntry>> getFormatsWithReadSupportForNonSeekableChannels() {
+    public Iterable<ArchiveFormat> getFormatsWithReadSupportForNonSeekableChannels() {
         return filter(ArchiveFormat::supportsReadingFromNonSeekableChannels);
     }
 
     /**
      * Iterates over all known formats that provide random access input.
      */
-    public Iterable<ArchiveFormat<? extends ArchiveEntry>> getFormatsWithRandomAccessInput() {
+    public Iterable<ArchiveFormat> getFormatsWithRandomAccessInput() {
         return filter(ArchiveFormat::supportsRandomAccessInput);
     }
 
     /**
      * Gets a format by its name.
      * @param name the {@link ArchiveFormat#getName name} of the format.
-     * @return the ArchiveFormat instance or null if not format is known by that name
+     * @return the Format instance if one is known by that name
      */
-    public ArchiveFormat getArchiveFormatByName(String name) {
-        return archivers.get(name);
+    public Optional<ArchiveFormat> getArchiveFormatByName(String name) {
+        return getFormatByName(name);
     }
 
-    private void fillMap() throws ServiceConfigurationError {
-        Set<ArchiveFormat<? extends ArchiveEntry>> ts =
-            new TreeSet<ArchiveFormat<? extends ArchiveEntry>>(Format.AUTO_DETECTION_ORDER);
-        ts.addAll(asList(formatLoader));
-        archivers = Collections.unmodifiableMap(ts.stream()
-            .collect(Collectors.toMap(ArchiveFormat::getName, Function.identity())));
-    }
-
-    private Iterable<ArchiveFormat<? extends ArchiveEntry>>
-        filter(final Predicate<ArchiveFormat<? extends ArchiveEntry>> p) {
-        return () -> StreamSupport.stream(Spliterators.spliterator(archivers.values(),
-                                                                   Spliterator.NONNULL),
-                                          false)
-            .filter(p)
-            .iterator();
-    }
-
-    private static <T> List<T> asList(Iterable<T> i) {
-        List<T> l = new ArrayList<T>();
-        for (T t : i) {
-            l.add(t);
-        }
-        return l;
-    }
 }
diff --git a/src/main/java/org/apache/commons/compress2/compressors/Compressors.java b/src/main/java/org/apache/commons/compress2/compressors/Compressors.java
new file mode 100644
index 0000000..b3a0672
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress2/compressors/Compressors.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress2.compressors;
+
+import java.util.Optional;
+import java.util.ServiceConfigurationError;
+import org.apache.commons.compress2.FormatDiscoverer;
+
+/**
+ * Loads CompressionFormats defined as "services" from {@code
+ * META-INF/services/org.apache.commons.compress2.compressors.CompressionFormat} and provides access to them.
+ *
+ * <p>Uses {@link java.util.ServiceLoader} under the covers but iterates over all formats found eagerly inside the
+ * constructor so errors are reported early.</p>
+ */
+public class Compressors extends FormatDiscoverer<CompressionFormat> {
+
+    /**
+     * Loads services using the current thread's context class loader.
+     * @throws ServiceConfigurationError if an error occurs reading a service file or instantiating a format
+     */
+    public Compressors() throws ServiceConfigurationError {
+        this(Thread.currentThread().getContextClassLoader());
+    }
+
+    /**
+     * Loads services using the given class loader.
+     * @throws ServiceConfigurationError if an error occurs reading a service file or instantiating a format
+     */
+    public Compressors(ClassLoader cl) throws ServiceConfigurationError {
+        super(cl, CompressionFormat.class);
+    }
+
+    /**
+     * Gets a format by its name.
+     * @param name the {@link CompressionFormat#getName name} of the format.
+     * @return the Format instance if one is known by that name
+     */
+    public Optional<CompressionFormat> getCompressionFormatByName(String name) {
+        return getFormatByName(name);
+    }
+}
diff --git a/src/main/resources/META-INF/services/org.apache.commons.compress2.compressors.CompressionFormat b/src/main/resources/META-INF/services/org.apache.commons.compress2.compressors.CompressionFormat
new file mode 100644
index 0000000..421c8fe
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.apache.commons.compress2.compressors.CompressionFormat
@@ -0,0 +1 @@
+org.apache.commons.compress2.formats.deflate.DeflateCompressionFormat
diff --git a/src/test/java/org/apache/commons/compress2/archivers/ArchiversTest.java b/src/test/java/org/apache/commons/compress2/archivers/ArchiversTest.java
index 0846244..397c10e 100644
--- a/src/test/java/org/apache/commons/compress2/archivers/ArchiversTest.java
+++ b/src/test/java/org/apache/commons/compress2/archivers/ArchiversTest.java
@@ -28,8 +28,9 @@
 
     @Test
     public void shouldFindArArchiveFormatByName() {
-        ArchiveFormat<? extends ArchiveEntry> arFormat =
-            new Archivers().getArchiveFormatByName(ArArchiveFormat.AR_FORMAT_NAME);
+        ArchiveFormat arFormat =
+            new Archivers().getArchiveFormatByName(ArArchiveFormat.AR_FORMAT_NAME)
+            .orElse(null);
         Assert.assertNotNull(arFormat);
         Assert.assertEquals(ArArchiveFormat.class, arFormat.getClass());
     }
@@ -59,8 +60,8 @@
         shouldNotFind(ArArchiveFormat.class, new Archivers().getFormatsWithRandomAccessInput());
     }
 
-    private void shouldFind(Class<?> archiveFormat, Iterable<ArchiveFormat<? extends ArchiveEntry>> i) {
-        for (ArchiveFormat<? extends ArchiveEntry> a : i) {
+    private void shouldFind(Class<?> archiveFormat, Iterable<ArchiveFormat> i) {
+        for (ArchiveFormat a : i) {
             if (archiveFormat.equals(a.getClass())) {
                 return;
             }
@@ -68,8 +69,8 @@
         Assert.fail("Expected to find " + archiveFormat);
     }
 
-    private void shouldNotFind(Class<?> archiveFormat, Iterable<ArchiveFormat<? extends ArchiveEntry>> i) {
-        for (ArchiveFormat<? extends ArchiveEntry> a : i) {
+    private void shouldNotFind(Class<?> archiveFormat, Iterable<ArchiveFormat> i) {
+        for (ArchiveFormat a : i) {
             if (archiveFormat.equals(a.getClass())) {
                 Assert.fail("Didn't expect to find " + archiveFormat);
             }
diff --git a/src/test/java/org/apache/commons/compress2/compressors/CompressorsTest.java b/src/test/java/org/apache/commons/compress2/compressors/CompressorsTest.java
new file mode 100644
index 0000000..723abb4
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress2/compressors/CompressorsTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress2.compressors;
+
+
+import org.apache.commons.compress2.formats.deflate.DeflateCompressionFormat;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CompressorsTest {
+
+    @Test
+    public void shouldFindDeflateCompressionFormatByName() {
+        CompressionFormat deflateFormat =
+            new Compressors().getCompressionFormatByName(DeflateCompressionFormat.DEFLATE_FORMAT_NAME)
+            .orElse(null);
+        Assert.assertNotNull(deflateFormat);
+        Assert.assertEquals(DeflateCompressionFormat.class, deflateFormat.getClass());
+    }
+
+    @Test
+    public void shouldFindDeflateCompressionFormatWhenIterating() {
+        shouldFind(DeflateCompressionFormat.class, new Compressors());
+    }
+
+    @Test
+    public void shouldFindDeflateCompressionFormatAsWritableFormat() {
+        shouldFind(DeflateCompressionFormat.class, new Compressors().getFormatsWithWriteSupport());
+    }
+
+    private void shouldFind(Class<?> compressionFormat, Iterable<CompressionFormat> i) {
+        for (CompressionFormat a : i) {
+            if (compressionFormat.equals(a.getClass())) {
+                return;
+            }
+        }
+        Assert.fail("Expected to find " + compressionFormat);
+    }
+
+    private void shouldNotFind(Class<?> compressionFormat, Iterable<CompressionFormat> i) {
+        for (CompressionFormat a : i) {
+            if (compressionFormat.equals(a.getClass())) {
+                Assert.fail("Didn't expect to find " + compressionFormat);
+            }
+        }
+    }
+}