View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.commons.resourcehandler;
20  
21  import java.io.BufferedInputStream;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.net.MalformedURLException;
30  import java.net.URL;
31  import java.util.Map;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.zip.GZIPOutputStream;
34  
35  import javax.faces.FacesException;
36  import javax.faces.context.FacesContext;
37  
38  import org.apache.myfaces.commons.resourcehandler.resource.ResourceLoader;
39  import org.apache.myfaces.commons.resourcehandler.resource.ResourceLoaderWrapper;
40  import org.apache.myfaces.commons.resourcehandler.resource.ResourceMeta;
41  import org.apache.myfaces.commons.resourcehandler.resource.ValueExpressionFilterInputStream;
42  
43  /**
44   * 
45   * @author Leonardo Uribe
46   * @author Jakob Korherr
47   *
48   */
49  public class GZIPResourceLoader extends ResourceLoaderWrapper
50  {
51      
52      public final static String COMPRESSED_FILES_MAP = "oam.commons.COMPRESSED_FILES_MAP";
53      
54      /**
55       * Subdir of the ServletContext tmp dir to store compressed resources.
56       */
57      private static final String COMPRESSION_BASE_DIR = "oam-resourcehandler-cache/";
58  
59      /**
60       * Suffix for compressed files.
61       */
62      private static final String COMPRESSED_FILE_SUFFIX = ".gzip";
63      
64      /**
65       * Size of the byte array buffer.
66       */
67      private static final int BUFFER_SIZE = 2048;
68      
69      private ResourceLoader delegate;
70      
71      private volatile File _tempDir;
72      
73      private final ExtendedDefaultResourceHandlerSupport _extendedDefaultResourceHandlerSupport;
74      
75      public GZIPResourceLoader(ResourceLoader delegate, ExtendedDefaultResourceHandlerSupport extendedDefaultResourceHandlerSupport)
76      {
77          this.delegate = delegate;
78          _extendedDefaultResourceHandlerSupport = extendedDefaultResourceHandlerSupport;
79          initialize();
80      }
81      
82      protected void initialize()
83      {
84          //Get startup FacesContext
85          FacesContext facesContext = FacesContext.getCurrentInstance();
86      
87          //1. Create temporal directory for compressed resources
88          Map<String, Object> applicationMap = facesContext.getExternalContext().getApplicationMap();
89          File tempdir = (File) applicationMap.get("javax.servlet.context.tempdir");
90          File imagesDir = new File(tempdir, COMPRESSION_BASE_DIR);
91          if (!imagesDir.exists())
92          {
93              imagesDir.mkdirs();
94          }
95          else
96          {
97              //Clear the cache
98              deleteDir(imagesDir);
99              imagesDir.mkdirs();
100         }
101         _tempDir = imagesDir;
102 
103         //2. Create map for register compressed resources
104         Map<String, FileProducer> compressedFilesMap = new ConcurrentHashMap<String, FileProducer>();
105         facesContext.getExternalContext().getApplicationMap().put(COMPRESSED_FILES_MAP, compressedFilesMap);
106     }
107 
108     private static boolean deleteDir(File dir)
109     {
110         if (dir.isDirectory())
111         {
112             String[] children = dir.list();
113             for (int i = 0; i < children.length; i++)
114             {
115                 boolean success = deleteDir(new File(dir, children[i]));
116                 if (!success)
117                 {
118                     return false;
119                 }
120             }
121         }
122         return dir.delete();
123     }
124     
125     @Override
126     public URL getResourceURL(ResourceMeta resourceMeta)
127     {
128         FacesContext facesContext = FacesContext.getCurrentInstance();
129 
130         if (!_extendedDefaultResourceHandlerSupport.isCompressable(resourceMeta) || !_extendedDefaultResourceHandlerSupport.userAgentSupportsCompression(facesContext))
131         {
132             return super.getResourceURL(resourceMeta);
133         }
134         
135         if (resourceExists(resourceMeta))
136         {
137             File file = createOrGetCompressedFile(facesContext, resourceMeta);
138             
139             try
140             {
141                 return file.toURL();
142             }
143             catch (MalformedURLException e)
144             {
145                 throw new FacesException(e);
146             }
147         }
148         else
149         {
150             return null;
151         }
152     }    
153     
154     @Override
155     public InputStream getResourceInputStream(ResourceMeta resourceMeta)
156     {
157         FacesContext facesContext = FacesContext.getCurrentInstance();
158 
159         if (!_extendedDefaultResourceHandlerSupport.isCompressable(resourceMeta) || !_extendedDefaultResourceHandlerSupport.userAgentSupportsCompression(facesContext))
160         {
161             return super.getResourceInputStream(resourceMeta);
162         }
163             
164         if (resourceExists(resourceMeta))
165         {
166             File file = createOrGetCompressedFile(facesContext, resourceMeta);
167             
168             try
169             {
170                 return new BufferedInputStream(new FileInputStream(file));
171             }
172             catch (FileNotFoundException e)
173             {
174                 throw new FacesException(e);
175             }
176         }
177         else
178         {
179             return null;
180         }
181     }
182     
183     @Override
184     public boolean resourceExists(ResourceMeta resourceMeta)
185     {
186         return super.resourceExists(resourceMeta);
187     }
188 
189     @SuppressWarnings("unchecked")
190     private File createOrGetCompressedFile(FacesContext facesContext, ResourceMeta resourceMeta)
191     {
192         String identifier = resourceMeta.getResourceIdentifier();
193         File file = getCompressedFile(resourceMeta);
194         if (!file.exists())
195         {
196             Map<String, FileProducer> map = (Map<String, FileProducer>) 
197                 facesContext.getExternalContext().getApplicationMap().get(COMPRESSED_FILES_MAP);
198 
199             FileProducer creator = map.get(identifier);
200             
201             if (creator == null)
202             {
203                 synchronized(this)
204                 {
205                     creator = map.get(identifier);
206                     
207                     if (creator == null)
208                     {
209                         creator = new FileProducer();
210                         map.put(identifier, creator);
211                     }
212                 }
213             }
214             
215             if (!creator.isCreated())
216             {
217                 creator.createFile(facesContext, resourceMeta, file, this);
218             }
219         }
220         return file;
221     }    
222     
223     private File getCompressedFile(ResourceMeta resourceMeta)
224     {
225         return new File(_tempDir, resourceMeta.getResourceIdentifier() + COMPRESSED_FILE_SUFFIX);
226     }
227 
228     private boolean couldResourceContainValueExpressions(ResourceMeta resourceMeta)
229     {
230         return resourceMeta.couldResourceContainValueExpressions() || resourceMeta.getResourceName().endsWith(".css");
231     }
232     
233     
234     /**
235      * Uses GZIPOutputStream to compress this resource.
236      * It will be stored where getCompressedFile() points to.
237      *
238      * Note that the resource really must be compressable (isCompressable() must return true).
239      *
240      * @return
241      */
242     protected void createCompressedFileVersion(FacesContext facesContext, ResourceMeta resourceMeta, File target)
243     {
244         //File target = getCompressedFile(resourceMeta);
245         target.mkdirs();  // ensure necessary directories exist
246         target.delete();  // remove any existing file
247 
248         InputStream inputStream = null;
249         FileOutputStream fileOutputStream;
250         GZIPOutputStream gzipOutputStream = null;
251         try
252         {
253             if (couldResourceContainValueExpressions(resourceMeta))
254             {
255                 inputStream = new ValueExpressionFilterInputStream(
256                         getWrapped().getResourceInputStream(resourceMeta),
257                         resourceMeta.getLibraryName(), 
258                         resourceMeta.getResourceName());
259             }
260             else
261             {
262                 inputStream = getWrapped().getResourceInputStream(resourceMeta);
263             }
264             fileOutputStream = new FileOutputStream(target);
265             gzipOutputStream = new GZIPOutputStream(fileOutputStream);
266             byte[] buffer = new byte[BUFFER_SIZE];
267 
268             pipeBytes(inputStream, gzipOutputStream, buffer);
269         }
270         catch (FileNotFoundException e)
271         {
272             throw new FacesException("Unexpected exception while create file:", e);
273         }
274         catch (IOException e)
275         {
276             throw new FacesException("Unexpected exception while create file:", e);
277         }
278         finally
279         {
280             if (inputStream != null)
281             {
282                 try
283                 {
284                     inputStream.close();
285                 }
286                 catch (IOException e)
287                 {
288                     // Ignore
289                 }
290             }
291             if (gzipOutputStream != null)
292             {
293                 // also closes fileOutputStream   
294                 try
295                 {
296                     gzipOutputStream.close();
297                 }
298                 catch (IOException e)
299                 {
300                     // Ignore
301                 }
302             }
303         }
304     }
305     
306     /**
307      * Reads the specified input stream into the provided byte array storage and
308      * writes it to the output stream.
309      */
310     private static void pipeBytes(InputStream in, OutputStream out, byte[] buffer) throws IOException
311     {
312         int length;
313 
314         while ((length = (in.read(buffer))) >= 0)
315         {
316             out.write(buffer, 0, length);
317         }
318     }
319     
320     public static class FileProducer {
321         
322         public volatile boolean created = false;
323         
324         public FileProducer()
325         {
326             super();
327         }
328 
329         public boolean isCreated()
330         {
331             return created;
332         }
333 
334         public synchronized void createFile(FacesContext facesContext, ResourceMeta resourceMeta, File file, GZIPResourceLoader loader)
335         {
336             if (!created)
337             {
338                 loader.createCompressedFileVersion(facesContext, resourceMeta, file);
339                 created = true;
340             }
341         }
342     }
343     
344     public ResourceLoader getWrapped()
345     {
346         return delegate;
347     }
348 }