| /* |
| * 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 <data dir>/<target>/<version> 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; |
| } |
| } |