/*
* Copyright 2007 Alin Dreghiciu, Stuart McCulloch.
*
* Licensed 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.ops4j.pax.exam.forked.provision;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.ops4j.pax.exam.TestContainerException;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles the workflow of creating the platform. Concrete platforms should implement only the
* PlatformBuilder interface. TODO Add unit tests
*
* @author Alin Dreghiciu, Stuart McCulloch, Harald Wellmann
* @since August 19, 2007
*/
public class PlatformImpl {
/**
* Logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(PlatformImpl.class);
/**
* Concrete platform builder as equinox, felix, kf.
*/
/**
* Downloads files from urls.
*
* @param workDir
* the directory where to download bundles
* @param url
* of the file to be downloaded
* @param displayName
* to be shown during download
* @param overwrite
* if the bundles should be overwritten
* @param checkAttributes
* whether or not to check attributes in the manifest
* @param failOnValidation
* if validation fails should or not fail with an exception (or just return null)
* @param downloadFeeback
* whether or not downloading process should display fine grained progres info
*
* @return the File corresponding to the downloaded file, or null if the bundle is invalid (not
* an osgi bundle)
*
* @throws TestContainerException
* if the url could not be downloaded
*/
public File download(final File workDir, final URL url, final String displayName,
final Boolean overwrite, final boolean checkAttributes, final boolean failOnValidation,
final boolean downloadFeeback) {
LOGGER.debug("Downloading [" + url + "]");
File downloadedBundlesFile = new File(workDir, "downloaded_bundles.properties");
Properties fileNamesForUrls = loadProperties(downloadedBundlesFile);
String downloadedFileName = fileNamesForUrls.getProperty(url.toExternalForm());
String hashFileName = "" + url.toExternalForm().hashCode();
if (downloadedFileName == null) {
// destination will be made based on the hashcode of the url to be downloaded
downloadedFileName = hashFileName + ".jar";
}
File destination = new File(workDir, downloadedFileName);
// download the bundle only if is a forced overwrite or the file does not exist or the file
// is there but is
// invalid
boolean forceOverwrite = overwrite || !destination.exists();
if (!forceOverwrite) {
try {
String cachingName = determineCachingName(destination, hashFileName);
if (!destination.getName().equals(cachingName)) {
throw new TestContainerException("File " + destination + " should have name "
+ cachingName);
}
}
catch (TestContainerException ignore) {
forceOverwrite = true;
}
}
if (forceOverwrite) {
try {
LOGGER.debug("Creating new file at destination: " + destination.getAbsolutePath());
destination.getParentFile().mkdirs();
destination.createNewFile();
FileOutputStream os = null;
try {
os = new FileOutputStream(destination);
FileChannel fileChannel = os.getChannel();
StreamUtils.ProgressBar progressBar = null;
if (LOGGER.isInfoEnabled()) {
if (downloadFeeback) {
progressBar = new StreamUtils.FineGrainedProgressBar(displayName);
}
else {
progressBar = new StreamUtils.CoarseGrainedProgressBar(displayName);
}
}
// Check if this is an exploded bundle...
if (url.getPath().endsWith("/") && "file".equals(url.getProtocol())) {
StreamUtils.streamCopy(new URL("assembly:" + url.toExternalForm()),
fileChannel, progressBar);
}
else {
StreamUtils.streamCopy(url, fileChannel, progressBar);
}
fileChannel.close();
LOGGER.debug("Successfully downloaded to [" + destination + "]");
}
finally {
if (os != null) {
os.close();
}
}
}
catch (IOException e) {
throw new TestContainerException("[" + url + "] could not be downloaded", e);
}
}
if (checkAttributes) {
try {
validateBundle(url, destination);
}
catch (TestContainerException e) {
if (failOnValidation) {
throw e;
}
return null;
}
}
String cachingName = determineCachingName(destination, hashFileName);
File newDestination = new File(destination.getParentFile(), cachingName);
if (!cachingName.equals(destination.getName())) {
if (newDestination.exists()) {
if (!newDestination.delete()) {
throw new TestContainerException("Cannot delete " + newDestination);
}
}
if (!destination.renameTo(newDestination)) {
throw new TestContainerException("Cannot rename " + destination + " to "
+ newDestination);
}
fileNamesForUrls.setProperty(url.toExternalForm(), cachingName);
saveProperties(fileNamesForUrls, downloadedBundlesFile);
}
return newDestination;
}
private Properties loadProperties(File file) {
Properties properties = new Properties();
FileInputStream in = null;
try {
in = new FileInputStream(file);
properties.load(in);
return properties;
}
catch (IOException e) {
return properties;
}
finally {
if (in != null) {
try {
in.close();
}
catch (IOException e) {
// ignore
}
}
}
}
private void saveProperties(Properties properties, File file) {
FileOutputStream os = null;
try {
os = new FileOutputStream(file);
properties.store(os, "");
}
catch (IOException e) {
throw new TestContainerException("Cannot store properties " + file, e);
}
finally {
if (os != null) {
try {
os.close();
}
catch (IOException e) {
// ignore
}
}
}
}
/**
* Validate that the file is an valid bundle. A valid bundle will be a loadable jar file that
* has manifest and the manifest contains at least an entry for Bundle-SymboliName or
* Bundle-Name (R3).
*
* @param url
* original url from where the bundle was created.
* @param file
* file to be validated
*
* @throws TestContainerException
* if the jar is not a valid bundle
*/
void validateBundle(final URL url, final File file) {
String bundleSymbolicName = null;
String bundleName = null;
if (url.getPath().endsWith("/")) {
// is this an exploded bundle?
try {
URL manifestURL = new URL(url, "META-INF/MANIFEST.MF");
InputStream stream = manifestURL.openStream();
try {
Manifest manifest = new Manifest(stream);
bundleSymbolicName = manifest.getMainAttributes().getValue(
Constants.BUNDLE_SYMBOLICNAME);
bundleName = manifest.getMainAttributes().getValue(Constants.BUNDLE_NAME);
}
finally {
stream.close();
}
}
catch (IOException e) {
throw new TestContainerException("[" + url + "] is not a valid exploded bundle", e);
}
}
else {
JarFile jar = null;
try {
// verify that is a valid jar. Do not verify that is signed (the false param).
jar = new JarFile(file, false);
final Manifest manifest = jar.getManifest();
if (manifest == null) {
throw new TestContainerException("[" + url
+ "] is not a valid bundle (manifest is missing)");
}
bundleSymbolicName = manifest.getMainAttributes().getValue(
Constants.BUNDLE_SYMBOLICNAME);
bundleName = manifest.getMainAttributes().getValue(Constants.BUNDLE_NAME);
}
catch (IOException e) {
throw new TestContainerException("[" + url
+ "] is not a valid bundle (reading jar failed)", e);
}
finally {
if (jar != null) {
try {
jar.close();
}
catch (IOException ignore) {
// just ignore as this is less probably to happen.
}
}
}
}
if (bundleSymbolicName == null && bundleName == null) {
throw new TestContainerException("[" + url
+ "] is not a valid bundle (bundleSymbolicName and bundleName are null)");
}
}
/**
* Determine name to be used for caching on local file system.
*
* @param file
* file to be validated
* @param defaultBundleSymbolicName
* default bundle symbolic name to be used if manifest does not have a bundle
* symbolic name
*
* @return file name based on bundle symbolic name and version
*/
String determineCachingName(final File file, final String defaultBundleSymbolicName) {
String bundleSymbolicName = null;
String bundleVersion = null;
JarFile jar = null;
try {
// verify that is a valid jar. Do not verify that is signed (the false param).
jar = new JarFile(file, false);
final Manifest manifest = jar.getManifest();
if (manifest != null) {
bundleSymbolicName = manifest.getMainAttributes().getValue(
Constants.BUNDLE_SYMBOLICNAME);
bundleVersion = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
}
}
catch (IOException ignore) {
// just ignore
}
finally {
if (jar != null) {
try {
jar.close();
}
catch (IOException ignore) {
// just ignore as this is less probably to happen.
}
}
}
if (bundleSymbolicName == null) {
bundleSymbolicName = defaultBundleSymbolicName;
}
else {
// remove directives like "; singleton:=true"
int semicolonPos = bundleSymbolicName.indexOf(";");
if (semicolonPos > 0) {
bundleSymbolicName = bundleSymbolicName.substring(0, semicolonPos);
}
}
if (bundleVersion == null) {
bundleVersion = "0.0.0";
}
return bundleSymbolicName + "_" + bundleVersion + ".jar";
}
}