/* * Copyright 2001-2013 Geert Bevin (gbevin[remove] at uwyn dot com) * Licensed under the Apache License, Version 2.0 (the "License") */ package com.uwyn.rife.rep; import com.uwyn.rife.ioc.HierarchicalProperties; import com.uwyn.rife.rep.exceptions.BlockingParticipantExpectedException; import com.uwyn.rife.rep.exceptions.RepException; import com.uwyn.rife.resources.ResourceFinder; import com.uwyn.rife.resources.ResourceFinderClasspath; import com.uwyn.rife.tools.ExceptionUtils; import java.util.*; import java.util.logging.Logger; /** * The <code>BlockingRepository</code> class provides a * <code>Repository</code> implementation that loads the participants from an * XML file. * <p>This file defaults to <code>rep/participants.xml</code>, but it can be * overridden by providing another filename to the <code>{@link * #initialize(String, ResourceFinder) initialize}</code> method. The * participants are initialized according to their listed order. * <p>Following is an example of such an XML file : * <pre><?xml version="1.0" encoding="UTF-8"?> * <!DOCTYPE rep SYSTEM "/dtd/rep.dtd"> * <rep> * <participant blocking="true" parameter="rep/config.xml">ParticipantConfig</participant> * <participant blocking="false" parameter="graphics/buttons/">ParticipantImages</participant> * <participant name="my cursors" blocking="false" parameter="graphics/cursors/">ParticipantCursors</participant> * </rep></pre> * <p>Each participant has a <code>blocking</code> attribute that determines * whether the repository should wait for the end of the participant's * initialization before progressing to the next participant or not. Using * this intelligently, it's possible to dramatically increase the perceived * startup time of an application. * <p>Optionally a participant can have a <code>name</code> attribute which * makes it possible to declare multiple participants of the same class. If no * name is provided, the participant's class name will be used to identify the * declared participant. * <p>Optionally a participant can also have a <code>parameter</code> * attribute which is merely a <code>String</code> that is provided to the * participant object for configuration purposes. * <p>Listeners can be added to the repository to receive notifications about * the initialization advancement of the participants and to know when the * initialization has completely finished. These notifications can, for * example, be used to display a progress bar in a splash window and to switch * to the real application window when the initialization has finished. * <p>The JDK's logging facility is used to output informative text during the * advancement of the initialization. Each participant has to provide an * initialization message that will be output. * * @author Geert Bevin (gbevin[remove] at uwyn dot com) * @see RepositoryListener * @see BlockingParticipant * @since 1.0 */ public class BlockingRepository extends ThreadGroup implements Repository { // TODO // /** // * The default path of the repository participant XML file is // * <code>rep/participants.xml</code>. It can be overridden by setting the // * <code>rep.path</code> system property. // */ // private static final String DEFAULT_REP_PATH = "rep/participants.xml"; /** * Object that is used for thread synchronization of the finish. */ private final Object finishedThreadMonitor = new Object(); /** * Object that is used for thread synchronization of the cleanup. */ private final Object cleanupThreadMonitor = new Object(); /** * A map of the registered participants, indexed according to their name. */ private Map<String, BlockingParticipant> repParticipants = new HashMap<>(); /** * A map of the registered participants class name with all the registered * participants. */ private Map<String, ArrayList<BlockingParticipant>> repParticipantClassnames = new HashMap<>(); /** * A list of the participant names, ordered according to their * registration moment. This will be used as the order of execution. */ private List<String> repParticipantsOrder = new ArrayList<>(); /** * The list of participant names that will be use to determine which * participant's initialization end the repository has to wait for. */ private List<String> repParticipantsToWaitFor = new ArrayList<>(); /** * The list of all registered <code>RepListener</code> objects. */ private Set<RepositoryListener> repListeners = new HashSet<>(); /** * Indicates whether the initialization is finished. */ private boolean finished = false; /** * Indicates whether the repository has been cleaned up. */ private boolean cleanedUp = false; /** * Exception that is thrown by a participant. */ private Throwable participantException = null; /** * The repository's properties. */ private HierarchicalProperties properties; /** * The repository's context. */ private Object context; /** * Default constructor without a repository context. */ public BlockingRepository() { this(null); } /** * Constructor which sets up a the context in which the repository is initialized. */ public BlockingRepository(Object context) { super("BlockingRepository"); HierarchicalProperties system_properties = new HierarchicalProperties().putAll(System.getProperties()); properties = new HierarchicalProperties().parent(system_properties); this.context = context; } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name for the name of the participant. * <p>The participant will not be blocking and have no parameter. * * @param className The fully resolved name of the participant's class, or * only the class name if the participant resides in the * <code>com.uwyn.rife.rep.participants</code> package. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(String className) throws RepException { return addParticipant(className, null, false, null); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name for the name of the participant. * <p>The participant will have no parameter. * * @param className The fully resolved name of the participant's class, or * only the class name if the participant resides in the * <code>com.uwyn.rife.rep.participants</code> package. * @param blocking Indicates if this a blocking participant or not. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(String className, boolean blocking) throws RepException { return addParticipant(className, null, blocking, null); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name for the name of the participant. * <p>The participant will not be blocking. * * @param className The fully resolved name of the participant's class, or * only the class name if the participant resides in the * <code>com.uwyn.rife.rep.participants</code> package. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(String className, String parameter) throws RepException { return addParticipant(className, null, false, parameter); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name for the name of the participant. * * @param className The fully resolved name of the participant's class, or * only the class name if the participant resides in the * <code>com.uwyn.rife.rep.participants</code> package. * @param blocking Indicates if this a blocking participant or not. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(String className, boolean blocking, String parameter) throws RepException { return addParticipant(className, null, blocking, parameter); } /** * Adds a <code>BlockingParticipant</code> to the repository. * * @param className The fully resolved name of the participant's class, or * only the class name if the participant resides in the * <code>com.uwyn.rife.rep.participants</code> package. * @param name The name under which the participant will be registered, if * the name is <code>null</code> the class name will be used * @param blocking Indicates if this a blocking participant or not. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.0 */ public boolean addParticipant(String className, String name, boolean blocking, String parameter) throws RepException { // Try to resolve the participant's classname to ensure that an // object can be instantiated. Class klass; try { klass = Class.forName(className); } catch (ClassNotFoundException e1) { className = "com.uwyn.rife.rep.participants." + className; try { klass = Class.forName(className); } catch (ClassNotFoundException e2) { return false; } } return addParticipant(klass, name, blocking, parameter); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name as the name of the participant. * * @param klass The class of the participant. * @param blocking Indicates if this a blocking participant or not. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(Class klass, boolean blocking, String parameter) throws RepException { return addParticipant(klass, null, blocking, parameter); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name as the name of the participant. * <p>The participant will not be blocking and have no parameter. * * @param klass The class of the participant. * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(Class klass) throws RepException { return addParticipant(klass, null, false, null); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name as the name of the participant. * <p>The participant will have no parameter. * * @param klass The class of the participant. * @param blocking Indicates if this a blocking participant or not. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(Class klass, boolean blocking) throws RepException { return addParticipant(klass, null, blocking, null); } /** * Adds a <code>BlockingParticipant</code> to the repository, using the * class name as the name of the participant. * <p>The participant will not be blocking. * * @param klass The class of the participant. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @see #addParticipant(Class, String, boolean, String) * @since 1.5 */ public boolean addParticipant(Class klass, String parameter) throws RepException { return addParticipant(klass, null, false, parameter); } /** * Adds a <code>BlockingParticipant</code> to the repository. * * @param klass The class of the participant. * @param name The name under which the participant will be registered, if * the name is <code>null</code> the class name will be used * @param blocking Indicates if this a blocking participant or not. * @param parameter An optional string that contains the parameter for * this participant. * @return <code>true</code> if the participants was added successfully; * or * <p><code>false</code> if errors occurred * @see BlockingParticipant * @see Participant * @since 1.0 */ public boolean addParticipant(Class klass, String name, boolean blocking, String parameter) throws RepException { boolean generated_name = false; if (null == name || 0 == name.length()) { name = klass.getName(); generated_name = true; } // Check if the participant isn't already present in the repository. if (repParticipants.containsKey(name)) { if (!generated_name) { return false; } String new_name; int counter = 2; do { new_name = name + counter; counter++; } while (repParticipants.containsKey(new_name)); name = new_name; } try { // Try to create an instance of the participant. BlockingParticipant participant_instance; if (!BlockingParticipant.class.isAssignableFrom(klass)) { if (Participant.class.isAssignableFrom(klass)) { participant_instance = new BlockingParticipantDelegate(klass); } else { throw new BlockingParticipantExpectedException(klass.getName()); } } else { participant_instance = (BlockingParticipant)klass.newInstance(); } // Sets the name of the blocking participant thread to the one // it will be registered with participant_instance.setName(name); // Setup the thread Thread thread = new Thread(this, participant_instance); participant_instance.setThread(thread); // Store the participant and its name, remember it's execution order // according to other participants and remember whether this // participant's initialization should be finished before // continuing the repository initialization. // Regardless of their names, all the participants are already // registered with their class name. // Store its optional parameter. participant_instance.setRepository(this); repParticipants.put(name, participant_instance); ArrayList<BlockingParticipant> participants = repParticipantClassnames.get(klass.getName()); if (null == participants) { participants = new ArrayList<>(); repParticipantClassnames.put(klass.getName(), participants); } participants.add(participant_instance); repParticipantsOrder.add(name); if (blocking) { repParticipantsToWaitFor.add(name); } if (null != parameter) { participant_instance.setParameter(parameter); } return true; } catch (IllegalAccessException e) { return false; } catch (InstantiationException e) { return false; } } /** * Verifies if a participant that corresponds to a given name is present. * * @param name The name of the participant object that you wish to * retrieve from the repository. See the {@link #getParticipant(String) * getParticipant} method for detailed information about how the * participant's name is resolved. * @return <code>true</code> if the provided class name could be found, or * <p><code>false</code> otherwise * @see BlockingParticipant * @see #getParticipant(String) * @since 1.0 */ public boolean hasParticipant(String name) { BlockingParticipant participant = getParticipant(name); return null != participant && !participant.hadInitializationError(); } /** * Looks for the participant that corresponds to a given name and returns * it when found. * * @param name The name of the participant instance that you wish to * retrieve from the repository. * <p>If no name was provided during the XML specification, the * participant will have been registered with its class name. If the * participant's class is not part of the * <code>com.uwyn.rife.rep.participants</code> package, its full class * name has to be provided, otherwise just the name of the class itself is * sufficient. * <p>Also, even though a participant has been registered with a name, * it'll still be known under its class name. When a class name is * provided as the argument, the first known participant of that class * will be returned. This can be seen as the default participant for the * specified type. * @return A <code>BlockingParticipant</code> instance if the provided * name could be found amongst the registered participants in the * repository; or * <p><code>null</code> if the participant couldn't be found * @see BlockingParticipant * @see #hasParticipant(String) * @since 1.0 */ public BlockingParticipant getParticipant(String name) { // check if the provided participant name is present in the repository if (repParticipants.containsKey(name)) { return repParticipants.get(name); } // check if the provided participant name can be expanded to a name that // is known by the repository String prefixed_name = "com.uwyn.rife.rep.participants." + name; if (repParticipants.containsKey(prefixed_name)) { return repParticipants.get(prefixed_name); } // check if the provided participant name has been remembered as the // first participant of a certain class if (repParticipantClassnames.containsKey(name)) { ArrayList<BlockingParticipant> participants = repParticipantClassnames.get(name); if (participants.size() > 0) { return participants.get(0); } } if (repParticipantClassnames.containsKey(prefixed_name)) { ArrayList<BlockingParticipant> participants = repParticipantClassnames.get(prefixed_name); if (participants.size() > 0) { return participants.get(0); } } return null; } /** * Returns all the participants with a given class name * * @param className The class name of the participants that you wish to * retrieve from the repository. * <p>If the participant's class is not part of the * <code>com.uwyn.rife.rep.participants</code> package, its full class * name has to be provided, otherwise just the name of the class itself is * sufficient. * @return A <code>Collection</code> of <code>BlockingParticipant</code> * instances of the provided class name; or * <p><code>null</code> if no participants with the provided class name * could be found * @see BlockingParticipant * @see #getParticipant(String) * @since 1.0 */ public Collection<BlockingParticipant> getParticipants(String className) { // check if the provided participant name has been remembered as the // first participant of a certain class if (repParticipantClassnames.containsKey(className)) { ArrayList<BlockingParticipant> participants = repParticipantClassnames.get(className); if (participants.size() > 0) { return participants; } } // check if the provided participant class name can be expanded to a // name that is known by the repository String prefixed_name = "com.uwyn.rife.rep.participants." + className; if (repParticipantClassnames.containsKey(prefixed_name)) { ArrayList<BlockingParticipant> participants = repParticipantClassnames.get(prefixed_name); if (participants.size() > 0) { return participants; } } return null; } /** * Sequentially execute the participants according to their registration * order. If the participant has already been run or is still running, it * is not executed anymore. The repository waits for the participant's * execution to finish if this has been indicated by registering with the * <code>blocking</code> attribute. * <p>The resource finder that will be used is an instance of {@link ResourceFinderClasspath}. * * @see ResourceFinderClasspath * @see #runParticipants(ResourceFinder) * @since 1.5 */ public void runParticipants() { runParticipants(ResourceFinderClasspath.getInstance()); } /** * Sequentially execute the participants according to their registration * order. If the participant has already been run or is still running, it * is not executed anymore. The repository waits for the participant's * execution to finish if this has been indicated by registering with the * <code>blocking</code> attribute. * * @param resourceFinder The resource finder that is used during the * initialization. * @since 1.0 */ public void runParticipants(ResourceFinder resourceFinder) { // Iterate over the participants according to their registration order. for (String name : repParticipantsOrder) { BlockingParticipant participant = repParticipants.get(name); try { // Only start the participant's initialization if it hasn't run // before. if (!participant.getThread().isAlive() && !participant.isFinished()) { Logger.getLogger("com.uwyn.rife.rep").info("INITIALIZATION : " + participant.getInitializationMessage()); // Initialize the participant participant.setResourceFinder(resourceFinder); participant.getThread().start(); // Wait for the initialization to finish if this has been // specified. if (repParticipantsToWaitFor.contains(name)) { participant.waitUntilFinished(); } } } finally { detectParticipantException(); } } synchronized (finishedThreadMonitor) { if (!finished) { BlockingRepositoryCleanup cleanupShutdownHook = new BlockingRepositoryCleanup(this); Runtime.getRuntime().addShutdownHook(cleanupShutdownHook); finished = true; fireInitFinished(); } } } private void detectParticipantException() { if (participantException != null) { if (participantException instanceof RuntimeException) { throw (RuntimeException)participantException; } throw new RuntimeException(participantException); } } /** * If participants call an exception, clean up correctly and rethrow the * exception afterwards. * * @since 1.0 */ public void uncaughtException(Thread thread, Throwable e) { if (e instanceof ThreadDeath) { super.uncaughtException(thread, e); } if (null == participantException) { participantException = e; } synchronized (finishedThreadMonitor) { if (!finished) { finished = true; fireInitFinished(); cleanup(); } // the repository has finished so log the exception else { if (participantException != null) { Logger.getLogger("com.uwyn.rife.rep").severe(ExceptionUtils.getExceptionStackTrace(participantException)); } } } } /** * Parses the XML file to determine what the participants are. Then, one * by one, initializes each participant with the {@link * #runParticipants(ResourceFinder) runParticipants} method, waiting for * it to finish if its <code>blocking</code> attribute was set to * <code>true</code>. * * @param repXmlPath The path of the XML file. * <p>If this is <code>null</code>, <code>rep/participants.xml</code> will * be used. * @param resourcefinder The resource finder that will be used to look up * resources such as XML files and DTDs. It will also be used by other * classes after initialization through the * <code>getResourceFinder()</code> method. * <p>If this is <code>null</code>, an instance of <code>{@link * ResourceFinderClasspath}</code> will be used. * @throws RepException when an error occurs during the initialization. * @since 1.0 */ public void initialize(String repXmlPath, ResourceFinder resourcefinder) throws RepException { finished = false; if (null == resourcefinder) { resourcefinder = ResourceFinderClasspath.getInstance(); } // TODO // if (null == repXmlPath) // { // repXmlPath = DEFAULT_REP_PATH; // } // // // Parse the repository configuration file. // Xml2BlockingRepository xml2rep = new Xml2BlockingRepository(this); // try // { // // Add the participants that have been specified in the // // configuration file to the repository. // xml2rep.addRepParticipants(repXmlPath, resourcefinder); // } // // If errors occured during the parsing of the repository configuration // // file, output a message and throw a runtime error. // catch (XmlErrorException e) // { // throw new InitializationErrorException(e); // } // Initialize the participants. runParticipants(resourcefinder); } /** * Obtains the finished status of the initialization. * * @return <code>false</code> if the initialization is still busy; or * <p><code>true</code> if the initialization is finished * @since 1.0 */ public boolean isFinished() { return finished; } /** * Cleans up the participants in the order in which they have been * declared. Every participant's <code>cleanup()</code> method is * successively called. * * @throws RepException when an error occurs during the cleanup. * @since 1.0 */ public void cleanup() throws RepException { if (!finished) { return; } synchronized (cleanupThreadMonitor) { if (cleanedUp) { return; } // iterate over all the registered participants in their reverse // registration order String name; BlockingParticipant participant; for (int i = repParticipantsOrder.size() - 1; i >= 0; i--) { name = repParticipantsOrder.get(i); // obtain each participant by its name and clean it up participant = getParticipant(name); if (participant != null) { Logger.getLogger("com.uwyn.rife.rep").info("CLEANUP : " + participant.getCleanupMessage()); participant.cleanup(); } } cleanedUp = true; } } public HierarchicalProperties getProperties() { return properties; } public Object getContext() { return context; } /** * Adds the specified repository listener to receive repository * initialization events. If <code>repListener</code> is null, no * exception is thrown and no action is performed. * * @param repListener The repository listener that will be added. * @see RepositoryListener * @see #removeRepListener(RepositoryListener) * @since 1.0 */ public void addRepListener(RepositoryListener repListener) { if (null == repListener) { return; } repListeners.add(repListener); } /** * Removes the repository listener so that it no longer receives * repository initialization events. This method performs no function, nor * does it throw an exception, if the listener specified by the argument * was not previously added to this component. If <code>repListener</code> * is <code>null</code>, no exception is thrown and no action is * performed. * * @param repListener The repository listener that will be removed. * @see RepositoryListener * @see #addRepListener(RepositoryListener) * @since 1.0 */ public void removeRepListener(RepositoryListener repListener) { repListeners.remove(repListener); } /** * Notifies the registered listeners that a new initialization action has * been performed. * <p>This is always triggered when a participant's initialization has * finished. Each participant however has the possibility to call this * method directly, allowed for finer-grained notification of the * advancement of the initialization. * * @param participant The participant that triggered the action. * @see RepositoryListener * @since 1.0 */ public void fireInitActionPerformed(BlockingParticipant participant) { if (repListeners.size() > 0) { for (RepositoryListener listener : repListeners) { listener.initActionPerformed(participant); } } } /** * Notifies the registered listeners that the repository initialization * has finished. * * @see RepositoryListener * @since 1.0 */ public void fireInitFinished() { if (repListeners.size() > 0) { for (RepositoryListener listener : repListeners) { listener.initFinished(); } } } }