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  
20  package org.apache.myfaces.tobago.internal.context;
21  
22  import org.apache.myfaces.tobago.application.ProjectStage;
23  import org.apache.myfaces.tobago.component.RendererTypes;
24  import org.apache.myfaces.tobago.context.ResourceManager;
25  import org.apache.myfaces.tobago.context.Theme;
26  import org.apache.myfaces.tobago.context.UserAgent;
27  import org.apache.myfaces.tobago.internal.config.TobagoConfigImpl;
28  import org.apache.myfaces.tobago.util.LocaleUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import javax.faces.context.FacesContext;
33  import javax.faces.render.Renderer;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.concurrent.ConcurrentHashMap;
39  
40  public class ResourceManagerImpl implements ResourceManager {
41  
42    private static final Logger LOG = LoggerFactory.getLogger(ResourceManagerImpl.class);
43    private static final String PROPERTY = "property";
44    private static final String JSP = "jsp";
45    private static final String TAG = "tag";
46    private static final String MINIMIZE_SUFFIX = ".min";
47  
48    public static final String[] EXT_NONE = new String[]{""};
49    public static final String[] EXT_JS = new String[]{".js"};
50    public static final String[] EXT_CSS = new String[]{".css"};
51    public static final String[] EXT_GIF = new String[]{".gif"};
52    public static final String[] EXT_PNG = new String[]{".png"};
53    public static final String[] EXT_JPG = new String[]{".jpg"};
54    public static final String[] EXT_ICO = new String[]{".ico"};
55    public static final String[] EXT_IMAGES = new String[]{".png", ".gif", ".jpg"};
56    public static final String[] EXT_JSP= new String[]{".jsp"};
57    public static final String[] EXT_JSPX= new String[]{".jspx"};
58    public static final String[] EXT_XHTML= new String[]{".xhtml"};
59  
60    private boolean production;
61  
62    private final Map<String, String> resourceList
63        = new ConcurrentHashMap<String, String>(100, 0.75f, 1);
64  
65    private final Map<RendererCacheKey, Renderer> rendererCache
66        = new ConcurrentHashMap<RendererCacheKey, Renderer>(100, 0.75f, 1);
67    private final Map<ImageCacheKey, StringValue> imageCache
68        = new ConcurrentHashMap<ImageCacheKey, StringValue>(100, 0.75f, 1);
69    private final Map<JspCacheKey, String> jspCache
70        = new ConcurrentHashMap<JspCacheKey, String>(100, 0.75f, 1);
71    private final Map<MiscCacheKey, String[]> miscCache
72        = new ConcurrentHashMap<MiscCacheKey, String[]>(100, 0.75f, 1);
73    private final Map<PropertyCacheKey, StringValue> propertyCache
74        = new ConcurrentHashMap<PropertyCacheKey, StringValue>(100, 0.75f, 1);
75    private final Map<ThemeConfigCacheKey, MeasureValue> themeCache
76        = new ConcurrentHashMap<ThemeConfigCacheKey, MeasureValue>(100, 0.75f, 1);
77    private final Map<String, String[]> extensionCache
78        = new ConcurrentHashMap<String, String[]>(10, 0.75f, 1);
79  
80    private TobagoConfigImpl tobagoConfig;
81  
82    public ResourceManagerImpl(final TobagoConfigImpl tobagoConfig) {
83      this.tobagoConfig = tobagoConfig;
84      this.production = tobagoConfig.getProjectStage() == ProjectStage.Production;
85  
86      extensionCache.put("", EXT_NONE);
87      extensionCache.put(".js", EXT_JS);
88      extensionCache.put(".css", EXT_CSS);
89      extensionCache.put(".gif", EXT_GIF);
90      extensionCache.put(".png", EXT_PNG);
91      extensionCache.put(".jpg", EXT_JPG);
92      extensionCache.put(".ico", EXT_ICO);
93      extensionCache.put(".jsp", EXT_JSP);
94      extensionCache.put(".jspx", EXT_JSPX);
95      extensionCache.put(".xhtml", EXT_XHTML);
96    }
97  
98    public void add(final String resourceKey) {
99      if (LOG.isDebugEnabled()) {
100       LOG.debug("adding resourceKey = '{}'", resourceKey);
101     }
102     resourceList.put(resourceKey, "");
103   }
104 
105   public void add(final String resourceKey, final String value) {
106     if (LOG.isDebugEnabled()) {
107       LOG.debug("adding resourceKey = '{}' value= '{}'", resourceKey, value);
108     }
109     resourceList.put(resourceKey, value);
110   }
111 
112   @Override
113   public String getProperty(final FacesContext facesContext, final String bundle, final String propertyKey) {
114 
115     if (bundle != null && propertyKey != null) {
116       final ClientPropertiesKey clientKey = ClientPropertiesKey.get(facesContext);
117       final PropertyCacheKey cacheKey = new PropertyCacheKey(clientKey, bundle, propertyKey);
118 
119       StringValue result = propertyCache.get(cacheKey);
120       if (result == null) {
121         final List properties
122             = getPaths(clientKey, "", PROPERTY, bundle, EXT_NONE, false, true, false, propertyKey, true, false);
123         if (properties != null) {
124           result = new StringValue((String) properties.get(0));
125         } else {
126           result = StringValue.NULL;
127         }
128         propertyCache.put(cacheKey, result);
129       }
130       return result.getValue();
131     }
132     return null;
133   }
134 
135   @Override
136   public Renderer getRenderer(final FacesContext facesContext, final String rendererType) {
137     Renderer renderer = null;
138 
139     if (rendererType != null) {
140       final ClientPropertiesKey clientKey = ClientPropertiesKey.get(facesContext);
141       final RendererCacheKey cacheKey = new RendererCacheKey(clientKey, rendererType);
142 
143       renderer = rendererCache.get(cacheKey);
144       if (renderer != null) {
145         return renderer;
146       }
147       String simpleClassName = null;
148       try {
149         simpleClassName = getRendererClassName(rendererType);
150         final List<Class> classes
151             = getPaths(clientKey, "", TAG, simpleClassName, EXT_NONE, false, true, true, null, false, false);
152         if (classes != null && !classes.isEmpty()) {
153           final Class clazz = classes.get(0);
154           renderer = (Renderer) clazz.newInstance();
155           rendererCache.put(cacheKey, renderer);
156         } else {
157           LOG.error("Don't find any RendererClass for " + simpleClassName + ". Please check you configuration.");
158         }
159       } catch (final InstantiationException e) {
160         LOG.error("name = '" + simpleClassName + "' clientProperties = '" + clientKey.toString() + "'", e);
161       } catch (final IllegalAccessException e) {
162         LOG.error("name = '" + simpleClassName + "' clientProperties = '" + clientKey.toString() + "'", e);
163       }
164     }
165     return renderer;
166   }
167 
168   @Override
169   public String[] getScripts(final FacesContext facesContext, final String name) {
170     return getStrings(facesContext, name, null);
171   }
172 
173   @Override
174   public String[] getStyles(final FacesContext facesContext, final String name) {
175     return getStrings(facesContext, name, null);
176   }
177 
178   /**
179    * {@inheritDoc}
180    */
181   @Override
182   @Deprecated
183   public String getImage(
184       final FacesContext facesContext, final String nameWithExtension, final boolean ignoreMissing) {
185     if (nameWithExtension != null) {
186       int dot = nameWithExtension.lastIndexOf('.');
187       if (dot == -1) {
188         dot = nameWithExtension.length();
189       }
190       return
191           getImage(facesContext, nameWithExtension.substring(0, dot), nameWithExtension.substring(dot), ignoreMissing);
192     }
193     return null;
194   }
195 
196   /**
197    * {@inheritDoc}
198    */
199   @Override
200   public String getImage(
201       final FacesContext facesContext, final String name, final String extension, final boolean ignoreMissing) {
202     if (name != null) {
203       final String[] extensions;
204       if (extension == null) {
205         extensions = EXT_IMAGES;
206       } else {
207         extensions = getExtensions(extension);
208       }
209 
210       final ClientPropertiesKey clientKey = ClientPropertiesKey.get(facesContext);
211       final ImageCacheKey cacheKey = new ImageCacheKey(clientKey, name, extension);
212 
213       StringValue result = imageCache.get(cacheKey);
214       if (result == null) {
215         final List paths
216             = getPaths(clientKey, "", null, name, extensions, false, true, true, null, true, ignoreMissing);
217         if (paths != null) {
218           result = new StringValue((String) paths.get(0));
219         } else {
220           result = StringValue.NULL;
221         }
222         imageCache.put(cacheKey, result);
223       }
224       if (LOG.isDebugEnabled()) {
225         if (result.getValue() == null) {
226           LOG.debug("Can't find image for '{}'", name);
227         }
228       }
229 
230       return result.getValue();
231     }
232 
233     return null;
234   }
235 
236   private List getPaths(
237       final ClientPropertiesKey clientKey, final String prefix, final String subDir, final String name,
238       final String[] extensions, final boolean reverseOrder, final boolean single, final boolean returnKey,
239       final String key, final boolean returnStrings, boolean ignoreMissing) {
240     final List matches = new ArrayList();
241     final String contentType = clientKey.getContentType();
242     final Theme theme = clientKey.getTheme();
243     final UserAgent browser = clientKey.getUserAgent();
244     final List<String> locales = LocaleUtils.getLocaleSuffixList(clientKey.getLocale());
245 
246     // check first the local web application directory
247     for (final String localeSuffix : locales) {
248       for (final String extension : extensions) {
249         if (production) {
250           boolean found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
251               name, MINIMIZE_SUFFIX, localeSuffix, extension, key);
252           if (found && (single || !returnStrings)) {
253             return matches;
254           }
255           if (!found) {
256             found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
257                 name, null, localeSuffix, extension, key);
258             if (found && (single || !returnStrings)) {
259               return matches;
260             }
261           }
262         } else {
263           final boolean found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
264               name, null, localeSuffix, extension, key);
265           if (found && (single || !returnStrings)) {
266             return matches;
267           }
268         }
269       }
270     }
271 
272     // after that check the whole resources tree
273     // e.g. 1. application, 2. library or renderkit
274     for (final Theme currentTheme : theme.getFallbackList()) {// theme loop
275       for (final String resourceDirectory : tobagoConfig.getResourceDirs()) {
276         for (final String browserType : browser.getFallbackList()) { // browser loop
277           for (final String localeSuffix : locales) { // locale loop
278             for (final String extension : extensions) { // extensions loop
279               if (production) {
280                 boolean found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
281                     resourceDirectory, contentType, currentTheme, browserType, subDir, name, MINIMIZE_SUFFIX,
282                     localeSuffix, extension, key);
283                 if (found && (single || !returnStrings)) {
284                   return matches;
285                 }
286                 if (!found) {
287                   found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
288                       resourceDirectory, contentType, currentTheme, browserType, subDir, name, null,
289                       localeSuffix, extension, key);
290                   if (found && (single || !returnStrings)) {
291                     return matches;
292                   }
293                 }
294               } else {
295                 final boolean found = checkPath(prefix, reverseOrder, returnKey, returnStrings, matches,
296                     resourceDirectory, contentType, currentTheme, browserType, subDir, name, null,
297                     localeSuffix, extension, key);
298                 if (found && (single || !returnStrings)) {
299                   return matches;
300                 }
301               }
302             }
303           }
304         }
305       }
306     }
307 
308     if (matches.isEmpty()) {
309 
310       // XXX hack for Tobago 2.0.x backward compatibility: renaming of style.css to tobago.css
311       // XXX style.css should be collected, but missing should be ignored
312       if ("style/style".equals(name) && EXT_CSS == extensions) {
313         ignoreMissing = true;
314       }
315 
316       if (!production && !ignoreMissing) {
317         LOG.warn("Path not found, and no fallback (using empty string) "
318             + "resourceDirs='" + tobagoConfig.getResourceDirs()
319             + "' contentType='" + contentType
320             + "' theme='" + theme.getName()
321             + "' browser='" + browser
322             + "' subDir='" + subDir
323             + "' name='" + name
324             + "' extension='" + Arrays.toString(extensions)
325             + "' key='" + key
326             + "'"/*, new Exception()*/);
327       }
328       return null;
329     } else {
330       return matches;
331     }
332   }
333 
334   private boolean checkPath(
335       final String prefix, final boolean reverseOrder, final boolean returnKey, final boolean returnStrings,
336       final List matches, final String name, final String minimizeSuffix, final String localeSuffix,
337       final String extension, final String key) {
338     String path = makePath(name, minimizeSuffix, localeSuffix, extension, key);
339     if (returnStrings && resourceList.containsKey(path)) {
340       final String result =
341           returnKey
342               ? prefix + path
343               : prefix + resourceList.get(path);
344 
345       if (reverseOrder) {
346         matches.add(0, result);
347       } else {
348         matches.add(result);
349       }
350       if (LOG.isTraceEnabled()) {
351         LOG.trace("testing path: {} *", path); // match
352       }
353 
354       return true;
355     } else if (!returnStrings) {
356       try {
357         path = path.substring(1).replace('/', '.');
358         final Class clazz = Class.forName(path);
359         if (LOG.isTraceEnabled()) {
360           LOG.trace("testing path: " + path + " *"); // match
361         }
362         matches.add(clazz);
363         return true;
364       } catch (final ClassNotFoundException e) {
365         // not found
366         if (LOG.isTraceEnabled()) {
367           LOG.trace("testing path: " + path); // no match
368         }
369       }
370     } else {
371       if (LOG.isTraceEnabled()) {
372         LOG.trace("testing path: " + path); // no match
373       }
374     }
375     return false;
376   }
377 
378   private boolean checkPath(
379       final String prefix, final boolean reverseOrder, final boolean returnKey, final boolean returnStrings,
380       final List matches, final String resourceDirectory, final String contentType, final Theme currentTheme,
381       final String browserType, final String subDir, final String name, final String minimizeSuffix,
382       final String localeSuffix, final String extension, final String key) {
383     String path = makePath(resourceDirectory, contentType, currentTheme, browserType, subDir, name, minimizeSuffix,
384         localeSuffix, extension, key, null);
385     if (returnStrings && resourceList.containsKey(path)) {
386       final String result;
387       if (prefix.length() == 0 && returnKey && resourceDirectory.equals(currentTheme.getResourcePath())) {
388         result = makePath(resourceDirectory, contentType, currentTheme, browserType, subDir, name, minimizeSuffix,
389             localeSuffix, extension, key, currentTheme.getVersion());
390       } else {
391         result = returnKey
392             ? prefix + path : prefix + resourceList.get(path);
393       }
394       if (reverseOrder) {
395         matches.add(0, result);
396       } else {
397         matches.add(result);
398       }
399       if (LOG.isTraceEnabled()) {
400         LOG.trace("testing path: {} *", path); // match
401       }
402 
403       return true;
404     } else if (!returnStrings) {
405       try {
406         path = path.substring(1).replace('/', '.');
407         final Class clazz = Class.forName(path);
408         if (LOG.isTraceEnabled()) {
409           LOG.trace("testing path: " + path + " *"); // match
410         }
411         matches.add(clazz);
412         return true;
413       } catch (final ClassNotFoundException e) {
414         // not found
415         if (LOG.isTraceEnabled()) {
416           LOG.trace("testing path: " + path); // no match
417         }
418       }
419     } else {
420       if (LOG.isTraceEnabled()) {
421         LOG.trace("testing path: " + path); // no match
422       }
423     }
424     return false;
425   }
426 
427   private String makePath(
428       final String project, final String language, final Theme theme, final String browser, final String subDir,
429       final String name, final String minimizeSuffix, final String localeSuffix, final String extension,
430       final String key, final String version) {
431     final StringBuilder searchtext = new StringBuilder(64);
432 
433     searchtext.append('/');
434     searchtext.append(project);
435     if (version != null) {
436       searchtext.append('/');
437       searchtext.append(version);
438     }
439     searchtext.append('/');
440     searchtext.append(language);
441     searchtext.append('/');
442     searchtext.append(theme.getName());
443     searchtext.append('/');
444     searchtext.append(browser);
445     if (subDir != null) {
446       searchtext.append('/');
447       searchtext.append(subDir);
448     }
449     searchtext.append('/');
450     searchtext.append(name);
451     if (minimizeSuffix != null) {
452       searchtext.append(minimizeSuffix);
453     }
454     searchtext.append(localeSuffix);
455     searchtext.append(extension);
456     if (key != null) {
457       searchtext.append('/');
458       searchtext.append(key);
459     }
460 
461     return searchtext.toString();
462   }
463 
464   private String makePath(
465       final String name, final String minimizeSuffix, final String localeSuffix, final String extension,
466       final String key) {
467     final StringBuilder searchtext = new StringBuilder(64);
468 
469     searchtext.append('/');
470     searchtext.append(name);
471     if (minimizeSuffix != null) {
472       searchtext.append(minimizeSuffix);
473     }
474     searchtext.append(localeSuffix);
475     searchtext.append(extension);
476     if (key != null) {
477       searchtext.append('/');
478       searchtext.append(key);
479     }
480 
481     return searchtext.toString();
482   }
483 
484   private String getRendererClassName(final String rendererType) {
485     String name;
486     if (LOG.isDebugEnabled()) {
487       LOG.debug("rendererType = '{}'", rendererType);
488     }
489     if ("javax.faces.Text".equals(rendererType)) { // TODO: find a better way
490       name = RendererTypes.Out.name();
491     } else {
492       name = rendererType;
493     }
494     name = name + "Renderer";
495     if (name.startsWith("javax.faces.")) { // FIXME: this is a hotfix from jsf1.0beta to jsf1.0fr
496       LOG.warn("patching renderer from {}", name);
497       name = name.substring("javax.faces.".length());
498       LOG.warn("patching renderer to {}", name);
499     }
500     return name;
501   }
502 
503   private String[] getStrings(final FacesContext facesContext, final String name, final String type) {
504     String[] result = new String[0];
505     if (name != null) {
506       final int dot = name.lastIndexOf('.');
507       final String nameWithoutExtension;
508       final String[] extensions;
509       if (dot == -1) {
510         nameWithoutExtension = name;
511         extensions = EXT_NONE;
512       } else {
513         nameWithoutExtension = name.substring(0, dot);
514         final String extension = name.substring(dot);
515         extensions = getExtensions(extension);
516       }
517 
518       final ClientPropertiesKey key = ClientPropertiesKey.get(facesContext);
519       final MiscCacheKey miscKey = new MiscCacheKey(key, name);
520       final String[] cacheResult = miscCache.get(miscKey);
521       if (cacheResult != null) {
522         return cacheResult;
523       }
524       try {
525         final List matches = getPaths(key, "", type,
526             nameWithoutExtension, extensions, true, false, true, null, true, false);
527         if (matches != null) {
528           result = (String[]) matches.toArray(new String[matches.size()]);
529         }
530         miscCache.put(miscKey, result);
531       } catch (final Exception e) {
532         LOG.error("name = '" + name + "' clientProperties = '" + key.toString() + "'", e);
533       }
534     }
535     return result;
536   }
537 
538   private String[] getExtensions(final String extension) {
539     final String[] extensions;
540     final String[] cached = extensionCache.get(extension);
541     if (cached != null) {
542       extensions = cached;
543     } else {
544       extensions = new String[] {extension};
545       extensionCache.put(extension, extensions);
546       if (LOG.isInfoEnabled()) {
547         LOG.info("Adding extension '{}' to cache.", extension);
548       }
549     }
550     return extensions;
551   }
552 
553   @Override
554   public String toString() {
555     return "ResourceManagerImpl{"
556         + "production=" + production
557         + ", resourceList=" + resourceList.size()
558         + ", rendererCache=" + rendererCache.size()
559         + ", imageCache=" + imageCache.size()
560         + ", jspCache=" + jspCache.size()
561         + ", miscCache=" + miscCache.size()
562         + ", propertyCache=" + propertyCache.size()
563         + ", themeCache=" + themeCache.size()
564         + ", extensionCache=" + extensionCache.size()
565         + '}';
566   }
567 }