blob: cc6824ee651ffadab73e11a684209fa107a981d0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.ace.it.deployment.provider.filebased;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import org.apache.ace.deployment.provider.ArtifactData;
import org.apache.ace.deployment.provider.DeploymentProvider;
import org.apache.ace.deployment.provider.OverloadedException;
import org.apache.ace.deployment.provider.impl.ArtifactDataImpl;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.log.LogService;
/**
* This class reads data from the filesystem. It contains deployment data in the following format: <storage dir>/<target-name>/<bundle-version>/<jars>
* example : storage-directory/ storage-directory/target-a storage-directory/target-a/1.0.0
* storage-directory/target-a/1.0.0/bundle1.jar storage-directory/target-a/1.0.0/bundle2.jar storage-directory/target-a/1.1.0
* storage-directory/target-a/1.1.0/bundle2.jar storage-directory/target-a/1.1.0/bundle3.jar The versions are in the
* org.osgi.framework.Version format.
*/
public class FileBasedProvider implements DeploymentProvider, ManagedService {
/** Directory where all the target ID folders are located. */
private static final String DIRECTORY_NAME = "BaseDirectoryName";
/** Fallback directory for all targets that have no specific versions. Defaults to BaseDirectoryName if not specified. */
private static final String DEFAULT_DIRECTORY_NAME = "DefaultDirectoryName";
/** Configuration key for the number of concurrent users */
private static final String MAXIMUM_NUMBER_OF_USERS = "MaximumNumberOfUsers";
private static final int OSGI_R4_MANIFEST_VERSION = 2;
private volatile File m_baseDirectory;
private volatile File m_defaultDirectory;
private volatile LogService m_log;
private final Semaphore m_disk = new Semaphore(1, true);
private final AtomicInteger m_usageCounter = new AtomicInteger();
/** Maximum number of concurrent users. Value 0 is used for unlimited users. */
private int m_maximumNumberOfUsers = 0;
/** The default backoff time for each new user over the limit */
private static final int BACKOFF_TIME_PER_USER = 5;
/**
* Get the bundle data from the bundles in the &lt;data dir&gt;/&lt;target&gt;/&lt;version&gt; directory It reads the manifest from all the
* .jar files in that directory. If the manifest cannot be found, This method can only parse OSGi R4 bundles
*/
public List<ArtifactData> getBundleData(String targetId, String version) throws OverloadedException, IllegalArgumentException {
try {
int concurrentUsers = m_usageCounter.incrementAndGet();
if (m_maximumNumberOfUsers != 0 && m_maximumNumberOfUsers < concurrentUsers) {
throw new OverloadedException("Too many users, maximum allowed = " + m_maximumNumberOfUsers + ", current = " + concurrentUsers, (concurrentUsers - m_maximumNumberOfUsers) * BACKOFF_TIME_PER_USER);
}
return internalGetBundleData(targetId, version);
} finally {
m_usageCounter.getAndDecrement();
}
}
/**
* Version folder and requested version do not always match (see implementation of getVersions, which uses Versions.parseVersion to allow different styles)
* like 1 instead of 1.0.0 and alike.
* So we need to do some crawling to map them.
*
* @param targetDirectory store directory
* @param version that has been requested.
*
* @return the matching folder.
*
* @throws IllegalArgumentException if no matching folder has been found. If this happens something is weirdly wrong.
*/
private File findMatchingVersionDirectory(File targetDirectory, String version) {
// first try the direct way:
File directTry = new File(targetDirectory, version);
if ((directTry != null) && directTry.isDirectory()) {
return directTry;
}
// otherwise try to find it:
Version requestedVersion;
try {
requestedVersion = Version.parseVersion(version);
}
catch (IllegalArgumentException iae) {
throw new IllegalArgumentException("Requested version " + version + " has no matching folder in store: " + targetDirectory.getAbsolutePath());
}
File[] files = targetDirectory.listFiles();
for (int i = 0; i < files.length; i++) {
File possibleVersionDirectory = files[i];
if (possibleVersionDirectory.isDirectory()) {
// ok, it is a directory. Now see if it is a version
try {
Version foundVersion = Version.parseVersion(possibleVersionDirectory.getName());
// no exception, but is could still be an empty version
if ((requestedVersion != null) && requestedVersion.equals(foundVersion)) {
return new File(targetDirectory, possibleVersionDirectory.getName());
}
}
catch (IllegalArgumentException iae) {
// dont' care at this point.
}
}
}
throw new IllegalArgumentException("Requested version " + version + " has no matching folder in store: " + targetDirectory.getAbsolutePath());
}
public List<ArtifactData> getBundleData(String targetId, String versionFrom, String versionTo) throws OverloadedException, IllegalArgumentException {
try {
int concurrentUsers = m_usageCounter.incrementAndGet();
if (m_maximumNumberOfUsers != 0 && m_maximumNumberOfUsers < concurrentUsers) {
throw new OverloadedException("Too many users, maximum allowed = " + m_maximumNumberOfUsers + ", current = " + concurrentUsers, (concurrentUsers - m_maximumNumberOfUsers) * BACKOFF_TIME_PER_USER);
}
List<ArtifactData> dataVersionFrom = internalGetBundleData(targetId, versionFrom);
List<ArtifactData> dataVersionTo = internalGetBundleData(targetId, versionTo);
Iterator<ArtifactData> it = dataVersionTo.iterator();
while (it.hasNext()) {
ArtifactDataImpl bundleDataVersionTo = (ArtifactDataImpl) it.next();
// see if there was previously a version of this bundle.
ArtifactData bundleDataVersionFrom = getBundleData(bundleDataVersionTo.getSymbolicName(), dataVersionFrom);
bundleDataVersionTo.setChanged(!bundleDataVersionTo.equals(bundleDataVersionFrom));
}
return dataVersionTo;
} finally {
m_usageCounter.getAndDecrement();
}
}
/**
* Check for the existence of bundledata in the collection for a bundle with the given symbolic name
*
* @param symbolicName
*/
private ArtifactData getBundleData(String symbolicName, Collection<ArtifactData> data) {
Iterator<ArtifactData> it = data.iterator();
while (it.hasNext()) {
ArtifactData bundle = it.next();
if (bundle.getSymbolicName().equals(symbolicName)) {
return bundle;
}
}
return null;
}
/**
* Look in the baseDirectory for the specified target. If it exists, get the list of directories in there and check if they
* conform to the <code>org.osgi.framework.Version</code> format. If it does, it will be in the list of versions that are
* returned. If there are no valid versions, return an empty list. If the target cannot be found, an
* IllegalArgumentException is thrown. The list will be sorted on version.
*/
public List<String> getVersions(String targetId) throws OverloadedException, IllegalArgumentException {
try {
int concurrentUsers = m_usageCounter.incrementAndGet();
if (m_maximumNumberOfUsers != 0 && m_maximumNumberOfUsers < concurrentUsers) {
throw new OverloadedException("Too many users, maximum allowed = " + m_maximumNumberOfUsers + ", current = " + concurrentUsers, (concurrentUsers - m_maximumNumberOfUsers) * BACKOFF_TIME_PER_USER);
}
List<Version> versionList = new ArrayList<>();
File targetDirectory = new File(m_baseDirectory.getAbsolutePath(), targetId);
if (targetDirectory.isDirectory()) {
getVersions(targetId, versionList, targetDirectory);
}
else {
// try the default
getVersions(targetId, versionList, m_defaultDirectory);
}
// now sort the list of versions and convert all values to strings.
Collections.sort(versionList);
List<String> stringVersionList = new ArrayList<>();
Iterator<Version> it = versionList.iterator();
while (it.hasNext()) {
String version = (it.next()).toString();
stringVersionList.add(version);
}
return stringVersionList;
} finally {
m_usageCounter.getAndDecrement();
}
}
private List<ArtifactData> internalGetBundleData(String targetId, String version) throws OverloadedException, IllegalArgumentException {
List<String> versions = getVersions(targetId);
if (!versions.contains(version)) {
throw new IllegalArgumentException("Unknown version " + version + " requested");
}
File targetDirectory = new File(m_baseDirectory, targetId);
File versionDirectory;
if (targetDirectory.isDirectory()) {
// target has its own folder
versionDirectory = findMatchingVersionDirectory(targetDirectory, version);
}
else {
versionDirectory = findMatchingVersionDirectory(m_defaultDirectory, version);
}
List<ArtifactData> bundleData = new ArrayList<>();
JarInputStream jarInputStream = null;
File[] jarFiles = versionDirectory.listFiles();
for (int i = 0; i < jarFiles.length; i++) {
Manifest bundleManifest = null;
File jarFile = jarFiles[i];
try {
jarInputStream = new JarInputStream(new FileInputStream(jarFile));
bundleManifest = jarInputStream.getManifest();
}
catch (IOException ioe) {
m_log.log(LogService.LOG_WARNING, "Error making inputstream", ioe);
continue;
}
finally {
if (jarInputStream != null) {
try {
jarInputStream.close();
}
catch (Exception ioe) {
m_log.log(LogService.LOG_ERROR, "Error closing the file input stream", ioe);
}
}
}
Attributes mainAttributes = bundleManifest.getMainAttributes();
String manifestVersion = mainAttributes.getValue(Constants.BUNDLE_MANIFESTVERSION);
String symbolicName = mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
String bundleVersion = mainAttributes.getValue(Constants.BUNDLE_VERSION);
if ((manifestVersion != null) && (symbolicName != null) && (bundleVersion != null)) {
// ok, now at least we have the required attributes
// now check if they have the expected values
if (OSGI_R4_MANIFEST_VERSION != new Integer(manifestVersion).intValue()) {
m_log.log(LogService.LOG_WARNING, "Invalid bundle:" + jarFile.getAbsolutePath() + " has the wrong manifest version.");
}
else {
// it is the right manifest version
if (symbolicName.trim().equals("")) {
m_log.log(LogService.LOG_WARNING, "Invalid bundle:" + jarFile.getAbsolutePath() + " the symbolic name is empty.");
}
else {
// it also has the right symbolic name
try {
new Version(bundleVersion);
// Do a file.toURI().toURL() to preserve special path characters
// see http://www.javalobby.org/java/forums/t19698.html
URL bundleUrl = new URL(null, jarFile.toURI().toURL().toString(), new URLStreamHandler() {
@Override
protected URLConnection openConnection(final URL u) throws IOException {
return new URLConnection(u) {
@Override
public void connect() throws IOException {
// TODO Auto-generated method stub
}
@Override
public InputStream getInputStream() throws IOException {
final InputStream parent;
try {
parent = new URL(u.toURI().toURL().toString()).openStream();
}
catch (URISyntaxException ex) {
throw new IOException(ex.getMessage());
}
return new InputStream() {
@Override
public int read() throws IOException {
return parent.read();
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int off, int length) throws IOException {
m_disk.acquireUninterruptibly();
try {
return parent.read(buffer, off, length);
}
finally {
m_disk.release();
}
}
@Override
public void close() throws IOException {
parent.close();
}
};
}
};
}
});
bundleData.add(new ArtifactDataImpl(jarFile.getName(), symbolicName, jarFile.length(), bundleVersion, bundleUrl, true));
}
catch (IllegalArgumentException iae) {
m_log.log(LogService.LOG_WARNING, "Invalid bundle:" + jarFile.getAbsolutePath() + " has an illegal version", iae);
}
catch (MalformedURLException mue) {
m_log.log(LogService.LOG_WARNING, "Invalid bundle:" + jarFile.getAbsolutePath() + " unable to convert path to URL", mue);
}
}
}
}
else {
m_log.log(LogService.LOG_WARNING, "Invalid bundle:" + jarFile.getAbsolutePath() + " is missing required attributes");
}
}
return bundleData;
}
/**
*
* @param targetId ID that requested versions
* @param versionList where collected versions will be put into.
* @param base folder to be crawled.
*/
private void getVersions(String targetId, List<Version> versionList, File base) {
// ok, it is a directory. Now treat all the subdirectories as seperate versions
File[] files = base.listFiles();
for (int i = 0; i < files.length; i++) {
File possibleVersionDirectory = files[i];
if (possibleVersionDirectory.isDirectory()) {
// ok, it is a directory. Now see if it is a version
try {
Version version = Version.parseVersion(possibleVersionDirectory.getName());
// no exception, but is could still be an empty version
if (!version.equals(Version.emptyVersion)) {
versionList.add(version);
}
}
catch (IllegalArgumentException iae) {
// do nothing. This version will be ignored.
}
}
}
if (files.length == 0) {
m_log.log(LogService.LOG_DEBUG, "No versions found for target: " + targetId);
}
}
/**
* Update the configuration for this bundle. It checks if the basedirectory exists and is a directory.
*/
public void updated(Dictionary<String, ?> settings) throws ConfigurationException {
if (settings != null) {
String maximumNumberOfUsers = (String) settings.get(MAXIMUM_NUMBER_OF_USERS);
if (maximumNumberOfUsers != null) {
m_maximumNumberOfUsers = Integer.parseInt(maximumNumberOfUsers);
}
String baseDirectoryName = getNotNull(settings, DIRECTORY_NAME, "The base directory cannot be null");
File baseDirectory = new File(baseDirectoryName);
if (!baseDirectory.exists() || !baseDirectory.isDirectory()) {
throw new ConfigurationException(DIRECTORY_NAME, "The directory called '" + baseDirectoryName + "' " + (baseDirectory.exists() ? "is no directory." : "doesn't exist."));
}
m_baseDirectory = baseDirectory;
String defaultDirectoryName = (String) settings.get(DEFAULT_DIRECTORY_NAME);
if (defaultDirectoryName != null) {
File defaultDirectory = new File(defaultDirectoryName);
if (defaultDirectory.exists() && defaultDirectory.isDirectory()) {
m_defaultDirectory = defaultDirectory;
return;
}
}
// fallback to using the base directory
m_defaultDirectory = baseDirectory;
}
}
/**
* Convenience method for getting settings from a configuration dictionary.
*/
private String getNotNull(Dictionary<String, ?> settings, String id, String errorMessage) throws ConfigurationException {
String result = (String) settings.get(id);
if (result == null) {
throw new ConfigurationException(id, errorMessage);
}
return result;
}
}