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");
+ }
}