View Javadoc

1   /*
2    * $Id: UrlDefinitionsFactory.java 680308 2008-07-28 10:03:41Z apetrelli $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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             // For each postfix, all the sources must be loaded.
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                     // Definition must be collected, starting from the base
244                     // source up to the last localized file.
245                     Map<String, Definition> defsMap = reader
246                             .read(connection.getInputStream());
247                     if (defsMap != null) {
248                         localeDefsMap.putAll(defsMap);
249                     }
250                 } catch (FileNotFoundException e) {
251                     // File not found. continue.
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         // At the end of definitions loading, they can be assigned to
263         // Definitions implementation, to allow inheritance resolution.
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         // Search file name extension.
337         // take care of Unix files starting with .
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         // The default configuration file must be loaded to allow correct
367         // definition inheritance.
368         result.add("");
369         if (languageLength + countryLength + variantLength == 0) {
370             //The locale is "", "", "".
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 }