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}