001    // Copyright 2004, 2005 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry.services.impl;
016    
017    import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
018    import org.apache.hivemind.ApplicationRuntimeException;
019    import org.apache.hivemind.Messages;
020    import org.apache.hivemind.Resource;
021    import org.apache.hivemind.util.Defense;
022    import org.apache.hivemind.util.LocalizedNameGenerator;
023    import org.apache.tapestry.IComponent;
024    import org.apache.tapestry.INamespace;
025    import org.apache.tapestry.event.ResetEventListener;
026    import org.apache.tapestry.resolver.IComponentResourceResolver;
027    import org.apache.tapestry.services.ClasspathResourceFactory;
028    import org.apache.tapestry.services.ComponentMessagesSource;
029    import org.apache.tapestry.services.ComponentPropertySource;
030    import org.apache.tapestry.util.text.LocalizedProperties;
031    
032    import java.io.BufferedInputStream;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.net.URL;
036    import java.util.*;
037    
038    /**
039     * Service used to access localized properties for a component.
040     *
041     * @author Howard Lewis Ship
042     * @since 2.0.4
043     */
044    
045    public class ComponentMessagesSourceImpl implements ComponentMessagesSource, ResetEventListener
046    {
047        /**
048         * The name of the component/application/etc property that will be used to
049         * determine the encoding to use when loading the messages.
050         */
051    
052        public static final String MESSAGES_ENCODING_PROPERTY_NAME = "org.apache.tapestry.messages-encoding";
053    
054        /**
055         * The alternate file name of a namespace properties file to lookup. Can be used to override the default
056         * behaviour which is to look for a <namespace name>.properties file to find localized/global properties.
057         */
058    
059        public static final String NAMESPACE_PROPERTIES_NAME = "org.apache.tapestry.namespace-properties-name";
060    
061        private static final String SUFFIX = ".properties";
062    
063        private Properties _emptyProperties = new Properties();
064    
065        /**
066         * Map of Maps. The outer map is keyed on component specification location
067         * (a{@link Resource}. This inner map is keyed on locale and the value is
068         * a {@link Properties}.
069         */
070    
071        private Map _componentCache = new ConcurrentHashMap();
072    
073        private ComponentPropertySource _componentPropertySource;
074    
075        /**
076         * For locating resources on the classpath as well as context path.
077         */
078        private ClasspathResourceFactory _classpathResourceFactory;
079    
080        private IComponentResourceResolver _resourceResolver;
081    
082        /**
083         * Returns an instance of {@link Properties}containing the properly
084         * localized messages for the component, in the {@link Locale}identified by
085         * the component's containing page.
086         */
087    
088        protected Properties getLocalizedProperties(IComponent component)
089        {
090            Defense.notNull(component, "component");
091    
092            Resource specificationLocation = component.getSpecification().getSpecificationLocation();
093    
094            Locale locale = component.getPage().getLocale();
095    
096            Map propertiesMap = findPropertiesMapForResource(specificationLocation);
097    
098            Properties result = (Properties) propertiesMap.get(locale);
099    
100            if (result == null)
101            {
102                // Not found, create it now.
103    
104                result = assembleComponentProperties(component, specificationLocation, propertiesMap, locale);
105    
106                propertiesMap.put(locale, result);
107            }
108    
109            return result;
110        }
111    
112        private Map findPropertiesMapForResource(Resource resource)
113        {
114            Map result = (Map) _componentCache.get(resource);
115    
116            if (result == null)
117            {
118                result = new HashMap();
119                _componentCache.put(resource, result);
120            }
121    
122            return result;
123        }
124    
125        private Properties getNamespaceProperties(IComponent component, Locale locale)
126        {
127            INamespace namespace = component.getNamespace();
128    
129            Resource namespaceLocation = namespace.getSpecificationLocation();
130    
131            Map propertiesMap = findPropertiesMapForResource(namespaceLocation);
132    
133            Properties result = (Properties) propertiesMap.get(locale);
134    
135            if (result == null)
136            {
137                result = assembleNamespaceProperties(namespace, propertiesMap, locale);
138    
139                propertiesMap.put(locale, result);
140            }
141    
142            return result;
143        }
144    
145        private Properties assembleComponentProperties(IComponent component, Resource baseResourceLocation,
146                                                       Map propertiesMap, Locale locale)
147        {
148            List localizations =  findLocalizationsForResource(component, baseResourceLocation, locale,
149                                                               component.getSpecification().getProperty(NAMESPACE_PROPERTIES_NAME));
150    
151            Properties parent = null;
152            Properties assembledProperties = null;
153    
154            Iterator i = localizations.iterator();
155    
156            while(i.hasNext())
157            {
158                ResourceLocalization rl = (ResourceLocalization) i.next();
159    
160                Locale l = rl.getLocale();
161    
162                // Retrieve namespace properties for current locale (and parent
163                // locales)
164                Properties namespaceProperties = getNamespaceProperties(component, l);
165    
166                // Use the namespace properties as default for assembled properties
167                assembledProperties = new Properties(namespaceProperties);
168    
169                // Read localized properties for component
170                Properties properties = readComponentProperties(component, l, rl.getResource(), null);
171    
172                // Override parent properties with current locale
173                if (parent != null)
174                {
175                    if (properties != null)
176                        parent.putAll(properties);
177                }
178                else
179                    parent = properties;
180    
181                // Add to assembled properties
182                if (parent != null)
183                    assembledProperties.putAll(parent);
184    
185                // Save result in cache
186                propertiesMap.put(l, assembledProperties);
187            }
188    
189            if (assembledProperties == null)
190                assembledProperties = new Properties();
191    
192            return assembledProperties;
193        }
194    
195        private Properties assembleNamespaceProperties(INamespace namespace,
196                                                       Map propertiesMap, Locale locale)
197        {
198            List localizations = findLocalizationsForResource(namespace.getSpecificationLocation(), locale,
199                                                              namespace.getPropertyValue(NAMESPACE_PROPERTIES_NAME));
200    
201            // Build them back up in reverse order.
202    
203            Properties parent = _emptyProperties;
204    
205            Iterator i = localizations.iterator();
206    
207            while(i.hasNext())
208            {
209                ResourceLocalization rl = (ResourceLocalization) i.next();
210    
211                Locale l = rl.getLocale();
212    
213                Properties properties = (Properties) propertiesMap.get(l);
214    
215                if (properties == null)
216                {
217                    properties = readNamespaceProperties(namespace, l, rl.getResource(), parent);
218    
219                    propertiesMap.put(l, properties);
220                }
221    
222                parent = properties;
223            }
224    
225            return parent;
226    
227        }
228    
229        /**
230         * Finds the localizations of the provided resource. Returns a List of
231         * {@link ResourceLocalization}(each pairing a locale with a localized
232         * resource). The list is ordered from most general (i.e., "foo.properties")
233         * to most specific (i.e., "foo_en_US_yokel.properties").
234         */
235    
236        private List findLocalizationsForResource(Resource resource, Locale locale, String alternateName)
237        {
238            List result = new ArrayList();
239    
240            String baseName = null;
241            if (alternateName != null) {
242    
243                baseName = alternateName.replace('.', '/');
244            } else {
245    
246                baseName = extractBaseName(resource);
247            }
248    
249            LocalizedNameGenerator g = new LocalizedNameGenerator(baseName, locale, SUFFIX);
250    
251            while(g.more())
252            {
253                String localizedName = g.next();
254                Locale l = g.getCurrentLocale();
255    
256                Resource localizedResource = resource.getRelativeResource(localizedName);
257    
258                if (localizedResource.getResourceURL() == null) {
259    
260                    localizedResource = _classpathResourceFactory.newResource(baseName + SUFFIX);
261                }
262    
263                result.add(new ResourceLocalization(l, localizedResource));
264            }
265    
266            Collections.reverse(result);
267    
268            return result;
269        }
270    
271        private List findLocalizationsForResource(IComponent component, Resource resource, Locale locale, String alternateName)
272        {
273            List result = new ArrayList();
274    
275            String baseName = null;
276            if (alternateName != null) {
277    
278                baseName = alternateName.replace('.', '/');
279            } else {
280    
281                baseName = extractBaseName(resource);
282            }
283    
284            LocalizedNameGenerator g = new LocalizedNameGenerator(baseName, locale, "");
285    
286            while(g.more())
287            {
288                String localizedName = g.next();
289                Locale l = g.getCurrentLocale();
290    
291                Resource localizedResource = _resourceResolver.findComponentResource(component, null, localizedName, SUFFIX, null);
292                if (localizedResource == null)
293                    continue;
294    
295                result.add(new ResourceLocalization(l, localizedResource));
296            }
297    
298            Collections.reverse(result);
299    
300            return result;
301        }
302    
303        private String extractBaseName(Resource baseResourceLocation)
304        {
305            String fileName = baseResourceLocation.getName();
306            int dotx = fileName.lastIndexOf('.');
307    
308            return dotx > -1 ? fileName.substring(0, dotx) : fileName;
309        }
310    
311        private Properties readComponentProperties(IComponent component,
312                                                   Locale locale, Resource propertiesResource, Properties parent)
313        {
314            String encoding = getComponentMessagesEncoding(component, locale);
315    
316            return readPropertiesResource(propertiesResource.getResourceURL(), encoding, parent);
317        }
318    
319        private Properties readNamespaceProperties(INamespace namespace,
320                                                   Locale locale, Resource propertiesResource, Properties parent)
321        {
322            String encoding = getNamespaceMessagesEncoding(namespace, locale);
323    
324            return readPropertiesResource(propertiesResource.getResourceURL(), encoding, parent);
325        }
326    
327        private Properties readPropertiesResource(URL resourceURL, String encoding, Properties parent)
328        {
329            if (resourceURL == null)
330                return parent;
331    
332            Properties result = new Properties(parent);
333    
334            LocalizedProperties wrapper = new LocalizedProperties(result);
335    
336            InputStream input = null;
337    
338            try
339            {
340                input = new BufferedInputStream(resourceURL.openStream());
341    
342                if (encoding == null)
343                    wrapper.load(input);
344                else
345                    wrapper.load(input, encoding);
346    
347                input.close();
348            }
349            catch (IOException ex)
350            {
351                throw new ApplicationRuntimeException(ImplMessages.unableToLoadProperties(resourceURL, ex), ex);
352            }
353            finally
354            {
355                close(input);
356            }
357    
358            return result;
359        }
360    
361        private void close(InputStream is)
362        {
363            if (is != null) try
364            {
365                is.close();
366            }
367            catch (IOException ex)
368            {
369                // Ignore.
370            }
371        }
372    
373        /**
374         * Clears the cache of read properties files.
375         */
376    
377        public void resetEventDidOccur()
378        {
379            _componentCache.clear();
380        }
381    
382        public Messages getMessages(IComponent component)
383        {
384            return new ComponentMessages(component.getPage().getLocale(),
385                                         getLocalizedProperties(component));
386        }
387    
388        private String getComponentMessagesEncoding(IComponent component,
389                                                    Locale locale)
390        {
391            String encoding = _componentPropertySource.getLocalizedComponentProperty(component, locale,
392                                                                                     MESSAGES_ENCODING_PROPERTY_NAME);
393    
394            if (encoding == null)
395                encoding = _componentPropertySource.getLocalizedComponentProperty(
396                        component, locale,
397                        TemplateSourceImpl.TEMPLATE_ENCODING_PROPERTY_NAME);
398    
399            return encoding;
400        }
401    
402        private String getNamespaceMessagesEncoding(INamespace namespace,
403                                                    Locale locale)
404        {
405            return _componentPropertySource.getLocalizedNamespaceProperty(
406                    namespace, locale, MESSAGES_ENCODING_PROPERTY_NAME);
407        }
408    
409        public void setComponentPropertySource(ComponentPropertySource componentPropertySource)
410        {
411            _componentPropertySource = componentPropertySource;
412        }
413    
414        public void setClasspathResourceFactory(ClasspathResourceFactory factory)
415        {
416            _classpathResourceFactory = factory;
417        }
418    
419        public void setComponentResourceResolver(IComponentResourceResolver resourceResolver)
420        {
421            _resourceResolver = resourceResolver;
422        }
423    }