1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.apache.tiles.definition;
22
23 import org.apache.commons.logging.Log;
24 import org.apache.commons.logging.LogFactory;
25 import org.apache.tiles.Definition;
26 import org.apache.tiles.TilesException;
27 import org.apache.tiles.context.TilesRequestContext;
28 import org.apache.tiles.definition.digester.DigesterDefinitionsReader;
29 import org.apache.tiles.locale.LocaleResolver;
30 import org.apache.tiles.locale.impl.DefaultLocaleResolver;
31 import org.apache.tiles.reflect.ClassUtil;
32
33 import java.io.FileNotFoundException;
34 import java.io.IOException;
35 import java.net.URL;
36 import java.net.URLConnection;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Set;
43
44 /***
45 * {@link DefinitionsFactory DefinitionsFactory} implementation
46 * that manages Definitions configuration data from URLs.
47 * <p/>
48 * <p>The Definition objects are read from the
49 * {@link org.apache.tiles.definition.digester.DigesterDefinitionsReader DigesterDefinitionsReader}
50 * class unless another implementation is specified.</p>
51 *
52 * @version $Rev: 680308 $ $Date: 2008-07-28 12:03:41 +0200 (Mon, 28 Jul 2008) $
53 */
54 public class UrlDefinitionsFactory
55 implements DefinitionsFactory, ReloadableDefinitionsFactory {
56
57 /***
58 * LOG instance for all UrlDefinitionsFactory instances.
59 */
60 private static final Log LOG = LogFactory.getLog(UrlDefinitionsFactory.class);
61
62 /***
63 * Contains the URL objects identifying where configuration data is found.
64 */
65 protected List<Object> sources;
66
67 /***
68 * Reader used to get definitions from the sources.
69 */
70 protected DefinitionsReader reader;
71
72 /***
73 * Contains the dates that the URL sources were last modified.
74 */
75 protected Map<String, Long> lastModifiedDates;
76
77 /***
78 * Contains a list of locales that have been processed.
79 */
80 private List<Locale> processedLocales;
81
82
83 /***
84 * The definitions holder object.
85 */
86 private Definitions definitions;
87
88 /***
89 * The locale resolver object.
90 */
91 private LocaleResolver localeResolver;
92
93 /***
94 * Creates a new instance of UrlDefinitionsFactory.
95 */
96 public UrlDefinitionsFactory() {
97 sources = new ArrayList<Object>();
98 lastModifiedDates = new HashMap<String, Long>();
99 processedLocales = new ArrayList<Locale>();
100 }
101
102 /***
103 * Initializes the DefinitionsFactory and its subcomponents.
104 * <p/>
105 * Implementations may support configuration properties to be passed in via
106 * the params Map.
107 *
108 * @param params The Map of configuration properties.
109 * @throws TilesException if an initialization error occurs.
110 */
111 public void init(Map<String, String> params) throws TilesException {
112 String readerClassName =
113 params.get(DefinitionsFactory.READER_IMPL_PROPERTY);
114
115 if (readerClassName != null) {
116 reader = (DefinitionsReader) ClassUtil.instantiate(readerClassName);
117 } else {
118 reader = new DigesterDefinitionsReader();
119 }
120 reader.init(params);
121
122 String resolverClassName = params
123 .get(DefinitionsFactory.LOCALE_RESOLVER_IMPL_PROPERTY);
124 if (resolverClassName != null) {
125 localeResolver = (LocaleResolver) ClassUtil.instantiate(resolverClassName);
126 } else {
127 localeResolver = new DefaultLocaleResolver();
128 }
129 localeResolver.init(params);
130 definitions = readDefinitions();
131 }
132
133 /***
134 * Returns the definitions holder object.
135 *
136 * @return The definitions holder.
137 * @throws DefinitionsFactoryException If something goes wrong during
138 * reading definitions.
139 */
140 protected Definitions getDefinitions()
141 throws DefinitionsFactoryException {
142 return definitions;
143 }
144
145
146 /***
147 * Returns a Definition object that matches the given name and
148 * Tiles context.
149 *
150 * @param name The name of the Definition to return.
151 * @param tilesContext The Tiles context to use to resolve the definition.
152 * @return the Definition matching the given name or null if none
153 * is found.
154 * @throws DefinitionsFactoryException if an error occurs reading definitions.
155 */
156 public Definition getDefinition(String name,
157 TilesRequestContext tilesContext)
158 throws DefinitionsFactoryException {
159
160 Definitions definitions = getDefinitions();
161 Locale locale = null;
162
163 if (tilesContext != null) {
164 locale = localeResolver.resolveLocale(tilesContext);
165 if (!isContextProcessed(tilesContext)) {
166 synchronized (definitions) {
167 addDefinitions(definitions, tilesContext);
168 }
169 }
170 }
171
172 return definitions.getDefinition(name, locale);
173 }
174
175 /***
176 * Adds a source where Definition objects are stored.
177 * <p/>
178 * Implementations should publish what type of source object they expect.
179 * The source should contain enough information to resolve a configuration
180 * source containing definitions. The source should be a "base" source for
181 * configurations. Internationalization and Localization properties will be
182 * applied by implementations to discriminate the correct data sources based
183 * on locale.
184 *
185 * @param source The configuration source for definitions.
186 * @throws DefinitionsFactoryException if an invalid source is passed in or
187 * an error occurs resolving the source to an actual data store.
188 */
189 public void addSource(Object source) throws DefinitionsFactoryException {
190 if (source == null) {
191 throw new DefinitionsFactoryException(
192 "Source object must not be null");
193 }
194
195 if (!(source instanceof URL)) {
196 throw new DefinitionsFactoryException(
197 "Source object must be an URL");
198 }
199
200 sources.add(source);
201 }
202
203 /***
204 * Appends locale-specific {@link Definition} objects to an existing
205 * {@link Definitions} set by reading locale-specific versions of
206 * the applied sources.
207 *
208 * @param definitions The Definitions object to append to.
209 * @param tilesContext The requested locale.
210 * @throws DefinitionsFactoryException if an error occurs reading definitions.
211 */
212 protected void addDefinitions(Definitions definitions,
213 TilesRequestContext tilesContext)
214 throws DefinitionsFactoryException {
215
216 Locale locale = localeResolver.resolveLocale(tilesContext);
217
218 if (isContextProcessed(tilesContext)) {
219 return;
220 }
221
222 if (locale == null) {
223 return;
224 }
225
226 processedLocales.add(locale);
227 List<String> postfixes = calculatePostfixes(locale);
228 Map<String, Definition> localeDefsMap = new HashMap<String, Definition>();
229 for (Object postfix : postfixes) {
230
231 for (Object source : sources) {
232 URL url = (URL) source;
233 String path = url.toExternalForm();
234
235 String newPath = concatPostfix(path, (String) postfix);
236 try {
237 URL newUrl = new URL(newPath);
238 URLConnection connection = newUrl.openConnection();
239 connection.connect();
240 lastModifiedDates.put(newUrl.toExternalForm(),
241 connection.getLastModified());
242
243
244
245 Map<String, Definition> defsMap = reader
246 .read(connection.getInputStream());
247 if (defsMap != null) {
248 localeDefsMap.putAll(defsMap);
249 }
250 } catch (FileNotFoundException e) {
251
252 if (LOG.isDebugEnabled()) {
253 LOG.debug("File " + newPath + " not found, continue");
254 }
255 } catch (IOException e) {
256 throw new DefinitionsFactoryException(
257 "I/O error processing configuration.");
258 }
259 }
260 }
261
262
263
264 definitions.addDefinitions(localeDefsMap, localeResolver
265 .resolveLocale(tilesContext));
266 }
267
268 /***
269 * Creates and returns a {@link Definitions} set by reading
270 * configuration data from the applied sources.
271 *
272 * @return The definitions holder object, filled with base definitions.
273 * @throws DefinitionsFactoryException if an error occurs reading the
274 * sources.
275 */
276 public Definitions readDefinitions()
277 throws DefinitionsFactoryException {
278 Definitions definitions = createDefinitions();
279 try {
280 for (Object source1 : sources) {
281 URL source = (URL) source1;
282 URLConnection connection = source.openConnection();
283 connection.connect();
284 lastModifiedDates.put(source.toExternalForm(),
285 connection.getLastModified());
286 Map<String, Definition> defsMap = reader
287 .read(connection.getInputStream());
288 definitions.addDefinitions(defsMap);
289 }
290 } catch (IOException e) {
291 throw new DefinitionsFactoryException("I/O error accessing source.", e);
292 }
293 return definitions;
294 }
295
296 /***
297 * Indicates whether a given context has been processed or not.
298 * <p/>
299 * This method can be used to avoid unnecessary synchronization of the
300 * DefinitionsFactory in multi-threaded situations. Check the return of
301 * isContextProcessed before synchronizing the object and reading
302 * locale-specific definitions.
303 *
304 * @param tilesContext The Tiles context to check.
305 * @return true if the given context has been processed and false otherwise.
306 */
307 protected boolean isContextProcessed(TilesRequestContext tilesContext) {
308 return processedLocales.contains(localeResolver
309 .resolveLocale(tilesContext));
310 }
311
312 /***
313 * Creates a new instance of <code>Definitions</code>. Override this method
314 * to provide your custom instance of Definitions.
315 *
316 * @return A new instance of <code>Definitions</code>.
317 */
318 protected Definitions createDefinitions() {
319 return new DefinitionsImpl();
320 }
321
322 /***
323 * Concat postfix to the name. Take care of existing filename extension.
324 * Transform the given name "name.ext" to have "name" + "postfix" + "ext".
325 * If there is no ext, return "name" + "postfix".
326 *
327 * @param name Filename.
328 * @param postfix Postfix to add.
329 * @return Concatenated filename.
330 */
331 protected String concatPostfix(String name, String postfix) {
332 if (postfix == null) {
333 return name;
334 }
335
336
337
338 int dotIndex = name.lastIndexOf(".");
339 int lastNameStart = name.lastIndexOf(java.io.File.pathSeparator);
340 if (dotIndex < 1 || dotIndex < lastNameStart) {
341 return name + postfix;
342 }
343
344 String ext = name.substring(dotIndex);
345 name = name.substring(0, dotIndex);
346 return name + postfix + ext;
347 }
348
349 /***
350 * Calculate the postfixes along the search path from the base bundle to the
351 * bundle specified by baseName and locale.
352 * Method copied from java.util.ResourceBundle
353 *
354 * @param locale the locale
355 * @return a list of
356 */
357 protected static List<String> calculatePostfixes(Locale locale) {
358 final List<String> result = new ArrayList<String>();
359 final String language = locale.getLanguage();
360 final int languageLength = language.length();
361 final String country = locale.getCountry();
362 final int countryLength = country.length();
363 final String variant = locale.getVariant();
364 final int variantLength = variant.length();
365
366
367
368 result.add("");
369 if (languageLength + countryLength + variantLength == 0) {
370
371 return result;
372 }
373
374 final StringBuffer temp = new StringBuffer();
375 temp.append('_');
376 temp.append(language);
377
378 if (languageLength > 0) {
379 result.add(temp.toString());
380 }
381
382 if (countryLength + variantLength == 0) {
383 return result;
384 }
385
386 temp.append('_');
387 temp.append(country);
388
389 if (countryLength > 0) {
390 result.add(temp.toString());
391 }
392
393 if (variantLength == 0) {
394 return result;
395 } else {
396 temp.append('_');
397 temp.append(variant);
398 result.add(temp.toString());
399 return result;
400 }
401 }
402
403
404 /*** {@inheritDoc} */
405 public void refresh() throws DefinitionsFactoryException {
406 LOG.debug("Updating Tiles definitions. . .");
407 synchronized (definitions) {
408 Definitions newDefs = readDefinitions();
409 definitions.reset();
410 definitions.addDefinitions(newDefs.getBaseDefinitions());
411 }
412 }
413
414
415 /***
416 * Indicates whether the DefinitionsFactory is out of date and needs to be
417 * reloaded.
418 *
419 * @return If the factory needs refresh.
420 */
421 public boolean refreshRequired() {
422 boolean status = false;
423
424 Set<String> urls = lastModifiedDates.keySet();
425
426 try {
427 for (String urlPath : urls) {
428 Long lastModifiedDate = lastModifiedDates.get(urlPath);
429 URL url = new URL(urlPath);
430 URLConnection connection = url.openConnection();
431 connection.connect();
432 long newModDate = connection.getLastModified();
433 if (newModDate != lastModifiedDate) {
434 status = true;
435 break;
436 }
437 }
438 } catch (Exception e) {
439 LOG.warn("Exception while monitoring update times.", e);
440 return true;
441 }
442 return status;
443 }
444 }