blob: b1d6bb8d6f09f011d5a1c5821d01b388e2a59ad9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2016 Ecliptical Software Inc. and others.
* 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:
* Ecliptical Software Inc. - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 490062
*******************************************************************************/
package org.eclipse.pde.ds.internal.annotations;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.BuildContext;
import org.eclipse.jdt.core.compiler.CompilationParticipant;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.build.IBuildModel;
import org.eclipse.pde.core.build.IBuildModelFactory;
import org.eclipse.pde.internal.core.WorkspaceModelManager;
import org.eclipse.pde.internal.core.ibundle.IBundleModel;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
import org.eclipse.pde.internal.core.natures.PDE;
import org.eclipse.pde.internal.core.project.PDEProject;
import org.eclipse.pde.internal.ui.util.ModelModification;
import org.eclipse.pde.internal.ui.util.PDEModelUtility;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
@SuppressWarnings("restriction")
public class DSAnnotationCompilationParticipant extends CompilationParticipant {
private static final String DS_MANIFEST_KEY = "Service-Component"; //$NON-NLS-1$
private static final String AP_MANIFEST_KEY = "Bundle-ActivationPolicy"; //$NON-NLS-1$
static final String COMPONENT_ANNOTATION = "org.osgi.service.component.annotations.Component"; //$NON-NLS-1$
static final String ANNOTATIONS_PACKAGE = COMPONENT_ANNOTATION.substring(0, COMPONENT_ANNOTATION.lastIndexOf('.'));
private static final QualifiedName PROP_STATE = new QualifiedName(Activator.PLUGIN_ID, "state"); //$NON-NLS-1$
private static final String STATE_FILENAME = "state.dat"; //$NON-NLS-1$
static final String BUILDPATH_PROBLEM_MARKER = "org.eclipse.pde.ds.annotations.buildpath_problem"; //$NON-NLS-1$
private static final Debug debug = Debug.getDebug("ds-annotation-builder"); //$NON-NLS-1$
private final Map<IJavaProject, ProjectContext> processingContext = Collections.synchronizedMap(new HashMap<IJavaProject, ProjectContext>());
@Override
public boolean isAnnotationProcessor() {
return true;
}
@Override
public boolean isActive(IJavaProject project) {
IPreferencesService prefs = Platform.getPreferencesService();
boolean enabled = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_ENABLED, false, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE });
if (!enabled)
return false;
IProject iproject = project.getProject();
if (!iproject.isOpen() || !PDE.hasPluginNature(iproject))
return false;
if (WorkspaceModelManager.isBinaryProject(project.getProject()))
return false;
boolean autoClasspath = prefs.getBoolean(Activator.PLUGIN_ID, Activator.PREF_CLASSPATH, true, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE });
if (autoClasspath)
return true;
try {
IType annotationType = project.findType(COMPONENT_ANNOTATION);
return annotationType != null && annotationType.isAnnotation();
} catch (JavaModelException e) {
Activator.log(e);
}
return false;
}
@Override
public int aboutToBuild(IJavaProject project) {
if (debug.isDebugging())
debug.trace(String.format("About to build project: %s", project.getElementName())); //$NON-NLS-1$
int result = READY_FOR_BUILD;
int[] retval = new int[1];
ProjectState state = getState(project, retval);
result = retval[0];
processingContext.put(project, new ProjectContext(state));
if (state.getFormatVersion() != ProjectState.FORMAT_VERSION) {
state.setFormatVersion(ProjectState.FORMAT_VERSION);
result = NEEDS_FULL_BUILD;
}
IPreferencesService prefs = Platform.getPreferencesService();
String path = prefs.getString(Activator.PLUGIN_ID, Activator.PREF_PATH, Activator.DEFAULT_PATH, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE });
if (!path.equals(state.getPath())) {
state.setPath(path);
result = NEEDS_FULL_BUILD;
}
String errorLevelStr = prefs.getString(Activator.PLUGIN_ID, Activator.PREF_VALIDATION_ERROR_LEVEL, ValidationErrorLevel.error.toString(), new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE });
ValidationErrorLevel errorLevel = getEnumValue(errorLevelStr, ValidationErrorLevel.class, ValidationErrorLevel.error);
if (errorLevel != state.getErrorLevel()) {
state.setErrorLevel(errorLevel);
result = NEEDS_FULL_BUILD;
}
String missingUnbindMethodLevelStr = prefs.getString(Activator.PLUGIN_ID, Activator.PREF_MISSING_UNBIND_METHOD_ERROR_LEVEL, errorLevelStr, new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE, DefaultScope.INSTANCE });
ValidationErrorLevel missingUnbindMethodLevel = getEnumValue(missingUnbindMethodLevelStr, ValidationErrorLevel.class, errorLevel);
if (missingUnbindMethodLevel != state.getMissingUnbindMethodLevel()) {
state.setMissingUnbindMethodLevel(missingUnbindMethodLevel);
result = NEEDS_FULL_BUILD;
}
Activator.getDefault().listenForClasspathPreferenceChanges(project);
return result;
}
private <E extends Enum<E>> E getEnumValue(String property, Class<E> enumType, E defaultValue) {
try {
return Enum.valueOf(enumType, property);
} catch (IllegalArgumentException e) {
return defaultValue;
}
}
public static ProjectState getState(IJavaProject project) {
return getState(project, null);
}
private static ProjectState getState(IJavaProject project, int[] result) {
ProjectState state = null;
try {
Object value = project.getProject().getSessionProperty(PROP_STATE);
if (value instanceof SoftReference<?>) {
@SuppressWarnings("unchecked")
SoftReference<ProjectState> ref = (SoftReference<ProjectState>) value;
state = ref.get();
}
} catch (CoreException e) {
Activator.log(e);
}
if (state == null) {
try {
state = loadState(project.getProject());
} catch (IOException e) {
Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error loading project state.", e)); //$NON-NLS-1$
}
if (state == null) {
state = new ProjectState();
if (result != null && result.length > 0)
result[0] = NEEDS_FULL_BUILD;
}
try {
project.getProject().setSessionProperty(PROP_STATE, new SoftReference<>(state));
} catch (CoreException e) {
Activator.log(e);
}
}
return state;
}
private static ProjectState loadState(IProject project) throws IOException {
File stateFile = getStateFile(project);
if (!stateFile.canRead()) {
if (debug.isDebugging())
debug.trace(String.format("Missing or invalid project state file: %s", stateFile)); //$NON-NLS-1$
return null;
}
ObjectInputStream in = new ObjectInputStream(new FileInputStream(stateFile));
try {
ProjectState state = (ProjectState) in.readObject();
if (debug.isDebugging()) {
debug.trace(String.format("Loaded state for project: %s", project.getName())); //$NON-NLS-1$
for (String cuKey : state.getCompilationUnits())
debug.trace(String.format("%s -> %s", cuKey, state.getModelFiles(cuKey))); //$NON-NLS-1$
}
return state;
} catch (ClassNotFoundException e) {
IOException ex = new IOException("Unable to deserialize project state."); //$NON-NLS-1$
ex.initCause(e);
throw ex;
} finally {
in.close();
}
}
@Override
public void buildFinished(IJavaProject project) {
ProjectContext projectContext = processingContext.remove(project);
if (projectContext != null) {
ProjectState state = projectContext.getState();
// check if unprocessed CUs still exist; if not, their mapped files are now abandoned
HashSet<String> abandoned = new HashSet<>(projectContext.getAbandoned());
for (String cuKey : projectContext.getUnprocessed()) {
boolean exists = false;
try {
IJavaElement cu = project.findElement(new Path(cuKey));
IResource file;
if (cu != null && cu.getElementType() == IJavaElement.COMPILATION_UNIT && (file = cu.getResource()) != null && file.exists())
exists = true;
} catch (JavaModelException e) {
Activator.log(e);
}
if (!exists) {
if (debug.isDebugging())
debug.trace(String.format("Mapped CU %s no longer exists.", cuKey)); //$NON-NLS-1$
Collection<String> dsKeys = state.removeMappings(cuKey);
if (dsKeys != null)
abandoned.addAll(dsKeys);
}
}
// retain abandoned files that are still mapped elsewhere
HashSet<String> retained = new HashSet<>();
for (String cuKey : state.getCompilationUnits()) {
Collection<String> dsKeys = state.getModelFiles(cuKey);
if (dsKeys != null)
retained.addAll(dsKeys);
}
try {
IMarker[] cpMarkers = project.getProject().findMarkers(BUILDPATH_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
if (retained.isEmpty()) {
for (IMarker marker : cpMarkers) {
marker.delete();
}
} else {
abandoned.removeAll(retained);
// check if we need a permanent annotations classpath entry
boolean markerNeeded = false;
IPackageFragmentRoot[] roots = project.getPackageFragmentRoots();
for (int i = roots.length - 1; i >= 0; --i) {
IPackageFragmentRoot root = roots[i];
IPackageFragment fragment = root.getPackageFragment(ANNOTATIONS_PACKAGE);
if (fragment.exists()) {
IClasspathEntry entry = root.getResolvedClasspathEntry();
IClasspathAttribute[] attrs = entry.getExtraAttributes();
for (IClasspathAttribute attr : attrs) {
if (Activator.CP_ATTRIBUTE.equals(attr.getName()) && Boolean.parseBoolean(attr.getValue())) {
markerNeeded = true;
break;
}
}
break;
}
}
if (markerNeeded) {
if (cpMarkers.length == 0) {
IMarker marker = project.getProject().createMarker(BUILDPATH_PROBLEM_MARKER);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
marker.setAttribute(IMarker.MESSAGE, Messages.DSAnnotationCompilationParticipant_buildpathProblemMarker_message);
marker.setAttribute(IMarker.LOCATION, Messages.DSAnnotationCompilationParticipant_buildpathProblemMarker_location);
}
} else {
for (IMarker marker : cpMarkers) {
marker.delete();
}
}
}
} catch (CoreException e) {
Activator.log(e);
}
if (projectContext.isChanged()) {
try {
saveState(project.getProject(), state);
} catch (IOException e) {
Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error saving file mappings.", e)); //$NON-NLS-1$
}
}
// delete all abandoned files
ArrayList<IStatus> deleteStatuses = new ArrayList<>(2);
for (String dsKey : abandoned) {
IPath path = Path.fromPortableString(dsKey);
if (debug.isDebugging())
debug.trace(String.format("Deleting %s", path)); //$NON-NLS-1$
IFile file = PDEProject.getBundleRelativeFile(project.getProject(), path);
if (file.exists()) {
try {
file.delete(true, null);
} catch (CoreException e) {
deleteStatuses.add(e.getStatus());
}
}
}
if (!deleteStatuses.isEmpty())
Activator.log(new MultiStatus(Activator.PLUGIN_ID, 0, deleteStatuses.toArray(new IStatus[deleteStatuses.size()]), "Error deleting generated files.", null)); //$NON-NLS-1$
if (!retained.isEmpty() || !abandoned.isEmpty())
updateProject(project.getProject(), retained, abandoned);
}
if (debug.isDebugging())
debug.trace(String.format("Build finished for project: %s", project.getElementName())); //$NON-NLS-1$
}
private void saveState(IProject project, ProjectState state) throws IOException {
File stateFile = getStateFile(project);
if (debug.isDebugging()) {
debug.trace(String.format("Saving state for project: %s", project.getName())); //$NON-NLS-1$
for (String cuKey : state.getCompilationUnits())
debug.trace(String.format("%s -> %s", cuKey, state.getModelFiles(cuKey))); //$NON-NLS-1$
}
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(stateFile));
try {
out.writeObject(state);
} finally {
out.close();
}
}
private void updateProject(IProject project, final Collection<String> retained, final Collection<String> abandoned) {
PDEModelUtility.modifyModel(new ModelModification(project) {
@Override
protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
if (model instanceof IBundlePluginModelBase)
updateManifest((IBundlePluginModelBase) model, retained, abandoned, project);
}
}, null);
// note: we can't combine both manifest and build.properties into a single edit
PDEModelUtility.modifyModel(new ModelModification(PDEProject.getBuildProperties(project)) {
@Override
protected void modifyModel(IBaseModel model, IProgressMonitor monitor) throws CoreException {
if (model instanceof IBuildModel)
updateBuildProperties((IBuildModel) model, retained, abandoned);
}
}, null);
}
private void updateManifest(IBundlePluginModelBase model, Collection<String> retained, Collection<String> abandoned,
IProject project) {
IBundleModel bundleModel = model.getBundleModel();
LinkedHashSet<IPath> entries = new LinkedHashSet<>();
collectManifestEntries(bundleModel, entries);
boolean changed = false;
for (String dsKey : abandoned) {
IPath path = Path.fromPortableString(dsKey);
changed |= entries.remove(path);
}
for (String dsKey : retained) {
IPath path = Path.fromPortableString(dsKey);
if (!isManifestEntryIncluded(entries, path))
changed |= entries.add(path);
}
if (!changed)
return;
StringBuilder buf = new StringBuilder();
for (IPath entry : entries) {
if (buf.length() > 0)
buf.append(",\n "); //$NON-NLS-1$
buf.append(entry.toString());
}
String value = buf.toString();
if (debug.isDebugging())
debug.trace(String.format("Setting manifest header in %s to %s: %s", model.getUnderlyingResource().getFullPath(), DS_MANIFEST_KEY, value)); //$NON-NLS-1$
// note: contrary to javadoc, setting header value to null does *not* remove it; setting it to empty string does
bundleModel.getBundle().setHeader(DS_MANIFEST_KEY, value);
boolean generateBAPL = Platform.getPreferencesService().getBoolean(Activator.PLUGIN_ID,
Activator.PREF_GENERATE_BAPL, true,
new IScopeContext[] { new ProjectScope(project.getProject()), InstanceScope.INSTANCE });
if (generateBAPL) {
if (debug.isDebugging())
debug.trace(String.format("Setting manifest header in %s to %s: %s", //$NON-NLS-1$
model.getUnderlyingResource().getFullPath(), AP_MANIFEST_KEY, "lazy")); //$NON-NLS-1$
bundleModel.getBundle().setHeader(AP_MANIFEST_KEY, "lazy"); //$NON-NLS-1$
}
}
private void collectManifestEntries(IBundleModel bundleModel, Collection<IPath> entries) {
String header = bundleModel.getBundle().getHeader(DS_MANIFEST_KEY);
if (header == null)
return;
String[] elements = header.split("\\s*,\\s*"); //$NON-NLS-1$
for (String element : elements) {
if (element.length() != 0)
entries.add(new Path(element));
}
}
private boolean isManifestEntryIncluded(Collection<IPath> entries, IPath path) {
for (IPath entry : entries) {
if (entry.equals(path))
return true;
if (entry.removeLastSegments(1).equals(path.removeLastSegments(1))) {
// check if wildcard match (last path segment)
Filter filter;
try {
filter = FrameworkUtil.createFilter("(filename=" + sanitizeFilterValue(entry.lastSegment()) + ")"); //$NON-NLS-1$ //$NON-NLS-2$
} catch (InvalidSyntaxException e) {
continue;
}
if (filter.matches(Collections.singletonMap("filename", path.lastSegment()))) //$NON-NLS-1$
return true;
}
}
return false;
}
private String sanitizeFilterValue(String value) {
return value.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
}
private void updateBuildProperties(IBuildModel model, Collection<String> retained, Collection<String> abandoned) throws CoreException {
IBuildEntry includes = model.getBuild().getEntry(IBuildEntry.BIN_INCLUDES);
if (includes != null) {
for (String dsKey : abandoned) {
String path = Path.fromPortableString(dsKey).toString();
if (includes.contains(path))
includes.removeToken(path);
}
}
if (!retained.isEmpty()) {
if (includes == null) {
IBuildModelFactory factory = model.getFactory();
includes = factory.createEntry(IBuildEntry.BIN_INCLUDES);
model.getBuild().add(includes);
}
LinkedHashSet<IPath> entries = new LinkedHashSet<>();
collectBuildEntries(includes, entries);
for (String dsKey : retained) {
IPath path = Path.fromPortableString(dsKey);
if (!isBuildEntryIncluded(entries, path))
includes.addToken(path.toString());
}
}
}
private void collectBuildEntries(IBuildEntry includes, Collection<IPath> entries) {
if (includes == null)
return;
for (String include : includes.getTokens()) {
if ((include = include.trim()).length() != 0)
entries.add(new Path(include));
}
}
private boolean isBuildEntryIncluded(Collection<IPath> entries, IPath path) {
for (IPath entry : entries) {
if (entry.equals(path))
return true;
if (entry.hasTrailingSeparator() && entry.isPrefixOf(path))
return true;
// TODO support full Ant path patterns
}
return false;
}
@Override
public void processAnnotations(BuildContext[] files) {
// we need to process CUs in context of a project; separate them by project
HashMap<IJavaProject, Map<ICompilationUnit, BuildContext>> filesByProject = new HashMap<>();
for (BuildContext file : files) {
if (debug.isDebugging())
debug.trace(String.format("Creating compilation unit from file %s.", file.getFile().getFullPath())); //$NON-NLS-1$
ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file.getFile());
if (cu == null) {
if (debug.isDebugging())
// TODO should we log instead? Don't want to spam the error log though
debug.trace(String.format("Unable to create compilation unit from file %s.", file.getFile().getFullPath())); //$NON-NLS-1$
continue;
}
if (canSkipFile(cu)) {
markAsAbandoned(cu);
continue;
}
Map<ICompilationUnit, BuildContext> map = filesByProject.get(cu.getJavaProject());
if (map == null) {
map = new HashMap<>();
filesByProject.put(cu.getJavaProject(), map);
}
map.put(cu, file);
}
// process all CUs in each project
for (Map.Entry<IJavaProject, Map<ICompilationUnit, BuildContext>> entry : filesByProject.entrySet()) {
if (debug.isDebugging())
debug.trace(String.format("Processing compilation units in project %s.", entry.getKey().getElementName())); //$NON-NLS-1$
processAnnotations(entry.getKey(), entry.getValue());
}
}
public boolean canSkipFile(ICompilationUnit cu) {
IType primaryType = cu.findPrimaryType();
if (primaryType == null)
return false;
try {
return !containsComponent(primaryType);
} catch (JavaModelException e) {
return false;
}
}
private boolean containsComponent(IType type) throws JavaModelException {
IAnnotation annotationWithImport = type.getAnnotation("Component"); //$NON-NLS-1$
IAnnotation fullyQualifiedAnnotation = type.getAnnotation(COMPONENT_ANNOTATION);
boolean hasComponentAnnotation = annotationWithImport.exists() || fullyQualifiedAnnotation.exists();
if (hasComponentAnnotation)
return true;
for (IJavaElement child : type.getChildren()) {
if ((child instanceof IType) && containsComponent((IType) child)) {
return true;
}
}
return false;
}
public void markAsAbandoned(ICompilationUnit cu) {
ProjectContext projectContext = processingContext.get(cu.getJavaProject());
String cuKey = AnnotationProcessor.getCompilationUnitKey(cu);
projectContext.getUnprocessed().remove(cuKey);
ProjectState state = projectContext.getState();
Collection<String> oldDSKeys = state.updateMappings(cuKey, new HashMap<>());
if (oldDSKeys != null) {
projectContext.getAbandoned().addAll(oldDSKeys);
}
}
private void processAnnotations(IJavaProject javaProject, Map<ICompilationUnit, BuildContext> fileMap) {
@SuppressWarnings("deprecation")
ASTParser parser = ASTParser.newParser(AST.JLS4);
parser.setResolveBindings(true);
parser.setBindingsRecovery(true);
parser.setProject(javaProject);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
ProjectContext projectContext = processingContext.get(javaProject);
ProjectState state = projectContext.getState();
parser.setIgnoreMethodBodies(state.getErrorLevel() == ValidationErrorLevel.ignore);
ICompilationUnit[] cuArr = fileMap.keySet().toArray(new ICompilationUnit[fileMap.size()]);
parser.createASTs(cuArr, new String[0], new AnnotationProcessor(projectContext, fileMap), null);
}
public static boolean isManaged(IProject project) {
try {
if (project.getSessionProperty(PROP_STATE) != null)
return true;
File stateFile = getStateFile(project);
return stateFile.canRead();
} catch (CoreException e) {
return false;
}
}
private static File getStateFile(IProject project) {
File workDir = project.getWorkingLocation(Activator.PLUGIN_ID).toFile();
File stateFile = new File(workDir, STATE_FILENAME);
return stateFile;
}
}