/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-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 org.glassfish.admin.payload;
import com.sun.enterprise.util.LocalStringManagerImpl;
import com.sun.enterprise.util.io.FileUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.api.ActionReport;
import org.glassfish.api.admin.Payload;
import org.glassfish.api.admin.Payload.Part;
/**
* Manages transferred files delivered via the request or response {@link Payload}.
* <p>
* Callers can process the entire payload
* at once, treating each Part as a file, using the {@link #processParts}
* method. Or, the caller can invoke the {@link #processPart}
* method to work with a single Part as a file.
* <p>
* If the caller wants to extract the payload's content as temporary files it should
* instantiate {@link Temp} which exposes a {@link PayLoadFilesManager.Temp#cleanup}
* method. The caller should invoke this method once it has finished with
* the transferred files, although the finalizer will invoke cleanup just in case.
* <p>
* On the other hand, if the caller wants to keep the transferred files it
* should instantiate {@link Perm}.
* <p>
* <code>Temp</code> uses a unique temporary directory, then creates one
* temp file for each part it is asked to deal with, either from an entire payload
* ({@link #processParts(org.glassfish.api.admin.Payload.Inbound)}) or a
* single part ({@link #processPart(org.glassfish.api.admin.Payload.Part)}). Recall that each part in the
* payload has a name which is a relative or absolute URI.
*
* @author tjquinn
*/
public abstract class PayloadFilesManager {
private static final String XFER_DIR_PREFIX = "xfer-";
public final static LocalStringManagerImpl strings = new LocalStringManagerImpl(PayloadFilesManager.class);
private final File targetDir;
protected final Logger logger;
private final ActionReport report;
private final ActionReportHandler reportHandler;
protected final Map<File,Long> dirTimestamps = new HashMap<File,Long>();
private PayloadFilesManager(
final File targetDir,
final ActionReport report,
final Logger logger,
final ActionReportHandler reportHandler) {
this.targetDir = targetDir;
this.report = report;
this.logger = logger;
this.reportHandler = reportHandler;
}
private PayloadFilesManager(
final File targetDir,
final ActionReport report,
final Logger logger) {
this(targetDir, report, logger, null);
}
protected File getTargetDir() {
return targetDir;
}
protected URI getParentURI(Part part) throws UnsupportedEncodingException {
/*
* parentURI and parentFile start as the target extraction directory for this
* manager, but will change iff the part specifies a file
* transfer root.
*/
File parentFile = getTargetDir();
URI parentFileURI = parentFile.toURI();
final Properties partProps = part.getProperties();
String parentPathFromPart = partProps.getProperty("file-xfer-root");
if (parentPathFromPart != null) {
if (! parentPathFromPart.endsWith(File.separator)) {
parentPathFromPart = parentPathFromPart + File.separator;
}
final File xferRootFile = new File(parentPathFromPart);
if (xferRootFile.isAbsolute()) {
parentFile = xferRootFile;
} else {
parentFile = new File(parentFile, parentPathFromPart);
}
/*
* If this parent directory does not exist, then the URI from
* the File object will lack the trailing slash. So create
* the URI a little oddly to account for that case.
*/
parentFileURI = URI.create(
parentFile.toURI().toASCIIString() +
(parentFile.exists() ? "" : "/"));
}
return parentFileURI;
}
/**
* Extracts files from a Payload and leaves them on disk.
* <p>
* The Perm manager constructs output file paths this way. The URI from the
* manager's targetDir (which the caller passes to the constructor) is the default
* parent URI for the output file.
* <p>
* Next, the Part's properties are checked for a file-xfer-root property.
* If found, it is used as a URI (either absolute or, if relative, resolved
* against the targetDir).
* <p>
* Finally, the "output name" is either the
* name from the Payload.Part for the {@link #extractFile(org.glassfish.api.admin.Payload.Part) }
* method or the caller-provided argument in the {@link #extractFile(org.glassfish.api.admin.Payload.Part, java.lang.String) }
* method.
* <p>
* In either case, the output name is used as a URI
* string and is resolved against the targetDir combined with (if present) the
* file-xfer-root property.
* <p>
* The net effect of this
* is that if the output name is an absolute URI then it will override the
* targetDir and the file-xfer-root setting. If the output name is
* relative then it will be resolved
* against the targetDir plus file-xfer-root URI to derive the URI for the output file.
*/
public static class Perm extends PayloadFilesManager {
/**
* Creates a new PayloadFilesManager for dealing with permanent files that
* will be anchored at the specified target directory.
* @param targetDir directory under which the payload's files should be stored
* @param report result report to which extraction results will be appened
* @param logger logger to receive messages
*/
public Perm(final File targetDir, final ActionReport report, final Logger logger) {
this(targetDir, report, logger, null);
}
/**
* Creates a new PayloadFilesManager for permanent files anchored at
* the specified target directory.
* @param targetDir directory under which the payload's files should be stored
* @param report result report to which extraction results will be appened
* @param logger logger to receive messages
* @param reportHandler handler to invoke for each ActionReport in the payload
*/
public Perm(final File targetDir, final ActionReport report,
final Logger logger, final ActionReportHandler reportHandler) {
super(targetDir != null ? targetDir :
new File(System.getProperty("user.dir")),
report, logger, reportHandler);
}
/**
* Creates a new PayloadFilesManager for permanent files anchored at
* the caller's current directory.
* @param report result report to which extraction results will be appended
* @param logger logger to receive messages
*/
public Perm(final ActionReport report, final Logger logger) {
this(report, logger, null);
}
/**
* Creates a new PayloadFilesManager for permanent files anchored at
* the caller's current directory.
* @param report result report to which extraction results will be appened
* @param logger logger to receive messages
* @param reportHandler handler to invoke for each ActionReport in the payload
*/
public Perm(final ActionReport report, final Logger logger,
final ActionReportHandler reportHandler) {
super(new File(System.getProperty("user.dir")), report, logger, reportHandler);
}
/**
* Creates a new PayloadFilesManager for permanent files anchored at
* the caller's current directory.
* @param logger logger to receive messages
*/
public Perm(final Logger logger) {
this(null, logger);
}
/**
* Creates a new PayloadFilesManager for permanent files anchored at
* the caller's current directory.
*/
public Perm() {
this((ActionReportHandler) null);
}
public Perm(final ActionReportHandler reportHandler) {
this(null, Logger.getLogger(Perm.class.getName()), reportHandler);
}
@Override
protected void postExtract(File extractedFile) {
// no-op for permanent files
}
@Override
protected void postProcessParts() {
final boolean isFine = logger.isLoggable(Level.FINE);
for (Map.Entry<File,Long> entry : dirTimestamps.entrySet()) {
if (isFine) {
final Date when = new Date(entry.getValue());
logger.log(Level.FINER, "Setting lastModified for {0} explicitly to {1}", new Object[]{entry.getKey().getAbsolutePath(), when});
}
if ( ! entry.getKey().setLastModified(entry.getValue())) {
logger.log(Level.WARNING, strings.getLocalString(
"payload.setLatModifiedFailed",
"Attempt to set lastModified for {0} failed; no further information is available. Continuing.",
entry.getKey().getAbsoluteFile()));
}
}
}
}
/**
* Extracts files from a payload, treating them as temporary files.
* The caller should invoke {@link #cleanup} once it is finished with the
* extracted files, although the finalizer will invoke cleanup if the
* caller has not.
*/
public static class Temp extends PayloadFilesManager {
// /*
// * regex to match colons and backslashes on Windows and slashes on non-Windows
// */
// private static final String DIR_PATH_TO_FLAT_NAME_PATTERN = (File.separatorChar == '\\') ?
// "[:\\\\]" : "/";
private boolean isCleanedUp = false;
// /** maps payload part name paths (excluding name and type) to temp file subdirs */
// private Map<String,File> pathToTempSubdir = new HashMap<String,File>();
public Temp(final File parentDir, final ActionReport report,
final Logger logger) throws IOException {
super(createTempFolder(
parentDir,
logger),
report,
logger);
}
/**
* Creates a new PayloadFilesManager for temporary files.
* @param report results report to which extraction results will be appended
* @param logger logger to receive messages
* @throws java.io.IOException
*/
public Temp(final ActionReport report, final Logger logger) throws IOException {
this(new File(System.getProperty("java.io.tmpdir")), report, logger);
}
/**
* Creates a new PayloadFilesManager for temporary files.
* @param logger logger to receive messages
* @throws java.io.IOException
*/
public Temp(final Logger logger) throws IOException {
this(null, logger);
}
/**
* Deletes the temporary files created by this temp PayloadFilesManager.
*/
public void cleanup() {
if ( ! isCleanedUp) {
FileUtils.whack(super.targetDir);
isCleanedUp = true;
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
cleanup();
}
@Override
protected void postExtract(File extractedFile) {
extractedFile.deleteOnExit();
}
@Override
protected void postProcessParts() {
// no-op
}
// private String getParentPath(String partName) {
// if (partName.endsWith("/")) {
// partName = partName.substring(0, partName.length() - 1);
// }
// int lastSlash = partName.lastIndexOf('/');
// if (lastSlash != -1) {
// return partName.substring(0, lastSlash);
// }
// return null;
// }
// URI getTempSubDirForPath(String path) throws IOException {
// /*
// * Convert the path (which is currently in URI form) to
// * the local file system form.
// */
// path = path.replace('/', File.separatorChar);
// File tempSubDir = pathToTempSubdir.get(path);
// if (tempSubDir == null) {
// /*
// * Replace slashes (forward or backward) that are directory
// * separators and replace colons (from Windows devices) with single
// * dashes. This technique generates unique but flat directory
// * names so same-named files in different directories will
// * go to different directories.
// *
// * The extra dashes make sure the prefix meets createTempFile's reqts.
// *
// */
// String tempDirPrefix = path.replaceAll(DIR_PATH_TO_FLAT_NAME_PATTERN, "-") + "---";
// tempSubDir = createTempFolder(getTargetDir(), tempDirPrefix, super.logger);
// pathToTempSubdir.put(path, tempSubDir);
// }
// return tempSubDir.toURI();
// }
// private String getNameAndType(String path) {
// if (path.endsWith("/")) {
// path = path.substring(0, path.length() - 1);
// }
// final int lastSlash = path.lastIndexOf('/');
// return path.substring(lastSlash + 1);
// }
}
protected abstract void postExtract(final File extractedFile);
protected URI getOutputFileURI(Part part, String name) throws IOException {
/*
* The part name might have path elements using / as the
* separator, so figure out the full path for the resulting
* file.
*/
if (name.startsWith("/")) {
name = name.substring(1);
}
URI targetURI = getParentURI(part).resolve(name);
return targetURI;
}
private File removeFile(final Payload.Part part) throws IOException {
final File result = removeFileWithoutConsumingPartBody(part);
consumePartBody(part);
return result;
}
private File removeFileWithoutConsumingPartBody(final Payload.Part part) throws IOException {
final boolean isFine = logger.isLoggable(Level.FINE);
File targetFile = new File(getOutputFileURI(part, part.getName()));
if (targetFile.exists()) {
final boolean isRemovalRecursive = targetFile.isDirectory() && part.isRecursive();
if (isRemovalRecursive ?
FileUtils.whack(targetFile) : targetFile.delete()) {
if (isFine) {
logger.log(Level.FINER, "Deleted {0}{1} as requested", new Object[]{targetFile.getAbsolutePath(), isRemovalRecursive ? " recursively" : ""});
}
reportDeletionSuccess();
} else {
if (isFine) {
logger.log(Level.FINER, "File {0} ({1}) requested for deletion exists but was not able to be deleted", new Object[]{part.getName(), targetFile.getAbsolutePath()});
}
reportDeletionFailure(part.getName(),
strings.getLocalString("payload.deleteFailedOnFile",
"Requested deletion of {0} failed; the file was found but the deletion attempt failed - no reason is available"));
}
} else {
if (isFine) {
logger.log(Level.FINER, "File {0} ({1}) requested for deletion does not exist.", new Object[]{part.getName(), targetFile.getAbsolutePath()});
}
reportDeletionFailure(part.getName(), new FileNotFoundException(targetFile.getAbsolutePath()));
}
return targetFile;
}
private File replaceFile(final Payload.Part part) throws IOException {
removeFileWithoutConsumingPartBody(part);
return extractFile(part, part.getName());
}
private void consumePartBody(final Part part) throws FileNotFoundException, IOException {
InputStream is = null;
try {
is = part.getInputStream();
byte[] buffer = new byte[1024 * 64];
while (is.read(buffer) != -1) {
}
} finally {
if (is != null) {
is.close();
}
}
}
private void processReport(final Payload.Part part) throws Exception {
if (reportHandler != null) {
reportHandler.handleReport(part.getInputStream());
} else {
consumePartBody(part);
}
}
/**
* Extracts the contents of the specified Part as a file, specifying
* the relative or absolute URI to use for creating the extracted file.
* If outputName is relative it is resolved against the manager's target
* directory (which the caller passed to the constructor) and the
* file-xfer-root Part property, if present.
* @param part the Part containing the file's contents
* @param outputName absolute or relative URI string to use for the extracted file
* @return File for the extracted file
* @throws java.io.IOException
*/
private File extractFile(final Payload.Part part, final String outputName) throws IOException {
final boolean isFine = logger.isLoggable(Level.FINE);
OutputStream os = null;
InputStream is = null;
/*
* Look in the Part's properties first for the URI of the target
* directory for the file. If there is none there then use the
* target directory for this manager.
*/
try {
File extractedFile = new File(getOutputFileURI(part, outputName));
/*
* Create the required directory tree under the target directory.
*/
File immediateParent = extractedFile.getParentFile();
if ( ! immediateParent.exists() && ! immediateParent.mkdirs()) {
logger.log(Level.WARNING, strings.getLocalString(
"payload.mkdirsFailed",
"Attempt to create directories for {0} failed; no further information is available. Continuing.",
immediateParent.getAbsolutePath()));
}
if (extractedFile.exists()) {
if (!extractedFile.delete() && ! extractedFile.isDirectory()) {
/*
* Don't warn if we cannot delete the directory - there
* are likely to be files in it preventing its removal.
*/
logger.warning(strings.getLocalString(
"payload.overwrite",
"Overwriting previously-uploaded file because the attempt to delete it failed: {0}",
extractedFile.getAbsolutePath()));
} else if (isFine) {
logger.log(Level.FINER, "Deleted pre-existing file {0} before extracting transferred file", extractedFile.getAbsolutePath());
}
}
/*
* If we are extracting a directory, then we need to consume the
* Part's body but we won't write anything into the directory
* file.
*/
if (outputName.endsWith("/")) {
if ( ! extractedFile.exists() && ! extractedFile.mkdir()) {
logger.log(Level.WARNING,
strings.getLocalString("payload.mkdirsFailed",
"Attempt to create directories for {0} failed; no further information is available. Continuing.",
extractedFile.getAbsolutePath()));
}
}
final boolean isDir = extractedFile.isDirectory();
os = isDir ? null :new BufferedOutputStream(new FileOutputStream(extractedFile));
is = part.getInputStream();
int bytesRead;
byte[] buffer = new byte[1024 * 64];
while ((bytesRead = is.read(buffer)) != -1) {
if (os != null) {
os.write(buffer, 0, bytesRead);
}
}
if (os != null) {
os.close();
}
final String lastModifiedString = part.getProperties().getProperty("last-modified");
final long lastModified = (lastModifiedString != null ?
Long.parseLong(lastModifiedString) :
System.currentTimeMillis());
if ( ! extractedFile.setLastModified(lastModified)) {
logger.log(Level.WARNING, strings.getLocalString(
"payload.setLatModifiedFailed",
"Attempt to set lastModified for {0} failed; no further information is available. Continuing.",
extractedFile.getAbsolutePath()));
}
if (extractedFile.isDirectory()) {
dirTimestamps.put(extractedFile, lastModified);
}
postExtract(extractedFile);
logger.log(Level.FINER, "Extracted transferred entry {0} to {1}", new Object[]{part.getName(), extractedFile.getAbsolutePath()});
reportExtractionSuccess();
return extractedFile;
}
catch (IOException e) {
reportExtractionFailure(part.getName(), e);
throw new IOException(e.getMessage(), e);
} finally {
if (os != null) {
os.close();
os = null;
}
}
}
/**
* Returns all Files extracted from the Payload, treating each Part as a
* separate file, via a Map from each File to its associated Properties.
*
* @param inboundPayload Payload containing file data to be extracted
* @return map from each extracted File to its corresponding Properties
* @throws java.io.IOException
*/
public Map<File,Properties> processPartsExtended(
final Payload.Inbound inboundPayload) throws Exception {
if (inboundPayload == null) {
return Collections.EMPTY_MAP;
}
final Map<File,Properties> result = new LinkedHashMap<File,Properties>();
OutputStream os = null;
boolean isReportProcessed = false;
Part possibleUnrecognizedReportPart = null;
StringBuilder uploadedEntryNames = new StringBuilder();
for (Iterator<Payload.Part> partIt = inboundPayload.parts(); partIt.hasNext();) {
Payload.Part part = partIt.next();
DataRequestType drt = DataRequestType.getType(part);
if (drt != null) {
result.put(drt.processPart(this, part, part.getName()), part.getProperties());
isReportProcessed |= (drt == DataRequestType.REPORT);
uploadedEntryNames.append(part.getName()).append(" ");
} else {
if ( (! isReportProcessed) && possibleUnrecognizedReportPart == null) {
possibleUnrecognizedReportPart = part;
}
}
}
if ( (! isReportProcessed) && possibleUnrecognizedReportPart != null) {
DataRequestType.REPORT.processPart(this, possibleUnrecognizedReportPart,
possibleUnrecognizedReportPart.getName());
isReportProcessed = true;
}
postProcessParts();
return result;
}
/**
* Returns all Files extracted from the Payload, treating each Part as a
* separate file.
* @param inboundPayload Payload containing file data to be extracted
* @parma reportHandler invoked for each ActionReport Part in the payload
* @return the Files corresponding to the content of each extracted file
* @throws java.io.IOException
*/
public List<File> processParts(
final Payload.Inbound inboundPayload) throws Exception {
return new ArrayList<File>(processPartsExtended(inboundPayload).keySet());
}
public static interface ActionReportHandler {
public void handleReport(final InputStream reportStream) throws Exception;
}
protected abstract void postProcessParts();
private void reportExtractionSuccess() {
reportSuccess();
}
private void reportSuccess() {
if (report != null) {
report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
}
}
private void reportDeletionSuccess() {
reportSuccess();
}
private void reportDeletionFailure(final String partName, final String msg) {
reportFailure(partName, msg, null);
}
private void reportDeletionFailure(final String partName, final Exception e) {
reportFailure(partName, strings.getLocalString(
"payload.errDeleting",
"Error deleting file {0}",
partName), e);
}
private void reportFailure(final String partName, final String formattedMessage, final Exception e) {
if (report != null) {
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
report.setMessage(formattedMessage);
report.setFailureCause(e);
}
}
private void reportExtractionFailure(final String partName, final Exception e) {
reportFailure(partName,
strings.getLocalString(
"payload.errExtracting",
"Error extracting transferred file {0}",
partName),
e);
}
/**
* Creates a unique temporary directory within the specified parent.
* @param parent directory within which to create the temp dir; will be created if absent
* @return the temporary folder
* @throws java.io.IOException
*/
private static File createTempFolder(final File parent, final String prefix, final Logger logger) throws IOException {
File result = File.createTempFile(prefix, "", parent);
try {
if ( ! result.delete()) {
throw new IOException(
strings.getLocalString(
"payload.command.errorDeletingTempFile",
"Unknown error deleting temporary file {0}",
result.getAbsolutePath()));
}
if ( ! result.mkdir()) {
throw new IOException(
strings.getLocalString(
"payload.command.errorCreatingDir",
"Unknown error creating directory {0}",
result.getAbsolutePath()));
}
logger.log(Level.FINER, "Created temporary upload folder {0}", result.getAbsolutePath());
return result;
} catch (Exception e) {
throw new IOException(strings.getLocalString(
"payload.command.errorCreatingXferFolder",
"Error creating temporary file transfer folder"), e);
}
}
private static File createTempFolder(final File parent, final Logger logger) throws IOException {
return createTempFolder(parent, XFER_DIR_PREFIX, logger);
}
/**
* Types of data requests the PayloadFilesManager understands.
* <p>
* To add a new type, add a new enum value with the value of the data
* request type as the constructor argument and implement the processPart
* method.
*/
private enum DataRequestType {
FILE_TRANSFER("file-xfer") {
@Override
protected File processPart(
final PayloadFilesManager pfm,
final Part part,
final String partName) throws Exception {
return pfm.extractFile(part, partName);
}
},
FILE_REMOVAL("file-remove") {
@Override
protected File processPart(
final PayloadFilesManager pfm,
final Part part,
final String partName) throws Exception {
return pfm.removeFile(part);
}
},
FILE_REPLACEMENT("file-replace") {
@Override
protected File processPart(
final PayloadFilesManager pfm,
final Part part,
final String partName) throws Exception {
return pfm.replaceFile(part);
}
},
REPORT("report") {
@Override
protected File processPart(
final PayloadFilesManager pfm, final
Part part,
final String partName) throws Exception {
pfm.processReport(part);
return null;
}
};
/** data-request-type value for this enum */
private final String dataRequestType;
/**
* Creates a new instance of the enum
* @param type
*/
private DataRequestType(final String type) {
dataRequestType = type;
}
/**
* Processes the specified part by delegating to the right method on
* the PayloadFilesManager.
* @param pfm
* @param part
* @param partName
* @return
* @throws IOException
*/
protected abstract File processPart(
final PayloadFilesManager pfm, final Part part, final String partName)
throws Exception;
/**
* Finds the DataRequestType enum which matches the data-request-type
* in the Part's properties.
* @param part
* @return DataRequestType matching the Part's data-request-type; null if no match exists
*/
private static DataRequestType getType(final Part part) {
final String targetDataRequestType = part.getProperties().getProperty("data-request-type");
for (DataRequestType candidateType : values()) {
if (candidateType.dataRequestType.equals(targetDataRequestType)) {
return candidateType;
}
}
return null;
}
}
}