/******************************************************************************* * Copyright (c) 2008, 2010 VMware Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VMware Inc. - initial contribution *******************************************************************************/ package org.eclipse.virgo.kernel.userregion.internal.importexpansion; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.virgo.kernel.osgi.framework.ImportMergeException; import org.eclipse.virgo.nano.serviceability.Assert; import org.eclipse.virgo.util.osgi.manifest.VersionRange; import org.eclipse.virgo.util.osgi.manifest.ImportedPackage; import org.eclipse.virgo.util.osgi.manifest.Resolution; /** * {@link AbstractTrackedPackageImports} provides the general implementations of {@link TrackedPackageImports}. * <p /> * * <strong>Concurrent Semantics</strong><br /> * * This class is thread safe. * */ abstract class AbstractTrackedPackageImports implements TrackedPackageImports { protected static final String SOURCE_SEPARATOR = ", "; private static final String VERSION_ATTRIBUTE_NAME = "version"; private static final String VERSION_ALTERNATE_ATTRIBUTE_NAME = "specification-version"; private static final String BUNDLE_VERSION_ATTRIBUTE_NAME = "bundle-version"; private static final Object tieMonitor = new Object(); // locking for hash clashes in isEquivalent private final Object monitor = new Object(); /** * The current merged package imports are held as a map of package name to {@link ImportedPackage}. Each package * import contains one and only one imported package name which corresponds to the package name used to index the * package import in the map. The map is valid if and only if mergeException is <code>null</code>. */ protected final Map<String, ImportedPackage> mergedPackageImports = new HashMap<String, ImportedPackage>(); private ImportMergeException mergeException = null; protected final List<TrackedPackageImports> sources = new ArrayList<TrackedPackageImports>(); /** * Construct an {@link AbstractTrackedPackageImports} from the given package imports. * * @param initialPackageImports a map of package name to {@link ImportedPackage} */ AbstractTrackedPackageImports(Map<String, ImportedPackage> packageImports) { this.mergedPackageImports.putAll(packageImports); } /** * {@inheritDoc} */ public void merge(TrackedPackageImports importsToMerge) throws ImportMergeException { synchronized (this.monitor) { checkMergeException(); try { // Add the new imports before merging so they are included in any diagnostics. sources.add(importsToMerge); doMerge(importsToMerge); } catch (ImportMergeException e) { this.mergeException = e; throw e; } } } /** * Merge the given package imports into this collection of package imports. If there is a conflict, issue * diagnostics and throw {@link ImportMergeException}. * <p /> * Pre-condition: the monitor is held and the current merged imports have no conflicts. * * @param importsToMerge * @throws ImportMergeException */ private void doMerge(TrackedPackageImports importsToMerge) throws ImportMergeException { List<ImportedPackage> mergedImportsToMerge = importsToMerge.getMergedImports(); for (ImportedPackage packageImportToMerge : mergedImportsToMerge) { String pkg = packageImportToMerge.getPackageName(); ImportedPackage mergedPackageImport = this.mergedPackageImports.get(pkg); if (mergedPackageImport == null) { this.mergedPackageImports.put(pkg, packageImportToMerge); } else { mergePackageImport(mergedPackageImport, packageImportToMerge); } } } /** * Merge the given source package import into the given target package import. Throw {@link ImportMergeException} if * and only if there is a merge clash. * * @param targetPackageImport the package import to be merged and updated * @param sourceImportToMerge the package import to be merged in * @throws ImportMergeException thrown if there is a merge clash */ private void mergePackageImport(ImportedPackage targetPackageImport, ImportedPackage sourceImportToMerge) throws ImportMergeException { mergeAttributes(targetPackageImport, sourceImportToMerge); mergeDirectives(targetPackageImport, sourceImportToMerge); } /** * Merge the attributes of the source package import into those of the target package import. Throw * {@link ImportMergeException} if and only if there is a merge clash. * * @param targetPackageImport the package import to be merged and updated * @param sourceImportToMerge the package import to be merged in * @throws ImportMergeException thrown if there is a merge clash */ private void mergeAttributes(ImportedPackage targetPackageImport, ImportedPackage sourceImportToMerge) throws ImportMergeException { Map<String, String> targetAttributes = targetPackageImport.getAttributes(); Map<String, String> sourceAttributes = sourceImportToMerge.getAttributes(); // Merge attributes before versions so, for example, conflicting bundle symbolic names take precedence over disjoint bundle version ranges. for (Entry<String, String> sourceAttributeEntry : sourceAttributes.entrySet()) { String sourceAttributeName = sourceAttributeEntry.getKey(); if (!isVersionAttribute(sourceAttributeName)) { String sourceAttributeValue = sourceAttributeEntry.getValue(); String targetAttributeValue = targetAttributes.get(sourceAttributeName); if (targetAttributeValue != null) { if (!targetAttributeValue.equals(sourceAttributeValue)) { throw new ImportMergeException(targetPackageImport.getPackageName(), getPackageSources(targetPackageImport), "conflicting values '" + sourceAttributeValue + "', '" + targetAttributeValue + "' of attribute '" + sourceAttributeName + "'"); } } else { targetAttributes.put(sourceAttributeName, sourceAttributeValue); } } } mergeVersionRanges(targetPackageImport, sourceImportToMerge); mergeBundleVersionRanges(targetPackageImport, sourceImportToMerge); } /** * Merge the version ranges of the given source and target attributes and update the target attributes if necessary * with the merged range. If the version ranges are disjoint, throw {@link ImportMergeException}. * * @param targetPackageImport the package import to be merged and updated * @param sourceAttributes * @throws ImportMergeException */ private void mergeVersionRanges(ImportedPackage targetPackageImport, ImportedPackage sourceImportToMerge) throws ImportMergeException { Map<String, String> sourceAttributes = sourceImportToMerge.getAttributes(); VersionRange sourceVersionRange = getVersionRange(sourceAttributes); if (sourceVersionRange != null) { Map<String, String> targetAttributes = targetPackageImport.getAttributes(); VersionRange targetVersionRange = getVersionRange(targetAttributes); VersionRange mergedVersionRange; if (targetVersionRange == null) { mergedVersionRange = sourceVersionRange; } else { mergedVersionRange = VersionRange.intersection(sourceVersionRange, targetVersionRange); if (mergedVersionRange.isEmpty()) { throw new ImportMergeException(targetPackageImport.getPackageName(), getPackageSources(targetPackageImport), "disjoint package version ranges"); } } targetAttributes.put(VERSION_ATTRIBUTE_NAME, mergedVersionRange.toParseString()); } } /** * Get the version range for the given attributes. * * @param attributes the attributes which may specify a version range * @return the version range or <code>null</code> if no version range is specified */ private VersionRange getVersionRange(Map<String, String> attributes) { String versionRangeString = attributes.get(VERSION_ATTRIBUTE_NAME); if (versionRangeString == null) { versionRangeString = attributes.get(VERSION_ALTERNATE_ATTRIBUTE_NAME); } return versionRangeString == null ? null : new VersionRange(versionRangeString); } /** * Merge the bundle version ranges of the given source and target attributes and update the target attributes if * necessary with the merged range. If the bundle version ranges are disjoint, throw {@link ImportMergeException}. * * @param targetPackageImport the package import to be merged and updated * @param sourceAttributes * @throws ImportMergeException */ private void mergeBundleVersionRanges(ImportedPackage targetPackageImport, ImportedPackage sourceImportToMerge) throws ImportMergeException { VersionRange sourceVersionRange = sourceImportToMerge.getBundleVersion(); // Map<String, String> targetAttributes = targetPackageImport.getAttributes(); VersionRange targetVersionRange = targetPackageImport.getBundleVersion(); VersionRange mergedVersionRange; if (targetVersionRange == null) { mergedVersionRange = sourceVersionRange; } else { mergedVersionRange = VersionRange.intersection(sourceVersionRange, targetVersionRange); if (mergedVersionRange.isEmpty()) { throw new ImportMergeException(targetPackageImport.getPackageName(), getPackageSources(targetPackageImport), "disjoint bundle version ranges " + sourceVersionRange.toString() + " and " + targetVersionRange.toString()); } } targetPackageImport.setBundleVersion(mergedVersionRange); } /** * Return <code>true</code> if and only if the given attribute name is that of a version or bundle version * attribute. * * @param attributeName the attribute name * @return whether or not the given attribute name is that of a version or bundle version attribute */ private boolean isVersionAttribute(String attributeName) { return VERSION_ATTRIBUTE_NAME.equals(attributeName) || VERSION_ALTERNATE_ATTRIBUTE_NAME.equals(attributeName) || BUNDLE_VERSION_ATTRIBUTE_NAME.equals(attributeName); } /** * Merge the directives of the source package import into those of the target package import. * * @param targetPackageImport * @param sourceImportToMerge */ private void mergeDirectives(ImportedPackage targetPackageImport, ImportedPackage sourceImportToMerge) { if (targetPackageImport.getResolution() == Resolution.OPTIONAL && sourceImportToMerge.getResolution() == Resolution.MANDATORY) { targetPackageImport.setResolution(Resolution.MANDATORY); } } /** * Get a string describing the sources of the package of the given package import. This should help in diagnosing * the root cause of a conflicting merge. * * @param pkg the package whose sources are required * @return a string describing the given package's sources */ private String getPackageSources(ImportedPackage packageImport) { return getSources(packageImport.getPackageName()); } /** * {@inheritDoc} */ public String getSources(String pkg) { synchronized (this.monitor) { StringBuilder sourcesDescription = new StringBuilder(); boolean first = true; String source = getSource(pkg); if (source != null) { sourcesDescription.append(source); first = false; } for (TrackedPackageImports trackedPackageImports : this.sources) { String sources = trackedPackageImports.getSources(pkg); if (sources != null) { if (!first) { sourcesDescription.append(SOURCE_SEPARATOR); } sourcesDescription.append(sources); first = false; } } return first ? null : sourcesDescription.toString(); } } /** * {@inheritDoc} */ public final List<ImportedPackage> getMergedImports() throws ImportMergeException { synchronized (this.monitor) { checkMergeException(); List<ImportedPackage> mergedImports = new ArrayList<ImportedPackage>(); for (ImportedPackage packageImport : this.mergedPackageImports.values()) { mergedImports.add(packageImport); } return mergedImports; } } /** * If a merge failure has occurred, re-throw the {@link ImportMergeException}. * * @throws ImportMergeException */ private void checkMergeException() throws ImportMergeException { if (this.mergeException != null) { throw this.mergeException; } } /** * Convert a given list of package imports with no duplicate package names to a map of package name to * {@link PackageImport}. * * @param importedPackages a list of <code>PackageImport</code> * @return a map of package name to <code>PackageImport</code> */ protected static Map<String, ImportedPackage> convertImportedPackageListToMap(List<ImportedPackage> importedPackages) { Map<String, ImportedPackage> initialPackageImports = new HashMap<String, ImportedPackage>(); for (ImportedPackage importedPackage : importedPackages) { Assert.isNull(initialPackageImports.put(importedPackage.getPackageName(), importedPackage), "input packageImports must not contain duplicate items"); } return initialPackageImports; } /** * {@inheritDoc} */ public boolean isEmpty() { synchronized (this.monitor) { return this.mergedPackageImports.isEmpty(); } } /** * {@inheritDoc} */ public boolean isEquivalent(TrackedPackageImports otherTrackedPackageImports) { if (otherTrackedPackageImports == null) { return isEmpty(); } Assert.isInstanceOf(AbstractTrackedPackageImports.class, otherTrackedPackageImports, "otherTrackedPackageImports must be of type AbstractTrackedPackageImports"); AbstractTrackedPackageImports otherAbstractTrackedPackageImports = (AbstractTrackedPackageImports) otherTrackedPackageImports; // Lock the object monitors in a predictable order to avoid deadlocks. // Use hash to determine ordering, and tieMonitor (static monitor) when hashes coincide. int thisHash = System.identityHashCode(this); int otherHash = System.identityHashCode(otherTrackedPackageImports); if (thisHash > otherHash) { synchronized (this.monitor) { synchronized (otherAbstractTrackedPackageImports.monitor) { return this.mergedPackageImports.equals(otherAbstractTrackedPackageImports.mergedPackageImports); } } } else if (thisHash < otherHash) { synchronized (otherAbstractTrackedPackageImports.monitor) { synchronized (this.monitor) { return this.mergedPackageImports.equals(otherAbstractTrackedPackageImports.mergedPackageImports); } } } else { synchronized (tieMonitor) { synchronized (otherAbstractTrackedPackageImports.monitor) { synchronized (this.monitor) { return this.mergedPackageImports.equals(otherAbstractTrackedPackageImports.mergedPackageImports); } } } } } }