/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2006-2011 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.enterprise.deploy.shared; import com.sun.enterprise.deployment.deploy.shared.Util; import com.sun.enterprise.util.io.FileUtils; import com.sun.logging.LogDomains; 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 org.jvnet.hk2.annotations.Inject; import org.jvnet.hk2.annotations.Service; import org.jvnet.hk2.annotations.Scoped; import org.jvnet.hk2.component.PerLookup; import java.io.*; import java.util.*; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.net.URI; /** * This implementation of the Archive interface maps to a directory/file * structure. *
* 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. *
* 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. *
* 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. *
* 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")
@Scoped(PerLookup.class)
public class FileArchive extends AbstractReadableArchive implements WritableArchive {
private final static Level DEBUG_LEVEL = Level.FINE;
@Inject
ArchiveFactory archiveFactory;
// the archive abstraction directory.
File archive;
URI uri;
// the currently opened entry
OutputStream os=null;
private static final Logger logger = LogDomains.getLogger(FileArchive.class, LogDomains.DPL_LOGGER);
/*
* tracks stale files in the archive and filters the archive's contents to
* exclude stale entries
*/
private StaleFileManager staleFileManager;
/**
* 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());
}
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);
archive.mkdirs();
}
/**
* 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
* 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, logger);
}
private boolean isEntryValid(final File entry, final boolean isLogging) {
return isEntryValid(entry, isLogging, logger);
}
private boolean isEntryValid(final File entry, final boolean isLogging, final Logger logger) {
return staleFileManager().isEntryValid(entry, isLogging, logger);
}
private StaleFileManager staleFileManager() {
ReadableArchive parent = getParentArchive();
if (parent == null) {
return staleFileManager;
}
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
File[] entries = directory.listFiles();
for (int i=0;i
* For FileArhive the name is all of the path that follows
* the last slash (ignoring a slash at the end of the path).
*
* Here are some example archive names for the specified FileArchive paths:
*
* 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
* 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.
*
* 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
* 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)) {
logger.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()) {
logger.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) {
logger.log(DEBUG_LEVEL, msg,
entryName);
} else {
logger.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()) {
logger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.flush deleting marker file; no more stale entries");
Util.markerFile(archiveFile).delete();
return;
}
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();
logger.log(DEBUG_LEVEL, "FileArchive.StaleFileManager.flush rewrote on-disk file {0} containing {1}",
new Object[] {markerFile.getAbsolutePath(), staleEntryNames.toString()});
}
}
}
*
* @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