blob: 7d4f32e7e92366068565dbfae135f61e2d492019 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 458995
*******************************************************************************/
package org.eclipse.pde.api.tools.internal.builder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.jar.JarFile;
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.IResourceDelta;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
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.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
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.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
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.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.builder.State;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.ApiDescriptionManager;
import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
import org.eclipse.pde.api.tools.internal.problems.ApiProblemFactory;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants;
import org.eclipse.pde.api.tools.internal.provisional.builder.IApiAnalyzer;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.core.build.IBuild;
import org.eclipse.pde.core.build.IBuildEntry;
import org.eclipse.pde.core.build.IBuildModel;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.ICoreConstants;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.PDEPreferencesManager;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
/**
* Builder for creating API Tools resource markers
*
* @since 1.0.0
*/
public class ApiAnalysisBuilder extends IncrementalProjectBuilder {
/**
* Project relative path to the .settings folder
*
* @since 1.0.1
*/
static final IPath SETTINGS_PATH = new Path(".settings"); //$NON-NLS-1$
/**
* Project relative path to the build.properties file
*/
public static final IPath BUILD_PROPERTIES_PATH = new Path("build.properties"); //$NON-NLS-1$
/**
* Project relative path to the manifest file.
*/
public static final IPath MANIFEST_PATH = new Path(JarFile.MANIFEST_NAME);
/**
* {@link Comparator} to sort {@link ManifestElement}s
*
* @since 1.0.3
*/
static final Comparator<ManifestElement> fgManifestElementComparator = (o1, o2) -> o1.getValue().compareTo(o2.getValue());
/**
* Array of header names that we care about when a manifest delta is
* discovered
*
* @since 1.0.3
*/
public static final HashSet<String> IMPORTANT_HEADERS = new HashSet<>(7);
static {
IMPORTANT_HEADERS.add(Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
IMPORTANT_HEADERS.add(Constants.BUNDLE_VERSION);
IMPORTANT_HEADERS.add(Constants.REQUIRE_BUNDLE);
IMPORTANT_HEADERS.add(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT);
IMPORTANT_HEADERS.add(Constants.EXPORT_PACKAGE);
IMPORTANT_HEADERS.add(Constants.IMPORT_PACKAGE);
IMPORTANT_HEADERS.add(Constants.BUNDLE_CLASSPATH);
}
/**
* Project relative path to the .api_filters file
*/
static final IPath FILTER_PATH = SETTINGS_PATH.append(IApiCoreConstants.API_FILTERS_XML_NAME);
/**
* Empty listing of projects to be returned by the builder if there is
* nothing to do
*/
static final IProject[] NO_PROJECTS = new IProject[0];
/**
* Constant representing the name of the 'source' attribute on API Tools
* markers. Value is <code>API Tools</code>
*/
static final String SOURCE = "API Tools"; //$NON-NLS-1$
/**
* Boolean flag to disable the API builder (the builder will always return
* {@link #NO_PROJECTS}. Not accessible from the UI by default, but can be
* set by other tools. The behaviour is not API and may be changed or
* removed in a future release. See bug 221913.
*/
private static boolean buildDisabled = false;
/**
* The current project for which this builder was defined
*/
private IProject currentproject = null;
/**
* The API analyzer for this builder
*/
private IApiAnalyzer analyzer = null;
/**
* Maps prerequisite projects to their output location(s)
*/
HashMap<IProject, HashSet<IPath>> output_locs = new HashMap<>();
/**
* Maps prerequisite projects to their source locations
*/
HashMap<IProject, HashSet<IPath>> src_locs = new HashMap<>();
/**
* Current build state
*/
private BuildState buildstate = null;
private ConcurrentLinkedQueue<Runnable> markersQueue = new ConcurrentLinkedQueue<>();
/**
* Bug 549838: In case auto-building on a API tools settings change is not desired,
* specify VM property: {@code -Dorg.eclipse.disableAutoBuildOnSettingsChange=true}
*/
private static final boolean DISABLE_AUTO_BUILDING_ON_SETTINGS_CHANGE = Boolean.getBoolean("org.eclipse.disableAutoBuildOnSettingsChange"); //$NON-NLS-1$
/**
* Cleans up markers associated with API Tools on the given resource.
*
* @param resource
*/
void cleanupMarkers(IResource resource) {
if (isRunningAsJob()) {
ApiAnalysisMarkersJob job = new ApiAnalysisMarkersJob(() -> cleanupMarkersInternally(resource));
job.schedule();
} else {
cleanupMarkersInternally(resource);
}
}
/**
* Cleans up markers associated with API Tools on the given resource.
*
* @param resource
*/
void cleanupMarkersInternally(IResource resource) {
cleanUnusedFilterMarkers(resource);
cleanupUsageMarkers(resource);
cleanupCompatibilityMarkers(resource);
cleanupUnsupportedTagMarkers(resource);
cleanupUnsupportedAnnotationMarkers(resource);
cleanApiUseScanMarkers(resource);
cleanupFatalMarkers(resource);
}
/**
* Cleans up API use scan breakage related markers on the specified resource
*
* @param resource
*/
void cleanApiUseScanMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: cleaning api use problems"); //$NON-NLS-1$
}
resource.deleteMarkers(IApiMarkerConstants.API_USESCAN_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
IProject project = resource.getProject();
IMarker[] markers = project.findMarkers(IApiMarkerConstants.API_USESCAN_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
for (IMarker marker : markers) {
String typeName = marker.getAttribute(IApiMarkerConstants.API_USESCAN_TYPE, null);
IJavaElement adaptor = resource.getAdapter(IJavaElement.class);
if (adaptor != null && adaptor instanceof ICompilationUnit) {
IType typeroot = ((ICompilationUnit) adaptor).findPrimaryType();
if (typeroot != null && typeName != null && typeName.startsWith(typeroot.getFullyQualifiedName())) {
marker.delete();
}
}
}
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* Cleans up unsupported Javadoc tag markers on the specified resource
*
* @param resource
*/
void cleanupUnsupportedTagMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: cleaning unsupported tag problems"); //$NON-NLS-1$
}
resource.deleteMarkers(IApiMarkerConstants.UNSUPPORTED_TAG_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* Removes all of the unsupported annotation markers from the given resource
* and all of its children
*
* @param resource
* @since 1.0.600
*/
void cleanupUnsupportedAnnotationMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: cleaning unsupported annotation problems"); //$NON-NLS-1$
}
resource.deleteMarkers(IApiMarkerConstants.UNSUPPORTED_ANNOTATION_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* Cleans up only API compatibility markers on the given {@link IResource}
*
* @param resource the given resource
*/
void cleanupCompatibilityMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
resource.deleteMarkers(IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
resource.deleteMarkers(IApiMarkerConstants.SINCE_TAGS_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
if (resource.getType() == IResource.PROJECT) {
// on full builds
resource.deleteMarkers(IApiMarkerConstants.VERSION_NUMBERING_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
resource.deleteMarkers(IApiMarkerConstants.DEFAULT_API_BASELINE_PROBLEM_MARKER, true, IResource.DEPTH_ZERO);
resource.deleteMarkers(IApiMarkerConstants.API_COMPONENT_RESOLUTION_PROBLEM_MARKER, true, IResource.DEPTH_ZERO);
}
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* cleans up only API usage markers from the given {@link IResource}
*
* @param resource
*/
void cleanupUsageMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
resource.deleteMarkers(IApiMarkerConstants.API_USAGE_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
if (resource.getType() != IResource.PROJECT) {
IProject pj = resource.getProject();
if (pj != null) {
pj.deleteMarkers(IApiMarkerConstants.API_USAGE_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
}
}
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* cleans up only fatal problem markers from the given {@link IResource}
*
* @param resource
*/
void cleanupFatalMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
resource.deleteMarkers(IApiMarkerConstants.FATAL_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
}
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
}
}
/**
* Cleans up the unused API filter problems from the given resource
*
* @param resource
*/
void cleanUnusedFilterMarkers(IResource resource) {
try {
if (resource != null && resource.isAccessible()) {
resource.deleteMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
}
} catch (CoreException ce) {
ApiPlugin.log(ce.getStatus());
}
}
@Override
public ISchedulingRule getRule(int kind, Map<String, String> args) {
// TODO probably we don't need even this and can return null if we are running as job
// if(isRunningAsJob()) return null;
return currentproject;
}
@Override
protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException {
PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
boolean disableAPIAnalysisBuilder = prefs.getBoolean(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER);
if (disableAPIAnalysisBuilder) {
return NO_PROJECTS;
}
this.currentproject = getProject();
if (buildDisabled || shouldAbort(this.currentproject)) {
return NO_PROJECTS;
}
// update build time stamp
BuildStamps.incBuildStamp(this.currentproject);
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("\nApiAnalysisBuilder: Starting build of " + this.currentproject.getName() + " @ " + new Date(System.currentTimeMillis())); //$NON-NLS-1$ //$NON-NLS-2$
}
IApiBaseline wbaseline = ApiPlugin.getDefault().getApiBaselineManager().getWorkspaceBaseline();
if (wbaseline == null) {
if (ApiPlugin.DEBUG_BUILDER) {
System.err.println("ApiAnalysisBuilder: Could not retrieve a workspace baseline"); //$NON-NLS-1$
}
return NO_PROJECTS;
}
final IProject[] projects = getRequiredProjects(true);
if (kind != FULL_BUILD && kind != AUTO_BUILD && kind != INCREMENTAL_BUILD) {
return projects;
}
boolean fullBuild = kind == FULL_BUILD;
boolean runAsJob = prefs.getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB);
if (runAsJob) {
ApiAnalysisJob job = new ApiAnalysisJob(BuilderMessages.api_analysis_builder, currentproject, fullBuild,
wbaseline, projects);
job.cancelSimilarJobs(fullBuild);
job.schedule(100);
} else {
work(fullBuild, wbaseline, projects, monitor);
}
return projects;
}
protected void work(final boolean fullBuild, IApiBaseline wbaseline, IProject[] projects, IProgressMonitor monitor)
throws CoreException {
SubMonitor localMonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_builder, 8);
IApiBaseline baseline = ApiPlugin.getDefault().getApiBaselineManager().getDefaultApiBaseline();
try {
SubMonitor switchMonitor = localMonitor.split(4);
if (fullBuild) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Performing full build as requested"); //$NON-NLS-1$
}
buildAll(baseline, wbaseline, switchMonitor);
} else {
this.buildstate = BuildState.getLastBuiltState(currentproject);
if (this.buildstate == null) {
buildAll(baseline, wbaseline, switchMonitor);
} else if (worthDoingFullBuild(projects)) {
buildAll(baseline, wbaseline, switchMonitor);
} else {
IResourceDelta[] deltas = getDeltas(projects);
if (deltas.length < 1) {
buildAll(baseline, wbaseline, switchMonitor);
} else {
IResourceDelta filters = null;
boolean full = false;
for (IResourceDelta delta : deltas) {
full = shouldFullBuild(delta);
if (full) {
break;
}
filters = delta.findMember(FILTER_PATH);
if (filters != null) {
switch (filters.getKind()) {
case IResourceDelta.ADDED:
case IResourceDelta.REMOVED: {
full = true;
break;
}
case IResourceDelta.CHANGED: {
full = (filters.getFlags() & (IResourceDelta.REPLACED | IResourceDelta.CONTENT)) > 0;
break;
}
default: {
break;
}
}
if (full) {
break;
}
}
}
if (full) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Performing full build since MANIFEST.MF or .api_filters was modified"); //$NON-NLS-1$
}
buildAll(baseline, wbaseline, switchMonitor);
} else {
switchMonitor.setWorkRemaining(2);
State state = (State) JavaModelManager.getJavaModelManager().getLastBuiltState(this.currentproject, switchMonitor.split(1));
if (state == null) {
buildAll(baseline, wbaseline, switchMonitor.split(1));
} else {
BuildState.setLastBuiltState(this.currentproject, null);
IncrementalApiBuilder builder = new IncrementalApiBuilder(this);
builder.build(baseline, wbaseline, deltas, state, this.buildstate, switchMonitor.split(1));
}
}
}
}
}
localMonitor.split(1);
} catch (OperationCanceledException oce) {
// do nothing, but don't forward it
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304315
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Trapped OperationCanceledException"); //$NON-NLS-1$
}
} catch (CoreException e) {
IStatus status = e.getStatus();
if (status == null || status.getCode() != ApiPlugin.REPORT_BASELINE_IS_DISPOSED) {
throw e;
}
ApiPlugin.log(e);
} finally {
try {
localMonitor.split(1);
if (this.analyzer != null) {
this.analyzer.dispose();
this.analyzer = null;
}
if (projects.length < 1) {
// if this build cycle indicates that more projects need to
// be built do not close
// the baselines yet, they might be re-read by another build
// cycle
// TODO: this seem to be not needed anymore.
// if (baseline != null) {
// baseline.close();
// }
}
localMonitor.split(1);
if (this.buildstate != null) {
for (IProject project : projects) {
if (Util.isApiProject(project)) {
this.buildstate.addApiToolingDependentProject(project.getName());
}
}
this.buildstate.setBuildPathCRC(BuildState.computeBuildPathCRC(this.currentproject));
IFile manifest = (IFile) currentproject.findMember(MANIFEST_PATH);
if (manifest != null && manifest.exists()) {
try {
this.buildstate.setManifestState(ManifestElement.parseBundleManifest(manifest.getContents(), null));
} catch (Exception e) {
ApiPlugin
.log(Status.error("Error parsing the manifest of: " + currentproject.getName(), e));//$NON-NLS-1$
}
}
IPluginModelBase base = PluginRegistry.findModel(currentproject);
if (base != null) {
try {
IBuildModel model = PluginRegistry.createBuildModel(base);
if (model != null) {
this.buildstate.setBuildPropertiesState(model);
}
} catch (CoreException ce) {
ApiPlugin.log(ce);
}
}
BuildState.saveBuiltState(this.currentproject, this.buildstate);
this.buildstate = null;
localMonitor.split(1);
}
SubMonitor.done(monitor);
} catch (OperationCanceledException oce) {
// do nothing, but don't forward it
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304315
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Trapped OperationCanceledException"); //$NON-NLS-1$
}
}
}
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Finished build of " + this.currentproject.getName() + " @ " + new Date(System.currentTimeMillis())); //$NON-NLS-1$ //$NON-NLS-2$
}
}
class ApiAnalysisJob extends Job {
private boolean fullBuild;
private IApiBaseline wbaseline;
private IProject[] projects;
private IProject project;
public ApiAnalysisJob(String name, IProject project, boolean fullBuild, IApiBaseline wbaseline,
IProject[] projects) {
super(name);
this.project = project;
this.fullBuild = fullBuild;
this.wbaseline = wbaseline;
this.projects = projects;
// Intentionally no rule set to allow run in parallel with build locking entire workspace
// setRule(project);
}
@Override
public IStatus run(IProgressMonitor monitor) {
try {
work(fullBuild, wbaseline, projects, monitor);
} catch (CoreException e) {
return e.getStatus();
}
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return super.belongsTo(family) || ApiAnalysisJob.class == family;
}
void cancelSimilarJobs(boolean fullBuild) {
Job[] jobs = Job.getJobManager().find(ApiAnalysisJob.class);
for (Job job : jobs) {
ApiAnalysisJob ajob = (ApiAnalysisJob) job;
if (fullBuild == ajob.fullBuild && project.equals(ajob.project)) {
job.cancel();
}
}
}
}
/**
* Returns if the backing project should be fully built, based on the delta
*
* @param delta the {@link IResourceDelta} to examine
* @return <code>true</code> if the project should have a full build,
* <code>false</code> otherwise
* @since 1.0.3
*/
boolean shouldFullBuild(IResourceDelta delta) {
switch (delta.getKind()) {
case IResourceDelta.CHANGED: {
IResourceDelta subdelta = delta.findMember(MANIFEST_PATH);
if (subdelta != null) {
IFile file = (IFile) subdelta.getResource();
return file.getProject().equals(currentproject) && compareManifest(file, buildstate);
}
subdelta = delta.findMember(BUILD_PROPERTIES_PATH);
if (subdelta != null) {
IFile file = (IFile) subdelta.getResource();
return file.getProject().equals(currentproject) && compareBuildProperties(buildstate);
}
subdelta = delta.findMember(SETTINGS_PATH);
if (subdelta != null) {
if (!DISABLE_AUTO_BUILDING_ON_SETTINGS_CHANGE) {
return true;
}
}
break;
}
default: {
break;
}
}
return false;
}
/**
* Compares the current <code>MANIFEST.MF</code> against the saved state. If
* the {@link BuildState} is <code>null</code> or there is no saved state
* for the current project context a full build is assumed.
*
* @param manifest the handle to the <code>MANIFEST.MF</code> file
* @param state the current {@link BuildState} or <code>null</code>
* @return <code>true</code> if there are changes that require a full build,
* <code>false</code> otherwise
* @since 1.0.3
*/
boolean compareManifest(IFile manifest, BuildState state) {
if (state != null) {
try {
Map<String, String> stateheaders = state.getManifestState();
if (stateheaders != null) {
Map<String, String> headers = ManifestElement.parseBundleManifest(manifest.getContents(), null);
Entry<String, String> entry = null;
for (Iterator<Entry<String, String>> i = stateheaders.entrySet().iterator(); i.hasNext();) {
entry = i.next();
String key = entry.getKey();
String value = headers.get(key);
ManifestElement[] e1 = ManifestElement.parseHeader(key, value);
ManifestElement[] e2 = ManifestElement.parseHeader(key, entry.getValue());
if (e1 != null && e2 != null && e1.length == e2.length) {
Arrays.sort(e1, fgManifestElementComparator);
Arrays.sort(e2, fgManifestElementComparator);
for (int j = 0; j < e1.length; j++) {
String[] v1 = e1[j].getValueComponents();
String[] v2 = e2[j].getValueComponents();
// compare value bits
if (v1.length == v2.length) {
Arrays.sort(v1);
Arrays.sort(v2);
for (int k = 0; k < v2.length; k++) {
if (!v1[k].equals(v2[k])) {
return true;
}
}
} else {
return true;
}
// compare directives
Enumeration<String> e = e1[j].getDirectiveKeys();
if (e != null) {
while (e.hasMoreElements()) {
String key2 = e.nextElement();
if (!Util.equalsOrNull(e1[j].getDirective(key2), e2[j].getDirective(key2))) {
return true;
}
}
}
// compare attributes
e = e1[j].getKeys();
if (e != null) {
while (e.hasMoreElements()) {
String key2 = e.nextElement();
if (!Util.equalsOrNull(e1[j].getAttribute(key2), e2[j].getAttribute(key2))) {
return true;
}
}
}
}
} else {
return true;
}
}
return false;
}
} catch (Exception e) {
ApiPlugin.log(e);
return false;
}
}
return true;
}
/**
* Compares the current <code>build.properties</code> against the saved one.
* If the given {@link BuildState} is <code>null</code> or there is no saved
* state for the current project context a full build is assumed.
*
* @param state the current {@link BuildState} or <code>null</code>
* @return <code>true</code> if there are changes that require a full build,
* <code>false</code> otherwise
* @since 1.0.3
*/
boolean compareBuildProperties(BuildState state) {
if (state != null) {
Map<String, String> map = state.getBuildPropertiesState();
if (map != null) {
IPluginModelBase base = PluginRegistry.findModel(currentproject);
if (base != null) {
try {
IBuildModel model = PluginRegistry.createBuildModel(base);
if (model != null) {
IBuild ibuild = model.getBuild();
Entry<String, String> entry;
for (Iterator<Entry<String, String>> i = map.entrySet().iterator(); i.hasNext();) {
entry = i.next();
IBuildEntry be = ibuild.getEntry(entry.getKey());
if (be != null && !entry.getValue().equals(Util.deepToString(be.getTokens()))) {
return true;
}
}
}
} catch (CoreException ce) {
ApiPlugin.log(ce);
return false;
}
}
}
return false;
}
return true;
}
/**
* Returns if the builder should abort the build of the given project. The
* build decides to abort if one of the following are true:
* <ul>
* <li>The project is not accessible</li>
* <li>The project does not have the API tools nature</li>
* <li>The project has already been built - as decided by the build
* framework</li>
* <li>The project has fatal JDT errors that prevent the creation of class
* files</li>
* </ul>
*
* @param project
* @return true if the builder should abort building the given project,
* false otherwise
* @throws CoreException
* @see {@link #hasBeenBuilt(IProject)}
* @see {@link #hasFatalProblems(IProject)}
* @since 1.1
*/
boolean shouldAbort(IProject project) throws CoreException {
return !project.isAccessible() || !project.hasNature(ApiPlugin.NATURE_ID) || hasBeenBuilt(project) || hasFatalProblems(project);
}
/**
* Returns if the project we are about to build has fatal JDT problems that
* prevent class files from being built
*
* @param project
* @return true if the given project has fatal JDT problems
* @see
* @throws CoreException
* @see {@link org.eclipse.jdt.core.IJavaModelMarker#BUILDPATH_PROBLEM_MARKER}
* @since 1.1
*/
boolean hasFatalProblems(IProject project) throws CoreException {
boolean hasFatalProblem = false;
IMarker[] problems = project.findMarkers(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_ZERO);
for (IMarker iMarker : problems) {
Object att = iMarker.getAttribute(IMarker.SEVERITY);
if (att != null && att instanceof Integer) {
if (((Integer) att).intValue() == IMarker.SEVERITY_ERROR) {
hasFatalProblem = true;
break;
}
}
}
Runnable task;
if (hasFatalProblem) {
task = () -> {
cleanupMarkers(project);
IApiProblem problem = ApiProblemFactory.newFatalProblem(Path.EMPTY.toString(), new String[] { project.getName() }, IApiProblem.FATAL_JDT_BUILDPATH_PROBLEM);
createMarkerForProblem(IApiProblem.CATEGORY_FATAL_PROBLEM, IApiMarkerConstants.FATAL_PROBLEM_MARKER, problem);
};
} else {
task = () -> cleanupFatalMarkers(project);
}
boolean runAsJob = isRunningAsJob();
if (runAsJob) {
new ApiAnalysisMarkersJob(task).schedule();
} else {
task.run();
}
return hasFatalProblem;
}
private static boolean isRunningAsJob() {
PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
boolean runAsJob = prefs.getBoolean(ICoreConstants.RUN_API_ANALYSIS_AS_JOB);
return runAsJob;
}
/**
* if its worth doing a full build considering the given set if projects
*
* @param projects projects to check the build state for
* @return true if a full build should take place, false otherwise
*/
boolean worthDoingFullBuild(IProject[] projects) {
Set<String> apiToolingDependentProjects = this.buildstate.getApiToolingDependentProjects();
for (IProject currentProject : projects) {
if (Util.isApiProject(currentProject)) {
if (apiToolingDependentProjects.contains(currentProject.getName())) {
continue;
}
return true;
} else if (apiToolingDependentProjects.contains(currentProject.getName())) {
return true;
}
}
return false;
}
/**
* Performs a full build for the project
*
* @param baseline the default baseline
* @param wbaseline the workspace baseline
* @param monitor
*/
void buildAll(IApiBaseline baseline, IApiBaseline wbaseline, IProgressMonitor monitor) throws CoreException {
PDEPreferencesManager prefs = PDECore.getDefault().getPreferencesManager();
boolean disableAPIAnalysisBuilder = prefs.getBoolean(ICoreConstants.DISABLE_API_ANALYSIS_BUILDER);
if (disableAPIAnalysisBuilder) {
return;
}
SubMonitor localMonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_on_0, 4);
try {
BuildState.setLastBuiltState(this.currentproject, null);
this.buildstate = new BuildState();
localMonitor.subTask(NLS.bind(BuilderMessages.ApiAnalysisBuilder_initializing_analyzer, currentproject.getName()));
cleanupMarkers(this.currentproject);
IPluginModelBase currentModel = getCurrentModel();
if (currentModel != null) {
localMonitor.subTask(BuilderMessages.building_workspace_profile);
localMonitor.split(1);
String id = currentModel.getBundleDescription().getSymbolicName();
// Compatibility checks
IApiComponent apiComponent = wbaseline.getApiComponent(id);
Set<IApiComponent> apiComponentMultiple = wbaseline.getAllApiComponents(id);
if (!apiComponentMultiple.isEmpty()) {
// add the exact match
for (Iterator<IApiComponent> iterator = apiComponentMultiple.iterator(); iterator.hasNext();) {
IApiComponent iApiComponent = iterator.next();
Version workspaceBaselineVersion = new Version(iApiComponent.getVersion());// removes
// qualifier
Version currentProjectVersion = currentModel.getBundleDescription().getVersion();
if (new Version(currentProjectVersion.getMajor(), currentProjectVersion.getMinor(), currentProjectVersion.getMicro()).compareTo(workspaceBaselineVersion) == 0) {
apiComponent = iApiComponent;
break;
}
}
}
if (apiComponent != null) {
if (getAnalyzer() instanceof BaseApiAnalyzer) {
((BaseApiAnalyzer)getAnalyzer()).checkBaselineMismatch(baseline, wbaseline);
}
getAnalyzer().analyzeComponent(this.buildstate, null, null, baseline, apiComponent, new BuildContext(), localMonitor.split(1));
localMonitor.split(1);
createMarkers();
localMonitor.split(1);
}
}
} finally {
if (localMonitor != null) {
localMonitor.done();
}
}
}
/**
* Creates or removes markers, uses the current project rule.
* The tasks to do are maintained by markersQueue and executed in the submission order
*/
class ApiAnalysisMarkersJob extends WorkspaceJob {
public ApiAnalysisMarkersJob(Runnable task) {
super("Creating markers on " + currentproject.getName()); //$NON-NLS-1$
markersQueue.add(task);
setRule(currentproject);
setSystem(true);
}
@Override
public boolean belongsTo(Object family) {
return super.belongsTo(family) || ApiAnalysisMarkersJob.class == family;
}
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
while (!markersQueue.isEmpty()) {
Runnable task = markersQueue.poll();
task.run();
}
return Status.OK_STATUS;
}
}
/**
* Creates new markers are for the listing of problems added to this
* reporter. If no problems have been added to this reporter, or we are not
* running in the framework, no work is done.
*/
protected void createMarkers() {
IApiProblem[] problems = getAnalyzer().getProblems();
if (isRunningAsJob()) {
new ApiAnalysisMarkersJob(() -> createMarkersInternally(problems)).schedule();
} else {
createMarkersInternally(problems);
}
}
/**
* Creates new markers are for the listing of problems added to this reporter.
* If no problems have been added to this reporter, or we are not running in the
* framework, no work is done.
*
* @param problems
*/
protected void createMarkersInternally(IApiProblem[] problems) {
try {
IResource manifest = Util.getManifestFile(this.currentproject);
if (manifest != null) {
manifest.deleteMarkers(IApiMarkerConstants.VERSION_NUMBERING_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
}
this.currentproject.deleteMarkers(IApiMarkerConstants.DEFAULT_API_BASELINE_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
this.currentproject.deleteMarkers(IApiMarkerConstants.API_COMPONENT_RESOLUTION_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
} catch (CoreException e) {
ApiPlugin.log(e);
}
String type = null;
for (IApiProblem problem : problems) {
int category = problem.getCategory();
type = getProblemTypeFromCategory(category, problem.getKind());
if (type == null) {
continue;
}
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: creating marker for: " + problem.toString()); //$NON-NLS-1$
}
createMarkerForProblem(category, type, problem);
}
}
/**
* Returns the {@link IApiMarkerConstants} problem type given the problem
* category
*
* @param category the problem category - see {@link IApiProblem} for
* problem categories
* @param kind the kind of the problem - see {@link IApiProblem} for problem
* kinds
* @return the problem type or <code>null</code>
*/
String getProblemTypeFromCategory(int category, int kind) {
switch (category) {
case IApiProblem.CATEGORY_API_COMPONENT_RESOLUTION: {
return IApiMarkerConstants.API_COMPONENT_RESOLUTION_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_API_BASELINE: {
return IApiMarkerConstants.DEFAULT_API_BASELINE_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_COMPATIBILITY: {
return IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_SINCETAGS: {
return IApiMarkerConstants.SINCE_TAGS_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_USAGE: {
if (kind == IApiProblem.UNSUPPORTED_TAG_USE) {
return IApiMarkerConstants.UNSUPPORTED_TAG_PROBLEM_MARKER;
}
if (kind == IApiProblem.UNSUPPORTED_ANNOTATION_USE) {
return IApiMarkerConstants.UNSUPPORTED_ANNOTATION_PROBLEM_MARKER;
}
if (kind == IApiProblem.UNUSED_PROBLEM_FILTERS) {
return IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER;
}
return IApiMarkerConstants.API_USAGE_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_VERSION: {
return IApiMarkerConstants.VERSION_NUMBERING_PROBLEM_MARKER;
}
case IApiProblem.CATEGORY_API_USE_SCAN_PROBLEM: {
return IApiMarkerConstants.API_USESCAN_PROBLEM_MARKER;
}
default: {
return null;
}
}
}
/**
* Creates an {@link IMarker} on the resource specified in the problem (via
* its path) with the given problem attributes
*
* @param category the category of the problem - see {@link IApiProblem} for
* categories
* @param type the marker type to create - see {@link IApiMarkerConstants}
* for types
* @param problem the problem to create a marker from
*/
void createMarkerForProblem(int category, String type, IApiProblem problem) {
IResource resource = resolveResource(problem);
if (resource == null) {
return;
}
try {
if (category == IApiProblem.CATEGORY_API_USE_SCAN_PROBLEM) {
IMarker[] markers = resource.findMarkers(type, true, IResource.DEPTH_ZERO);
for (IMarker marker : markers) {
String msg = marker.getAttribute(IMarker.MESSAGE, null);
if (msg == null || msg.equalsIgnoreCase(problem.getMessage())) {
int markerSeverity = marker.getAttribute(IMarker.SEVERITY, 0);
int problemSeverity = ApiPlugin.getDefault().getSeverityLevel(ApiProblemFactory.getProblemSeverityId(problem), this.currentproject);
if (markerSeverity == problemSeverity) {
return; // Marker already exists
}
} else {
marker.delete(); // create the marker afresh
}
}
}
IMarker marker = null;
if (problem.getKind() == IApiProblem.API_BASELINE_MISMATCH
&& category == IApiProblem.CATEGORY_API_BASELINE) {
// need a workspace marker
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IMarker[] findMarkers = root.findMarkers(type, false, IResource.DEPTH_ZERO);
if (findMarkers.length == 0) {
marker = root.createMarker(type);
}
else {
marker = findMarkers[0];
}
} else {
marker = resource.createMarker(type);
}
int line = problem.getLineNumber();
switch (category)
{
case IApiProblem.CATEGORY_VERSION:
case IApiProblem.CATEGORY_API_BASELINE:
case IApiProblem.CATEGORY_API_COMPONENT_RESOLUTION:
case IApiProblem.CATEGORY_API_USE_SCAN_PROBLEM: {
break;
}
default: {
line++;
}
}
marker.setAttributes(new String[] {
IMarker.MESSAGE, IMarker.SEVERITY, IMarker.LINE_NUMBER,
IMarker.CHAR_START, IMarker.CHAR_END, IMarker.SOURCE_ID,
IApiMarkerConstants.MARKER_ATTR_PROBLEM_ID }, new Object[] {
problem.getMessage(),
Integer.valueOf(ApiPlugin.getDefault().getSeverityLevel(ApiProblemFactory.getProblemSeverityId(problem), this.currentproject)),
Integer.valueOf(line), Integer.valueOf(problem.getCharStart()),
Integer.valueOf(problem.getCharEnd()),
ApiAnalysisBuilder.SOURCE, Integer.valueOf(problem.getId()) });
// add message arguments, if any
String[] args = problem.getMessageArguments();
if (args.length > 0) {
marker.setAttribute(IApiMarkerConstants.MARKER_ATTR_MESSAGE_ARGUMENTS, createArgAttribute(args));
}
String typeName = problem.getTypeName();
if (typeName != null) {
marker.setAttribute(IApiMarkerConstants.MARKER_ATTR_PROBLEM_TYPE_NAME, typeName);
}
// add all other extra arguments, if any
if (problem.getExtraMarkerAttributeIds().length > 0) {
marker.setAttributes(problem.getExtraMarkerAttributeIds(), problem.getExtraMarkerAttributeValues());
}
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Created the marker: " + marker.getId() + " - " + marker.getAttributes().entrySet()); //$NON-NLS-1$ //$NON-NLS-2$
}
} catch (CoreException e) {
ApiPlugin.log(e);
return;
}
}
/**
* Resolves the resource from the path in the problem, returns
* <code>null</code> in the following cases:
* <ul>
* <li>The resource is not found in the parent project (findMember() returns
* null)</li>
* <li>The resource is not accessible (isAccessible() returns false</li>
* </ul>
*
* @param problem the problem to get the resource for
* @return the resource or <code>null</code>
*/
IResource resolveResource(IApiProblem problem) {
String resourcePath = problem.getResourcePath();
if (resourcePath == null) {
return null;
}
IResource resource = currentproject.findMember(new Path(resourcePath));
if (resource == null) {
// might be re-exported try to look it up
IJavaProject jp = JavaCore.create(currentproject);
try {
IType type = jp.findType(problem.getTypeName());
if (type != null) {
return type.getResource();
}
} catch (JavaModelException jme) {
// do nothing
}
return null;
}
if (!resource.isAccessible()) {
return null;
}
return resource;
}
/**
* Creates a single string attribute from an array of strings. Uses the '#'
* char as a delimiter
*
* @param args
* @return a single string attribute from an array or arguments
*/
String createArgAttribute(String[] args) {
StringBuilder buff = new StringBuilder();
for (int i = 0; i < args.length; i++) {
buff.append(args[i]);
if (i < args.length - 1) {
buff.append("#"); //$NON-NLS-1$
}
}
return buff.toString();
}
@Override
protected void clean(IProgressMonitor monitor) throws CoreException {
this.currentproject = getProject();
SubMonitor localmonitor = SubMonitor.convert(monitor, MessageFormat.format(BuilderMessages.CleaningAPIDescription, this.currentproject.getName()), 2);
try {
// clean up all existing markers
cleanupUsageMarkers(this.currentproject);
cleanupCompatibilityMarkers(this.currentproject);
cleanupUnsupportedTagMarkers(this.currentproject);
cleanupUnsupportedAnnotationMarkers(this.currentproject);
this.currentproject.deleteMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE);
localmonitor.split(1);
// clean up the .api_settings
cleanupApiDescription(this.currentproject);
localmonitor.split(1);
} finally {
BuildState.setLastBuiltState(this.currentproject, null);
localmonitor.done();
}
}
/**
* Cleans the .api_settings file for the given project
*
* @param project
*/
void cleanupApiDescription(IProject project) {
if (project != null && project.exists()) {
ApiDescriptionManager.getManager().clean(JavaCore.create(project), true, false);
}
}
/**
* @return the current {@link IPluginModelBase} based on the current project
* for this builder
*/
IPluginModelBase getCurrentModel() {
IPluginModelBase[] workspaceModels = PluginRegistry.getWorkspaceModels();
IPath location = this.currentproject.getLocation();
IPluginModelBase currentModel = null;
BundleDescription desc = null;
loop: for (int i = 0, max = workspaceModels.length; i < max; i++) {
desc = workspaceModels[i].getBundleDescription();
if (desc != null) {
Path path = new Path(desc.getLocation());
if (path.equals(location)) {
currentModel = workspaceModels[i];
break loop;
}
} else if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Tried to look up bundle description for: " + workspaceModels[i].toString()); //$NON-NLS-1$
}
}
return currentModel;
}
/**
* Returns a listing of deltas for this project and for dependent projects
*
* @param projects
* @return
*/
IResourceDelta[] getDeltas(IProject[] projects) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Searching for deltas for build of project: " + this.currentproject.getName()); //$NON-NLS-1$
}
ArrayList<IResourceDelta> deltas = new ArrayList<>();
IResourceDelta delta = getDelta(this.currentproject);
if (delta != null) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Found a delta: " + delta); //$NON-NLS-1$
}
deltas.add(delta);
}
for (IProject project : projects) {
delta = getDelta(project);
if (delta != null) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Found a delta: " + delta); //$NON-NLS-1$
}
deltas.add(delta);
}
}
return deltas.toArray(new IResourceDelta[deltas.size()]);
}
/**
* Returns the API analyzer to use with this instance of the builder
*
* @return the API analyzer to use
*/
protected synchronized IApiAnalyzer getAnalyzer() {
if (this.analyzer == null) {
this.analyzer = new BaseApiAnalyzer();
}
return this.analyzer;
}
/**
* Returns the complete listing of required projects from the classpath of
* the backing project
*
* @param includeBinaryPrerequisites
* @return the list of projects required
* @throws CoreException
*/
IProject[] getRequiredProjects(boolean includebinaries) throws CoreException {
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
if (this.currentproject == null || workspaceRoot == null) {
return new IProject[0];
}
ArrayList<IProject> projects = new ArrayList<>();
try {
IJavaProject javaProject = JavaCore.create(this.currentproject);
HashSet<IPath> blocations = new HashSet<>();
blocations.add(javaProject.getOutputLocation());
this.output_locs.put(this.currentproject, blocations);
HashSet<IPath> slocations = new HashSet<>();
IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
for (IPackageFragmentRoot root : roots) {
if (root.isArchive()) {
continue;
}
slocations.add(root.getPath());
}
this.src_locs.put(this.currentproject, slocations);
IClasspathEntry[] entries = javaProject.getResolvedClasspath(true);
for (IClasspathEntry entry : entries) {
IPath path = entry.getPath();
IProject p = null;
switch (entry.getEntryKind()) {
case IClasspathEntry.CPE_PROJECT: {
p = workspaceRoot.getProject(path.lastSegment()); // missing
// projects
// are
// considered
// too
if (isOptional(entry) && !p.hasNature(ApiPlugin.NATURE_ID)) {// except
// if
// entry
// is
// optional
p = null;
}
break;
}
case IClasspathEntry.CPE_LIBRARY: {
if (includebinaries && path.segmentCount() > 1) {
// some binary resources on the class path can come
// from projects that are not included in the
// project references
IResource resource = workspaceRoot.findMember(path.segment(0));
if (resource instanceof IProject) {
p = (IProject) resource;
}
}
break;
}
case IClasspathEntry.CPE_SOURCE: {
IPath entrypath = entry.getOutputLocation();
if (entrypath != null) {
blocations.add(entrypath);
}
break;
}
default: {
break;
}
}
if (p != null && !projects.contains(p)) {
projects.add(p);
// try to derive all of the output locations for each of the
// projects
javaProject = JavaCore.create(p);
HashSet<IPath> bins = new HashSet<>();
HashSet<IPath> srcs = new HashSet<>();
if (javaProject.exists()) {
bins.add(javaProject.getOutputLocation());
IClasspathEntry[] source = javaProject.getRawClasspath();
IPath entrypath = null;
for (IClasspathEntry element : source) {
if (element.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
srcs.add(element.getPath());
entrypath = element.getOutputLocation();
if (entrypath != null) {
bins.add(entrypath);
}
}
}
this.output_locs.put(p, bins);
this.src_locs.put(p, srcs);
}
}
}
} catch (JavaModelException e) {
return new IProject[0];
}
IProject[] result = new IProject[projects.size()];
projects.toArray(result);
return result;
}
/**
* Returns the output paths of the given project or <code>null</code> if
* none have been computed
*
* @param project
* @return the output paths for the given project or <code>null</code>
*/
HashSet<IPath> getProjectOutputPaths(IProject project) {
return this.output_locs.get(project);
}
/**
* Returns is the given classpath entry is optional or not
*
* @param entry
* @return true if the specified {@link IClasspathEntry} is optional, false
* otherwise
*/
boolean isOptional(IClasspathEntry entry) {
IClasspathAttribute[] attribs = entry.getExtraAttributes();
for (IClasspathAttribute attribute : attribs) {
if (IClasspathAttribute.OPTIONAL.equals(attribute.getName()) && "true".equals(attribute.getValue())) { //$NON-NLS-1$
return true;
}
}
return false;
}
@Override
public String toString() {
return NLS.bind(BuilderMessages.ApiAnalysisBuilder_builder_for_project, this.currentproject != null ? this.currentproject.getName() : null);
}
}