Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.apache.geode.internal; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Logger; import org.apache.geode.internal.logging.LogService; public class JarDeployer implements Serializable { private static final long serialVersionUID = 1L; private static final Logger logger = LogService.getLogger(); public static final String JAR_PREFIX_FOR_REGEX = ""; private static final Lock lock = new ReentrantLock(); private final Map<String, DeployedJar> deployedJars = new ConcurrentHashMap<>(); // Split a versioned filename into its name and version public static final Pattern versionedPattern = Pattern.compile(JAR_PREFIX_FOR_REGEX + "(.*)\\.v(\\d++).jar$"); private final File deployDirectory; public JarDeployer() { this.deployDirectory = new File(System.getProperty("user.dir")); } public JarDeployer(final File deployDirectory) { this.deployDirectory = deployDirectory; } public File getDeployDirectory() { return this.deployDirectory; } /** * Writes the jarBytes for the given jarName to the next version of that jar file (if the bytes do * not match the latest deployed version) * * @return the DeployedJar that was written from jarBytes, or null if those bytes matched the * latest deployed version */ public DeployedJar deployWithoutRegistering(final String jarName, final byte[] jarBytes) throws IOException { lock.lock(); try { boolean shouldDeployNewVersion = shouldDeployNewVersion(jarName, jarBytes); if (!shouldDeployNewVersion) { logger.debug("No need to deploy a new version of {}", jarName); return null; } verifyWritableDeployDirectory(); File newVersionedJarFile = getNextVersionedJarFile(jarName); writeJarBytesToFile(newVersionedJarFile, jarBytes); return new DeployedJar(newVersionedJarFile, jarName, jarBytes); } finally { lock.unlock(); } } /** * Get a list of all currently deployed jars. * * @return The list of DeployedJars */ public List<DeployedJar> findDeployedJars() { return getDeployedJars().values().stream().collect(toList()); } /** * Suspend all deploy and undeploy operations. This is done by acquiring and holding the lock * needed in order to perform a deploy or undeploy and so it will cause all threads attempting to * do one of these to block. This makes it somewhat of a time sensitive call as forcing these * other threads to block for an extended period of time may cause other unforeseen problems. It * must be followed by a call to {@link #resumeAll()}. */ public void suspendAll() { lock.lock(); } /** * Release the lock that controls entry into the deploy/undeploy methods which will allow those * activities to continue. */ public void resumeAll() { lock.unlock(); } protected File getNextVersionedJarFile(String unversionedJarName) { File[] oldVersions = findSortedOldVersionsOfJar(unversionedJarName); String nextVersionedJarName; if (oldVersions == null || oldVersions.length == 0) { nextVersionedJarName = removeJarExtension(unversionedJarName) + ".v1.jar"; } else { String latestVersionedJarName = oldVersions[0].getName(); int nextVersion = extractVersionFromFilename(latestVersionedJarName) + 1; nextVersionedJarName = removeJarExtension(unversionedJarName) + ".v" + nextVersion + ".jar"; } logger.debug("Next versioned jar name for {} is {}", unversionedJarName, nextVersionedJarName); return new File(deployDirectory, nextVersionedJarName); } /** * Attempt to write the given bytes to the given file. If this VM is able to successfully write * the contents to the file, or another VM writes the exact same contents, then the write is * considered to be successful. * * @param file File of the JAR file to deploy. * @param jarBytes Contents of the JAR file to deploy. * @return True if the file was successfully written, false otherwise */ private boolean writeJarBytesToFile(final File file, final byte[] jarBytes) throws IOException { final boolean isDebugEnabled = logger.isDebugEnabled(); if (file.createNewFile()) { if (isDebugEnabled) { logger.debug("Successfully created new JAR file: {}", file.getAbsolutePath()); } final OutputStream outStream = new FileOutputStream(file); outStream.write(jarBytes); outStream.close(); return true; } return doesFileMatchBytes(file, jarBytes); } /** * Determine if the contents of the file referenced is an exact match for the bytes provided. The * method first checks to see if the file is actively being written by checking the length over * time. If it appears that the file is actively being written, then it loops waiting for that to * complete before doing the comparison. * * @param file File to compare * @param bytes Bytes to compare * @return True if there's an exact match, false otherwise * @throws IOException If there's a problem reading the file */ private boolean doesFileMatchBytes(final File file, final byte[] bytes) throws IOException { // First check to see if the file is actively being written (if it's not big enough) final String absolutePath = file.getAbsolutePath(); boolean keepTrying = true; final boolean isDebugEnabled = logger.isDebugEnabled(); while (file.length() < bytes.length && keepTrying) { if (isDebugEnabled) { logger.debug("Loop waiting for another to write file: {}", absolutePath); } long startingFileLength = file.length(); try { Thread.sleep(500); } catch (InterruptedException iex) { // Just keep looping } if (startingFileLength == file.length()) { if (isDebugEnabled) { logger.debug("Done waiting for another to write file: {}", absolutePath); } // Assume the other process has finished writing keepTrying = false; } } // If they don't have the same number of bytes then nothing to do if (file.length() != bytes.length) { if (isDebugEnabled) { logger.debug("Unmatching file length when waiting for another to write file: {}", absolutePath); } return false; } // Open the file then loop comparing each byte BufferedInputStream inStream = new BufferedInputStream(new FileInputStream(file)); int index = 0; try { for (; index < bytes.length; index++) { if (((byte) inStream.read()) != bytes[index]) { if (isDebugEnabled) { logger.debug("Did not find a match when waiting for another to write file: {}", absolutePath); } return false; } } } finally { inStream.close(); } return true; } /** * Find the version number that's embedded in the name of this file * * @param filename Filename to get the version number from * @return The version number embedded in the filename */ public static int extractVersionFromFilename(final String filename) { final Matcher matcher = versionedPattern.matcher(filename); if (matcher.find()) { return Integer.parseInt(matcher.group(2)); } else { return 0; } } protected Set<String> findDistinctDeployedJarsOnDisk() { // Find all deployed JAR files final File[] oldFiles = this.deployDirectory .listFiles((file, name) -> versionedPattern.matcher(name).matches()); // Now add just the original JAR name to the set final Set<String> jarNames = new HashSet<>(); for (File oldFile : oldFiles) { Matcher matcher = versionedPattern.matcher(oldFile.getName()); matcher.find(); jarNames.add(matcher.group(1) + ".jar"); } return jarNames; } /** * Find all versions of the JAR file that are currently on disk and return them sorted from newest * (highest version) to oldest * * @param unversionedJarName Name of the JAR file that we want old versions of * @return Sorted array of files that are older versions of the given JAR */ protected File[] findSortedOldVersionsOfJar(final String unversionedJarName) { logger.debug("Finding sorted old versions of {}", unversionedJarName); // Find all matching files final Pattern pattern = Pattern .compile(JAR_PREFIX_FOR_REGEX + removeJarExtension(unversionedJarName) + "\\.v\\d++\\.jar$"); final File[] oldJarFiles = this.deployDirectory .listFiles((file, name) -> (pattern.matcher(name).matches())); // Sort them in order from newest (highest version) to oldest Arrays.sort(oldJarFiles, (file1, file2) -> { int file1Version = extractVersionFromFilename(file1.getName()); int file2Version = extractVersionFromFilename(file2.getName()); return file2Version - file1Version; }); logger.debug("Found [{}]", Arrays.stream(oldJarFiles).map(File::getAbsolutePath).collect(joining(","))); return oldJarFiles; } protected String removeJarExtension(String jarName) { if (jarName != null && jarName.endsWith(".jar")) { return jarName.replaceAll("\\.jar$", ""); } else { return jarName; } } /** * Make sure that the deploy directory is writable. * * @throws IOException If the directory isn't writable */ public void verifyWritableDeployDirectory() throws IOException { try { if (this.deployDirectory.canWrite()) { return; } } catch (SecurityException ex) { throw new IOException("Unable to write to deploy directory", ex); } throw new IOException("Unable to write to deploy directory: " + this.deployDirectory.getCanonicalPath()); } final Pattern oldNamingPattern = Pattern.compile("^vf\\.gf#(.*)\\.jar#(\\d+)$"); /* * In Geode 1.1.0, the deployed version of 'myjar.jar' would be named 'vf.gf#myjar.jar#1'. Now it * is be named 'myjar.v1.jar'. We need to rename all existing deployed jars to the new convention * if this is the first time starting up with the new naming format. */ protected void renameJarsWithOldNamingConvention() throws IOException { Set<File> jarsWithOldNamingConvention = findJarsWithOldNamingConvention(); if (jarsWithOldNamingConvention.isEmpty()) { return; } for (File jar : jarsWithOldNamingConvention) { renameJarWithOldNamingConvention(jar); } } protected Set<File> findJarsWithOldNamingConvention() { return Stream.of(this.deployDirectory.listFiles()) .filter((File file) -> isOldNamingConvention(file.getName())).collect(toSet()); } protected boolean isOldNamingConvention(String fileName) { return oldNamingPattern.matcher(fileName).matches(); } private void renameJarWithOldNamingConvention(File oldJar) throws IOException { Matcher matcher = oldNamingPattern.matcher(oldJar.getName()); if (!matcher.matches()) { throw new IllegalArgumentException( "The given jar " + oldJar.getCanonicalPath() + " does not match the old naming convention"); } String unversionedJarNameWithoutExtension = matcher.group(1); String jarVersion = matcher.group(2); String newJarName = unversionedJarNameWithoutExtension + ".v" + jarVersion + ".jar"; File newJar = new File(this.deployDirectory, newJarName); logger.debug("Renaming deployed jar from {} to {}", oldJar.getCanonicalPath(), newJar.getCanonicalPath()); FileUtils.moveFile(oldJar, newJar); } /** * Re-deploy all previously deployed JAR files on disk. */ public void loadPreviouslyDeployedJarsFromDisk() { logger.info("Loading previously deployed jars"); lock.lock(); try { verifyWritableDeployDirectory(); renameJarsWithOldNamingConvention(); final Set<String> jarNames = findDistinctDeployedJarsOnDisk(); if (jarNames.isEmpty()) { return; } List<DeployedJar> latestVersionOfEachJar = new ArrayList<>(); for (String jarName : jarNames) { DeployedJar deployedJar = findLatestValidDeployedJarFromDisk(jarName); if (deployedJar != null) { latestVersionOfEachJar.add(deployedJar); deleteOtherVersionsOfJar(deployedJar); } } registerNewVersions(latestVersionOfEachJar); } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } /** * Deletes all versions of this jar on disk other than the given version */ public void deleteOtherVersionsOfJar(DeployedJar deployedJar) { logger.info("Deleting all versions of " + deployedJar.getJarName() + " other than " + deployedJar.getFileName()); final File[] jarFiles = findSortedOldVersionsOfJar(deployedJar.getJarName()); Stream.of(jarFiles).filter(jarFile -> !jarFile.equals(deployedJar.getFile())).forEach(jarFile -> { logger.info("Deleting old version of jar: " + jarFile.getAbsolutePath()); FileUtils.deleteQuietly(jarFile); }); } public DeployedJar findLatestValidDeployedJarFromDisk(String unversionedJarName) throws IOException { final File[] jarFiles = findSortedOldVersionsOfJar(unversionedJarName); Optional<File> latestValidDeployedJarOptional = Arrays.stream(jarFiles).filter(Objects::nonNull) .filter(jarFile -> { try { return DeployedJar.hasValidJarContent(FileUtils.readFileToByteArray(jarFile)); } catch (IOException e) { return false; } }).findFirst(); if (!latestValidDeployedJarOptional.isPresent()) { // No valid version of this jar return null; } File latestValidDeployedJar = latestValidDeployedJarOptional.get(); return new DeployedJar(latestValidDeployedJar, unversionedJarName); } public URL[] getDeployedJarURLs() { return this.deployedJars.values().stream().map(DeployedJar::getFileURL).toArray(URL[]::new); } public List<DeployedJar> registerNewVersions(List<DeployedJar> deployedJars) throws ClassNotFoundException { lock.lock(); try { Map<DeployedJar, DeployedJar> newVersionToOldVersion = new HashMap<>(); for (DeployedJar deployedJar : deployedJars) { if (deployedJar != null) { logger.info("Registering new version of jar: {}", deployedJar); DeployedJar oldJar = this.deployedJars.put(deployedJar.getJarName(), deployedJar); newVersionToOldVersion.put(deployedJar, oldJar); } } ClassPathLoader.getLatest().rebuildClassLoaderForDeployedJars(); // Finally, unregister functions that were removed for (Map.Entry<DeployedJar, DeployedJar> entry : newVersionToOldVersion.entrySet()) { DeployedJar newjar = entry.getKey(); DeployedJar oldJar = entry.getValue(); newjar.registerFunctions(); if (oldJar != null) { oldJar.cleanUp(newjar); } } } finally { lock.unlock(); } return deployedJars; } /** * Deploy the given JAR files. * * @param jarNames Array of names of the JAR files to deploy. * @param jarBytes Array of contents of the JAR files to deploy. * @return An array of newly created JAR class loaders. Entries will be null for an JARs that were * already deployed. * @throws IOException When there's an error saving the JAR file to disk */ public List<DeployedJar> deploy(final String jarNames[], final byte[][] jarBytes) throws IOException, ClassNotFoundException { DeployedJar[] deployedJars = new DeployedJar[jarNames.length]; for (int i = 0; i < jarNames.length; i++) { if (!DeployedJar.hasValidJarContent(jarBytes[i])) { throw new IllegalArgumentException("File does not contain valid JAR content: " + jarNames[i]); } } lock.lock(); try { for (int i = 0; i < jarNames.length; i++) { String jarName = jarNames[i]; byte[] newJarBytes = jarBytes[i]; deployedJars[i] = deployWithoutRegistering(jarName, newJarBytes); } return registerNewVersions(Arrays.asList(deployedJars)); } finally { lock.unlock(); } } private boolean shouldDeployNewVersion(String jarName, byte[] newJarBytes) throws IOException { DeployedJar oldDeployedJar = this.deployedJars.get(jarName); if (oldDeployedJar == null) { return true; } if (oldDeployedJar.hasSameContentAs(newJarBytes)) { logger.warn("Jar is identical to the latest deployed version: {}", oldDeployedJar.getFileCanonicalPath()); return false; } return true; } /** * Returns the latest registered {@link DeployedJar} for the given JarName * * @param jarName - the unversioned jar name, e.g. myJar.jar */ public DeployedJar findDeployedJar(String jarName) { return this.deployedJars.get(jarName); } public DeployedJar deploy(final String jarName, final byte[] jarBytes) throws IOException, ClassNotFoundException { lock.lock(); try { List<DeployedJar> deployedJars = deploy(new String[] { jarName }, new byte[][] { jarBytes }); if (deployedJars == null || deployedJars.size() == 0) { return null; } return deployedJars.get(0); } finally { lock.unlock(); } } public Map<String, DeployedJar> getDeployedJars() { return Collections.unmodifiableMap(this.deployedJars); } /** * Undeploy the given JAR file. * * @param jarName The name of the JAR file to undeploy * @return The path to the location on disk where the JAR file had been deployed * @throws IOException If there's a problem deleting the file */ public String undeploy(final String jarName) throws IOException { lock.lock(); try { DeployedJar deployedJar = deployedJars.remove(jarName); if (deployedJar == null) { throw new IllegalArgumentException("JAR not deployed"); } ClassPathLoader.getLatest().rebuildClassLoaderForDeployedJars(); deployedJar.cleanUp(null); deleteAllVersionsOfJar(jarName); return deployedJar.getFileCanonicalPath(); } finally { lock.unlock(); } } public void deleteAllVersionsOfJar(String unversionedJarName) { lock.lock(); try { File[] jarFiles = findSortedOldVersionsOfJar(unversionedJarName); for (File jarFile : jarFiles) { logger.info("Deleting: {}", jarFile.getAbsolutePath()); FileUtils.deleteQuietly(jarFile); } } finally { lock.unlock(); } } }