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