/******************************************************************************* * Copyright (c) 2009, 2010 SpringSource, a divison of 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: * SpringSource, a division of VMware, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.virgo.ide.jdt.internal.core.classpath; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.jdt.core.IAccessRule; import org.eclipse.jdt.core.IClasspathAttribute; import org.eclipse.jdt.core.IClasspathContainer; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.virgo.ide.facet.core.FacetUtils; import org.eclipse.virgo.ide.jdt.core.JdtCorePlugin; import org.eclipse.virgo.ide.jdt.internal.core.util.ClasspathUtils; import org.eclipse.virgo.ide.jdt.internal.core.util.MarkerUtils; import org.eclipse.virgo.ide.manifest.core.BundleManifestCorePlugin; import org.eclipse.virgo.ide.manifest.core.BundleManifestUtils; import org.eclipse.virgo.ide.manifest.core.IBundleManifestManager; import org.eclipse.virgo.ide.manifest.core.IBundleManifestMangerWorkingCopy; import org.eclipse.virgo.ide.manifest.core.dependencies.IDependencyLocator; import org.eclipse.virgo.ide.par.Bundle; import org.eclipse.virgo.ide.par.Par; import org.eclipse.virgo.ide.runtime.core.ServerUtils; import org.eclipse.virgo.kernel.osgi.provisioning.tools.DependencyLocationException; import org.eclipse.virgo.kernel.osgi.provisioning.tools.DependencyLocator; import org.eclipse.virgo.util.osgi.manifest.BundleManifest; import org.eclipse.wst.server.core.IRuntime; /** * {@link IClasspathContainer} that installs the resolved dependencies taken from a {@link IJavaProject}'s bundle * manifest. * <p> * This implementation creates very rigorous accessibility rules on every {@link IClasspathEntry} that it creates. Those * rules match the OSGi runtime environment and therefore mirror the runtime class path in the SpringSource AP. * * @author Christian Dupuis * @since 1.0.0 */ public class ServerClasspathContainer implements IClasspathContainer { /** * Key of the {@link IClasspathAttribute} that indicates that a certain {@link IClasspathEntry} has been created by * this container */ public static final String CLASSPATH_ATTRIBUTE_VALUE = JdtCorePlugin.PLUGIN_ID + ".CLASSPATH_ENTRY"; public static final String MANIFEST_TIMESTAMP = "MANIFEST_TIMESTAMP"; /** Name of this class path container to be stored by JDT */ private static final String CLASSPATH_CONTAINER = JdtCorePlugin.PLUGIN_ID + ".MANIFEST_CLASSPATH_CONTAINER"; /** Classpath container */ public static final String CLASSPATH_CONTAINER_DESCRIPTION = "Bundle Dependencies"; /** Unique path of this class path container */ public static final IPath CLASSPATH_CONTAINER_PATH = new Path(CLASSPATH_CONTAINER); /** * Key of the {@link IClasspathAttribute} that indicates that a certain {@link IClasspathEntry} has been created by * this container and is a test dependency */ public static final String TEST_CLASSPATH_ATTRIBUTE_VALUE = JdtCorePlugin.PLUGIN_ID + ".TEST_CLASSPATH_ENTRY"; /** * {@link IClasspathAttribute} to prevent WTP warning of non exportable container */ public static final IClasspathAttribute[] CLASSPATH_CONTAINER_ATTRIBUTE = new IClasspathAttribute[] { JavaCore.newClasspathAttribute("org.eclipse.jst.component.nondependency", "") }; /** Internal cache of {@link IAccessRule}s keyed by a {@link IPath} */ private static Map<IPath, IAccessRule> accessibleRules = new ConcurrentHashMap<IPath, IAccessRule>(); /** * {@link IClasspathAttribute} that is installed on a {@link IClasspathEntry} to indicate that a certain entry has * been created by this class path container */ private static final IClasspathAttribute[] CLASSPATH_ATTRIBUTES = new IClasspathAttribute[] { JavaCore.newClasspathAttribute(CLASSPATH_ATTRIBUTE_VALUE, "true") }; /** Wildcard string used to append to a package */ private static final String PACKAGE_WILDCARD = "/*"; /** * {@link IClasspathAttribute} that is installed on a {@link IClasspathEntry} to indicate that a certain entry has * been created by this class path container and is a test dependency */ private static final IClasspathAttribute[] TEST_CLASSPATH_ATTRIBUTES = new IClasspathAttribute[] { JavaCore.newClasspathAttribute(TEST_CLASSPATH_ATTRIBUTE_VALUE, "true") }; /** Wildcard string indicating the entire packages in a certain {@link IClasspathEntry} */ private static final String WILDCARD_PATH = "**/*"; /** {@link IAccessRule} that enables access to any package in a {@link IClasspathEntry}. */ private static final IAccessRule WILDCARD_ACCESSIBLE_RULE = JavaCore.newAccessRule(new Path(WILDCARD_PATH), IAccessRule.K_ACCESSIBLE); /** * {@link IAccessRule} that disables access to any package in a {@link IClasspathEntry}. This rule will be ignored * for any package that specifies {@link IAccessRule#K_ACCESSIBLE}, even for the same package (due to * {@link IAccessRule#IGNORE_IF_BETTER}. */ private static final IAccessRule WILDCARD_NON_ACCESSIBLE_RULE = JavaCore.newAccessRule(new Path(WILDCARD_PATH), IAccessRule.K_NON_ACCESSIBLE | IAccessRule.IGNORE_IF_BETTER); /** * Looks up and returns a {@link IAccessRule} for the given <code>path</code>. * * @param path the path to look up or create the access rule for * @return a {@link IAccessRule} that allows to access the given <code>path</code> */ private static IAccessRule getAccessibleRule(IPath path) { if (!accessibleRules.containsKey(path)) { accessibleRules.put(path, JavaCore.newAccessRule(path, IAccessRule.K_ACCESSIBLE)); } return accessibleRules.get(path); } /** The calculated and stored {@link IClasspathEntry}s */ private IClasspathEntry[] entries; /** The internal flag to indicate the container has been initialized */ private volatile boolean initialized = false; /** The {@link IJavaProject} this class path container instance is responsible for */ private final IJavaProject javaProject; /** * Temporal storage for manifest locations to {@link IJavaProject}s. This is used to resolve inter-workspace * dependencies */ private Map<String, IJavaProject> manifestLocationsByProject; /** The set of server runtimes that are used to resolve the dependencies */ private IRuntime[] serverRuntimes; /** * Constructor to create a new class path container * * @param javaProject the {@link IJavaProject} that this container is responsible for */ public ServerClasspathContainer(IJavaProject javaProject) { this.javaProject = javaProject; this.entries = new IClasspathEntry[0]; } /** * Constructor to create a new class path container * * @param javaProject the {@link IJavaProject} that this container is responsible for * @param entries populate the list of {@link IClasspathEntry}s with the given list */ public ServerClasspathContainer(IJavaProject javaProject, IClasspathEntry[] entries) { this.javaProject = javaProject; this.entries = entries; this.initialized = true; // Store targeted runtimes to display in the description this.serverRuntimes = ServerUtils.getTargettedRuntimes(javaProject.getProject()); } /** * Returns the {@link IClasspathEntry}s calculated by this class path container */ public synchronized IClasspathEntry[] getClasspathEntries() { // make sure that the container is initialized on first access if (this.initialized) { return this.entries; } // refresh container before giving out the empty entries list refreshClasspathEntries(); return this.entries; } /** * Returns the description for this class path container */ public String getDescription() { return CLASSPATH_CONTAINER_DESCRIPTION; } /** * Returns the {@link IRuntime} that is project is targeted against * * @return the serverRuntimes */ public String getDescriptionSuffix() { StringBuilder builder = new StringBuilder(); if (this.serverRuntimes != null && this.serverRuntimes.length > 0) { builder.append(" ["); for (int i = 0; i < this.serverRuntimes.length; i++) { if (this.serverRuntimes[i] != null) { builder.append(this.serverRuntimes[i].getName()); if (i + 1 < this.serverRuntimes.length) { builder.append(", "); } } } builder.append("]"); } return builder.toString(); } /** * Returns the kind of this class path container */ public int getKind() { return K_APPLICATION; } /** * Returns the path of the class path container */ public IPath getPath() { return CLASSPATH_CONTAINER_PATH; } /** * Refresh the class path entries of the given {@link IJavaProject}. * <p> * This will install the new class path entries on the java project only if the entries have changed since the last * refresh. */ public void refreshClasspathEntries() { this.manifestLocationsByProject = new HashMap<String, IJavaProject>(); List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); IDependencyLocator locator = null; try { BundleManifest manifest = BundleManifestCorePlugin.getBundleManifestManager().getBundleManifest(this.javaProject); BundleManifest testManifest = BundleManifestCorePlugin.getBundleManifestManager().getTestBundleManifest(this.javaProject); if (manifest != null) { // Create DependencyLocator locator = createDependencyLocator(this.javaProject); // Resolve dependencies for the main manifest resolveDependencies(entries, manifest, locator, false); // Resolve dependencies for the test manifest if (testManifest != null) { resolveDependencies(entries, testManifest, locator, true); } } } catch (Throwable e) { JdtCorePlugin.log(e); } finally { // Shutdown DependencyLocator if (locator != null) { locator.shutdown(); } // Sort entries alphabetically sort better support the user Collections.sort(entries, new Comparator<IClasspathEntry>() { public int compare(IClasspathEntry entry1, IClasspathEntry entry2) { String path1 = entry1.getPath().lastSegment(); String path2 = entry2.getPath().lastSegment(); return path1.compareTo(path2); } }); this.entries = entries.toArray(new IClasspathEntry[entries.size()]); this.manifestLocationsByProject = null; this.initialized = true; // Save class path entries to file ServerClasspathUtils.persistClasspathEntries(this.javaProject, this.entries); saveTimestamp(BundleManifestUtils.locateManifest(this.javaProject, false)); saveTimestamp(BundleManifestUtils.locateManifest(this.javaProject, true)); } } /** * Saves the last modified timestamp to the file resource. */ private void saveTimestamp(IFile manifestFile) { if (manifestFile != null && manifestFile.exists()) { try { manifestFile.setPersistentProperty(new QualifiedName(JdtCorePlugin.PLUGIN_ID, MANIFEST_TIMESTAMP), Long.toString(manifestFile.getLocalTimeStamp())); } catch (CoreException e) { JdtCorePlugin.log(e); } } } /** * Create and add the classpath entries for the given {@link BundleManifest}. * * @param manifest the given {@link BundleManifest} to add classpath entries for * @param testManifest <code>true</code> if the manifest is the TEST.MF */ private void addClasspathEntriesFromBundleClassPath(List<IClasspathEntry> entries, BundleManifest manifest, boolean testManifest) { List<String> bundleClassPathEntries = manifest.getBundleClasspath(); for (String bundleClassPathEntry : bundleClassPathEntries) { IResource resource = this.javaProject.getProject().findMember(bundleClassPathEntry.trim()); if (!".".equals(bundleClassPathEntry.trim()) && resource != null) { IPath bundleClassPathEntryPath = resource.getRawLocation(); if (bundleClassPathEntryPath != null) { File bundleClassPathEntryFile = bundleClassPathEntryPath.toFile(); if (bundleClassPathEntryFile != null && bundleClassPathEntryFile.exists()) { createClasspathEntryForFile(entries, bundleClassPathEntryFile, testManifest, WILDCARD_ACCESSIBLE_RULE); } } } } } /** * Creates and adds {@link IClasspathEntry}s to the given set. * * @param entries set to add the newly create {@link IClasspathEntry}s to * @param dependencies the resolved dependencies keyed by {@link File} */ // TODO CD merge if classpath entry already exists when resolving the test manifest private void addClasspathEntriesFromResolutionResult(List<IClasspathEntry> entries, Map<File, List<String>> dependencies, boolean testManifest) { Set<String> resolvedPackageImports = new LinkedHashSet<String>(); for (Map.Entry<File, List<String>> entry : dependencies.entrySet()) { File file = entry.getKey(); if (file != null) { Set<IAccessRule> allowedRules = createAccessRulesFromPackageImports(entry.getValue()); if (file.isDirectory()) { // Adjust file name to eliminate cross platform problems String fileName = file.toString().replace("\\", "/"); if (this.manifestLocationsByProject.containsKey(fileName)) { createClasspathForProject(entries, fileName, testManifest, allowedRules.toArray(new IAccessRule[allowedRules.size()])); } } else { createClasspathEntryForFile(entries, file, testManifest, allowedRules.toArray(new IAccessRule[allowedRules.size()])); } } if (!testManifest) { // Store all resolved packages resolvedPackageImports.addAll(entry.getValue()); } } if (!testManifest) { // Store the resolved package imports back into the model IBundleManifestManager bundleManifestManager = BundleManifestCorePlugin.getBundleManifestManager(); if (bundleManifestManager instanceof IBundleManifestMangerWorkingCopy) { ((IBundleManifestMangerWorkingCopy) bundleManifestManager).updateResolvedPackageImports(this.javaProject, resolvedPackageImports); } } } /** * Adds the given project as a workspace project to the list * * @param workspaceBundles the already existing workspace bundles * @param project the bundle project to add */ private void addWorkspaceBundle(Set<String> workspaceBundles, IProject project) { if (project.isAccessible() && FacetUtils.isBundleProject(project)) { String manifestFolder = BundleManifestUtils.locateManifestFolder(JavaCore.create(project)); if (manifestFolder != null) { workspaceBundles.add(manifestFolder); this.manifestLocationsByProject.put(manifestFolder, JavaCore.create(project)); } } } /** * Creates {@link IAccessRule}s for the given list of imported packages. * <p> * Only those packages from the {@link IClasspathEntry} that are imported will be visible. * * @param packageImports the resolved and explicit package imports * @return the set of {@link IAccessRule}s */ private Set<IAccessRule> createAccessRulesFromPackageImports(List<String> packageImports) { Set<IAccessRule> allowedRules = new LinkedHashSet<IAccessRule>(); for (String packageImport : packageImports) { allowedRules.add(getAccessibleRule(new Path(packageImport.replace('.', '/') + PACKAGE_WILDCARD))); } allowedRules.add(WILDCARD_NON_ACCESSIBLE_RULE); return allowedRules; } /** * Creates a single {@link IClasspathEntry} for the given <code>file</code> and <code>allowedRules</code>. * * @param entries the list of {@link IClasspathEntry} * @param file the {@link File} representing a JAR file * @param allowedRules the set if {@link IAccessRule}s indicating package export restrictions */ private void createClasspathEntryForFile(List<IClasspathEntry> entries, File file, boolean testManifest, IAccessRule... allowedRules) { IPath path = new Path(file.getAbsolutePath()); allowedRules = mergeAccessRules(entries, path, allowedRules); if (testManifest) { entries.add(JavaCore.newLibraryEntry(new Path(file.getAbsolutePath()), getSourceAttachmentPath(file), null, allowedRules, TEST_CLASSPATH_ATTRIBUTES, false)); } else { entries.add(JavaCore.newLibraryEntry(new Path(file.getAbsolutePath()), getSourceAttachmentPath(file), null, allowedRules, CLASSPATH_ATTRIBUTES, false)); } } /** * Creates a single {@link IClasspathEntry} for the given <code>file</code> and <code>allowedRules</code>. * <p> * The <code>file</code> actually points to the workspace source folder and therefore this method creates a project * reference in contrast to a JAR reference. * * @param entries the list of {@link IClasspathEntry} * @param fileName the file name representing a source folder * @param allowedRules the set if {@link IAccessRule}s indicating package export restrictions */ private void createClasspathForProject(List<IClasspathEntry> entries, String fileName, boolean testManifest, IAccessRule... allowedRules) { IJavaProject referencedProject = this.manifestLocationsByProject.get(fileName); if (referencedProject == null || this.javaProject.equals(referencedProject)) { return; } IPath path = this.manifestLocationsByProject.get(fileName).getPath(); allowedRules = mergeAccessRules(entries, path, allowedRules); if (testManifest) { entries.add(JavaCore.newProjectEntry(path, allowedRules, false, TEST_CLASSPATH_ATTRIBUTES, false)); } else { entries.add(JavaCore.newProjectEntry(path, allowedRules, false, CLASSPATH_ATTRIBUTES, false)); } } private IAccessRule[] mergeAccessRules(List<IClasspathEntry> entries, IPath path, IAccessRule... allowedRules) { IClasspathEntry entry = null; // Check if the path is already in and merge if so for (IClasspathEntry existingEntry : entries) { if (existingEntry.getPath().equals(path)) { Set<IAccessRule> existingRules = new TreeSet<IAccessRule>(new Comparator<IAccessRule>() { public int compare(IAccessRule o1, IAccessRule o2) { if (o1.getKind() == o2.getKind()) { return o1.getPattern().toString().compareTo(o2.getPattern().toString()); } else if (o1.getKind() == IAccessRule.K_NON_ACCESSIBLE) { return 1; } else if (o2.getKind() == IAccessRule.K_NON_ACCESSIBLE) { return -1; } else if (o1.getKind() == IAccessRule.K_ACCESSIBLE) { return 1; } else if (o2.getKind() == IAccessRule.K_ACCESSIBLE) { return -1; } return 0; } }); existingRules.addAll(Arrays.asList(existingEntry.getAccessRules())); existingRules.addAll(Arrays.asList(allowedRules)); allowedRules = existingRules.toArray(new IAccessRule[existingRules.size()]); entry = existingEntry; break; } } if (entry != null) { entries.remove(entry); } return allowedRules; } /** * Creates the {@link DependencyLocator} to be used for resolution. * * @param javaProject the {@link IJavaProject} to resolve dependencies for. * @return a configured and ready to use {@link DependencyLocator} * @throws IOException */ private IDependencyLocator createDependencyLocator(IJavaProject javaProject) throws CoreException, IOException { final Set<String> workspaceBundles = new LinkedHashSet<String>(); // First add projects that belong to the same par project for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { Set<String> parBundles = new HashSet<String>(); if (FacetUtils.isParProject(project)) { boolean hasBundle = false; Par par = FacetUtils.getParDefinition(project); if (par != null && par.getBundle() != null) { for (Bundle bundle : par.getBundle()) { if (bundle.getSymbolicName().equals(javaProject.getElementName())) { hasBundle = true; } parBundles.add(bundle.getSymbolicName()); } } if (hasBundle) { for (String bundleName : parBundles) { IProject bundleProject = ResourcesPlugin.getWorkspace().getRoot().getProject(bundleName); addWorkspaceBundle(workspaceBundles, bundleProject); } // Add any nested or linked jars from the PAR project.accept(new IResourceVisitor() { public boolean visit(IResource resource) throws CoreException { if (resource instanceof IFile && resource.getFileExtension().equals("jar")) { IPath jarLocation = resource.getRawLocation(); IPath resolvedJarLocation = JavaCore.getResolvedVariablePath(jarLocation); if (resolvedJarLocation != null) { workspaceBundles.add(resolvedJarLocation.removeLastSegments(1).toString() + File.separator + "{bundle}"); } else { workspaceBundles.add(jarLocation.removeLastSegments(1).toString() + File.separator + "{bundle}"); } } return true; } }, IResource.DEPTH_ONE, false); } } } // Secondly add all explicit dependent projects for (IProject project : javaProject.getProject().getDescription().getReferencedProjects()) { addWorkspaceBundle(workspaceBundles, project); } // Thirdly add the current plugin to resolve exported packages from the same bundle addWorkspaceBundle(workspaceBundles, javaProject.getProject()); // Store targeted runtimes to display in the description this.serverRuntimes = ServerUtils.getTargettedRuntimes(javaProject.getProject()); // Adjust the last modified date on the META-INF and root folder ClasspathUtils.adjustLastModifiedDate(javaProject, false); ClasspathUtils.adjustLastModifiedDate(javaProject, true); // Create DependencyLocator with path to server.config and server.profile return ServerUtils.createDependencyLocator(javaProject.getProject(), workspaceBundles.toArray(new String[workspaceBundles.size()])); } /** * Returns a path to the source attachment following the BRITS conventions if the sources jar can be find. * <p> * First checks if the user has overridden the convention and attached a custom archive. * * @param file the JAR file to * @return the source JAR path */ private IPath getSourceAttachmentPath(File file) { // first check manual configured source attachments IPath sourceAttachmentPath = ClasspathUtils.getSourceAttachment(this.javaProject, file); if (sourceAttachmentPath != null) { return sourceAttachmentPath; } // secondly check for source attachment following the conventions File sourceFile = ServerUtils.getSourceFile(file.toURI()); if (sourceFile != null && sourceFile.exists() && sourceFile.canRead()) { sourceAttachmentPath = new Path(sourceFile.getAbsolutePath()); } return sourceAttachmentPath; } /** * Creates error markers for unresolved dependencies stored in the {@link DependencyLocationException}. * * @param e a {@link DependencyLocationException} occurred during resolution */ private void handleDependencyLocationException(DependencyLocationException e, boolean testManifest) { MarkerUtils.createErrorMarkers(e, this.javaProject, testManifest); } /** * Resolve the dependencies of the given {@link BundleManifest}. * * @param entries the already collected {@link IClasspathEntry} * @param manifest the {@link BundleManifest} to add dependencies for * @param locator the {@link DependencyLocator} instance to use * @param testManifest <code>true</code> */ private void resolveDependencies(List<IClasspathEntry> entries, BundleManifest manifest, IDependencyLocator locator, boolean testManifest) { DependencyLocationException dependencyLocationException = null; if (locator != null) { try { // Resolve dependencies for the main manifest addClasspathEntriesFromResolutionResult(entries, locator.locateDependencies(manifest), testManifest); // Add classpath entries that are configured in the bundle manifest addClasspathEntriesFromBundleClassPath(entries, manifest, testManifest); } catch (DependencyLocationException e) { // Store for later removal of error markers dependencyLocationException = e; // Install the resolved dependencies as a safe fallback addClasspathEntriesFromResolutionResult(entries, e.getSatisfiedDependencies(), testManifest); // Add classpath entries that are configured in the bundle manifest addClasspathEntriesFromBundleClassPath(entries, manifest, testManifest); } finally { // Create error markers for un-resolved dependencies handleDependencyLocationException(dependencyLocationException, testManifest); } } } }