/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.libraries.base.boot; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.config.PropertyFileConfiguration; import org.pentaho.reporting.libraries.base.util.ObjectUtilities; import org.pentaho.reporting.libraries.base.util.PadMessage; import org.pentaho.reporting.libraries.base.util.StopWatch; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; /** * The PackageManager is used to load and configure the modules of JFreeReport. Modules are used to extend the basic * capabilities of JFreeReport by providing a simple plugin-interface. * <p/> * Modules provide a simple capability to remove unneeded functionality from the JFreeReport system and to reduce the * overall code size. The modularisation provides a very strict way of removing unnecessary dependencies beween the * various packages. * <p/> * The package manager can be used to add new modules to the system or to check the existence and state of installed * modules. * * @author Thomas Morgner */ public final class PackageManager { /** * The PackageConfiguration handles the module level configuration. * * @author Thomas Morgner */ public static class PackageConfiguration extends PropertyFileConfiguration { private static final long serialVersionUID = -2170306139946858878L; /** * DefaultConstructor. Creates a new package configuration. */ public PackageConfiguration() { // nothing required } } public class BootTimeEntry implements Comparable<BootTimeEntry> { private long time; private String name; public BootTimeEntry( final String name, final long time ) { if ( name == null ) { throw new NullPointerException( "Name must not be null" ); } this.name = name; this.time = time; } public int compareTo( final BootTimeEntry o ) { if ( time < o.time ) { return -1; } if ( time > o.time ) { return +1; } return name.compareTo( o.name ); } public boolean equals( final Object o ) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } final BootTimeEntry that = (BootTimeEntry) o; if ( time != that.time ) { return false; } if ( name != null ? !name.equals( that.name ) : that.name != null ) { return false; } return true; } public int hashCode() { int result = (int) ( time ^ ( time >>> 32 ) ); result = 31 * result + ( name != null ? name.hashCode() : 0 ); return result; } } private static final Log LOGGER = LogFactory.getLog( PackageManager.class ); /** * An internal constant declaring that the specified module was already loaded. */ private static final int RETURN_MODULE_LOADED = 0; /** * An internal constant declaring that the specified module is not known. */ private static final int RETURN_MODULE_UNKNOWN = 1; /** * An internal constant declaring that the specified module produced an error while loading. */ private static final int RETURN_MODULE_ERROR = 2; private static final boolean trackBootTime = false; /** * The module configuration instance that should be used to store module properties. This separates the user defined * properties from the implementation defined properties. */ private final PackageConfiguration packageConfiguration; /** * A list of all defined modules. */ private final ArrayList<PackageState> modules; /** * A list of module name definitions. */ private final ArrayList<String> initSections; private HashMap<String, PackageState> modulesByClass; /** * The boot implementation for which the modules are managed. */ private AbstractBoot booter; /** * Creates a new package manager. * * @param booter the booter (<code>null</code> not permitted). */ public PackageManager( final AbstractBoot booter ) { if ( booter == null ) { throw new NullPointerException(); } this.booter = booter; this.packageConfiguration = new PackageConfiguration(); this.modules = new ArrayList<PackageState>(); this.modulesByClass = new HashMap<String, PackageState>(); this.initSections = new ArrayList<String>(); } /** * Checks, whether a certain module is available. * * @param moduleDescription the module description of the desired module. * @return true, if the module is available and the version of the module is compatible, false otherwise. */ public boolean isModuleAvailable( final ModuleInfo moduleDescription ) { if ( moduleDescription == null ) { throw new NullPointerException(); } final PackageState[] packageStates = this.modules.toArray( new PackageState[ this.modules.size() ] ); for ( int i = 0; i < packageStates.length; i++ ) { final PackageState state = packageStates[ i ]; if ( state.getModule().getModuleClass().equals( moduleDescription.getModuleClass() ) ) { return ( state.getState() == PackageState.STATE_INITIALIZED ); } } return false; } /** * Checks whether the given module is available. The method returns true if the module is defined and has been * properly initialized. * * @param moduleClass the module class to be checked. * @return true, if the module is available and initialized, false otherwise. */ public boolean isModuleAvailable( final String moduleClass ) { if ( moduleClass == null ) { throw new NullPointerException(); } final PackageState state = modulesByClass.get( moduleClass ); if ( state == null ) { return false; } return state.getState() == PackageState.STATE_INITIALIZED; } /** * Loads all modules mentioned in the report configuration starting with the given prefix. This method is used during * the boot process of JFreeReport. You should never need to call this method directly. * * @param modulePrefix the module prefix. */ public void load( final String modulePrefix ) { if ( modulePrefix == null ) { throw new NullPointerException(); } if ( this.initSections.contains( modulePrefix ) ) { return; } this.initSections.add( modulePrefix ); final Configuration config = this.booter.getGlobalConfig(); final Iterator it = config.findPropertyKeys( modulePrefix ); int count = 0; while ( it.hasNext() ) { final String key = (String) it.next(); if ( key.endsWith( ".Module" ) ) { final String moduleClass = config.getConfigProperty( key ); if ( moduleClass != null && moduleClass.length() > 0 ) { addModule( moduleClass ); count++; } } } LOGGER.debug( "Loaded a total of " + count + " modules under prefix: " + modulePrefix ); } /** * Initializes all previously uninitialized modules. Once a module is initialized, it is not re-initialized a second * time. */ public synchronized void initializeModules() { final List<BootTimeEntry> times = new ArrayList<BootTimeEntry>(); // sort by subsystems and dependency PackageSorter.sort( this.modules ); for ( int i = 0; i < this.modules.size(); i++ ) { final PackageState mod = this.modules.get( i ); if ( isConfigurable( mod ) == false ) { mod.markError(); continue; } if ( mod.configure( this.booter ) ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( "Conf: " + new PadMessage( mod.getModule().getModuleClass(), 70 ) + " [" + mod.getModule().getSubSystem() + ']' ); } } } for ( int i = 0; i < this.modules.size(); i++ ) { final PackageState mod = this.modules.get( i ); if ( isInitializable( mod ) == false ) { mod.markError(); continue; } final StopWatch stopWatch = StopWatch.startNew(); if ( mod.initialize( this.booter ) ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.debug( "Init: " + new PadMessage( mod.getModule().getModuleClass(), 70 ) + " [" + mod.getModule().getSubSystem() + ']' ); } } times.add( new BootTimeEntry( mod.getModule().getModuleClass(), stopWatch.getElapsedTime() ) ); } if ( trackBootTime ) { Collections.sort( times ); LOGGER.debug( "Detailed Module boot times" ); long totalTime = 0; for ( final BootTimeEntry time : times ) { totalTime += time.time; LOGGER.debug( time.name + " - " + time.time ); } LOGGER.debug( "Total modules boot time: " + totalTime ); } } // 1290661000 // 4457704000 /** * Checks whether the module is configurable. A module is considered configurable if all dependencies exist and are * configured. * * @param state the package state that should be checked. * @return true, if the module can be configured, false otherwise. */ private boolean isConfigurable( final PackageState state ) { final ModuleInfo[] requiredModules = state.getModule().getRequiredModules(); for ( int i = 0; i < requiredModules.length; i++ ) { final ModuleInfo module = requiredModules[ i ]; final String key = module.getModuleClass(); final PackageState dependentState = modulesByClass.get( key ); if ( dependentState == null ) { LOGGER.warn( "Required dependency '" + key + "' for module '" + state.getModule().getModuleClass() + " not found." ); return false; } if ( dependentState.getState() != PackageState.STATE_CONFIGURED ) { LOGGER.warn( "Required dependency '" + key + "' for module '" + state.getModule().getModuleClass() + " not configured." ); return false; } } return true; } /** * Checks whether the module is configurable. A module is considered configurable if all dependencies exist and are * initialized. * * @param state the package state that should be checked. * @return true, if the module can be configured, false otherwise. */ private boolean isInitializable( final PackageState state ) { final ModuleInfo[] requiredModules = state.getModule().getRequiredModules(); for ( int i = 0; i < requiredModules.length; i++ ) { final ModuleInfo module = requiredModules[ i ]; final String key = module.getModuleClass(); final PackageState dependentState = modulesByClass.get( key ); if ( dependentState == null ) { LOGGER.warn( "Required dependency '" + key + "' for module '" + state.getModule().getModuleClass() + " not found." ); return false; } if ( dependentState.getState() != PackageState.STATE_INITIALIZED ) { LOGGER.warn( "Required dependency '" + key + "' for module '" + state.getModule().getModuleClass() + " not initializable." ); return false; } } return true; } /** * Adds a module to the package manager. Once all modules are added, you have to call initializeModules() to configure * and initialize the new modules. * * @param modClass the module class */ public synchronized void addModule( final String modClass ) { if ( modClass == null ) { throw new NullPointerException(); } final ArrayList<Module> loadModules = new ArrayList<Module>(); final ModuleInfo modInfo = new DefaultModuleInfo( modClass, null, null, null ); if ( loadModule( modInfo, new ArrayList<Module>(), loadModules, false ) ) { for ( int i = 0; i < loadModules.size(); i++ ) { final Module mod = loadModules.get( i ); final PackageState state = new PackageState( mod ); this.modules.add( state ); this.modulesByClass.put( mod.getModuleClass(), state ); } } } /** * Checks, whether the given module is already loaded in either the given tempModules list or the global package * registry. If tmpModules is null, only the previously installed modules are checked. * * @param tempModules a list of previously loaded modules. * @param module the module specification that is checked. * @return true, if the module is already loaded, false otherwise. */ private int containsModule( final ArrayList<Module> tempModules, final ModuleInfo module ) { if ( tempModules != null ) { final ModuleInfo[] mods = tempModules.toArray( new ModuleInfo[ tempModules.size() ] ); for ( int i = 0; i < mods.length; i++ ) { if ( mods[ i ].getModuleClass().equals( module.getModuleClass() ) ) { return RETURN_MODULE_LOADED; } } } final PackageState[] packageStates = this.modules.toArray( new PackageState[ this.modules.size() ] ); for ( int i = 0; i < packageStates.length; i++ ) { if ( packageStates[ i ].getModule().getModuleClass().equals( module.getModuleClass() ) ) { if ( packageStates[ i ].getState() == PackageState.STATE_ERROR ) { return RETURN_MODULE_ERROR; } else { return RETURN_MODULE_LOADED; } } } return RETURN_MODULE_UNKNOWN; } /** * A utility method that collects all failed modules. Such an module caused an error while being loaded, and is now * cached in case it is referenced elsewhere. * * @param state the failed module. */ private void dropFailedModule( final PackageState state ) { if ( this.modules.contains( state ) == false ) { this.modules.add( state ); } } /** * Tries to load a given module and all dependent modules. If the dependency check fails for that module (or for one * of the dependent modules), the loaded modules are discarded and no action is taken. * * @param moduleInfo the module info of the module that should be loaded. * @param incompleteModules a list of incompletly loaded modules. This are module specifications which depend on the * current module and wait for the module to be completly loaded. * @param modules the list of previously loaded modules for this module. * @param fatal a flag that states, whether the failure of loading a module should be considered an error. * Root-modules load errors are never fatal, as we try to load all known modules, regardless * whether they are active or not. * @return true, if the module was loaded successfully, false otherwise. */ private boolean loadModule( final ModuleInfo moduleInfo, final ArrayList<Module> incompleteModules, final ArrayList<Module> modules, final boolean fatal ) { try { final Module module = ObjectUtilities.loadAndInstantiate ( moduleInfo.getModuleClass(), booter.getClass(), Module.class ); if ( module == null ) { if ( fatal ) { LOGGER.warn( "Unresolved dependency for package: " + moduleInfo.getModuleClass() ); } LOGGER.debug( "Module class referenced, but not in classpath: " + moduleInfo.getModuleClass() ); return false; } if ( acceptVersion( moduleInfo, module ) == false ) { // module conflict! LOGGER.warn( "Module " + module.getName() + ": required version: " + moduleInfo + ", but found Version: \n" + module ); final PackageState state = new PackageState( module, PackageState.STATE_ERROR ); dropFailedModule( state ); return false; } final int moduleContained = containsModule( modules, module ); if ( moduleContained == RETURN_MODULE_ERROR ) { // the module caused harm before ... LOGGER.debug( "Indicated failure for module: " + module.getModuleClass() ); final PackageState state = new PackageState( module, PackageState.STATE_ERROR ); dropFailedModule( state ); return false; } else if ( moduleContained == RETURN_MODULE_UNKNOWN ) { if ( incompleteModules.contains( module ) ) { // we assume that loading will continue ... LOGGER.error ( "Circular module reference: This module definition is invalid: " + module.getClass() ); final PackageState state = new PackageState( module, PackageState.STATE_ERROR ); dropFailedModule( state ); return false; } incompleteModules.add( module ); final ModuleInfo[] required = module.getRequiredModules(); for ( int i = 0; i < required.length; i++ ) { if ( loadModule( required[ i ], incompleteModules, modules, true ) == false ) { LOGGER.debug( "Indicated failure for module: " + module.getModuleClass() ); final PackageState state = new PackageState( module, PackageState.STATE_ERROR ); dropFailedModule( state ); return false; } } final ModuleInfo[] optional = module.getOptionalModules(); for ( int i = 0; i < optional.length; i++ ) { if ( loadModule( optional[ i ], incompleteModules, modules, true ) == false ) { LOGGER.debug( "Optional module: " + optional[ i ].getModuleClass() + " was not loaded." ); } } // maybe a dependent module defined the same base module ... if ( containsModule( modules, module ) == RETURN_MODULE_UNKNOWN ) { modules.add( module ); } incompleteModules.remove( module ); } return true; } catch ( Exception e ) { LOGGER.warn( "Exception while loading module: " + moduleInfo, e ); return false; } } /** * Checks, whether the given module meets the requirements defined in the module information. * * @param moduleRequirement the required module specification. * @param module the module that should be checked against the specification. * @return true, if the module meets the given specifications, false otherwise. */ private boolean acceptVersion( final ModuleInfo moduleRequirement, final Module module ) { if ( moduleRequirement.getMajorVersion() == null ) { return true; } if ( module.getMajorVersion() == null ) { LOGGER.warn( "Module " + module.getName() + " does not define a major version." ); } else { final int compare = acceptVersion( moduleRequirement.getMajorVersion(), module.getMajorVersion() ); if ( compare > 0 ) { return false; } else if ( compare < 0 ) { return true; } } if ( moduleRequirement.getMinorVersion() == null ) { return true; } if ( module.getMinorVersion() == null ) { LOGGER.warn( "Module " + module.getName() + " does not define a minor version." ); } else { final int compare = acceptVersion( moduleRequirement.getMinorVersion(), module.getMinorVersion() ); if ( compare > 0 ) { return false; } else if ( compare < 0 ) { return true; } } if ( moduleRequirement.getPatchLevel() == null ) { return true; } if ( module.getPatchLevel() == null ) { LOGGER.debug( "Module " + module.getName() + " does not define a patch level." ); } else { if ( acceptVersion( moduleRequirement.getPatchLevel(), module.getPatchLevel() ) > 0 ) { LOGGER.debug( "Did not accept patchlevel: " + moduleRequirement.getPatchLevel() + " - " + module.getPatchLevel() ); return false; } } return true; } /** * Compare the version strings. If the strings have a different length, the shorter string is padded with spaces to * make them compareable. * * @param modVer the version string of the module * @param depModVer the version string of the dependent or optional module * @return 0, if the dependent module version is equal tothe module's required version, a negative number if the * dependent module is newer or a positive number if the dependent module is older and does not fit. */ private int acceptVersion( final String modVer, final String depModVer ) { final int mLength = Math.max( modVer.length(), depModVer.length() ); final char[] modVerArray; final char[] depVerArray; if ( modVer.length() > depModVer.length() ) { modVerArray = modVer.toCharArray(); depVerArray = new char[ mLength ]; final int delta = modVer.length() - depModVer.length(); Arrays.fill( depVerArray, 0, delta, ' ' ); System.arraycopy( depVerArray, delta, depModVer.toCharArray(), 0, depModVer.length() ); } else if ( modVer.length() < depModVer.length() ) { depVerArray = depModVer.toCharArray(); modVerArray = new char[ mLength ]; final char[] b1 = new char[ mLength ]; final int delta = depModVer.length() - modVer.length(); Arrays.fill( b1, 0, delta, ' ' ); System.arraycopy( b1, delta, modVer.toCharArray(), 0, modVer.length() ); } else { depVerArray = depModVer.toCharArray(); modVerArray = modVer.toCharArray(); } return new String( modVerArray ).compareTo( new String( depVerArray ) ); } /** * Returns the default package configuration. Private report configuration instances may be inserted here. These * inserted configuration can never override the settings from this package configuration. * * @return the package configuration. */ public PackageConfiguration getPackageConfiguration() { return this.packageConfiguration; } /** * Returns an array of the currently active modules. The module definition returned contain all known modules, * including buggy and unconfigured instances. * * @return the modules. */ public Module[] getAllModules() { final Module[] mods = new Module[ this.modules.size() ]; for ( int i = 0; i < this.modules.size(); i++ ) { final PackageState state = this.modules.get( i ); mods[ i ] = state.getModule(); } return mods; } /** * Returns all active modules. This array does only contain modules which were successfully configured and * initialized. * * @return the list of all active modules. */ public Module[] getActiveModules() { final ArrayList<Module> mods = new ArrayList<Module>(); for ( int i = 0; i < this.modules.size(); i++ ) { final PackageState state = this.modules.get( i ); if ( state.getState() == PackageState.STATE_INITIALIZED ) { mods.add( state.getModule() ); } } return mods.toArray( new Module[ mods.size() ] ); } /** * Prints the modules that are used. * * @param p the print stream. */ public void printUsedModules( final PrintStream p ) { final Module[] allMods = getAllModules(); final ArrayList<Module> activeModules = new ArrayList<Module>(); //final ArrayList failedModules = new ArrayList(); for ( int i = 0; i < allMods.length; i++ ) { if ( isModuleAvailable( allMods[ i ] ) ) { activeModules.add( allMods[ i ] ); } // else // { // failedModules.add(allMods[i]); // } } p.print( "Active modules: " ); p.println( activeModules.size() ); p.println( "----------------------------------------------------------" ); for ( int i = 0; i < activeModules.size(); i++ ) { final Module mod = activeModules.get( i ); p.print( new PadMessage( mod.getModuleClass(), 70 ) ); p.print( " [" ); p.print( mod.getSubSystem() ); p.println( "]" ); p.print( " Version: " ); p.print( mod.getMajorVersion() ); p.print( "-" ); p.print( mod.getMinorVersion() ); p.print( "-" ); p.print( mod.getPatchLevel() ); p.print( " Producer: " ); p.println( mod.getProducer() ); p.print( " Description: " ); p.println( mod.getDescription() ); } } }