RepositoryImpl.java
/*
* JPPF.
* Copyright (C) 2005-2019 JPPF Team.
* http://www.jppf.org
*
* Licensed 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.jppf.example.extendedclassloading;
import java.io.*;
import java.net.URL;
import java.util.*;
import org.jppf.classloader.AbstractJPPFClassLoader;
import org.jppf.utils.*;
import org.jppf.utils.streams.StreamUtils;
/**
* A simple implementation of a repository.
* <p>This repository is pesrsisted locally. The persistence only handles jar files in this implementation.
* These files are saved in a root directory specified in the constructor, with a flat structure (no sub-folders).
* The file names in this folder have the format <code><i>actual_jar_name</i>-<i>signature</i>.jar</code>.
* <p>In addition to the jar files, the persistence maintains a text file named 'toDelete.txt' which contains a list
* of jar files to delete upon loading of the repository. This is a workaround for the fact that on some OSes (e.g. Windows),
* the JVM keeps a lock on the jar files it uses, which makes it impossible to delete them as long as the JVM is alive.
* @author Laurent Cohen
*/
public class RepositoryImpl implements Repository {
/**
* Location where the libraries are stored on the local file system.
*/
private final String rootDir;
/**
* Location of the file listing libraries to delete on the local file system.
*/
private final String toDeleteFile;
/**
* A list of old library files to delete whenever possible.
*/
private Set<String> filesToDelete = new HashSet<>();
/**
* Create a repository persisted in the specified root folder.
* @param rootDir the folder where the libraries are stored on the local file system.
*/
public RepositoryImpl(final String rootDir) {
this.rootDir = rootDir;
checkOrCreateRootFolder();
toDeleteFile = rootDir + "/toDelete.txt";
loadFilesToDelete();
cleanup();
}
@Override
public URL[] download(final ClassPath classpath, final AbstractJPPFClassLoader cl) {
final URL[] urls = new URL[classpath.size()];
final List<String> toDownload = new ArrayList<>();
final List<Integer> toDownloadIndices = new ArrayList<>();
int idx = 0;
for (final Map.Entry<String, String> elt: classpath.elements().entrySet()) {
try {
final String name = elt.getKey();
final String signature = elt.getValue();
final File file = getLibFilePath(name, signature);
// is the file already in the repository ?
if (file.exists()) urls[idx] = file.toURI().toURL(); // yes: add it to the results
else {
// no: collect the file name and its position in the result array
toDownload.add(name);
toDownloadIndices.add(idx);
}
} catch (final Exception e) {
urls[idx] = null;
e.printStackTrace();
} finally {
idx++;
}
}
// download the missing files from the client and save them in the repository
if (!toDownload.isEmpty()) {
System.out.println("downloading files " + toDownload);
final String[] fileNames = toDownload.toArray(new String[toDownload.size()]);
// download the files from the remote client's classpath
final URL[] tempUrls = cl.getMultipleResources(fileNames);
// copy each downloaded file to a permanent location so it will survive a node restart
for (int i=0; i<toDownload.size(); i++) {
final String name = toDownload.get(i);
final int index = toDownloadIndices.get(i);
final URL tempUrl = tempUrls[i];
if (tempUrl != null) {
// compute the signature and save the file to rootDir/name-signature.jar
final String signature = ClassPathHelper.computeSignature(tempUrl);
try {
urls[index] = saveLibToFile(name, signature, tempUrl);
} catch (final Exception e) {
urls[index] = null;
System.out.println("could not copy '" + name + "' to a permanent location : " + ExceptionUtils.getMessage(e));
}
}
else System.out.println("library file '" + name + "' could not be downloaded from the client");
}
}
return urls;
}
/**
* Save a library file obtained from the resource cache of the node.
* @param fileName the name of the resource.
* @param signature the library file MD5 signature.
* @param tempUrl URl provided by the JPPF class loader which points to a location in the temporary cache.
* @return a URL pointing to the permanenent location where the resource is saved.
* @throws Exception if any error occurs.
*/
private URL saveLibToFile(final String fileName, final String signature, final URL tempUrl) throws Exception {
// save the file to the local directory
final File file = getLibFilePath(fileName, signature);
final OutputStream os = FileUtils.getFileOutputStream(file);
final InputStream is = tempUrl.openStream();
// copy the input stream into the output stream
StreamUtils.copyStream(is, os);
return file.toURI().toURL();
}
/**
* Compute the actual file path for a library, based on the original name and its signature.
* The resulting file path should be in the form <code>root_folder/<i>name</i>-<i>signature</i>.jar</code>
* @param libName the original name of the library.
* @param signature the file's signature.
* @return a normalized fie path.
*/
private File getLibFilePath(final String libName, final String signature) {
final StringBuilder sb = new StringBuilder();
sb.append(rootDir).append('/').append(getLibFileName(libName, signature));
return new File(sb.toString());
}
/**
* Compute the file name for a library, based on the original name and its signature.
* The resulting file name should be in the form <code><i>name</i>-<i>signature</i>.jar</code>
* @param libName the original name of the library.
* @param signature the file's signature.
* @return a file name as a string.
*/
private static String getLibFileName(final String libName, final String signature) {
final StringBuilder sb = new StringBuilder();
final int n = libName.lastIndexOf('.');
if (n >= 0) {
final String ext = libName.substring(n);
final String s = libName.substring(0, n);
sb.append(s).append('-').append(signature).append(ext);
} else sb.append(libName).append('-').append(signature);
return sb.toString();
}
@Override
public void delete(final RepositoryFilter filter) {
try {
final File dir = new File(rootDir + "/");
// get the list of all jar files in the repository
final File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(final File path) {
final String name = path.getName();
return name.endsWith(".jar");
}
});
// apply the filter to the list of files
for (final File file: files) {
final String filename = file.getName();
if (!filesToDelete.contains(filename)) {
// extract the original name and signature from the full file name
final int idx = filename.lastIndexOf('-');
final int idx2 = filename.lastIndexOf('.');
final String name = filename.substring(0, idx) + ".jar";
final String signature = filename.substring(idx + 1, idx2);
// if the file is accepted by the filer, add it to the list of files to delete
final boolean accepted = filter.accepts(name, signature);
if (accepted) filesToDelete.add(filename);
}
}
} finally {
// perform the actual file deletions
deleteFilesToDelete();
}
}
/**
* Load the list of libraries to delete from the file system.
*/
private void loadFilesToDelete() {
try {
final File file = new File(toDeleteFile);
if (!file.exists()) return;
final Reader reader = new BufferedReader(new FileReader(file));
// transform the text file into a set of strings and close the reader.
filesToDelete = new HashSet<>(FileUtils.textFileAsLines(reader));
deleteFilesToDelete();
} catch (@SuppressWarnings("unused") final IOException ignore) {
}
}
/**
* Save the list of old libraries to delete from the file system.
*/
private void saveFilesToDelete() {
final File file = new File(toDeleteFile);
if (filesToDelete.isEmpty()) {
if (file.exists()) file.delete();
} else {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(file));
for (String s: filesToDelete) writer.write(s + '\n');
} catch (@SuppressWarnings("unused") final IOException ignore) {
} finally {
StreamUtils.closeSilent(writer);
}
}
}
/**
* Delete the old libraries if possible.
*/
private void deleteFilesToDelete() {
try {
final File dir = new File(rootDir + "/");
final Iterator<String> it = filesToDelete.iterator();
while (it.hasNext()) {
final File file = new File(dir, it.next());
// remove the file from the list if it doesn't exist or if its deletion is successful
if (!file.exists() || file.delete()) it.remove();
}
} finally {
saveFilesToDelete();
}
}
@Override
public void cleanup() {
deleteFilesToDelete();
}
/**
* Check that the root folder for this repository exists and create it if it doesn't.
*/
private void checkOrCreateRootFolder() {
final File file = new File(rootDir);
if (!file.exists()) {
if (!file.mkdirs()) throw new IllegalStateException("could not create the repository root '" + rootDir + "'");
}
}
}