diff options
author | rfeng <rfeng@13f79535-47bb-0310-9956-ffa450edef68> | 2008-11-07 20:20:02 +0000 |
---|---|---|
committer | rfeng <rfeng@13f79535-47bb-0310-9956-ffa450edef68> | 2008-11-07 20:20:02 +0000 |
commit | 2dc8ffc38dc909aafcbbd3050581f0ec602ea84e (patch) | |
tree | d54675539c69e18b549c63b4422b4291a770fbbf /branches | |
parent | c0e2941c34301966f1400b12c05080309e0d2cdf (diff) |
Add initial support to enforce OSGi bundle resolution during compilation
git-svn-id: http://svn.us.apache.org/repos/asf/tuscany@712253 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'branches')
4 files changed, 1048 insertions, 24 deletions
diff --git a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/pom.xml b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/pom.xml index 9a200e03af..a313a0204c 100644 --- a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/pom.xml +++ b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/pom.xml @@ -91,6 +91,13 @@ </exclusion> </exclusions> </dependency> + + <dependency> + <groupId>org.eclipse</groupId> + <artifactId>osgi</artifactId> + <version>3.3.0-v20070530</version> + <scope>compile</scope> + </dependency> <!-- <dependency> diff --git a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/JavaCompiler.java b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/JavaCompiler.java index 2bc96137b8..d3bfac5810 100644 --- a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/JavaCompiler.java +++ b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/JavaCompiler.java @@ -43,6 +43,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import org.apache.tuscany.sca.tools.maven.compiler.osgi.BundleResolver; import org.codehaus.plexus.compiler.AbstractCompiler; import org.codehaus.plexus.compiler.CompilerConfiguration; import org.codehaus.plexus.compiler.CompilerError; @@ -53,6 +54,8 @@ import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; +import org.eclipse.osgi.service.resolver.BundleDescription; +import org.osgi.framework.BundleException; /** * A custom Plexus Java compiler plugin that uses the Eclipse compiler. @@ -60,26 +63,53 @@ import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; * @version $Rev: $ $Date: $ */ public class JavaCompiler extends AbstractCompiler { - + public JavaCompiler() { super(ONE_OUTPUT_FILE_PER_INPUT_FILE, ".java", ".class", null); } public List<CompilerError> compile(CompilerConfiguration configuration) throws CompilerException { - + BundleResolver stateController = new BundleResolver(getLogger()); + getLogger().info("Invoking Tuscany Eclipse JDT compiler"); - List<URL> urls; + // getLogger().info(configuration.getCustomCompilerArguments().toString()); + boolean osgi = "true".equals(configuration.getCustomCompilerArguments().get("-osgi")); + if (osgi) { + getLogger().info("Enforcing OSGi bundle resolution"); + } + List<URL> urls = new ArrayList<URL>(); try { - urls = new ArrayList<URL>(); - urls.add(new File(configuration.getOutputLocation()).toURI().toURL()); - for (String entry: (List<String>)configuration.getClasspathEntries()) { + // urls.add(new File(configuration.getOutputLocation()).toURI().toURL()); + for (String entry : (List<String>)configuration.getClasspathEntries()) { + if (osgi) { + try { + stateController.addBundle(new File(entry)); + } catch (BundleException e) { + getLogger().error(e.getMessage(), e); + } + } urls.add(new File(entry).toURI().toURL()); } } catch (MalformedURLException e) { throw new CompilerException(e.getMessage(), e); } + if (osgi) { + stateController.resolveState(); + for (BundleDescription b : stateController.getBundles()) { + if (b != null) { + try { + stateController.assertResolved(b); + } catch (BundleException e) { + getLogger().error(stateController.getAllErrors().toString()); + // FIXME: For now, only a warning is reported + // throw new CompilerException(e.getMessage(), e); + } + } + } + } + ClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()])); // Determine compiler configuration @@ -108,48 +138,49 @@ public class JavaCompiler extends AbstractCompiler { // Create a compiler List<CompilerError> compilerErrors = new ArrayList<CompilerError>(); - INameEnvironment nameEnvironment = new ClassLoaderNameEnvironment(classLoader, configuration.getSourceLocations()); - ICompilerRequestor requestor = new CompilerRequestor(configuration.getOutputLocation(), configuration.isShowWarnings(), compilerErrors); - Compiler compiler = new Compiler(nameEnvironment, - proceedWithAllProblems(), - new CompilerOptions(settings), - requestor, - new DefaultProblemFactory(Locale.getDefault())); + INameEnvironment nameEnvironment = + new ClassLoaderNameEnvironment(classLoader, configuration.getSourceLocations()); + ICompilerRequestor requestor = + new CompilerRequestor(configuration.getOutputLocation(), configuration.isShowWarnings(), compilerErrors); + Compiler compiler = + new Compiler(nameEnvironment, proceedWithAllProblems(), new CompilerOptions(settings), requestor, + new DefaultProblemFactory(Locale.getDefault())); // Create compilation units for the source files List<FileCompilationUnit> compilationUnits = new ArrayList<FileCompilationUnit>(); - + // Go over the input source locations - List<String> sourceLocations = (List<String>)configuration.getSourceLocations(); + List<String> sourceLocations = (List<String>)configuration.getSourceLocations(); for (String sourceLocation : sourceLocations) { - + // Exclude nested source locations - List<String> excludeLocations = new ArrayList<String>(); + List<String> excludeLocations = new ArrayList<String>(); for (String nestedLocation : sourceLocations) { if (nestedLocation != sourceLocation && nestedLocation.startsWith(sourceLocation)) { excludeLocations.add(nestedLocation); } } - + // List source files in each source location - for (String sourceFile: (Set<String>)getSourceFilesForSourceRoot(configuration, sourceLocation)) { - + for (String sourceFile : (Set<String>)getSourceFilesForSourceRoot(configuration, sourceLocation)) { + // Exclude files from excluded nested locations boolean excluded = false; - for (String excludeLocation: excludeLocations) { + for (String excludeLocation : excludeLocations) { if (sourceFile.startsWith(excludeLocation)) { excluded = true; } } if (!excluded) { - + // Create a compilation unit for the source file - FileCompilationUnit compilationUnit = new FileCompilationUnit(sourceFile, makeClassName(sourceFile, sourceLocation)); + FileCompilationUnit compilationUnit = + new FileCompilationUnit(sourceFile, makeClassName(sourceFile, sourceLocation)); compilationUnits.add(compilationUnit); } } } - + // Compile all the compilation units getLogger().info("Compiling " + compilationUnits.size() + " to " + configuration.getOutputLocation()); compiler.compile((ICompilationUnit[])compilationUnits.toArray(new ICompilationUnit[compilationUnits.size()])); diff --git a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleResolver.java b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleResolver.java new file mode 100644 index 0000000000..98b96b3cd1 --- /dev/null +++ b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleResolver.java @@ -0,0 +1,402 @@ +/* + * 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.tuscany.sca.tools.maven.compiler.osgi; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.logging.Logger; +import org.eclipse.osgi.service.resolver.BundleDescription; +import org.eclipse.osgi.service.resolver.BundleSpecification; +import org.eclipse.osgi.service.resolver.ExportPackageDescription; +import org.eclipse.osgi.service.resolver.HostSpecification; +import org.eclipse.osgi.service.resolver.ImportPackageSpecification; +import org.eclipse.osgi.service.resolver.ResolverError; +import org.eclipse.osgi.service.resolver.State; +import org.eclipse.osgi.service.resolver.StateObjectFactory; +import org.eclipse.osgi.service.resolver.VersionConstraint; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; + +public class BundleResolver { + private static final String PROP_MAVEN_PROJECT = "MavenProject"; + private static final String PROP_MANIFEST = "BundleManifest"; + + private StateObjectFactory factory = StateObjectFactory.defaultFactory; + private State state; + private long id = 0; + private Logger logger; + + public static BundleDescription[] getDependentBundles(BundleDescription root) { + if (root == null) + return new BundleDescription[0]; + BundleDescription[] imported = getImportedBundles(root); + BundleDescription[] required = getRequiredBundles(root); + BundleDescription[] dependents = new BundleDescription[imported.length + required.length]; + System.arraycopy(imported, 0, dependents, 0, imported.length); + System.arraycopy(required, 0, dependents, imported.length, required.length); + return dependents; + } + + public static BundleDescription[] getImportedBundles(BundleDescription root) { + if (root == null) + return new BundleDescription[0]; + ExportPackageDescription[] packages = root.getResolvedImports(); + List<BundleDescription> resolvedImports = new ArrayList<BundleDescription>(packages.length); + for (int i = 0; i < packages.length; i++) + if (!root.getLocation().equals(packages[i].getExporter().getLocation()) && !resolvedImports + .contains(packages[i].getExporter())) + resolvedImports.add(packages[i].getExporter()); + return (BundleDescription[])resolvedImports.toArray(new BundleDescription[resolvedImports.size()]); + } + + public static BundleDescription[] getRequiredBundles(BundleDescription root) { + if (root == null) + return new BundleDescription[0]; + return root.getResolvedRequires(); + } + + public BundleResolver(Logger logger) { + this.logger = logger; + this.state = factory.createState(true); + Properties props = new Properties(); + props.putAll(System.getProperties()); + BundleUtil.loadVMProfile(props); + state.setPlatformProperties(props); + URL url = Bundle.class.getProtectionDomain().getCodeSource().getLocation(); + + File osgi = toFile(url); + try { + addBundle(osgi); + } catch (BundleException e) { + throw new IllegalArgumentException(e); + } + } + + private long getNextId() { + return ++id; + } + + public BundleDescription addBundle(File bundleLocation) throws BundleException { + return addBundle(bundleLocation, false); + } + + public BundleDescription addBundle(File bundleLocation, boolean override) throws BundleException { + if (bundleLocation == null || !bundleLocation.exists()) + throw new IllegalArgumentException("bundleLocation not found: " + bundleLocation); + Dictionary manifest = loadManifestAttributes(bundleLocation); + if (manifest == null) { + // throw new BundleException("manifest not found in " + bundleLocation); + return null; + } + return addBundle(manifest, bundleLocation, override); + } + + public BundleDescription addBundle(File manifestLocation, File bundleLocation, boolean override) + throws BundleException { + if (bundleLocation == null || !bundleLocation.exists()) + throw new IllegalArgumentException("bundleLocation not found: " + bundleLocation); + Dictionary manifest = loadManifestAttributes(manifestLocation); + if (manifest == null) + throw new IllegalArgumentException("manifest not found in " + manifestLocation); + return addBundle(manifest, bundleLocation, override); + } + + private Dictionary loadManifestAttributes(File bundleLocation) { + Manifest m = loadManifest(bundleLocation); + if (m == null) { + return null; + } + + return manifestToProperties(m.getMainAttributes()); + } + + public Manifest loadManifest(File bundleLocation) { + try { + return BundleUtil.getManifest(bundleLocation); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + private Properties manifestToProperties(Attributes d) { + Iterator iter = d.keySet().iterator(); + Properties result = new Properties(); + while (iter.hasNext()) { + Attributes.Name key = (Attributes.Name)iter.next(); + result.put(key.toString(), d.get(key)); + } + return result; + } + + private BundleDescription addBundle(Dictionary enhancedManifest, File bundleLocation, boolean override) + throws BundleException { + + BundleDescription descriptor = + factory.createBundleDescription(state, enhancedManifest, bundleLocation.getAbsolutePath(), getNextId()); + + setUserProperty(descriptor, PROP_MANIFEST, enhancedManifest); + if (override) { + BundleDescription[] conflicts = state.getBundles(descriptor.getSymbolicName()); + if (conflicts != null) { + for (BundleDescription conflict : conflicts) { + state.removeBundle(conflict); + logger + .warn(conflict.toString() + " has been replaced by another bundle with the same symbolic name " + + descriptor.toString()); + } + } + } + + state.addBundle(descriptor); + return descriptor; + } + + public BundleDescription getResolvedBundle(String bundleId) { + BundleDescription[] description = state.getBundles(bundleId); + if (description == null) + return null; + for (int i = 0; i < description.length; i++) { + if (description[i].isResolved()) + return description[i]; + } + return null; + } + + public void resolveState() { + state.resolve(false); + + if (logger.isDebugEnabled()) { + StringBuilder sb = new StringBuilder("Resolved OSGi state\n"); + for (BundleDescription bundle : state.getBundles()) { + if (!bundle.isResolved()) { + sb.append("NOT "); + } + sb.append("RESOLVED "); + sb.append(bundle.toString()).append(" : ").append(bundle.getLocation()); + sb.append('\n'); + for (ResolverError error : state.getResolverErrors(bundle)) { + sb.append('\t').append(error.toString()).append('\n'); + } + } + logger.debug(sb.toString()); + } + } + + public State getState() { + return state; + } + + public BundleDescription[] getBundles() { + return state.getBundles(); + } + + public ResolverError[] getResolverErrors(BundleDescription bundle) { + Set<ResolverError> errors = new LinkedHashSet<ResolverError>(); + getRelevantErrors(errors, bundle); + return (ResolverError[])errors.toArray(new ResolverError[errors.size()]); + } + + private void getRelevantErrors(Set<ResolverError> errors, BundleDescription bundle) { + ResolverError[] bundleErrors = state.getResolverErrors(bundle); + for (int j = 0; j < bundleErrors.length; j++) { + ResolverError error = bundleErrors[j]; + errors.add(error); + + VersionConstraint constraint = error.getUnsatisfiedConstraint(); + if (constraint instanceof BundleSpecification || constraint instanceof HostSpecification) { + BundleDescription[] requiredBundles = state.getBundles(constraint.getName()); + for (int i = 0; i < requiredBundles.length; i++) { + getRelevantErrors(errors, requiredBundles[i]); + } + } + } + } + + public Set<ResolverError> getAllErrors() { + BundleDescription[] bundles = state.getBundles(); + Set<ResolverError> errors = new LinkedHashSet<ResolverError>(); + for (int i = 0; i < bundles.length; i++) { + BundleDescription bundle = bundles[i]; + ResolverError[] bundleErrors = state.getResolverErrors(bundle); + if (bundleErrors != null) { + errors.addAll(Arrays.asList(bundleErrors)); + } + } + return errors; + } + + public List<BundleDescription> getDependencies(BundleDescription desc) { + Set<Long> bundleIds = new LinkedHashSet<Long>(); + addBundleAndDependencies(desc, bundleIds, true); + List<BundleDescription> dependencies = new ArrayList<BundleDescription>(); + for (long bundleId : bundleIds) { + if (desc.getBundleId() != bundleId) { + BundleDescription dependency = state.getBundle(bundleId); + BundleDescription supplier = dependency.getSupplier().getSupplier(); + HostSpecification host = supplier.getHost(); + if (host == null || !desc.equals(host.getSupplier())) { + dependencies.add(dependency); + } + } + } + return dependencies; + } + + /** + * Code below is copy&paste from org.eclipse.pde.internal.core.DependencyManager + * which seems to calculate runtime dependencies. In particular, it adds + * fragments' dependencies to the host bundle (see TychoTest#testFragment unit test). + * This may or may not cause problems... + * + * RequiredPluginsClasspathContainer#computePluginEntries has the logic to + * calculate compile-time dependencies in IDE. + * + * TODO find the code used by PDE/Build + */ + private static void addBundleAndDependencies(BundleDescription desc, Set<Long> bundleIds, boolean includeOptional) { + if (desc != null && bundleIds.add(new Long(desc.getBundleId()))) { + BundleSpecification[] required = desc.getRequiredBundles(); + for (int i = 0; i < required.length; i++) { + if (includeOptional || !required[i].isOptional()) + addBundleAndDependencies((BundleDescription)required[i].getSupplier(), bundleIds, includeOptional); + } + ImportPackageSpecification[] importedPkgs = desc.getImportPackages(); + for (int i = 0; i < importedPkgs.length; i++) { + ExportPackageDescription exporter = (ExportPackageDescription)importedPkgs[i].getSupplier(); + // Continue if the Imported Package is unresolved of the package is optional and don't want optional packages + if (exporter == null || (!includeOptional && Constants.RESOLUTION_OPTIONAL.equals(importedPkgs[i] + .getDirective(Constants.RESOLUTION_DIRECTIVE)))) + continue; + addBundleAndDependencies(exporter.getExporter(), bundleIds, includeOptional); + } + BundleDescription[] fragments = desc.getFragments(); + for (int i = 0; i < fragments.length; i++) { + if (!fragments[i].isResolved()) + continue; + String id = fragments[i].getSymbolicName(); + if (!"org.eclipse.ui.workbench.compatibility".equals(id)) //$NON-NLS-1$ + addBundleAndDependencies(fragments[i], bundleIds, includeOptional); + } + HostSpecification host = desc.getHost(); + if (host != null) + addBundleAndDependencies((BundleDescription)host.getSupplier(), bundleIds, includeOptional); + } + } + + public BundleDescription getBundleDescription(MavenProject project) { + String location = project.getFile().getParentFile().getAbsolutePath(); + return state.getBundleByLocation(location); + } + + public BundleDescription getBundleDescription(File location) { + String absolutePath = location.getAbsolutePath(); + return state.getBundleByLocation(absolutePath); + } + + private static void setUserProperty(BundleDescription desc, String name, Object value) { + Object userObject = desc.getUserObject(); + + if (userObject != null && !(userObject instanceof Map)) { + throw new IllegalStateException("Unexpected user object " + desc.toString()); + } + + Map props = (Map)userObject; + if (props == null) { + props = new HashMap(); + desc.setUserObject(props); + } + + props.put(name, value); + } + + private static Object getUserProperty(BundleDescription desc, String name) { + Object userObject = desc.getUserObject(); + if (userObject instanceof Map) { + return ((Map)userObject).get(name); + } + return null; + } + + public MavenProject getMavenProject(BundleDescription desc) { + return (MavenProject)getUserProperty(desc, PROP_MAVEN_PROJECT); + } + + public void assertResolved(BundleDescription desc) throws BundleException { + if (!desc.isResolved()) { + StringBuffer msg = new StringBuffer(); + msg.append("Bundle ").append(desc.getSymbolicName()).append(" cannot be resolved\n"); + msg.append("Resolution errors:\n"); + ResolverError[] errors = getResolverErrors(desc); + for (int i = 0; i < errors.length; i++) { + ResolverError error = errors[i]; + msg.append(error).append("\n"); + } + + throw new BundleException(msg.toString()); + } + } + + public String getManifestAttribute(BundleDescription desc, String attr) { + Dictionary mf = (Dictionary)getUserProperty(desc, PROP_MANIFEST); + if (mf != null) { + return (String)mf.get(attr); + } + return null; + } + + private static File toFile(URL url) { + if (url.getProtocol().equals("file") == false) { + return null; + } else { + String filename = url.getFile().replace('/', File.separatorChar).replace("%20", " "); + return new File(filename); + } + } + + /* + public static void main(String[] args) throws Exception { + BundleResolver resolver = new BundleResolver(new ConsoleLogger(Logger.LEVEL_INFO, "tuscany")); + + String home = System.getProperty("user.home"); + File jar = + new File(new File(home), + ".m2/repository/org/apache/tuscany/sca/tuscany-sca-api/1.4-SNAPSHOT/tuscany-sca-api-1.4-SNAPSHOT.jar"); + BundleDescription bundle = resolver.addBundle(jar); + resolver.resolveState(); + resolver.assertResolved(bundle); + } + */ +} diff --git a/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleUtil.java b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleUtil.java new file mode 100644 index 0000000000..0869183686 --- /dev/null +++ b/branches/sca-equinox/tools/maven/maven-eclipse-compiler/src/main/java/org/apache/tuscany/sca/tools/maven/compiler/osgi/BundleUtil.java @@ -0,0 +1,584 @@ +/* + * 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.tuscany.sca.tools.maven.compiler.osgi; + +import static org.osgi.framework.Constants.BUNDLE_CLASSPATH; +import static org.osgi.framework.Constants.BUNDLE_MANIFESTVERSION; +import static org.osgi.framework.Constants.BUNDLE_NAME; +import static org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME; +import static org.osgi.framework.Constants.BUNDLE_VERSION; +import static org.osgi.framework.Constants.DYNAMICIMPORT_PACKAGE; +import static org.osgi.framework.Constants.EXPORT_PACKAGE; +import static org.osgi.framework.Constants.IMPORT_PACKAGE; + +import java.io.BufferedInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.eclipse.osgi.framework.internal.core.Constants; +import org.eclipse.osgi.framework.internal.core.FrameworkProperties; +import org.eclipse.osgi.util.ManifestElement; +import org.osgi.framework.Version; + +/** + * Common functions used by the plugin. + * + * @version $Rev$ $Date$ + */ +final class BundleUtil { + + /** + * Returns the name of a bundle, or null if the given file is not a bundle. + * + * @param file + * @return + * @throws IOException + */ + static String getBundleSymbolicName(File file) throws IOException { + if (!file.exists()) { + return null; + } + String bundleName = null; + if (file.isDirectory()) { + File mf = new File(file, JarFile.MANIFEST_NAME); + if (mf.isFile()) { + Manifest manifest = new Manifest(new FileInputStream(mf)); + bundleName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME); + } + } else { + JarFile jar = new JarFile(file, false); + Manifest manifest = jar.getManifest(); + bundleName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME); + jar.close(); + } + if (bundleName == null) { + return bundleName; + } + int sc = bundleName.indexOf(';'); + if (sc != -1) { + bundleName = bundleName.substring(0, sc); + } + return bundleName; + } + + static Manifest getManifest(File file) throws IOException { + if (!file.exists()) { + return null; + } + Manifest manifest = null; + String bundleName = null; + if (file.isDirectory()) { + File mf = new File(file, JarFile.MANIFEST_NAME); + if (!mf.isFile()) { + mf = new File(file, "../../" + JarFile.MANIFEST_NAME); + } + if (mf.isFile()) { + manifest = new Manifest(new FileInputStream(mf)); + bundleName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME); + } + } else { + JarFile jar = new JarFile(file, false); + manifest = jar.getManifest(); + bundleName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME); + jar.close(); + } + if (bundleName != null) { + return manifest; + } + + if (file.isFile()) { + Set<File> jars = new HashSet<File>(); + jars.add(file); + String name = file.getName().substring(0, file.getName().lastIndexOf(".jar")); + manifest = libraryManifest(jars, name, name, "0.0.0", null); + } + return manifest; + } + + /** + * Generate a Bundle manifest for a set of JAR files. + * + * @param jarFiles + * @param name + * @param symbolicName + * @param version + * @param dir + * @return + * @throws IllegalStateException + */ + static Manifest libraryManifest(Set<File> jarFiles, String name, String symbolicName, String version, String dir) + throws IllegalStateException { + try { + + // List exported packages and bundle classpath entries + StringBuffer classpath = new StringBuffer(); + Set<String> exportedPackages = new HashSet<String>(); + for (File jarFile : jarFiles) { + addPackages(jarFile, exportedPackages, version); + if (dir != null) { + classpath.append(dir).append("/"); + classpath.append(jarFile.getName()); + } else { + classpath.append("\"external:"); + classpath.append(jarFile.getPath().replace(File.separatorChar, '/')); + classpath.append("\""); + } + classpath.append(","); + } + + // Generate export-package and import-package declarations + StringBuffer exports = new StringBuffer(); + StringBuffer imports = new StringBuffer(); + Set<String> importedPackages = new HashSet<String>(); + for (String export : exportedPackages) { + + // Add export declaration + exports.append(export); + exports.append(','); + + // Add corresponding import declaration + String packageName = packageName(export); + if (!importedPackages.contains(packageName)) { + importedPackages.add(packageName); + imports.append(packageName); + imports.append(','); + } + } + + // Create a manifest + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue("Manifest-Version", "1.0"); + attributes.putValue(BUNDLE_MANIFESTVERSION, "2"); + attributes.putValue(BUNDLE_SYMBOLICNAME, symbolicName); + attributes.putValue(BUNDLE_NAME, name); + attributes.putValue(BUNDLE_VERSION, version); + attributes.putValue(DYNAMICIMPORT_PACKAGE, "*"); + if (exports.length() > 1) { + attributes.putValue(EXPORT_PACKAGE, exports.substring(0, exports.length() - 1)); + } + if (imports.length() > 1) { + attributes.putValue(IMPORT_PACKAGE, imports.substring(0, imports.length() - 1)); + } + if (classpath.length() > 1) { + attributes.putValue(BUNDLE_CLASSPATH, classpath.substring(0, classpath.length() - 1)); + } + + return manifest; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Write a bundle manifest. + * + * @param manifest + * @param out + * @throws IOException + */ + static void write(Manifest manifest, OutputStream out) throws IOException { + DataOutputStream dos = new DataOutputStream(out); + Attributes attributes = manifest.getMainAttributes(); + write(attributes, "Manifest-Version", dos); + write(attributes, BUNDLE_MANIFESTVERSION, dos); + write(attributes, BUNDLE_SYMBOLICNAME, dos); + write(attributes, BUNDLE_NAME, dos); + write(attributes, BUNDLE_VERSION, dos); + write(attributes, DYNAMICIMPORT_PACKAGE, dos); + write(attributes, BUNDLE_CLASSPATH, dos); + write(attributes, IMPORT_PACKAGE, dos); + write(attributes, EXPORT_PACKAGE, dos); + dos.flush(); + } + + /** + * Add packages to be exported out of a JAR file. + * + * @param jarFile + * @param packages + * @throws IOException + */ + private static void addPackages(File jarFile, Set<String> packages, String version) throws IOException { + if (getBundleSymbolicName(jarFile) == null) { + String ver = ";version=" + version; + addAllPackages(jarFile, packages, ver); + } else { + addExportedPackages(jarFile, packages); + } + } + + /** + * Write manifest attributes. + * + * @param attributes + * @param key + * @param dos + * @throws IOException + */ + private static void write(Attributes attributes, String key, DataOutputStream dos) throws IOException { + String value = attributes.getValue(key); + if (value == null) { + return; + } + StringBuffer line = new StringBuffer(); + line.append(key); + line.append(": "); + line.append(new String(value.getBytes("UTF8"))); + line.append("\r\n"); + int l = line.length(); + if (l > 72) { + for (int i = 70; i < l - 2;) { + line.insert(i, "\r\n "); + i += 72; + l += 3; + } + } + dos.writeBytes(line.toString()); + } + + /** + * Strip an OSGi export, only retain the package name and version. + * + * @param export + * @return + */ + private static String stripExport(String export) { + int sc = export.indexOf(';'); + if (sc == -1) { + return export; + } + String base = export.substring(0, sc); + int v = export.indexOf("version="); + if (v != -1) { + sc = export.indexOf(';', v + 1); + if (sc != -1) { + return base + ";" + export.substring(v, sc); + } else { + return base + ";" + export.substring(v); + } + } else { + return base; + } + } + + /** + * Add all the packages out of a JAR. + * + * @param jarFile + * @param packages + * @param version + * @throws IOException + */ + private static void addAllPackages(File jarFile, Set<String> packages, String version) throws IOException { + ZipInputStream is = new ZipInputStream(new FileInputStream(jarFile)); + ZipEntry entry; + while ((entry = is.getNextEntry()) != null) { + String entryName = entry.getName(); + if (!entry.isDirectory() && entryName != null + && entryName.length() > 0 + && !entryName.startsWith(".") + && entryName.endsWith(".class") // Exclude resources from Export-Package + && entryName.lastIndexOf("/") > 0 + && Character.isJavaIdentifierStart(entryName.charAt(0))) { + String pkg = entryName.substring(0, entryName.lastIndexOf("/")).replace('/', '.'); + if (!pkg.endsWith(".enum")) { + packages.add(pkg + version); + } + } + } + is.close(); + } + + /** + * Returns the name of the exported package in the given export. + * @param export + * @return + */ + private static String packageName(String export) { + int sc = export.indexOf(';'); + if (sc != -1) { + export = export.substring(0, sc); + } + return export; + } + + /** + * Add the packages exported by a bundle. + * + * @param file + * @param packages + * @return + * @throws IOException + */ + private static void addExportedPackages(File file, Set<String> packages) throws IOException { + if (!file.exists()) { + return; + } + + // Read the export-package declaration and get a list of the packages available in a JAR + Set<String> existingPackages = null; + String exports = null; + if (file.isDirectory()) { + File mf = new File(file, "META-INF/MANIFEST.MF"); + if (mf.isFile()) { + Manifest manifest = new Manifest(new FileInputStream(mf)); + exports = manifest.getMainAttributes().getValue(EXPORT_PACKAGE); + } + } else { + JarFile jar = new JarFile(file, false); + Manifest manifest = jar.getManifest(); + exports = manifest.getMainAttributes().getValue(EXPORT_PACKAGE); + jar.close(); + existingPackages = new HashSet<String>(); + addAllPackages(file, existingPackages, ""); + } + if (exports == null) { + return; + } + + // Parse the export-package declaration, and extract the individual packages + StringBuffer buffer = new StringBuffer(); + boolean q = false; + for (int i = 0, n = exports.length(); i < n; i++) { + char c = exports.charAt(i); + if (c == '\"') { + q = !q; + } + if (!q) { + if (c == ',') { + + // Add the exported package to the set, after making sure it really exists in + // the JAR + String export = buffer.toString(); + if (existingPackages == null || existingPackages.contains(packageName(export))) { + packages.add(stripExport(export)); + } + buffer = new StringBuffer(); + continue; + } + } + buffer.append(c); + } + if (buffer.length() != 0) { + + // Add the exported package to the set, after making sure it really exists in + // the JAR + String export = buffer.toString(); + if (existingPackages == null || existingPackages.contains(packageName(export))) { + packages.add(stripExport(export)); + } + } + } + + /** + * Convert the maven version into OSGi version + * @param mavenVersion + * @return + */ + static String osgiVersion(String mavenVersion) { + ArtifactVersion ver = new DefaultArtifactVersion(mavenVersion); + String qualifer = ver.getQualifier(); + if (qualifer != null) { + StringBuffer buf = new StringBuffer(qualifer); + for (int i = 0; i < buf.length(); i++) { + char c = buf.charAt(i); + if (Character.isLetterOrDigit(c) || c == '-' || c == '_') { + // Keep as-is + } else { + buf.setCharAt(i, '_'); + } + } + qualifer = buf.toString(); + } + Version osgiVersion = + new Version(ver.getMajorVersion(), ver.getMinorVersion(), ver.getIncrementalVersion(), qualifer); + String version = osgiVersion.toString(); + return version; + } + + private static String J2SE = "J2SE-"; + private static String JAVASE = "JavaSE-"; + private static String PROFILE_EXT = ".profile"; + + public static void loadVMProfile(Properties properties) { + Properties profileProps = findVMProfile(properties); + String systemExports = properties.getProperty(Constants.OSGI_FRAMEWORK_SYSTEM_PACKAGES); + // set the system exports property using the vm profile; only if the property is not already set + if (systemExports == null) { + systemExports = profileProps.getProperty(Constants.OSGI_FRAMEWORK_SYSTEM_PACKAGES); + if (systemExports != null) + properties.put(Constants.OSGI_FRAMEWORK_SYSTEM_PACKAGES, systemExports); + } + // set the org.osgi.framework.bootdelegation property according to the java profile + String type = properties.getProperty(Constants.OSGI_JAVA_PROFILE_BOOTDELEGATION); // a null value means ignore + String profileBootDelegation = profileProps.getProperty(Constants.OSGI_BOOTDELEGATION); + if (Constants.OSGI_BOOTDELEGATION_OVERRIDE.equals(type)) { + if (profileBootDelegation == null) + properties.remove(Constants.OSGI_BOOTDELEGATION); // override with a null value + else + properties.put(Constants.OSGI_BOOTDELEGATION, profileBootDelegation); // override with the profile value + } else if (Constants.OSGI_BOOTDELEGATION_NONE.equals(type)) + properties.remove(Constants.OSGI_BOOTDELEGATION); // remove the bootdelegation property in case it was set + // set the org.osgi.framework.executionenvironment property according to the java profile + if (properties.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT) == null) { + // get the ee from the java profile; if no ee is defined then try the java profile name + String ee = + profileProps.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT, profileProps + .getProperty(Constants.OSGI_JAVA_PROFILE_NAME)); + if (ee != null) + properties.put(Constants.FRAMEWORK_EXECUTIONENVIRONMENT, ee); + } + } + + private static Properties findVMProfile(Properties properties) { + Properties result = new Properties(); + // Find the VM profile name using J2ME properties + String j2meConfig = properties.getProperty(Constants.J2ME_MICROEDITION_CONFIGURATION); + String j2meProfiles = properties.getProperty(Constants.J2ME_MICROEDITION_PROFILES); + String vmProfile = null; + String javaEdition = null; + Version javaVersion = null; + if (j2meConfig != null && j2meConfig.length() > 0 && j2meProfiles != null && j2meProfiles.length() > 0) { + // save the vmProfile based off of the config and profile + // use the last profile; assuming that is the highest one + String[] j2meProfileList = ManifestElement.getArrayFromList(j2meProfiles, " "); + if (j2meProfileList != null && j2meProfileList.length > 0) + vmProfile = j2meConfig + '_' + j2meProfileList[j2meProfileList.length - 1]; + } else { + // No J2ME properties; use J2SE properties + // Note that the CDC spec appears not to require VM implementations to set the + // javax.microedition properties!! So we will try to fall back to the + // java.specification.name property, but this is pretty ridiculous!! + String javaSpecVersion = properties.getProperty("java.specification.version"); + // set the profile and EE based off of the java.specification.version + // TODO We assume J2ME Foundation and J2SE here. need to support other profiles J2EE ... + if (javaSpecVersion != null) { + StringTokenizer st = new StringTokenizer(javaSpecVersion, " _-"); + javaSpecVersion = st.nextToken(); + String javaSpecName = properties.getProperty("java.specification.name"); + if ("J2ME Foundation Specification".equals(javaSpecName)) + vmProfile = "CDC-" + javaSpecVersion + "_Foundation-" + javaSpecVersion; //$NON-NLS-2$ + else { + // look for JavaSE if 1.6 or greater; otherwise look for J2SE + Version v16 = new Version("1.6"); + javaEdition = J2SE; + try { + javaVersion = new Version(javaSpecVersion); + if (v16.compareTo(javaVersion) <= 0) + javaEdition = JAVASE; + } catch (IllegalArgumentException e) { + // do nothing + } + vmProfile = javaEdition + javaSpecVersion; + } + } + } + URL url = null; + // check for the java profile property for a url + String propJavaProfile = FrameworkProperties.getProperty(Constants.OSGI_JAVA_PROFILE); + if (propJavaProfile != null) + try { + // we assume a URL + url = new URL(propJavaProfile); + } catch (MalformedURLException e1) { + // try using a relative path in the system bundle + url = findInSystemBundle(propJavaProfile); + } + if (url == null && vmProfile != null) { + // look for a profile in the system bundle based on the vm profile + String javaProfile = vmProfile + PROFILE_EXT; + url = findInSystemBundle(javaProfile); + if (url == null) + url = getNextBestProfile(javaEdition, javaVersion); + } + if (url == null) + // the profile url is still null then use the osgi min profile in OSGi by default + url = findInSystemBundle("OSGi_Minimum-1.1.profile"); + if (url != null) { + InputStream in = null; + try { + in = url.openStream(); + result.load(new BufferedInputStream(in)); + } catch (IOException e) { + // TODO consider logging ... + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ee) { + // do nothing + } + } + } + // set the profile name if it does not provide one + if (result.getProperty(Constants.OSGI_JAVA_PROFILE_NAME) == null) + if (vmProfile != null) + result.put(Constants.OSGI_JAVA_PROFILE_NAME, vmProfile.replace('_', '/')); + else + // last resort; default to the absolute minimum profile name for the framework + result.put(Constants.OSGI_JAVA_PROFILE_NAME, "OSGi/Minimum-1.1"); + return result; + } + + private static URL getNextBestProfile(String javaEdition, Version javaVersion) { + if (javaVersion == null || (javaEdition != J2SE && javaEdition != JAVASE)) + return null; // we cannot automatically choose the next best profile unless this is a J2SE or JavaSE vm + URL bestProfile = findNextBestProfile(javaEdition, javaVersion); + if (bestProfile == null && javaEdition == JAVASE) + // if this is a JavaSE VM then search for a lower J2SE profile + bestProfile = findNextBestProfile(J2SE, javaVersion); + return bestProfile; + } + + private static URL findNextBestProfile(String javaEdition, Version javaVersion) { + URL result = null; + int minor = javaVersion.getMinor(); + do { + result = findInSystemBundle(javaEdition + javaVersion.getMajor() + "." + minor + PROFILE_EXT); + minor = minor - 1; + } while (result == null && minor > 0); + return result; + } + + private static URL findInSystemBundle(String entry) { + ClassLoader loader = BundleUtil.class.getClassLoader(); + return loader == null ? ClassLoader.getSystemResource(entry) : loader.getResource(entry); + } + + +} |