View Javadoc
1   /*
2    * Copyright (c) 2002-2020, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.service.template;
35  
36  import java.io.File;
37  import java.io.IOException;
38  import java.io.StringWriter;
39  import java.io.UnsupportedEncodingException;
40  import java.security.MessageDigest;
41  import java.security.NoSuchAlgorithmException;
42  import java.util.ArrayList;
43  import java.util.HashMap;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Map.Entry;
48  
49  import fr.paris.lutece.util.html.HtmlTemplate;
50  import fr.paris.lutece.util.html.exception.LuteceFreemarkerException;
51  import freemarker.cache.FileTemplateLoader;
52  import freemarker.cache.MultiTemplateLoader;
53  import freemarker.cache.StringTemplateLoader;
54  import freemarker.cache.TemplateLoader;
55  import freemarker.template.Configuration;
56  import freemarker.template.Template;
57  import freemarker.template.TemplateException;
58  import freemarker.template.Version;
59  
60  /**
61   *
62   * Template service based on the Freemarker template engine
63   *
64   */
65  public abstract class AbstractFreeMarkerTemplateService implements IFreeMarkerTemplateService
66  {
67      private static final String NUMBER_FORMAT_PATTERN = "0.######";
68      private static final String SETTING_DATE_FORMAT = "date_format";
69      private static final String CONSTANT_HASH_ENCODING = "UTF-8";
70      private static final String CONSTANT_HASH_DIGEST = "MD5";
71      
72  
73      /** the list contains plugins specific macros */
74      private List<String> _listPluginsAutoIncludes = new ArrayList<>( );
75      private Map<String, String> _mapPluginsAutoImports = new HashMap<>( );
76      private Map<String, Object> _mapSharedVariables = new HashMap<>( );
77      private Map<String, Configuration> _mapConfigurations = new HashMap<>( );
78      private String _strDefaultPath;
79      private int _nTemplateUpdateDelay;
80      private boolean _bAcceptIncompatibleImprovements;
81      
82  
83      /**
84       * {@inheritDoc}
85       */
86      @Override
87      public void setTemplateUpdateDelay( int nTemplateUpdateDelay )
88      {
89          _nTemplateUpdateDelay = nTemplateUpdateDelay;
90      }
91  
92      /**
93       * {@inheritDoc}
94       */
95      @Override
96      public void addPluginMacros( String strFileName )
97      {
98          _listPluginsAutoIncludes.add( strFileName );
99      }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
105     public void addPluginAutoInclude( String strFileName )
106     {
107         _listPluginsAutoIncludes.add( strFileName );
108     }
109 
110     /**
111      * {@inheritDoc}
112      */
113     @Override
114     public void addPluginAutoImport( String strNamespace, String strFileName )
115     {
116         _mapPluginsAutoImports.put( strNamespace, strFileName );
117     }
118 
119     /**
120      * {@inheritDoc}
121      */
122     @Override
123     public void setSharedVariable( String name, Object obj )
124     {
125         _mapSharedVariables.put( name, obj );
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     @Override
132     public void init( String strTemplatePath )
133     {
134         _strDefaultPath = strTemplatePath;
135         _bAcceptIncompatibleImprovements = false;
136     }
137 
138     /**
139      * {@inheritDoc}
140      */
141     @Override
142     public void init( String strTemplatePath, boolean bAcceptIncompatibleImprovements )
143     {
144         _strDefaultPath = strTemplatePath;
145         _bAcceptIncompatibleImprovements = bAcceptIncompatibleImprovements;
146     }
147 
148     /**
149      * {@inheritDoc}
150      */
151     @Override
152     public HtmlTemplate loadTemplate( String strPath, String strTemplate )
153     {
154         return loadTemplate( strPath, strTemplate, null, null );
155     }
156 
157     /**
158      * {@inheritDoc}
159      */
160     @Override
161     public HtmlTemplate loadTemplate( String strPath, String strTemplate, Locale locale, Object rootMap )
162     {
163         Configuration cfg = _mapConfigurations.get( strPath );
164 
165         if ( cfg == null )
166         {
167             cfg = initConfig( _strDefaultPath, Locale.getDefault( ) );
168         }
169 
170         return processTemplate( cfg, strTemplate, rootMap, locale );
171     }
172 
173   
174     /**
175     * {@inheritDoc}
176    */
177     @Override
178 	public HtmlTemplate loadTemplateFromStringFtl(String strTemplateData, Locale locale, Object rootMap) {
179 		try {
180 			String strContentKey = getHash(strTemplateData);
181 			return loadTemplateFromStringFtl(strContentKey, strTemplateData, locale, rootMap, false);
182 		}
183 		catch (NoSuchAlgorithmException | UnsupportedEncodingException hashEx) {
184 
185 			throw new LuteceFreemarkerException(
186 					"Can not create hash for template content " + strTemplateData + hashEx.getMessage(), hashEx);
187 
188 		}
189 
190 	}
191     
192     
193 
194     /**
195     * {@inheritDoc}
196    */
197     @Override
198 	public HtmlTemplate loadTemplateFromStringFtl(String strTemplateName,String strTemplateData, Locale locale, Object rootMap,boolean bResetCacheTemplate) {
199 	
200 
201 			Configuration cfg = _mapConfigurations.get(_strDefaultPath);
202 
203 			if (cfg == null) {
204 				cfg = initConfig(_strDefaultPath, Locale.getDefault());
205 			}
206 
207 			MultiTemplateLoader mtl = (MultiTemplateLoader) cfg.getTemplateLoader();
208 			if ( bResetCacheTemplate || ((StringTemplateLoader) mtl.getTemplateLoader(1)).findTemplateSource(strTemplateName) == null) {
209 				((StringTemplateLoader) mtl.getTemplateLoader(1)).putTemplate(strTemplateName, strTemplateData);
210 			}
211 
212 			return processTemplate(cfg, strTemplateName, rootMap, locale);
213 		
214 
215 	}
216     
217 
218     /**
219      * {@inheritDoc}
220      */
221     @Override
222     public void resetConfiguration( )
223     {
224         _mapConfigurations = new HashMap<>( );
225     }
226 
227     /**
228      * {@inheritDoc}
229      */
230     @Override
231     public void resetCache( )
232     {
233         for ( Configuration cfg : _mapConfigurations.values( ) )
234         {
235             cfg.clearTemplateCache( );
236         }
237     }
238 
239     /**
240      * Init a configuration using the current default path
241      * @param locale The Locale
242      */
243     public void initConfig( Locale locale )
244     {
245         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
246         if ( cfg == null )
247         {
248             initConfig( _strDefaultPath, locale );
249         }
250     }
251     
252     /**
253      * Initialize a configuration
254      * 
255      * @param strPath
256      *            The template's path
257      * @param locale
258      *            The locale
259      * @return A configuration object
260      */
261     private Configuration initConfig( String strPath, Locale locale )
262     {
263         try
264         {
265             Configuration cfg = buildConfiguration( locale );
266             // set the root directory for template loading
267             File directory = new File( this.getAbsolutePathFromRelativePath( strPath ) );
268             FileTemplateLoader ftl1 = new FileTemplateLoader( directory );
269             StringTemplateLoader stringLoader = new StringTemplateLoader( );
270 
271             TemplateLoader [ ] loaders = new TemplateLoader [ ] {
272                     ftl1, stringLoader
273             };
274             
275             MultiTemplateLoader mtl = new MultiTemplateLoader( loaders );
276             cfg.setTemplateLoader( mtl );
277             
278             
279             _mapConfigurations.put( strPath, cfg );
280             return cfg;
281         }
282         catch( IOException | TemplateException e )
283         {
284             throw new LuteceFreemarkerException( e.getMessage( ), e );
285         }
286 
287     }
288 
289     /**
290      * Build a configuration with default settings
291      * 
292      * @param locale
293      *            The given locale
294      * @return A configuration
295      * @throws TemplateException
296      *             if an error occurs
297      */
298     private Configuration buildConfiguration( Locale locale ) throws TemplateException
299     {
300         Version version = ( _bAcceptIncompatibleImprovements ) ? Configuration.VERSION_2_3_28 : Configuration.VERSION_2_3_0;
301         Configuration cfg =  new Configuration( version );
302 
303         // add core and plugin auto-includes such as macros
304         for ( String strFileName : _listPluginsAutoIncludes )
305         {
306             cfg.addAutoInclude( strFileName );
307         }
308 
309         // add core and plugin auto-imports
310         for ( Map.Entry<String, String> importEntry : _mapPluginsAutoImports.entrySet( ) )
311         {
312             cfg.addAutoImport( importEntry.getKey( ), importEntry.getValue( ) );
313         }
314 
315         for ( Entry<String, Object> entry : _mapSharedVariables.entrySet( ) )
316         {
317             cfg.setSharedVariable( entry.getKey( ), entry.getValue( ) );
318         }
319 
320         // activate lazy auto-imports to automatically import just really used templates 
321         cfg.setLazyAutoImports( true );
322 
323         // disable the localized look-up process to find a template
324         cfg.setLocalizedLookup( false );
325 
326         // keep control localized number formating (can cause pb on ids, and we don't want to use the ?c directive all the time)
327         cfg.setNumberFormat( NUMBER_FORMAT_PATTERN );
328 
329         // Used to set the default format to display a date and datetime
330         cfg.setSetting( SETTING_DATE_FORMAT, this.getDefaultPattern( locale ) );
331 
332         // Time in seconds that must elapse before checking whether there is a newer version of a template file
333         cfg.setTemplateUpdateDelayMilliseconds( ( ( long ) _nTemplateUpdateDelay ) * 1000L );
334         return cfg;
335     }
336 
337     /**
338      * Process the template transformation and return the {@link HtmlTemplate}
339      * 
340      * @param cfg
341      *            The Freemarker configuration to use
342      * @param strTemplate
343      *            The template name to call
344      * @param rootMap
345      *            The HashMap model
346      * @param locale
347      *            The {@link Locale}
348      * @return The {@link HtmlTemplate}
349      */
350     private HtmlTemplate processTemplate( Configuration cfg, String strTemplate, Object rootMap, Locale locale )
351     {
352         HtmlTemplate template = null;
353 
354         try
355         {
356             Template ftl;
357 
358             if ( locale == null )
359             {
360                 ftl = cfg.getTemplate( strTemplate );
361             }
362             else
363             {
364                 ftl = cfg.getTemplate( strTemplate, locale );
365             }
366 
367             StringWriter writer = new StringWriter( 1024 );
368             // Used to set the default format to display a date and datetime
369             ftl.setDateFormat( this.getDefaultPattern( locale ) );
370 
371             ftl.process( rootMap, writer );
372             template = new HtmlTemplate( writer.toString( ) );
373         }
374         catch( IOException | TemplateException e )
375         {
376             throw new LuteceFreemarkerException( e.getMessage( ), e );
377         }
378 
379         return template;
380     }
381 
382     /**
383      * {@inheritDoc}
384      */
385     @Override
386     public List<String> getAutoIncludes( )
387     {
388         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
389         if ( cfg == null )
390         {
391             cfg = initConfig( _strDefaultPath, Locale.getDefault( ) );
392         }
393         return cfg.getAutoIncludes( );
394     }
395 
396     /**
397      * {@inheritDoc}
398      */
399     @Override
400     public void addAutoInclude( String strFile )
401     {
402         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
403         if ( cfg != null )
404         {
405             cfg.addAutoInclude( strFile );
406         }
407     }
408 
409     /**
410      * {@inheritDoc}
411      */
412     @Override
413     public void removeAutoInclude( String strFile )
414     {
415         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
416         if ( cfg != null )
417         {
418             cfg.removeAutoInclude( strFile );
419         }
420     }
421 
422     /**
423      * {@inheritDoc}
424      */
425     @Override
426     public Map<String,String> getAutoImports( )
427     {
428         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
429         if ( cfg == null )
430         {
431             cfg = initConfig( _strDefaultPath, Locale.getDefault( ) );
432         }
433         return cfg.getAutoImports( );
434     }
435 
436     /**
437      * {@inheritDoc}
438      */
439     @Override
440     public void addAutoImport( String strNamespace, String strFile )
441     {
442         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
443         if ( cfg != null )
444         {
445             cfg.addAutoImport( strNamespace, strFile );
446         }
447     }
448 
449     /**
450      * {@inheritDoc}
451      */
452     @Override
453     public void removeAutoImport( String strNamespace )
454     {
455         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
456         if ( cfg != null )
457         {
458             cfg.removeAutoImport( strNamespace );
459         }
460     }
461     
462     
463     /**
464      * get hash
465      *
466      * @param message
467      * @param last
468      *            hash
469      *
470      * @return the hash in String
471      * @throws UnsupportedEncodingException 
472      * @throws NoSuchAlgorithmException 
473      */
474     private static String getHash( String message ) throws UnsupportedEncodingException, NoSuchAlgorithmException 
475     {
476 
477         byte [ ] byteChaine;
478         byteChaine = message.getBytes( CONSTANT_HASH_ENCODING );
479         MessageDigest md = MessageDigest.getInstance( CONSTANT_HASH_DIGEST );
480         byte [ ] hash = md.digest( byteChaine );
481 
482         // convert byte array to Hexadecimal String
483         StringBuilder sb = new StringBuilder( 2 * hash.length );
484         for ( byte b : hash )
485         {
486             sb.append( String.format( "%02x", b & 0xff ) );
487         }
488 
489           	return sb.toString( );
490 
491       
492 
493     }
494 
495 
496 
497 }