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.view.facelets.impl;
20  
21  import java.io.FileNotFoundException;
22  import java.io.IOException;
23  import java.lang.reflect.Method;
24  import java.net.URL;
25  import java.net.URLConnection;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.logging.Level;
29  import java.util.logging.Logger;
30  import java.util.regex.Pattern;
31  
32  import javax.el.ELException;
33  import javax.faces.FacesException;
34  import javax.faces.FactoryFinder;
35  import javax.faces.application.ViewResource;
36  import javax.faces.context.FacesContext;
37  import javax.faces.view.facelets.Facelet;
38  import javax.faces.view.facelets.FaceletCache;
39  import javax.faces.view.facelets.FaceletCacheFactory;
40  import javax.faces.view.facelets.FaceletContext;
41  import javax.faces.view.facelets.FaceletException;
42  import javax.faces.view.facelets.FaceletHandler;
43  import javax.faces.view.facelets.ResourceResolver;
44  
45  import org.apache.myfaces.shared.resource.ResourceLoaderUtils;
46  import org.apache.myfaces.view.facelets.AbstractFaceletCache;
47  import org.apache.myfaces.view.facelets.FaceletFactory;
48  import org.apache.myfaces.view.facelets.compiler.Compiler;
49  import org.apache.myfaces.view.facelets.util.ParameterCheck;
50  
51  /**
52   * Default FaceletFactory implementation.
53   * 
54   * @author Jacob Hookom
55   * @version $Id$
56   */
57  public final class DefaultFaceletFactory extends FaceletFactory
58  {
59      private static final long INFINITE_DELAY = -1;
60      private static final long NO_CACHE_DELAY = 0;
61      
62      //protected final Log log = LogFactory.getLog(DefaultFaceletFactory.class);
63      protected final Logger log = Logger.getLogger(DefaultFaceletFactory.class.getName());
64  
65      private URL _baseUrl;
66  
67      private Compiler _compiler;
68      
69      //private Map<String, DefaultFacelet> _facelets;
70      
71      //private Map<String, DefaultFacelet> _viewMetadataFacelets;
72      
73      private Map<String, DefaultFacelet> _compositeComponentMetadataFacelets;
74  
75      private long _refreshPeriod;
76  
77      private Map<String, URL> _relativeLocations;
78  
79      private javax.faces.view.facelets.ResourceResolver _resolver;
80      private DefaultResourceResolver _defaultResolver;
81      
82      private FaceletCache<Facelet> _faceletCache;
83      private AbstractFaceletCache<Facelet> _abstractFaceletCache;
84      
85      public DefaultFaceletFactory(Compiler compiler, ResourceResolver resolver) throws IOException
86      {
87          this(compiler, resolver, -1);
88      }
89  
90      public DefaultFaceletFactory(Compiler compiler, ResourceResolver resolver, long refreshPeriod)
91      {
92          ParameterCheck.notNull("compiler", compiler);
93          ParameterCheck.notNull("resolver", resolver);
94  
95          _compiler = compiler;
96  
97          //_facelets = new HashMap<String, DefaultFacelet>();
98          
99          //_viewMetadataFacelets = new HashMap<String, DefaultFacelet>();
100         
101         _compositeComponentMetadataFacelets = new HashMap<String, DefaultFacelet>();
102 
103         _relativeLocations = new HashMap<String, URL>();
104 
105         _resolver = resolver;
106         if (_resolver instanceof DefaultResourceResolver)
107         {
108             _defaultResolver = (DefaultResourceResolver) _resolver;
109         }
110 
111         //_baseUrl = resolver.resolveUrl("/");
112 
113         _refreshPeriod = refreshPeriod < 0 ? INFINITE_DELAY : refreshPeriod * 1000;
114         
115         // facelet cache. Lookup here, because after all this is a "part" of the facelet factory implementation.
116         FaceletCacheFactory cacheFactory
117                 = (FaceletCacheFactory) FactoryFinder.getFactory(FactoryFinder.FACELET_CACHE_FACTORY);
118         _faceletCache = (FaceletCache<Facelet>) cacheFactory.getFaceletCache();
119         
120         FaceletCache.MemberFactory<Facelet> faceletFactory = new FaceletCache.MemberFactory<Facelet>()
121         {
122             public Facelet newInstance(URL url) throws IOException
123             {
124                 return _createFacelet(url);
125             }
126         };
127         FaceletCache.MemberFactory<Facelet> viewMetadataFaceletFactory = new FaceletCache.MemberFactory<Facelet>()
128         {
129             public Facelet newInstance(URL url) throws IOException
130             {
131                 return _createViewMetadataFacelet(url);
132             }
133         };
134         
135         if (_faceletCache instanceof AbstractFaceletCache)
136         {
137             _abstractFaceletCache = (AbstractFaceletCache<Facelet>) _faceletCache;
138             
139             FaceletCache.MemberFactory<Facelet> compositeComponentMetadataFaceletFactory = 
140                 new FaceletCache.MemberFactory<Facelet>()
141             {
142                 public Facelet newInstance(URL url) throws IOException
143                 {
144                     return _createCompositeComponentMetadataFacelet(url);
145                 }
146             };
147 
148             try
149             {
150                 Method setMemberFactoriesMethod = AbstractFaceletCache.class.getDeclaredMethod("setMemberFactories",
151                         new Class[]{FaceletCache.MemberFactory.class, FaceletCache.MemberFactory.class, 
152                                     FaceletCache.MemberFactory.class});
153                 setMemberFactoriesMethod.setAccessible(true);
154                 setMemberFactoriesMethod.invoke(_faceletCache, faceletFactory, viewMetadataFaceletFactory, 
155                     compositeComponentMetadataFaceletFactory);
156             } 
157             catch (Exception e)
158             {
159                 throw new FacesException(
160                     "Cannot call setMemberFactories method, Initialization of FaceletCache failed.",
161                                          e);
162             }   
163         }
164         else
165         {
166             // Note that FaceletCache.setMemberFactories method is protected, and this is the place where call
167             // this method has sense, because DefaultFaceletFactory is the responsible to create Facelet instances.
168             // The only way to do it is using reflection, and it has sense, because in this way it is possible to
169             // setup a java SecurityManager that prevents call this method (because it is protected, and to do that
170             // the code first check for "suppressAccessChecks" permission).
171             try
172             {
173                 Method setMemberFactoriesMethod = FaceletCache.class.getDeclaredMethod("setMemberFactories",
174                         new Class[]{FaceletCache.MemberFactory.class, FaceletCache.MemberFactory.class});
175                 setMemberFactoriesMethod.setAccessible(true);
176                 setMemberFactoriesMethod.invoke(_faceletCache, faceletFactory, viewMetadataFaceletFactory);
177             } 
178             catch (Exception e)
179             {
180                 throw new FacesException(
181                     "Cannot call setMemberFactories method, Initialization of FaceletCache failed.",
182                                          e);
183             }            
184         }
185 
186         if (log.isLoggable(Level.FINE))
187         {
188             log.fine("Using ResourceResolver: " + _resolver);
189             log.fine("Using Refresh Period: " + _refreshPeriod);
190         }
191     }
192 
193     /**
194      * Compiler this factory uses
195      * 
196      * @return final Compiler instance
197      */
198     public Compiler getCompiler()
199     {
200         return _compiler;
201     }
202     
203     private URL getBaseUrl()
204     {
205         if (_baseUrl == null)
206         {
207             _baseUrl = _resolver.resolveUrl("/");
208         }
209         return _baseUrl;
210     }
211 
212     /*
213      * (non-Javadoc)
214      * 
215      * @see org.apache.myfaces.view.facelets.FaceletFactory#getFacelet(java.lang.String)
216      */
217     @Override
218     public Facelet getFacelet(FacesContext facesContext, String uri) 
219         throws IOException, FaceletException, FacesException, ELException
220     {
221         URL url = (URL) _relativeLocations.get(uri);
222         if (url == null)
223         {
224             url = resolveURL(facesContext, getBaseUrl(), uri);
225             if (url != null)
226             {
227                 ViewResource viewResource = (ViewResource) facesContext.getAttributes().get(
228                     FaceletFactory.LAST_RESOURCE_RESOLVED);
229                 if (viewResource != null)
230                 {
231                     // If a view resource has been used to resolve a resource, the cache is in
232                     // the ResourceHandler implementation. No need to cache in _relativeLocations.
233                 }
234                 else
235                 {
236                     Map<String, URL> newLoc = new HashMap<String, URL>(_relativeLocations);
237                     newLoc.put(uri, url);
238                     _relativeLocations = newLoc;
239                 }
240             }
241             else
242             {
243                 throw new IOException("'" + uri + "' not found.");
244             }
245         }
246         return this.getFacelet(url);
247     }
248 
249     /**
250      * Create a Facelet from the passed URL. This method checks if the cached Facelet needs to be refreshed before
251      * returning. If so, uses the passed URL to build a new instance;
252      * 
253      * @param url
254      *            source url
255      * @return Facelet instance
256      * @throws IOException
257      * @throws FaceletException
258      * @throws FacesException
259      * @throws ELException
260      */
261     @Override
262     public Facelet getFacelet(URL url) throws IOException, FaceletException, FacesException, ELException
263     {
264         return _faceletCache.getFacelet(url);
265     }
266     
267     
268     @Override
269     public Facelet getFacelet(FaceletContext ctx, URL url) 
270             throws IOException, FaceletException, FacesException, ELException
271     {
272         if (_abstractFaceletCache != null)
273         {
274             return _abstractFaceletCache.getFacelet(ctx, url);
275         }
276         else
277         {
278             return _faceletCache.getFacelet(url);
279         }
280     }
281 
282     public long getRefreshPeriod()
283     {
284         return _refreshPeriod;
285     }
286 
287     /**
288      * Resolves a path based on the passed URL. If the path starts with '/', then resolve the path against
289      * {@link javax.faces.context.ExternalContext#getResource(java.lang.String)
290      * javax.faces.context.ExternalContext#getResource(java.lang.String)}. Otherwise create a new URL via
291      * {@link URL#URL(java.net.URL, java.lang.String) URL(URL, String)}.
292      * 
293      * @param source
294      *            base to resolve from
295      * @param path
296      *            relative path to the source
297      * @return resolved URL
298      * @throws IOException
299      */
300     public URL resolveURL(FacesContext context, URL source, String path) throws IOException
301     {
302         if (path.startsWith("/"))
303         {
304             context.getAttributes().put(LAST_RESOURCE_RESOLVED, null);
305             URL url = resolveURL(context, path);
306             if (url == null)
307             {
308                 throw new FileNotFoundException(path + " Not Found in ExternalContext as a Resource");
309             }
310             return url;
311         }
312         else
313         {
314             return new URL(source, path);
315         }
316     }
317 
318     /**
319      * Template method for determining if the Facelet needs to be refreshed.
320      * 
321      * @param facelet
322      *            Facelet that could have expired
323      * @return true if it needs to be refreshed
324      */
325     protected boolean needsToBeRefreshed(DefaultFacelet facelet)
326     {
327         // if set to 0, constantly reload-- nocache
328         if (_refreshPeriod == NO_CACHE_DELAY)
329         {
330             return true;
331         }
332 
333         // if set to -1, never reload
334         if (_refreshPeriod == INFINITE_DELAY)
335         {
336             return false;
337         }
338 
339         long target = facelet.getCreateTime() + _refreshPeriod;
340         if (System.currentTimeMillis() > target)
341         {
342             // Should check for file modification
343 
344             URLConnection conn = null;
345             try
346             {
347                 conn = facelet.getSource().openConnection();
348                 long lastModified = ResourceLoaderUtils.getResourceLastModified(conn);
349 
350                 return lastModified == 0 || lastModified > target;
351             }
352             catch (IOException e)
353             {
354                 throw new FaceletException("Error Checking Last Modified for " + facelet.getAlias(), e);
355             }
356             finally
357             {
358                 if (conn != null)
359                 {
360                     try
361                     {
362                         conn.getInputStream().close();
363                     }
364                     catch (Exception e)
365                     {
366                         // Ignored
367                     }
368                 }
369             }
370         }
371 
372         return false;
373     }
374 
375     /**
376      * Uses the internal Compiler reference to build a Facelet given the passed URL.
377      * 
378      * @param url
379      *            source
380      * @return a Facelet instance
381      * @throws IOException
382      * @throws FaceletException
383      * @throws FacesException
384      * @throws ELException
385      */
386     private DefaultFacelet _createFacelet(URL url) throws IOException, FaceletException, FacesException, ELException
387     {
388         if (log.isLoggable(Level.FINE))
389         {
390             log.fine("Creating Facelet for: " + url);
391         }
392 
393         String alias = "/" + _removeFirst(url.getFile(), getBaseUrl().getFile());
394         try
395         {
396             FaceletHandler h = _compiler.compile(url, alias);
397             DefaultFacelet f = new DefaultFacelet(this, _compiler.createExpressionFactory(), url, alias, alias, h);
398             return f;
399         }
400         catch (FileNotFoundException fnfe)
401         {
402             throw new FileNotFoundException("Facelet " + alias + " not found at: " + url.toExternalForm());
403         }
404     }
405     
406     /**
407      * @since 2.0
408      * @param url
409      * @return
410      * @throws IOException
411      * @throws FaceletException
412      * @throws FacesException
413      * @throws ELException
414      */
415     private DefaultFacelet _createViewMetadataFacelet(URL url)
416             throws IOException, FaceletException, FacesException, ELException
417     {
418         if (log.isLoggable(Level.FINE))
419         {
420             log.fine("Creating Facelet used to create View Metadata for: " + url);
421         }
422 
423         // The alias is used later for informative purposes, so we append 
424         // some prefix to identify later where the errors comes from.
425         String faceletId = "/"+ _removeFirst(url.getFile(), getBaseUrl().getFile());
426         String alias = "/viewMetadata" + faceletId;
427         try
428         {
429             FaceletHandler h = _compiler.compileViewMetadata(url, alias);
430             DefaultFacelet f = new DefaultFacelet(this, _compiler.createExpressionFactory(), url, alias, 
431                     faceletId, h);
432             return f;
433         }
434         catch (FileNotFoundException fnfe)
435         {
436             throw new FileNotFoundException("Facelet " + alias + " not found at: " + url.toExternalForm());
437         }
438 
439     }
440     
441     /**
442      * @since 2.0.1
443      * @param url
444      * @return
445      * @throws IOException
446      * @throws FaceletException
447      * @throws FacesException
448      * @throws ELException
449      */
450     private DefaultFacelet _createCompositeComponentMetadataFacelet(URL url)
451             throws IOException, FaceletException, FacesException, ELException
452     {
453         if (log.isLoggable(Level.FINE))
454         {
455             log.fine("Creating Facelet used to create Composite Component Metadata for: " + url);
456         }
457 
458         // The alias is used later for informative purposes, so we append 
459         // some prefix to identify later where the errors comes from.
460         String alias = "/compositeComponentMetadata/" + _removeFirst(url.getFile(), getBaseUrl().getFile());
461         try
462         {
463             FaceletHandler h = _compiler.compileCompositeComponentMetadata(url, alias);
464             DefaultFacelet f = new DefaultFacelet(this, _compiler.createExpressionFactory(), url, alias,
465                     alias, h, true);
466             return f;
467         }
468         catch (FileNotFoundException fnfe)
469         {
470             throw new FileNotFoundException("Facelet " + alias + " not found at: " + url.toExternalForm());
471         }
472     }
473 
474     /**
475      * Works in the same way as getFacelet(String uri), but redirect
476      * to getViewMetadataFacelet(URL url)
477      * @since 2.0
478      */
479     @Override
480     public Facelet getViewMetadataFacelet(FacesContext facesContext, String uri) 
481         throws IOException
482     {
483         URL url = (URL) _relativeLocations.get(uri);
484         if (url == null)
485         {
486             url = resolveURL(facesContext, getBaseUrl(), uri);
487             ViewResource viewResource = (ViewResource) facesContext.getAttributes().get(
488                 FaceletFactory.LAST_RESOURCE_RESOLVED);
489             if (url != null)
490             {
491                 if (viewResource != null)
492                 {
493                     // If a view resource has been used to resolve a resource, the cache is in
494                     // the ResourceHandler implementation. No need to cache in _relativeLocations.
495                 }
496                 else
497                 {
498                     Map<String, URL> newLoc = new HashMap<String, URL>(_relativeLocations);
499                     newLoc.put(uri, url);
500                     _relativeLocations = newLoc;
501                 }
502             }
503             else
504             {
505                 throw new IOException("'" + uri + "' not found.");
506             }
507         }
508         return this.getViewMetadataFacelet(url);
509     }
510 
511     /**
512      * @since 2.0
513      */
514     @Override
515     public Facelet getViewMetadataFacelet(URL url) throws IOException,
516             FaceletException, FacesException, ELException
517     {
518         if (_abstractFaceletCache != null)
519         {
520             return _abstractFaceletCache.getViewMetadataFacelet(url);
521         }
522         else
523         {
524             return _faceletCache.getViewMetadataFacelet(url);
525         }
526     }
527     
528     /**
529      * Works in the same way as getFacelet(String uri), but redirect
530      * to getViewMetadataFacelet(URL url)
531      * @since 2.0.1
532      */
533     @Override
534     public Facelet getCompositeComponentMetadataFacelet(FacesContext facesContext, String uri)
535         throws IOException
536     {
537         URL url = (URL) _relativeLocations.get(uri);
538         if (url == null)
539         {
540             url = resolveURL(facesContext, getBaseUrl(), uri);
541             ViewResource viewResource = (ViewResource) facesContext.getAttributes().get(
542                 FaceletFactory.LAST_RESOURCE_RESOLVED);            
543             if (url != null)
544             {
545                 if (viewResource != null)
546                 {
547                     // If a view resource has been used to resolve a resource, the cache is in
548                     // the ResourceHandler implementation. No need to cache in _relativeLocations.
549                 }
550                 else
551                 {
552                     Map<String, URL> newLoc = new HashMap<String, URL>(_relativeLocations);
553                     newLoc.put(uri, url);
554                     _relativeLocations = newLoc;
555                 }
556             }
557             else
558             {
559                 throw new IOException("'" + uri + "' not found.");
560             }
561         }
562         return this.getCompositeComponentMetadataFacelet(url);
563     }
564 
565     /**
566      * @since 2.0.1
567      */
568     @Override
569     public Facelet getCompositeComponentMetadataFacelet(URL url) throws IOException,
570             FaceletException, FacesException, ELException
571     {
572         if (_abstractFaceletCache != null)
573         {
574             return _abstractFaceletCache.getCompositeComponentMetadataFacelet(url);
575         }
576         else
577         {
578             ParameterCheck.notNull("url", url);
579 
580             String key = url.toString();
581 
582             DefaultFacelet f = _compositeComponentMetadataFacelets.get(key);
583 
584             if (f == null || this.needsToBeRefreshed(f))
585             {
586                 f = this._createCompositeComponentMetadataFacelet(url);
587                 if (_refreshPeriod != NO_CACHE_DELAY)
588                 {
589                     Map<String, DefaultFacelet> newLoc
590                             = new HashMap<String, DefaultFacelet>(_compositeComponentMetadataFacelets);
591                     newLoc.put(key, f);
592                     _compositeComponentMetadataFacelets = newLoc;
593                 }
594             }
595             return f;
596         }
597     }
598     
599     private URL resolveURL(FacesContext context, String path)
600     {
601         if (_defaultResolver != null)
602         {
603             return _defaultResolver.resolveUrl(context, path);
604         }
605         else
606         {
607             return _resolver.resolveUrl(path);
608         }
609     }
610 
611     public Facelet compileComponentFacelet(String taglibURI, String tagName, Map<String,Object> attributes)
612     {
613         FaceletHandler handler = _compiler.compileComponent(taglibURI, tagName, attributes);
614         String alias = "/component/oamf:"+tagName;
615         return new DefaultFacelet(this, _compiler.createExpressionFactory(), getBaseUrl(), alias, alias, handler);
616     }
617     
618     /**
619      * Removes the first appearance of toRemove in string.
620      *
621      * Works just like string.replaceFirst(toRemove, ""), except that toRemove
622      * is not treated as a regex (which could cause problems with filenames).
623      *
624      * @param string
625      * @param toRemove
626      * @return
627      */
628     private String _removeFirst(String string, String toRemove)
629     {
630         // do exactly what String.replaceFirst(toRemove, "") internally does,
631         // except treating the search as literal text and not as regex
632 
633         return Pattern.compile(toRemove, Pattern.LITERAL).matcher(string).replaceFirst("");
634     }
635 
636 }