View Javadoc
1   /*
2    * Copyright (c) 2002-2022, 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.spring;
35  
36  import java.io.File;
37  import java.io.FilenameFilter;
38  import java.util.ArrayList;
39  import java.util.Date;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  
44  import javax.servlet.ServletContext;
45  
46  import org.apache.commons.lang3.StringUtils;
47  import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
48  import org.springframework.context.ApplicationContext;
49  import org.springframework.context.support.AbstractApplicationContext;
50  import org.springframework.web.context.support.GenericWebApplicationContext;
51  
52  import fr.paris.lutece.portal.service.init.LuteceInitException;
53  import fr.paris.lutece.portal.service.plugin.Plugin;
54  import fr.paris.lutece.portal.service.plugin.PluginEvent;
55  import fr.paris.lutece.portal.service.plugin.PluginEventListener;
56  import fr.paris.lutece.portal.service.plugin.PluginService;
57  import fr.paris.lutece.portal.service.util.AppLogService;
58  import fr.paris.lutece.portal.service.util.AppPathService;
59  
60  /**
61   * This class provides a way to use Spring Framework ligthweight containers offering IoC (Inversion of Control) features.
62   * 
63   * @see <a href= "http://www.springframework.org">http://www.springframework.org</a>
64   */
65  public final class SpringContextService implements PluginEventListener
66  {
67      private static final String PROTOCOL_FILE = "file:";
68      private static final String PATH_CONF = "/WEB-INF/conf/";
69      private static final String DIR_PLUGINS = "plugins/";
70      private static final String DIR_OVERRIDE = "override/";
71      private static final String DIR_OVERRIDE_PLUGINS = DIR_OVERRIDE + DIR_PLUGINS;
72      private static final String SUFFIX_CONTEXT_FILE = "_context.xml";
73      private static final String FILE_CORE_CONTEXT = "core_context.xml";
74      private static ApplicationContext _context;
75      private static Map<Class, List> _mapBeansOfType = new HashMap<>( );
76      private static SpringContextServicepringContextService.html#SpringContextService">SpringContextService _instance = new SpringContextService( );
77  
78      /** Creates a new instance of SpringContextService */
79      private SpringContextService( )
80      {
81      }
82  
83      /**
84       * Return an instance, which may be shared or independent, of the given bean name. This method allows a Spring BeanFactory to be used as a replacement for
85       * the Singleton or Prototype design pattern.<br>
86       * The bean is retreived from the main context defined in the WEB-INF/conf/core_context.xml.
87       *
88       * @param <T>
89       *            the generic type
90       * @param strName
91       *            The bean's name
92       * @return The instance of the bean
93       */
94      public static <T> T getBean( String strName )
95      {
96          return (T) _context.getBean( strName );
97      }
98  
99      /**
100      * Return an instance of the given bean name loaded by the a Spring BeanFactory. The bean is retreived from a plugin context defined in the
101      * WEB-INF/conf/plugins/[plugin_name]_context.xml.
102      * 
103      * @param strPluginName
104      *            The Plugin's name
105      * @param strName
106      *            The bean's name
107      * @return The instance of the bean
108      * @deprecated use {@link #getBean(String)} instead
109      */
110     @Deprecated
111     public static Object getPluginBean( String strPluginName, String strName )
112     {
113         return _context.getBean( strName );
114     }
115 
116     /**
117      * Indicates if a bean, referenced by its name, is part of an enabled plugin.
118      * 
119      * Per Lutece convention, the plugin is determined from the bean's name prefix. If no prefix is present, the bean is considered enabled.
120      * 
121      * @param strBeanName
122      *            The bean's name
123      * @return <code>true</code> if the bean is part of an enabled plugin, <code>false</code> otherwise
124      */
125     public static boolean isBeanEnabled( String strBeanName )
126     {
127         String strPrefix = getPrefix( strBeanName );
128         return strPrefix == null || isEnabled( strPrefix );
129     }
130 
131     /**
132      * Initialize a global Application Context containing all beans (core + plugins) Now uses GenericApplicationContext for better performances. A wrong
133      * formatted file will not block block context to be built (without the file), but a wrong bean (i.e. cannot be instantiated) will cause a full context
134      * failure. Context is less "failure-friendly"
135      * 
136      * @param servletContext
137      *            The servlet context
138      * @throws LuteceInitException
139      *             The lutece init exception
140      * @since 2.4
141      */
142     public static void init( ServletContext servletContext ) throws LuteceInitException
143     {
144         try
145         {
146             // Register this service as a PluginEventListener
147             PluginService.registerPluginEventListener( _instance );
148 
149             // timing
150             Date dateBegin = new Date( );
151 
152             // Load the core context file : core_context.xml
153             String strConfPath = AppPathService.getAbsolutePathFromRelativePath( PATH_CONF );
154             String strContextFile = PROTOCOL_FILE + strConfPath + FILE_CORE_CONTEXT;
155 
156             GenericWebApplicationContext gwac = new GenericWebApplicationContext( servletContext );
157             gwac.setId( getContextName( servletContext ) );
158 
159             XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader( gwac );
160             xmlReader.loadBeanDefinitions( strContextFile );
161 
162             AppLogService.info( "Context file loaded : {}", FILE_CORE_CONTEXT );
163 
164             // Load all context files found in the conf/plugins directory
165             // Files are loaded separatly with an individual try/catch block
166             // to avoid stopping the process in case of a failure
167             // The global context generation will fail if a bean in any file cannot be
168             // built.
169             String strConfPluginsPath = strConfPath + DIR_PLUGINS;
170             File dirConfPlugins = new File( strConfPluginsPath );
171             FilenameFilter filterContext = new ContextFileFilter( );
172             String [ ] filesContext = dirConfPlugins.list( filterContext );
173 
174             loadContexts( filesContext, strConfPluginsPath, xmlReader );
175 
176             // we now load overriding beans
177             AppLogService.info( "Loading plugins context overrides" );
178 
179             String strCoreContextOverrideFile = strConfPath + DIR_OVERRIDE + FILE_CORE_CONTEXT;
180             File fileCoreContextOverride = new File( strCoreContextOverrideFile );
181 
182             if ( fileCoreContextOverride.exists( ) )
183             {
184                 AppLogService.debug( "Context file loaded : core_context" );
185                 xmlReader.loadBeanDefinitions( PROTOCOL_FILE + strCoreContextOverrideFile );
186             }
187             else
188             {
189                 AppLogService.debug( "No core_context override found" );
190             }
191 
192             // load plugins overrides
193             String strConfPluginsOverridePath = strConfPath + DIR_OVERRIDE_PLUGINS;
194             File dirConfOverridePlugins = new File( strConfPluginsOverridePath );
195 
196             if ( dirConfOverridePlugins.exists( ) )
197             {
198                 String [ ] filesOverrideContext = dirConfOverridePlugins.list( filterContext );
199                 loadContexts( filesOverrideContext, strConfPluginsOverridePath, xmlReader );
200             }
201 
202             gwac.refresh( );
203 
204             _context = gwac;
205 
206             AppLogService.info( "Spring context loaded in {} ms", ( ) -> ( new Date( ).getTime( ) - dateBegin.getTime( ) ) );
207         }
208         catch( Exception e )
209         {
210             AppLogService.error( "Error initializing Spring Context Service {}", e.getMessage( ), e );
211             throw new LuteceInitException( "Error initializing Spring Context Service", e );
212         }
213     }
214 
215     /**
216      * Returns a name for this context
217      * 
218      * @param servletContext
219      *            the servlet context
220      * @return name for this context
221      */
222     private static String getContextName( ServletContext servletContext )
223     {
224         String name = "lutece";
225 
226         if ( servletContext != null )
227         {
228             String contextName = servletContext.getServletContextName( );
229 
230             if ( contextName == null )
231             {
232                 contextName = servletContext.getContextPath( );
233             }
234 
235             if ( StringUtils.isNotBlank( contextName ) )
236             {
237                 name = contextName;
238             }
239         }
240 
241         return name;
242     }
243 
244     /**
245      * Loads plugins contexts.
246      * 
247      * @param filesContext
248      *            context files names
249      * @param strConfPluginsPath
250      *            full path
251      * @param xmlReader
252      *            the xml reader
253      */
254     private static void loadContexts( String [ ] filesContext, String strConfPluginsPath, XmlBeanDefinitionReader xmlReader )
255     {
256         if ( filesContext != null )
257         {
258             for ( String fileContext : filesContext )
259             {
260                 String [ ] file = {
261                         PROTOCOL_FILE + strConfPluginsPath + fileContext
262                 };
263 
264                 // Safe loading of plugin context file
265                 try
266                 {
267                     xmlReader.loadBeanDefinitions( file );
268                     AppLogService.info( "Context file loaded : {}", fileContext );
269                 }
270                 catch( Exception e )
271                 {
272                     AppLogService.error( "Unable to load Spring context file : {} - cause :  {}", fileContext, e.getMessage( ), e );
273                 }
274             }
275         }
276     }
277 
278     /**
279      * Gets the application context
280      *
281      * @return The application context
282      */
283     public static ApplicationContext getContext( )
284     {
285         return _context;
286     }
287 
288     /**
289      * Returns a list of bean among all that implements a given interface or extends a given class
290      * 
291      * @param <T>
292      *            The class type
293      * @param classDef
294      *            The class type
295      * @return A list of beans
296      */
297     public static <T> List<T> getBeansOfType( Class<T> classDef )
298     {
299         // Search the list in the cache
300         List<T> list = _mapBeansOfType.get( classDef );
301 
302         if ( list != null )
303         {
304             return new ArrayList<>( list );
305         }
306 
307         // The list is not in the cache, so we have to build it
308         list = new ArrayList<>( );
309 
310         Map<String, T> map = _context.getBeansOfType( classDef );
311         String [ ] sBeanNames = map.keySet( ).toArray( new String [ map.size( )] );
312 
313         for ( String strBeanName : sBeanNames )
314         {
315             String strPluginPrefix = getPrefix( strBeanName );
316 
317             if ( ( strPluginPrefix == null ) || ( isEnabled( strPluginPrefix ) ) )
318             {
319                 list.add( map.get( strBeanName ) );
320             }
321         }
322 
323         _mapBeansOfType.put( classDef, new ArrayList<>( list ) );
324 
325         return list;
326     }
327 
328     /**
329      * Gets the prefix of the bean (supposed to be the plugin name)
330      * 
331      * @param strBeanName
332      *            The bean name
333      * @return The prefix
334      */
335     private static String getPrefix( String strBeanName )
336     {
337         int nPos = strBeanName.indexOf( '.' );
338 
339         if ( nPos > 0 )
340         {
341             return strBeanName.substring( 0, nPos );
342         }
343 
344         return null;
345     }
346 
347     /**
348      * Analyze a bean prefix to tell if it matchs an activated plugin
349      * 
350      * @param strPrefix
351      *            The prefix of a bean
352      * @return True if the prefix matchs an activated plugin
353      */
354     private static boolean isEnabled( String strPrefix )
355     {
356         Plugin plugin = PluginService.getPlugin( strPrefix );
357 
358         return ( plugin != null ) && plugin.isInstalled( );
359     }
360 
361     /**
362      * {@inheritDoc }
363      */
364     @Override
365     public void processPluginEvent( PluginEvent event )
366     {
367         // Reset cache of beansOfType if a plugin is installed or uninstalled
368         if ( ( event.getEventType( ) == PluginEvent.PLUGIN_INSTALLED || event.getEventType( ) == PluginEvent.PLUGIN_UNINSTALLED )
369                 && !_mapBeansOfType.isEmpty( ) )
370         {
371             _mapBeansOfType.clear( );
372             AppLogService.info( "SpringService cache cleared due to a plugin installation change - Plugin : {}", event.getPlugin( ).getName( ) );
373         }
374     }
375 
376     /**
377      * Closes the Spring context
378      * 
379      * @since 5.1.0
380      */
381     public static void shutdown( )
382     {
383         if ( _context != null )
384         {
385             ( (AbstractApplicationContext) _context ).close( );
386         }
387     }
388 
389     /**
390      * Utils filename filter to identify context files
391      */
392     static class ContextFileFilter implements FilenameFilter
393     {
394         /**
395          * Filter filename
396          * 
397          * @param file
398          *            The current file
399          * @param strName
400          *            The file name
401          * @return true if the file is a context file otherwise false
402          */
403         @Override
404         public boolean accept( File file, String strName )
405         {
406             return strName.endsWith( SUFFIX_CONTEXT_FILE );
407         }
408     }
409 }