001 // Copyright 2006, 2007, 2008, 2010, 2011 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.tapestry5.internal.services; 016 017 import java.util.Collections; 018 import java.util.List; 019 import java.util.Map; 020 021 import org.apache.tapestry5.func.F; 022 import org.apache.tapestry5.internal.event.InvalidationEventHubImpl; 023 import org.apache.tapestry5.internal.util.MultiKey; 024 import org.apache.tapestry5.ioc.Messages; 025 import org.apache.tapestry5.ioc.Resource; 026 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 027 import org.apache.tapestry5.ioc.internal.util.URLChangeTracker; 028 import org.apache.tapestry5.services.messages.PropertiesFileParser; 029 import org.apache.tapestry5.services.pageload.ComponentResourceLocator; 030 import org.apache.tapestry5.services.pageload.ComponentResourceSelector; 031 032 /** 033 * A utility class that encapsulates all the logic for reading properties files and assembling {@link Messages} from 034 * them, in accordance with extension rules and locale. This represents code that was refactored out of 035 * {@link ComponentMessagesSourceImpl}. This class can be used as a base class, though the existing code base uses it as 036 * a utility. Composition trumps inheritance! 037 * <p/> 038 * The message catalog for a component is the combination of all appropriate properties files for the component, plus 039 * any keys inherited form base components and, ultimately, the application global message catalog. At some point we 040 * should add support for per-library message catalogs. 041 * <p/> 042 * Message catalogs are read using the UTF-8 character set. This is tricky in JDK 1.5; we read the file into memory then 043 * feed that bytestream to Properties.load(). 044 */ 045 public class MessagesSourceImpl extends InvalidationEventHubImpl implements MessagesSource 046 { 047 private final URLChangeTracker tracker; 048 049 private final PropertiesFileParser propertiesFileParser; 050 051 private final ComponentResourceLocator resourceLocator; 052 053 /** 054 * Keyed on bundle id and ComponentResourceSelector. 055 */ 056 private final Map<MultiKey, Messages> messagesByBundleIdAndSelector = CollectionFactory.newConcurrentMap(); 057 058 /** 059 * Keyed on bundle id and ComponentResourceSelector, the cooked properties include properties inherited from less 060 * locale-specific properties files, or inherited from parent bundles. 061 */ 062 private final Map<MultiKey, Map<String, String>> cookedProperties = CollectionFactory.newConcurrentMap(); 063 064 /** 065 * Raw properties represent just the properties read from a specific properties file, in isolation. 066 */ 067 private final Map<Resource, Map<String, String>> rawProperties = CollectionFactory.newConcurrentMap(); 068 069 private final Map<String, String> emptyMap = Collections.emptyMap(); 070 071 public MessagesSourceImpl(boolean productionMode, URLChangeTracker tracker, 072 ComponentResourceLocator resourceLocator, PropertiesFileParser propertiesFileParser) 073 { 074 super(productionMode); 075 076 this.tracker = tracker; 077 this.propertiesFileParser = propertiesFileParser; 078 this.resourceLocator = resourceLocator; 079 } 080 081 public void checkForUpdates() 082 { 083 if (tracker != null && tracker.containsChanges()) 084 { 085 messagesByBundleIdAndSelector.clear(); 086 cookedProperties.clear(); 087 rawProperties.clear(); 088 089 tracker.clear(); 090 091 fireInvalidationEvent(); 092 } 093 } 094 095 public Messages getMessages(MessagesBundle bundle, ComponentResourceSelector selector) 096 { 097 MultiKey key = new MultiKey(bundle.getId(), selector); 098 099 Messages result = messagesByBundleIdAndSelector.get(key); 100 101 if (result == null) 102 { 103 result = buildMessages(bundle, selector); 104 messagesByBundleIdAndSelector.put(key, result); 105 } 106 107 return result; 108 } 109 110 private Messages buildMessages(MessagesBundle bundle, ComponentResourceSelector selector) 111 { 112 Map<String, String> properties = findBundleProperties(bundle, selector); 113 114 return new MapMessages(selector.locale, properties); 115 } 116 117 /** 118 * Assembles a set of properties appropriate for the bundle in question, and the desired locale. The properties 119 * reflect the properties of the bundles' parent (if any) for the locale, overalyed with any properties defined for 120 * this bundle and its locale. 121 */ 122 private Map<String, String> findBundleProperties(MessagesBundle bundle, ComponentResourceSelector selector) 123 { 124 if (bundle == null) 125 return emptyMap; 126 127 MultiKey key = new MultiKey(bundle.getId(), selector); 128 129 Map<String, String> existing = cookedProperties.get(key); 130 131 if (existing != null) 132 return existing; 133 134 // What would be cool is if we could maintain a cache of bundle id + locale --> 135 // Resource. That would optimize quite a bit of this; may need to use an alternative to 136 // LocalizedNameGenerator. 137 138 Resource propertiesResource = bundle.getBaseResource().withExtension("properties"); 139 140 List<Resource> localizations = resourceLocator.locateMessageCatalog(propertiesResource, selector); 141 142 // Localizations are now in least-specific to most-specific order. 143 144 Map<String, String> previous = findBundleProperties(bundle.getParent(), selector); 145 146 for (Resource localization : F.flow(localizations).reverse()) 147 { 148 Map<String, String> rawProperties = getRawProperties(localization); 149 150 // Woould be nice to write into the cookedProperties cache here, 151 // but we can't because we don't know the selector part of the MultiKey. 152 153 previous = extend(previous, rawProperties); 154 } 155 156 cookedProperties.put(key, previous); 157 158 return previous; 159 } 160 161 /** 162 * Returns a new map consisting of all the properties in previous overlayed with all the properties in 163 * rawProperties. If rawProperties is empty, returns just the base map. 164 */ 165 private Map<String, String> extend(Map<String, String> base, Map<String, String> rawProperties) 166 { 167 if (rawProperties.isEmpty()) 168 return base; 169 170 // Make a copy of the base Map 171 172 Map<String, String> result = CollectionFactory.newCaseInsensitiveMap(base); 173 174 // Add or overwrite properties to the copy 175 176 result.putAll(rawProperties); 177 178 return result; 179 } 180 181 private Map<String, String> getRawProperties(Resource localization) 182 { 183 Map<String, String> result = rawProperties.get(localization); 184 185 if (result == null) 186 { 187 result = readProperties(localization); 188 189 rawProperties.put(localization, result); 190 } 191 192 return result; 193 } 194 195 /** 196 * Creates and returns a new map that contains properties read from the properties file. 197 */ 198 private Map<String, String> readProperties(Resource resource) 199 { 200 if (!resource.exists()) 201 return emptyMap; 202 203 if (tracker != null) 204 { 205 tracker.add(resource.toURL()); 206 } 207 208 try 209 { 210 return propertiesFileParser.parsePropertiesFile(resource); 211 } 212 catch (Exception ex) 213 { 214 throw new RuntimeException(ServicesMessages.failureReadingMessages(resource, ex), ex); 215 } 216 } 217 218 }