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