/* * 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.openejb.util; import org.apache.openejb.OpenEJBRuntimeException; import org.apache.openejb.loader.FileUtils; import org.apache.openejb.loader.Files; import org.apache.openejb.loader.IO; import org.apache.openejb.loader.SystemInstance; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; public class UrlCache { private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB, UrlCache.class); public static final boolean antiJarLocking; public static final File cacheDir; static { antiJarLocking = SystemInstance.get().getOptions().get("antiJarLocking", false); if (antiJarLocking) { cacheDir = createCacheDir(); logger.info("AntiJarLocking enabled. Using URL cache dir " + cacheDir); } else { cacheDir = null; } } private final Map<String, Map<URL, File>> cache = new TreeMap<String, Map<URL, File>>(); public synchronized URL[] cacheUrls(final String appId, final URL[] urls) { if (!antiJarLocking) { return urls; } // the final cached urls final LinkedHashSet<URL> cachedUrls = new LinkedHashSet<URL>(); // this stack contains the urls to be processed... when manifest class path entries // are added they are added to the top (front) of the stack so manifest order is maintained final LinkedList<URL> locationStack = new LinkedList<URL>(Arrays.asList(urls)); while (!locationStack.isEmpty()) { final URL url = locationStack.removeFirst(); // Skip any duplicate urls in the claspath if (cachedUrls.contains(url)) { continue; } // cache the URL final File file = cacheUrl(appId, url); // if the url was successfully cached, process it's manifest classpath if (file != null) { try { cachedUrls.add(file.toURI().toURL()); // push the manifest classpath on the stack (make sure to maintain the order) final List<URL> manifestClassPath = getManifestClassPath(url, file); locationStack.addAll(0, manifestClassPath); } catch (final MalformedURLException e) { // invalid cache file - this should never happen logger.error("Error caching url. Original jar file will be used which may result in a file lock: url=" + url, e); cachedUrls.add(url); } } else { // URL was not cached - simply pass through the url cachedUrls.add(url); } } return cachedUrls.toArray(new URL[cachedUrls.size()]); } public synchronized void releaseUrls(final String appId) { logger.debug("Releasing URLs for application " + appId); final Map<URL, File> urlFileMap = cache.remove(appId); if (urlFileMap != null) { for (final File file : urlFileMap.values()) { if (file.delete()) { logger.debug("Deleted cached file " + file); } else { logger.debug("Unable to delete cached file " + file); } } } } public File getUrlCachedName(final String appId, final URL url) { final Map<URL, File> appCache = getAppCache(appId); if (appCache.containsKey(url)) { return appCache.get(url); } return null; } public boolean isUrlCached(final String appId, final URL url) { final Map<URL, File> appCache = getAppCache(appId); return appCache.containsKey(url); } public URL getUrlKeyCached(final String appId, final File file) { if (file == null) { return null; } final Map<URL, File> appCache = getAppCache(appId); for (final Map.Entry<URL, File> entry : appCache.entrySet()) { if (entry.getValue().equals(file)) { return entry.getKey(); } } final URL keyUrl; try { keyUrl = file.toURI().toURL(); } catch (final MalformedURLException e) { return null; } if (appCache.containsKey(keyUrl)) { return keyUrl; } return null; } private synchronized File cacheUrl(final String appId, URL url) { File sourceFile; if (!"file".equals(url.getProtocol())) { // todo: download the jar ourselves? // for now return null which means we did not cache return null; } else { // verify file sourceFile = URLs.toFile(url); if (!sourceFile.exists()) { return null; } if (!sourceFile.canRead()) { return null; } // if file is a directory, there is no need to cache if (sourceFile.isDirectory()) { return sourceFile; } // Create absolute file URL sourceFile = sourceFile.getAbsoluteFile(); try { url = sourceFile.toURI().toURL(); } catch (final MalformedURLException ignored) { // no-op } } // check if file is already cached final Map<URL, File> appCache = getAppCache(appId); if (appCache.containsKey(url)) { return appCache.get(url); } // if the file is already in the cache, don't recopy it to the cache dir if (sourceFile.getParentFile().equals(cacheDir)) { // mark it as part of the application, so it cleaned up when the application is undeployed appCache.put(url, sourceFile); return sourceFile; } // generate a nice cache file name final String name = sourceFile.getName(); final int dot = name.lastIndexOf("."); String prefix = name; String suffix = ""; if (dot > 0) { prefix = name.substring(0, dot) + "-"; suffix = name.substring(dot, name.length()); } // copy the file to the cache dir to avoid file locks File cacheFile = null; boolean success; try { try { cacheFile = File.createTempFile(prefix, suffix, cacheDir); } catch (final Throwable e) { final File tmp = new File("tmp"); if (!tmp.exists() && !tmp.mkdirs()) { throw new IOException("Failed to create local tmp directory: " + tmp.getAbsolutePath()); } cacheFile = File.createTempFile(prefix, suffix, tmp); } cacheFile.deleteOnExit(); success = JarExtractor.copyRecursively(sourceFile, cacheFile); } catch (final IOException e) { success = false; } if (success) { // add cache file to cache appCache.put(url, cacheFile); logger.debug("Coppied jar file to " + cacheFile); return cacheFile; } else { // clean up failed copy JarExtractor.delete(cacheFile); logger.error("Unable to copy jar into URL cache directory. Original jar file will be used which may result in a file lock: file=" + sourceFile); return null; } } private synchronized Map<URL, File> getAppCache(final String appId) { Map<URL, File> urlFileMap = cache.get(appId); if (urlFileMap == null) { urlFileMap = new LinkedHashMap<URL, File>(); cache.put(appId, urlFileMap); } return urlFileMap; } private List<URL> getManifestClassPath(final URL codeSource, final File location) { try { // get the manifest, if possible final Manifest manifest = loadManifest(location); if (manifest == null) { // some locations don't have a manifest return Collections.emptyList(); } // get the class-path attribute, if possible final String manifestClassPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); if (manifestClassPath == null) { return Collections.emptyList(); } // build the urls... // the class-path attribute is space delimited final LinkedList<URL> classPathUrls = new LinkedList<URL>(); for (final StringTokenizer tokenizer = new StringTokenizer(manifestClassPath, " "); tokenizer.hasMoreTokens(); ) { final String entry = tokenizer.nextToken(); try { // the class path entry is relative to the resource location code source final URL entryUrl = new URL(codeSource, entry); classPathUrls.addLast(entryUrl); } catch (final MalformedURLException ignored) { // most likely a poorly named entry } } return classPathUrls; } catch (final IOException ignored) { // error opening the manifest return Collections.emptyList(); } } private Manifest loadManifest(final File location) throws IOException { if (location.isDirectory()) { final File manifestFile = new File(location, "META-INF/MANIFEST.MF"); if (manifestFile.isFile() && manifestFile.canRead()) { InputStream in = null; try { in = IO.read(manifestFile); return new Manifest(in); } finally { close(in); } } } else { final JarFile jarFile = new JarFile(location); try { return jarFile.getManifest(); } finally { close(jarFile); } } return null; } private static File createCacheDir() { try { final FileUtils openejbBase = SystemInstance.get().getBase(); File dir = null; // if we are not embedded, cache (temp) dir is under base dir if (SystemInstance.get().getConf(null).exists()) { try { dir = openejbBase.getDirectory("temp"); } catch (final IOException e) { //Ignore } } // if we are embedded, tmp dir is in the system tmp dir if (dir == null) { dir = Files.tmpdir(); } // If the cache dir already exists then empty its contents if (dir.exists()) { final File[] files = dir.listFiles(); if (null != files) { for (final File f : files) { deleteDir(f); } } } else { dir = createCacheDir(new File(dir.getAbsolutePath())); } return dir; } catch (final IOException e) { throw new OpenEJBRuntimeException(e); } } private static File createCacheDir(final File dir) throws IOException { if (dir.exists() && dir.isDirectory()) { return dir; } if (dir.exists() && !dir.isDirectory()) { throw new IOException("Cache temp directory held by file: " + dir); } if (!dir.mkdirs()) { throw new IOException("Unable to create cache temp directory: " + dir); } Thread.yield(); return dir; } /** * Delete the specified directory, including all of its contents and * subdirectories recursively. * * @param dir File object representing the directory to be deleted */ public static void deleteDir(final File dir) { if (dir == null) { return; } final File[] fileNames = dir.listFiles(); if (fileNames != null) { for (final File file : fileNames) { if (file.isDirectory()) { deleteDir(file); } else { if (file.delete()) { logger.debug("Deleted file " + file); } else { logger.debug("Unable to delete file " + file); } } } } if (dir.delete()) { logger.debug("Deleted file " + dir); } else { logger.debug("Unable to delete file " + dir); } } private static void close(final Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (final IOException ignored) { // no-op } } } private static void close(final JarFile closeable) { if (closeable != null) { try { closeable.close(); } catch (final IOException ignored) { // no-op } } } }