001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package org.apache.myfaces.tobago.context;
021    
022    import org.apache.commons.logging.Log;
023    import org.apache.commons.logging.LogFactory;
024    import org.apache.myfaces.tobago.config.TobagoConfig;
025    import org.apache.myfaces.tobago.renderkit.RendererBase;
026    
027    import javax.faces.component.UIViewRoot;
028    import javax.faces.render.Renderer;
029    import java.util.ArrayList;
030    import java.util.HashMap;
031    import java.util.List;
032    import java.util.Locale;
033    import java.util.Map;
034    import java.util.StringTokenizer;
035    import java.util.concurrent.ConcurrentHashMap;
036    
037    import static org.apache.myfaces.tobago.TobagoConstants.RENDERER_TYPE_OUT;
038    
039    public class ResourceManagerImpl implements ResourceManager {
040    
041      private static final Log LOG = LogFactory.getLog(ResourceManagerImpl.class);
042      private static final String PROPERTY = "property";
043      private static final String JSP = "jsp";
044      private static final String TAG = "tag";
045      private static final Renderer NULL_CACHE_RENDERER = new RendererBase();
046    
047      private final HashMap<String, String> resourceList;
048    
049      private final Map<RendererCacheKey, Renderer> rendererCache =
050          new ConcurrentHashMap<RendererCacheKey, Renderer>(100, 0.75f, 1);
051      private final Map<ImageCacheKey, String> imageCache = new ConcurrentHashMap<ImageCacheKey, String>(100, 0.75f, 1);
052      private final Map<JspCacheKey, String> jspCache = new ConcurrentHashMap<JspCacheKey, String>(100, 0.75f, 1);
053      private final Map<MiscCacheKey, String[]> miscCache = new ConcurrentHashMap<MiscCacheKey, String[]>(100, 0.75f, 1);
054      private final Map<PropertyCacheKey, CachedString> propertyCache =
055          new ConcurrentHashMap<PropertyCacheKey, CachedString>(100, 0.75f, 1);
056    
057      private TobagoConfig tobagoConfig;
058    
059      public ResourceManagerImpl(TobagoConfig tobagoConfig) {
060        resourceList = new HashMap<String, String>();
061        this.tobagoConfig = tobagoConfig;
062      }
063    
064      public void add(String resourceKey) {
065        if (LOG.isDebugEnabled()) {
066          LOG.debug("adding resourceKey = '" + resourceKey + "'");
067        }
068        resourceList.put(resourceKey, "");
069      }
070    
071      public void add(String resourceKey, String value) {
072        if (LOG.isDebugEnabled()) {
073          LOG.debug(
074              "adding resourceKey = '" + resourceKey + "' value='" + value + "'");
075        }
076        resourceList.put(resourceKey, value);
077      }
078    
079    
080      public String getImage(UIViewRoot viewRoot, String name) {
081        return getImage(viewRoot, name, false);
082      }
083    
084      public String getImage(UIViewRoot viewRoot, String name, boolean ignoreMissing) {
085        String result = null;
086        if (name != null) {
087          int dot = name.lastIndexOf('.');
088          if (dot == -1) {
089            dot = name.length();
090          }
091          CacheKey key = getCacheKey(viewRoot);
092    
093          ImageCacheKey imageKey = new ImageCacheKey(key, name);
094    
095          result = imageCache.get(imageKey);
096          if (result == null) {
097            try {
098              List paths = getPaths(key.getClientPropertyId(), key.getLocale(), "", null, name.substring(0, dot),
099                  name.substring(dot), false, true, true, null, true, ignoreMissing);
100              if (paths != null) {
101                result = (String) paths.get(0);
102              } else {
103                result = "";
104              }
105              synchronized (imageCache) {
106                imageCache.put(imageKey, result);
107              }
108            } catch (Exception e) {
109              LOG.error("name = '" + name + "' clientProperties = '" + key.getClientPropertyId() + "'", e);
110            }
111          }
112        }
113    
114        if (result == null || result.length() == 0) {
115          if (LOG.isDebugEnabled()) {
116            LOG.debug("Can't find image for \"" + name + "\"");
117          }
118          return null;
119        }
120        return result;
121      }
122    
123      private CacheKey getCacheKey(UIViewRoot viewRoot) {
124        CacheKey key;
125        if (viewRoot instanceof org.apache.myfaces.tobago.component.UIViewRoot) {
126          key = ((org.apache.myfaces.tobago.component.UIViewRoot) viewRoot).getRendererCacheKey();
127        } else {
128          String clientPropertyId = ClientProperties.getInstance(viewRoot).getId();
129          Locale locale = viewRoot.getLocale();
130          key = new CacheKey(clientPropertyId, locale);
131        }
132        return key;
133      }
134    
135      public String getJsp(UIViewRoot viewRoot, String name) {
136        String result = null;
137        if (name != null) {
138          CacheKey key = getCacheKey(viewRoot);
139    
140          JspCacheKey jspKey = new JspCacheKey(key, name);
141    
142          result = jspCache.get(jspKey);
143          if (result == null) {
144            try {
145              result = (String) getPaths(key.getClientPropertyId(), key.getLocale(), "",
146                  JSP, name, "", false, true, true, null, true, false).get(0);
147              synchronized (jspCache) {
148                jspCache.put(jspKey, result);
149              }
150            } catch (Exception e) {
151              LOG.error("name = '" + name + "' clientProperties = '" + key.getClientPropertyId() + "'", e);
152            }
153          }
154          if (result != null && result.length() == 0) {
155            return null;
156          }
157        }
158        return result;
159      }
160    
161      public String getProperty(
162          UIViewRoot viewRoot, String bundle, String propertyKey) {
163        if (bundle != null && propertyKey != null) {
164          CacheKey key = getCacheKey(viewRoot);
165    
166          PropertyCacheKey propertyCacheKey = new PropertyCacheKey(key, bundle, propertyKey);
167          CachedString result = propertyCache.get(propertyCacheKey);
168          if (result == null) {
169            List properties = getPaths(key.getClientPropertyId(), key.getLocale(), "", PROPERTY, bundle,
170                "", false, true, false, propertyKey, true, false);
171            if (properties != null) {
172              result = new CachedString((String) properties.get(0));
173            } else {
174              result = new CachedString(null);
175            }
176            synchronized (propertyCache) {
177              propertyCache.put(propertyCacheKey, result);
178            }
179          }
180          return result.getValue();
181        }
182        return null;
183      }
184    
185      private List getPaths(
186          String clientProperties, Locale locale, String prefix, String subDir, String name, String suffix,
187          boolean reverseOrder, boolean single, boolean returnKey, String key, boolean returnStrings,
188          boolean ignoreMissing) {
189        List matches = new ArrayList();
190    
191        StringTokenizer tokenizer = new StringTokenizer(clientProperties, "/");
192        String contentType = tokenizer.nextToken();
193        Theme theme = tobagoConfig.getTheme(tokenizer.nextToken());
194        UserAgent browser = UserAgent.getInstanceForId(tokenizer.nextToken());
195        List<String> locales = ClientProperties.getLocaleList(locale, false);
196    
197        String path;
198    
199        if (tobagoConfig.isFixResourceOrder()) {
200          if (getLocalPaths(prefix, name, suffix, reverseOrder, single, returnKey, key, returnStrings, matches, locales)) {
201            return matches;
202          }
203        }
204    
205        // e.g. 1. application, 2. library or renderkit
206        for (Theme themeName : theme.getFallbackList()) { // theme loop
207          for (String resourceDirectory : tobagoConfig.getResourceDirs()) {
208            for (String browserType : browser.getFallbackList()) { // browser loop
209              for (String localeSuffix : locales) { // locale loop
210                path = makePath(
211                    resourceDirectory,
212                    contentType,
213                    themeName,
214                    browserType,
215                    subDir,
216                    name,
217                    localeSuffix,
218                    suffix,
219                    key);
220                if (checkPath(prefix, reverseOrder, single, returnKey, returnStrings, matches, path)) {
221                  return matches;
222                }
223              }
224            }
225          }
226        }
227    
228        if (!tobagoConfig.isFixResourceOrder()) {
229          if (getLocalPaths(prefix, name, suffix, reverseOrder, single, returnKey, key, returnStrings, matches, locales)) {
230            return matches;
231          }
232        }
233    
234        if (matches.isEmpty()) {
235          if (!ignoreMissing) {
236            LOG.error("Path not found, and no fallback. Using empty string.\n"
237                + "resourceDirs = '" + tobagoConfig.getResourceDirs()
238                + "' contentType = '" + contentType
239                + "' theme = '" + theme
240                + "' browser = '" + browser
241                + "' subDir = '" + subDir
242                + "' name = '" + name
243                + "' suffix = '" + suffix
244                + "' key = '" + key
245                + "'");
246            if (LOG.isDebugEnabled()) {
247              LOG.debug("Show stacktrace", new Exception());
248            }
249          }
250          return null;
251        } else {
252          return matches;
253        }
254      }
255    
256      /**
257       * @return indicates, if the search should be terminated.
258       */
259      private boolean getLocalPaths(
260          String prefix, String name, String suffix, boolean reverseOrder, boolean single, boolean returnKey, String key,
261          boolean returnStrings, List matches, List<String> locales) {
262        String path;
263        for (String localeSuffix : locales) { // locale loop
264          path = makePath(name, localeSuffix, suffix, key);
265          if (checkPath(prefix, reverseOrder, single, returnKey, returnStrings, matches, path)) {
266            return true;
267          }
268        }
269        return false;
270      }
271    
272      /**
273       * @return indicates, if the search should be terminated.
274       */
275      private boolean checkPath(
276          String prefix, boolean reverseOrder, boolean single, boolean returnKey, boolean returnStrings,
277          List matches, String path) {
278        if (returnStrings && resourceList.containsKey(path)) {
279          String result =
280              returnKey
281                  ? prefix + path
282                  : prefix + resourceList.get(path);
283    
284          if (reverseOrder) {
285            matches.add(0, result);
286          } else {
287            matches.add(result);
288          }
289          if (LOG.isDebugEnabled()) {
290            LOG.debug("testing path: " + path + " *"); // match
291          }
292    
293          if (single) {
294            return true;
295          }
296        } else if (!returnStrings) {
297          try {
298            path = path.substring(1).replace('/', '.');
299            Class clazz = Class.forName(path);
300            if (LOG.isDebugEnabled()) {
301              LOG.debug("testing path: " + path + " *"); // match
302            }
303            matches.add(clazz);
304            return true;
305          } catch (ClassNotFoundException e) {
306            // not found
307            if (LOG.isDebugEnabled()) {
308              LOG.debug("testing path: " + path); // no match
309            }
310          }
311        } else {
312          if (LOG.isDebugEnabled()) {
313            LOG.debug("testing path: " + path); // no match
314          }
315        }
316        return false;
317      }
318    
319      private String makePath(
320          String project, String language, Theme theme, String browser, String subDir, String name, String localeSuffix,
321          String extension, String key) {
322        StringBuilder searchtext = new StringBuilder(64);
323    
324        searchtext.append('/');
325        searchtext.append(project);
326        searchtext.append('/');
327        searchtext.append(language);
328        searchtext.append('/');
329        searchtext.append(theme.getName());
330        searchtext.append('/');
331        searchtext.append(browser);
332        if (subDir != null) {
333          searchtext.append('/');
334          searchtext.append(subDir);
335        }
336        searchtext.append('/');
337        searchtext.append(name);
338        searchtext.append(localeSuffix);
339        searchtext.append(extension);
340        if (key != null) {
341          searchtext.append('/');
342          searchtext.append(key);
343        }
344    
345        return searchtext.toString();
346      }
347    
348      private String makePath(
349          String name, String localeSuffix, String extension, String key) {
350        StringBuilder searchtext = new StringBuilder(32);
351    
352        searchtext.append('/');
353        searchtext.append(name);
354        searchtext.append(localeSuffix);
355        searchtext.append(extension);
356        if (key != null) {
357          searchtext.append('/');
358          searchtext.append(key);
359        }
360    
361        return searchtext.toString();
362      }
363    
364      public Renderer getRenderer(UIViewRoot viewRoot, String name) {
365        Renderer renderer = null;
366    
367        if (name != null) {
368          CacheKey key = getCacheKey(viewRoot);
369    
370          RendererCacheKey rendererKey = new RendererCacheKey(key, name);
371          renderer = rendererCache.get(rendererKey);
372          if (renderer == null) {
373            try {
374              name = getRendererClassName(name);
375              List<Class> classes = getPaths(key.getClientPropertyId(), key.getLocale(), "", TAG, name, "",
376                  false, true, true, null, false, false);
377              if (classes != null && !classes.isEmpty()) {
378                Class clazz = classes.get(0);
379                renderer = (Renderer) clazz.newInstance();
380              } else {
381                renderer = NULL_CACHE_RENDERER;
382                LOG.error("Don't find any RendererClass for " + name + ". Please check you configuration.");
383              }
384              synchronized (rendererCache) {
385                rendererCache.put(rendererKey, renderer);
386              }
387            } catch (InstantiationException e) {
388              LOG.error("name = '" + name + "' clientProperties = '" + key.getClientPropertyId() + "'", e);
389            } catch (IllegalAccessException e) {
390              LOG.error("name = '" + name + "' clientProperties = '" + key.getClientPropertyId() + "'", e);
391            }
392            if (renderer == NULL_CACHE_RENDERER) {
393              return null;
394            }
395          }
396        }
397        return renderer;
398      }
399    
400    
401      private String getRendererClassName(String rendererType) {
402        String name;
403        if (LOG.isDebugEnabled()) {
404          LOG.debug("rendererType = '" + rendererType + "'");
405        }
406        if ("javax.faces.Text".equals(rendererType)) { // TODO: find a better way
407          name = RENDERER_TYPE_OUT;
408        } else {
409          name = rendererType;
410        }
411        name = name + "Renderer";
412        if (name.startsWith("javax.faces.")) { // FIXME: this is a hotfix from jsf1.0beta to jsf1.0fr
413          LOG.warn("patching renderer from " + name);
414          name = name.substring("javax.faces.".length());
415          LOG.warn("patching renderer to   " + name);
416        }
417        return name;
418      }
419    
420      public String[] getScripts(UIViewRoot viewRoot, String name) {
421        return getStrings(viewRoot, name, null);
422      }
423    
424      public String[] getStyles(UIViewRoot viewRoot, String name) {
425        return getStrings(viewRoot, name, null);
426      }
427    
428      private String[] getStrings(UIViewRoot viewRoot, String name, String type) {
429        String[] result = null;
430        if (name != null) {
431          int dot = name.lastIndexOf('.');
432          if (dot == -1) {
433            dot = name.length();
434          }
435          CacheKey key = getCacheKey(viewRoot);
436          MiscCacheKey miscKey = new MiscCacheKey(key, name);
437          result = miscCache.get(miscKey);
438          if (result == null) {
439            try {
440              List matches = getPaths(key.getClientPropertyId(), key.getLocale(), "", type,
441                  name.substring(0, dot), name.substring(dot), true, false, true, null, true, false);
442              result = (String[]) matches.toArray(new String[matches.size()]);
443              synchronized (miscCache) {
444                miscCache.put(miscKey, result);
445              }
446            } catch (Exception e) {
447              LOG.error("name = '" + name + "' clientProperties = '" + key.getClientPropertyId() + "'", e);
448            }
449          }
450        }
451        return result;
452      }
453    
454      public String getThemeProperty(UIViewRoot viewRoot, String bundle, String propertyKey) {
455        if (bundle != null && propertyKey != null) {
456          CacheKey key = getCacheKey(viewRoot);
457    
458          PropertyCacheKey propertyCacheKey = new PropertyCacheKey(key, bundle, propertyKey);
459          CachedString result = propertyCache.get(propertyCacheKey);
460          if (result == null) {
461            List properties = getPaths(key.getClientPropertyId(), key.getLocale(), "", PROPERTY,
462                bundle, "", false, true, false, propertyKey, true, true);
463            if (properties != null) {
464              result = new CachedString((String) properties.get(0));
465            } else {
466              result = new CachedString(null);
467            }
468            synchronized (propertyCache) {
469              propertyCache.put(propertyCacheKey, result);
470            }
471          }
472          return result.getValue();
473        }
474        return null;
475      }
476    
477      public static CacheKey getRendererCacheKey(String clientPropertyId, Locale locale) {
478        return new CacheKey(clientPropertyId, locale);
479      }
480    
481    
482      private static final class ImageCacheKey {
483        private CacheKey cacheKey;
484        private String name;
485        private int hashCode;
486    
487        private ImageCacheKey(CacheKey cacheKey, String name) {
488          this.name = name;
489          this.cacheKey = cacheKey;
490          hashCode = calcHashCode();
491        }
492    
493        @Override
494        public boolean equals(Object o) {
495          if (this == o) {
496            return true;
497          }
498          if (o == null || getClass() != o.getClass()) {
499            return false;
500          }
501    
502          ImageCacheKey that = (ImageCacheKey) o;
503    
504          return cacheKey.equals(that.cacheKey) && name.equals(that.name);
505        }
506    
507        private int calcHashCode() {
508          int result;
509          result = cacheKey.hashCode();
510          result = 31 * result + name.hashCode();
511          return result;
512        }
513    
514        @Override
515        public int hashCode() {
516          return hashCode;
517        }
518    
519        @Override
520        public String toString() {
521          return cacheKey + " + " + name;
522        }
523      }
524    
525      private static final class JspCacheKey {
526        private final CacheKey cacheKey;
527        private final String name;
528        private final int hashCode;
529    
530        private JspCacheKey(CacheKey cacheKey, String name) {
531          this.cacheKey = cacheKey;
532          this.name = name;
533          hashCode = calcHashCode();
534        }
535    
536        @Override
537        public boolean equals(Object o) {
538          if (this == o) {
539            return true;
540          }
541          if (o == null || getClass() != o.getClass()) {
542            return false;
543          }
544    
545          JspCacheKey that = (JspCacheKey) o;
546    
547          return cacheKey.equals(that.cacheKey) && name.equals(that.name);
548    
549        }
550    
551        private int calcHashCode() {
552          int result;
553          result = cacheKey.hashCode();
554          result = 31 * result + name.hashCode();
555          return result;
556        }
557    
558        @Override
559        public int hashCode() {
560          return hashCode;
561        }
562      }
563    
564      private static final class PropertyCacheKey {
565        private final CacheKey cacheKey;
566        private final String name;
567        private final String key;
568        private final int hashCode;
569    
570        private PropertyCacheKey(CacheKey cacheKey, String name, String key) {
571          this.cacheKey = cacheKey;
572          this.name = name;
573          this.key = key;
574          hashCode = calcHashCode();
575        }
576    
577        @Override
578        public boolean equals(Object o) {
579          if (this == o) {
580            return true;
581          }
582          if (o == null || getClass() != o.getClass()) {
583            return false;
584          }
585    
586          PropertyCacheKey that = (PropertyCacheKey) o;
587    
588          return cacheKey.equals(that.cacheKey) && key.equals(that.key) && name.equals(that.name);
589    
590        }
591    
592        private int calcHashCode() {
593          int result;
594          result = cacheKey.hashCode();
595          result = 31 * result + name.hashCode();
596          result = 31 * result + key.hashCode();
597          return result;
598        }
599    
600        @Override
601        public int hashCode() {
602          return hashCode;
603        }
604      }
605    
606      private static final class MiscCacheKey {
607        private final CacheKey cacheKey;
608        private final String name;
609        private final int hashCode;
610    
611        private MiscCacheKey(CacheKey cacheKey, String name) {
612          this.cacheKey = cacheKey;
613          this.name = name;
614          hashCode = calcHashCode();
615        }
616    
617        @Override
618        public boolean equals(Object o) {
619          if (this == o) {
620            return true;
621          }
622          if (o == null || getClass() != o.getClass()) {
623            return false;
624          }
625    
626          MiscCacheKey that = (MiscCacheKey) o;
627    
628          return cacheKey.equals(that.cacheKey) && name.equals(that.name);
629    
630        }
631    
632        private int calcHashCode() {
633          int result;
634          result = cacheKey.hashCode();
635          result = 31 * result + name.hashCode();
636          return result;
637        }
638    
639        @Override
640        public int hashCode() {
641          return hashCode;
642        }
643      }
644    
645      private static final class RendererCacheKey {
646        private final CacheKey cacheKey;
647        private final String name;
648        private final int hashCode;
649    
650        private RendererCacheKey(CacheKey cacheKey, String name) {
651          this.cacheKey = cacheKey;
652          this.name = name;
653          hashCode = calcHashCode();
654        }
655    
656        @Override
657        public boolean equals(Object o) {
658          if (this == o) {
659            return true;
660          }
661          if (o == null || getClass() != o.getClass()) {
662            return false;
663          }
664    
665          RendererCacheKey that = (RendererCacheKey) o;
666    
667          return cacheKey.equals(that.cacheKey) && name.equals(that.name);
668    
669        }
670    
671        private int calcHashCode() {
672          int result;
673          result = cacheKey.hashCode();
674          result = 31 * result + name.hashCode();
675          return result;
676        }
677    
678        @Override
679        public int hashCode() {
680          return hashCode;
681        }
682      }
683    
684      public static final class CacheKey {
685        private final String clientPropertyId;
686        private final Locale locale;
687        private final int hashCode;
688    
689        private CacheKey(String clientPropertyId, Locale locale) {
690          this.clientPropertyId = clientPropertyId;
691          if (locale == null) { //  FIXME: should not happen, but does.
692            LOG.warn("locale == null");
693            locale = Locale.getDefault();
694          }
695          this.locale = locale;
696          hashCode = calcHashCode();
697        }
698    
699        public String getClientPropertyId() {
700          return clientPropertyId;
701        }
702    
703        public Locale getLocale() {
704          return locale;
705        }
706    
707        @Override
708        public boolean equals(Object o) {
709          if (this == o) {
710            return true;
711          }
712          if (o == null || getClass() != o.getClass()) {
713            return false;
714          }
715    
716          CacheKey cacheKey = (CacheKey) o;
717    
718          return clientPropertyId.equals(cacheKey.clientPropertyId) && locale.equals(cacheKey.locale);
719    
720        }
721    
722        private int calcHashCode() {
723          int result;
724          result = clientPropertyId.hashCode();
725          result = 31 * result + locale.hashCode();
726          return result;
727        }
728    
729        @Override
730        public int hashCode() {
731          return hashCode;
732        }
733    
734        @Override
735        public String toString() {
736          return clientPropertyId + " + " + locale;
737        }
738      }
739    
740      public static final class CachedString {
741        private String value;
742    
743        public CachedString(String value) {
744          this.value = value;
745        }
746    
747        public String getValue() {
748          return value;
749        }
750    
751        @Override
752        public boolean equals(Object o) {
753          if (this == o) {
754            return true;
755          }
756          if (o == null || getClass() != o.getClass()) {
757            return false;
758          }
759    
760          CachedString that = (CachedString) o;
761    
762          if (value != null ? !value.equals(that.value) : that.value != null) {
763            return false;
764          }
765    
766          return true;
767        }
768    
769        @Override
770        public int hashCode() {
771          return (value != null ? value.hashCode() : 0);
772        }
773      }
774    }
775