/*
* Jopr Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.plugins.jbossas.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.Stack;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.content.PackageDetails;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
import org.rhq.core.pluginapi.util.FileUtils;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.ZipUtil;
import org.rhq.core.util.file.FileUtil;
/**
* Delegate class used for manipulating artifacts in a JON plugin.
*
* @author Greg Hinkle
* @author Jason Dobies
*/
public class FileContentDelegate {
private static final String RHQ_SHA_256 = "RHQ-Sha256";
private static final String MANIFEST_RELATIVE_PATH = "META-INF/MANIFEST.MF";
private Log log = LogFactory.getLog(FileContentDelegate.class);
protected File directory;
private String fileEnding;
private String packageTypeName;
public FileContentDelegate(File directory, String fileEnding, String packageTypeName) {
this.directory = directory;
this.fileEnding = fileEnding;
this.packageTypeName = packageTypeName;
}
public String getFileEnding() {
return fileEnding;
}
public String getPackageTypeName() {
return packageTypeName;
}
public File getDirectory() {
return directory;
}
/**
* Creates a new package described by the specified details. The destination of the content in the provided
* file will be determined by the package name.
*
* @param details describes the package being created
* @param sourceContentFile content file to be written for the package.
* @param unzip if <code>true</code>, the content stream will be treated like a ZIP file and be unzipped as
* it is written, using the package name as the base directory; if <code>false</code> the
* @param createBackup If <code>true</code>, the original file will be backed up to file.bak
*/
public void createContent(PackageDetails details, File sourceContentFile, boolean unzip, boolean createBackup) {
File destinationContentFile = getPath(details);
try {
if (createBackup) {
moveToBackup(destinationContentFile, ".bak");
}
if (unzip) {
ZipUtil.unzipFile(sourceContentFile, destinationContentFile);
String shaString = new MessageDigestGenerator(MessageDigestGenerator.SHA_256)
.calcDigestString(sourceContentFile);
writeSHAToManifest(destinationContentFile, shaString);
} else {
FileUtil.copyFile(sourceContentFile, destinationContentFile);
}
details.setFileName(destinationContentFile.getPath());
} catch (IOException e) {
throw new RuntimeException("Error creating artifact from details: " + destinationContentFile, e);
}
}
/**
* Creates a new package described by the specified details. The destination of the content in the provided input
* stream will be determined by the package name.
*
* @param details describes the package being created
* @param content content to be written for the package. NOTE this Stream will be closed by this method.
* @param unzip if <code>true</code>, the content stream will be treated like a ZIP file and be unzipped as
* it is written, using the package name as the base directory; if <code>false</code> the
* @param createBackup If <code>true</code>, the original file will be backed up to file.bak
* @deprecated Method deprecated because of SHA256 computations. Method added only for backwards compatibility with SOA-P plugin.
* Replaced by {@link #createContent(PackageDetails, File, boolean, boolean)}
*/
@Deprecated
public void createContent(PackageDetails details, InputStream content, boolean unzip, boolean createBackup) {
File destinationContentFile = getPath(details);
try {
if (createBackup) {
moveToBackup(destinationContentFile, ".bak");
}
if (unzip) {
ZipUtil.unzipFile(content, destinationContentFile);
computeAndSaveSHA(destinationContentFile);
} else {
FileUtil.writeFile(content, destinationContentFile);
}
details.setFileName(destinationContentFile.getPath());
} catch (IOException e) {
throw new RuntimeException("Error creating artifact from details: " + destinationContentFile, e);
}
}
/**
* Try to move the passed contentFile to a backup named contentFile + suffix
* @param contentFile File object pointing to the original file
* @param suffix the suffix to tack on
*/
private void moveToBackup(File contentFile, String suffix) {
File backupFile = new File(contentFile.getAbsolutePath() + suffix);
if (backupFile.exists()) {
// remove
if (!backupFile.delete())
log.warn("Removing of old backup file " + backupFile + " failed ");
}
if (!contentFile.renameTo(backupFile)) {
log.warn("Moving " + contentFile + " to backup " + backupFile + " failed");
}
}
public File getPath(PackageDetails details) {
/* JBNADM-2022 - It still needs to be determined if it is the responsibility of the plugin container or the
* plugin to be concerned with path information in the package name. For now, it's the plugin's
* responsibility. We strip out the path information to keep control of where the JARs are
* deployed to. Note: when we add support for more package types, we'll need to refactor this
* out on a package type basis.
*
* jdobies, Sep 20, 2007
*/
PackageDetailsKey key = details.getKey();
String fileName = key.getName();
int lastPathStart = fileName.lastIndexOf(File.separatorChar);
if (lastPathStart > -1) {
fileName = fileName.substring(lastPathStart + 1);
}
if (!fileName.endsWith(fileEnding)) {
fileName = fileName + fileEnding;
}
return new File(this.directory, fileName);
}
/**
* Returns a stream from which the content of the specified package can be read.
*
* @param details package being loaded
*
* @return buffered input stream containing the contents of the package; will not be <code>null</code>, an
* exception is thrown if the content cannot be loaded
*/
public InputStream getContent(PackageDetails details) {
File contentFile = getPath(details);
try {
return new BufferedInputStream(new FileInputStream(contentFile));
} catch (FileNotFoundException e) {
throw new RuntimeException("Package content not found for package " + contentFile, e);
}
}
/**
* Deletes the underlying file for the specified package.
*
* @param details package to delete
*/
public void deleteContent(PackageDetails details) {
File contentFile = getPath(details);
if (!contentFile.exists())
return;
try {
FileUtils.purge(contentFile, true);
} catch (IOException e) {
throw new RuntimeException("Failed to delete underlying file [" + contentFile + "] for " + details + ".", e);
}
}
public Set<ResourcePackageDetails> discoverDeployedPackages() {
/*
* This is a stub implementation, you need to implement a
* discovery for artifacts of your particular content type
*/
return null;
}
/**
* Retrieves the SHA256 for a deployed application.
* 1) If the app is exploded then return RHQ-Sha256 manifest attribute.
* 1.1) If RHQ-Sha256 is missing then compute it, save it and return the result.
* 2) If the app is an archive then compute SHA256 on fly and return it.
*
* @param deploymentFile deployment file
* @return
*/
public String getSHA(File deploymentFile) {
String sha = null;
try {
if (deploymentFile.isDirectory()) {
File manifestFile = new File(deploymentFile.getAbsolutePath(), MANIFEST_RELATIVE_PATH);
if (manifestFile.exists()) {
InputStream manifestStream = new FileInputStream(manifestFile);
Manifest manifest = null;
try {
manifest = new Manifest(manifestStream);
sha = manifest.getMainAttributes().getValue(RHQ_SHA_256);
} finally {
manifestStream.close();
}
}
if (sha == null || sha.trim().isEmpty()) {
sha = computeAndSaveSHA(deploymentFile);
}
} else {
sha = new MessageDigestGenerator(MessageDigestGenerator.SHA_256).calcDigestString(deploymentFile);
}
} catch (IOException ex) {
throw new RuntimeException("Problem calculating digest of package [" + deploymentFile.getPath() + "].", ex);
}
return sha;
}
/**
* Compute SHA256 for the content of an exploded war deployment. This method should be used to
* compute the SHA256 for content deployed outside RHQ or for the initial content delivered
* with the server.
*
* @param deploymentDirectory app deployment folder
* @return
*/
private String computeAndSaveSHA(File deploymentDirectory) {
String sha = null;
try {
if (deploymentDirectory.isDirectory()) {
MessageDigestGenerator messageDigest = new MessageDigestGenerator(MessageDigestGenerator.SHA_256);
Stack<File> unvisitedFolders = new Stack<File>();
unvisitedFolders.add(deploymentDirectory);
while (!unvisitedFolders.empty()) {
File[] files = unvisitedFolders.pop().listFiles();
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
try {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
} catch (IOException e) {
//do nothing if the sort fails at this point
}
return 0;
}
});
for (File file : files) {
if (file.isDirectory()) {
unvisitedFolders.add(file);
} else {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
messageDigest.add(inputStream);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
}
}
sha = messageDigest.getDigestString();
writeSHAToManifest(deploymentDirectory, sha);
}
} catch (IOException e) {
throw new RuntimeException("Error creating artifact for contentFile: " + deploymentDirectory, e);
}
return sha;
}
/**
* Write the SHA256 to the manifest using the RHQ-Sha256 attribute tag.
*
* @param deploymentFolder app deployment folder
* @param sha SHA256
* @throws IOException
*/
private void writeSHAToManifest(File deploymentFolder, String sha) throws IOException {
File manifestFile = new File(deploymentFolder, MANIFEST_RELATIVE_PATH);
Manifest manifest;
if (manifestFile.exists()) {
FileInputStream inputStream = new FileInputStream(manifestFile);
try {
manifest = new Manifest(inputStream);
} finally {
inputStream.close();
}
} else {
manifest = new Manifest();
manifestFile.getParentFile().mkdirs();
manifestFile.createNewFile();
}
Attributes attribs = manifest.getMainAttributes();
//The main section of the manifest file does not get saved if both of
//these two attributes are missing. Please see Attributes implementation.
if (!attribs.containsKey(Attributes.Name.MANIFEST_VERSION.toString())
&& !attribs.containsKey(Attributes.Name.SIGNATURE_VERSION.toString())) {
attribs.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
}
attribs.putValue(RHQ_SHA_256, sha);
FileOutputStream outputStream = new FileOutputStream(manifestFile);
try {
manifest.write(outputStream);
} finally {
outputStream.close();
}
}
}