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.trinidad.resource;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.SequenceInputStream;
24  
25  import java.net.URL;
26  import java.net.URLConnection;
27  import java.net.URLStreamHandler;
28  
29  import java.util.Arrays;
30  import java.util.ArrayList;
31  import java.util.Enumeration;
32  import java.util.NoSuchElementException;
33  
34  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
35  import org.apache.myfaces.trinidad.util.URLUtils;
36  
37  /**
38   * A resource loader implementation which combines multiple resources
39   * into a single stream.  This version leverages the DynamicResourceLoader.
40   *
41   */
42  public class AggregatingResourceLoader extends DynamicResourceLoader
43  {
44    /**
45     * Creates a new AggregatingResourceLoader.
46     *
47     * @param path    the aggregated resource path
48     * @param paths   the target resource paths to aggregate
49     * @param target  the resource loader use to find target resource paths
50     * @param parent  the parent resource loader
51     */
52    public AggregatingResourceLoader(
53      String         path,
54      String[]       paths,
55      ResourceLoader target,
56      ResourceLoader parent)
57    {
58      super(path, parent);
59      
60      if ((paths == null) || (paths.length == 0))
61        throw new IllegalArgumentException("No paths specified");
62  
63      if (target == null)
64        throw new NullPointerException();
65  
66      _paths = paths.clone();
67      _target = target;
68    }
69  
70    /**
71     * Creates a new AggregatingResourceLoader.
72     *
73     * @param path    the aggregated resource path
74     * @param paths   the target resource paths to aggregate
75     * @param target  the resource loader use to find target resource paths
76     */
77    public AggregatingResourceLoader(
78      String         path,
79      String[]       paths,
80      ResourceLoader target)
81    {
82      this(path, paths, target, null);
83    }
84  
85    /**
86     * Sets the separator to use in between streams.  This will typically contain a newline character.
87     * By default the value is <code>null</code> which implies no separator.
88     *
89     * @param separator a string containing the separator characters
90     */
91    public void setSeparator(String separator)
92    {
93      _separator = separator;
94    }
95  
96    /**
97     * Returns a URL which is an aggregate of all the paths.
98     *
99     * @param path the current path
100    * @return a aggregate url
101    * @throws IOException when something bad happens
102    */
103   @Override
104   protected URL getURL(String path) throws IOException
105   {
106     int len = _paths.length;
107     ArrayList<URL> urls = new ArrayList<URL>(len);
108     for(int i = 0; i < len; i++)
109     {
110       URL u = _target.getResource(_paths[i]);
111       if(u != null)
112       {
113         urls.add(u);
114       }
115       else
116       {
117         _LOG.warning("RESOURCE_NOT_FOUND", new Object[]{_paths[i], path});
118       }
119     }
120 
121     if (!urls.isEmpty())
122     {
123       urls.trimToSize();
124       URL[] urlArray = urls.toArray(new URL[urls.size()]);
125 
126       AggregatingURLStreamHandler handler = new AggregatingURLStreamHandler(urlArray, _separator);
127       return new URL("aggregating", null, -1, path, handler);
128     }
129     else
130     {
131       // none of the paths worked, throw a meaninful exception
132       throw new IOException("Could not find any of:" + Arrays.toString(_paths));
133     }
134   }
135   
136   private final String[] _paths;
137   private final ResourceLoader _target;
138   private volatile String  _separator;
139   
140   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(AggregatingResourceLoader.class);
141 
142   /**
143    * This is a Stream Handler which can be used to construct a URL that is an Aggregate of a list of
144    * other urls.
145    *
146    */
147   public class AggregatingURLStreamHandler extends URLStreamHandler
148   {
149     /**
150      * Constructs and AggregatingURLStreamHandler from an array of URLs containing other data.
151      * This constructor assumes a null separator.
152      *
153      * @param urls the urls
154      */
155     public AggregatingURLStreamHandler(URL[] urls)
156     {
157       this(urls, null);
158     }
159 
160     /**
161      * Constructs and AggregatingURLStreamHandler from an array of URLs containing other data.
162      *
163      * @param urls the urls
164      * @param separator a String containing a separator.  This will typically be an newline character
165      *                  or null.
166      */
167     public AggregatingURLStreamHandler(URL[] urls, String separator)
168     {
169       if ((urls == null) || (urls.length == 0))
170       {
171         throw new IllegalArgumentException("No URLS specified");
172       }
173       
174       _urls = urls.clone();
175       _separator = separator;
176     }
177 
178     /**
179      * Opens a connection containing all of the data from the provided urls.  The seperator character,
180      * if one is provided, will seperate the content of each seperate stream.
181      *
182      * @param u the parent URL object
183      * @return a URLConnection
184      * @throws IOException when something bad happens
185      */
186     @Override
187     protected URLConnection openConnection(URL u) throws IOException
188     {
189       int len = _urls.length;
190       URLConnection[] conn = new URLConnection[len];
191       for(int i = 0; i < len; i++)
192       {
193         conn[i] = _urls[i].openConnection();
194       }
195 
196       return new AggregatingURLConnection(u, conn, _separator);
197     }
198 
199     private final URL[]  _urls;
200     private final String _separator;
201 
202   }
203 
204   static private class AggregatingURLConnection extends URLConnection
205   {
206     public AggregatingURLConnection(URL url, URLConnection[] connections, String separator)
207     {
208       super(url);
209       
210       if ((connections == null) || (connections.length == 0))
211         throw new IllegalArgumentException("No connections specified");
212       
213       _connections = connections;
214       _separator = (separator != null) ? separator.getBytes() : null;
215     }
216 
217     @Override
218     public void connect() throws IOException
219     {
220       for (int i=0, len = _connections.length; i < len; i++)
221       {
222         _connections[i].connect();
223       }
224     }
225 
226     @Override
227     public InputStream getInputStream() throws IOException
228     {
229       boolean hasseparator = (_separator!=null);
230       InputStream[] streams;
231       if(hasseparator)
232       {
233         streams = new InputStream[(_connections.length *2)-1];
234       }
235       else
236       {
237         streams = new InputStream[_connections.length];
238       }
239 
240       for (int i=0, len=_connections.length, sublen = len -1, streamCounter = 0; i < len; i++, streamCounter++)
241       {
242         streams[streamCounter] = _connections[i].getInputStream();
243 
244         //Add the separator if needed
245         if(hasseparator && (i < sublen))
246         {
247           streams[++streamCounter] = new SeparatorInputStream(_separator);
248         }
249       }
250       return new SequenceInputStream(new ArrayEnumeration<InputStream>(streams));
251     }
252 
253     @Override
254     public String getContentType()
255     {
256       return _connections[0].getContentType();
257     }
258 
259     @Override
260     public int getContentLength()
261     {
262       int totalContentLength = _contentLength;
263 
264       // Calculate the total content length
265       // If any piece is unknown in length, then the total is unknown also
266       if (totalContentLength == Integer.MIN_VALUE)
267       {
268         totalContentLength = 0;
269 
270         URLConnection[] connects = _connections;
271 
272         // Ensure that separator calculation happens first
273         // to avoid adding extra length in the case when
274         // the total content length is unknown.
275         if (_separator != null)
276         {
277           totalContentLength += ((connects.length - 1)*_separator.length);
278         }
279 
280         for (int i=0, len = _connections.length; i < len; i++)
281         {
282           int contentLength = _connections[i].getContentLength();
283           if (contentLength < 0)
284           {
285             // Unknown content length for one part implies
286             // unknown content length for aggregated whole
287             totalContentLength = -1;
288             break;
289           }
290           totalContentLength += contentLength;
291         }
292 
293         _contentLength = totalContentLength;
294       }
295 
296       return totalContentLength;
297     }
298 
299     @Override
300     public long getLastModified()
301     {
302       long maxLastModified = -1;
303 
304       for (int i=0, len = _connections.length; i < len; i++)
305       {
306         long lastModified;
307         try
308         {
309           lastModified = URLUtils.getLastModified(_connections[i]);
310         }
311         catch (IOException exception)
312         {
313           maxLastModified = -1;
314           break;
315         }
316 
317         if (lastModified < 0)
318         {
319           maxLastModified = lastModified;
320           break;
321         }
322         maxLastModified = Math.max(maxLastModified, lastModified);
323       }
324 
325       return maxLastModified;
326     }
327 
328     @Override
329     public String getHeaderField(
330       String name)
331     {
332       if ("content-length".equals(name))
333       {
334         return String.valueOf(getContentLength());
335       }
336       else if ("content-type".equals(name))
337       {
338         return getContentType();
339       }
340       else if ("last-modified".equals(name))
341       {
342         return String.valueOf(getLastModified());
343       }
344       else
345       {
346         return super.getHeaderField(name);
347       }
348     }
349 
350     private volatile int _contentLength = Integer.MIN_VALUE;
351     private final URLConnection[] _connections;
352     private final byte[] _separator;
353 
354   }
355 
356   static private class ArrayEnumeration<T> implements Enumeration<T>
357   {
358     public ArrayEnumeration(T[] array)
359     {
360       if (array == null)
361         throw new NullPointerException();
362       
363       _array = array;
364       _len = array.length;
365     }
366 
367     public boolean hasNext()
368     {
369       return _pointer < _len;
370     }
371 
372     public T nextElement() throws NoSuchElementException
373     {
374       try
375       {
376         return _array[_pointer++];
377       }
378       catch (IndexOutOfBoundsException e)
379       {
380         throw new NoSuchElementException();
381       }
382     }
383 
384     public boolean hasMoreElements()
385     {
386       return hasNext();
387     }
388 
389     private final T[] _array;
390     private final int _len;
391     private volatile int _pointer = 0;
392   }
393 
394   static private class SeparatorInputStream extends InputStream
395   {
396     public SeparatorInputStream (byte[] separatorBytes)
397     {
398       _separator = separatorBytes;
399       _length = _separator.length;
400     }
401 
402     @Override
403     public int read() throws IOException
404     {
405       if(_index < _length)
406       {
407         return _separator[_index++];
408       }
409 
410       return -1;
411     }
412 
413     @Override
414     public int read(byte[] b, int off, int len) throws IOException
415     {
416       int bytesLeft = available();
417 
418       if(len <= bytesLeft)
419       {
420         System.arraycopy(_separator,_index,b,off,len);
421         _index += len;
422         return len;
423       }
424       else
425       {
426         System.arraycopy(_separator, _index, b,off, bytesLeft);
427         _index += bytesLeft;
428         return bytesLeft;
429       }
430     }
431 
432     @Override
433     public long skip(long n) throws IOException
434     {
435       int bytesLeft = available();
436 
437       if(n < 0)
438       {
439         return 0;
440       }
441       else if(n < bytesLeft || n == bytesLeft)
442       {
443         _index += n;
444         return n;
445       }
446       else
447       {
448         _index += bytesLeft;
449         return bytesLeft;
450       }
451     }
452 
453     @Override
454     public int available() throws IOException
455     {
456       return _length - _index;
457     }
458 
459     private final    byte[] _separator;
460     private final    int    _length;
461     private volatile int    _index = 0;
462   }
463 }