1 /*******************************************************************************
2  * Copyright (c) 2009, 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.search;
15 
16 import java.io.BufferedWriter;
17 import java.io.File;
18 import java.io.FileInputStream;
19 import java.io.FileNotFoundException;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStreamWriter;
23 import java.nio.charset.StandardCharsets;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Iterator;
27 import java.util.Map.Entry;
28 
29 import javax.xml.parsers.DocumentBuilder;
30 import javax.xml.parsers.DocumentBuilderFactory;
31 import javax.xml.parsers.FactoryConfigurationError;
32 import javax.xml.parsers.ParserConfigurationException;
33 
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.pde.api.tools.internal.IApiXmlConstants;
36 import org.eclipse.pde.api.tools.internal.builder.Reference;
37 import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
38 import org.eclipse.pde.api.tools.internal.provisional.VisibilityModifiers;
39 import org.eclipse.pde.api.tools.internal.provisional.builder.IReference;
40 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IComponentDescriptor;
41 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IElementDescriptor;
42 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IFieldDescriptor;
43 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IMemberDescriptor;
44 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IMethodDescriptor;
45 import org.eclipse.pde.api.tools.internal.provisional.descriptors.IReferenceTypeDescriptor;
46 import org.eclipse.pde.api.tools.internal.provisional.model.IApiMember;
47 import org.eclipse.pde.api.tools.internal.util.Signatures;
48 import org.eclipse.pde.api.tools.internal.util.Util;
49 import org.w3c.dom.Document;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.NodeList;
52 import org.xml.sax.SAXException;
53 import org.xml.sax.helpers.DefaultHandler;
54 
55 /**
56  * Writes reference descriptions to XML files.
57  *
58  * @since 1.0.1
59  */
60 public class XmlReferenceDescriptorWriter {
61 
62 	/**
63 	 * file names for the output reference files
64 	 */
65 	public static final String TYPE_REFERENCES = "type_references"; //$NON-NLS-1$
66 	public static final String METHOD_REFERENCES = "method_references"; //$NON-NLS-1$
67 	public static final String FIELD_REFERENCES = "field_references"; //$NON-NLS-1$
68 	private static final Integer V_ILLEGAL = Integer.valueOf(VisibilityModifiers.ILLEGAL_API);
69 	private String fLocation = null;
70 	private HashMap<String, HashMap<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>>> fReferenceMap = null;
71 	private DocumentBuilder parser = null;
72 
73 	/**
74 	 * Alternate API component where references were unresolved, or
75 	 * <code>null</code> if not to be reported.
76 	 */
77 	private IComponentDescriptor alternate;
78 
79 	/**
80 	 * Constructor
81 	 *
82 	 * @param location the absolute path in the local file system to the folder
83 	 *            to write the reports to
84 	 * @param debug if debugging infos should be written out to the console
85 	 */
XmlReferenceDescriptorWriter(String location)86 	public XmlReferenceDescriptorWriter(String location) {
87 		fLocation = location;
88 		try {
89 			parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
90 			parser.setErrorHandler(new DefaultHandler());
91 		} catch (FactoryConfigurationError | ParserConfigurationException pce) {
92 			ApiPlugin.log(pce);
93 		}
94 	}
95 
96 	/**
97 	 * Writes the given references to XML files.
98 	 *
99 	 * @param references
100 	 */
writeReferences(IReferenceDescriptor[] references)101 	public void writeReferences(IReferenceDescriptor[] references) {
102 		if (fLocation != null) {
103 			try {
104 				File parent = new File(fLocation);
105 				if (!parent.exists()) {
106 					parent.mkdirs();
107 				}
108 				collateResults(references);
109 				writeXML(parent);
110 			} catch (Exception e) {
111 				ApiPlugin.log(e);
112 			} finally {
113 				if (fReferenceMap != null) {
114 					fReferenceMap.clear();
115 					fReferenceMap = null;
116 				}
117 			}
118 		}
119 	}
120 
121 	/**
122 	 * Collates the results into like reference kinds. If two references have
123 	 * the same reference, referencer, type, visibility, and member, one will be
124 	 * removed (even if the line numbers differ). Updates {@link #fReferenceMap}
125 	 * with a map based tree structure as follows:
126 	 *
127 	 * <pre>
128 	 * Returned Map (Referenced Component ID -> rmap)
129 	 * rmap (Referencing Component ID -> mmap)
130 	 * mmap (Visibility -> vmap)
131 	 * vmap (Reference Type -> tmap)
132 	 * tmap (Referenced Member -> Reference Descriptor)
133 	 * </pre>
134 	 *
135 	 * @param references
136 	 */
collateResults(IReferenceDescriptor[] references)137 	private void collateResults(IReferenceDescriptor[] references) throws CoreException {
138 		if (fReferenceMap == null) {
139 			fReferenceMap = new HashMap<>();
140 		}
141 		Integer type = null;
142 		Integer visibility = null;
143 		String id = null;
144 		String tname = null;
145 		HashMap<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>> rmap = null;
146 		HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>> mmap = null;
147 		HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>> vmap = null;
148 		HashMap<String, HashSet<IReferenceDescriptor>> tmap = null;
149 		HashSet<IReferenceDescriptor> reflist = null;
150 		IComponentDescriptor rcomponent = null;
151 		IComponentDescriptor mcomponent = null;
152 		for (int i = 0; i < references.length; i++) {
153 			rcomponent = references[i].getReferencedComponent();
154 			id = getId(rcomponent);
155 			rmap = fReferenceMap.get(id);
156 			if (rmap == null) {
157 				rmap = new HashMap<>();
158 				fReferenceMap.put(id, rmap);
159 			}
160 			mcomponent = references[i].getComponent();
161 			id = getId(mcomponent);
162 			mmap = rmap.get(id);
163 			if (mmap == null) {
164 				mmap = new HashMap<>();
165 				rmap.put(id, mmap);
166 			}
167 			if ((references[i].getReferenceFlags() & IReference.F_ILLEGAL) > 0) {
168 				visibility = V_ILLEGAL;
169 			} else {
170 				visibility = Integer.valueOf(references[i].getVisibility());
171 			}
172 			vmap = mmap.get(visibility);
173 			if (vmap == null) {
174 				vmap = new HashMap<>();
175 				mmap.put(visibility, vmap);
176 			}
177 			type = Integer.valueOf(references[i].getReferenceType());
178 			tmap = vmap.get(type);
179 			if (tmap == null) {
180 				tmap = new HashMap<>();
181 				vmap.put(type, tmap);
182 			}
183 			tname = getText(references[i].getReferencedMember());
184 			reflist = tmap.get(tname);
185 			if (reflist == null) {
186 				reflist = new HashSet<>();
187 				tmap.put(tname, reflist);
188 			}
189 			reflist.add(references[i]);
190 		}
191 	}
192 
193 	/**
194 	 * Resolves the id to use for the component in the mapping
195 	 *
196 	 * @param component
197 	 * @return the id to use for the component in the mapping, includes the
198 	 *         version information as well
199 	 * @throws CoreException
200 	 */
getId(IComponentDescriptor component)201 	String getId(IComponentDescriptor component) throws CoreException {
202 		StringBuilder buffer = new StringBuilder();
203 		buffer.append(component.getId()).append(" ").append('(').append(component.getVersion()).append(')'); //$NON-NLS-1$
204 		return buffer.toString();
205 	}
206 
207 	/**
208 	 * Returns a formatted version of the references xml file name for use
209 	 * during conversion via the default XSLT file
210 	 *
211 	 * @param groupname
212 	 * @return a formatted version of the references file name
213 	 */
getFormattedTypeName(String groupname)214 	private String getFormattedTypeName(String groupname) {
215 		if (TYPE_REFERENCES.equals(groupname)) {
216 			return "Types"; //$NON-NLS-1$
217 		}
218 		if (METHOD_REFERENCES.equals(groupname)) {
219 			return "Methods"; //$NON-NLS-1$
220 		}
221 		if (FIELD_REFERENCES.equals(groupname)) {
222 			return "Fields"; //$NON-NLS-1$
223 		}
224 		return "unknown references"; //$NON-NLS-1$
225 	}
226 
227 	/**
228 	 * Returns the name for the file of references base on the given type
229 	 *
230 	 * @param type
231 	 * @return
232 	 */
getRefTypeName(int type)233 	private String getRefTypeName(int type) {
234 		switch (type) {
235 			case IReference.T_TYPE_REFERENCE:
236 				return TYPE_REFERENCES;
237 			case IReference.T_METHOD_REFERENCE:
238 				return METHOD_REFERENCES;
239 			case IReference.T_FIELD_REFERENCE:
240 				return FIELD_REFERENCES;
241 			default:
242 				break;
243 		}
244 		return "unknown_reference_kinds"; //$NON-NLS-1$
245 	}
246 
247 	/**
248 	 * Writes out the XML for the given api element using the collated
249 	 * {@link IReference}s
250 	 *
251 	 * @param parent
252 	 * @throws CoreException
253 	 * @throws FileNotFoundException
254 	 * @throws IOException
255 	 */
writeXML(File parent)256 	private void writeXML(File parent) throws CoreException, FileNotFoundException, IOException {
257 		HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>> vismap = null;
258 		HashMap<String, HashSet<IReferenceDescriptor>> typemap = null;
259 		HashMap<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>> rmap = null;
260 		HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>> mmap = null;
261 		Integer type = null;
262 		Integer vis = null;
263 		String id = null;
264 		String referee = null;
265 		File root = null;
266 		File location = null;
267 		File base = null;
268 		for (Iterator<Entry<String, HashMap<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>>>> iter = fReferenceMap.entrySet().iterator(); iter.hasNext();) {
269 			Entry<String, HashMap<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>>> entry = iter.next();
270 			id = entry.getKey();
271 			referee = id;
272 			base = new File(parent, id);
273 			if (!base.exists()) {
274 				base.mkdir();
275 			}
276 			rmap = entry.getValue();
277 			for (Iterator<Entry<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>>> iter2 = rmap.entrySet().iterator(); iter2.hasNext();) {
278 				Entry<String, HashMap<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>> entry2 = iter2.next();
279 				id = entry2.getKey();
280 				root = new File(base, id);
281 				if (!root.exists()) {
282 					root.mkdir();
283 				}
284 				mmap = entry2.getValue();
285 				for (Iterator<Entry<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>>> iter4 = mmap.entrySet().iterator(); iter4.hasNext();) {
286 					Entry<Integer, HashMap<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>> entry3 = iter4.next();
287 					vis = entry3.getKey();
288 					location = new File(root, VisibilityModifiers.getVisibilityName(vis.intValue()));
289 					if (!location.exists()) {
290 						location.mkdir();
291 					}
292 					vismap = entry3.getValue();
293 					for (Iterator<Entry<Integer, HashMap<String, HashSet<IReferenceDescriptor>>>> iter3 = vismap.entrySet().iterator(); iter3.hasNext();) {
294 						Entry<Integer, HashMap<String, HashSet<IReferenceDescriptor>>> entry4 = iter3.next();
295 						type = entry4.getKey();
296 						typemap = entry4.getValue();
297 						writeGroup(id, referee, location, getRefTypeName(type.intValue()), typemap, vis.intValue());
298 					}
299 				}
300 			}
301 		}
302 	}
303 
304 	/**
305 	 * Writes out a group of references under the newly created element with the
306 	 * given name
307 	 *
308 	 * @param origin the name of the bundle that has the references in it
309 	 * @param referee the name of the bundle that is referenced
310 	 * @param parent
311 	 * @param name
312 	 * @param map
313 	 * @param visibility
314 	 */
writeGroup(String origin, String referee, File parent, String name, HashMap<String, HashSet<IReferenceDescriptor>> map, int visibility)315 	private void writeGroup(String origin, String referee, File parent, String name, HashMap<String, HashSet<IReferenceDescriptor>> map, int visibility) throws CoreException, FileNotFoundException, IOException {
316 		if (parent.exists()) {
317 			BufferedWriter writer = null;
318 			try {
319 				Document doc = null;
320 				Element root = null;
321 				int count = 0;
322 				File out = new File(parent, name + ".xml"); //$NON-NLS-1$
323 				if (out.exists()) {
324 					try {
325 						FileInputStream inputStream = null;
326 						try {
327 							inputStream = new FileInputStream(out);
328 							doc = this.parser.parse(inputStream);
329 						} catch (IOException e) {
330 							e.printStackTrace();
331 						} finally {
332 							if (inputStream != null) {
333 								inputStream.close();
334 							}
335 						}
336 						if (doc == null) {
337 							return;
338 						}
339 						root = doc.getDocumentElement();
340 						String value = root.getAttribute(IApiXmlConstants.ATTR_REFERENCE_COUNT);
341 						count = Integer.parseInt(value);
342 					} catch (SAXException se) {
343 						se.printStackTrace();
344 					}
345 				} else {
346 					doc = Util.newDocument();
347 					root = doc.createElement(IApiXmlConstants.REFERENCES);
348 					doc.appendChild(root);
349 					root.setAttribute(IApiXmlConstants.ATTR_REFERENCE_VISIBILITY, Integer.toString(visibility));
350 					root.setAttribute(IApiXmlConstants.ATTR_ORIGIN, origin);
351 					root.setAttribute(IApiXmlConstants.ATTR_REFEREE, referee);
352 					root.setAttribute(IApiXmlConstants.ATTR_NAME, getFormattedTypeName(name));
353 					if (alternate != null) {
354 						root.setAttribute(IApiXmlConstants.ATTR_ALTERNATE, getId(alternate));
355 					}
356 				}
357 				if (doc == null || root == null) {
358 					return;
359 				}
360 				String tname = null;
361 				HashSet<IReferenceDescriptor> refs = null;
362 				Element telement = null;
363 				for (Iterator<Entry<String, HashSet<IReferenceDescriptor>>> iter = map.entrySet().iterator(); iter.hasNext();) {
364 					Entry<String, HashSet<IReferenceDescriptor>> entry = iter.next();
365 					tname = entry.getKey();
366 					telement = findTypeElement(root, tname);
367 					if (telement == null) {
368 						telement = doc.createElement(IApiXmlConstants.ELEMENT_TARGET);
369 						telement.setAttribute(IApiXmlConstants.ATTR_NAME, tname);
370 						root.appendChild(telement);
371 					}
372 					refs = entry.getValue();
373 					if (refs != null) {
374 						for (Iterator<IReferenceDescriptor> iter2 = refs.iterator(); iter2.hasNext();) {
375 							count++;
376 							IReferenceDescriptor ref = iter2.next();
377 							writeReference(doc, telement, ref);
378 							if (!iter2.hasNext()) {
379 								// set qualified referenced attributes
380 								IMemberDescriptor resolved = ref.getReferencedMember();
381 								if (resolved != null) {
382 									addMemberDetails(telement, resolved);
383 								}
384 							}
385 						}
386 					}
387 				}
388 				root.setAttribute(IApiXmlConstants.ATTR_REFERENCE_COUNT, Integer.toString(count));
389 				writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8));
390 				writer.write(Util.serializeDocument(doc));
391 				writer.flush();
392 			} finally {
393 				if (writer != null) {
394 					writer.close();
395 				}
396 			}
397 		}
398 	}
399 
400 	/**
401 	 * Add member descriptor details to the given element.
402 	 *
403 	 * @param element XML element
404 	 * @param member member to add details for
405 	 */
addMemberDetails(Element element, IMemberDescriptor member)406 	private void addMemberDetails(Element element, IMemberDescriptor member) {
407 		switch (member.getElementType()) {
408 			case IElementDescriptor.TYPE:
409 				element.setAttribute(IApiXmlConstants.ATTR_TYPE, ((IReferenceTypeDescriptor) member).getQualifiedName());
410 				break;
411 			case IElementDescriptor.FIELD:
412 				IReferenceTypeDescriptor encl = member.getEnclosingType();
413 				element.setAttribute(IApiXmlConstants.ATTR_TYPE, encl.getQualifiedName());
414 				element.setAttribute(IApiXmlConstants.ATTR_MEMBER_NAME, member.getName());
415 				break;
416 			case IElementDescriptor.METHOD:
417 				encl = member.getEnclosingType();
418 				element.setAttribute(IApiXmlConstants.ATTR_TYPE, encl.getQualifiedName());
419 				element.setAttribute(IApiXmlConstants.ATTR_MEMBER_NAME, member.getName());
420 				element.setAttribute(IApiXmlConstants.ATTR_SIGNATURE, ((IMethodDescriptor) member).getSignature());
421 				break;
422 			default:
423 				break;
424 		}
425 	}
426 
427 	/**
428 	 * gets the root kind element
429 	 *
430 	 * @param root
431 	 * @param kind
432 	 * @return
433 	 */
findTypeElement(Element root, String tname)434 	private Element findTypeElement(Element root, String tname) {
435 		if (tname == null) {
436 			return null;
437 		}
438 		Element kelement = null;
439 		NodeList nodes = root.getElementsByTagName(IApiXmlConstants.ELEMENT_TARGET);
440 		for (int i = 0; i < nodes.getLength(); i++) {
441 			kelement = (Element) nodes.item(i);
442 			if (tname.equals(kelement.getAttribute(IApiXmlConstants.ATTR_NAME))) {
443 				return kelement;
444 			}
445 		}
446 		return null;
447 	}
448 
449 	/**
450 	 * gets the root kind element
451 	 *
452 	 * @param root
453 	 * @param kind
454 	 * @return
455 	 */
findKindElement(Element root, Integer kind)456 	private Element findKindElement(Element root, Integer kind) {
457 		Element kelement = null;
458 		NodeList nodes = root.getElementsByTagName(IApiXmlConstants.REFERENCE_KIND);
459 		for (int i = 0; i < nodes.getLength(); i++) {
460 			kelement = (Element) nodes.item(i);
461 			if (kind.toString().equals(kelement.getAttribute(IApiXmlConstants.ATTR_KIND))) {
462 				return kelement;
463 			}
464 		}
465 		return null;
466 	}
467 
468 	/**
469 	 * Writes the attributes from the given {@link IReference} into a new
470 	 * {@link Element} that is added to the given parent.
471 	 *
472 	 * @param document
473 	 * @param parent
474 	 * @param reference
475 	 */
writeReference(Document document, Element parent, IReferenceDescriptor reference)476 	private void writeReference(Document document, Element parent, IReferenceDescriptor reference) throws CoreException {
477 		Element kelement = null;
478 		Integer kind = Integer.valueOf(reference.getReferenceKind());
479 		kelement = findKindElement(parent, kind);
480 		if (kelement == null) {
481 			kelement = document.createElement(IApiXmlConstants.REFERENCE_KIND);
482 			kelement.setAttribute(IApiXmlConstants.ATTR_REFERENCE_KIND_NAME, Reference.getReferenceText(kind.intValue()));
483 			kelement.setAttribute(IApiXmlConstants.ATTR_KIND, kind.toString());
484 			kelement.setAttribute(IApiXmlConstants.ATTR_FLAGS, Integer.toString(reference.getReferenceFlags()));
485 			parent.appendChild(kelement);
486 		}
487 		Element relement = document.createElement(IApiXmlConstants.ATTR_REFERENCE);
488 		IMemberDescriptor member = reference.getMember();
489 		relement.setAttribute(IApiXmlConstants.ATTR_ORIGIN, getText(member));
490 		String[] messages = reference.getProblemMessages();
491 		if (messages != null) {
492 			relement.setAttribute(IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENTS, getText(messages));
493 		}
494 		// add detailed information about origin
495 		addMemberDetails(relement, member);
496 		member = reference.getReferencedMember();
497 		if (member != null) {
498 			relement.setAttribute(IApiXmlConstants.ATTR_LINE_NUMBER, Integer.toString(reference.getLineNumber()));
499 			kelement.appendChild(relement);
500 		}
501 	}
502 
503 	/**
504 	 * Gets the {@link String} value of the given array by calling
505 	 * {@link #toString()} on each of the elements in the array.
506 	 *
507 	 * @param array the array to convert to a string
508 	 * @return the {@link String} or an empty {@link String} never
509 	 *         <code>null</code>
510 	 * @since 1.1
511 	 */
getText(Object[] array)512 	String getText(Object[] array) {
513 		StringBuilder buffer = new StringBuilder();
514 		for (int i = 0; i < array.length; i++) {
515 			buffer.append(array[i].toString());
516 			if (i < array.length - 1) {
517 				buffer.append(","); //$NON-NLS-1$
518 			}
519 		}
520 		return buffer.toString();
521 	}
522 
523 	/**
524 	 * Returns the text to set in the attribute for the given {@link IApiMember}
525 	 *
526 	 * @param member
527 	 * @return
528 	 * @throws CoreException
529 	 */
getText(IMemberDescriptor member)530 	private String getText(IMemberDescriptor member) throws CoreException {
531 		switch (member.getElementType()) {
532 			case IElementDescriptor.TYPE:
533 				return Signatures.getQualifiedTypeSignature((IReferenceTypeDescriptor) member);
534 			case IElementDescriptor.METHOD:
535 				return Signatures.getQualifiedMethodSignature((IMethodDescriptor) member);
536 			case IElementDescriptor.FIELD:
537 				return Signatures.getQualifiedFieldSignature((IFieldDescriptor) member);
538 			default:
539 				break;
540 		}
541 		return null;
542 	}
543 
544 	/**
545 	 * Sets the alternate component where references were unresolved, or
546 	 * <code>null</code> if none.
547 	 *
548 	 * @param other component descriptor or <code>null</code>
549 	 */
setAlternate(IComponentDescriptor other)550 	public void setAlternate(IComponentDescriptor other) {
551 		alternate = other;
552 	}
553 }
554