Java tutorial
/* * The MIT License * * Copyright (c) 2009, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.tools; import hudson.AbortException; import hudson.Extension; import hudson.FilePath; import hudson.Util; import hudson.util.FormValidation; import hudson.util.ArgumentListBuilder; import hudson.model.Node; import hudson.model.TaskListener; import hudson.model.DownloadService.Downloadable; import hudson.model.JDK; import static hudson.tools.JDKInstaller.Preference.*; import hudson.remoting.Callable; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullWriter; import org.w3c.tidy.Tidy; import org.dom4j.io.DOMReader; import org.dom4j.Document; import org.dom4j.Element; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.PrintStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Serializable; import java.io.PrintWriter; import java.net.URL; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Arrays; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sf.json.JSONObject; /** * Install JDKs from java.sun.com. * * @author Kohsuke Kawaguchi * @since 1.305 */ public class JDKInstaller extends ToolInstaller { /** * The release ID that Sun assigns to each JDK, such as "jdk-6u13-oth-JPR@CDS-CDS_Developer" * * <p> * This ID can be seen in the "ProductRef" query parameter of the download page, like * https://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD/ViewProductDetail-Start?ProductRef=jdk-6u13-oth-JPR@CDS-CDS_Developer */ public final String id; /** * We require that the user accepts the license by clicking a checkbox, to make up for the part * that we auto-accept cds.sun.com license click through. */ public final boolean acceptLicense; @DataBoundConstructor public JDKInstaller(String id, boolean acceptLicense) { super(null); this.id = id; this.acceptLicense = acceptLicense; } public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException { FilePath expectedLocation = preferredLocation(tool, node); PrintStream out = log.getLogger(); try { if (!acceptLicense) { out.println("Unable to perform installation until the license is accepted."); return expectedLocation; } // already installed? FilePath marker = expectedLocation.child(".installedByHudson"); if (marker.exists()) return expectedLocation; expectedLocation.mkdirs(); Platform p = Platform.of(node); URL url = locate(log, p, CPU.of(node)); out.println("Downloading " + url); FilePath file = expectedLocation.child(fileName(p)); file.copyFrom(url); out.println("Installing " + file); switch (p) { case LINUX: case SOLARIS: file.chmod(0755); if (node.createLauncher(log).launch().cmds(file.getRemote(), "-noregister") .stdin(new ByteArrayInputStream("yes".getBytes())).stdout(out).pwd(expectedLocation) .join() != 0) throw new AbortException("Failed to install JDK"); // JDK creates its own sub-directory, so pull them up List<FilePath> paths = expectedLocation.list(new JdkFinder()); if (paths.size() != 1) throw new AbortException("Failed to find the extracted JDKs: " + paths); // remove the intermediate directory paths.get(0).moveAllChildrenTo(expectedLocation); break; case WINDOWS: /* Windows silent installation is full of bad know-how. On Windows, command line argument to a process at the OS level is a single string, not a string array like POSIX. When we pass arguments as string array, JRE eventually turn it into a single string with adding quotes to "the right place". Unfortunately, with the strange argument layout of InstallShield (like /v/qn" INSTALLDIR=foobar"), it appears that the escaping done by JRE gets in the way, and prevents the installation. Presumably because of this, my attempt to use /q/vn" INSTALLDIR=foo" didn't work with JDK5. I tried to locate exactly how InstallShield parses the arguments (and why it uses awkward option like /qn, but couldn't find any. Instead, experiments revealed that "/q/vn ARG ARG ARG" works just as well. This is presumably due to the Visual C++ runtime library (which does single string -> string array conversion to invoke the main method in most Win32 process), and this consistently worked on JDK5 and JDK4. Some of the official documentations are available at - http://java.sun.com/j2se/1.5.0/sdksilent.html - http://java.sun.com/j2se/1.4.2/docs/guide/plugin/developer_guide/silent.html */ // see // FilePath logFile = node.getRootPath().createTempFile("jdk-install", ".log"); // JDK6u13 doesn't like path representation like "/tmp/foo", so make it a strict Windows format String normalizedPath = expectedLocation.absolutize().getRemote(); ArgumentListBuilder args = new ArgumentListBuilder(); args.add(file.getRemote()); args.add("/s"); // according to http://community.acresso.com/showthread.php?t=83301, \" is the trick to quote values with whitespaces. // Oh Windows, oh windows, why do you have to be so difficult? args.add("/v/qn REBOOT=Suppress INSTALLDIR=\\\"" + normalizedPath + "\\\" /L \\\"" + logFile.getRemote() + "\\\""); if (node.createLauncher(log).launch().cmds(args).stdout(out).pwd(expectedLocation).join() != 0) { out.println("Failed to install JDK"); // log file is in UTF-16 InputStreamReader in = new InputStreamReader(logFile.read(), "UTF-16"); try { IOUtils.copy(in, new OutputStreamWriter(out)); } finally { in.close(); } throw new AbortException(); } logFile.delete(); break; } // successfully installed file.delete(); marker.touch(System.currentTimeMillis()); } catch (DetectionFailedException e) { out.println("JDK installation skipped: " + e.getMessage()); } return expectedLocation; } /** * Choose the file name suitable for the downloaded JDK bundle. */ private String fileName(Platform p) { switch (p) { case LINUX: case SOLARIS: return "jdk.sh"; case WINDOWS: return "jdk.exe"; } throw new AssertionError(); } /** * Finds the directory that JDK has created. */ private static class JdkFinder implements FileFilter, Serializable { private static final long serialVersionUID = 1L; public boolean accept(File f) { return f.isDirectory() && f.getName().matches("j(2s)?dk.*"); } } /** * Performs a license click through and obtains the one-time URL for downloading bits. */ public URL locate(TaskListener log, Platform platform, CPU cpu) throws IOException { HttpURLConnection con = locateStage1(platform, cpu); String page = IOUtils.toString(con.getInputStream()); return locateStage2(log, page); } @SuppressWarnings("unchecked") // dom4j doesn't do generics, apparently... should probably switch to XOM private HttpURLConnection locateStage1(Platform platform, CPU cpu) throws IOException { URL url = new URL( "https://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD/ViewProductDetail-Start?ProductRef=" + id); HttpURLConnection con = (HttpURLConnection) url.openConnection(); String cookie = con.getHeaderField("Set-Cookie"); LOGGER.fine("Cookie=" + cookie); Tidy tidy = new Tidy(); tidy.setErrout(new PrintWriter(new NullWriter())); DOMReader domReader = new DOMReader(); Document dom = domReader.read(tidy.parseDOM(con.getInputStream(), null)); Element form = null; for (Element e : (List<Element>) dom.selectNodes("//form")) { String action = e.attributeValue("action"); LOGGER.fine("Found form:" + action); if (action.contains("ViewFilteredProducts")) { form = e; break; } } con = (HttpURLConnection) new URL(form.attributeValue("action")).openConnection(); con.setRequestMethod("POST"); con.setDoOutput(true); con.setRequestProperty("Cookie", cookie); con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); PrintStream os = new PrintStream(con.getOutputStream()); // select platform String primary = null, secondary = null; Element p = (Element) form.selectSingleNode(".//select[@id='dnld_platform']"); for (Element opt : (List<Element>) p.elements("option")) { String value = opt.attributeValue("value"); String vcap = value.toUpperCase(Locale.ENGLISH); if (!platform.is(vcap)) continue; switch (cpu.accept(vcap)) { case PRIMARY: primary = value; break; case SECONDARY: secondary = value; break; case UNACCEPTABLE: break; } } if (primary == null) primary = secondary; if (primary == null) throw new AbortException( "Couldn't find the right download for " + platform + " and " + cpu + " combination"); os.print(p.attributeValue("name") + '=' + primary); LOGGER.fine("Platform choice:" + primary); // select language Element l = (Element) form.selectSingleNode(".//select[@id='dnld_language']"); if (l != null) { os.print("&" + l.attributeValue("name") + "=" + l.element("option").attributeValue("value")); } // the rest for (Element e : (List<Element>) form.selectNodes(".//input")) { os.print('&'); os.print(e.attributeValue("name")); os.print('='); String value = e.attributeValue("value"); if (value == null) os.print("on"); // assume this is a checkbox else os.print(URLEncoder.encode(value, "UTF-8")); } os.close(); return con; } private URL locateStage2(TaskListener log, String page) throws MalformedURLException { Pattern HREF = Pattern.compile("<a href=\"(http://cds.sun.com/[^\"]+/VerifyItem-Start[^\"]+)\""); Matcher m = HREF.matcher(page); // this page contains a missing --> that confuses dom4j/jtidy log.getLogger().println("Choosing the download bundle"); List<String> urls = new ArrayList<String>(); while (m.find()) { String url = m.group(1); LOGGER.fine("Considering a download link:" + url); // still more options to choose from. // avoid rpm bundles, and avoid tar.Z bundle if (url.contains("rpm")) continue; if (url.contains("tar.Z")) continue; // sparcv9 bundle is add-on to the sparc bundle, so just download 32bit sparc bundle, even on 64bit system if (url.contains("sparcv9")) continue; urls.add(url); LOGGER.fine("Found a download candidate: " + url); } // prefer the first match because sometimes "optional downloads" follow the main bundle return new URL(urls.get(0)); } public enum Preference { PRIMARY, SECONDARY, UNACCEPTABLE } /** * Supported platform. */ public enum Platform { LINUX, SOLARIS, WINDOWS; public boolean is(String line) { return line.contains(name()); } /** * Determines the platform of the given node. */ public static Platform of(Node n) throws IOException, InterruptedException, DetectionFailedException { return n.toComputer().getChannel().call(new Callable<Platform, DetectionFailedException>() { public Platform call() throws DetectionFailedException { return current(); } }); } public static Platform current() throws DetectionFailedException { String arch = System.getProperty("os.name").toLowerCase(); if (arch.contains("linux")) return LINUX; if (arch.contains("windows")) return WINDOWS; if (arch.contains("sun") || arch.contains("solaris")) return SOLARIS; throw new DetectionFailedException("Unknown CPU name: " + arch); } } /** * CPU type. */ public enum CPU { i386, amd64, Sparc, Itanium; /** * In JDK5u3, I see platform like "Linux AMD64", while JDK6u3 refers to "Linux x64", so * just use "64" for locating bits. */ public Preference accept(String line) { switch (this) { // these two guys are totally incompatible with everything else, so no fallback case Sparc: return must(line.contains("SPARC")); case Itanium: return must(line.contains("ITANIUM")); // 64bit Solaris, Linux, and Windows can all run 32bit executable, so fall back to 32bit if 64bit bundle is not found case amd64: if (line.contains("64")) return PRIMARY; if (line.contains("SPARC") || line.contains("ITANIUM")) return UNACCEPTABLE; return SECONDARY; case i386: if (line.contains("64") || line.contains("SPARC") || line.contains("ITANIUM")) return UNACCEPTABLE; return PRIMARY; } return UNACCEPTABLE; } private static Preference must(boolean b) { return b ? PRIMARY : UNACCEPTABLE; } /** * Determines the CPU of the given node. */ public static CPU of(Node n) throws IOException, InterruptedException, DetectionFailedException { return n.toComputer().getChannel().call(new Callable<CPU, DetectionFailedException>() { public CPU call() throws DetectionFailedException { return current(); } }); } /** * Determines the CPU of the current JVM. * * http://lopica.sourceforge.net/os.html was useful in writing this code. */ public static CPU current() throws DetectionFailedException { String arch = System.getProperty("os.arch").toLowerCase(); if (arch.contains("sparc")) return Sparc; if (arch.contains("ia64")) return Itanium; if (arch.contains("amd64") || arch.contains("86_64")) return amd64; if (arch.contains("86")) return i386; throw new DetectionFailedException("Unknown CPU architecture: " + arch); } } /** * Indicates the failure to detect the OS or CPU. */ private static final class DetectionFailedException extends Exception { private DetectionFailedException(String message) { super(message); } } public static final class JDKFamilyList { public JDKFamily[] jdks = new JDKFamily[0]; } public static final class JDKFamily { public String name; public InstallableJDK[] list; } public static final class InstallableJDK { public String name; /** * Product code. */ public String id; } @Extension public static final class DescriptorImpl extends ToolInstallerDescriptor<JDKInstaller> { public String getDisplayName() { return Messages.JDKInstaller_DescriptorImpl_displayName(); } @Override public boolean isApplicable(Class<? extends ToolInstallation> toolType) { return toolType == JDK.class; } public FormValidation doCheckId(@QueryParameter String value) { if (Util.fixEmpty(value) == null) { return FormValidation.error(Messages.JDKInstaller_DescriptorImpl_doCheckId()); // improve message } else { // XXX further checks? return FormValidation.ok(); } } /** * List of installable JDKs. * @return never null. */ public List<JDKFamily> getInstallableJDKs() throws IOException { return Arrays.asList(JDKList.all().get(JDKList.class).toList().jdks); } public FormValidation doCheckAcceptLicense(@QueryParameter boolean value) { if (value) { return FormValidation.ok(); } else { return FormValidation.error(Messages.JDKInstaller_DescriptorImpl_doCheckAcceptLicense()); } } } /** * JDK list. */ @Extension public static final class JDKList extends Downloadable { public JDKList() { super(JDKInstaller.class); } public JDKFamilyList toList() throws IOException { JSONObject d = getData(); if (d == null) return new JDKFamilyList(); return (JDKFamilyList) JSONObject.toBean(d, JDKFamilyList.class); } } private static final Logger LOGGER = Logger.getLogger(JDKInstaller.class.getName()); }