1   package eu.fbk.knowledgestore.filestore;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.OutputStream;
6   import java.util.zip.Deflater;
7   import java.util.zip.GZIPInputStream;
8   import java.util.zip.GZIPOutputStream;
9   
10  import javax.annotation.Nullable;
11  
12  import com.google.common.base.Function;
13  import com.google.common.base.MoreObjects;
14  import com.google.common.base.Preconditions;
15  
16  import org.slf4j.Logger;
17  import org.slf4j.LoggerFactory;
18  
19  import eu.fbk.knowledgestore.data.Data;
20  import eu.fbk.knowledgestore.data.Stream;
21  
22  /**
23   * A {@code FileStore} decorator that GZIPs all the compressible files written to it.
24   * <p>
25   * A {@code GzippedFileStore} intercepts reads and writes to an underlying {@code FileStore},
26   * respectively applying GZIP compression and decompression to stored files in case they are
27   * compressible. Compression level and size of buffer used for compression / decompression can be
28   * configured by the user. Whether a file can be compressed is detected starting from its
29   * extension and the matching Internet MIME type using the facilities of {@link Data}. In case
30   * compression is applied, the name of the stored file is changed adding a {@code .gz} suffix.
31   * </p>
32   */
33  public final class GzippedFileStore extends ForwardingFileStore {
34  
35      private static final Logger LOGGER = LoggerFactory.getLogger(GzippedFileStore.class);
36  
37      private static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION;
38  
39      private static final int DEFAULT_BUFFER_SIZE = 512;
40  
41      private static final boolean DEFAULT_FORCE_COMPRESSION = true;
42  
43      private final FileStore delegate;
44  
45      private final int compressionLevel;
46  
47      private final int bufferSize;
48  
49      private final boolean forceCompression;
50  
51      /**
52       * Creates a new instance wrapping the {@code FileStore} supplied and using the default
53       * compression level and buffer size.
54       *
55       * @param delegate
56       *            the wrapped {@code FileStore}
57       * @see #GzippedFileStore(FileStore, Integer, Integer)
58       */
59      public GzippedFileStore(final FileStore delegate) {
60          this(delegate, null, null, null);
61      }
62  
63      /**
64       * Creates a new instance wrapping the {@code FileStore} supplied and using the specified
65       * compression level.
66       *
67       * @param delegate
68       *            the wrapped {@code FileStore}, not null
69       * @param compressionLevel
70       *            the desired compression level for compressible files, on a 0-9 scale (from no
71       *            compression to best compression) or -1 for default compression; if null defaults
72       *            to -1
73       * @param bufferSize
74       *            the size of the buffer used by the GZIP inflaters / deflaters; if null defaults
75       *            to 512
76       * @param forceCompression
77       *            if true, supplied files are always compressed independently of their (detected)
78       *            MIME type; if null defaults to false
79       */
80      public GzippedFileStore(final FileStore delegate, @Nullable final Integer compressionLevel,
81              @Nullable final Integer bufferSize, @Nullable final Boolean forceCompression) {
82  
83          Preconditions.checkNotNull(delegate);
84          Preconditions.checkArgument(compressionLevel == null
85                  || compressionLevel == Deflater.DEFAULT_COMPRESSION //
86                  || compressionLevel >= Deflater.BEST_SPEED
87                  && compressionLevel <= Deflater.BEST_COMPRESSION);
88          Preconditions.checkArgument(bufferSize > 0);
89  
90          this.delegate = Preconditions.checkNotNull(delegate);
91          this.compressionLevel = MoreObjects.firstNonNull(compressionLevel,
92                  DEFAULT_COMPRESSION_LEVEL);
93          this.bufferSize = MoreObjects.firstNonNull(bufferSize, DEFAULT_BUFFER_SIZE);
94          this.forceCompression = MoreObjects.firstNonNull(forceCompression,
95                  DEFAULT_FORCE_COMPRESSION);
96  
97          LOGGER.info("GZippedFileStore configured, compression={}, buffer={}", compressionLevel,
98                  bufferSize);
99      }
100 
101     @Override
102     protected FileStore delegate() {
103         return this.delegate;
104     }
105 
106     @Override
107     public InputStream read(final String filename) throws FileMissingException, IOException {
108         final String internalFilename = toInternalFilename(filename);
109         if (internalFilename.equals(filename)) {
110             return super.read(filename);
111         } else {
112             return new GZIPInputStream(super.read(internalFilename), this.bufferSize);
113         }
114     }
115 
116     @Override
117     public OutputStream write(final String filename) throws FileExistsException, IOException {
118         final String internalFilename = toInternalFilename(filename);
119         if (internalFilename.equals(filename)) {
120             return super.write(filename);
121         } else {
122             return new GZIPOutputStream(super.write(internalFilename), this.bufferSize) {
123 
124                 {
125                     this.def.setLevel(GzippedFileStore.this.compressionLevel);
126                 }
127 
128             };
129         }
130     }
131 
132     @Override
133     public void delete(final String filename) throws FileMissingException, IOException {
134         super.delete(toInternalFilename(filename));
135     }
136 
137     @Override
138     public Stream<String> list() throws IOException {
139         return super.list().transform(new Function<String, String>() {
140 
141             @Override
142             public String apply(final String filename) {
143                 return toExternalFilename(filename);
144             }
145 
146         }, 0);
147     }
148 
149     private String toInternalFilename(final String filename) {
150         return this.forceCompression || Data.isMimeTypeCompressible( //
151                 Data.extensionToMimeType(filename)) ? filename + ".gz" : filename;
152     }
153 
154     private String toExternalFilename(final String filename) {
155         return filename.endsWith(".gz") ? filename.substring(0, filename.length() - 3) : filename;
156     }
157 
158 }