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