/******************************************************************************* * Copyright (c) 2005 RadRails.org and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.radrails.server.core; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Observer; import java.util.Set; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.debug.core.model.IProcess; import org.eclipse.ui.PlatformUI; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; /** * Manages a collection of all servers. Operations on servers should be called * through this class, because it notifies any listening views of changes to the * server. * * The <code>ServerManager</code> manages all of the servers in the workspace. * It is also the model represented by <code>ServersView</code>. * * When a new <code>Server</code> has been created, it should be added to * <code>ServerManager</code> using {@link #addServer(Server) addServer} * method. Similarly, to remove a <code>Server</code>, use the * {@link #removeServer(Server) removeServer} method. * * The <code>ServerManager</code> also maintains a list of * <code>Observer</code> objects. These are conceptually the observers of the * <code>ServerManager</code>, but actually the observers of the * <code>Server</code>s maintained by the <code>ServerManager</code>. As * such, the {@link #addServerObserver(Observer) addServerObserver} and * {@link #deleteServerObserver(Observer) deleteServerObserver} methods should * be used instead of the <code>addObserver</code> and * <code>deleteObserver</code> methods on <code>Server</code>. * * @author mkent * */ public class ServerManager implements IResourceChangeListener { private static final String SERVERS_XML = "servers.xml"; private static ServerManager instance; private Set<Server> fServers; private List<Observer> serverObservers; /** * Default constructor. */ private ServerManager() { fServers = new HashSet<Server>(); serverObservers = new ArrayList<Observer>(); ResourcesPlugin.getWorkspace().addResourceChangeListener(this); } /** * @return the singleton instance of <code>ServerManager</code>. */ public static ServerManager getInstance() { if (instance == null) { instance = new ServerManager(); } return instance; } /** * Convenience method to stop all servers in the <code>ServerManager</code>. */ public void stopAll() { for (Server s : fServers) { if (s.isStarted()) { s.stop(); // Force the status to STOPPED // Needed on workbench shutdown because the view gets disposed // before the status can be updated s.updateStatus(IServerConstants.STOPPED); } } } /** * @return the next available server port */ public String getNextAvailablePort() { String port = IServerConstants.DEFAULT_WEBRICK_PORT; boolean found = false; while (!found) { if (!portInUse(port)) { found = true; } else { port = String.valueOf(Integer.parseInt(port) + 1); } } return port; } /** * Determines if the given port is currently in use by a <code>Server</code>. * * @param port * the port to check * @return true if the port is in use, false otherwise */ public boolean portInUse(String port) { boolean taken = false; Iterator i = fServers.iterator(); while (i.hasNext() && !taken) { Server s = (Server) i.next(); taken = port.equals(s.getPort()); } return taken; } /** * Determines if the given project has a <code>Server</code> of the given * type. * * @param projectName * the name of the <code>Server</code> to check * @param type * the type of <code>Server</code> to check for * @return true if the project has the <code>Server</code>, false * otherwise */ public boolean projectHasServer(String projectName, String type) { for (Server s : fServers) { if (s.getProject().getName().equals(projectName) && s.getType().equals(type)) return true; } return false; } /** * Adds a <code>Server</code> to the <code>ServerManager</code>. * * @param server * the <code>Server</code> to add */ public synchronized void addServer(Server server) { fServers.add(server); saveServers(); // Need to add the current observers to the new server Iterator i = serverObservers.iterator(); while (i.hasNext()) { Observer o = (Observer) i.next(); server.addServerObserver(o); } // Notify the observers that the server has been added server.touch(); server.notifyServerObservers(IServerConstants.ADD); } /** * Removes a <code>Server</code> from the <code>ServerManager</code>. * * @param server * the <code>Server</code> to remove */ public synchronized void removeServer(Server server) { fServers.remove(server); saveServers(); // Notify the observers that the server has been removed server.touch(); server.notifyServerObservers(IServerConstants.REMOVE); } /** * Clients should not call this method. Exists only for use by ServersView. * * @return the servers held by the manager */ public Collection<Server> getServers() { return Collections.unmodifiableCollection(fServers); } /** * Convenience method for the Rails App launch configuration * * @param project * @return collection of servers whose root is the given project */ public Collection<Server> getServersForProject(IProject project) { Collection<Server> ret = new ArrayList<Server>(); for (Server s : fServers) { if(s.getProject().equals(project)) { ret.add(s); } } return ret; } /** * Convenience method for the Rails App launch configuration * * @param name * @return the server for the given name */ public Server getServer(String name) { for (Server s : fServers) { if(s.getName().equals(name)) { return s; } } return null; } public Server findByProcess(IProcess process) { for (Server server : fServers) { if (process.equals(server.getProcess())) { return server; } } return null; } /** * Adds an <code>Observer</code> to the list of current server observers. * * @param ob * the observer to add */ public void addServerObserver(Observer ob) { serverObservers.add(ob); // Add observer of all servers Iterator<Server> i = fServers.iterator(); while (i.hasNext()) { Server s = i.next(); s.addServerObserver(ob); } } /** * Deletes an <code>Observer</code> from the list of current server * observers. * * @param ob * the observer to delete */ public void deleteServerObserver(Observer ob) { serverObservers.remove(ob); // Remove observer of all servers for (Server s : fServers) { s.deleteServerObserver(ob); } } /** * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) */ public void resourceChanged(IResourceChangeEvent event) { // When a project is deleted, delete its servers if (event.getType() == IResourceChangeEvent.PRE_DELETE) { IProject project = (IProject) event.getResource(); // Find all of this project's servers and remove them List<Server> servers = new ArrayList<Server>(fServers); for (int i = 0; i < servers.size(); i++) { final Server s = (Server) servers.get(i); if (project.equals(s.getProject())) { // This method will be called from a separate thread, so // use syncExec PlatformUI.getWorkbench().getDisplay().syncExec( new Runnable() { public void run() { // Stop the server, then remove s.stop(); long start = System.currentTimeMillis(); while (!s.getStatus().equals(IServerConstants.STOPPED)) { Thread.yield(); if (System.currentTimeMillis() > start + 10000) break; } removeServer(s); } }); } } } // When a project is closed, stop its servers else if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { IProject project = (IProject) event.getResource(); // Find all of this project's servers and stop them List<Server> servers = new ArrayList<Server>(fServers); for (int i = 0; i < servers.size(); i++) { final Server s = (Server) servers.get(i); if (project.equals(s.getProject())) { // This method will be called from a separate thread, so // use syncExec PlatformUI.getWorkbench().getDisplay().syncExec( new Runnable() { public void run() { // Stop the server s.stop(); } }); } } } } /** * Saves all current server configurations to file in XML format. The status * of each server is not saved, because this method is only used on * workbench shutdown and each server is assumed to be stopped. */ public synchronized void saveServers() { File f = getConfigFile(); PrintWriter out = null; try { out = new PrintWriter(new FileWriter(f)); writeXML(out); } catch (FileNotFoundException e) { ServerLog.logError("Servers config file not found", e); } catch (IOException e) { ServerLog.logError("Error opening servers config", e); } finally { if (out != null) out.close(); } } /** * Discards all existing server configurations, then loads saved server * configurations from file. */ public void loadServers() { try { Reader fileReader = new FileReader(getConfigFile()); XMLReader reader = SAXParserFactory.newInstance().newSAXParser() .getXMLReader(); ServerManagerContentHandler handler = new ServerManagerContentHandler(); reader.setContentHandler(handler); reader.parse(new InputSource(fileReader)); // Discard current servers fServers.clear(); // Load each saved server into the map Collection servers = handler.getServers(); Iterator i = servers.iterator(); while (i.hasNext()) { Server s = (Server) i.next(); s.checkIfLeftHanging(); fServers.add(s); } } catch (FileNotFoundException e) { // This is okay, will get thrown if no config exists yet } catch (SAXException e) { ServerLog.logError("Error parsing servers config file", e); } catch (ParserConfigurationException e) { ServerLog.logError("Error configuring XML parser", e); } catch (FactoryConfigurationError e) { ServerLog.logError("Error configuring parser factory", e); } catch (IOException e) { ServerLog.logError("Error reading servers config file", e); } } /** * Writes each server configuration to file in XML format. * * @param out * the writer to use */ private void writeXML(PrintWriter out) { out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); out.println("<server-manager>"); Iterator i = fServers.iterator(); while (i.hasNext()) { Server s = (Server) i.next(); out.println("<server type=\"" + s.getType() + "\">"); out.println("<project>" + s.getProject().getLocation().toPortableString() + "</project>"); out.println("<name>" + s.getName() + "</name>"); out.println("<host>" + s.getHost() + "</host>"); out.println("<port>" + s.getPort() + "</port>"); out.println("<environment>" + s.getEnvironment() + "</environment>"); out.println("<runMode>" + s.getRunMode() + "</runMode>"); out.println("</server>"); } out.println("</server-manager>"); out.flush(); } /** * Returns the configuration file to use for the servers. The file is * located in the plugin state directory and called <code>servers.xml</code>. * * @return the config file */ private File getConfigFile() { return ServerPlugin.getInstance().getStateLocation().append(SERVERS_XML).toFile(); } }