All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bndtools.builder.classpath.BndContainerInitializer Maven / Gradle / Ivy

package org.bndtools.builder.classpath;

import static java.util.stream.Collectors.toList;
import static org.bndtools.builder.classpath.BndContainer.TEST;
import static org.bndtools.builder.classpath.BndContainer.hasAttribute;

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.Objects;
import java.util.Set;
import java.util.WeakHashMap;
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.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.JavaRuntime;

import aQute.bnd.build.CircularDependencyException;
import aQute.bnd.build.Container;
import aQute.bnd.build.Project;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.lib.io.IO;
import aQute.lib.strings.Strings;
import aQute.service.reporter.Reporter.SetLocation;
import bndtools.central.Central;
import bndtools.preferences.BndPreferences;

/**
 * ClasspathContainerInitializer for the aQute.bnd.classpath.container name.
 * 

* Used in the .classpath file of bnd project to couple the bnd -buildpath into * the Eclipse IDE. */ public class BndContainerInitializer extends ClasspathContainerInitializer implements ModelListener { private static final ILogger logger = Logger .getLogger(BndContainerInitializer.class); private static final ClasspathContainerSerializationHelper serializationHelper = new ClasspathContainerSerializationHelper<>(); public BndContainerInitializer() { super(); Central.onCnfWorkspace(workspace -> Central.getInstance() .addModelListener(BndContainerInitializer.this)); JavaRuntime.addContainerResolver(new BndContainerRuntimeClasspathEntryResolver(), BndtoolsConstants.BND_CLASSPATH_ID.segment(0)); } @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.hasCnfWorkspace() || !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.onCnfWorkspace( workspace -> requestClasspathContainerUpdate(BndtoolsConstants.BND_CLASSPATH_ID, javaProject, 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. * @return true if the classpath container was updated. * @throws CoreException */ public static boolean requestClasspathContainerUpdate(IJavaProject javaProject) throws CoreException { IClasspathContainer oldContainer = getClasspathContainer(javaProject); if (oldContainer == null) { return false; // 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); } return getClasspathContainer(javaProject) != oldContainer; } private static BndContainer loadClasspathContainer(IProject project) { File containerFile = getContainerFile(project); if (!containerFile.isFile()) { return new BndContainer.Builder().build(); } try { return serializationHelper.readClasspathContainer(containerFile); } catch (Exception e) { logger.logError("Unable to load serialized classpath container from file " + containerFile, e); IO.delete(containerFile); return new BndContainer.Builder().build(); } } static void storeClasspathContainer(IProject project, BndContainer container) { File containerFile = getContainerFile(project); try { serializationHelper.writeClasspathContainer(container, containerFile); } catch (Exception e) { logger.logError("Unable to store serialized classpath container in file " + containerFile, e); IO.delete(containerFile); } } private static File getContainerFile(IProject p) { return IO.getFile(BuilderPlugin.getInstance() .getStateLocation() .toFile(), p.getName() + ".container"); } private static class Updater { 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 IClasspathAttribute WITHOUT_TEST_CODE = JavaCore .newClasspathAttribute("without_test_code", Boolean.TRUE.toString()); private static final Pattern packagePattern = Pattern.compile("(?<=^|\\.)\\*(?=\\.|$)|\\."); private static final Map jarInfo = Collections .synchronizedMap(new WeakHashMap<>()); private final IProject project; private final IJavaProject javaProject; private final IWorkspaceRoot root; private final Project model; private final BndContainer.Builder builder; Updater(IProject project, IJavaProject javaProject) { assert project != null; assert javaProject != null; this.project = project; this.javaProject = javaProject; this.root = project.getWorkspace() .getRoot(); this.builder = new BndContainer.Builder(); 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, builder.build()); return; } try { model.getWorkspace() .readLocked(this::calculateProjectClasspath); } catch (Exception e) { SetLocation error = error("Unable to calculate classpath for project %s", e, project.getName()); logger.logError(error.location().message, e); } builder.entries(BndContainerSourceManager.loadAttachedSources(project, builder.entries())); if (!init) { IClasspathContainer container = getClasspathContainer(javaProject); if (container instanceof BndContainer) { BndContainer bndContainer = (BndContainer) container; List currentClasspath = Arrays.asList(bndContainer.getClasspathEntries()); if (builder.entries() .equals(currentClasspath) && (builder.lastModified() <= bndContainer.lastModified())) { bndContainer.refresh(); return; // no change; so no need for new container } } } BndContainer bndContainer = builder.build(); bndContainer.refresh(); setClasspathContainer(javaProject, bndContainer); storeClasspathContainer(project, bndContainer); } static void setClasspathContainer(IJavaProject javaProject, BndContainer container) throws JavaModelException { BndPreferences prefs = new BndPreferences(); if (prefs.getBuildLogging() == BuildLogger.LOG_FULL) { StringBuilder sb = new StringBuilder(); sb.append(container.getDescription()) .append(" for ") .append(javaProject.getProject() .getName()) .append("\n\n=== Compile Classpath ==="); for (IClasspathEntry cpe : container.getClasspathEntries()) { sb.append("\n--- ") .append(cpe); } sb.append("\n\n=== Runtime Classpath ==="); for (IRuntimeClasspathEntry cpe : container.getRuntimeClasspathEntries()) { sb.append("\n--- ") .append(cpe); } logger.logInfo(sb.append("\n") .toString(), null); } JavaCore.setClasspathContainer(BndtoolsConstants.BND_CLASSPATH_ID, new IJavaProject[] { javaProject }, new IClasspathContainer[] { container }, null); } private Void calculateProjectClasspath() throws CoreException { if (!project.isOpen()) { return null; } try { Collection containers = model.getBuildpath(); calculateContainersClasspath(Constants.BUILDPATH, containers); containers = model.getTestpath(); calculateContainersClasspath(Constants.TESTPATH, containers); containers = model.getBootclasspath(); calculateContainersClasspath(Constants.BUILDPATH, containers); } catch (CircularDependencyException e) { error("Circular dependency during classpath calculation: %s", e, e.getMessage()); builder.entries(Collections.emptyList()); } catch (Exception e) { error("Unexpected error during classpath calculation: %s", e, e.getMessage()); builder.entries(Collections.emptyList()); } return null; } private void calculateContainersClasspath(String instruction, Collection containers) throws CoreException { boolean testattr = instruction.equals(Constants.TESTPATH) && Arrays.stream(javaProject.getRawClasspath()) .filter(cpe -> cpe.getEntryKind() == IClasspathEntry.CPE_SOURCE) .anyMatch(cpe -> hasAttribute(cpe, TEST)); for (Container c : containers) { File file = c.getFile(); assert file.isAbsolute(); if (!file.exists()) { switch (c.getType()) { case REPO : error(c, instruction, "Repository file %s does not exist", file); break; case LIBRARY : error(c, instruction, "Library file %s does not exist", file); break; case PROJECT : error(c, instruction, "Project bundle %s does not exist", file); break; case EXTERNAL : error(c, instruction, "External file %s does not exist", file); break; default : break; } } IPath path = null; try { path = fileToPath(file); } catch (Exception e) { error(c, instruction, "Failed to convert file %s to Eclipse path: %s", e, file, e.getMessage()); continue; } IResource resource = Central.toResource(file); if (resource != null) { builder.refresh(resource); } List extraAttrs = calculateContainerAttributes(c); if (testattr) { extraAttrs.add(TEST); } List accessRules = calculateContainerAccessRules(c); switch (c.getType()) { case PROJECT : if (testattr) { extraAttrs.add(WITHOUT_TEST_CODE); } IProject otherProject = root.getFile(path) .getProject(); if (isVersionProject(c)) { // version=project addProjectEntry(otherProject.getFullPath(), accessRules, extraAttrs); } else { // version=latest or version=snapshot /* * When the source value is not equal to "project" * (the default value), we make the source folder * discouraged. This can be necessary when using the * Eclipse Transformer Bnd Analyzer plugin which can * make the class files in the jar incompatible with * the class files in the source output folder. We * tried to always make the source folder * discouraged which worked fine for compiling but * it messed up the Open Type Hierarchy support in * Eclipse. See * https://github.com/bndtools/bnd/issues/5250. So * we maintain the prior behavior unless source is * set to a value other than "project". e.g. * source=none */ String source = c.getAttributes() .getOrDefault("source", "project"); List projectAccessRules = Objects.equals(source, "project") ? accessRules : Collections.emptyList(); /* * Add project entry for source code before library * entry for generated jar. */ addProjectEntry(otherProject.getFullPath(), projectAccessRules, extraAttrs); /* * 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); /* * Compute source attachment path for library entry. */ IPath sourceAttachmentPath = calculateSourceAttachmentPath(path, file); IPath sourceAttachmentRootPath = null; // default if (sourceAttachmentPath == null) { IJavaProject otherJavaProject = JavaCore.create(otherProject); if (otherJavaProject != null) { for (IClasspathEntry raw : otherJavaProject.getRawClasspath()) { if ((raw.getEntryKind() == IClasspathEntry.CPE_SOURCE) && !hasAttribute(raw, TEST)) { sourceAttachmentPath = raw.getSourceAttachmentPath(); if (sourceAttachmentPath != null) { sourceAttachmentRootPath = raw.getSourceAttachmentRootPath(); } else { sourceAttachmentPath = raw.getPath(); } break; } } } } addLibraryEntry(path, file, accessRules, extraAttrs, sourceAttachmentPath, sourceAttachmentRootPath); } break; default : IPath sourceAttachmentPath = calculateSourceAttachmentPath(path, file); IPath sourceAttachmentRootPath = null; // default addLibraryEntry(path, file, accessRules, extraAttrs, sourceAttachmentPath, sourceAttachmentRootPath); break; } } } private void addProjectEntry(IPath path, List accessRules, List extraAttrs) { List classpath = builder.entries(); 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 oldAccessRules = Arrays.asList(entry.getAccessRules()); int last = oldAccessRules.size() - 1; if (last < 0) { return; // project entry already has full access } List combinedAccessRules = null; if (accessRules != null) { // if not full access request combinedAccessRules = new ArrayList<>(oldAccessRules); if (DISCOURAGED.equals(combinedAccessRules.get(last))) { combinedAccessRules.remove(last); } combinedAccessRules.addAll(accessRules); } IClasspathEntry projectEntry = JavaCore.newProjectEntry(path, toAccessRulesArray(combinedAccessRules), false, entry.getExtraAttributes(), false); builder.entry(i, projectEntry); return; } // Add a new project entry for the project IClasspathEntry projectEntry = JavaCore.newProjectEntry(path, toAccessRulesArray(accessRules), false, toClasspathAttributesArray(extraAttrs), false); builder.entry(projectEntry); } 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 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/")) { // use library path as source attachment path info.hasSource = true; break; } } } catch (IOException e) { logger.logInfo("Failed to read " + file, e); } jarInfo.put(file, info); return info; } private void addLibraryEntry(IPath path, File file, List accessRules, List extraAttrs, IPath sourceAttachmentPath, IPath sourceAttachmentRootPath) { IClasspathEntry libraryEntry = JavaCore.newLibraryEntry(path, sourceAttachmentPath, sourceAttachmentRootPath, toAccessRulesArray(accessRules), toClasspathAttributesArray(extraAttrs), false); builder.entry(libraryEntry); builder.updateLastModified(file.lastModified()); } private List calculateContainerAttributes(Container c) { List attrs = new ArrayList<>(); 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 calculateContainerAccessRules(Container c) { String packageList = c.getAttributes() .get("packages"); if (packageList != null) { // Use packages=* for full access return Strings.splitAsStream(packageList) .map(exportPkg -> { int ruleKind = IAccessRule.K_ACCESSIBLE; if (exportPkg.startsWith("!")) { exportPkg = exportPkg.substring(1); ruleKind = IAccessRule.K_NON_ACCESSIBLE | IAccessRule.IGNORE_IF_BETTER; } Matcher m = packagePattern.matcher(exportPkg); StringBuilder pathStr = new StringBuilder(exportPkg.length() + 1); int start = 0; for (; m.find(); start = m.end()) { pathStr.append(exportPkg, start, m.start()) .append(m.group() .equals("*") ? "**" : "/"); } pathStr.append(exportPkg, start, exportPkg.length()) .append("/*"); return JavaCore.newAccessRule(new Path(pathStr.toString()), ruleKind); }) .collect(toList()); } switch (c.getType()) { case PROJECT : // if version=project, try Project for exports if (isVersionProject(c)) { 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 accessRules = new ArrayList<>(); 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 calculateProjectAccessRules(Project p) { File accessPatternsFile = getAccessPatternsFile(p); String oldAccessPatterns = ""; boolean exists = accessPatternsFile.exists(); if (exists) { // read persisted access patterns try { oldAccessPatterns = IO.collect(accessPatternsFile); builder.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 accessRules = new ArrayList<>(patterns.length); // if not empty, there are access patterns if (!oldAccessPatterns.isEmpty()) { for (String pathStr : patterns) { accessRules.add(JavaCore.newAccessRule(new Path(pathStr), IAccessRule.K_ACCESSIBLE)); } } return accessRules; } Set exportPkgs = p.getExports() .keySet(); List accessRules = new ArrayList<>(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 state changed; persist updated access patterns if (!exists || !newAccessPatterns.equals(oldAccessPatterns)) { try { IO.store(newAccessPatterns, accessPatternsFile); builder.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 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 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 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 static class JarInfo { boolean hasSource; String[] exports; long lastModified; JarInfo() {} } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy