org.apache.geode.deployment.internal.JarDeployer Maven / Gradle / Ivy
/*
* 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.deployment.internal;
import static java.util.stream.Collectors.toSet;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
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.commons.io.FilenameUtils;
import org.apache.logging.log4j.Logger;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.internal.utils.JarFileUtils;
public class JarDeployer implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LogService.getLogger();
// The pound version scheme predates the sequenced version scheme
private static final Pattern POUND_VERSION_SCHEME =
Pattern.compile("^vf\\.gf#(?.*)\\.jar#(?\\d+)$");
@MakeNotStatic
private static final Lock lock = new ReentrantLock();
private final Map deployedJars = new ConcurrentHashMap<>();
private final File deployDirectory;
public JarDeployer() {
this(new File(System.getProperty("user.dir")));
}
public JarDeployer(final File deployDirectory) {
if (deployDirectory == null) {
this.deployDirectory = new File(System.getProperty("user.dir"));
} else {
this.deployDirectory = 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(String artifactId, final File stagedJar)
throws IOException {
lock.lock();
try {
boolean shouldDeployNewVersion = shouldDeployNewVersion(artifactId, stagedJar);
if (!shouldDeployNewVersion) {
logger.debug("No need to deploy a new version of {}", stagedJar.getName());
return null;
}
verifyWritableDeployDirectory();
Path deployedFile = getNextVersionedJarFile(stagedJar.getName()).toPath();
FileUtils.copyFile(stagedJar, deployedFile.toFile());
return new DeployedJar(deployedFile.toFile());
} finally {
lock.unlock();
}
}
protected File getNextVersionedJarFile(String unversionedJarName) {
int maxVersion = getMaxVersion(JarFileUtils.getArtifactId(unversionedJarName));
String nextVersionJarName =
FilenameUtils.getBaseName(unversionedJarName) + ".v" + (maxVersion + 1) + ".jar";
logger.debug("Next versioned jar name for {} is {}", unversionedJarName, nextVersionJarName);
return new File(deployDirectory, nextVersionJarName);
}
protected int getMaxVersion(String artifactId) {
return Arrays.stream(deployDirectory.list()).filter(x -> artifactId.equals(
JarFileUtils.toArtifactId(x)))
.map(JarFileUtils::extractVersionFromFilename)
.reduce(Integer::max).orElse(0);
}
/**
* Make sure that the deploy directory is writable.
*
* @throws IOException If the directory isn't writable
*/
public void verifyWritableDeployDirectory() throws IOException {
try {
if (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: " + deployDirectory.getCanonicalPath());
}
/*
* 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 jarsWithOldNamingConvention = findJarsWithOldNamingConvention();
if (jarsWithOldNamingConvention.isEmpty()) {
return;
}
for (File jar : jarsWithOldNamingConvention) {
renameJarWithOldNamingConvention(jar);
}
}
protected Set findJarsWithOldNamingConvention() {
return Stream.of(deployDirectory.listFiles())
.filter((File file) -> isOldNamingConvention(file.getName())).collect(toSet());
}
protected boolean isOldNamingConvention(String fileName) {
return POUND_VERSION_SCHEME.matcher(fileName).matches();
}
private void renameJarWithOldNamingConvention(File oldJar) throws IOException {
Matcher matcher = POUND_VERSION_SCHEME.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(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.
* It will clean up the old version of deployed jars that are in the deployed directory
*/
public Map getLatestVersionOfJarsOnDisk() {
logger.info("Loading previously deployed jars");
lock.lock();
try {
verifyWritableDeployDirectory();
renameJarsWithOldNamingConvention();
// find all the artifacts and its max versions
Map artifactToMaxVersion = findArtifactsAndMaxVersion();
Map latestVersionOfEachJar = new HashMap<>();
// clean up the old versions and find the latest version of each jar
for (File file : deployDirectory.listFiles()) {
String artifactId = JarFileUtils.toArtifactId(file.getName());
if (artifactId == null) {
continue;
}
int version = JarFileUtils.extractVersionFromFilename(file.getName());
if (version < artifactToMaxVersion.get(artifactId)) {
FileUtils.deleteQuietly(file);
} else {
DeployedJar deployedJar = new DeployedJar(file);
latestVersionOfEachJar.put(deployedJar.getArtifactId(), deployedJar);
}
}
return latestVersionOfEachJar;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
Map findArtifactsAndMaxVersion() {
Map artifactToMaxVersion = new HashMap<>();
for (String fileName : deployDirectory.list()) {
String artifactId = JarFileUtils.toArtifactId(fileName);
if (artifactId == null) {
continue;
}
int version = JarFileUtils.extractVersionFromFilename(fileName);
Integer maxVersion = artifactToMaxVersion.get(artifactId);
if (maxVersion == null || maxVersion < version) {
artifactToMaxVersion.put(artifactId, version);
}
}
return artifactToMaxVersion;
}
public void registerNewVersions(String artifactId, DeployedJar deployedJar)
throws ClassNotFoundException {
lock.lock();
try {
if (deployedJar != null) {
logger.info("Registering new version of jar: {}", deployedJar);
DeployedJar oldJar = deployedJars.put(artifactId, deployedJar);
ClassPathLoader.getLatest().chainClassloader(deployedJar.getFile());
}
} finally {
lock.unlock();
}
}
/**
* Deploy the given JAR files.
*
* When deploying a jar file, it will always append a sequence number .v to the end of
* the file, no matter how the original file is named. This is to allow server on startup to
* know what's the last version that gets deployed without cluster configuration.
*
* @param stagedJarFile A File which have been staged in another location and is ready
* to be deployed.
* @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 DeployedJar deploy(final File stagedJarFile)
throws IOException, ClassNotFoundException {
if (!JarFileUtils.hasValidJarContent(stagedJarFile)) {
throw new IllegalArgumentException(
"File does not contain valid JAR content: " + stagedJarFile.getName());
}
lock.lock();
try {
String artifactId = JarFileUtils.getArtifactId(stagedJarFile.getName());
DeployedJar deployedJar = deployWithoutRegistering(artifactId, stagedJarFile);
registerNewVersions(artifactId, deployedJar);
return deployedJar;
} finally {
lock.unlock();
}
}
private boolean shouldDeployNewVersion(String artifactId, File stagedJar) throws IOException {
DeployedJar oldDeployedJar = deployedJars.get(artifactId);
if (oldDeployedJar == null) {
return true;
}
if (oldDeployedJar.hasSameContentAs(stagedJar)) {
logger.warn("Jar is identical to the latest deployed version: {}",
oldDeployedJar.getFileCanonicalPath());
return false;
}
return true;
}
public Map getDeployedJars() {
return Collections.unmodifiableMap(deployedJars);
}
/**
* Undeploy the jar file identified by the given artifact ID.
*
* @param artifactId The artifact 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(String artifactId) throws IOException {
lock.lock();
logger.debug("JarDeployer Undeploying artifactId: {}", artifactId);
try {
logger.debug("JarDeployer deployedJars list before remove: {}",
Arrays.toString(deployedJars.keySet().toArray()));
// remove the deployedJar
DeployedJar deployedJar = deployedJars.remove(artifactId);
if (deployedJar == null) {
throw new IllegalArgumentException(artifactId + " not deployed");
}
logger.debug("JarDeployer deployedJars list after remove: {}",
Arrays.toString(deployedJars.keySet().toArray()));
ClassPathLoader.getLatest().unloadClassloaderForArtifact(artifactId);
deleteAllVersionsOfJar(deployedJar.getFile().getName());
return deployedJar.getFileCanonicalPath();
} finally {
lock.unlock();
}
}
/**
* @param jarName a user deployed jar name (abc.jar or abc-1.0.jar)
*/
public void deleteAllVersionsOfJar(String jarName) {
lock.lock();
logger.info("Deleting all versions of jar: {}", jarName);
String artifactId = JarFileUtils.toArtifactId(jarName);
if (artifactId == null) {
artifactId = JarFileUtils.getArtifactId(jarName);
}
logger.debug("ArtifactId to delete: {}", artifactId);
try {
for (File file : deployDirectory.listFiles()) {
logger.debug("File in deploy directory: {} with artifactId: {}", file.getName(),
JarFileUtils.toArtifactId(file.getName()));
if (artifactId.equals(JarFileUtils.toArtifactId(file.getName()))) {
logger.info("Deleting: {}", file.getAbsolutePath());
FileUtils.deleteQuietly(file);
}
}
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return getClass().getName() + '@' + System.identityHashCode(this) + '{'
+ "deployDirectory=" + deployDirectory
+ '}';
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy