1 /*******************************************************************************
2  * Copyright (c) 2009, 2018 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.pde.api.tools.ui.internal.use;
15 
16 import java.io.File;
17 import java.text.DateFormat;
18 import java.text.MessageFormat;
19 import java.util.ArrayList;
20 import java.util.Calendar;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Set;
24 import java.util.TreeSet;
25 import java.util.regex.Pattern;
26 
27 import org.eclipse.core.runtime.CoreException;
28 import org.eclipse.core.runtime.IPath;
29 import org.eclipse.core.runtime.IProgressMonitor;
30 import org.eclipse.core.runtime.IStatus;
31 import org.eclipse.core.runtime.OperationCanceledException;
32 import org.eclipse.core.runtime.Path;
33 import org.eclipse.core.runtime.Status;
34 import org.eclipse.core.runtime.SubMonitor;
35 import org.eclipse.core.runtime.URIUtil;
36 import org.eclipse.core.runtime.jobs.Job;
37 import org.eclipse.debug.core.ILaunchConfiguration;
38 import org.eclipse.osgi.service.resolver.ResolverError;
39 import org.eclipse.osgi.util.NLS;
40 import org.eclipse.pde.api.tools.internal.ApiBaselineManager;
41 import org.eclipse.pde.api.tools.internal.model.ApiModelFactory;
42 import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
43 import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
44 import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
45 import org.eclipse.pde.api.tools.internal.provisional.model.IApiElement;
46 import org.eclipse.pde.api.tools.internal.provisional.search.ApiSearchEngine;
47 import org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchReporter;
48 import org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchRequestor;
49 import org.eclipse.pde.api.tools.internal.search.ApiDescriptionModifier;
50 import org.eclipse.pde.api.tools.internal.search.ConsumerReportConvertor;
51 import org.eclipse.pde.api.tools.internal.search.SkippedComponent;
52 import org.eclipse.pde.api.tools.internal.search.UseMetadata;
53 import org.eclipse.pde.api.tools.internal.search.UseReportConverter;
54 import org.eclipse.pde.api.tools.internal.search.UseSearchRequestor;
55 import org.eclipse.pde.api.tools.internal.search.XmlSearchReporter;
56 import org.eclipse.pde.api.tools.internal.util.Util;
57 import org.eclipse.pde.api.tools.ui.internal.ApiUIPlugin;
58 import org.eclipse.pde.core.target.ITargetDefinition;
59 import org.eclipse.pde.core.target.ITargetHandle;
60 import org.eclipse.pde.core.target.ITargetPlatformService;
61 import org.eclipse.pde.core.target.TargetBundle;
62 import org.eclipse.ui.IEditorDescriptor;
63 import org.eclipse.ui.IWorkbenchWindow;
64 import org.eclipse.ui.PartInitException;
65 import org.eclipse.ui.PlatformUI;
66 import org.eclipse.ui.ide.IDE;
67 import org.eclipse.ui.progress.UIJob;
68 
69 /**
70  * Job that performs a API use scan.
71  */
72 public class ApiUseScanJob extends Job {
73 
74 	/**
75 	 * Associated launch configuration
76 	 */
77 	private ILaunchConfiguration configuration = null;
78 
79 	/**
80 	 * List of components that were not searched
81 	 */
82 	Set<SkippedComponent> notsearched = null;
83 
84 	/**
85 	 * @param name
86 	 */
ApiUseScanJob(ILaunchConfiguration configuration)87 	public ApiUseScanJob(ILaunchConfiguration configuration) {
88 		super(MessageFormat.format(Messages.ApiUseScanJob_api_use_report, configuration.getName()));
89 		this.configuration = configuration;
90 	}
91 
92 	@Override
run(IProgressMonitor monitor)93 	protected IStatus run(IProgressMonitor monitor) {
94 		// Build API baseline
95 		SubMonitor localmonitor = SubMonitor.convert(monitor);
96 		try {
97 			IPath rootpath = null;
98 			String xmlPath = this.configuration.getAttribute(ApiUseLaunchDelegate.REPORT_PATH, (String) null);
99 			if (xmlPath == null) {
100 				abort(Messages.ApiUseScanJob_missing_xml_loc);
101 			}
102 			rootpath = new Path(xmlPath);
103 			int kind = this.configuration.getAttribute(ApiUseLaunchDelegate.TARGET_KIND, 0);
104 			if (kind != ApiUseLaunchDelegate.KIND_HTML_ONLY) {
105 				localmonitor.setTaskName(Messages.ApiUseScanJob_preparing_for_scan);
106 				localmonitor.setWorkRemaining((isSpecified(ApiUseLaunchDelegate.CREATE_HTML) ? 14 : 13));
107 				// create baseline
108 				IApiBaseline baseline = createApiBaseline(kind, localmonitor.split(1));
109 				Set<String> ids = new HashSet<>();
110 				TreeSet<IApiComponent> scope = new TreeSet<>(Util.componentsorter);
111 				getContext(baseline, ids, scope, localmonitor.split(2));
112 				int kinds = 0;
113 				if (isSpecified(ApiUseLaunchDelegate.MOD_API_REFERENCES)) {
114 					kinds |= IApiSearchRequestor.INCLUDE_API;
115 				}
116 				if (isSpecified(ApiUseLaunchDelegate.MOD_INTERNAL_REFERENCES)) {
117 					kinds |= IApiSearchRequestor.INCLUDE_INTERNAL;
118 				}
119 				if (isSpecified(ApiUseLaunchDelegate.MOD_ILLEGAL_USE)) {
120 					kinds |= IApiSearchRequestor.INCLUDE_ILLEGAL_USE;
121 				}
122 				UseSearchRequestor requestor = new UseSearchRequestor(ids, scope.toArray(new IApiElement[scope.size()]), kinds);
123 				List<String> jars = this.configuration.getAttribute(ApiUseLaunchDelegate.JAR_PATTERNS_LIST, (List<String>) null);
124 				String[] sjars = getStrings(jars);
125 				requestor.setJarPatterns(sjars);
126 				List<String> api = this.configuration.getAttribute(ApiUseLaunchDelegate.API_PATTERNS_LIST, (List<String>) null);
127 				String[] sapi = getStrings(api);
128 				String filterRoot = this.configuration.getAttribute(ApiUseLaunchDelegate.FILTER_ROOT, (String) null);
129 				requestor.setFilterRoot(filterRoot);
130 				List<String> internal = this.configuration.getAttribute(ApiUseLaunchDelegate.INTERNAL_PATTERNS_LIST, (List<String>) null);
131 				String[] sinternal = getStrings(internal);
132 				if (sapi != null || sinternal != null) {
133 					// modify API descriptions
134 					IApiComponent[] components = baseline.getApiComponents();
135 					ApiDescriptionModifier visitor = new ApiDescriptionModifier(sinternal, sapi);
136 					for (IApiComponent component : components) {
137 						if (!component.isSystemComponent() && !component.isSourceComponent()) {
138 							visitor.setApiDescription(component.getApiDescription());
139 							component.getApiDescription().accept(visitor, null);
140 						}
141 					}
142 				}
143 				xmlPath = rootpath.append("xml").toOSString(); //$NON-NLS-1$
144 				if (isSpecified(ApiUseLaunchDelegate.CLEAN_XML)) {
145 					localmonitor.setTaskName(Messages.ApiUseScanJob_cleaning_xml_loc);
146 					scrubReportLocation(new File(xmlPath), localmonitor.split(1));
147 				}
148 				UseMetadata data = new UseMetadata(kinds, this.configuration.getAttribute(ApiUseLaunchDelegate.TARGET_SCOPE, (String) null), this.configuration.getAttribute(ApiUseLaunchDelegate.SEARCH_SCOPE, (String) null), baseline.getLocation(), xmlPath, sapi, sinternal, sjars, this.configuration.getAttribute(ApiUseLaunchDelegate.FILTER_ROOT, (String) null), DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime()), this.configuration.getAttribute(ApiUseLaunchDelegate.DESCRIPTION, (String) null));
149 				IApiSearchReporter reporter = new XmlSearchReporter(xmlPath, false);
150 				try {
151 					ApiSearchEngine engine = new ApiSearchEngine();
152 					engine.search(baseline, requestor, reporter, localmonitor.split(6));
153 				} finally {
154 					reporter.reportNotSearched(ApiUseScanJob.this.notsearched.toArray(new IApiElement[ApiUseScanJob.this.notsearched.size()]));
155 					reporter.reportMetadata(data);
156 					reporter.reportCounts();
157 					// Dispose the baseline if it's not managed (it's temporary)
158 					ApiBaselineManager apiManager = ApiBaselineManager.getManager();
159 					IApiBaseline[] baselines = apiManager.getApiBaselines();
160 					boolean dispose = true;
161 					for (IApiBaseline baseline2 : baselines) {
162 						if (baseline.equals(baseline2)) {
163 							dispose = false;
164 							break;
165 						}
166 					}
167 					if (dispose) {
168 						baseline.dispose();
169 					}
170 				}
171 			} else {
172 				localmonitor.setWorkRemaining(10);
173 			}
174 			if (isSpecified(ApiUseLaunchDelegate.CREATE_HTML)) {
175 				localmonitor.setTaskName(Messages.ApiUseScanJob_generating_html_reports);
176 				String htmlPath = rootpath.append("html").toOSString(); //$NON-NLS-1$
177 
178 				int reportType = ApiUseLaunchDelegate.REPORT_KIND_PRODUCER;
179 				if (this.configuration.getAttribute(ApiUseLaunchDelegate.REPORT_TYPE, ApiUseLaunchDelegate.REPORT_KIND_PRODUCER) == ApiUseLaunchDelegate.REPORT_KIND_CONSUMER) {
180 					reportType = ApiUseLaunchDelegate.REPORT_KIND_CONSUMER;
181 				}
182 
183 				performReportCreation(reportType, isSpecified(ApiUseLaunchDelegate.CLEAN_HTML), htmlPath, xmlPath, isSpecified(ApiUseLaunchDelegate.DISPLAY_REPORT), getStrings(this.configuration.getAttribute(ApiUseLaunchDelegate.REPORT_TO_PATTERNS_LIST, (List<String>) null)), getStrings(this.configuration.getAttribute(ApiUseLaunchDelegate.REPORT_PATTERNS_LIST, (List<String>) null)), localmonitor.split(10));
184 			}
185 
186 		} catch (CoreException e) {
187 			return e.getStatus();
188 		} finally {
189 			localmonitor.done();
190 		}
191 		return Status.OK_STATUS;
192 	}
193 
getStrings(List<String> list)194 	private String[] getStrings(List<String> list) {
195 		if (list == null || list.isEmpty()) {
196 			return null;
197 		}
198 		return list.toArray(new String[list.size()]);
199 	}
200 
201 	/**
202 	 * Throws a new {@link CoreException} with the given message
203 	 *
204 	 * @param message
205 	 * @throws CoreException
206 	 */
abort(String message)207 	void abort(String message) throws CoreException {
208 		throw new CoreException(new Status(IStatus.ERROR, ApiUIPlugin.PLUGIN_ID, message));
209 	}
210 
211 	/**
212 	 * Creates a new {@link IApiBaseline} from the location set in the backing
213 	 * launch configuration
214 	 *
215 	 * @param kind
216 	 * @param monitor
217 	 * @return the new {@link IApiBaseline}
218 	 * @throws CoreException
219 	 */
createApiBaseline(int kind, IProgressMonitor monitor)220 	private IApiBaseline createApiBaseline(int kind, IProgressMonitor monitor) throws CoreException {
221 		ApiBaselineManager bmanager = ApiBaselineManager.getManager();
222 		switch (kind) {
223 			case ApiUseLaunchDelegate.KIND_API_BASELINE:
224 				String name = this.configuration.getAttribute(ApiUseLaunchDelegate.BASELINE_NAME, (String) null);
225 				if (name == null) {
226 					abort(Messages.ApiUseScanJob_baseline_name_missing);
227 				}
228 				IApiBaseline baseline = bmanager.getApiBaseline(name);
229 				if (baseline == null) {
230 					abort(MessageFormat.format(Messages.ApiUseScanJob_baseline_does_not_exist, name));
231 				}
232 				return baseline;
233 			case ApiUseLaunchDelegate.KIND_INSTALL_PATH:
234 				String path = this.configuration.getAttribute(ApiUseLaunchDelegate.INSTALL_PATH, (String) null);
235 				if (path == null) {
236 					abort(Messages.ApiUseScanJob_unspecified_install_path);
237 				}
238 				File file = new File(path);
239 				if (!file.exists() || !file.isDirectory()) {
240 					abort(MessageFormat.format(Messages.ApiUseScanJob_intall_dir_does_no_exist, path));
241 				}
242 				return createBaseline(path, monitor);
243 			case ApiUseLaunchDelegate.KIND_TARGET_DEFINITION:
244 				String memento = this.configuration.getAttribute(ApiUseLaunchDelegate.TARGET_HANDLE, (String) null);
245 				if (memento == null) {
246 					abort(Messages.ApiUseScanJob_target_unspecified);
247 				}
248 				ITargetPlatformService service = getTargetService();
249 				ITargetHandle handle = service.getTarget(memento);
250 				ITargetDefinition definition = handle.getTargetDefinition();
251 				return createBaseline(definition, monitor);
252 			default:
253 				abort(Messages.ApiUseScanJob_target_api_unspecified);
254 		}
255 		return null;
256 	}
257 
258 	/**
259 	 * Returns the target service or <code>null</code>
260 	 *
261 	 * @return service or <code>null</code>
262 	 */
getTargetService()263 	private ITargetPlatformService getTargetService() {
264 		return ApiPlugin.getDefault().acquireService(ITargetPlatformService.class);
265 	}
266 
267 	/**
268 	 * Collects the context of reference ids and scope elements in one pass
269 	 *
270 	 * @param baseline the baseline to check components from
271 	 * @param ids the reference ids to consider
272 	 * @param scope the scope of elements to search
273 	 * @param monitor
274 	 * @throws CoreException
275 	 */
getContext(IApiBaseline baseline, Set<String> ids, Set<IApiComponent> scope, IProgressMonitor monitor)276 	private void getContext(IApiBaseline baseline, Set<String> ids, Set<IApiComponent> scope, IProgressMonitor monitor) throws CoreException {
277 		SubMonitor localmonitor = SubMonitor.convert(monitor, Messages.ApiUseScanJob_collecting_target_components, 10);
278 		this.notsearched = new TreeSet<>(Util.componentsorter);
279 		String regex = this.configuration.getAttribute(ApiUseLaunchDelegate.TARGET_SCOPE, (String) null);
280 		Pattern pattern = getPattern(regex);
281 		regex = this.configuration.getAttribute(ApiUseLaunchDelegate.SEARCH_SCOPE, (String) null);
282 		Pattern pattern2 = getPattern(regex);
283 		IApiComponent[] components = baseline.getApiComponents();
284 		localmonitor.setWorkRemaining(components.length);
285 		for (IApiComponent component : components) {
286 			localmonitor.subTask(NLS.bind(Messages.ApiUseScanJob_checking_component, component.getSymbolicName()));
287 			localmonitor.split(1);
288 			if (acceptComponent(component, pattern, true)) {
289 				ids.add(component.getSymbolicName());
290 			}
291 			if (acceptComponent(component, pattern2, false)) {
292 				scope.add(component);
293 			} else {
294 				localmonitor.subTask(NLS.bind(Messages.ApiUseScanJob_skipping_component, component.getSymbolicName()));
295 				this.notsearched.add(new SkippedComponent(component.getSymbolicName(), component.getVersion(), null));
296 			}
297 		}
298 	}
299 
300 	/**
301 	 * Returns a pattern for the given regular expression or <code>null</code>
302 	 * if none.
303 	 *
304 	 * @param regex expression, <code>null</code> or empty
305 	 * @return associated pattern or <code>null</code>
306 	 */
getPattern(String regex)307 	private Pattern getPattern(String regex) {
308 		if (regex == null) {
309 			return null;
310 		}
311 		if (regex.trim().length() == 0) {
312 			return null;
313 		}
314 		return Pattern.compile(regex);
315 	}
316 
317 	/**
318 	 * Returns if we should add the given component to our search scope
319 	 *
320 	 * @param component
321 	 * @param pattern
322 	 * @param allowresolve
323 	 * @return
324 	 * @throws CoreException
325 	 */
acceptComponent(IApiComponent component, Pattern pattern, boolean allowresolve)326 	boolean acceptComponent(IApiComponent component, Pattern pattern, boolean allowresolve) throws CoreException {
327 		if (!allowresolve) {
328 			ResolverError[] errors = component.getErrors();
329 			if (errors != null) {
330 				this.notsearched.add(new SkippedComponent(component.getSymbolicName(), component.getVersion(), errors));
331 				return false;
332 			}
333 		}
334 		if (component.isSystemComponent()) {
335 			return false;
336 		}
337 		if (pattern != null) {
338 			return pattern.matcher(component.getSymbolicName()).matches();
339 		}
340 		return true;
341 	}
342 
343 	/**
344 	 * Returns if the given search modifier is set in the backing
345 	 * {@link ILaunchConfiguration}
346 	 *
347 	 * @param modifier
348 	 * @return true if the modifier is set, false otherwise
349 	 * @throws CoreException
350 	 */
isSpecified(int modifier)351 	private boolean isSpecified(int modifier) throws CoreException {
352 		int modifiers = configuration.getAttribute(ApiUseLaunchDelegate.SEARCH_MODIFIERS, 0);
353 		return (modifiers & modifier) > 0;
354 	}
355 
356 	/**
357 	 * Performs the report creation
358 	 *
359 	 * @param reportType what report converter to use, either
360 	 *            {@link ApiUseLaunchDelegate#REPORT_KIND_PRODUCER} or
361 	 *            {@link ApiUseLaunchDelegate#REPORT_KIND_CONSUMER}
362 	 * @param cleanh
363 	 * @param hlocation
364 	 * @param rlocation
365 	 * @param openhtml
366 	 * @param monitor
367 	 * @throws OperationCanceledException
368 	 */
performReportCreation(int reportType, boolean cleanh, String hlocation, String rlocation, boolean openhtml, String[] topatterns, String[] frompatterns, IProgressMonitor monitor)369 	void performReportCreation(int reportType, boolean cleanh, String hlocation, String rlocation, boolean openhtml, String[] topatterns, String[] frompatterns, IProgressMonitor monitor) {
370 		SubMonitor localmonitor = SubMonitor.convert(monitor, Messages.ApiUseScanJob_creating_html_reports, 10);
371 		if (cleanh) {
372 			cleanReportLocation(hlocation, localmonitor.split(5));
373 		}
374 		try {
375 			UseReportConverter converter = null;
376 			if (reportType == ApiUseLaunchDelegate.REPORT_KIND_CONSUMER) {
377 				converter = new ConsumerReportConvertor(hlocation, rlocation, topatterns, frompatterns);
378 			} else {
379 				converter = new UseReportConverter(hlocation, rlocation, topatterns, frompatterns);
380 			}
381 
382 			converter.convert(null, localmonitor.split(5));
383 			if (openhtml) {
384 				final File index = converter.getReportIndex();
385 				if (index != null) {
386 					UIJob ujob = new UIJob(Util.EMPTY_STRING) {
387 						@Override
388 						public IStatus runInUIThread(IProgressMonitor monitor) {
389 							IEditorDescriptor edesc = null;
390 							try {
391 								edesc = IDE.getEditorDescriptor(index.getName());
392 								IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
393 								IDE.openEditor(window.getActivePage(), index.toURI(), edesc.getId(), true);
394 							} catch (PartInitException e) {
395 								e.printStackTrace();
396 							}
397 							return Status.OK_STATUS;
398 						}
399 					};
400 					ujob.setPriority(Job.INTERACTIVE);
401 					ujob.setSystem(true);
402 					ujob.schedule();
403 				}
404 			}
405 		} catch (OperationCanceledException oce) {
406 			// re-throw
407 			throw oce;
408 		} catch (Exception e) {
409 			ApiPlugin.log(e);
410 		}
411 	}
412 
413 	/**
414 	 * Cleans the report location specified by the parameter reportLocation
415 	 *
416 	 * @param monitor
417 	 */
cleanReportLocation(String location, IProgressMonitor monitor)418 	void cleanReportLocation(String location, IProgressMonitor monitor) {
419 		File file = new File(location);
420 		SubMonitor localmonitor = SubMonitor.convert(monitor, Messages.ApiUseScanJob_deleting_old_reports, 1);
421 		if (file.exists()) {
422 			scrubReportLocation(file, localmonitor.split(1));
423 			localmonitor.subTask(NLS.bind(Messages.ApiUseScanJob_deleting_root_folder, file.getName()));
424 		}
425 	}
426 
427 	/**
428 	 * Cleans the location if it exists
429 	 *
430 	 * @param file
431 	 * @param monitor
432 	 */
scrubReportLocation(File file, IProgressMonitor monitor)433 	void scrubReportLocation(File file, IProgressMonitor monitor) {
434 		SubMonitor subMonitor = SubMonitor.convert(monitor);
435 		if (file.exists() && file.isDirectory()) {
436 			File[] files = file.listFiles();
437 			if (files != null) {
438 				subMonitor.setWorkRemaining(files.length);
439 				for (File file2 : files) {
440 					SubMonitor iterationMonitor = subMonitor.split(1);
441 					iterationMonitor.subTask(NLS.bind(Messages.ApiUseScanJob_deleteing_file, file2.getPath()));
442 					if (file2.isDirectory()) {
443 						scrubReportLocation(file2, iterationMonitor);
444 					} else {
445 						file2.delete();
446 					}
447 				}
448 			}
449 			file.delete();
450 		}
451 	}
452 
453 	/**
454 	 * Creates an API baseline from a target definition.
455 	 *
456 	 * @param definition
457 	 * @param monitor progress monitor
458 	 */
createBaseline(ITargetDefinition definition, IProgressMonitor monitor)459 	private IApiBaseline createBaseline(ITargetDefinition definition, IProgressMonitor monitor) throws CoreException {
460 		SubMonitor localmonitor = SubMonitor.convert(monitor, Messages.ApiUseScanJob_reading_target, 10);
461 		definition.resolve(localmonitor.split(2));
462 		TargetBundle[] bundles = definition.getBundles();
463 		List<IApiComponent> components = new ArrayList<>();
464 		IApiBaseline profile = ApiModelFactory.newApiBaseline(definition.getName());
465 		localmonitor.setWorkRemaining(bundles.length);
466 		for (int i = 0; i < bundles.length; i++) {
467 			localmonitor.split(1);
468 			if (bundles[i].getStatus().isOK() && !bundles[i].isSourceBundle()) {
469 				IApiComponent component = ApiModelFactory.newApiComponent(profile, URIUtil.toFile(bundles[i].getBundleInfo().getLocation()).getAbsolutePath());
470 				if (component != null) {
471 					components.add(component);
472 				}
473 			}
474 		}
475 		profile.addApiComponents(components.toArray(new IApiComponent[components.size()]));
476 		return profile;
477 	}
478 
479 	/**
480 	 * Creates a baseline at an install location
481 	 *
482 	 * @param path
483 	 * @param monitor
484 	 */
createBaseline(String installLocation, IProgressMonitor monitor)485 	private IApiBaseline createBaseline(String installLocation, IProgressMonitor monitor) throws CoreException {
486 		SubMonitor localmonitor = SubMonitor.convert(monitor, Messages.ApiUseScanJob_scanning, 10);
487 		IApiBaseline baseline = ApiModelFactory.newApiBaseline(this.configuration.getName());
488 		IApiComponent[] components = ApiModelFactory.addComponents(baseline, installLocation, localmonitor);
489 		if (components.length == 0) {
490 			abort(MessageFormat.format(Messages.ApiUseScanJob_no_bundles, installLocation));
491 		}
492 		return baseline;
493 	}
494 
495 }
496