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