Bug 332772 - Support problem filters for API Use Scan Task

Work in progress, generates filters but not apply them
diff --git a/apitools/org.eclipse.pde.api.tools/META-INF/MANIFEST.MF b/apitools/org.eclipse.pde.api.tools/META-INF/MANIFEST.MF
index f3e2b6a..5d2b50c 100644
--- a/apitools/org.eclipse.pde.api.tools/META-INF/MANIFEST.MF
+++ b/apitools/org.eclipse.pde.api.tools/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.pde.api.tools;singleton:=true
-Bundle-Version: 1.0.400.qualifier
+Bundle-Version: 1.0.500.qualifier
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Require-Bundle: org.eclipse.osgi;bundle-version="[3.4.0,4.0.0)",
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/ApiProblemFactory.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/ApiProblemFactory.java
index dc38f62..ebd5a8b 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/ApiProblemFactory.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/ApiProblemFactory.java
@@ -650,10 +650,7 @@
 	
 	/**
 	 * Returns the problem severity id for the given problem parameters.
-	 * @param category
-	 * @param element
-	 * @param kind
-	 * @param flags
+	 * @param problem the problem to get severity for
 	 * @return the id of the preference to use to lookup the user specified severity level for the given {@link IApiProblem}
 	 */
 	public static String getProblemSeverityId(IApiProblem problem) {
@@ -726,4 +723,5 @@
 		}
 		return null;
 	}
+	
 }
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/problemmessages.properties b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/problemmessages.properties
index 3939aaf..8dacc59 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/problemmessages.properties
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/problemmessages.properties
@@ -30,9 +30,10 @@
 20 = The minor version should be incremented in version {0}, because the modification of the version range for the re-exported bundle {1} requires a minor version change
 
 #api usage problems
-#{0} = referenced type name
+#{0} = referenced type or member name
 #{1} = local type name
 #{2} = field or method name
+#{3} = required execution environment
 
 8 = {1} illegally implements {0}
 9 = {1} illegally extends {0}
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/ReferenceFilterStore.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/ReferenceFilterStore.java
new file mode 100644
index 0000000..0516701
--- /dev/null
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/ReferenceFilterStore.java
@@ -0,0 +1,354 @@
+/*******************************************************************************
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.api.tools.internal.search;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
+import org.eclipse.pde.api.tools.internal.IApiXmlConstants;
+import org.eclipse.pde.api.tools.internal.problems.ApiProblemFactory;
+import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
+import org.eclipse.pde.api.tools.internal.provisional.builder.IReference;
+import org.eclipse.pde.api.tools.internal.provisional.model.IApiElement;
+import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem;
+import org.eclipse.pde.api.tools.internal.util.Util;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Processes API problem filter files into a collection of reference filters. The
+ * filters can later be applied to a set of references to exclude references with
+ * matching to/from types and members.
+ * 
+ * @since 1.0.500
+ */
+public class ReferenceFilterStore {
+
+	/**
+	 * Describes a single reference filter. Contents can be accessed via fields.
+	 */
+	class ReferenceFilter{
+		/**
+		 * The type being referenced, possibly <code>null</code>
+		 */
+		public String referencedTypeName = null;
+		/**
+		 * The local type referencing the {@link #referencedTypeName}, possibly <code>null</code>
+		 */
+		public String localTypeName = null;
+		/**
+		 * The field or method from {@link #localTypeName} that is referencing {@link #referencedTypeName}, possibly <code>null</code> 
+		 */
+		public String fieldOrMethodName = null; 
+		
+		/**
+		 * Constructs a new reference filter with available information. 
+		 * @param referencedTypeName
+		 * @param localTypeName
+		 * @param fieldOrMethodName
+		 */
+		public ReferenceFilter(String referencedTypeName, String localTypeName, String fieldOrMethodName){
+			this.referencedTypeName = referencedTypeName;
+			this.localTypeName = localTypeName;
+			this.fieldOrMethodName = fieldOrMethodName;
+		}
+		
+		/* (non-Javadoc)
+		 * @see java.lang.Object#equals(java.lang.Object)
+		 */
+		public boolean equals(Object obj) {
+			if (obj instanceof ReferenceFilter){
+				ReferenceFilter filter = (ReferenceFilter)obj;
+				if ((filter.referencedTypeName == null && referencedTypeName == null) || (filter.referencedTypeName != null && filter.referencedTypeName.equals(referencedTypeName))){
+					if ((filter.localTypeName == null && localTypeName == null) || (filter.localTypeName != null && filter.localTypeName.equals(localTypeName))){
+						if ((filter.fieldOrMethodName == null && fieldOrMethodName == null) || (filter.fieldOrMethodName != null && filter.fieldOrMethodName.equals(fieldOrMethodName))){
+							return true;
+						}
+					}
+				}
+			}
+			return false;
+		}
+		
+		/* (non-Javadoc)
+		 * @see java.lang.Object#hashCode()
+		 */
+		public int hashCode() {
+			int hashCode = 1;
+			if (referencedTypeName != null){
+				hashCode += referencedTypeName.hashCode();
+			}
+			if (localTypeName != null){
+				hashCode += localTypeName.hashCode();
+			}
+			if (fieldOrMethodName != null){
+				hashCode += fieldOrMethodName.hashCode();
+			}
+			return hashCode;
+		}
+		
+		/* (non-Javadoc)
+		 * @see java.lang.Object#toString()
+		 */
+		public String toString() {
+			return "Reference Filter: " + referencedTypeName + " | " + localTypeName + " | " + fieldOrMethodName; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+		}
+	}
+	
+	/**
+	 * Fully qualified type name to a IApiProblem
+	 */
+	private Map/*<String, IApiProblem>*/ fFilterMap;
+	private int fFilterCount;
+	private boolean debug = false;
+	
+	/**
+	 * Constructs a new filter store for the given componentID, processing all appropriate
+	 * api filters files found in the directory specified by the filtersRoot string path.
+	 * 
+	 * @param filterRoot path to folder containing api problem filters files
+	 * @param componentID string id of the component this filter store is for
+	 * @param debug whether to print out debug statements to the system out stream
+	 */
+	public ReferenceFilterStore(String filterRoot, String componentID, boolean debug){
+		this.debug = debug;
+		this.initialize(filterRoot, componentID);
+	}
+	
+	/**
+	 * Disposes of any resources this filter store is using
+	 */
+	public void dispose(){
+		// Map should be handled by GC, but clear it in case something is holding on to a filter
+		fFilterMap.clear();
+		fFilterMap = null;
+		fFilterCount = 0;
+	}
+	
+	/**
+	 * Returns whether to filter the given reference because it matches one of 
+	 * the filters in this filter store.
+	 *  
+	 * @param reference the reference to compare against filters, the member's type must be available
+	 * @return <code>true</code> if the reference matches a filter
+	 */
+	public boolean isFiltered(IReference reference){
+		IApiElement member = reference.getMember();
+		while (member.getType() != IApiElement.TYPE){
+			member = member.getParent();
+			if (member == null){
+				// No parent type could be found
+				return false;
+			}
+		}
+		
+		// member.getName gives qualified name for types
+		Set filters = (Set)fFilterMap.get(member.getName());
+		
+		
+		// XXX Remove
+		System.out.println("Search filters for " + member.getName());
+		if (filters != null){
+			System.out.println(filters.size() + " filters available for " + member.getName());
+		}
+		
+		
+		if (filters != null){
+			// Referencing types match, check referenced type names and method/field names
+			// TODO
+			
+		}
+		
+		
+		return false;
+	}
+	
+	/**
+	 * Returns the number of filters in this filter store
+	 * 
+	 * @return number of filters in this filter store
+	 */
+	public int getFilterCount(){
+		return fFilterCount;
+	}
+	
+	/**
+	 * Initialize the filter store using the given component id
+	 */
+	private void initialize(String filtersRoot, String componentID) {
+		if(fFilterMap != null) {
+			return;
+		}
+		fFilterCount = 0;
+		fFilterMap = new HashMap(5);
+		String xml = null;
+		InputStream contents = null;
+		try {
+			File filterFileParent = new File(filtersRoot, componentID);
+			if (!filterFileParent.exists()) {
+				if(this.debug) {
+					System.out.println("No filters found for component " + componentID); //$NON-NLS-1$
+				}
+				return;
+			}
+			contents = new BufferedInputStream(new FileInputStream(new File(filterFileParent, IApiCoreConstants.API_FILTERS_XML_NAME)));
+			xml = new String(Util.getInputStreamAsCharArray(contents, -1, IApiCoreConstants.UTF_8));
+		}
+		catch(IOException ioe) {}
+		finally {
+			if (contents != null) {
+				try {
+					contents.close();
+				} catch(IOException e) {
+					// ignore
+				}
+			}
+		}
+		if(xml == null) {
+			return;
+		}
+		Element root = null;
+		try {
+			root = Util.parseDocument(xml);
+		}
+		catch(CoreException ce) {
+			ApiPlugin.log(ce);
+		}
+		if (root == null) {
+			return;
+		}
+		if (!root.getNodeName().equals(IApiXmlConstants.ELEMENT_COMPONENT)) {
+			return;
+		}
+		String component = root.getAttribute(IApiXmlConstants.ATTR_ID);
+		if(component.length() == 0) {
+			return;
+		}
+
+		int version = loadIntegerAttribute(root, IApiXmlConstants.ATTR_VERSION);
+		if (version < 2) {
+			if(this.debug) {
+				System.out.println("All filters of versions earlier than 2 are discarded because there is no way to retrieve the type name"); //$NON-NLS-1$
+			}
+			// we discard all filters since there is no way to retrieve the type name
+			return;
+		}
+		
+		NodeList resources = root.getElementsByTagName(IApiXmlConstants.ELEMENT_RESOURCE);
+		for(int i = 0; i < resources.getLength(); i++) {
+			Element element = (Element) resources.item(i);
+			String typeName = element.getAttribute(IApiXmlConstants.ATTR_TYPE);
+			if(typeName == null || typeName.length() == 0) {
+				// Only problems with types are used to filter references
+				continue;
+			}
+			NodeList filters = element.getElementsByTagName(IApiXmlConstants.ELEMENT_FILTER);
+			for(int j = 0; j < filters.getLength(); j++) {
+				element = (Element) filters.item(j);
+				int id = loadIntegerAttribute(element, IApiXmlConstants.ATTR_ID);
+				if(id <= 0) {
+					continue;
+				}
+				String[] messageargs = null;
+				NodeList elements = element.getElementsByTagName(IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENTS);
+				if (elements.getLength() != 1) continue;
+				Element messageArguments = (Element) elements.item(0);
+				NodeList arguments = messageArguments.getElementsByTagName(IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENT);
+				int length = arguments.getLength();
+				messageargs = new String[length];
+				for (int k = 0; k < length; k++) {
+					Element messageArgument = (Element) arguments.item(k);
+					messageargs[k] = messageArgument.getAttribute(IApiXmlConstants.ATTR_VALUE);
+				}
+
+				ReferenceFilter reference = recoverReference(ApiProblemFactory.newApiProblem(null, typeName, messageargs, null, null, -1, -1, -1, id));
+				
+				// XXX Remove
+				System.out.println(reference);
+				
+				
+				if (reference != null){
+					Set filterSet = (Set) fFilterMap.get(typeName);
+					if(filterSet == null) {
+						filterSet = new HashSet();
+						fFilterMap.put(typeName, filterSet);
+					}
+					filterSet.add(reference);
+					fFilterCount++;
+				}
+			}
+		}
+		
+		if (debug){
+			System.out.println(fFilterCount + " reference filters found for component " + componentID); //$NON-NLS-1$
+		}
+	}
+	
+	private int loadIntegerAttribute(Element element, String name) {
+		String value = element.getAttribute(name);
+		if(value.length() == 0) {
+			return -1;
+		}
+		try {
+			int number = Integer.parseInt(value);
+			return number;
+		}
+		catch(NumberFormatException nfe) {}
+		return -1;
+	}
+	
+	
+	/**
+	 * Returns a reference object created by parsing the message arguments, type and id of the given problem or
+	 * <code>null</code> if the problem does not reflect a reference.  The returned reference may be missing information
+	 * and the type/method names may not be fully qualified.
+	 * 
+	 * @param problem the problem to recover a reference from
+	 * @return a reference containing all information collected from the problem or <code>null</code> if a reference cannot be recovered
+	 */
+	private ReferenceFilter recoverReference(IApiProblem problem) {
+		if (problem.getCategory() == IApiProblem.CATEGORY_USAGE){
+			/*
+			#api usage problems
+			#{0} = referenced type or member name
+			#{1} = local type name
+			#{2} = field or method name
+			#{3} = required execution environment
+			*/
+			String referencedTypeName = null;
+			String localTypeName = null;
+			String fieldOrMethodName = null; 
+			String[] arguments = problem.getMessageArguments();
+			if (arguments.length > 0){
+				referencedTypeName = arguments[0];
+				if (arguments.length > 1){
+					localTypeName = arguments[1];
+					if (arguments.length > 2){
+						fieldOrMethodName = arguments[2];
+					}
+				}
+				return new ReferenceFilter(referencedTypeName, localTypeName, fieldOrMethodName);
+			}
+			
+		}
+		return null;
+	}
+
+}
diff --git a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/UseSearchRequestor.java b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/UseSearchRequestor.java
index 967c17a..893c7b1 100644
--- a/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/UseSearchRequestor.java
+++ b/apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/search/UseSearchRequestor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2009, 2010 IBM Corporation and others.
+ * Copyright (c) 2009, 2011 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -65,7 +65,22 @@
 	 * The default {@link ReferenceAnalyzer} for detecting illegal API use
 	 * @see #includesIllegalUse()
 	 */
-	ReferenceAnalyzer fAnalyzer = null;
+	private ReferenceAnalyzer fAnalyzer = null;
+	
+	/**
+	 * The string root location of any api_filter files to apply, possbly <code>null</code> 
+	 */
+	private String fFilterRoot = null;
+	
+	/**
+	 * The filter store for the current component, only one is cached at a time
+	 */
+	private ReferenceFilterStore fCurrentFilterStore = null;
+	
+	/**
+	 * Provide more verbose output if debug flag is set
+	 */
+	private boolean debug = false;
 	
 	/**
 	 * Constructor
@@ -89,6 +104,22 @@
 	 * @see org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchRequestor#acceptComponent(org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent)
 	 */
 	public boolean acceptComponent(IApiComponent component) {
+		if (fFilterRoot != null){
+			
+			
+			
+			// TODO Change debug value back from true
+			debug = true;
+			
+			
+			
+			fCurrentFilterStore = new ReferenceFilterStore(fFilterRoot, component.getSymbolicName(), debug);
+			if (fCurrentFilterStore.getFilterCount() == 0){
+				// If no relevant filters are found, skip filtering
+				fCurrentFilterStore.dispose();
+				fCurrentFilterStore = null;
+			}
+		}
 		try {
 			if(!component.isSystemComponent() && getScope().encloses(component)) {
 				if(includesIllegalUse()) {
@@ -156,23 +187,39 @@
 		try {
 			IApiMember member = reference.getResolvedReference();
 			if(member != null) {
+				// Check for component filtering
 				IApiComponent component = member.getApiComponent();
 				if(!fComponentIds.contains(component.getSymbolicName()) || component.equals(reference.getMember().getApiComponent())) {
 					return false;
 				}
+
+				// Check search type filtering
+				boolean searchTypeValid = false;			
 				if(isIllegalUse(reference) || (includesAPI() && includesInternal())) {
-					return true;
+					searchTypeValid = true;
 				}
 				IApiAnnotations annots = component.getApiDescription().resolveAnnotations(member.getHandle());
 				if(annots != null) {
 					int vis = annots.getVisibility();
 					if(VisibilityModifiers.isAPI(vis) && includesAPI()) {
-						return true;
+						searchTypeValid = true;
 					}
 					else if(VisibilityModifiers.isPrivate(vis) && includesInternal()) {
-						return true;
+						searchTypeValid = true;
 					}
 				}
+				if (!searchTypeValid){
+					return false;
+				}
+				
+				// Check against api_filters file
+				if (fCurrentFilterStore != null){
+					if (fCurrentFilterStore.isFiltered(reference)){
+						return false;
+					}
+				}
+				
+				return true;
 			}
 		}
 		catch(CoreException ce) {
@@ -260,4 +307,20 @@
 	public void setJarPatterns(String[] patterns) {
 		jarPatterns = patterns;
 	}
+	
+	/**
+	 * Set the root directory location of api_filter files that should be used to filter the accepted references
+	 * @param filterRoot string file location of any api_filter files to apply or <code>null</code> to not filter
+	 */
+	public void setFilterRoot(String filterRoot){
+		this.fFilterRoot = filterRoot;
+	}
+	
+	/**
+	 * Sets whether this requestor is in debug mode, with more verbose output.  Default is <code>false</code>.
+	 * @param debug new debug setting
+	 */
+	public void setDebug(boolean debug){
+		this.debug = debug;
+	}
 }
diff --git a/apitools/org.eclipse.pde.api.tools/src_ant/org/eclipse/pde/api/tools/internal/tasks/ApiUseTask.java b/apitools/org.eclipse.pde.api.tools/src_ant/org/eclipse/pde/api/tools/internal/tasks/ApiUseTask.java
index b94c505..1d2b2fe 100644
--- a/apitools/org.eclipse.pde.api.tools/src_ant/org/eclipse/pde/api/tools/internal/tasks/ApiUseTask.java
+++ b/apitools/org.eclipse.pde.api.tools/src_ant/org/eclipse/pde/api/tools/internal/tasks/ApiUseTask.java
@@ -108,6 +108,11 @@
 	private FilteredElements includedElements = null;
 	
 	/**
+	 * Root directory of api_filters files to apply
+	 */
+	private String filterRoot = null;
+	
+	/**
 	 * Set the location of the current product you want to search.
 	 * 
 	 * <p>It can be a .zip, .jar, .tgz, .tar.gz file, or a directory that corresponds to 
@@ -292,11 +297,10 @@
 			TreeSet scope = new TreeSet(Util.componentsorter);
 			getContext(baseline, ids, scope);
 			ApiSearchEngine engine = new ApiSearchEngine();
-			UseSearchRequestor requestor = new UseSearchRequestor(
-					ids,
-					(IApiElement[]) scope.toArray(new IApiElement[scope.size()]), 
-					getSearchFlags());
+			UseSearchRequestor requestor = new UseSearchRequestor(ids, (IApiElement[]) scope.toArray(new IApiElement[scope.size()]), getSearchFlags());
+			requestor.setFilterRoot(filterRoot);
 			requestor.setJarPatterns(archivePatterns);
+			requestor.setDebug(debug);
 			// override API descriptions as required
 			if (apiPatterns != null || internalPatterns != null) {
 				// modify API descriptions
@@ -491,15 +495,20 @@
 			System.out.println("Searching for internal references : " + this.considerinternal); //$NON-NLS-1$
 			System.out.println("Searching for illegal API use : "+ this.considerillegaluse); //$NON-NLS-1$
 			if (this.excludeListLocation != null) {
-				System.out.println("exclude list location : " + this.excludeListLocation); //$NON-NLS-1$
+				System.out.println("Exclude list location : " + this.excludeListLocation); //$NON-NLS-1$
 			} else {
 				System.out.println("No exclude list location"); //$NON-NLS-1$
 			}
 			if (this.includeListLocation != null) {
-				System.out.println("include list location : " + this.includeListLocation); //$NON-NLS-1$
+				System.out.println("Include list location : " + this.includeListLocation); //$NON-NLS-1$
 			} else {
 				System.out.println("No include list location"); //$NON-NLS-1$
 			}
+			if (this.filterRoot != null) {
+				System.out.println("API Filter location : " + this.filterRoot); //$NON-NLS-1$
+			} else {
+				System.out.println("No API filter location"); //$NON-NLS-1$
+			}
 			if(this.scopepattern == null) {
 				System.out.println("No scope pattern defined - searching all bundles"); //$NON-NLS-1$
 			}
@@ -571,4 +580,28 @@
 	public void setIncludeList(String includeListLocation) {
 		this.includeListLocation = includeListLocation;
 	}
+	
+	/**
+	 * Set the root directory of API filters to use during the use scan.
+	 *
+	 * The argument is the root directory of the .api_filters files that should be used to filter references.
+	 * 
+	 * The .api_filters files specify specific problems to ignore during api analysis. During the use scan, the 
+	 * problem filters will be converted to a list of references that will be filtered from the use scan results.
+	 *
+	 * The root is specified using an absolute path.
+	 * The root needs to contain the following structure: 
+	 * <pre>
+	 * root
+	 *  |
+	 *  +-- component name (i.e. org.eclipse.jface)
+	 *         |
+	 *         +--- .api_filters
+	 * </pre>
+	 *
+	 * @param filters the root of the .api_filters files
+	 */
+	public void setFilters(String filtersRoot) {
+		this.filterRoot = filtersRoot; 
+	}
 }