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 }