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
015package org.apache.tapestry.services.impl;
016
017import java.io.BufferedInputStream;
018import java.io.IOException;
019import java.io.InputStream;
020import java.net.URL;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Properties;
029
030import org.apache.hivemind.ApplicationRuntimeException;
031import org.apache.hivemind.Messages;
032import org.apache.hivemind.Resource;
033import org.apache.hivemind.util.Defense;
034import org.apache.hivemind.util.LocalizedNameGenerator;
035import org.apache.tapestry.IComponent;
036import org.apache.tapestry.INamespace;
037import org.apache.tapestry.event.ResetEventListener;
038import org.apache.tapestry.services.ComponentMessagesSource;
039import org.apache.tapestry.services.ComponentPropertySource;
040import 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
049public 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}