Index: main/src/net/sourceforge/cruisecontrol/CruiseControlController.java =================================================================== RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/CruiseControlController.java,v retrieving revision 1.12 diff -u -r1.12 CruiseControlController.java --- main/src/net/sourceforge/cruisecontrol/CruiseControlController.java 17 Dec 2004 16:59:33 -0000 1.12 +++ main/src/net/sourceforge/cruisecontrol/CruiseControlController.java 22 Feb 2005 21:26:41 -0000 @@ -44,10 +44,17 @@ import java.io.FileInputStream; import java.io.ObjectInputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimerTask; +import java.util.Timer; /** * @@ -57,17 +64,55 @@ private static final Logger LOG = Logger.getLogger(CruiseControlController.class); public static final String DEFAULT_CONFIG_FILE_NAME = "config.xml"; private File configFile; - private List projects = new ArrayList(); - private BuildQueue buildQueue = new BuildQueue(); + private Map projectsMap = new HashMap(); + private final BuildQueue buildQueue = new BuildQueue(); + private ConfigReloader configReloader = new TimerBasedConfigReloader(); + private Object projectsLock = new Object(); + + protected void setConfigReloader(ConfigReloader configReloader) { + this.configReloader = configReloader; + } + + interface ConfigReloader { + void start(); + } + + class TimerBasedConfigReloader implements ConfigReloader { + private final Timer timer = new Timer(true); + private TimerTask configReloadTask = new TimerTask() { + public void run() { + parseConfigFileIfNecessary(); + } + }; + + public synchronized void start() { + if (configReloadTask == null) { + configReloadTask = new TimerTask() { + public void run() { + parseConfigFileIfNecessary(); + } + }; + timer.schedule(configReloadTask, timeBetweenConfigReload, timeBetweenConfigReload); + } + } + } + + private long lastConfigLoadTime = 0; + private long timeBetweenConfigReload = 10 * 1000; - private List listeners = new ArrayList(); + private final List listeners = new ArrayList(); public File getConfigFile() { return configFile; } + public void setTimeBetweenConfigReload(long time) { + timeBetweenConfigReload = time; + } + public void setConfigFile(File configFile) throws CruiseControlException { this.configFile = configFile; + lastConfigLoadTime = 0; if (configFile == null) { throw new CruiseControlException("No config file"); } @@ -75,16 +120,74 @@ throw new CruiseControlException("Config file not found: " + configFile.getName()); } parseConfigFile(); + configReloader.start(); + } + + public void parseConfigFileIfNecessary() { + // FIXME doesn't handle when changes happen twice in same second... + long lastModified = configFile.lastModified(); + if (lastModified <= lastConfigLoadTime) { + return; + }; + LOG.info("Parsing configuration file. File modified " + lastModified + " since " + lastConfigLoadTime); + // pause(); ?? + try { + parseConfigFile(); + } catch (CruiseControlException e) { + // FIXME how do we handle failures? Should we stop and wait for a fix? + LOG.error("Failure to reload config file \n" + e.getMessage()); + } + // resume(); + notifyConfigChange(); // should we notify on first read? + } + + private void notifyConfigChange() { + for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) { + LOG.debug("Informing listener of config change"); + Listener listener = (Listener) listenIter.next(); + listener.configChanged(); + } + } + + static void copySet(Set source, Set dest) { + final Iterator iterator = source.iterator(); + while (iterator.hasNext()) { + dest.add(iterator.next()); + } } private void parseConfigFile() throws CruiseControlException { Element configRoot = Util.loadConfigFile(configFile); addPluginsToRootRegistry(configRoot); - List projectList = getAllProjects(configRoot); - for (Iterator iterator = projectList.iterator(); iterator.hasNext();) { - Project project = (Project) iterator.next(); - addProject(project); + Set oldProjectNames = new HashSet(); + copySet(projectsMap.keySet(), oldProjectNames); + List newProjectList = getAllProjects(configRoot); + synchronized (projectsLock) { + for (Iterator iterator = newProjectList.iterator(); iterator.hasNext();) { + Project project = (Project) iterator.next(); + final String name = project.getName(); + if (projectsMap.containsKey(name)) { + boolean removed = oldProjectNames.remove(name); + if (!removed) + { + throw new CruiseControlException("Two projects with identical name " + name); + } + } else { + addProject(project); + } + } + + // all non treated projects have been removed + Iterator it = oldProjectNames.iterator(); + while (it.hasNext()) { + String projectName = (String) it.next(); + Project project = (Project) projectsMap.get(projectName); + removeProject(project); + // FIXME where should I stop/dequeue? If I was wrapping in a pause/resume, I wouldn't have this problem + project.stop(); + } } + lastConfigLoadTime = configFile.lastModified(); } private void addPluginsToRootRegistry(Element configRoot) { @@ -104,7 +207,9 @@ } private void addProject(Project project) { - projects.add(project); + synchronized (projectsLock) { + projectsMap.put(project.getName(), project); + } for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) { LOG.debug("Informing listener of new project"); Listener listener = (Listener) listenIter.next(); @@ -112,20 +217,35 @@ } } + private void removeProject(Project project) { + synchronized (projectsLock) { + projectsMap.remove(project.getName()); + } + for (Iterator listenIter = listeners.iterator(); listenIter.hasNext();) { + LOG.debug("Informing listener of removed project"); + Listener listener = (Listener) listenIter.next(); + listener.projectRemoved(project); + } + } + public void resume() { buildQueue.start(); - for (Iterator iterator = projects.iterator(); iterator.hasNext();) { - Project currentProject = (Project) iterator.next(); - currentProject.setBuildQueue(buildQueue); - currentProject.start(); + synchronized (projectsLock) { + for (Iterator iterator = projectsMap.values().iterator(); iterator.hasNext();) { + Project currentProject = (Project) iterator.next(); + currentProject.setBuildQueue(buildQueue); + currentProject.start(); + } } } public void pause() { buildQueue.stop(); - for (Iterator iterator = projects.iterator(); iterator.hasNext();) { - Project currentProject = (Project) iterator.next(); - currentProject.stop(); + synchronized (projectsLock) { + for (Iterator iterator = projectsMap.values().iterator(); iterator.hasNext();) { + Project currentProject = (Project) iterator.next(); + currentProject.stop(); + } } } @@ -147,12 +267,16 @@ } public List getProjects() { - return Collections.unmodifiableList(projects); + final Collection c; + synchronized (projectsLock) { + c = projectsMap.values(); + } + return Collections.unmodifiableList(new ArrayList(c)); } private List getAllProjects(Element configRoot) throws CruiseControlException { String[] projectNames = getProjectNames(configRoot); - ArrayList allProjects = new ArrayList(projectNames.length); + List allProjects = new ArrayList(projectNames.length); for (int i = 0; i < projectNames.length; i++) { String projectName = projectNames[i]; LOG.info("projectName = [" + projectName + "]"); @@ -231,5 +355,7 @@ public static interface Listener extends EventListener { void projectAdded(Project project); + void projectRemoved(Project project); + void configChanged(); } } Index: main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java =================================================================== RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java,v retrieving revision 1.9 diff -u -r1.9 CruiseControlControllerTest.java --- main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java 22 Aug 2004 09:30:20 -0000 1.9 +++ main/test/net/sourceforge/cruisecontrol/CruiseControlControllerTest.java 22 Feb 2005 21:26:41 -0000 @@ -41,6 +41,8 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.util.List; +import java.util.ArrayList; /** * @@ -51,8 +53,14 @@ private File configFile = new File("_tempConfigFile"); private CruiseControlController test; + class DummyConfigReloader implements CruiseControlController.ConfigReloader { + public void start() { + } + } + protected void setUp() throws Exception { test = new CruiseControlController(); + test.setConfigReloader(new DummyConfigReloader()); } public void tearDown() { @@ -108,6 +116,134 @@ test.setConfigFile(configFile); assertEquals(configFile, test.getConfigFile()); assertEquals(2, test.getProjects().size()); + + } + + public void testLoadSomeProjectsWithDuplicates() throws IOException, CruiseControlException { + test = new CruiseControlController() { + protected Project configureProject(String projectName) { + final Project project = new Project(); + project.setName(projectName); + return project; + } + }; + FileWriter configOut = new FileWriter(configFile); + configOut.write("\n"); + configOut.write("\n"); + writeProjectDetails(configOut, "testProject1"); + writeProjectDetails(configOut, "testProject1"); + configOut.write("\n"); + configOut.close(); + + try { + test.setConfigFile(configFile); + fail("duplicate project names should fail"); + } catch (CruiseControlException e) { + // expected + } + } + + public void testConfigReloading() throws IOException, CruiseControlException { + class MyListener implements CruiseControlController.Listener { + List addedProjects = new ArrayList(); + List removedProjects = new ArrayList(); + int configReloads = 0; + public void clear() { + addedProjects.clear(); + removedProjects.clear(); + configReloads = 0; + } + public void projectAdded(Project project) { + addedProjects.add(project); + } + public void projectRemoved(Project project) { + removedProjects.add(project); + } + public void configChanged() { configReloads++; } + }; + MyListener listener = new MyListener(); + + test = new CruiseControlController() { + protected Project configureProject(String projectName) { + final Project project = new Project(); + project.setName(projectName); + return project; + } + }; + test.addListener(listener); + FileWriter configOut = new FileWriter(configFile); + configOut.write("\n"); + configOut.write("\n"); + writeProjectDetails(configOut, "testProject1"); + writeProjectDetails(configOut, "testProject2"); + configOut.write("\n"); + configOut.close(); + test.setConfigFile(configFile); + + assertEquals(configFile, test.getConfigFile()); + assertEquals(2, test.getProjects().size()); + assertEquals(2, listener.addedProjects.size()); + assertEquals(0, listener.removedProjects.size()); + assertEquals(0, listener.configReloads); + + listener.clear(); + + // no change - no reload + test.parseConfigFileIfNecessary(); + // nothing happened + assertEquals(0, listener.configReloads); + assertEquals(0, listener.addedProjects.size()); + assertEquals(0, listener.removedProjects.size()); + + + // add a project: + + listener.clear(); + + sleep(1200); + configOut = new FileWriter(configFile); + configOut.write("\n"); + configOut.write("\n"); + writeProjectDetails(configOut, "testProject1"); + writeProjectDetails(configOut, "testProject2"); + writeProjectDetails(configOut, "testProject3"); + configOut.write("\n"); + configOut.close(); + + test.parseConfigFileIfNecessary(); + + assertEquals(1, listener.configReloads); + assertEquals(3, test.getProjects().size()); + assertEquals(1, listener.addedProjects.size()); + assertEquals(0, listener.removedProjects.size()); + + // remove 2 projects + + listener.clear(); + + sleep(1200); + configOut = new FileWriter(configFile); + configOut.write("\n"); + configOut.write("\n"); + writeProjectDetails(configOut, "testProject3"); + configOut.write("\n"); + configOut.close(); + + test.parseConfigFileIfNecessary(); + + assertEquals(1, listener.configReloads); + assertEquals(1, test.getProjects().size()); + assertEquals(0, listener.addedProjects.size()); + assertEquals(2, listener.removedProjects.size()); + + } + + private void sleep(long l) { + try { + Thread.sleep(l); + } catch (InterruptedException e) { + + } } public void testReadProject() throws IOException { @@ -119,6 +255,7 @@ assertTrue(project.getBuildForced()); } + public void testRegisterPlugins() throws Exception { FileWriter configOut = new FileWriter(configFile); configOut.write("\n"); Index: main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java =================================================================== RCS file: /cvsroot/cruisecontrol/cruisecontrol/main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java,v retrieving revision 1.6 diff -u -r1.6 CruiseControlControllerJMXAdaptor.java --- main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java 28 Jan 2004 00:54:04 -0000 1.6 +++ main/src/net/sourceforge/cruisecontrol/jmx/CruiseControlControllerJMXAdaptor.java 22 Feb 2005 21:26:41 -0000 @@ -122,4 +122,22 @@ LOG.error("Could not register project " + project.getName(), e); } } + + public void projectRemoved(Project project) { + // FIXME implement +/* + try { + LOG.debug("Unregistering project mbean"); + ProjectController projectController = new ProjectController(project); + projectController.unregister(server); + } catch (JMException e) { + LOG.error("Could not register project " + project.getName(), e); + } +*/ + } + + + public void configChanged() { + LOG.info("config changed"); + } }