SpringContextService.java

  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. import java.io.File;
  36. import java.io.FilenameFilter;
  37. import java.util.ArrayList;
  38. import java.util.Date;
  39. import java.util.HashMap;
  40. import java.util.List;
  41. import java.util.Map;

  42. import javax.servlet.ServletContext;

  43. import org.apache.commons.lang3.StringUtils;
  44. import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
  45. import org.springframework.context.ApplicationContext;
  46. import org.springframework.context.support.AbstractApplicationContext;
  47. import org.springframework.web.context.support.GenericWebApplicationContext;

  48. import fr.paris.lutece.portal.service.init.LuteceInitException;
  49. import fr.paris.lutece.portal.service.plugin.Plugin;
  50. import fr.paris.lutece.portal.service.plugin.PluginEvent;
  51. import fr.paris.lutece.portal.service.plugin.PluginEventListener;
  52. import fr.paris.lutece.portal.service.plugin.PluginService;
  53. import fr.paris.lutece.portal.service.util.AppLogService;
  54. import fr.paris.lutece.portal.service.util.AppPathService;

  55. /**
  56.  * This class provides a way to use Spring Framework ligthweight containers offering IoC (Inversion of Control) features.
  57.  *
  58.  * @see <a href= "http://www.springframework.org">http://www.springframework.org</a>
  59.  */
  60. public final class SpringContextService implements PluginEventListener
  61. {
  62.     private static final String PROTOCOL_FILE = "file:";
  63.     private static final String PATH_CONF = "/WEB-INF/conf/";
  64.     private static final String DIR_PLUGINS = "plugins/";
  65.     private static final String DIR_OVERRIDE = "override/";
  66.     private static final String DIR_OVERRIDE_PLUGINS = DIR_OVERRIDE + DIR_PLUGINS;
  67.     private static final String SUFFIX_CONTEXT_FILE = "_context.xml";
  68.     private static final String FILE_CORE_CONTEXT = "core_context.xml";
  69.     private static ApplicationContext _context;
  70.     private static Map<Class, List> _mapBeansOfType = new HashMap<>( );
  71.     private static SpringContextService _instance = new SpringContextService( );

  72.     /** Creates a new instance of SpringContextService */
  73.     private SpringContextService( )
  74.     {
  75.     }

  76.     /**
  77.      * 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
  78.      * the Singleton or Prototype design pattern.<br>
  79.      * The bean is retreived from the main context defined in the WEB-INF/conf/core_context.xml.
  80.      *
  81.      * @param <T>
  82.      *            the generic type
  83.      * @param strName
  84.      *            The bean's name
  85.      * @return The instance of the bean
  86.      */
  87.     public static <T> T getBean( String strName )
  88.     {
  89.         return (T) _context.getBean( strName );
  90.     }

  91.     /**
  92.      * 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
  93.      * WEB-INF/conf/plugins/[plugin_name]_context.xml.
  94.      *
  95.      * @param strPluginName
  96.      *            The Plugin's name
  97.      * @param strName
  98.      *            The bean's name
  99.      * @return The instance of the bean
  100.      * @deprecated use {@link #getBean(String)} instead
  101.      */
  102.     @Deprecated
  103.     public static Object getPluginBean( String strPluginName, String strName )
  104.     {
  105.         return _context.getBean( strName );
  106.     }

  107.     /**
  108.      * Indicates if a bean, referenced by its name, is part of an enabled plugin.
  109.      *
  110.      * Per Lutece convention, the plugin is determined from the bean's name prefix. If no prefix is present, the bean is considered enabled.
  111.      *
  112.      * @param strBeanName
  113.      *            The bean's name
  114.      * @return <code>true</code> if the bean is part of an enabled plugin, <code>false</code> otherwise
  115.      */
  116.     public static boolean isBeanEnabled( String strBeanName )
  117.     {
  118.         String strPrefix = getPrefix( strBeanName );
  119.         return strPrefix == null || isEnabled( strPrefix );
  120.     }

  121.     /**
  122.      * Initialize a global Application Context containing all beans (core + plugins) Now uses GenericApplicationContext for better performances. A wrong
  123.      * 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
  124.      * failure. Context is less "failure-friendly"
  125.      *
  126.      * @param servletContext
  127.      *            The servlet context
  128.      * @throws LuteceInitException
  129.      *             The lutece init exception
  130.      * @since 2.4
  131.      */
  132.     public static void init( ServletContext servletContext ) throws LuteceInitException
  133.     {
  134.         try
  135.         {
  136.             // Register this service as a PluginEventListener
  137.             PluginService.registerPluginEventListener( _instance );

  138.             // timing
  139.             Date dateBegin = new Date( );

  140.             // Load the core context file : core_context.xml
  141.             String strConfPath = AppPathService.getAbsolutePathFromRelativePath( PATH_CONF );
  142.             String strContextFile = PROTOCOL_FILE + strConfPath + FILE_CORE_CONTEXT;

  143.             GenericWebApplicationContext gwac = new GenericWebApplicationContext( servletContext );
  144.             gwac.setId( getContextName( servletContext ) );

  145.             XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader( gwac );
  146.             xmlReader.loadBeanDefinitions( strContextFile );

  147.             AppLogService.info( "Context file loaded : {}", FILE_CORE_CONTEXT );

  148.             // Load all context files found in the conf/plugins directory
  149.             // Files are loaded separatly with an individual try/catch block
  150.             // to avoid stopping the process in case of a failure
  151.             // The global context generation will fail if a bean in any file cannot be
  152.             // 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.             loadContexts( filesContext, strConfPluginsPath, xmlReader );

  158.             // we now load overriding beans
  159.             AppLogService.info( "Loading plugins context overrides" );

  160.             String strCoreContextOverrideFile = strConfPath + DIR_OVERRIDE + FILE_CORE_CONTEXT;
  161.             File fileCoreContextOverride = new File( strCoreContextOverrideFile );

  162.             if ( fileCoreContextOverride.exists( ) )
  163.             {
  164.                 AppLogService.debug( "Context file loaded : core_context" );
  165.                 xmlReader.loadBeanDefinitions( PROTOCOL_FILE + strCoreContextOverrideFile );
  166.             }
  167.             else
  168.             {
  169.                 AppLogService.debug( "No core_context override found" );
  170.             }

  171.             // load plugins overrides
  172.             String strConfPluginsOverridePath = strConfPath + DIR_OVERRIDE_PLUGINS;
  173.             File dirConfOverridePlugins = new File( strConfPluginsOverridePath );

  174.             if ( dirConfOverridePlugins.exists( ) )
  175.             {
  176.                 String [ ] filesOverrideContext = dirConfOverridePlugins.list( filterContext );
  177.                 loadContexts( filesOverrideContext, strConfPluginsOverridePath, xmlReader );
  178.             }

  179.             gwac.refresh( );

  180.             _context = gwac;

  181.             AppLogService.info( "Spring context loaded in {} ms", ( ) -> ( new Date( ).getTime( ) - dateBegin.getTime( ) ) );
  182.         }
  183.         catch( Exception e )
  184.         {
  185.             AppLogService.error( "Error initializing Spring Context Service {}", e.getMessage( ), e );
  186.             throw new LuteceInitException( "Error initializing Spring Context Service", e );
  187.         }
  188.     }

  189.     /**
  190.      * Returns a name for this context
  191.      *
  192.      * @param servletContext
  193.      *            the servlet context
  194.      * @return name for this context
  195.      */
  196.     private static String getContextName( ServletContext servletContext )
  197.     {
  198.         String name = "lutece";

  199.         if ( servletContext != null )
  200.         {
  201.             String contextName = servletContext.getServletContextName( );

  202.             if ( contextName == null )
  203.             {
  204.                 contextName = servletContext.getContextPath( );
  205.             }

  206.             if ( StringUtils.isNotBlank( contextName ) )
  207.             {
  208.                 name = contextName;
  209.             }
  210.         }

  211.         return name;
  212.     }

  213.     /**
  214.      * Loads plugins contexts.
  215.      *
  216.      * @param filesContext
  217.      *            context files names
  218.      * @param strConfPluginsPath
  219.      *            full path
  220.      * @param xmlReader
  221.      *            the xml reader
  222.      */
  223.     private static void loadContexts( String [ ] filesContext, String strConfPluginsPath, XmlBeanDefinitionReader xmlReader )
  224.     {
  225.         if ( filesContext != null )
  226.         {
  227.             for ( String fileContext : filesContext )
  228.             {
  229.                 String [ ] file = {
  230.                         PROTOCOL_FILE + strConfPluginsPath + fileContext
  231.                 };

  232.                 // Safe loading of plugin context file
  233.                 try
  234.                 {
  235.                     xmlReader.loadBeanDefinitions( file );
  236.                     AppLogService.info( "Context file loaded : {}", fileContext );
  237.                 }
  238.                 catch( Exception e )
  239.                 {
  240.                     AppLogService.error( "Unable to load Spring context file : {} - cause :  {}", fileContext, e.getMessage( ), e );
  241.                 }
  242.             }
  243.         }
  244.     }

  245.     /**
  246.      * Gets the application context
  247.      *
  248.      * @return The application context
  249.      */
  250.     public static ApplicationContext getContext( )
  251.     {
  252.         return _context;
  253.     }

  254.     /**
  255.      * Returns a list of bean among all that implements a given interface or extends a given class
  256.      *
  257.      * @param <T>
  258.      *            The class type
  259.      * @param classDef
  260.      *            The class type
  261.      * @return A list of beans
  262.      */
  263.     public static <T> List<T> getBeansOfType( Class<T> classDef )
  264.     {
  265.         // Search the list in the cache
  266.         List<T> list = _mapBeansOfType.get( classDef );

  267.         if ( list != null )
  268.         {
  269.             return new ArrayList<>( list );
  270.         }

  271.         // The list is not in the cache, so we have to build it
  272.         list = new ArrayList<>( );

  273.         Map<String, T> map = _context.getBeansOfType( classDef );
  274.         String [ ] sBeanNames = map.keySet( ).toArray( new String [ map.size( )] );

  275.         for ( String strBeanName : sBeanNames )
  276.         {
  277.             String strPluginPrefix = getPrefix( strBeanName );

  278.             if ( ( strPluginPrefix == null ) || ( isEnabled( strPluginPrefix ) ) )
  279.             {
  280.                 list.add( map.get( strBeanName ) );
  281.             }
  282.         }

  283.         _mapBeansOfType.put( classDef, new ArrayList<>( list ) );

  284.         return list;
  285.     }

  286.     /**
  287.      * Gets the prefix of the bean (supposed to be the plugin name)
  288.      *
  289.      * @param strBeanName
  290.      *            The bean name
  291.      * @return The prefix
  292.      */
  293.     private static String getPrefix( String strBeanName )
  294.     {
  295.         int nPos = strBeanName.indexOf( '.' );

  296.         if ( nPos > 0 )
  297.         {
  298.             return strBeanName.substring( 0, nPos );
  299.         }

  300.         return null;
  301.     }

  302.     /**
  303.      * Analyze a bean prefix to tell if it matchs an activated plugin
  304.      *
  305.      * @param strPrefix
  306.      *            The prefix of a bean
  307.      * @return True if the prefix matchs an activated plugin
  308.      */
  309.     private static boolean isEnabled( String strPrefix )
  310.     {
  311.         Plugin plugin = PluginService.getPlugin( strPrefix );

  312.         return ( plugin != null ) && plugin.isInstalled( );
  313.     }

  314.     /**
  315.      * {@inheritDoc }
  316.      */
  317.     @Override
  318.     public void processPluginEvent( PluginEvent event )
  319.     {
  320.         // Reset cache of beansOfType if a plugin is installed or uninstalled
  321.         if ( ( event.getEventType( ) == PluginEvent.PLUGIN_INSTALLED || event.getEventType( ) == PluginEvent.PLUGIN_UNINSTALLED )
  322.                 && !_mapBeansOfType.isEmpty( ) )
  323.         {
  324.             _mapBeansOfType.clear( );
  325.             AppLogService.info( "SpringService cache cleared due to a plugin installation change - Plugin : {}", event.getPlugin( ).getName( ) );
  326.         }
  327.     }

  328.     /**
  329.      * Closes the Spring context
  330.      *
  331.      * @since 5.1.0
  332.      */
  333.     public static void shutdown( )
  334.     {
  335.         if ( _context != null )
  336.         {
  337.             ( (AbstractApplicationContext) _context ).close( );
  338.         }
  339.     }

  340.     /**
  341.      * Utils filename filter to identify context files
  342.      */
  343.     static class ContextFileFilter implements FilenameFilter
  344.     {
  345.         /**
  346.          * Filter filename
  347.          *
  348.          * @param file
  349.          *            The current file
  350.          * @param strName
  351.          *            The file name
  352.          * @return true if the file is a context file otherwise false
  353.          */
  354.         @Override
  355.         public boolean accept( File file, String strName )
  356.         {
  357.             return strName.endsWith( SUFFIX_CONTEXT_FILE );
  358.         }
  359.     }
  360. }