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