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> _listPluginsMacros = new ArrayList<>( );
75      private Map<String, Object> _mapSharedVariables = new HashMap<>( );
76      private Map<String, Configuration> _mapConfigurations = new HashMap<>( );
77      private String _strDefaultPath;
78      private int _nTemplateUpdateDelay;
79      private boolean _bAcceptIncompatibleImprovements;
80      
81  
82      /**
83       * {@inheritDoc}
84       */
85      @Override
86      public void setTemplateUpdateDelay( int nTemplateUpdateDelay )
87      {
88          _nTemplateUpdateDelay = nTemplateUpdateDelay;
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      @Override
95      public void addPluginMacros( String strFileName )
96      {
97          _listPluginsMacros.add( strFileName );
98      }
99  
100     /**
101      * {@inheritDoc}
102      */
103     @Override
104     public void setSharedVariable( String name, Object obj )
105     {
106         _mapSharedVariables.put( name, obj );
107     }
108 
109     /**
110      * {@inheritDoc}
111      */
112     @Override
113     public void init( String strTemplatePath )
114     {
115         _strDefaultPath = strTemplatePath;
116         _bAcceptIncompatibleImprovements = false;
117     }
118 
119     /**
120      * {@inheritDoc}
121      */
122     @Override
123     public void init( String strTemplatePath, boolean bAcceptIncompatibleImprovements )
124     {
125         _strDefaultPath = strTemplatePath;
126         _bAcceptIncompatibleImprovements = bAcceptIncompatibleImprovements;
127     }
128 
129     /**
130      * {@inheritDoc}
131      */
132     @Override
133     public HtmlTemplate loadTemplate( String strPath, String strTemplate )
134     {
135         return loadTemplate( strPath, strTemplate, null, null );
136     }
137 
138     /**
139      * {@inheritDoc}
140      */
141     @Override
142     public HtmlTemplate loadTemplate( String strPath, String strTemplate, Locale locale, Object rootMap )
143     {
144         Configuration cfg = _mapConfigurations.get( strPath );
145 
146         if ( cfg == null )
147         {
148             cfg = initConfig( _strDefaultPath, Locale.getDefault( ) );
149         }
150 
151         return processTemplate( cfg, strTemplate, rootMap, locale );
152     }
153 
154   
155     /**
156     * {@inheritDoc}
157    */
158     @Override
159 	public HtmlTemplate loadTemplateFromStringFtl(String strTemplateData, Locale locale, Object rootMap) {
160 		try {
161 			String strContentKey = getHash(strTemplateData);
162 			return loadTemplateFromStringFtl(strContentKey, strTemplateData, locale, rootMap, false);
163 		}
164 		catch (NoSuchAlgorithmException | UnsupportedEncodingException hashEx) {
165 
166 			throw new LuteceFreemarkerException(
167 					"Can not create hash for template content " + strTemplateData + hashEx.getMessage(), hashEx);
168 
169 		}
170 
171 	}
172     
173     
174 
175     /**
176     * {@inheritDoc}
177    */
178     @Override
179 	public HtmlTemplate loadTemplateFromStringFtl(String strTemplateName,String strTemplateData, Locale locale, Object rootMap,boolean bResetCacheTemplate) {
180 	
181 
182 			Configuration cfg = _mapConfigurations.get(_strDefaultPath);
183 
184 			if (cfg == null) {
185 				cfg = initConfig(_strDefaultPath, Locale.getDefault());
186 			}
187 
188 			MultiTemplateLoader mtl = (MultiTemplateLoader) cfg.getTemplateLoader();
189 			if ( bResetCacheTemplate || ((StringTemplateLoader) mtl.getTemplateLoader(1)).findTemplateSource(strTemplateName) == null) {
190 				((StringTemplateLoader) mtl.getTemplateLoader(1)).putTemplate(strTemplateName, strTemplateData);
191 			}
192 
193 			return processTemplate(cfg, strTemplateName, rootMap, locale);
194 		
195 
196 	}
197     
198 
199     /**
200      * {@inheritDoc}
201      */
202     @Override
203     public void resetConfiguration( )
204     {
205         _mapConfigurations = new HashMap<>( );
206     }
207 
208     /**
209      * {@inheritDoc}
210      */
211     @Override
212     public void resetCache( )
213     {
214         for ( Configuration cfg : _mapConfigurations.values( ) )
215         {
216             cfg.clearTemplateCache( );
217         }
218     }
219 
220     /**
221      * Init a configuration using the current default path
222      * @param locale The Locale
223      */
224     public void initConfig( Locale locale )
225     {
226         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
227         if ( cfg == null )
228         {
229             initConfig( _strDefaultPath, locale );
230         }
231     }
232     
233     /**
234      * Initialize a configuration
235      * 
236      * @param strPath
237      *            The template's path
238      * @param locale
239      *            The locale
240      * @return A configuration object
241      */
242     private Configuration initConfig( String strPath, Locale locale )
243     {
244         try
245         {
246             Configuration cfg = buildConfiguration( locale );
247             // set the root directory for template loading
248             File directory = new File( this.getAbsolutePathFromRelativePath( strPath ) );
249             FileTemplateLoader ftl1 = new FileTemplateLoader( directory );
250             StringTemplateLoader stringLoader = new StringTemplateLoader( );
251 
252             TemplateLoader [ ] loaders = new TemplateLoader [ ] {
253                     ftl1, stringLoader
254             };
255             
256             MultiTemplateLoader mtl = new MultiTemplateLoader( loaders );
257             cfg.setTemplateLoader( mtl );
258             
259             
260             _mapConfigurations.put( strPath, cfg );
261             return cfg;
262         }
263         catch( IOException | TemplateException e )
264         {
265             throw new LuteceFreemarkerException( e.getMessage( ), e );
266         }
267 
268     }
269 
270     /**
271      * Build a configuration with default settings
272      * 
273      * @param locale
274      *            The given locale
275      * @return A configuration
276      * @throws TemplateException
277      *             if an error occurs
278      */
279     private Configuration buildConfiguration( Locale locale ) throws TemplateException
280     {
281         Version version = ( _bAcceptIncompatibleImprovements ) ? Configuration.VERSION_2_3_28 : Configuration.VERSION_2_3_0;
282         Configuration cfg =  new Configuration( version );
283 
284         // add core and plugin auto-includes such as macros
285         for ( String strFileName : _listPluginsMacros )
286         {
287             cfg.addAutoInclude( strFileName );
288         }
289 
290         for ( Entry<String, Object> entry : _mapSharedVariables.entrySet( ) )
291         {
292             cfg.setSharedVariable( entry.getKey( ), entry.getValue( ) );
293         }
294 
295         // disable the localized look-up process to find a template
296         cfg.setLocalizedLookup( false );
297 
298         // keep control localized number formating (can cause pb on ids, and we don't want to use the ?c directive all the time)
299         cfg.setNumberFormat( NUMBER_FORMAT_PATTERN );
300 
301         // Used to set the default format to display a date and datetime
302         cfg.setSetting( SETTING_DATE_FORMAT, this.getDefaultPattern( locale ) );
303 
304         // Time in seconds that must elapse before checking whether there is a newer version of a template file
305         cfg.setTemplateUpdateDelayMilliseconds( ( ( long ) _nTemplateUpdateDelay ) * 1000L );
306         return cfg;
307     }
308 
309     /**
310      * Process the template transformation and return the {@link HtmlTemplate}
311      * 
312      * @param cfg
313      *            The Freemarker configuration to use
314      * @param strTemplate
315      *            The template name to call
316      * @param rootMap
317      *            The HashMap model
318      * @param locale
319      *            The {@link Locale}
320      * @return The {@link HtmlTemplate}
321      */
322     private HtmlTemplate processTemplate( Configuration cfg, String strTemplate, Object rootMap, Locale locale )
323     {
324         HtmlTemplate template = null;
325 
326         try
327         {
328             Template ftl;
329 
330             if ( locale == null )
331             {
332                 ftl = cfg.getTemplate( strTemplate );
333             }
334             else
335             {
336                 ftl = cfg.getTemplate( strTemplate, locale );
337             }
338 
339             StringWriter writer = new StringWriter( 1024 );
340             // Used to set the default format to display a date and datetime
341             ftl.setDateFormat( this.getDefaultPattern( locale ) );
342 
343             ftl.process( rootMap, writer );
344             template = new HtmlTemplate( writer.toString( ) );
345         }
346         catch( IOException | TemplateException e )
347         {
348             throw new LuteceFreemarkerException( e.getMessage( ), e );
349         }
350 
351         return template;
352     }
353 
354     /**
355      * {@inheritDoc}
356      */
357     @Override
358     public List<String> getAutoIncludes( )
359     {
360         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
361         if ( cfg == null )
362         {
363             cfg = initConfig( _strDefaultPath, Locale.getDefault( ) );
364         }
365         return cfg.getAutoIncludes( );
366     }
367 
368     /**
369      * {@inheritDoc}
370      */
371     @Override
372     public void addAutoInclude( String strFile )
373     {
374         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
375         if ( cfg != null )
376         {
377             cfg.addAutoInclude( strFile );
378         }
379     }
380 
381     /**
382      * {@inheritDoc}
383      */
384     @Override
385     public void removeAutoInclude( String strFile )
386     {
387         Configuration cfg = _mapConfigurations.get( _strDefaultPath );
388         if ( cfg != null )
389         {
390             cfg.removeAutoInclude( strFile );
391         }
392     }
393     
394     
395     /**
396      * get hash
397      *
398      * @param message
399      * @param last
400      *            hash
401      *
402      * @return the hash in String
403      * @throws UnsupportedEncodingException 
404      * @throws NoSuchAlgorithmException 
405      */
406     private static String getHash( String message ) throws UnsupportedEncodingException, NoSuchAlgorithmException 
407     {
408 
409         byte [ ] byteChaine;
410         byteChaine = message.getBytes( CONSTANT_HASH_ENCODING );
411         MessageDigest md = MessageDigest.getInstance( CONSTANT_HASH_DIGEST );
412         byte [ ] hash = md.digest( byteChaine );
413 
414         // convert byte array to Hexadecimal String
415         StringBuilder sb = new StringBuilder( 2 * hash.length );
416         for ( byte b : hash )
417         {
418             sb.append( String.format( "%02x", b & 0xff ) );
419         }
420 
421           	return sb.toString( );
422 
423       
424 
425     }
426 
427 
428 
429 }