| /* |
| * Copyright (c) 2006, 2018 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0, which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the |
| * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, |
| * version 2 with the GNU Classpath Exception, which is available at |
| * https://www.gnu.org/software/classpath/license.html. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| */ |
| |
| package com.sun.enterprise.deploy.shared; |
| |
| import com.sun.enterprise.deployment.deploy.shared.Util; |
| import com.sun.enterprise.util.io.FileUtils; |
| |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import org.glassfish.api.deployment.archive.Archive; |
| import org.glassfish.api.deployment.archive.ReadableArchive; |
| import org.glassfish.api.deployment.archive.WritableArchive; |
| |
| import jakarta.inject.Inject; |
| |
| import org.jvnet.hk2.annotations.Service; |
| import org.glassfish.hk2.api.PerLookup; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.util.jar.JarFile; |
| import java.util.jar.Manifest; |
| import java.net.URI; |
| import java.nio.file.Files; |
| |
| import org.glassfish.logging.annotation.LogMessageInfo; |
| import org.glassfish.logging.annotation.LoggerInfo; |
| import org.glassfish.logging.annotation.LogMessagesResourceBundle; |
| |
| /** |
| * This implementation of the Archive interface maps to a directory/file |
| * structure. |
| * <p> |
| * If the directory underlying the FileArchive is created by GlassFish |
| * then FileArchive filters its contents so only |
| * those files more recent than the creation of the archive itself are visible to |
| * consumers. |
| * <p> |
| * The main motivation is to hide unwanted "left-over" files |
| * from previous deployments that might linger, especially on Windows, |
| * after the previous app had been undeployed. (Deployment uses a FileArchive |
| * to extract the user's JAR-based archive into the applications directory.) |
| * Historically such left-over files arise after GlassFish expands an archive |
| * into its exploded form but then some |
| * code opens but does not close a file in that exploded directory tree. |
| * <p> |
| * An open left-over file can be overwritten-in-place on Windows, and |
| * this happens when a caller invokes {@link #putNextEntry(java.lang.String) } |
| * to create a new entry (file) inside the archive. But a |
| * left-over file that is not in the new app but is |
| * still open by GlassFish cannot be deleted or renamed on Windows and so it will |
| * remain in the expansion directory. Such left-over files, if not filtered out, |
| * can confuse GlassFish and the application. By "stamping" the archive |
| * creation date we can filter out such old, left-over files. |
| * <p> |
| * To support this feature, when FileArchive creates a directory it stores a |
| * marker file there, the contents of which records the creation date/time of |
| * the archive. We cannot just use the lastModified value for the top-level |
| * directory. Users might legitimately use "touch .reload" in the applications/appName |
| * directory to trigger a dynamic reload of the app. If .reload does not already |
| * exist then touch creates it, and this would update the lastModified of the |
| * directory file. |
| * |
| * @author Jerome Dochez |
| * @author Tim Quinn |
| */ |
| @Service(name="file") |
| @PerLookup |
| public class FileArchive extends AbstractReadableArchive implements WritableArchive { |
| |
| private final static Level DEBUG_LEVEL = Level.FINE; |
| |
| @Inject |
| ArchiveFactory archiveFactory; |
| |
| // the archive abstraction directory. |
| File archive = null; |
| URI uri = null; |
| |
| // the currently opened entry |
| OutputStream os=null; |
| |
| public static final Logger deplLogger = org.glassfish.deployment.common.DeploymentContextImpl.deplLogger; |
| |
| @LogMessageInfo(message = "Attempt to list files in {0} failed, perhaps because that is not a valid directory or because file permissions do not allow GlassFish to access it", level="WARNING") |
| private static final String FILE_LIST_FAILURE = "NCLS-DEPLOYMENT-00022"; |
| |
| @LogMessageInfo(message = "Ignoring {0} because the containing archive {1} recorded it as a pre-existing stale file", level="WARNING") |
| private static final String STALE_FILES_SKIPPED = "NCLS-DEPLOYMENT-00023"; |
| |
| /* |
| * tracks stale files in the archive and filters the archive's contents to |
| * exclude stale entries |
| */ |
| private StaleFileManager staleFileManager; |
| |
| /* |
| * Records whether open or create has been invoked. Otherwise we can't |
| * be sure that the staleFileManager field has been set. |
| */ |
| private boolean isOpenedOrCreated = false; |
| |
| /** |
| * Open an abstract archive |
| * @param uri path to the archive |
| */ |
| @Override |
| public void open(URI uri) throws IOException { |
| if (!uri.getScheme().equals("file")) { |
| throw new IOException("Wrong scheme for FileArchive : " + uri.getScheme()); |
| } |
| this.uri = uri; |
| archive = new File(uri); |
| if (!archive.exists()) { |
| throw new FileNotFoundException(uri.getSchemeSpecificPart()); |
| } |
| isOpenedOrCreated = true; |
| staleFileManager = StaleFileManager.Util.getInstance(archive); |
| } |
| |
| /** |
| * @see #open(URI) |
| * @param uri a string representing URI |
| */ |
| public void open(String uri) throws IOException |
| { |
| open(URI.create(uri)); |
| } |
| |
| /** |
| * Get the size of the archive |
| * @return tje the size of this archive or -1 on error |
| */ |
| @Override |
| public long getArchiveSize() throws NullPointerException, SecurityException { |
| if(uri == null) { |
| return -1; |
| } |
| File tmpFile = new File(uri); |
| return(tmpFile.length()); |
| } |
| |
| /** |
| * creates a new abstract archive with the given path |
| * @param uri path to create the archive |
| */ |
| @Override |
| public void create(URI uri) throws IOException { |
| |
| |
| this.uri = uri; |
| archive = new File(uri); |
| /* |
| * Get the stale file manager before creating the directories; it's |
| * slightly faster that way. |
| */ |
| staleFileManager = StaleFileManager.Util.getInstance(archive); |
| if (!archive.exists() && !archive.mkdirs()) { |
| throw new IOException("Unable to create directory for " + archive.getAbsolutePath()); |
| } |
| isOpenedOrCreated = true; |
| } |
| |
| /** |
| * Close a previously returned sub archive |
| * |
| * @param subArchive output stream to close |
| * @link Archive.getSubArchive} |
| */ |
| @Override |
| public void closeEntry(WritableArchive subArchive) throws IOException { |
| subArchive.close(); |
| |
| } |
| |
| /** |
| * close the abstract archive |
| */ |
| @Override |
| public void close() throws IOException { |
| } |
| |
| /** |
| * delete the archive |
| */ |
| @Override |
| public boolean delete() { |
| // delete the directory structure... |
| try { |
| final boolean result = deleteDir(archive); |
| /* |
| * Create the stale file marker file, if needed. |
| */ |
| StaleFileManager.Util.markDeletedArchive(this); |
| return result; |
| } catch (IOException e) { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean isDirectory(String name) { |
| final File candidate = new File(this.archive, name); |
| return isEntryValid(candidate) && candidate.isDirectory(); |
| } |
| |
| /** |
| * @return an @see java.util.Enumeration of entries in this abstract |
| * archive |
| */ |
| @Override |
| public Enumeration entries() { |
| final List namesList = new ArrayList(); |
| getListOfFiles(archive, namesList, null); |
| return Collections.enumeration(namesList); |
| } |
| |
| /** |
| * Returns the enumeration of first level directories in this |
| * archive |
| * @return enumeration of directories under the root of this archive |
| */ |
| @Override |
| public Collection<String> getDirectories() throws IOException { |
| List<String> results = new ArrayList<String>(); |
| if (archive != null) { |
| for (File f : archive.listFiles()) { |
| if (f.isDirectory() && isEntryValid(f)) { |
| results.add(f.getName()); |
| } |
| } |
| } |
| return results; |
| } |
| |
| /** |
| * @return an @see java.util.Enumeration of entries in this abstract |
| * archive, providing the list of embedded archive to not count their |
| * entries as part of this archive |
| */ |
| public Enumeration entries(Enumeration embeddedArchives) { |
| List<String> nameList = new ArrayList<String>(); |
| List massagedNames = new ArrayList(); |
| while (embeddedArchives.hasMoreElements()) { |
| String subArchiveName = (String) embeddedArchives.nextElement(); |
| massagedNames.add(FileUtils.makeFriendlyFilenameExtension(subArchiveName)); |
| } |
| getListOfFiles(archive, nameList, massagedNames); |
| return Collections.enumeration(nameList); |
| } |
| |
| /** |
| * Returns an enumeration of the module file entries with the |
| * specified prefix. All elements in the enumeration are of |
| * type String. Each String represents a file name relative |
| * to the root of the module. |
| * |
| * @param prefix the prefix of entries to be included |
| * @return an enumeration of the archive file entries. |
| */ |
| @Override |
| public Enumeration<String> entries(String prefix) { |
| prefix = prefix.replace('/', File.separatorChar); |
| File file = new File(archive, prefix); |
| List<String> namesList = new ArrayList<String>(); |
| getListOfFiles(file, namesList, null); |
| return Collections.enumeration(namesList); |
| } |
| |
| /** |
| * @return true if this archive exists |
| */ |
| @Override |
| public boolean exists() { |
| if (archive != null) { |
| return archive.exists(); |
| } |
| return false; |
| } |
| |
| /** |
| * |
| * create or obtain an embedded archive within this abstraction. |
| * |
| * @param name name of the embedded archive. |
| */ |
| @Override |
| public ReadableArchive getSubArchive(String name) throws IOException { |
| String subEntryName = getFileSubArchivePath(name); |
| File subEntry = new File(subEntryName); |
| if (subEntry.exists() && isEntryValid(subEntry)) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.getSubArchive for {0} found that it is valid", |
| subEntry.getAbsolutePath()); |
| ReadableArchive result = archiveFactory.openArchive(subEntry); |
| if (result instanceof AbstractReadableArchive) { |
| ((AbstractReadableArchive) result).setParentArchive(this); |
| } |
| return result; |
| } else if (subEntry.exists()) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.getSubArchive for {0} found that it is not a valid entry; it is stale", |
| subEntry.getAbsolutePath()); |
| } |
| return null; |
| } |
| /** |
| * create or obtain an embedded archive within this abstraction. |
| * |
| * @param name name of the embedded archive. |
| */ |
| @Override |
| public WritableArchive createSubArchive(String name) throws IOException { |
| String subEntryName = getFileSubArchivePath(name); |
| File subEntry = new File(subEntryName); |
| if (!subEntry.exists()) { |
| // time to create a new sub directory |
| if (!subEntry.exists() && !subEntry.mkdirs()) { |
| throw new IOException("Unable to create directory for " + subEntry.getAbsolutePath()); |
| } |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.createSubArchive created dirs for {0}", |
| subEntry.getAbsolutePath()); |
| } else { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.createSubArchive found existing dir for {0}", |
| subEntry.getAbsolutePath()); |
| /* |
| * This subdirectory already exists, so it might be marked as |
| * stale. Because this invocation is creating the subarchive in |
| * the current archive, the subdirectory is no longer stale. |
| */ |
| staleFileManager().recordValidEntry(subEntry); |
| } |
| final WritableArchive result = archiveFactory.createArchive(subEntry); |
| if (result instanceof AbstractReadableArchive) { |
| ((AbstractReadableArchive) result).setParentArchive(this); |
| } |
| return result; |
| } |
| |
| /** |
| * |
| * create or obtain an embedded archive within this abstraction. |
| * |
| * @param name name of the embedded archive. |
| */ |
| private String getFileSubArchivePath(String name) throws IOException { |
| // Convert name to native form. See bug #6345029 for more details. |
| name = name.replace('/', File.separatorChar); |
| File file = new File(name); |
| File subDir; |
| if (file.isAbsolute()) { |
| subDir = file; |
| } else { |
| // first we try to see if a sub directory with the right file |
| // name exist |
| subDir = new File(archive, FileUtils.makeFriendlyFilenameExtension(name)); |
| if (!subDir.exists()) { |
| // now we try to open a sub jar file... |
| subDir = new File(archive, name); |
| if (!subDir.exists()) { |
| // ok, nothing worked, reassing the name to the |
| // sub directory one |
| subDir = new File(archive, FileUtils.makeFriendlyFilenameExtension(name)); |
| } |
| } |
| } |
| return subDir.getPath(); |
| } |
| |
| /** |
| * Returns the existence of the given entry name |
| * The file name must be relative to the root of the module. |
| * |
| * @param name the file name relative to the root of the module. |
| * @return the existence the given entry name. |
| */ |
| @Override |
| public boolean exists(String name) throws IOException { |
| name = name.replace('/', File.separatorChar); |
| File input = new File(archive, name); |
| return input.exists() && isEntryValid(input); |
| } |
| |
| /** |
| * @return a @see java.io.InputStream for an existing entry in |
| * the current abstract archive |
| * @param name the entry name |
| */ |
| @Override |
| public InputStream getEntry(String name) throws IOException { |
| |
| File input = getEntryFile(name); |
| if (!input.exists() || input.isDirectory() |
| || ! isEntryValid(input)) { // If name corresponds to directory, return null as it can not be opened |
| return null; |
| } |
| FileInputStream fis = new FileInputStream(input); |
| try { |
| BufferedInputStream bis = new BufferedInputStream(fis); |
| return bis; |
| } catch (Throwable tx) { |
| if (fis != null) { |
| try { |
| fis.close(); |
| } catch (Throwable thr) { |
| throw new IOException("Error closing FileInputStream after error opening BufferedInputStream for entry " + name, thr); |
| } |
| } |
| throw new IOException("Error opening BufferedInputStream for entry " + name, tx); |
| } |
| } |
| |
| private File getEntryFile(String name) { |
| name = name.replace('/', File.separatorChar); |
| return new File(archive, name); |
| } |
| |
| /** |
| * Returns the entry size for a given entry name or 0 if not known |
| * |
| * @param name the entry name |
| * @return the entry size |
| */ |
| @Override |
| public long getEntrySize(String name) { |
| name = name.replace('/', File.separatorChar); |
| File input = new File(archive, name); |
| if (!input.exists() || ! isEntryValid(input)) { |
| return 0; |
| } |
| return input.length(); |
| } |
| |
| /** |
| * @return the manifest information for this abstract archive |
| */ |
| @Override |
| public Manifest getManifest() throws IOException { |
| InputStream is = null; |
| try { |
| is = getEntry(JarFile.MANIFEST_NAME); |
| if (is!=null) { |
| Manifest m = new Manifest(is); |
| return m; |
| } |
| } finally { |
| if (is != null) { |
| is.close(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the URI used to create or open the underlyong archive |
| * |
| * @return the URI for this archive. |
| */ |
| @Override |
| public URI getURI() { |
| return uri; |
| } |
| |
| /** |
| * rename the archive |
| * |
| * @param name the archive name |
| */ |
| @Override |
| public boolean renameTo(String name) { |
| return FileUtils.renameFile(archive, new File(name)); |
| } |
| |
| /** |
| * Reports whether the entry is valid, in the sense that if this |
| * archive has been created during this execution then the entry |
| * requested was created later than the archive itself. |
| * <p> |
| * It is possible (for example, on Windows) for GlassFish to want to create |
| * a new archive in a directory that already exists and contains stale |
| * "left-over" files from a previous deployment, for example. This method |
| * causes the FileArchive implementation to hide any files that |
| * reside in the directory for an archive that was created during this VM |
| * execution but were not explicitly added to the archive using putNextEntry. |
| * |
| * @param entry file to check |
| * @return |
| */ |
| private boolean isEntryValid(final File entry) { |
| return isEntryValid(entry, true, deplLogger); |
| } |
| |
| private boolean isEntryValid(final File entry, final boolean isLogging) { |
| return isEntryValid(entry, isLogging, deplLogger); |
| } |
| |
| private boolean isEntryValid(final File entry, final boolean isLogging, final Logger logger) { |
| return staleFileManager().isEntryValid(entry, isLogging, logger); |
| } |
| |
| private StaleFileManager myStaleFileManager() { |
| /* |
| * If the FileArchive has been opened or created then its |
| * staleFileManager has been set. |
| */ |
| if ( ! isOpenedOrCreated) { |
| throw new IllegalStateException(); |
| } |
| return staleFileManager; |
| } |
| |
| private StaleFileManager staleFileManager() { |
| ReadableArchive parent = getParentArchive(); |
| if (parent == null) { |
| return myStaleFileManager(); |
| } |
| if (parent instanceof FileArchive) { |
| return ((FileArchive) parent).staleFileManager(); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Reports whether the entry is valid, in the sense that the entry is |
| * more recent than the archive itself. |
| * @param entryName name of the entry to check |
| * @return |
| */ |
| private boolean isEntryValid(final String entryName, final Logger logger) { |
| return isEntryValid(getEntryFile(entryName), true, logger); |
| } |
| |
| /** |
| * utility method for deleting a directory and all its content |
| */ |
| private boolean deleteDir(File directory) throws IOException { |
| if (!directory.isDirectory()) { |
| throw new FileNotFoundException(directory.getPath()); |
| } |
| |
| boolean allDeletesSucceeded = true; |
| // delete contents |
| |
| /* |
| *Do not recursively delete the contents if the current directory |
| *is a symbolic link. |
| */ |
| |
| /* |
| * Fix for bug Glassfish-21261 , method safeIsRealDirectory(File) might return false in case if the currently directory |
| * has a symbolic link in its hierarchy and the currently directory itself might not be a symbolic link. |
| */ |
| if (!Files.isSymbolicLink(directory.toPath())) { |
| File[] entries = directory.listFiles(); |
| for (int i=0;i<entries.length;i++) { |
| if (entries[i].isDirectory()) { |
| allDeletesSucceeded &= deleteDir(entries[i]); |
| } else { |
| if ( ! entries[i].equals(StaleFileManager.Util.markerFile(archive))) { |
| final boolean fileDeleteOK = FileUtils.deleteFileWithWaitLoop(entries[i]); |
| if (fileDeleteOK) { |
| myStaleFileManager().recordDeletedEntry(entries[i]); |
| } |
| allDeletesSucceeded &= fileDeleteOK; |
| } |
| } |
| } |
| } |
| // delete self (the directory or the symbolic link) |
| return (allDeletesSucceeded && FileUtils.deleteFileWithWaitLoop(directory)); |
| } |
| |
| /** |
| * utility method for getting contents of directory and |
| * sub directories |
| */ |
| |
| private void getListOfFiles(File directory, List<String> files, List embeddedArchives) { |
| getListOfFiles(directory, files, embeddedArchives, deplLogger); |
| } |
| |
| /** |
| * Adds the files in the specified directory to the collection of files |
| * already assembled. Excludes the contents of embedded archives in the current archive which |
| * appear in the file tree anchored at the given directory. |
| * @param directory the directory to scan for files |
| * @param files collection of files already assembled to which this directory's files are to be added |
| * @param embeddedArchives collection of embedded archives in the current archive |
| * @param logger logger to which to report inability to get the list of files from the directory |
| */ |
| void getListOfFiles(File directory, List<String> files, List embeddedArchives, final Logger logger) { |
| if(archive == null || directory == null || !directory.isDirectory()) |
| return; |
| final File[] fileList = directory.listFiles(); |
| if (fileList == null) { |
| deplLogger.log(Level.WARNING, |
| FILE_LIST_FAILURE, |
| directory.getAbsolutePath()); |
| return; |
| } |
| for (File aList : fileList) { |
| String fileName = aList.getAbsolutePath().substring(archive.getAbsolutePath().length() + 1); |
| fileName = fileName.replace(File.separatorChar, '/'); |
| if (!aList.isDirectory()) { |
| if (!fileName.equals(JarFile.MANIFEST_NAME) && isEntryValid(fileName, logger)) { |
| files.add(fileName); |
| } |
| } else if (isEntryValid(fileName, logger)) { |
| files.add(fileName); // Add entry corresponding to the directory also to the list |
| if (embeddedArchives != null) { |
| if (!embeddedArchives.contains(fileName)) { |
| getListOfFiles(aList, files, null, logger); |
| } |
| } else { |
| getListOfFiles(aList, files, null, logger); |
| } |
| } |
| } |
| } |
| |
| /** @return true if this archive abstraction supports overwriting of elements |
| * |
| */ |
| public boolean supportsElementsOverwriting() { |
| return true; |
| } |
| |
| /** delete an entry in the archive |
| * @param name the entry name |
| * @return true if the entry was successfully deleted |
| * |
| */ |
| public boolean deleteEntry(String name) { |
| return deleteEntry(name, true); |
| } |
| |
| private boolean deleteEntry(String name, final boolean isLogging) { |
| name = name.replace('/', File.separatorChar); |
| File input = new File(archive, name); |
| if (!input.exists() || ! isEntryValid(input, isLogging)) { |
| return false; |
| } |
| final boolean result = input.delete(); |
| myStaleFileManager().recordDeletedEntry(input); |
| return result; |
| } |
| |
| /** |
| * Closes the current entry |
| */ |
| @Override |
| public void closeEntry() throws IOException { |
| if (os!=null) { |
| os.flush(); |
| os.close(); |
| os = null; |
| } |
| } |
| |
| /** |
| * @returns an @see java.io.OutputStream for a new entry in this |
| * current abstract archive. |
| * @param name the entry name |
| */ |
| @Override |
| public OutputStream putNextEntry(String name) throws java.io.IOException { |
| name = name.replace('/', File.separatorChar); |
| |
| File newFile = new File(archive, name); |
| if (newFile.exists()) { |
| if (!deleteEntry(name, false /* isLogging */) && uri != null) { |
| deplLogger.log(Level.FINE, |
| "Could not delete file {0} in FileArchive {1} during putNextEntry; continuing", |
| new Object[]{name, uri.toASCIIString()}); |
| } |
| } |
| // if the entry name contains directory structure, we need |
| // to create those directories first. |
| if (name.lastIndexOf(File.separatorChar)!=-1) { |
| String dirs = name.substring(0, name.lastIndexOf(File.separatorChar)); |
| File dirsFile = new File(archive, dirs); |
| if (!dirsFile.exists() && !dirsFile.mkdirs()) { |
| throw new IOException("Unable to create directory for " + dirsFile.getAbsolutePath()); |
| } |
| } |
| staleFileManager().recordValidEntry(newFile); |
| os = new BufferedOutputStream(new FileOutputStream(newFile)); |
| return os; |
| } |
| |
| /** |
| * Returns the name portion of the archive's URI. |
| * <p> |
| * For FileArhive the name is all of the path that follows |
| * the last slash (ignoring a slash at the end of the path). |
| * <p> |
| * Here are some example archive names for the specified FileArchive paths: |
| * <ul> |
| * <li>/a/b/c/d/ -> d |
| * <li>/a/b/c/d -> d |
| * <li>/a/b/c.jar -> c.jar |
| * </ul> |
| * @return the name of the archive |
| * |
| */ |
| @Override |
| public String getName() { |
| return Util.getURIName(getURI()); |
| } |
| |
| /** |
| * API which FileArchive methods should use for dealing with the StaleFileManager |
| * implementation. |
| */ |
| public static interface StaleFileManager { |
| |
| /** |
| * Returns whether the specified file is valid - that is, is dated |
| * after the archive was created. |
| * @param f the file to check |
| * @param isLogging whether to log a warning about the check of the entry |
| * @return true if the file is valid; false otherwise |
| */ |
| boolean isEntryValid(File f, boolean isLogging); |
| |
| boolean isEntryValid(File f, boolean isLogging, Logger logger); |
| |
| /** |
| * Returns whether the specified file is for the hidden timestamp file |
| * which FileArchive uses internally. |
| * @param f the file to check |
| * @return true if the File is the hidden timestamp file; false otherwise |
| */ |
| boolean isEntryMarkerFile(File f); |
| |
| void recordValidEntry(File f); |
| |
| void recordDeletedEntry(File f); |
| |
| void flush(); |
| |
| public class Util { |
| |
| private final static String MARKER_FILE_PATH = ".glassfishStaleFiles"; |
| |
| private static File markerFile(final File archive) { |
| return new File(archive, MARKER_FILE_PATH); |
| } |
| |
| /** |
| * Creates a marker file in the archive directory - if it still |
| * exists and contains any stale files. |
| * @param archive the File for the archive to mark |
| */ |
| public static void markDeletedArchive(final Archive archive) { |
| if ( ! (archive instanceof FileArchive)) { |
| return; |
| } |
| |
| final File archiveFile = new File(archive.getURI()); |
| markDeletedArchive(archiveFile); |
| } |
| |
| /** |
| * Creates a marker file in the archive directory - if it still |
| * exists and contains any stale files. |
| * @param archive the File for the archive to mark |
| */ |
| public static void markDeletedArchive(final File archiveFile) { |
| if ( ! archiveFile.exists()) { |
| return; |
| } |
| |
| final Iterator<File>staleFileIt = findFiles(archiveFile); |
| if ( ! staleFileIt.hasNext()) { |
| return; |
| } |
| |
| final URI archiveURI = archiveFile.toURI(); |
| PrintStream ps = null; |
| try { |
| ps = new PrintStream(markerFile(archiveFile)); |
| } catch (FileNotFoundException ex) { |
| throw new RuntimeException(ex); |
| } |
| for ( ; staleFileIt.hasNext(); ) { |
| final URI relativeURI = archiveURI.relativize(staleFileIt.next().toURI()); |
| ps.println(relativeURI); |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager recording left-over file {0}", relativeURI); |
| } |
| ps.close(); |
| } |
| |
| /** |
| * Returns an Iterator over the files contained in the directory tree |
| * anchored at the given directory, excluding any stale file |
| * marker file. |
| * <p> |
| * For efficiency, this implementation avoids creating a list of |
| * all the files in the directory tree all at once. It traverses |
| * each directory as it encounters it. |
| * @param dir root of the directory tree to be traversed |
| * @return Iterator over the contained files |
| */ |
| private static Iterator<File> findFiles(final File dir) { |
| return new Iterator<File>() { |
| |
| private final List<File> fileList; |
| private final ListIterator<File> fileListIt; |
| |
| { |
| fileList = new ArrayList<File>(Arrays.asList(dir.listFiles( |
| new MarkerExcluderFileFilter()))); |
| fileListIt = fileList.listIterator(); |
| } |
| |
| |
| @Override |
| public boolean hasNext() { |
| return fileListIt.hasNext(); |
| } |
| |
| @Override |
| public File next() { |
| |
| final File result = fileListIt.next(); |
| if (result.isDirectory()) { |
| for (File f : result.listFiles( |
| new MarkerExcluderFileFilter())) { |
| fileListIt.add(f); |
| /* |
| * Back up so the next invocation of this method |
| * will return the just-added entry. |
| */ |
| fileListIt.previous(); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| }; |
| } |
| |
| private static final class MarkerExcluderFileFilter implements FileFilter { |
| |
| @Override |
| public boolean accept(File pathname) { |
| return ! pathname.getName().equals(MARKER_FILE_PATH); |
| } |
| } |
| |
| /** |
| * Factory method for a StaleFileManager. |
| * <p> |
| * Callers should invoke this method only after they have finished with |
| * the FileArchive and have tried to delete it. If the directory |
| * for the archive remains then it contains one or more stale files |
| * that could not be deleted, and the factory method returns a |
| * instance that tracks the stale files. If the directory no longer |
| * exists then the delete succeeded, there are |
| * @param archive the directory to contain the archive |
| * @return StaleFileManager for the FileArchive to use |
| */ |
| public static StaleFileManager getInstance(final File archive) throws IOException { |
| if (archive.exists()) { |
| return new StaleFileManagerImpl(archive); |
| } else { |
| return new StaleFileManagerImplNoop(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Acts as a stale file manager but does no real work. |
| * <p> |
| * Used as a stale file manager for an archive that was successfully |
| * deleted and therefore contains no stale files. |
| */ |
| private static class StaleFileManagerImplNoop implements StaleFileManager { |
| |
| @Override |
| public boolean isEntryValid(File f, boolean isLogging) { |
| return true; |
| } |
| |
| @Override |
| public boolean isEntryValid(File f, boolean isLogging, Logger logger) { |
| return true; |
| } |
| |
| @Override |
| public boolean isEntryMarkerFile(File f) { |
| return false; |
| } |
| |
| @Override |
| public void recordValidEntry(File f) { |
| } |
| |
| @Override |
| public void recordDeletedEntry(File f) { |
| } |
| |
| @Override |
| public void flush() { |
| } |
| } |
| |
| /** |
| * Implements stale file manager that might contain stale files. |
| */ |
| private static class StaleFileManagerImpl implements StaleFileManager { |
| |
| private final static String LINE_SEP = System.getProperty("line.separator"); |
| private final File archiveFile; |
| private final URI archiveURI; |
| private final Collection<String> staleEntryNames; |
| private final File markerFile; |
| |
| private StaleFileManagerImpl(final File archive) throws FileNotFoundException, IOException { |
| archiveFile = archive; |
| archiveURI = archive.toURI(); |
| markerFile = StaleFileManager.Util.markerFile(archive); |
| staleEntryNames = readStaleEntryNames(markerFile); |
| } |
| |
| /** |
| * Reads entry names of stale files from the marker file, if it exists. |
| * @param markerFile the marker file to be read |
| * @return Collection of stale entry names. |
| * @throws FileNotFoundException if the marker file existed initially but vanished before it could be opened |
| * @throws IOException in case of errors reading the marker file |
| */ |
| private static Collection<String> readStaleEntryNames(final File markerFile) throws FileNotFoundException, IOException { |
| final Collection<String> result = new ArrayList<String>(); |
| if ( ! markerFile.exists()) { |
| return result; |
| } |
| LineNumberReader reader = null; |
| try { |
| reader = new LineNumberReader(new FileReader(markerFile)); |
| |
| // Avoid some work if logging is coarser than FINE. |
| final boolean isShowEntriesToBeSkipped = deplLogger.isLoggable(DEBUG_LEVEL); |
| final StringBuffer entriesToSkip = isShowEntriesToBeSkipped ? new StringBuffer() : null; |
| String line; |
| while ((line = reader.readLine()) != null) { |
| result.add(line); |
| if (isShowEntriesToBeSkipped) { |
| entriesToSkip.append(line).append(LINE_SEP); |
| } |
| } |
| if (isShowEntriesToBeSkipped) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager will skip following file(s): {0}{1}", |
| new Object[] {LINE_SEP, entriesToSkip.toString()}); |
| } |
| return result; |
| } finally { |
| if (reader != null) { |
| reader.close(); |
| } |
| } |
| } |
| |
| @Override |
| public boolean isEntryValid(final File f, final boolean isLogging) { |
| return isEntryValid(f, isLogging, deplLogger); |
| } |
| |
| |
| @Override |
| public boolean isEntryValid(final File f, final boolean isLogging, final Logger logger) { |
| final boolean result = ( ! isEntryMarkerFile(f)) && |
| ( ! staleEntryNames.contains(archiveURI.relativize(f.toURI()).getPath())); |
| if ( ! result && ! isEntryMarkerFile(f) && isLogging) { |
| deplLogger.log(Level.WARNING, |
| STALE_FILES_SKIPPED, |
| new Object[] {archiveURI.relativize(f.toURI()).toASCIIString(), archiveFile.getAbsolutePath()}); |
| } |
| return result; |
| } |
| |
| @Override |
| public boolean isEntryMarkerFile(File f) { |
| return markerFile.equals(f); |
| } |
| |
| /** |
| * Records that the specified file is valid. |
| * <p> |
| * If the file had previously been marked as stale, it will no longer be |
| * considered stale. |
| * @param f the File which is now valid |
| */ |
| @Override |
| public void recordValidEntry(File f) { |
| if (updateStaleEntry(f, "FileArchive.StaleFileManager marking formerly stale entry {0} as active")) { |
| /* |
| * Process not only the file itself but the directories from the |
| * file to the owning archive, since the directories are now |
| * implicitly valid as well. |
| */ |
| do { |
| f = f.getParentFile(); |
| updateStaleEntry(f, "FileArchive.StaleFileManager marking formerly stale ancestor {0} as active"); |
| } while ( ! f.equals(archiveFile)); |
| flush(); |
| } |
| } |
| |
| @Override |
| public void recordDeletedEntry(File f) { |
| if (updateStaleEntry(f, "FileArchive.StaleFileManager recording deletion of entry {0}")) { |
| /* |
| * If there are no other stale files in the same directory as |
| * the file just deleted, then remove the directory from |
| * the stale files collection and check its ancestors. |
| */ |
| do { |
| if (isStaleEntryInDir(f.getParentFile())) { |
| return; |
| } |
| updateStaleEntry(f, "FileArchive.StaleFileManager recording that formerly stale directory {0} is no longer stale"); |
| f = f.getParentFile(); |
| } while ( ! f.equals(archiveFile)); |
| |
| flush(); |
| } |
| } |
| |
| private boolean isStaleEntryInDir(final File dir) { |
| final String dirURIPath = archiveURI.relativize(dir.toURI()).getPath(); |
| for (String staleEntryName : staleEntryNames) { |
| if (staleEntryName.startsWith(dirURIPath) && ! staleEntryName.equals(dirURIPath)) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.isStaleEntryInDir found remaining stale entry {0} in {1}", |
| new Object[] {staleEntryName, dir.getAbsolutePath()}); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean updateStaleEntry(File f, final String msg) { |
| if (staleEntryNames.isEmpty()) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.updateStaleEntry finds staleEntryNames is empty; skipping"); |
| return false; |
| } |
| |
| final String entryName = archiveURI.relativize(f.toURI()).toASCIIString(); |
| final boolean wasStale = staleEntryNames.remove(entryName); |
| if (wasStale) { |
| deplLogger.log(DEBUG_LEVEL, msg, |
| entryName); |
| } else { |
| deplLogger.log(DEBUG_LEVEL, "updateStaleEntry did not find {0} in the stale entries {1}", |
| new Object[] {entryName, staleEntryNames.toString()}); |
| } |
| return wasStale; |
| } |
| |
| @Override |
| public void flush() { |
| if (staleEntryNames.isEmpty()) { |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.flush deleting marker file; no more stale entries"); |
| final File marker = Util.markerFile(archiveFile); |
| if ( ! marker.exists() || marker.delete()) { |
| return; |
| } |
| /* |
| * Couldn't delete the marker file, so try to write out an empty one |
| * so its old contents will not confuse the stale file manager. |
| */ |
| deplLogger.log(Level.FINE, "FileArchive.StatleFileManager.flush could not delete marker file {0}; continuing by writing out an empty marker file", |
| marker.getAbsolutePath()); |
| } |
| PrintStream ps = null; |
| try { |
| ps = new PrintStream(Util.markerFile(archiveFile)); |
| } catch (FileNotFoundException ex) { |
| throw new RuntimeException(ex); |
| } |
| for (String staleEntryName : staleEntryNames) { |
| ps.println(staleEntryName); |
| } |
| ps.close(); |
| deplLogger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.flush rewrote on-disk file {0} containing {1}", |
| new Object[] {markerFile.getAbsolutePath(), staleEntryNames.toString()}); |
| } |
| |
| |
| } |
| } |