package org.bndtools.builder.classpath; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bndtools.api.BndtoolsConstants; import org.bndtools.api.ILogger; import org.bndtools.api.Logger; import org.bndtools.api.ModelListener; import org.bndtools.builder.BndtoolsBuilder; import org.bndtools.builder.BuildLogger; import org.bndtools.builder.BuilderPlugin; import org.bndtools.utils.jar.PseudoJar; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.ClasspathContainerInitializer; 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.jdt.core.JavaModelException; import org.eclipse.jdt.internal.core.JavaModelManager; import org.osgi.util.promise.Promise; import org.osgi.util.promise.Success; import aQute.bnd.build.CircularDependencyException; import aQute.bnd.build.Container; import aQute.bnd.build.Project; import aQute.bnd.build.Workspace; import aQute.bnd.header.Parameters; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Descriptors.PackageRef; import aQute.lib.io.IO; import aQute.service.reporter.Reporter.SetLocation; import bndtools.central.Central; import bndtools.central.RefreshFileJob; import bndtools.preferences.BndPreferences; /** * ClasspathContainerInitializer for the aQute.bnd.classpath.container name. * <p> * Used in the .classpath file of bnd project to couple the bnd -buildpath into the Eclipse IDE. */ public class BndContainerInitializer extends ClasspathContainerInitializer implements ModelListener { static final ILogger logger = Logger.getLogger(BndContainerInitializer.class); private static final ClasspathContainerSerializationHelper<BndContainer> serializationHelper = new ClasspathContainerSerializationHelper<>(); public BndContainerInitializer() { super(); Central.onWorkspaceInit(new Success<Workspace,Void>() { @Override public Promise<Void> call(Promise<Workspace> resolved) throws Exception { Central.getInstance().addModelListener(BndContainerInitializer.this); return null; } }); } @Override public void initialize(IPath containerPath, final IJavaProject javaProject) throws CoreException { IProject project = javaProject.getProject(); /* If workspace is already initialized or there is no * saved container information, then update the container * now. */ File containerFile = getContainerFile(project); if (Central.isWorkspaceInited() || !containerFile.isFile()) { Updater updater = new Updater(project, javaProject); updater.updateClasspathContainer(true); return; } /* * Read the saved container information and update the container now. * Request an update using the project information after the * workspace is initialized. */ BndContainer container = loadClasspathContainer(project); Updater.setClasspathContainer(javaProject, container); Central.onWorkspaceInit(new Success<Workspace,Void>() { @Override public Promise<Void> call(Promise<Workspace> resolved) throws Exception { requestClasspathContainerUpdate(BndtoolsConstants.BND_CLASSPATH_ID, javaProject, null); return null; } }); } @Override public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject javaProject) { return true; } @Override public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject javaProject, IClasspathContainer containerSuggestion) throws CoreException { IProject project = javaProject.getProject(); if (containerSuggestion != null) { BndContainerSourceManager.saveAttachedSources(project, containerSuggestion.getClasspathEntries()); } Updater updater = new Updater(project, javaProject); updater.updateClasspathContainer(false); } @Override public String getDescription(IPath containerPath, IJavaProject project) { return BndContainer.DESCRIPTION; } /** * ModelListener modelChanged method. */ @Override public void modelChanged(Project model) throws Exception { IJavaProject javaProject = Central.getJavaProject(model); if (javaProject == null) { return; // bnd project is not loaded in the workspace } requestClasspathContainerUpdate(javaProject); } /** * Return the BndContainer for the project, if there is one. This will not create one if there is not already one. * * @param javaProject * The java project of interest. Must not be null. * @return The BndContainer for the java project. */ public static IClasspathContainer getClasspathContainer(IJavaProject javaProject) { return JavaModelManager.getJavaModelManager().containerGet(javaProject, BndtoolsConstants.BND_CLASSPATH_ID); } /** * Request the BndContainer for the project, if there is one, be updated. This will not create one if there is not * already one. * * @param javaProject * The java project of interest. Must not be null. * @throws CoreException */ public static void requestClasspathContainerUpdate(IJavaProject javaProject) throws CoreException { if (getClasspathContainer(javaProject) == null) { return; // project does not have a BndContainer } ClasspathContainerInitializer initializer = JavaCore.getClasspathContainerInitializer(BndtoolsConstants.BND_CLASSPATH_ID.segment(0)); if (initializer != null) { initializer.requestClasspathContainerUpdate(BndtoolsConstants.BND_CLASSPATH_ID, javaProject, null); } } /** * Suggests whether an update request on the classpath container, if there is one, should be made. * * @param javaProject * The java project of interest. Must not be null. * @throws CoreException */ public static boolean suggestClasspathContainerUpdate(IJavaProject javaProject) throws Exception { if (getClasspathContainer(javaProject) == null) { return false; // project does not have a BndContainer } ClasspathContainerInitializer initializer = JavaCore.getClasspathContainerInitializer(BndtoolsConstants.BND_CLASSPATH_ID.segment(0)); if (initializer == null) { return false; } IProject project = javaProject.getProject(); Updater updater = new Updater(project, javaProject); return updater.suggestClasspathContainerUpdate(); } private static BndContainer loadClasspathContainer(IProject project) { File containerFile = getContainerFile(project); if (!containerFile.isFile()) { return new BndContainer(Updater.EMPTY_ENTRIES, 0L); } try { return serializationHelper.readClasspathContainer(containerFile); } catch (IOException | ClassNotFoundException e) { logger.logError("Unable to load stored classpath container", e); return new BndContainer(Updater.EMPTY_ENTRIES, 0L); } } static void storeClasspathContainer(IProject project, BndContainer container) { File containerFile = getContainerFile(project); try { serializationHelper.writeClasspathContainer(container, containerFile); } catch (IOException e) { logger.logError("Unable to store classpath container", e); IO.delete(containerFile); } } private static File getContainerFile(IProject p) { return IO.getFile(BuilderPlugin.getInstance().getStateLocation().toFile(), p.getName() + ".container"); } private static class Updater { static final IClasspathEntry[] EMPTY_ENTRIES = new IClasspathEntry[0]; private static final IAccessRule DISCOURAGED = JavaCore.newAccessRule(new Path("**"), IAccessRule.K_DISCOURAGED | IAccessRule.IGNORE_IF_BETTER); private static final IClasspathAttribute EMPTY_INDEX = JavaCore.newClasspathAttribute(IClasspathAttribute.INDEX_LOCATION_ATTRIBUTE_NAME, "platform:/plugin/" + BndtoolsBuilder.PLUGIN_ID + "/org/bndtools/builder/classpath/empty.index"); private static final Pattern packagePattern = Pattern.compile("(?<=^|\\.)\\*(?=\\.|$)|\\."); private static final Map<File,JarInfo> jarInfo = Collections.synchronizedMap(new WeakHashMap<File,JarInfo>()); private final IProject project; private final IJavaProject javaProject; private final IWorkspaceRoot root; private final Project model; private long lastModified; Updater(IProject project, IJavaProject javaProject) { assert project != null; assert javaProject != null; this.project = project; this.javaProject = javaProject; this.root = project.getWorkspace().getRoot(); Project p = null; try { p = Central.getProject(project); } catch (Exception e) { // this can happen during first project creation in an empty workspace logger.logInfo("Unable to get bnd project for project " + project.getName(), e); } this.model = p; } void updateClasspathContainer(boolean init) throws CoreException { if (model == null) { // this can happen during new project creation setClasspathContainer(javaProject, new BndContainer(EMPTY_ENTRIES, 0L)); return; } List<IClasspathEntry> newClasspath = Collections.emptyList(); try { newClasspath = Central.bndCall(new Callable<List<IClasspathEntry>>() { @Override public List<IClasspathEntry> call() throws Exception { return calculateProjectClasspath(); } }); } catch (Exception e) { SetLocation error = error("Unable to calculate classpath for project %s", e, project.getName()); logger.logError(error.location().message, e); } newClasspath = BndContainerSourceManager.loadAttachedSources(project, newClasspath); if (!init) { IClasspathContainer container = getClasspathContainer(javaProject); if (container instanceof BndContainer) { BndContainer bndContainer = (BndContainer) container; List<IClasspathEntry> currentClasspath = Arrays.asList(bndContainer.getClasspathEntries()); if (newClasspath.equals(currentClasspath)) { if (bndContainer.updateLastModified(lastModified)) { storeClasspathContainer(project, bndContainer); } return; // no change; so no need to set entries } } } BndContainer bndContainer = new BndContainer(newClasspath.toArray(new IClasspathEntry[0]), lastModified); setClasspathContainer(javaProject, bndContainer); storeClasspathContainer(project, bndContainer); } boolean suggestClasspathContainerUpdate() throws Exception { if (model == null) { return false; } BndContainer container = (BndContainer) JavaCore.getClasspathContainer(BndtoolsConstants.BND_CLASSPATH_ID, javaProject); long containerLastModified = container.lastModified(); for (IClasspathEntry cpe : container.getClasspathEntries()) { IPath path = cpe.getPath(); IResource resource = root.findMember(path); switch (cpe.getEntryKind()) { case IClasspathEntry.CPE_LIBRARY : File library = resource != null ? resource.getLocation().toFile() : path.toFile(); if (library.lastModified() > containerLastModified) { return true; } break; case IClasspathEntry.CPE_PROJECT : if (!isVersionProject(cpe)) { break; // only proceed if project's library not in container } Project p = Central.getProject((IProject) resource); if (p == null) { break; } File accessPatternsFile = getAccessPatternsFile(p); if (accessPatternsFile.lastModified() > containerLastModified) { return true; } break; default : break; } } return false; } static void setClasspathContainer(IJavaProject javaProject, BndContainer container) throws JavaModelException { JavaCore.setClasspathContainer(BndtoolsConstants.BND_CLASSPATH_ID, new IJavaProject[] { javaProject }, new IClasspathContainer[] { container }, null); BndPreferences prefs = new BndPreferences(); if (prefs.getBuildLogging() == BuildLogger.LOG_FULL) { StringBuilder sb = new StringBuilder(); sb.append("ClasspathEntries ").append(javaProject.getProject().getName()); for (IClasspathEntry cpe : container.getClasspathEntries()) { sb.append("\n--- ").append(cpe); } logger.logInfo(sb.append("\n").toString(), null); } } private List<IClasspathEntry> calculateProjectClasspath() { if (!project.isOpen()) return Collections.emptyList(); List<IClasspathEntry> classpath = new ArrayList<IClasspathEntry>(20); List<File> filesToRefresh = new ArrayList<File>(20); try { Collection<Container> containers = model.getBuildpath(); calculateContainersClasspath(Constants.BUILDPATH, containers, classpath, filesToRefresh); containers = model.getTestpath(); calculateContainersClasspath(Constants.TESTPATH, containers, classpath, filesToRefresh); containers = model.getBootclasspath(); calculateContainersClasspath(Constants.BUILDPATH, containers, classpath, filesToRefresh); } catch (CircularDependencyException e) { error("Circular dependency during classpath calculation: %s", e, e.getMessage()); return Collections.emptyList(); } catch (Exception e) { error("Unexpected error during classpath calculation: %s", e, e.getMessage()); return Collections.emptyList(); } // Refresh once, instead of for each dependent project. RefreshFileJob refreshJob = new RefreshFileJob(filesToRefresh, false, project); if (refreshJob.needsToSchedule()) refreshJob.schedule(100); return classpath; } private void calculateContainersClasspath(String header, Collection<Container> containers, List<IClasspathEntry> classpath, List<File> filesToRefresh) { for (Container c : containers) { File file = c.getFile(); assert file.isAbsolute(); if (!file.exists()) { switch (c.getType()) { case REPO : error(c, header, "Repository file %s does not exist", file); break; case LIBRARY : error(c, header, "Library file %s does not exist", file); break; case PROJECT : error(c, header, "Project bundle %s does not exist", file); break; case EXTERNAL : error(c, header, "External file %s does not exist", file); break; default : break; } } IPath path = null; try { path = fileToPath(file); filesToRefresh.add(file); } catch (Exception e) { error(c, header, "Failed to convert file %s to Eclipse path: %s", e, file, e.getMessage()); } if (path == null) { continue; } List<IClasspathAttribute> extraAttrs = calculateContainerAttributes(c); List<IAccessRule> accessRules = calculateContainerAccessRules(c); switch (c.getType()) { case PROJECT : IPath projectPath = root.getFile(path).getProject().getFullPath(); addProjectEntry(classpath, projectPath, accessRules, extraAttrs); if (!isVersionProject(c)) { // if not version=project, add entry for generated jar /* Supply an empty index for the generated JAR of a workspace project dependency. * This prevents the non-editable source files in the generated jar from appearing * in the Open Type dialog. */ extraAttrs.add(EMPTY_INDEX); addLibraryEntry(classpath, path, file, accessRules, extraAttrs); } break; default : addLibraryEntry(classpath, path, file, accessRules, extraAttrs); break; } } } private void addProjectEntry(List<IClasspathEntry> classpath, IPath path, List<IAccessRule> accessRules, List<IClasspathAttribute> extraAttrs) { for (int i = 0; i < classpath.size(); i++) { IClasspathEntry entry = classpath.get(i); if (entry.getEntryKind() != IClasspathEntry.CPE_PROJECT) { continue; } if (!entry.getPath().equals(path)) { continue; } // Found a project entry for the project List<IAccessRule> oldAccessRules = Arrays.asList(entry.getAccessRules()); int last = oldAccessRules.size() - 1; if (last < 0) { return; // project entry already has full access } List<IAccessRule> combinedAccessRules = null; if (accessRules != null) { // if not full access request combinedAccessRules = new ArrayList<IAccessRule>(oldAccessRules); if (DISCOURAGED.equals(combinedAccessRules.get(last))) { combinedAccessRules.remove(last); } combinedAccessRules.addAll(accessRules); } classpath.set(i, JavaCore.newProjectEntry(path, toAccessRulesArray(combinedAccessRules), false, entry.getExtraAttributes(), false)); return; } // Add a new project entry for the project classpath.add(JavaCore.newProjectEntry(path, toAccessRulesArray(accessRules), false, toClasspathAttributesArray(extraAttrs), false)); } private IPath calculateSourceAttachmentPath(IPath path, File file) { JarInfo info = getJarInfo(file); return info.hasSource ? path : null; } private JarInfo getJarInfo(File file) { final long lastModified = file.lastModified(); JarInfo info = jarInfo.get(file); if ((info != null) && (lastModified == info.lastModified)) { return info; } info = new JarInfo(); if (!file.exists()) { return info; } info.lastModified = lastModified; try (PseudoJar jar = new PseudoJar(file)) { Manifest mf = jar.readManifest(); if ((mf != null) && (mf.getMainAttributes().getValue(Constants.BUNDLE_MANIFESTVERSION) != null)) { Parameters exportPkgs = new Parameters(mf.getMainAttributes().getValue(Constants.EXPORT_PACKAGE)); Set<String> exports = exportPkgs.keySet(); info.exports = exports.toArray(new String[0]); } for (String entry = jar.nextEntry(); entry != null; entry = jar.nextEntry()) { if (entry.startsWith("OSGI-OPT/src/")) { info.hasSource = true; // use library path as source attachment path break; } } } catch (IOException e) { logger.logInfo("Failed to read " + file, e); } jarInfo.put(file, info); return info; } private void addLibraryEntry(List<IClasspathEntry> classpath, IPath path, File file, List<IAccessRule> accessRules, List<IClasspathAttribute> extraAttrs) { IPath sourceAttachmentPath = calculateSourceAttachmentPath(path, file); classpath.add(JavaCore.newLibraryEntry(path, sourceAttachmentPath, null, toAccessRulesArray(accessRules), toClasspathAttributesArray(extraAttrs), false)); updateLastModified(file.lastModified()); } private List<IClasspathAttribute> calculateContainerAttributes(Container c) { List<IClasspathAttribute> attrs = new ArrayList<IClasspathAttribute>(); attrs.add(JavaCore.newClasspathAttribute("bsn", c.getBundleSymbolicName())); attrs.add(JavaCore.newClasspathAttribute("type", c.getType().name())); attrs.add(JavaCore.newClasspathAttribute("project", c.getProject().getName())); String version = c.getAttributes().get(Constants.VERSION_ATTRIBUTE); if (version != null) { attrs.add(JavaCore.newClasspathAttribute(Constants.VERSION_ATTRIBUTE, version)); } String packages = c.getAttributes().get("packages"); if (packages != null) { attrs.add(JavaCore.newClasspathAttribute("packages", packages)); } return attrs; } private List<IAccessRule> calculateContainerAccessRules(Container c) { String packageList = c.getAttributes().get("packages"); if (packageList != null) { // Use packages=* for full access List<IAccessRule> accessRules = new ArrayList<IAccessRule>(); for (String exportPkg : packageList.trim().split("\\s*,\\s*")) { Matcher m = packagePattern.matcher(exportPkg); StringBuffer pathStr = new StringBuffer(exportPkg.length() + 1); while (m.find()) { m.appendReplacement(pathStr, m.group().equals("*") ? "**" : "/"); } m.appendTail(pathStr).append("/*"); accessRules.add(JavaCore.newAccessRule(new Path(pathStr.toString()), IAccessRule.K_ACCESSIBLE)); } return accessRules; } switch (c.getType()) { case PROJECT : if (isVersionProject(c)) { // if version=project, try Project for exports return calculateProjectAccessRules(c.getProject()); } //$FALL-THROUGH$ case REPO : case EXTERNAL : JarInfo info = getJarInfo(c.getFile()); if (info.exports == null) { break; // no export; so full access } List<IAccessRule> accessRules = new ArrayList<IAccessRule>(); for (String exportPkg : info.exports) { String pathStr = exportPkg.replace('.', '/') + "/*"; accessRules.add(JavaCore.newAccessRule(new Path(pathStr), IAccessRule.K_ACCESSIBLE)); } return accessRules; default : break; } return null; // full access } private List<IAccessRule> calculateProjectAccessRules(Project p) { File accessPatternsFile = getAccessPatternsFile(p); String oldAccessPatterns = ""; boolean exists = accessPatternsFile.exists(); if (exists) { // read persisted access patterns try { oldAccessPatterns = IO.collect(accessPatternsFile); updateLastModified(accessPatternsFile.lastModified()); } catch (final IOException e) { logger.logError("Failed to read access patterns file for project " + p.getName(), e); } } if (p.getContained().isEmpty()) { // project not recently built; use persisted access patterns if (!exists) { return null; // no persisted access patterns; full access } String[] patterns = oldAccessPatterns.split(","); List<IAccessRule> accessRules = new ArrayList<IAccessRule>(patterns.length); if (!oldAccessPatterns.isEmpty()) { // if not empty, there are access patterns for (String pathStr : patterns) { accessRules.add(JavaCore.newAccessRule(new Path(pathStr), IAccessRule.K_ACCESSIBLE)); } } return accessRules; } Set<PackageRef> exportPkgs = p.getExports().keySet(); List<IAccessRule> accessRules = new ArrayList<IAccessRule>(exportPkgs.size()); StringBuilder sb = new StringBuilder(oldAccessPatterns.length()); for (PackageRef exportPkg : exportPkgs) { String pathStr = exportPkg.getBinary() + "/*"; if (sb.length() > 0) { sb.append(','); } sb.append(pathStr); accessRules.add(JavaCore.newAccessRule(new Path(pathStr), IAccessRule.K_ACCESSIBLE)); } String newAccessPatterns = sb.toString(); if (!exists || !newAccessPatterns.equals(oldAccessPatterns)) { // if state changed; persist updated access patterns try { IO.store(newAccessPatterns, accessPatternsFile); updateLastModified(accessPatternsFile.lastModified()); } catch (final IOException e) { logger.logError("Failed to write access patterns file for project " + p.getName(), e); } } return accessRules; } private File getAccessPatternsFile(Project p) { return IO.getFile(BuilderPlugin.getInstance().getStateLocation().toFile(), p.getName() + ".accesspatterns"); } private IAccessRule[] toAccessRulesArray(List<IAccessRule> rules) { if (rules == null) { return null; } final int size = rules.size(); IAccessRule[] accessRules = rules.toArray(new IAccessRule[size + 1]); accessRules[size] = DISCOURAGED; return accessRules; } private IClasspathAttribute[] toClasspathAttributesArray(List<IClasspathAttribute> attrs) { if (attrs == null) { return null; } IClasspathAttribute[] attrsArray = attrs.toArray(new IClasspathAttribute[0]); return attrsArray; } private IPath fileToPath(File file) throws Exception { IPath path = Central.toPath(file); if (path == null) path = Path.fromOSString(file.getAbsolutePath()); return path; } private boolean isVersionProject(Container c) { return Constants.VERSION_ATTR_PROJECT.equals(c.getAttributes().get(Constants.VERSION_ATTRIBUTE)); } private boolean isVersionProject(IClasspathEntry cpe) { for (IClasspathAttribute extraAttr : cpe.getExtraAttributes()) { if (Constants.VERSION_ATTRIBUTE.equals(extraAttr.getName())) { return Constants.VERSION_ATTR_PROJECT.equals(extraAttr.getValue()); } } return false; } private SetLocation error(String message, Throwable t, Object... args) { return model.error(message, t, args).context(model.getName()).header(Constants.BUILDPATH).file(model.getPropertiesFile().getAbsolutePath()); } private SetLocation error(Container c, String header, String message, Object... args) { return model.error(message, args).context(c.getBundleSymbolicName()).header(header).file(model.getPropertiesFile().getAbsolutePath()); } private SetLocation error(Container c, String header, String message, Throwable t, Object... args) { return model.error(message, t, args).context(c.getBundleSymbolicName()).header(header).file(model.getPropertiesFile().getAbsolutePath()); } private void updateLastModified(long time) { if (time > lastModified) { lastModified = time; } } } private static class JarInfo { boolean hasSource; String[] exports; long lastModified; JarInfo() {} } }