001: // Copyright 2006, 2007 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.internal.services;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newCaseInsensitiveMap;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newConcurrentMap;
019: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
020:
021: import java.io.BufferedInputStream;
022: import java.io.InputStream;
023: import java.net.URL;
024: import java.util.Collections;
025: import java.util.List;
026: import java.util.Locale;
027: import java.util.Map;
028: import java.util.Properties;
029:
030: import org.apache.tapestry.internal.TapestryInternalUtils;
031: import org.apache.tapestry.internal.event.InvalidationEventHubImpl;
032: import org.apache.tapestry.internal.util.MultiKey;
033: import org.apache.tapestry.internal.util.URLChangeTracker;
034: import org.apache.tapestry.ioc.Messages;
035: import org.apache.tapestry.ioc.Resource;
036: import org.apache.tapestry.ioc.internal.util.LocalizedNameGenerator;
037:
038: /**
039: * A utility class that encapsulates all the logic for reading properties files and assembling
040: * {@link Messages} from them, in accordance with extension rules and locale. This represents code
041: * that was refactored out of {@link ComponentMessagesSourceImpl}. This class can be used as a base
042: * class, though the existing code base uses it as a utility. Composition trumps inheritance!
043: */
044: public class MessagesSourceImpl extends InvalidationEventHubImpl
045: implements MessagesSource {
046: private final URLChangeTracker _tracker;
047:
048: /** Keyed on bundle id and locale. */
049: private final Map<MultiKey, Messages> _messagesByBundleIdAndLocale = newConcurrentMap();
050:
051: /**
052: * Keyed on bundle id and locale, the coooked properties include properties inherited from less
053: * locale-specific properties files, or inherited from parent bundles.
054: */
055: private final Map<MultiKey, Map<String, String>> _cookedProperties = newConcurrentMap();
056:
057: /**
058: * Raw properties represent just the properties read from a specific properties file, in
059: * isolation.
060: */
061: private final Map<Resource, Map<String, String>> _rawProperties = newConcurrentMap();
062:
063: private final Map<String, String> _emptyMap = Collections
064: .emptyMap();
065:
066: public MessagesSourceImpl(URLChangeTracker tracker) {
067: _tracker = tracker;
068: }
069:
070: public void checkForUpdates() {
071: if (_tracker.containsChanges()) {
072: _messagesByBundleIdAndLocale.clear();
073: _cookedProperties.clear();
074: _rawProperties.clear();
075:
076: _tracker.clear();
077:
078: fireInvalidationEvent();
079: }
080: }
081:
082: public Messages getMessages(MessagesBundle bundle, Locale locale) {
083: MultiKey key = new MultiKey(bundle.getId(), locale);
084:
085: Messages result = _messagesByBundleIdAndLocale.get(key);
086:
087: if (result == null) {
088: result = buildMessages(bundle, locale);
089: _messagesByBundleIdAndLocale.put(key, result);
090: }
091:
092: return result;
093: }
094:
095: private Messages buildMessages(MessagesBundle bundle, Locale locale) {
096: Map<String, String> properties = findBundleProperties(bundle,
097: locale);
098:
099: return new MapMessages(properties);
100: }
101:
102: /**
103: * Assembles a set of properties appropriate for the bundle in question, and the desired locale.
104: * The properties reflect the properties of the bundles' parent (if any) for the locale,
105: * overalyed with any properties defined for this bundle and its locale.
106: */
107: private Map<String, String> findBundleProperties(
108: MessagesBundle bundle, Locale locale) {
109: if (bundle == null)
110: return _emptyMap;
111:
112: MultiKey key = new MultiKey(bundle.getId(), locale);
113:
114: Map<String, String> existing = _cookedProperties.get(key);
115:
116: if (existing != null)
117: return existing;
118:
119: // What would be cool is if we could maintain a cache of bundle id + locale -->
120: // Resource. That would optimize quite a bit of this; may need to use an alternative to
121: // LocalizedNameGenerator.
122:
123: Resource propertiesResource = bundle.getBaseResource()
124: .withExtension("properties");
125:
126: List<Resource> localizations = newList();
127:
128: for (String localizedFile : new LocalizedNameGenerator(
129: propertiesResource.getFile(), locale)) {
130: Resource localized = propertiesResource
131: .forFile(localizedFile);
132:
133: localizations.add(localized);
134: }
135:
136: // We need them in least-specific to most-specific order, the opposite
137: // of how the LocalizedNameGenerator provides them.
138:
139: Collections.reverse(localizations);
140:
141: // Localizations are now in least-specific to most-specific order.
142:
143: Map<String, String> previous = findBundleProperties(bundle
144: .getParent(), locale);
145:
146: for (Resource localization : localizations) {
147: Map<String, String> rawProperties = getRawProperties(localization);
148:
149: Map<String, String> properties = extend(previous,
150: rawProperties);
151:
152: // Woould be nice to write into the _cookedProperties cache here,
153: // but we can't because we don't know the locale part of the MultiKey.
154:
155: previous = properties;
156: }
157:
158: _cookedProperties.put(key, previous);
159:
160: return previous;
161: }
162:
163: /**
164: * Returns a new map consisting of all the properties in previous overlayed with all the
165: * properties in rawProperties. If rawProperties is empty, returns just the base map.
166: */
167: private Map<String, String> extend(Map<String, String> base,
168: Map<String, String> rawProperties) {
169: if (rawProperties.isEmpty())
170: return base;
171:
172: // Make a copy of the base Map
173:
174: Map<String, String> result = newCaseInsensitiveMap(base);
175:
176: // Add or overwrite properties to the copy
177:
178: result.putAll(rawProperties);
179:
180: return result;
181: }
182:
183: private Map<String, String> getRawProperties(Resource localization) {
184: Map<String, String> result = _rawProperties.get(localization);
185:
186: if (result == null) {
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: URL url = resource.toURL();
200:
201: if (url == null)
202: return _emptyMap;
203:
204: _tracker.add(url);
205:
206: Map<String, String> result = newCaseInsensitiveMap();
207:
208: Properties p = new Properties();
209: InputStream is = null;
210:
211: try {
212: is = new BufferedInputStream(url.openStream());
213:
214: p.load(is);
215:
216: is.close();
217:
218: is = null;
219: } catch (Exception ex) {
220: throw new RuntimeException(ServicesMessages
221: .failureReadingMessages(url, ex), ex);
222: } finally {
223: TapestryInternalUtils.close(is);
224: }
225:
226: for (Map.Entry e : p.entrySet()) {
227: String key = e.getKey().toString();
228:
229: String value = p.getProperty(key);
230:
231: result.put(key, value);
232: }
233:
234: return result;
235: }
236: }
|