blob: 833935aff58bb9f433367bdf656fcedb297fd166 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.api.tools.internal;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.JarFile;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.pde.api.tools.internal.ApiDescription.ManifestNode;
import org.eclipse.pde.api.tools.internal.ProjectApiDescription.TypeNode;
import org.eclipse.pde.api.tools.internal.model.ApiModelCache;
import org.eclipse.pde.api.tools.internal.model.ProjectComponent;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.Factory;
import org.eclipse.pde.api.tools.internal.provisional.IApiDescription;
import org.eclipse.pde.api.tools.internal.provisional.descriptors.IElementDescriptor;
import org.eclipse.pde.api.tools.internal.provisional.descriptors.IReferenceTypeDescriptor;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiElement;
import org.eclipse.pde.api.tools.internal.provisional.scanner.ScannerMessages;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.ibm.icu.text.MessageFormat;
/**
* Manages a cache of API descriptions for Java projects. Descriptions are
* re-used between API components for the same project.
*
* @since 1.0
*/
public final class ApiDescriptionManager implements ISaveParticipant {
/**
* Singleton
*/
private static ApiDescriptionManager fgDefault;
/**
* Maps Java projects to API descriptions
*/
private Map<IJavaProject, IApiDescription> fDescriptions = new HashMap<>();
/**
* Path to the local directory where API descriptions are cached per
* project.
*/
public static final IPath API_DESCRIPTIONS_CONTAINER_PATH = ApiPlugin.getDefault().getStateLocation();
/**
* Constructs an API description manager.
*/
private ApiDescriptionManager() {
ApiPlugin.getDefault().addSaveParticipant(this);
}
/**
* Cleans up Java element listener
*/
public static void shutdown() {
if (fgDefault != null) {
ApiPlugin.getDefault().removeSaveParticipant(fgDefault);
}
}
/**
* Returns the singleton API description manager.
*
* @return API description manager
*/
public synchronized static ApiDescriptionManager getManager() {
if (fgDefault == null) {
fgDefault = new ApiDescriptionManager();
}
return fgDefault;
}
/**
* Returns an API description for the given project component and connect it
* to the given bundle description.
*
* @param project Java project
* @param bundle
*
* @return API description
*/
public synchronized IApiDescription getApiDescription(ProjectComponent component, BundleDescription bundle) {
IJavaProject project = component.getJavaProject();
ProjectApiDescription description = (ProjectApiDescription) fDescriptions.get(project);
if (description == null) {
if (Util.isApiProject(project)) {
description = new ProjectApiDescription(project);
} else {
description = new NonApiProjectDescription(project);
}
try {
restoreDescription(project, description);
} catch (CoreException e) {
ApiPlugin.log(e.getStatus());
description = new ProjectApiDescription(project);
}
fDescriptions.put(project, description);
}
return description;
}
/**
* Cleans the API description for the given project.
*
* @param project
* @param delete whether to delete the file on disk
* @param remove whether to remove the cached API description
*/
public synchronized void clean(IJavaProject project, boolean delete, boolean remove) {
ProjectApiDescription desc = null;
if (remove) {
desc = (ProjectApiDescription) fDescriptions.remove(project);
} else {
desc = (ProjectApiDescription) fDescriptions.get(project);
}
if (desc != null) {
desc.clean();
}
if (delete) {
File file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).append(IApiCoreConstants.API_DESCRIPTION_XML_NAME).toFile();
if (file.exists()) {
file.delete();
}
file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).toFile();
if (file.exists() && file.isDirectory()) {
file.delete();
}
}
}
/**
* Notifies the API description that the underlying project has changed.
*
* @param project
*/
synchronized void projectChanged(IJavaProject project) {
ProjectApiDescription desc = (ProjectApiDescription) fDescriptions.get(project);
if (desc != null) {
desc.projectChanged();
}
}
/**
* Notifies the API description that the underlying project classpath has
* changed.
*
* @param project
*/
synchronized void projectClasspathChanged(IJavaProject project) {
ProjectApiDescription desc = (ProjectApiDescription) fDescriptions.get(project);
if (desc != null) {
desc.projectClasspathChanged();
}
}
/**
* Flushes the changed element from the model cache
*
* @param element
*/
void flushElementCache(IJavaElement element) {
switch (element.getElementType()) {
case IJavaElement.COMPILATION_UNIT: {
ICompilationUnit unit = (ICompilationUnit) element;
IType type = unit.findPrimaryType();
if (type != null) {
ApiModelCache.getCache().removeElementInfo(ApiBaselineManager.WORKSPACE_API_BASELINE_ID, element.getJavaProject().getElementName(), type.getFullyQualifiedName(), IApiElement.TYPE);
}
break;
}
case IJavaElement.JAVA_PROJECT: {
ApiModelCache.getCache().removeElementInfo(ApiBaselineManager.WORKSPACE_API_BASELINE_ID, element.getElementName(), null, IApiElement.COMPONENT);
break;
}
default:
break;
}
}
@Override
public void doneSaving(ISaveContext context) {
//
}
@Override
public void prepareToSave(ISaveContext context) throws CoreException {
//
}
@Override
public void rollback(ISaveContext context) {
//
}
@Override
public synchronized void saving(ISaveContext context) throws CoreException {
if (context.getKind() == ISaveContext.PROJECT_SAVE && !Util.isJavaProject(context.getProject())) {
return;
}
Iterator<Entry<IJavaProject, IApiDescription>> entries = fDescriptions.entrySet().iterator();
while (entries.hasNext()) {
Entry<IJavaProject, IApiDescription> entry = entries.next();
IJavaProject project = entry.getKey();
ProjectApiDescription desc = (ProjectApiDescription) entry.getValue();
if (desc.isModified()) {
File dir = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).toFile();
dir.mkdirs();
String xml = desc.getXML();
try {
Util.saveFile(new File(dir, IApiCoreConstants.API_DESCRIPTION_XML_NAME), xml);
desc.setModified(false);
} catch (IOException e) {
abort(MessageFormat.format(ScannerMessages.ApiDescriptionManager_0, project.getElementName()), e);
}
}
}
}
/**
* Restores the API description from its saved file, if any and returns true
* if successful.
*
* @param project
* @param description
* @return whether the restore succeeded
* @throws CoreException
*/
private boolean restoreDescription(IJavaProject project, ProjectApiDescription description) throws CoreException {
File file = API_DESCRIPTIONS_CONTAINER_PATH.append(project.getElementName()).append(IApiCoreConstants.API_DESCRIPTION_XML_NAME).toFile();
if (file.exists()) {
BufferedInputStream stream = null;
try {
stream = new BufferedInputStream(new FileInputStream(file));
String xml = new String(Util.getInputStreamAsCharArray(stream, -1, StandardCharsets.UTF_8));
Element root = Util.parseDocument(xml);
if (!root.getNodeName().equals(IApiXmlConstants.ELEMENT_COMPONENT)) {
abort(ScannerMessages.ComponentXMLScanner_0, null);
}
long timestamp = getLong(root, IApiXmlConstants.ATTR_MODIFICATION_STAMP);
String version = root.getAttribute(IApiXmlConstants.ATTR_VERSION);
description.setEmbeddedVersion(version);
if (IApiXmlConstants.API_DESCRIPTION_CURRENT_VERSION.equals(version)) {
description.fPackageTimeStamp = timestamp;
description.fManifestFile = project.getProject().getFile(JarFile.MANIFEST_NAME);
restoreChildren(description, root, null, description.fPackageMap);
return true;
}
} catch (IOException e) {
abort(MessageFormat.format(ScannerMessages.ApiDescriptionManager_1, project.getElementName()), e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// ignore
}
}
}
}
return false;
}
private void restoreChildren(ProjectApiDescription apiDesc, Element element, ManifestNode parentNode, Map<IElementDescriptor, ManifestNode> childrenMap) throws CoreException {
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
restoreNode(apiDesc, (Element) child, parentNode, childrenMap);
}
}
}
private void restoreNode(ProjectApiDescription apiDesc, Element element, ManifestNode parentNode, Map<IElementDescriptor, ManifestNode> childrenMap) throws CoreException {
ManifestNode node = null;
IElementDescriptor elementDesc = null;
if (element.getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE)) {
int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY);
int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS);
// collect fragments
List<IJavaElement> fragments = new ArrayList<>();
NodeList childNodes = element.getChildNodes();
String pkgName = null;
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
if (((Element) child).getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE_FRAGMENT)) {
Element fragment = (Element) child;
String handle = fragment.getAttribute(IApiXmlConstants.ATTR_HANDLE);
IJavaElement je = JavaCore.create(handle);
if (je.getElementType() != IJavaElement.PACKAGE_FRAGMENT) {
abort(ScannerMessages.ApiDescriptionManager_2 + handle, null);
}
pkgName = je.getElementName();
fragments.add(je);
}
}
}
if (!fragments.isEmpty()) {
elementDesc = Factory.packageDescriptor(pkgName);
node = apiDesc.newPackageNode(fragments.toArray(new IPackageFragment[fragments.size()]), parentNode, elementDesc, vis, res);
} else {
abort(ScannerMessages.ApiDescriptionManager_2, null);
}
} else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_PACKAGE_FRAGMENT)) {
return; // nothing to do
} else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_TYPE)) {
String handle = element.getAttribute(IApiXmlConstants.ATTR_HANDLE);
int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY);
int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS);
IJavaElement je = JavaCore.create(handle);
if (je.getElementType() != IJavaElement.TYPE) {
abort(ScannerMessages.ApiDescriptionManager_3 + handle, null);
}
IType type = (IType) je;
elementDesc = Factory.typeDescriptor(type.getFullyQualifiedName('$'));
TypeNode tn = apiDesc.newTypeNode(type, parentNode, elementDesc, vis, res);
node = tn;
tn.fTimeStamp = getLong(element, IApiXmlConstants.ATTR_MODIFICATION_STAMP);
} else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_FIELD)) {
if (parentNode.element instanceof IReferenceTypeDescriptor) {
IReferenceTypeDescriptor type = (IReferenceTypeDescriptor) parentNode.element;
int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY);
int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS);
String name = element.getAttribute(IApiXmlConstants.ATTR_NAME);
elementDesc = type.getField(name);
node = apiDesc.newNode(parentNode, elementDesc, vis, res);
}
} else if (element.getTagName().equals(IApiXmlConstants.ELEMENT_METHOD)) {
if (parentNode.element instanceof IReferenceTypeDescriptor) {
IReferenceTypeDescriptor type = (IReferenceTypeDescriptor) parentNode.element;
int vis = getInt(element, IApiXmlConstants.ATTR_VISIBILITY);
int res = getInt(element, IApiXmlConstants.ATTR_RESTRICTIONS);
String name = element.getAttribute(IApiXmlConstants.ATTR_NAME);
String sig = element.getAttribute(IApiXmlConstants.ATTR_SIGNATURE);
if (sig.indexOf('.') != -1) {
// old files might use '.' instead of '/'
sig = sig.replace('.', '/');
}
elementDesc = type.getMethod(name, sig);
node = apiDesc.newNode(parentNode, elementDesc, vis, res);
}
}
if (node != null) {
childrenMap.put(elementDesc, node);
restoreChildren(apiDesc, element, node, node.children);
} else {
abort(ScannerMessages.ApiDescriptionManager_4, null);
}
}
/**
* Returns an integer attribute.
*
* @param element element with the integer
* @param attr attribute name
* @return attribute value as an integer
*/
private int getInt(Element element, String attr) {
String attribute = element.getAttribute(attr);
try {
return Integer.parseInt(attribute);
} catch (NumberFormatException e) {
// ignore
}
return 0;
}
/**
* Returns a long attribute.
*
* @param element element with the long
* @param attr attribute name
* @return attribute value as an long
*/
private long getLong(Element element, String attr) {
String attribute = element.getAttribute(attr);
if (attribute != null) {
try {
return Long.parseLong(attribute);
} catch (NumberFormatException e) {
// ignore
}
}
return 0L;
}
/**
* Throws an exception with the given message and underlying exception.
*
* @param message error message
* @param exception underlying exception, or <code>null</code>
* @throws CoreException
*/
private static void abort(String message, Throwable exception) throws CoreException {
IStatus status = new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, message, exception);
throw new CoreException(status);
}
}