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 }