1 /*******************************************************************************
2  * Copyright (c) 2010, 2019 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.preferences;
15 
16 import java.io.File;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.Iterator;
21 
22 import org.eclipse.core.resources.IProject;
23 import org.eclipse.core.resources.IResource;
24 import org.eclipse.core.runtime.CoreException;
25 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
26 import org.eclipse.core.runtime.preferences.InstanceScope;
27 import org.eclipse.jface.dialogs.Dialog;
28 import org.eclipse.jface.dialogs.MessageDialog;
29 import org.eclipse.jface.preference.PreferencePage;
30 import org.eclipse.jface.resource.ImageDescriptor;
31 import org.eclipse.jface.viewers.ArrayContentProvider;
32 import org.eclipse.jface.viewers.CheckboxTableViewer;
33 import org.eclipse.jface.viewers.ColumnLabelProvider;
34 import org.eclipse.jface.viewers.IStructuredSelection;
35 import org.eclipse.jface.viewers.StructuredSelection;
36 import org.eclipse.osgi.util.NLS;
37 import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
38 import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
39 import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants;
40 import org.eclipse.pde.api.tools.internal.search.UseScanManager;
41 import org.eclipse.pde.api.tools.internal.util.Util;
42 import org.eclipse.pde.api.tools.ui.internal.ApiUIPlugin;
43 import org.eclipse.pde.api.tools.ui.internal.IApiToolsHelpContextIds;
44 import org.eclipse.pde.api.tools.ui.internal.SWTFactory;
45 import org.eclipse.swt.SWT;
46 import org.eclipse.swt.events.KeyListener;
47 import org.eclipse.swt.events.SelectionListener;
48 import org.eclipse.swt.graphics.Image;
49 import org.eclipse.swt.layout.GridData;
50 import org.eclipse.swt.widgets.Button;
51 import org.eclipse.swt.widgets.Composite;
52 import org.eclipse.swt.widgets.Control;
53 import org.eclipse.swt.widgets.DirectoryDialog;
54 import org.eclipse.swt.widgets.FileDialog;
55 import org.eclipse.swt.widgets.Table;
56 import org.eclipse.ui.IWorkbench;
57 import org.eclipse.ui.IWorkbenchPreferencePage;
58 import org.eclipse.ui.PlatformUI;
59 import org.eclipse.ui.dialogs.PreferenceLinkArea;
60 import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer;
61 import org.eclipse.ui.preferences.IWorkingCopyManager;
62 import org.eclipse.ui.preferences.WorkingCopyManager;
63 import org.osgi.service.prefs.BackingStoreException;
64 
65 /**
66  * Preference page to allow users to add use scans. The use scans are analyzed
67  * in the API Tools builder to see if any methods found in the scan have been
68  * removed.
69  *
70  * @since 3.7
71  */
72 public class ApiUseScanPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
73 
74 	public static final String ID = "org.eclipse.pde.api.tools.ui.apiusescan.prefpage"; //$NON-NLS-1$
75 
76 	private IWorkingCopyManager fManager;
77 	CheckboxTableViewer fTableViewer;
78 	HashSet<String> fLocationList = new HashSet<>();
79 	Button remove = null;
80 	Button editbutton = null;
81 
82 	/**
83 	 * Column provider for the use scan table
84 	 */
85 	class TableColumnLabelProvider extends ColumnLabelProvider {
86 
87 		Image archive = null;
88 
89 		@Override
dispose()90 		public void dispose() {
91 			if (archive != null) {
92 				archive.dispose();
93 			}
94 			super.dispose();
95 		}
96 
97 		@Override
getImage(Object element)98 		public Image getImage(Object element) {
99 			File file = new File(element.toString());
100 			if (file.isDirectory()) {
101 				return PlatformUI.getWorkbench().getSharedImages().getImage(org.eclipse.ui.ISharedImages.IMG_OBJ_FOLDER);
102 			}
103 			if (archive == null) {
104 				ImageDescriptor image = PlatformUI.getWorkbench().getEditorRegistry().getImageDescriptor(file.getName());
105 				archive = image.createImage();
106 			}
107 			return archive;
108 		}
109 	}
110 
111 	@Override
init(IWorkbench workbench)112 	public void init(IWorkbench workbench) {
113 	}
114 
115 	@Override
createContents(Composite parent)116 	protected Control createContents(Composite parent) {
117 		Composite comp = SWTFactory.createComposite(parent, 2, 1, GridData.FILL_HORIZONTAL, 0, 0);
118 
119 		SWTFactory.createWrapLabel(comp, PreferenceMessages.ApiUseScanPreferencePage_0, 2, 250);
120 		SWTFactory.createVerticalSpacer(comp, 1);
121 		SWTFactory.createWrapLabel(comp, PreferenceMessages.ApiUseScanPreferencePage_2, 2);
122 
123 		Table table = new Table(comp, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER | SWT.CHECK | SWT.V_SCROLL);
124 		table.setLayoutData(new GridData(GridData.FILL_BOTH));
125 		GridData gd = (GridData) table.getLayoutData();
126 		gd.widthHint = 250;
127 		table.addKeyListener(KeyListener.keyReleasedAdapter(e -> {
128 			if (e.stateMask == SWT.NONE && e.keyCode == SWT.DEL) {
129 				removeLocation();
130 			}
131 		}));
132 		fTableViewer = new CheckboxTableViewer(table);
133 		fTableViewer.setLabelProvider(new TableColumnLabelProvider());
134 		fTableViewer.setContentProvider(ArrayContentProvider.getInstance());
135 
136 		Composite bcomp = SWTFactory.createComposite(comp, 1, 1, GridData.FILL_VERTICAL | GridData.VERTICAL_ALIGN_BEGINNING, 0, 0);
137 		Button button = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_3, null);
138 		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> select(true)));
139 		button = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_10, null);
140 		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> select(false)));
141 
142 		SWTFactory.createHorizontalSpacer(bcomp, 1);
143 
144 		button = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_4, null);
145 		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
146 			String loc = getDirectory(null);
147 			if (loc != null) {
148 				String exactLocation = UseScanManager.getExactScanLocation(loc);
149 				if (exactLocation == null) {
150 					addLocation(loc);
151 				} else {
152 					addLocation(exactLocation);
153 				}
154 			}
155 		}));
156 		button = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_5, null);
157 		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
158 			String loc = getArchive(null);
159 			if (loc != null) {
160 				addLocation(loc);
161 			}
162 		}));
163 
164 		editbutton = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_1, null);
165 		editbutton.setEnabled(false);
166 		editbutton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> edit()));
167 		remove = SWTFactory.createPushButton(bcomp, PreferenceMessages.ApiUseScanPreferencePage_6, null);
168 		remove.setEnabled(false);
169 		remove.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> removeLocation()));
170 
171 		fTableViewer.addSelectionChangedListener(event -> {
172 			IStructuredSelection selection = fTableViewer.getStructuredSelection();
173 			remove.setEnabled(!selection.isEmpty());
174 			editbutton.setEnabled(selection.size() == 1);
175 		});
176 		fTableViewer.addDoubleClickListener(event -> edit());
177 
178 		HashMap<String, Object> linkdata = new HashMap<>();
179 		linkdata.put(ApiErrorsWarningsPreferencePage.INITIAL_TAB, Integer.valueOf(ApiErrorsWarningsConfigurationBlock.API_USE_SCANS_PAGE_ID));
180 		PreferenceLinkArea apiErrorLinkArea = new PreferenceLinkArea(comp, SWT.NONE, ApiErrorsWarningsPreferencePage.ID, PreferenceMessages.ApiUseScanPreferencePage_9, (IWorkbenchPreferenceContainer) getContainer(), linkdata);
181 		GridData data = new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL);
182 		data.widthHint = 250;
183 		data.horizontalSpan = 2;
184 		apiErrorLinkArea.getControl().setLayoutData(data);
185 
186 		PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, IApiToolsHelpContextIds.APIUSESCANS_PREF_PAGE);
187 		performInit();
188 		validateScans();
189 		Dialog.applyDialogFont(comp);
190 		return comp;
191 	}
192 
193 	/**
194 	 * Selects (checks) all of the entries in the table
195 	 *
196 	 * @param checked
197 	 */
select(boolean checked)198 	void select(boolean checked) {
199 		fTableViewer.setAllChecked(checked);
200 		fTableViewer.refresh();
201 	}
202 
203 	/**
204 	 * Allows users to select a directory with a use scan in it
205 	 *
206 	 * @param prevLocation
207 	 * @return the new directory or <code>null</code> if the dialog was
208 	 *         cancelled
209 	 */
getDirectory(String prevLocation)210 	String getDirectory(String prevLocation) {
211 		DirectoryDialog dialog = new DirectoryDialog(getShell());
212 		dialog.setMessage(PreferenceMessages.ApiUseScanPreferencePage_7);
213 		if (prevLocation != null) {
214 			dialog.setFilterPath(prevLocation);
215 		}
216 		return dialog.open();
217 	}
218 
219 	/**
220 	 * Allows the user to select an archive from the file system
221 	 *
222 	 * @param file a starting file
223 	 * @return the path to the new archive or <code>null</code> if cancelled
224 	 */
getArchive(File file)225 	String getArchive(File file) {
226 		FileDialog dialog = new FileDialog(getShell(), SWT.OPEN);
227 		dialog.setFilterNames(new String[] { PreferenceMessages.archives__zip });
228 		dialog.setFilterExtensions(new String[] { "*.zip;*.jar" }); //$NON-NLS-1$
229 		if (file != null) {
230 			dialog.setFilterPath(file.getParent());
231 			dialog.setFileName(file.getName());
232 		}
233 		return dialog.open();
234 	}
235 
236 	/**
237 	 * Adds the given location to the table
238 	 *
239 	 * @param location
240 	 */
addLocation(String location)241 	void addLocation(String location) {
242 		fLocationList.add(location);
243 		fTableViewer.refresh();
244 		fTableViewer.setChecked(location, true);
245 		fTableViewer.setSelection(new StructuredSelection(location));
246 		// do the whole pass in case you have more than one invalid location
247 		validateScans();
248 	}
249 
250 	/**
251 	 * Allows you to edit the location of the scan
252 	 */
edit()253 	void edit() {
254 		IStructuredSelection selection = fTableViewer.getStructuredSelection();
255 		String location = selection.getFirstElement().toString();
256 		File file = new File(location);
257 		String newloc = null;
258 		if (file.isDirectory()) {
259 			newloc = getDirectory(location);
260 		} else {
261 			newloc = getArchive(file);
262 		}
263 		if (newloc != null) {
264 			fLocationList.remove(location);
265 			addLocation(newloc);
266 		}
267 	}
268 
269 	/**
270 	 * Removes the selected locations
271 	 */
removeLocation()272 	void removeLocation() {
273 		IStructuredSelection selection = fTableViewer.getStructuredSelection();
274 		fLocationList.removeAll(selection.toList());
275 		fTableViewer.refresh();
276 		validateScans();
277 	}
278 
279 	/**
280 	 * Validates that the scan are all still valid
281 	 */
validateScans()282 	private void validateScans() {
283 		if (fLocationList.size() > 0) {
284 			String loc = null;
285 			for (Iterator<String> iterator = fLocationList.iterator(); iterator.hasNext();) {
286 				loc = iterator.next();
287 				if (!UseScanManager.isValidScanLocation(loc)) {
288 					setErrorMessage(NLS.bind(PreferenceMessages.ApiUseScanPreferencePage_8, loc));
289 					setValid(false);
290 					return;
291 				}
292 			}
293 		}
294 		setValid(true);
295 		setErrorMessage(null);
296 	}
297 
298 	@Override
performOk()299 	public boolean performOk() {
300 		applyChanges();
301 		return true;
302 	}
303 
304 	@Override
performApply()305 	protected void performApply() {
306 		applyChanges();
307 	}
308 
309 	@Override
performDefaults()310 	protected void performDefaults() {
311 		fLocationList.clear();
312 		fTableViewer.refresh();
313 		setValid(true);
314 		setErrorMessage(null);
315 		super.performDefaults();
316 	}
317 
318 	/**
319 	 * Initializes the page
320 	 */
performInit()321 	private void performInit() {
322 		if (getContainer() == null) {
323 			fManager = new WorkingCopyManager();
324 		} else {
325 			fManager = ((IWorkbenchPreferenceContainer) getContainer()).getWorkingCopyManager();
326 		}
327 
328 		fLocationList.clear();
329 
330 		String location = getStoredValue(IApiCoreConstants.API_USE_SCAN_LOCATION, null);
331 
332 		ArrayList<String> checkedLocations = new ArrayList<>();
333 		if (location != null && location.length() > 0) {
334 			String[] locations = location.split(UseScanManager.ESCAPE_REGEX + UseScanManager.LOCATION_DELIM);
335 			for (String locationString : locations) {
336 				String values[] = locationString.split(UseScanManager.ESCAPE_REGEX + UseScanManager.STATE_DELIM);
337 				fLocationList.add(values[0]);
338 				if (Boolean.parseBoolean(values[1])) {
339 					checkedLocations.add(values[0]);
340 				}
341 			}
342 			fLocationList.remove(""); //$NON-NLS-1$
343 		}
344 		fTableViewer.setInput(fLocationList);
345 		fTableViewer.setCheckedElements(checkedLocations.toArray(new String[checkedLocations.size()]));
346 		fTableViewer.refresh();
347 
348 		setErrorMessage(null);
349 	}
350 
351 	/**
352 	 * Save changes to the preferences
353 	 */
applyChanges()354 	private void applyChanges() {
355 		StringBuilder locations = new StringBuilder();
356 		for (String string : fLocationList) {
357 			Object location = string;
358 			locations.append(location);
359 			locations.append(UseScanManager.STATE_DELIM);
360 			locations.append(fTableViewer.getChecked(location));
361 			locations.append(UseScanManager.LOCATION_DELIM);
362 		}
363 
364 		Object[] newCheckedLocations = fTableViewer.getCheckedElements();
365 		boolean hasLocationChanges = hasLocationsChanges(locations.toString());
366 		if (hasLocationChanges && newCheckedLocations.length != 0) {
367 			IProject[] projects = Util.getApiProjects();
368 			// If there are API projects in the workspace, ask the user if they
369 			// should be cleaned and built to run the new tooling
370 			if (projects != null) {
371 				if (MessageDialog.openQuestion(getShell(), PreferenceMessages.ApiUseScanPreferencePage_11, PreferenceMessages.ApiUseScanPreferencePage_12)) {
372 					Util.getBuildJob(projects).schedule();
373 				}
374 			}
375 		}
376 		if (hasLocationChanges && newCheckedLocations.length == 0) {
377 			findAndDeleteAPIUseScanMarkers();
378 		}
379 
380 		setStoredValue(IApiCoreConstants.API_USE_SCAN_LOCATION, locations.toString());
381 
382 		try {
383 			fManager.applyChanges();
384 		} catch (BackingStoreException e) {
385 			ApiUIPlugin.log(e);
386 		}
387 	}
388 
findAndDeleteAPIUseScanMarkers()389 	private void findAndDeleteAPIUseScanMarkers() {
390 		IProject[] apiProjects = Util.getApiProjects();
391 		if (apiProjects == null) {
392 			return;
393 		}
394 		for (IProject iProject : apiProjects) {
395 			try {
396 				iProject.deleteMarkers(IApiMarkerConstants.API_USESCAN_PROBLEM_MARKER, false,
397 						IResource.DEPTH_INFINITE);
398 			} catch (CoreException e) {
399 			}
400 
401 		}
402 		return;
403 	}
404 
405 	/**
406 	 * Detects changes to the use scan locations
407 	 *
408 	 * @param newLocations
409 	 * @return if there have been changes to the use scan entries
410 	 */
hasLocationsChanges(String newLocations)411 	private boolean hasLocationsChanges(String newLocations) {
412 		String oldLocations = getStoredValue(IApiCoreConstants.API_USE_SCAN_LOCATION, null);
413 
414 		if (oldLocations != null && oldLocations.equalsIgnoreCase(newLocations)) {
415 			return false;
416 		}
417 
418 		ArrayList<String> oldCheckedElements = new ArrayList<>();
419 		if (oldLocations != null && oldLocations.length() > 0) {
420 			String[] locations = oldLocations.split(UseScanManager.ESCAPE_REGEX + UseScanManager.LOCATION_DELIM);
421 			for (String location : locations) {
422 				String values[] = location.split(UseScanManager.ESCAPE_REGEX + UseScanManager.STATE_DELIM);
423 				if (Boolean.parseBoolean(values[1])) {
424 					oldCheckedElements.add(values[0]);
425 				}
426 			}
427 		}
428 		Object[] newCheckedLocations = fTableViewer.getCheckedElements();
429 		if (newCheckedLocations.length != oldCheckedElements.size()) {
430 			return true;
431 		}
432 		for (int i = 0; i < newCheckedLocations.length; i++) {
433 			if (!oldCheckedElements.contains(newCheckedLocations[i])) {
434 				return true;
435 			}
436 		}
437 		return false;
438 	}
439 
440 	/**
441 	 * Sets the value to the given preference key
442 	 *
443 	 * @param key
444 	 * @param value
445 	 */
setStoredValue(String key, String value)446 	public void setStoredValue(String key, String value) {
447 		IEclipsePreferences node = getNode();
448 		if (value != null) {
449 			node.put(key, value);
450 		} else {
451 			node.remove(key);
452 		}
453 	}
454 
455 	/**
456 	 * Retrieves the value for the given preference key or the returns the given
457 	 * default if it is not defined
458 	 *
459 	 * @param key
460 	 * @param defaultValue
461 	 * @return the stored value or the specified default
462 	 */
getStoredValue(String key, String defaultValue)463 	public String getStoredValue(String key, String defaultValue) {
464 		IEclipsePreferences node = getNode();
465 		if (node != null) {
466 			return node.get(key, defaultValue);
467 		}
468 		return defaultValue;
469 	}
470 
471 	/**
472 	 * @return the root preference node for the API tools core plug-in
473 	 */
getNode()474 	private IEclipsePreferences getNode() {
475 		IEclipsePreferences node = (InstanceScope.INSTANCE).getNode(ApiPlugin.PLUGIN_ID);
476 		if (fManager != null) {
477 			return fManager.getWorkingCopy(node);
478 		}
479 		return node;
480 	}
481 }
482