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