package org.embulk.cli; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.maven.artifact.versioning.ComparableVersion; // It uses |java.net.HttpURLConnection| so that embulk-cli does not need additional dependedcies. // TODO(dmikurube): Support HTTP proxy. The original Ruby version did not support as well, though. public class EmbulkSelfUpdate { // TODO(dmikurube): Stop catching Exceptions here when embulk_run.rb is replaced to Java. public void updateSelf(final String runningVersionString, final String specifiedVersionString, final String embulkRunRubyPathString, final boolean isForced) throws IOException, URISyntaxException { try { updateSelfWithExceptions(runningVersionString, specifiedVersionString, embulkRunRubyPathString, isForced); } catch (Throwable ex) { ex.printStackTrace(); throw ex; } } private void updateSelfWithExceptions(final String runningVersionString, final String specifiedVersionString, final String embulkRunRubyPathString, final boolean isForced) throws IOException, URISyntaxException { final Path jarPathJava = Paths.get( EmbulkSelfUpdate.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); if ((!Files.exists(jarPathJava)) || (!Files.isRegularFile(jarPathJava))) { throw exceptionNoSingleJar(); } if (embulkRunRubyPathString != null) { final String[] splitRubyFile = embulkRunRubyPathString.split("!", 2); if (splitRubyFile.length < 2) { throw exceptionNoSingleJar(); } final Path jarPathRuby = Paths.get(splitRubyFile[0]); if (!jarPathJava.equals(jarPathRuby)) { throw exceptionNoSingleJar(); } } final String targetVersionString; if (specifiedVersionString != null) { System.out.printf("Checking version %s...\n", specifiedVersionString); targetVersionString = checkTargetVersion(specifiedVersionString); if (targetVersionString == null) { throw new RuntimeException(String.format("Specified version does not exist: %s", specifiedVersionString)); } System.out.printf("Found version %s.\n", specifiedVersionString); } else { System.out.println("Checking the latest version..."); final ComparableVersion runningVersion = new ComparableVersion(runningVersionString); targetVersionString = checkLatestVersion(); final ComparableVersion targetVersion = new ComparableVersion(targetVersionString); if (targetVersion.compareTo(runningVersion) <= 0) { System.out.printf("Already up-to-date. %s is the latest version.\n", runningVersion); return; } System.out.printf("Found a newer version %s.\n", targetVersion); } if (!Files.isWritable(jarPathJava)) { throw new RuntimeException(String.format("The existing %s is not writable. May need to sudo?", jarPathJava.toString())); } final URL downloadUrl = new URL(String.format("https://dl.bintray.com/embulk/maven/embulk-%s.jar", targetVersionString)); System.out.printf("Downloading %s ...\n", downloadUrl.toString()); Path jarPathTemp = Files.createTempFile("embulk-selfupdate", ".jar"); try { final HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection(); try { // Follow the redicrect from the Bintray URL. connection.setInstanceFollowRedirects(true); connection.setRequestMethod("GET"); connection.connect(); final int statusCode = connection.getResponseCode(); if (HttpURLConnection.HTTP_OK != statusCode) { throw new FileNotFoundException( String.format("Unexpected HTTP status code: %d", statusCode)); } InputStream input = connection.getInputStream(); // TODO(dmikurube): Confirm if it is okay to replace a temp file created by Files.createTempFile. Files.copy(input, jarPathTemp, StandardCopyOption.REPLACE_EXISTING); Files.setPosixFilePermissions(jarPathTemp, Files.getPosixFilePermissions(jarPathJava)); } finally { connection.disconnect(); } if (!isForced) { // Check corruption final String versionJarTemp; try { versionJarTemp = getJarVersion(jarPathTemp); } catch (FileNotFoundException ex) { throw new RuntimeException("Failed to check corruption. Downloaded version may include incompatible changes. Try the '-f' option to force updating without checking.", ex); } if (!versionJarTemp.equals(targetVersionString)) { throw new RuntimeException( String.format("Downloaded version does not match: %s (downloaded) / %s (target)", versionJarTemp, targetVersionString)); } } Files.move(jarPathTemp, jarPathJava, StandardCopyOption.REPLACE_EXISTING); } finally { Files.deleteIfExists(jarPathTemp); } System.out.println(String.format("Updated to %s.", targetVersionString)); } private RuntimeException exceptionNoSingleJar() { return new RuntimeException("Embulk is not installed as a single jar. \"selfupdate\" does not work. If you installed Embulk through gem, run \"gem install embulk\" instead."); } /** * Checks the latest version from bintray.com. * * It passes all {@code IOException} and {@code RuntimeException} through out. */ private String checkLatestVersion() throws IOException { final URL bintrayUrl = new URL("https://bintray.com/embulk/maven/embulk/_latestVersion"); final HttpURLConnection connection = (HttpURLConnection) bintrayUrl.openConnection(); try { // Stop HttpURLConnection from following redirects when the status code is 301 or 302. connection.setInstanceFollowRedirects(false); connection.setRequestMethod("GET"); connection.connect(); final int statusCode = connection.getResponseCode(); if (HttpURLConnection.HTTP_MOVED_TEMP != statusCode) { throw new FileNotFoundException( String.format("Unexpected HTTP status code: %d", statusCode)); } final String location = connection.getHeaderField("Location"); final Matcher versionMatcher = VERSION_URL_PATTERN.matcher(location); if (!versionMatcher.matches()) { throw new FileNotFoundException( String.format("Invalid version number in \"Location\" header: %s", location)); } return versionMatcher.group(1); } finally { connection.disconnect(); } } /** * Checks the target version in bintray.com. * * It passes all {@code IOException} and {@code RuntimeException} through out. */ private String checkTargetVersion(String version) throws IOException { final URL bintrayUrl = new URL(String.format("https://bintray.com/embulk/maven/embulk/%s", version)); final HttpURLConnection connection = (HttpURLConnection) bintrayUrl.openConnection(); try { connection.setInstanceFollowRedirects(false); connection.setRequestMethod("GET"); connection.connect(); final int statusCode = connection.getResponseCode(); if (HttpURLConnection.HTTP_NOT_FOUND == statusCode) { return null; } else if (HttpURLConnection.HTTP_OK != statusCode) { throw new FileNotFoundException( String.format("Unexpected HTTP status code: %d", statusCode)); } else { return version; } } finally { connection.disconnect(); } } private String getJarVersion(Path jarPath) throws IOException { try (final JarFile jarFile = new JarFile(jarPath.toFile())) { final Manifest manifest; try { manifest = jarFile.getManifest(); } catch (IOException ex) { throw new IOException("Version not found. Failed to load the manifest.", ex); } String manifestContents; try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { manifest.write(outputStream); manifestContents = outputStream.toString(); } catch (IOException ex) { manifestContents = "(Failed to read the contents of the manifest.)"; } final Attributes mainAttributes = manifest.getMainAttributes(); final String implementationVersion = mainAttributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION); if (implementationVersion == null) { throw new IOException("Version not found. Failed to read \"" + Attributes.Name.IMPLEMENTATION_VERSION + "\": " + manifestContents); } return implementationVersion; } catch (IOException ex) { throw new IOException("Version not found. Failed to load the jar file.", ex); } // NOTE: Checking embulk/version.rb is no longer needed. // The jar manifest with "Implementation-Version" has been included in Embulk jars from v0.4.0. } private static final Pattern VERSION_URL_PATTERN = Pattern.compile("^https?://.*/embulk/(\\d+\\.\\d+[^\\/]+).*$"); private static final Pattern VERSION_RUBY_PATTERN = Pattern.compile("^\\s*VERSION\\s*\\=\\s*(\\p{Graph}+)\\s*$"); }