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