package org.hudson.trayapp.model; import java.io.IOException; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.hudson.trayapp.gui.tray.TrayIconImplementation; import org.hudson.trayapp.util.XMLHelper; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class Server { private static final long serialVersionUID = 1L; private String description; private String url; private String name = ""; private List jobs = new Vector(); private transient boolean bVersion173OrGreater = false; public Server() {} public Server(String url, String name) { defaults(); this.name = name; this.url = Job.getRFC2396CompliantURL(url); if (url.length() > 0) update(); } private void defaults() { description = ""; url = ""; jobs = new Vector(); } private static String getRootHudsonURL(String url) { try { URL urlo = new URL(url); if (urlo.getFile().startsWith("/hudson/")) { return new URL(urlo.getProtocol(), urlo.getHost(), urlo.getPort(), "/hudson").toString(); } else { return new URL(urlo.getProtocol(), urlo.getHost(), urlo.getPort(), "").toString(); } } catch (MalformedURLException e) { return url; } } public boolean update() { boolean updated = false; try { boolean hudsonBuild173orGreater = isHudsonBuild173orGreater(true); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); URL urlo; if (hudsonBuild173orGreater) { urlo = new URL(url + "/api/xml?depth=1"); } else { urlo = new URL(url + "/api/xml"); } URLConnection conn = urlo.openConnection(); InputSource inputSource = new InputSource(conn.getInputStream()); Document document = builder.parse(inputSource); process(document.getChildNodes()); /* * OK, if we have an older version of Hudson, then we won't have fetched back all the information * that we needed. Thus we will have to fetch all the information about each server, job by job. */ if (updated == true && !hudsonBuild173orGreater) { Iterator iterator = jobs.iterator(); while (iterator.hasNext()) { ((Job) iterator.next()).update(); } } } catch (ParserConfigurationException e) { TrayIconImplementation.displayException("Server Update Exception", "Updating Server " + name, e); } catch (IOException e) { TrayIconImplementation.displayException("Server Update Exception", "Updating Server " + name, e); } catch (SAXException e) { TrayIconImplementation.displayException("Server Update Exception", "Updating Server " + name, e); } return updated; } public String getDescription() { return description; } public String getURL() { return url; } public Collection getJobs() { return jobs; } public String getName() { return name; } public void process(NodeList nodes) { defaults(); for (int i = 0; i < nodes.getLength(); i++) { Node node = nodes.item(i); String name = node.getNodeName(); String value = XMLHelper.getTextContent(node); if (name.equals("hudson")) { process(node.getChildNodes()); } else if (name.equals("listView")) { process(node.getChildNodes()); } else if (name.equals("description")) { description = value; } else if (name.equals("url")) { url = Job.getRFC2396CompliantURL(value); } else if (name.equals("job")) { Job job = Job.process(node); jobs.add(job); } else if (name.equals("internalname")) { this.name = value; } } } /** * This method will traverse all of the jobs, and determine the worst state of the jobs * and return all of those jobs that share that same state. * @return This returns an array of Job's that share the same colour state. This will * always return an array, but this array may be empty if there are no jobs defined. */ public Job[] getWorstJobs() { Vector vecWorst = new Vector(jobs.size()); int worstCase = -1; Iterator jobiter = jobs.iterator(); for (int i = 0; jobiter.hasNext(); i++) { Job job = (Job) jobiter.next(); String colour = job.getColour(); for (int j = 0; worstCase != j && j < Model.colours.length; j++) { if (colour.equals(Model.colours[j])) { worstCase = j; vecWorst.clear(); } } if (colour.equals(Model.colours[worstCase])) { vecWorst.add(job); } } Job[] jobaReturn = new Job[vecWorst.size()]; for (int i = 0; i < jobaReturn.length; i ++) { jobaReturn[i] = (Job) vecWorst.get(i); } return jobaReturn; } /** * This method will traverse all the HudsonJobs, and determine what * the worst colour is in the following order (worst to best): * red_anime, red, aborted_anime, aborted, yellow_anime, yellow, blue_anime, blue, * disabled_anime, disabled * @return */ public String getColour() { int worstCase = -1; Iterator jobiter = jobs.iterator(); for (int i = 0; jobiter.hasNext(); i++) { Job job = (Job) jobiter.next(); String colour = job.getColour(); for (int j = 0; worstCase != j && j < Model.colours.length; j++) { if (colour.equals(Model.colours[j])) { worstCase = j; } } } if (worstCase == -1) { return null; } else { return Model.colours[worstCase]; } } /** * This method will traverse all of the Jobs, and determine the total * of all of the jobs that share the same colour that you pass in * @param colour The colour you want to get a count for. * @return The total number of jobs that are the same state as the colour provided */ public int getNumberOfJobsWithColour(String colour) { int iReturn = 0; Iterator jobiter = jobs.iterator(); while (jobiter.hasNext()) { Job job = (Job) jobiter.next(); if (job.getColour().equals(colour)) iReturn++; } return iReturn; } /** * This method is provided as a convenience method to determine the total * number of jobs that are currently red (this includes those building). * @return The total number of jobs that are in a Red state (including those building). */ public int getNumberOfRedJobs() { return getNumberOfJobsWithColour("red") + getNumberOfJobsWithColour("red_anime"); } /** * This method is provided as a convenience method to determine the total * number of jobs that are currently yellow (this includes those building). * @return The total number of jobs that are in a Yellow state (including those building). */ public int getNumberOfYellowJobs() { return getNumberOfJobsWithColour("yellow") + getNumberOfJobsWithColour("yellow_anime"); } /** * This method is provided as a convenience method to determine the total * number of jobs that are currently blue (this includes those building). * @return The total number of jobs that are in a Blue state (including those building). */ public int getNumberOfBlueJobs() { return getNumberOfJobsWithColour("blue") + getNumberOfJobsWithColour("blue_anime"); } /** * This method is provided as a convenience method to determine the total * number of jobs that are currently grey. * @return The total number of jobs that are in a grey state. */ public int getNumberOfGreyJobs() { return getNumberOfJobsWithColour("grey"); } /** * This method is provided as a convenience method to determine the total * number of jobs that are currently building. * @return The total number of jobs that are building. */ public int getNumberOfBuildingJobs() { return getNumberOfJobsWithColour("red_anime") + getNumberOfJobsWithColour("yellow_anime") + getNumberOfJobsWithColour("blue_anime"); } public Object clone() { Server modelReturn = new Server(); modelReturn.description = description; modelReturn.url = url; modelReturn.name = name; Iterator iterator = jobs.iterator(); while (iterator.hasNext()) { Job job = (Job) iterator.next(); modelReturn.jobs.add(job.clone()); } return modelReturn; } /** This method is added, as from Hudson build 173 onwards, you can apply a paremeter to the api/xml of * ?depth=1 that will give you the details about the build, including the HealthReports. Prior to this * you have to request specifically about each job what build number it is. Note that older versions do * not support reporting of HealthReports, and thus restricted information will have to be displayed. * * @return Returns true if the build of Hudson this URL points to is of version 173 or greater. Returns false otherwise. */ public boolean isHudsonBuild173orGreater() { return isHudsonBuild173orGreater(false); } private boolean isHudsonBuild173orGreater(boolean fetchFromServer) { if (fetchFromServer) { URL urlo = null; try { urlo = new URL(getRootHudsonURL(url)); URLConnection conn = urlo.openConnection(); String version = conn.getHeaderField("X-Hudson"); if (version == null) { TrayIconImplementation.displayException("Exception on Version Check", "No X-Hudson header. Is "+url+" really Hudson?", new Exception()); bVersion173OrGreater = false; return false; } Matcher matcher = pattern.matcher(version); if (matcher.matches()) { float f = Float.parseFloat(matcher.group(1)); bVersion173OrGreater = f > 1.172; return bVersion173OrGreater; } else { bVersion173OrGreater = false; return bVersion173OrGreater; } } catch (MalformedURLException e) { TrayIconImplementation.displayException("Exception on Version Check", "Trying to get Hudson Version", e); } catch (IOException e) { TrayIconImplementation.displayException("Exception on Version Check", "Trying to get Hudson Version", e); } bVersion173OrGreater = false; return false; } else { return bVersion173OrGreater; } } private static Pattern pattern = Pattern.compile("([0-9.]+).*"); public void setUrl(String url) { this.url = Job.getRFC2396CompliantURL(url); } public void setName(String name) { this.name = name; } public void writeXML(Writer w) throws IOException { w.write("<server>"); w.write("<internalname>"); w.write(name); w.write("</internalname>"); w.write("<url>"); w.write(url); w.write("</url>"); w.write("</server>"); } public boolean isHealthSupported() { return isHudsonBuild173orGreater(); } }