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}