1 | /******************************************************************************* |
2 | * Copyright (c) 2009 IBM Corporation and others. |
3 | * All rights reserved. This program and the accompanying materials |
4 | * are made available under the terms of the Eclipse Public License v1.0 |
5 | * which accompanies this distribution, and is available at |
6 | * http://www.eclipse.org/legal/epl-v10.html |
7 | * |
8 | * Contributors: |
9 | * IBM Corporation - initial API and implementation |
10 | *******************************************************************************/ |
11 | package org.eclipse.pde.api.tools.internal.builder; |
12 | |
13 | import java.util.HashSet; |
14 | import java.util.Iterator; |
15 | |
16 | import org.eclipse.core.resources.IFile; |
17 | import org.eclipse.core.resources.IMarker; |
18 | import org.eclipse.core.resources.IProject; |
19 | import org.eclipse.core.resources.IResource; |
20 | import org.eclipse.core.resources.IResourceDelta; |
21 | import org.eclipse.core.resources.IResourceDeltaVisitor; |
22 | import org.eclipse.core.resources.IWorkspaceRoot; |
23 | import org.eclipse.core.resources.ResourcesPlugin; |
24 | import org.eclipse.core.runtime.CoreException; |
25 | import org.eclipse.core.runtime.IPath; |
26 | import org.eclipse.core.runtime.IProgressMonitor; |
27 | import org.eclipse.core.runtime.SubMonitor; |
28 | import org.eclipse.jdt.core.ICompilationUnit; |
29 | import org.eclipse.jdt.core.IType; |
30 | import org.eclipse.jdt.core.JavaCore; |
31 | import org.eclipse.jdt.core.JavaModelException; |
32 | import org.eclipse.jdt.internal.core.builder.ReferenceCollection; |
33 | import org.eclipse.jdt.internal.core.builder.State; |
34 | import org.eclipse.jdt.internal.core.builder.StringSet; |
35 | import org.eclipse.osgi.util.NLS; |
36 | import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; |
37 | import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants; |
38 | import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline; |
39 | import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent; |
40 | import org.eclipse.pde.api.tools.internal.util.Util; |
41 | import org.eclipse.pde.core.plugin.IPluginModelBase; |
42 | |
43 | /** |
44 | * Used to incrementally build changed Java types |
45 | * |
46 | * @since 3.5 |
47 | */ |
48 | public class IncrementalApiBuilder { |
49 | |
50 | /** |
51 | * Visits a resource delta to collect changes that need to be built |
52 | */ |
53 | class ResourceDeltaVisitor implements IResourceDeltaVisitor { |
54 | HashSet projects = null; |
55 | IProject project = null; |
56 | |
57 | /** |
58 | * Constructor |
59 | * @param project |
60 | * @param projects |
61 | */ |
62 | public ResourceDeltaVisitor(IProject project, HashSet projects) { |
63 | this.project = project; |
64 | this.projects = projects; |
65 | } |
66 | /* (non-Javadoc) |
67 | * @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta) |
68 | */ |
69 | public boolean visit(IResourceDelta delta) throws CoreException { |
70 | switch (delta.getResource().getType()) { |
71 | case IResource.ROOT: |
72 | case IResource.PROJECT: |
73 | case IResource.FOLDER: { |
74 | return true; |
75 | } |
76 | case IResource.FILE: { |
77 | IResource resource = delta.getResource(); |
78 | String fileName = resource.getName(); |
79 | if (Util.isClassFile(fileName)) { |
80 | findAffectedSourceFiles(delta); |
81 | } else if (Util.isJavaFileName(fileName)) { |
82 | IProject project = resource.getProject(); |
83 | if (this.project.equals(project)) { |
84 | addTypeToContext((IFile) resource); |
85 | } |
86 | else if (this.projects != null && this.projects.contains(project)) { |
87 | addTypeToContext((IFile) resource); |
88 | } |
89 | } |
90 | } |
91 | } |
92 | return false; |
93 | } |
94 | } |
95 | |
96 | ApiAnalysisBuilder builder = null; |
97 | BuildContext context = null; |
98 | StringSet typenames = new StringSet(16); |
99 | StringSet packages = new StringSet(16); |
100 | |
101 | |
102 | /** |
103 | * Constructor |
104 | * @param project the current project context being built |
105 | * @param delta the {@link IResourceDelta} from the build framework |
106 | * @param buildstate the current build state from the {@link org.eclipse.jdt.internal.core.builder.JavaBuilder} |
107 | */ |
108 | public IncrementalApiBuilder(ApiAnalysisBuilder builder) { |
109 | this.builder = builder; |
110 | } |
111 | |
112 | /** |
113 | * Incrementally builds using the {@link org.eclipse.pde.api.tools.internal.provisional.builder.IApiAnalyzer} |
114 | * from the given {@link ApiAnalysisBuilder} |
115 | * |
116 | * @param baseline the baseline to compare with |
117 | * @param wbaseline the workspace baseline |
118 | * @param deltas the deltas to be built |
119 | * @param state the current JDT build state |
120 | * @param buildstate the current API tools build state |
121 | * @param monitor |
122 | * @throws CoreException |
123 | */ |
124 | public void build(IApiBaseline baseline, IApiBaseline wbaseline, IResourceDelta[] deltas, State state, BuildState buildstate, IProgressMonitor monitor) throws CoreException { |
125 | IProject project = this.builder.getProject(); |
126 | SubMonitor localmonitor = SubMonitor.convert(monitor, NLS.bind(BuilderMessages.IncrementalBuilder_builder_for_project, project.getName()), 1); |
127 | this.context = new BuildContext(); |
128 | try { |
129 | String[] projectNames = buildstate.getReexportedComponents(); |
130 | HashSet depprojects = null; |
131 | if (projectNames.length != 0) { |
132 | depprojects = new HashSet(); |
133 | IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); |
134 | IProject pj = null; |
135 | for (int i = 0, max = projectNames.length; i < max; i++) { |
136 | pj = root.getProject(projectNames[i]); |
137 | if (pj.isAccessible()) { |
138 | // select only projects that don't exist in the reference baseline |
139 | if (baseline != null && baseline.getApiComponent(projectNames[i]) == null) { |
140 | depprojects.add(pj); |
141 | } |
142 | } |
143 | } |
144 | } |
145 | |
146 | ResourceDeltaVisitor visitor = new ResourceDeltaVisitor(project, depprojects); |
147 | for (int i = 0; i < deltas.length; i++) { |
148 | deltas[i].accept(visitor); |
149 | } |
150 | build(project, baseline, wbaseline, state, buildstate, localmonitor.newChild(1)); |
151 | } |
152 | finally { |
153 | if(!localmonitor.isCanceled()) { |
154 | localmonitor.done(); |
155 | } |
156 | this.context.dispose(); |
157 | this.typenames.clear(); |
158 | this.packages.clear(); |
159 | } |
160 | } |
161 | |
162 | /** |
163 | * Builds an API delta using the default profile (from the workspace settings and the current |
164 | * @param project |
165 | * @param baseline the baseline to compare to |
166 | * @param wbaseline the current workspace baseline |
167 | * @param state the current JDT build state |
168 | * @param buildstate the current API tools build state |
169 | * @param monitor |
170 | */ |
171 | void build(final IProject project, final IApiBaseline baseline, final IApiBaseline wbaseline, final State state, BuildState buildstate, IProgressMonitor monitor) throws CoreException { |
172 | SubMonitor localmonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_on_0, 6); |
173 | try { |
174 | collectAffectedSourceFiles(project, state); |
175 | Util.updateMonitor(localmonitor, 1); |
176 | localmonitor.subTask(NLS.bind(BuilderMessages.ApiAnalysisBuilder_finding_affected_source_files, project.getName())); |
177 | Util.updateMonitor(localmonitor, 0); |
178 | if (this.context.hasTypes()) { |
179 | IPluginModelBase currentModel = this.builder.getCurrentModel(); |
180 | if (currentModel != null) { |
181 | String id = currentModel.getBundleDescription().getSymbolicName(); |
182 | IApiComponent comp = wbaseline.getApiComponent(id); |
183 | if(comp == null) { |
184 | return; |
185 | } |
186 | extClean(project, buildstate, localmonitor.newChild(1)); |
187 | Util.updateMonitor(localmonitor, 1); |
188 | this.builder.getAnalyzer().analyzeComponent(buildstate, |
189 | null, |
190 | null, |
191 | baseline, |
192 | comp, |
193 | this.context, |
194 | localmonitor.newChild(1)); |
195 | Util.updateMonitor(localmonitor, 1); |
196 | this.builder.createMarkers(); |
197 | Util.updateMonitor(localmonitor, 1); |
198 | } |
199 | } |
200 | } |
201 | finally { |
202 | if(localmonitor != null) { |
203 | localmonitor.done(); |
204 | } |
205 | } |
206 | } |
207 | |
208 | /** |
209 | * Records the type name from the given IFile as a changed type |
210 | * in the given build context |
211 | * @param file |
212 | */ |
213 | void addTypeToContext(IFile file) { |
214 | String type = resolveTypeName(file); |
215 | if(type == null) { |
216 | return; |
217 | } |
218 | if(!this.context.containsChangedType(type)) { |
219 | this.builder.cleanupMarkers(file); |
220 | //TODO implement detecting description changed types |
221 | this.context.recordStructurallyChangedType(type); |
222 | collectInnerTypes(file); |
223 | } |
224 | } |
225 | |
226 | /** |
227 | * Records the type name from the given IFile as a dependent type in the |
228 | * given build context |
229 | * @param file |
230 | */ |
231 | private void addDependentTypeToContext(IFile file) { |
232 | String type = resolveTypeName(file); |
233 | if(type == null) { |
234 | return; |
235 | } |
236 | if(!this.context.containsDependentType(type)) { |
237 | this.builder.cleanupMarkers(file); |
238 | this.context.recordDependentType(type); |
239 | collectInnerTypes(file); |
240 | } |
241 | } |
242 | |
243 | /** |
244 | * Collects the inner types from the compilation unit |
245 | * @param file |
246 | */ |
247 | private void collectInnerTypes(IFile file) { |
248 | ICompilationUnit unit = (ICompilationUnit) JavaCore.create(file); |
249 | IType[] types = null; |
250 | try { |
251 | types = unit.getAllTypes(); |
252 | String typename = null; |
253 | for (int i = 0; i < types.length; i++) { |
254 | typename = types[i].getFullyQualifiedName('$'); |
255 | if(this.context.containsChangedType(typename)) { |
256 | continue; |
257 | } |
258 | this.context.recordDependentType(typename); |
259 | } |
260 | } |
261 | catch(JavaModelException jme) { |
262 | //do nothing, just don't consider types |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * Collects the complete set of affected source files from the current project context based on the current JDT build state. |
268 | * |
269 | * @param project the current project being built |
270 | * @param state the current JDT build state |
271 | */ |
272 | void collectAffectedSourceFiles(final IProject project, State state) { |
273 | // the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are just 'X' |
274 | char[][][] internedQualifiedNames = ReferenceCollection.internQualifiedNames(this.packages); |
275 | // if a well known qualified name was found then we can skip over these |
276 | if (internedQualifiedNames.length < this.packages.elementSize) { |
277 | internedQualifiedNames = null; |
278 | } |
279 | char[][] internedSimpleNames = ReferenceCollection.internSimpleNames(this.typenames, true); |
280 | // if a well known name was found then we can skip over these |
281 | if (internedSimpleNames.length < this.typenames.elementSize) { |
282 | internedSimpleNames = null; |
283 | } |
284 | Object[] keyTable = state.getReferences().keyTable; |
285 | Object[] valueTable = state.getReferences().valueTable; |
286 | IFile file = null; |
287 | String typeLocator = null; |
288 | for (int i = 0; i < valueTable.length; i++) { |
289 | typeLocator = (String) keyTable[i]; |
290 | if (typeLocator != null) { |
291 | ReferenceCollection refs = (ReferenceCollection) valueTable[i]; |
292 | if (refs.includes(internedQualifiedNames, internedSimpleNames, null)) { |
293 | file = project.getFile(typeLocator); |
294 | if (file == null) { |
295 | continue; |
296 | } |
297 | if (ApiAnalysisBuilder.DEBUG) { |
298 | System.out.println(" adding affected source file " + file.getName()); //$NON-NLS-1$ |
299 | } |
300 | addDependentTypeToContext(file); |
301 | } |
302 | } |
303 | } |
304 | } |
305 | |
306 | /** |
307 | * Finds affected source files for a resource that has changed that either contains class files or is itself a class file |
308 | * @param binaryDelta |
309 | */ |
310 | void findAffectedSourceFiles(IResourceDelta binaryDelta) { |
311 | IResource resource = binaryDelta.getResource(); |
312 | if(resource.getType() == IResource.FILE) { |
313 | String typename = resolveTypeName(resource); |
314 | if(typename == null) { |
315 | return; |
316 | } |
317 | switch (binaryDelta.getKind()) { |
318 | case IResourceDelta.REMOVED : { |
319 | if (ApiAnalysisBuilder.DEBUG) { |
320 | System.out.println("Found removed class file " + typename); //$NON-NLS-1$ |
321 | } |
322 | //directly add the removed type |
323 | this.context.recordStructurallyChangedType(typename); |
324 | this.context.recordRemovedType(typename); |
325 | } |
326 | //$FALL-THROUGH$ |
327 | case IResourceDelta.ADDED : { |
328 | if (ApiAnalysisBuilder.DEBUG) { |
329 | System.out.println("Found added class file " + typename); //$NON-NLS-1$ |
330 | } |
331 | addDependentsOf(typename); |
332 | return; |
333 | } |
334 | case IResourceDelta.CHANGED : { |
335 | if ((binaryDelta.getFlags() & IResourceDelta.CONTENT) == 0) { |
336 | return; // skip it since it really isn't changed |
337 | } |
338 | if (ApiAnalysisBuilder.DEBUG) { |
339 | System.out.println("Found changed class file " + typename); //$NON-NLS-1$ |
340 | } |
341 | addDependentsOf(typename); |
342 | } |
343 | } |
344 | return; |
345 | } |
346 | } |
347 | |
348 | /** |
349 | * Adds a type to search for dependents of in considered projects for an incremental build |
350 | * |
351 | * @param path |
352 | */ |
353 | void addDependentsOf(String typename) { |
354 | // the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are just 'X' |
355 | int idx = typename.lastIndexOf('.'); |
356 | String packageName = (idx < 0 ? Util.EMPTY_STRING : typename.substring(0, idx)); |
357 | String typeName = (idx < 0 ? typename : typename.substring(idx+1, typename.length())); |
358 | idx = typeName.indexOf('$'); |
359 | if (idx > 0) { |
360 | typeName = typeName.substring(0, idx); |
361 | } |
362 | if (this.typenames.add(typeName) && this.packages.add(packageName) && ApiAnalysisBuilder.DEBUG) { |
363 | System.out.println(" will look for dependents of " + typeName + " in " + packageName); //$NON-NLS-1$ //$NON-NLS-2$ |
364 | } |
365 | } |
366 | |
367 | /** |
368 | * Returns an array of type names, and cleans up markers for the specified resource |
369 | * @param project the project being built |
370 | * @param state the current build state for the given project |
371 | * @param monitor |
372 | */ |
373 | void extClean(final IProject project, BuildState state, IProgressMonitor monitor) throws CoreException { |
374 | //clean up the state - https://bugs.eclipse.org/bugs/show_bug.cgi?id=271110 |
375 | String[] types = this.context.getRemovedTypes(); |
376 | for (int i = 0; i < types.length; i++) { |
377 | state.cleanup(types[i]); |
378 | } |
379 | Util.updateMonitor(monitor, 0); |
380 | IResource resource = project.findMember(ApiAnalysisBuilder.MANIFEST_PATH); |
381 | if (resource != null) { |
382 | try { |
383 | //TODO we should find a way to cache markers to type names, that way to get all |
384 | //the manifest markers for a given type name is time of O(1) |
385 | IMarker[] markers = resource.findMarkers(IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER, false, IResource.DEPTH_ZERO); |
386 | String tname = null; |
387 | for (int i = 0; i < markers.length; i++) { |
388 | tname = Util.getTypeNameFromMarker(markers[i]); |
389 | if(this.context.containsDependentType(tname) || this.context.containsChangedType(tname)) { |
390 | markers[i].delete(); |
391 | } |
392 | } |
393 | Util.updateMonitor(monitor, 0); |
394 | //TODO we should find a way to cache markers to type names, that way to get all |
395 | //the manifest markers for a given type name is time of O(1) |
396 | markers = resource.findMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, false, IResource.DEPTH_ZERO); |
397 | for (int i = 0; i < markers.length; i++) { |
398 | tname = Util.getTypeNameFromMarker(markers[i]); |
399 | if(this.context.containsDependentType(tname) || this.context.containsChangedType(tname)) { |
400 | markers[i].delete(); |
401 | } |
402 | } |
403 | Util.updateMonitor(monitor, 0); |
404 | } catch (CoreException e) { |
405 | ApiPlugin.log(e); |
406 | } |
407 | } |
408 | } |
409 | |
410 | /** |
411 | * Resolves the java path from the given resource |
412 | * @param resource |
413 | * @return the resolved path or <code>null</code> if the resource is not part of the java model |
414 | */ |
415 | String resolveTypeName(IResource resource) { |
416 | IPath typepath = resource.getFullPath(); |
417 | HashSet paths = null; |
418 | if(Util.isClassFile(resource.getName())) { |
419 | paths = (HashSet) this.builder.output_locs.get(resource.getProject()); |
420 | } |
421 | else if(Util.isJavaFileName(resource.getName())) { |
422 | paths = (HashSet) this.builder.src_locs.get(resource.getProject()); |
423 | } |
424 | if(paths != null) { |
425 | IPath path = null; |
426 | for (Iterator iterator = paths.iterator(); iterator.hasNext();) { |
427 | path = (IPath) iterator.next(); |
428 | if(path.isPrefixOf(typepath)) { |
429 | typepath = typepath.removeFirstSegments(path.segmentCount()).removeFileExtension(); |
430 | return typepath.toString().replace('/', '.'); |
431 | } |
432 | } |
433 | } |
434 | return null; |
435 | } |
436 | } |