1 | /******************************************************************************* |
2 | * Copyright (c) 2008, 2009 IBM Corporation and others. |
3 | * All rights reserved. This program and the accompanying materials |
4 | * are made available under the terms of the Eclipse Public License v1.0 |
5 | * which accompanies this distribution, and is available at |
6 | * http://www.eclipse.org/legal/epl-v10.html |
7 | * |
8 | * Contributors: |
9 | * IBM Corporation - initial API and implementation |
10 | *******************************************************************************/ |
11 | package org.eclipse.pde.api.tools.internal; |
12 | |
13 | import java.io.BufferedInputStream; |
14 | import java.io.File; |
15 | import java.io.FileInputStream; |
16 | import java.io.IOException; |
17 | import java.util.ArrayList; |
18 | import java.util.HashMap; |
19 | import java.util.Iterator; |
20 | import java.util.List; |
21 | import java.util.Map; |
22 | import java.util.Map.Entry; |
23 | import java.util.jar.JarFile; |
24 | |
25 | import org.eclipse.core.resources.ISaveContext; |
26 | import org.eclipse.core.resources.ISaveParticipant; |
27 | import org.eclipse.core.runtime.CoreException; |
28 | import org.eclipse.core.runtime.IPath; |
29 | import org.eclipse.core.runtime.IStatus; |
30 | import org.eclipse.core.runtime.Status; |
31 | import org.eclipse.jdt.core.ElementChangedEvent; |
32 | import org.eclipse.jdt.core.ICompilationUnit; |
33 | import org.eclipse.jdt.core.IElementChangedListener; |
34 | import org.eclipse.jdt.core.IJavaElement; |
35 | import org.eclipse.jdt.core.IJavaElementDelta; |
36 | import org.eclipse.jdt.core.IJavaProject; |
37 | import org.eclipse.jdt.core.IPackageFragment; |
38 | import org.eclipse.jdt.core.IType; |
39 | import org.eclipse.jdt.core.JavaCore; |
40 | import org.eclipse.osgi.service.resolver.BundleDescription; |
41 | import org.eclipse.pde.api.tools.internal.ApiDescription.ManifestNode; |
42 | import org.eclipse.pde.api.tools.internal.ProjectApiDescription.TypeNode; |
43 | import org.eclipse.pde.api.tools.internal.model.ApiModelCache; |
44 | import org.eclipse.pde.api.tools.internal.model.PluginProjectApiComponent; |
45 | import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; |
46 | import org.eclipse.pde.api.tools.internal.provisional.Factory; |
47 | import org.eclipse.pde.api.tools.internal.provisional.IApiDescription; |
48 | import org.eclipse.pde.api.tools.internal.provisional.descriptors.IElementDescriptor; |
49 | import org.eclipse.pde.api.tools.internal.provisional.descriptors.IReferenceTypeDescriptor; |
50 | import org.eclipse.pde.api.tools.internal.provisional.model.IApiElement; |
51 | import org.eclipse.pde.api.tools.internal.provisional.scanner.ScannerMessages; |
52 | import org.eclipse.pde.api.tools.internal.util.Util; |
53 | import org.w3c.dom.Element; |
54 | import org.w3c.dom.Node; |
55 | import org.w3c.dom.NodeList; |
56 | |
57 | import com.ibm.icu.text.MessageFormat; |
58 | |
59 | /** |
60 | * Manages a cache of API descriptions for Java projects. Descriptions |
61 | * are re-used between API components for the same project. |
62 | * |
63 | * @since 1.0 |
64 | */ |
65 | public final class ApiDescriptionManager implements IElementChangedListener, ISaveParticipant { |
66 | |
67 | /** |
68 | * Singleton |
69 | */ |
70 | private static ApiDescriptionManager fgDefault; |
71 | |
72 | /** |
73 | * Maps Java projects to API descriptions |
74 | */ |
75 | private Map fDescriptions = new HashMap(); |
76 | |
77 | /** |
78 | * Path to the local directory where API descriptions are cached |
79 | * per project. |
80 | */ |
81 | public static final IPath API_DESCRIPTIONS_CONTAINER_PATH = |
82 | ApiPlugin.getDefault().getStateLocation(); |
83 | |
84 | /** |
85 | * Constructs an API description manager. |
86 | */ |
87 | private ApiDescriptionManager() { |
88 | JavaCore.addElementChangedListener(this, ElementChangedEvent.POST_CHANGE); |
89 | ApiPlugin.getDefault().addSaveParticipant(this); |
90 | } |
91 | |
92 | /** |
93 | * Cleans up Java element listener |
94 | */ |
95 | public static void shutdown() { |
96 | if (fgDefault != null) { |
97 | JavaCore.removeElementChangedListener(fgDefault); |
98 | ApiPlugin.getDefault().removeSaveParticipant(fgDefault); |
99 | } |
100 | } |
101 | |
102 | /** |
103 | * Returns the singleton API description manager. |
104 | * |
105 | * @return API description manager |
106 | */ |
107 | public synchronized static ApiDescriptionManager getDefault() { |
108 | if (fgDefault == null) { |
109 | fgDefault = new ApiDescriptionManager(); |
110 | } |
111 | return fgDefault; |
112 | } |
113 | |
114 | /** |
115 | * Returns an API description for the given project component and connect it to the |
116 | * given bundle description. |
117 | * |
118 | * @param project Java project |
119 | * @return API description |
120 | */ |
121 | public synchronized IApiDescription getApiDescription(PluginProjectApiComponent component, BundleDescription bundle) { |
122 | IJavaProject project = component.getJavaProject(); |
123 | ProjectApiDescription description = (ProjectApiDescription) fDescriptions.get(project); |
124 | if (description == null) { |
125 | if (Util.isApiProject(project)) { |
126 | description = new ProjectApiDescription(project); |
127 | } else { |
128 | description = new NonApiProjectDescription(project); |
129 | } |
130 | try { |
131 | restoreDescription(project, description); |
132 | } catch (CoreException e) { |
133 | ApiPlugin.log(e.getStatus()); |
134 | description = new ProjectApiDescription(project); |
135 | } |
136 | fDescriptions.put(project, description); |
137 | } |
138 | return description; |
139 | } |
140 | /** |
141 | * Cleans the API description for the given project. |
142 | * |
143 | * @param project |
144 | * @param delete whether to delete the file on disk |
145 | * @param remove whether to remove the cached API description |
146 | */ |
147 | public synchronized void clean(IJavaProject project, boolean delete, boolean remove) { |
148 | ProjectApiDescription desc = null; |
149 | if (remove) { |
150 | desc = (ProjectApiDescription) fDescriptions.remove(project); |
151 | } else { |
152 | desc = (ProjectApiDescription) fDescriptions.get(project); |
153 | } |
154 | if (desc != null) { |
155 | desc.clean(); |
156 | } |
157 | if (delete) { |
158 | File file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()) |
159 | .append(IApiCoreConstants.API_DESCRIPTION_XML_NAME).toFile(); |
160 | if (file.exists()) { |
161 | file.delete(); |
162 | } |
163 | file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).toFile(); |
164 | if(file.exists() && file.isDirectory()) { |
165 | file.delete(); |
166 | } |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Notifies the API description that the underlying project has changed. |
172 | * |
173 | * @param project |
174 | */ |
175 | synchronized void projectChanged(IJavaProject project) { |
176 | ProjectApiDescription desc = (ProjectApiDescription) fDescriptions.get(project); |
177 | if (desc != null) { |
178 | desc.projectChanged(); |
179 | } |
180 | } |
181 | |
182 | /** |
183 | * Notifies the API description that the underlying project classpath has changed. |
184 | * |
185 | * @param project |
186 | */ |
187 | synchronized void projectClasspathChanged(IJavaProject project) { |
188 | ProjectApiDescription desc = (ProjectApiDescription) fDescriptions.get(project); |
189 | if (desc != null) { |
190 | desc.projectClasspathChanged(); |
191 | } |
192 | } |
193 | |
194 | /* (non-Javadoc) |
195 | * @see org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse.jdt.core.ElementChangedEvent) |
196 | */ |
197 | public void elementChanged(ElementChangedEvent event) { |
198 | IJavaElementDelta delta = event.getDelta(); |
199 | processJavaElementDeltas(delta.getAffectedChildren(), null); |
200 | } |
201 | |
202 | /** |
203 | * Remove projects that get closed or removed. |
204 | * |
205 | * @param deltas |
206 | */ |
207 | private synchronized void processJavaElementDeltas(IJavaElementDelta[] deltas, IJavaProject proj) { |
208 | IJavaElementDelta delta = null; |
209 | for(int i = 0; i < deltas.length; i++) { |
210 | delta = deltas[i]; |
211 | switch(delta.getElement().getElementType()) { |
212 | case IJavaElement.JAVA_PROJECT: { |
213 | IJavaProject jproj = (IJavaProject) delta.getElement(); |
214 | switch (delta.getKind()) { |
215 | case IJavaElementDelta.CHANGED: |
216 | int flags = delta.getFlags(); |
217 | if((flags & IJavaElementDelta.F_CLOSED) != 0) { |
218 | clean(jproj, false, true); |
219 | flushElementCache(delta); |
220 | } else if((flags & (IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED |
221 | | IJavaElementDelta.F_CLASSPATH_CHANGED)) != 0) { |
222 | if (jproj != null) { |
223 | projectClasspathChanged(jproj); |
224 | flushElementCache(delta); |
225 | } |
226 | } else if((flags & IJavaElementDelta.F_CONTENT) != 0) { |
227 | if (jproj != null) { |
228 | processJavaElementDeltas(delta.getAffectedChildren(), jproj); |
229 | } |
230 | } else if ((flags & IJavaElementDelta.F_CHILDREN) != 0) { |
231 | processJavaElementDeltas(delta.getAffectedChildren(), jproj); |
232 | } |
233 | break; |
234 | case IJavaElementDelta.REMOVED: |
235 | clean(jproj, true, true); |
236 | flushElementCache(delta); |
237 | break; |
238 | } |
239 | break; |
240 | } |
241 | case IJavaElement.PACKAGE_FRAGMENT : { |
242 | int flags = delta.getFlags(); |
243 | if ((flags & IJavaElementDelta.F_CHILDREN) != 0) { |
244 | processJavaElementDeltas(delta.getAffectedChildren(), proj); |
245 | } |
246 | break; |
247 | } |
248 | case IJavaElement.PACKAGE_FRAGMENT_ROOT : { |
249 | int flags = delta.getFlags(); |
250 | if ((flags & (IJavaElementDelta.F_ARCHIVE_CONTENT_CHANGED |
251 | | IJavaElementDelta.F_ADDED_TO_CLASSPATH |
252 | | IJavaElementDelta.F_REMOVED_FROM_CLASSPATH)) != 0) { |
253 | projectClasspathChanged(proj); |
254 | } else if ((flags & IJavaElementDelta.F_CHILDREN) != 0) { |
255 | processJavaElementDeltas(delta.getAffectedChildren(), proj); |
256 | } |
257 | break; |
258 | } |
259 | case IJavaElement.COMPILATION_UNIT : { |
260 | int flags = delta.getFlags(); |
261 | switch (delta.getKind()) { |
262 | case IJavaElementDelta.CHANGED: { |
263 | if ((flags & (IJavaElementDelta.F_CONTENT | |
264 | IJavaElementDelta.F_FINE_GRAINED | |
265 | IJavaElementDelta.F_PRIMARY_RESOURCE)) != 0){ |
266 | if (proj != null) { |
267 | projectChanged(proj); |
268 | flushElementCache(delta); |
269 | continue; |
270 | } |
271 | } |
272 | break; |
273 | } |
274 | case IJavaElementDelta.ADDED : |
275 | case IJavaElementDelta.REMOVED : { |
276 | if (proj != null) { |
277 | projectChanged(proj); |
278 | flushElementCache(delta); |
279 | continue; |
280 | } |
281 | } |
282 | } |
283 | } |
284 | } |
285 | } |
286 | } |
287 | |
288 | /** |
289 | * Flushes the changed element from the model cache |
290 | * @param delta |
291 | */ |
292 | void flushElementCache(IJavaElementDelta delta) { |
293 | IJavaElement element = delta.getElement(); |
294 | if((delta.getFlags() & IJavaElementDelta.F_MOVED_TO) > 0) { |
295 | element = delta.getMovedToElement(); |
296 | } |
297 | if((delta.getFlags() & IJavaElementDelta.F_MOVED_FROM) > 0) { |
298 | element = delta.getMovedFromElement(); |
299 | } |
300 | switch(element.getElementType()) { |
301 | case IJavaElement.COMPILATION_UNIT: { |
302 | ICompilationUnit unit = (ICompilationUnit) element; |
303 | IType type = unit.findPrimaryType(); |
304 | if(type != null) { |
305 | ApiModelCache.getCache().removeElementInfo( |
306 | ApiBaselineManager.WORKSPACE_API_BASELINE_ID, |
307 | element.getJavaProject().getElementName(), |
308 | type.getFullyQualifiedName(), |
309 | IApiElement.TYPE); |
310 | } |
311 | break; |
312 | } |
313 | case IJavaElement.JAVA_PROJECT: { |
314 | ApiModelCache.getCache().removeElementInfo( |
315 | ApiBaselineManager.WORKSPACE_API_BASELINE_ID, |
316 | element.getElementName(), |
317 | null, |
318 | IApiElement.COMPONENT); |
319 | break; |
320 | } |
321 | } |
322 | } |
323 | |
324 | /* (non-Javadoc) |
325 | * @see org.eclipse.core.resources.ISaveParticipant#doneSaving(org.eclipse.core.resources.ISaveContext) |
326 | */ |
327 | public void doneSaving(ISaveContext context) { |
328 | } |
329 | |
330 | /* (non-Javadoc) |
331 | * @see org.eclipse.core.resources.ISaveParticipant#prepareToSave(org.eclipse.core.resources.ISaveContext) |
332 | */ |
333 | public void prepareToSave(ISaveContext context) throws CoreException { |
334 | } |
335 | |
336 | /* (non-Javadoc) |
337 | * @see org.eclipse.core.resources.ISaveParticipant#rollback(org.eclipse.core.resources.ISaveContext) |
338 | */ |
339 | public void rollback(ISaveContext context) { |
340 | } |
341 | |
342 | /* (non-Javadoc) |
343 | * @see org.eclipse.core.resources.ISaveParticipant#saving(org.eclipse.core.resources.ISaveContext) |
344 | */ |
345 | public synchronized void saving(ISaveContext context) throws CoreException { |
346 | Iterator entries = fDescriptions.entrySet().iterator(); |
347 | while (entries.hasNext()) { |
348 | Entry entry = (Entry) entries.next(); |
349 | IJavaProject project = (IJavaProject) entry.getKey(); |
350 | ProjectApiDescription desc = (ProjectApiDescription) entry.getValue(); |
351 | if (desc.isModified()) { |
352 | File dir = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).toFile(); |
353 | dir.mkdirs(); |
354 | String xml = desc.getXML(); |
355 | try { |
356 | Util.saveFile(new File(dir, IApiCoreConstants.API_DESCRIPTION_XML_NAME), xml); |
357 | } catch (IOException e) { |
358 | abort(MessageFormat.format(ScannerMessages.ApiDescriptionManager_0, new String[]{project.getElementName()}), e); |
359 | } |
360 | } |
361 | } |
362 | } |
363 | |
364 | /** |
365 | * Restores the API description from its saved file, if any and returns |
366 | * true if successful. |
367 | * |
368 | * @param project |
369 | * @param description |
370 | * @return whether the restore succeeded |
371 | * @throws CoreException |
372 | */ |
373 | private boolean restoreDescription(IJavaProject project, ProjectApiDescription description) throws CoreException { |
374 | File file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()). |
375 | append(IApiCoreConstants.API_DESCRIPTION_XML_NAME).toFile(); |
376 | if (file.exists()) { |
377 | BufferedInputStream stream = null; |
378 | try { |
379 | stream = new BufferedInputStream(new FileInputStream(file)); |
380 | String xml = new String(Util.getInputStreamAsCharArray(stream, -1, IApiCoreConstants.UTF_8)); |
381 | Element root = Util.parseDocument(xml); |
382 | if (!root.getNodeName().equals(IApiXmlConstants.ELEMENT_COMPONENT)) { |
383 | abort(ScannerMessages.ComponentXMLScanner_0, null); |
384 | } |
385 | long timestamp = getLong(root, IApiXmlConstants.ATTR_MODIFICATION_STAMP); |
386 | String version = root.getAttribute(IApiXmlConstants.ATTR_VERSION); |
387 | description.setEmbeddedVersion(version); |
388 | if (IApiXmlConstants.API_DESCRIPTION_CURRENT_VERSION.equals(version)) { |
389 | description.fPackageTimeStamp = timestamp; |
390 | description.fManifestFile = project.getProject().getFile(JarFile.MANIFEST_NAME); |
391 | restoreChildren(description, root, null, description.fPackageMap); |
392 | return true; |
393 | } |
394 | } catch (IOException e) { |
395 | abort(MessageFormat.format(ScannerMessages.ApiDescriptionManager_1, |
396 | new String[]{project.getElementName()}), e); |
397 | } finally { |
398 | if (stream != null) { |
399 | try { |
400 | stream.close(); |
401 | } catch (IOException e) { |
402 | // ignore |
403 | } |
404 | } |
405 | } |
406 | } |
407 | return false; |
408 | } |
409 | |
410 | private void restoreChildren(ProjectApiDescription apiDesc, Element element, ManifestNode parentNode, Map childrenMap) throws CoreException { |
411 | NodeList children = element.getChildNodes(); |
412 | for (int i = 0; i < children.getLength(); i++) { |
413 | Node child = children.item(i); |
414 | if (child.getNodeType() == Node.ELEMENT_NODE) { |
415 | restoreNode(apiDesc, (Element) child, parentNode, childrenMap); |
416 | } |
417 | } |
418 | } |
419 | |
420 | private void restoreNode(ProjectApiDescription apiDesc, Element element, ManifestNode parentNode, Map childrenMap) throws CoreException { |
421 | ManifestNode node = null; |
422 | IElementDescriptor elementDesc = null; |
423 | if (element.getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE)) { |
424 | int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY); |
425 | int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS); |
426 | // collect fragments |
427 | List fragments = new ArrayList(); |
428 | NodeList childNodes = element.getChildNodes(); |
429 | String pkgName = null; |
430 | for (int i = 0; i < childNodes.getLength(); i++) { |
431 | Node child = childNodes.item(i); |
432 | if (child.getNodeType() == Node.ELEMENT_NODE) { |
433 | if (((Element)child).getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE_FRAGMENT)) { |
434 | Element fragment = (Element) child; |
435 | String handle = fragment.getAttribute(IApiXmlConstants.ATTR_HANDLE); |
436 | IJavaElement je = JavaCore.create(handle); |
437 | if (je.getElementType() != IJavaElement.PACKAGE_FRAGMENT) { |
438 | abort(ScannerMessages.ApiDescriptionManager_2 + handle, null); |
439 | } |
440 | pkgName = je.getElementName(); |
441 | fragments.add(je); |
442 | } |
443 | } |
444 | } |
445 | if (!fragments.isEmpty()) { |
446 | elementDesc = Factory.packageDescriptor(pkgName); |
447 | node = apiDesc.newPackageNode((IPackageFragment[])fragments.toArray(new IPackageFragment[fragments.size()]), parentNode, elementDesc, vis, res); |
448 | } else { |
449 | abort(ScannerMessages.ApiDescriptionManager_2, null); |
450 | } |
451 | } else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE_FRAGMENT)) { |
452 | return; // nothing to do |
453 | } else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_TYPE)) { |
454 | String handle = element.getAttribute(IApiXmlConstants.ATTR_HANDLE); |
455 | int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY); |
456 | int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS); |
457 | IJavaElement je = JavaCore.create(handle); |
458 | if (je.getElementType() != IJavaElement.TYPE) { |
459 | abort(ScannerMessages.ApiDescriptionManager_3 + handle, null); |
460 | } |
461 | IType type = (IType) je; |
462 | elementDesc = Factory.typeDescriptor(type.getFullyQualifiedName('$')); |
463 | TypeNode tn = apiDesc.newTypeNode(type, parentNode, elementDesc, vis, res); |
464 | node = tn; |
465 | tn.fTimeStamp = getLong(element, IApiXmlConstants.ATTR_MODIFICATION_STAMP); |
466 | } else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_FIELD)) { |
467 | if(parentNode.element instanceof IReferenceTypeDescriptor) { |
468 | IReferenceTypeDescriptor type = (IReferenceTypeDescriptor) parentNode.element; |
469 | int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY); |
470 | int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS); |
471 | String name = element.getAttribute(IApiXmlConstants.ATTR_NAME); |
472 | elementDesc = type.getField(name); |
473 | node = apiDesc.newNode(parentNode, elementDesc, vis, res); |
474 | } |
475 | } else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_METHOD)) { |
476 | if(parentNode.element instanceof IReferenceTypeDescriptor) { |
477 | IReferenceTypeDescriptor type = (IReferenceTypeDescriptor) parentNode.element; |
478 | int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY); |
479 | int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS); |
480 | String name = element.getAttribute(IApiXmlConstants.ATTR_NAME); |
481 | String sig = element.getAttribute(IApiXmlConstants.ATTR_SIGNATURE); |
482 | if (sig.indexOf('.') != -1) { |
483 | // old files might use '.' instead of '/' |
484 | sig = sig.replace('.', '/'); |
485 | } |
486 | elementDesc = type.getMethod(name,sig); |
487 | node = apiDesc.newNode(parentNode, elementDesc, vis, res); |
488 | } |
489 | } |
490 | if (node == null) { |
491 | abort(ScannerMessages.ApiDescriptionManager_4, null); |
492 | } |
493 | childrenMap.put(elementDesc, node); |
494 | restoreChildren(apiDesc, element, node, node.children); |
495 | } |
496 | |
497 | /** |
498 | * Returns an integer attribute. |
499 | * |
500 | * @param element element with the integer |
501 | * @param attr attribute name |
502 | * @return attribute value as an integer |
503 | */ |
504 | private int getInt(Element element, String attr) { |
505 | String attribute = element.getAttribute(attr); |
506 | try { |
507 | return Integer.parseInt(attribute); |
508 | } |
509 | catch (NumberFormatException e) {} |
510 | return 0; |
511 | } |
512 | |
513 | /** |
514 | * Returns a long attribute. |
515 | * |
516 | * @param element element with the long |
517 | * @param attr attribute name |
518 | * @return attribute value as an long |
519 | */ |
520 | private long getLong(Element element, String attr) { |
521 | String attribute = element.getAttribute(attr); |
522 | if (attribute != null) { |
523 | try { |
524 | return Long.parseLong(attribute); |
525 | } |
526 | catch (NumberFormatException e) {} |
527 | } |
528 | return 0L; |
529 | } |
530 | |
531 | /** |
532 | * Throws an exception with the given message and underlying exception. |
533 | * |
534 | * @param message error message |
535 | * @param exception underlying exception, or <code>null</code> |
536 | * @throws CoreException |
537 | */ |
538 | private static void abort(String message, Throwable exception) throws CoreException { |
539 | IStatus status = new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, message, exception); |
540 | throw new CoreException(status); |
541 | } |
542 | } |