1 /*******************************************************************************
2  * Copyright (c) 2012, 2017 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.internal;
15 
16 import java.io.File;
17 import java.io.FileInputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.Set;
26 import java.util.zip.ZipEntry;
27 import java.util.zip.ZipFile;
28 
29 import org.eclipse.core.resources.IResource;
30 import org.eclipse.core.runtime.CoreException;
31 import org.eclipse.core.runtime.Path;
32 import org.eclipse.pde.api.tools.internal.model.BundleComponent;
33 import org.eclipse.pde.api.tools.internal.problems.ApiProblemFactory;
34 import org.eclipse.pde.api.tools.internal.problems.ApiProblemFilter;
35 import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
36 import org.eclipse.pde.api.tools.internal.provisional.IApiFilterStore;
37 import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem;
38 import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblemFilter;
39 import org.eclipse.pde.api.tools.internal.util.Util;
40 import org.w3c.dom.Element;
41 import org.w3c.dom.NodeList;
42 
43 /**
44  * A generic {@link IApiFilterStore} that does not depend on workspace resources
45  * to filter {@link IApiProblem}s. <br>
46  * This filter store can have filters added and removed from it, but those
47  * changes are not saved.
48  */
49 public class FilterStore implements IApiFilterStore {
50 
51 	public static final String GLOBAL = "!global!"; //$NON-NLS-1$
52 	/**
53 	 * Represents no filters
54 	 */
55 	public static IApiProblemFilter[] NO_FILTERS = new IApiProblemFilter[0];
56 	/**
57 	 * The current version of this filter store file format
58 	 */
59 	public static final int CURRENT_STORE_VERSION = 2;
60 	/**
61 	 * Constant representing the name of the .settings folder
62 	 */
63 	static final String SETTINGS_FOLDER = ".settings"; //$NON-NLS-1$
64 	/**
65 	 * The mapping of filters for this store.
66 	 */
67 	protected HashMap<String, Set<IApiProblemFilter>> fFilterMap = null;
68 	/**
69 	 * The bundle component backing this store
70 	 */
71 	private BundleComponent fComponent = null;
72 
73 	/**
74 	 * Constructor
75 	 */
FilterStore()76 	public FilterStore() {
77 	}
78 
79 	/**
80 	 * Constructor
81 	 *
82 	 * @param component
83 	 */
FilterStore(BundleComponent component)84 	public FilterStore(BundleComponent component) {
85 		fComponent = component;
86 	}
87 
88 	@Override
addFilters(IApiProblemFilter[] filters)89 	public void addFilters(IApiProblemFilter[] filters) {
90 		if (filters != null && filters.length > 0) {
91 			initializeApiFilters();
92 			// This store does not use resources so all filters are stored in
93 			// the global set
94 			Set<IApiProblemFilter> globalFilters = fFilterMap.get(GLOBAL);
95 			if (globalFilters == null) {
96 				globalFilters = new HashSet<>();
97 				fFilterMap.put(GLOBAL, globalFilters);
98 			}
99 			Collections.addAll(globalFilters, filters);
100 		}
101 	}
102 
103 	@Override
addFiltersFor(IApiProblem[] problems)104 	public void addFiltersFor(IApiProblem[] problems) {
105 		if (problems != null && problems.length > 0) {
106 			initializeApiFilters();
107 			internalAddFilters(problems, null);
108 		}
109 	}
110 
111 	@Override
getFilters(IResource resource)112 	public IApiProblemFilter[] getFilters(IResource resource) {
113 		return null;
114 	}
115 
116 	@Override
getResources()117 	public IResource[] getResources() {
118 		return null;
119 	}
120 
121 	@Override
removeFilters(IApiProblemFilter[] filters)122 	public boolean removeFilters(IApiProblemFilter[] filters) {
123 		if (filters != null && filters.length > 0) {
124 			initializeApiFilters();
125 			boolean removed = true;
126 			// This filter store does not support resources so all filters are
127 			// stored under GLOBAL
128 			Set<IApiProblemFilter> globalFilters = fFilterMap.get(GLOBAL);
129 			if (globalFilters != null && globalFilters.size() > 0) {
130 				for (IApiProblemFilter filter : filters) {
131 					removed &= globalFilters.remove(filter);
132 				}
133 				return removed;
134 			}
135 		}
136 		return false;
137 	}
138 
139 	/**
140 	 * Loads the filters from the .api_filters file
141 	 */
initializeApiFilters()142 	protected synchronized void initializeApiFilters() {
143 		if (fFilterMap == null) {
144 			fFilterMap = new HashMap<>(5);
145 			ZipFile jarFile = null;
146 			InputStream filterstream = null;
147 			File loc = new File(fComponent.getLocation());
148 			String extension = new Path(loc.getName()).getFileExtension();
149 			try {
150 				if (extension != null && extension.equals("jar") && loc.isFile()) { //$NON-NLS-1$
151 					jarFile = new ZipFile(loc, ZipFile.OPEN_READ);
152 					ZipEntry filterfile = jarFile.getEntry(IApiCoreConstants.API_FILTERS_XML_NAME);
153 					if (filterfile != null) {
154 						if (ApiPlugin.DEBUG_FILTER_STORE) {
155 							System.out.println("found api filter file: [" + fComponent.getName() + "] inside jar file " + loc); //$NON-NLS-1$ //$NON-NLS-2$
156 						}
157 						filterstream = jarFile.getInputStream(filterfile);
158 					}
159 				} else {
160 					File file = new File(loc, SETTINGS_FOLDER + File.separator + IApiCoreConstants.API_FILTERS_XML_NAME);
161 					if (file.exists()) {
162 						if (ApiPlugin.DEBUG_FILTER_STORE) {
163 							System.out.println("found api filter file: [" + fComponent.getName() + "] at " + file); //$NON-NLS-1$ //$NON-NLS-2$
164 						}
165 						filterstream = new FileInputStream(file);
166 					}
167 				}
168 				if (filterstream == null) {
169 					return;
170 				}
171 				readFilterFile(filterstream);
172 
173 			} catch (IOException e) {
174 				ApiPlugin.log(e);
175 			} finally {
176 				fComponent.closingZipFileAndStream(filterstream, jarFile);
177 			}
178 		}
179 	}
180 
181 	@Override
isFiltered(IApiProblem problem)182 	public boolean isFiltered(IApiProblem problem) {
183 		initializeApiFilters();
184 		if (fFilterMap == null || fFilterMap.isEmpty()) {
185 			return false;
186 		}
187 		Set<IApiProblemFilter> globalFilters = fFilterMap.get(GLOBAL);
188 		if (globalFilters == null) {
189 			return false;
190 		}
191 		for (IApiProblemFilter filter : globalFilters) {
192 			if (problemsMatch(filter.getUnderlyingProblem(), problem)) {
193 				if (ApiPlugin.DEBUG_FILTER_STORE) {
194 					System.out.println("filter used: [" + filter.toString() + "]"); //$NON-NLS-1$//$NON-NLS-2$
195 				}
196 				return true;
197 			}
198 		}
199 		return false;
200 	}
201 
202 	/**
203 	 * Returns <code>true</code> if the attributes of the problems match,
204 	 * <code>false</code> otherwise
205 	 *
206 	 * @param filterProblem the problem from the filter store
207 	 * @param problem the problem from the builder
208 	 * @return <code>true</code> if the problems match, <code>false</code>
209 	 *         otherwise
210 	 */
problemsMatch(IApiProblem filterProblem, IApiProblem problem)211 	protected boolean problemsMatch(IApiProblem filterProblem, IApiProblem problem) {
212 		if (problem.getId() == filterProblem.getId()) {
213 			// Two problems are different if their paths are different, but if
214 			// one is missing a path they may still be equal
215 			String problemPath = problem.getResourcePath();
216 			String filterProblemPath = filterProblem.getResourcePath();
217 			if (problemPath != null && filterProblemPath != null && !(new Path(problemPath).equals(new Path(filterProblemPath)))) {
218 				return false;
219 			}
220 			String problemTypeName = problem.getTypeName();
221 			String filterProblemTypeName = filterProblem.getTypeName();
222 			if (problemTypeName == null) {
223 				if (filterProblemTypeName != null) {
224 					return false;
225 				}
226 			} else if (filterProblemTypeName == null) {
227 				return false;
228 			} else if (!problemTypeName.equals(filterProblemTypeName)) {
229 				return false;
230 			}
231 			return argumentsEquals(problem.getMessageArguments(), filterProblem.getMessageArguments());
232 		}
233 		return false;
234 	}
235 
236 	/**
237 	 * Returns if the arrays of message arguments are equal. <br>
238 	 * The arrays are considered equal iff:
239 	 * <ul>
240 	 * <li>both are <code>null</code></li>
241 	 * <li>both are the same length</li>
242 	 * <li>both have equal elements at equal positions in the array</li>
243 	 * </ul>
244 	 *
245 	 * @param problemMessageArguments
246 	 * @param filterProblemMessageArguments
247 	 * @return <code>true</code> if the arrays are equal, <code>false</code>
248 	 *         otherwise
249 	 */
argumentsEquals(String[] problemMessageArguments, String[] filterProblemMessageArguments)250 	private boolean argumentsEquals(String[] problemMessageArguments, String[] filterProblemMessageArguments) {
251 		// filter problems message arguments are always simple name
252 		// problem message arguments are fully qualified name outside the IDE
253 		int length = problemMessageArguments.length;
254 		if (length == filterProblemMessageArguments.length) {
255 			for (int i = 0; i < length; i++) {
256 				String problemMessageArgument = problemMessageArguments[i];
257 				String filterProblemMessageArgument = filterProblemMessageArguments[i];
258 				if (problemMessageArgument.equals(filterProblemMessageArgument)) {
259 					continue;
260 				}
261 				int index = problemMessageArgument.lastIndexOf('.');
262 				int filterProblemIndex = filterProblemMessageArgument.lastIndexOf('.');
263 				if (index == -1) {
264 					if (filterProblemIndex == -1) {
265 						return false; // simple names should match
266 					}
267 					if (filterProblemMessageArgument.substring(filterProblemIndex + 1).equals(problemMessageArgument)) {
268 						continue;
269 					} else {
270 						return false;
271 					}
272 				} else if (filterProblemIndex != -1) {
273 					return false; // fully qualified name should match
274 				} else {
275 					if (problemMessageArgument.substring(index + 1).equals(filterProblemMessageArgument)) {
276 						continue;
277 					} else {
278 						return false;
279 					}
280 				}
281 			}
282 			return true;
283 		}
284 		return false;
285 	}
286 
287 	@Override
dispose()288 	public void dispose() {
289 		if (fFilterMap != null) {
290 			fFilterMap.clear();
291 			fFilterMap = null;
292 		}
293 	}
294 
295 	/**
296 	 * Reads the API problem filter file and calls back to
297 	 * {@link #addFilters(IApiProblemFilter[])} to store the filters. <br>
298 	 * This method will not close the given input stream when done reading it.
299 	 *
300 	 * @param contents the {@link InputStream} for the contents of the filter
301 	 *            file, <code>null</code> is not allowed.
302 	 * @throws IOException if the stream cannot be read or fails
303 	 */
readFilterFile(InputStream contents)304 	protected void readFilterFile(InputStream contents) throws IOException {
305 		if (contents == null) {
306 			throw new IOException(CoreMessages.FilterStore_0);
307 		}
308 		String xml = new String(Util.getInputStreamAsCharArray(contents, -1, StandardCharsets.UTF_8));
309 		Element root = null;
310 		try {
311 			root = Util.parseDocument(xml);
312 		} catch (CoreException ce) {
313 			ApiPlugin.log(ce);
314 		}
315 		if (root == null) {
316 			return;
317 		}
318 		if (!root.getNodeName().equals(IApiXmlConstants.ELEMENT_COMPONENT)) {
319 			return;
320 		}
321 		String component = root.getAttribute(IApiXmlConstants.ATTR_ID);
322 		if (component.length() == 0) {
323 			return;
324 		}
325 		String versionValue = root.getAttribute(IApiXmlConstants.ATTR_VERSION);
326 		int currentVersion = Integer.parseInt(IApiXmlConstants.API_FILTER_STORE_CURRENT_VERSION);
327 		int version = 0;
328 		if (versionValue.length() != 0) {
329 			try {
330 				version = Integer.parseInt(versionValue);
331 			} catch (NumberFormatException e) {
332 				// ignore
333 			}
334 		}
335 		if (version != currentVersion) {
336 			return;
337 		}
338 		NodeList resources = root.getElementsByTagName(IApiXmlConstants.ELEMENT_RESOURCE);
339 		ArrayList<IApiProblem> newfilters = new ArrayList<>();
340 		ArrayList<String> comments = new ArrayList<>();
341 		for (int i = 0; i < resources.getLength(); i++) {
342 			Element element = (Element) resources.item(i);
343 			String typeName = element.getAttribute(IApiXmlConstants.ATTR_TYPE);
344 			if (typeName.length() == 0) {
345 				// if there is no type attribute, an empty string is returned
346 				typeName = null;
347 			}
348 			String path = element.getAttribute(IApiXmlConstants.ATTR_PATH);
349 			if (path.trim().length() == 0) {
350 				path = null; // it is valid to have a filter without a path
351 			}
352 			NodeList filters = element.getElementsByTagName(IApiXmlConstants.ELEMENT_FILTER);
353 			for (int j = 0; j < filters.getLength(); j++) {
354 				element = (Element) filters.item(j);
355 				int id = loadIntegerAttribute(element, IApiXmlConstants.ATTR_ID);
356 				if (id <= 0) {
357 					continue;
358 				}
359 				String[] messageargs = null;
360 				NodeList elements = element.getElementsByTagName(IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENTS);
361 				if (elements.getLength() == 1) {
362 					Element messageArguments = (Element) elements.item(0);
363 					NodeList arguments = messageArguments.getElementsByTagName(IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENT);
364 					int length = arguments.getLength();
365 					messageargs = new String[length];
366 					for (int k = 0; k < length; k++) {
367 						Element messageArgument = (Element) arguments.item(k);
368 						messageargs[k] = messageArgument.getAttribute(IApiXmlConstants.ATTR_VALUE);
369 					}
370 				}
371 
372 				String comment = element.getAttribute(IApiXmlConstants.ATTR_COMMENT);
373 				comments.add((comment.length() < 1 ? null : comment));
374 				newfilters.add(ApiProblemFactory.newApiProblem(path, typeName, messageargs, null, null, -1, -1, -1, id));
375 			}
376 		}
377 		if (ApiPlugin.DEBUG_FILTER_STORE) {
378 			System.out.println(newfilters.size() + " filters found and added for: [" + component + "]"); //$NON-NLS-1$ //$NON-NLS-2$
379 		}
380 		internalAddFilters(newfilters.toArray(new IApiProblem[newfilters.size()]), comments.toArray(new String[comments.size()]));
381 		newfilters.clear();
382 	}
383 
384 	/**
385 	 * Internal use method that allows auto-persisting of the filter file to be
386 	 * turned on or off
387 	 *
388 	 * @param problems the problems to add the the store
389 	 * @param persist if the filters should be auto-persisted after they are
390 	 *            added
391 	 */
internalAddFilters(IApiProblem[] problems, String[] comments)392 	protected void internalAddFilters(IApiProblem[] problems, String[] comments) {
393 		if (problems == null || problems.length == 0) {
394 			return;
395 		}
396 		// This filter store doesn't handle resources so all filters are added
397 		// to GLOBAL
398 		Set<IApiProblemFilter> globalFilters = fFilterMap.get(GLOBAL);
399 		if (globalFilters == null) {
400 			globalFilters = new HashSet<>();
401 			fFilterMap.put(GLOBAL, globalFilters);
402 		}
403 
404 		for (int i = 0; i < problems.length; i++) {
405 			IApiProblem problem = problems[i];
406 			String comment = comments != null ? comments[i] : null;
407 			IApiProblemFilter filter = new ApiProblemFilter(fComponent.getSymbolicName(), problem, comment);
408 			globalFilters.add(filter);
409 		}
410 	}
411 
412 	/**
413 	 * Loads the specified integer attribute from the given XML element
414 	 *
415 	 * @param element the XML element
416 	 * @param name the name of the attribute
417 	 * @return the specified value in XML or -1
418 	 */
loadIntegerAttribute(Element element, String name)419 	protected int loadIntegerAttribute(Element element, String name) {
420 		String value = element.getAttribute(name);
421 		if (value.length() == 0) {
422 			return -1;
423 		}
424 		try {
425 			int number = Integer.parseInt(value);
426 			return number;
427 		} catch (NumberFormatException nfe) {
428 			// ignore
429 		}
430 		return -1;
431 	}
432 
433 }
434