blob: 4a8b6bc40fcf1d971a9722a8997f07a2f281eb26 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2021 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.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
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.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.pde.api.tools.internal.builder.ApiAnalysisBuilder.ApiAnalysisJob;
import org.eclipse.pde.api.tools.internal.model.ApiBaseline;
import org.eclipse.pde.api.tools.internal.model.ApiModelCache;
import org.eclipse.pde.api.tools.internal.model.ApiModelFactory;
import org.eclipse.pde.api.tools.internal.model.StubApiComponent;
import org.eclipse.pde.api.tools.internal.model.WorkspaceBaseline;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.IApiBaselineManager;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.ModelEntry;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.DependencyManager;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* This manager is used to maintain (persist, restore, access, update) API
* baselines. This manager is lazy, in that caches are built and maintained when
* requests are made for information, nothing is pre-loaded when the manager is
* initialized.
*
* @since 1.0.0
* @noinstantiate This class is not intended to be instantiated by clients.
*/
public final class ApiBaselineManager implements IApiBaselineManager, ISaveParticipant {
/**
* Constant for the default API baseline. Value is:
* <code>default_api_profile</code>
*/
private static final String DEFAULT_BASELINE = "default_api_profile"; //$NON-NLS-1$
/**
* Constant representing the id of the workspace {@link IApiBaseline}. Value
* is: <code>workspace</code>
*/
public static final String WORKSPACE_API_BASELINE_ID = "workspace"; //$NON-NLS-1$
/**
* Constant representing the file extension for a baseline file. Value is:
* <code>.profile</code>
*/
private static final String BASELINE_FILE_EXTENSION = ".profile"; //$NON-NLS-1$
/**
* The main cache for the manager. The form of the cache is:
*
* <pre>
* Map<String(baselineid), {@link IApiBaseline}>
* </pre>
*/
private volatile ConcurrentHashMap<String, IApiBaseline> baselinecache;
/**
* Cache of baseline names to the location with their infos in it
*/
private volatile Map<String, String> handlecache;
private volatile Set<String> hasinfos;
/**
* The current default {@link IApiBaseline}
*/
private String defaultbaseline = null;
/**
* The current workspace baseline
*/
private volatile IApiBaseline workspacebaseline;
/**
* The default save location for persisting the cache from this manager.
*/
private IPath savelocation = null;
/**
* If the cache of baselines needs to be saved or not.
*/
private volatile boolean fNeedsSaving;
/**
* The singleton instance
*/
private static ApiBaselineManager fInstance = null;
/**
* Constructor
*/
private ApiBaselineManager(boolean framework) {
if (framework) {
ApiPlugin.getDefault().addSaveParticipant(this);
savelocation = ApiPlugin.getDefault().getStateLocation().append(".api_profiles").addTrailingSeparator(); //$NON-NLS-1$
}
hasinfos = Collections.emptySet();
}
/**
* Returns the singleton instance of the manager
*
* @return the singleton instance of the manager
*/
public static synchronized ApiBaselineManager getManager() {
if (fInstance == null) {
fInstance = new ApiBaselineManager(ApiPlugin.isRunningInFramework());
}
return fInstance;
}
@Override
public IApiBaseline getApiBaseline(String name) {
if (name == null) {
return null;
}
initializeStateCache();
return baselinecache.get(name);
}
@Override
public IApiBaseline[] getApiBaselines() {
initializeStateCache();
return baselinecache.values().toArray(new IApiBaseline[0]);
}
@Override
public void addApiBaseline(IApiBaseline newbaseline) {
if (newbaseline != null) {
initializeStateCache();
baselinecache.put(newbaseline.getName(), newbaseline);
if (((ApiBaseline) newbaseline).peekInfos()) {
hasinfos.add(newbaseline.getName());
}
fNeedsSaving = true;
}
}
@Override
public boolean removeApiBaseline(String name) {
if (name == null) {
return false;
}
initializeStateCache();
IApiBaseline baseline = baselinecache.remove(name);
if (baseline == null) {
return false;
}
synchronized (this) {
baseline.dispose();
boolean success = true;
if (savelocation == null) {
return success;
}
// remove from filesystem
File file = savelocation.append(name + BASELINE_FILE_EXTENSION).toFile();
if (file.exists()) {
try {
success &= Files.deleteIfExists(file.toPath());
} catch (IOException e) {
ApiPlugin.log(e);
}
}
fNeedsSaving = true;
// flush the model cache
ApiModelCache.getCache().removeElementInfo(baseline);
return success;
}
}
/**
* Loads the infos for the given baseline from persisted storage (the
* *.profile file)
*
* @param baseline the given baseline
* @throws CoreException if an exception occurs while loading baseline infos
*/
public void loadBaselineInfos(ApiBaseline baseline) throws CoreException {
initializeStateCache();
if (isBaselineLoaded(baseline)) {
return;
}
String filename = handlecache.get(baseline.getName());
if (filename != null) {
File file = new File(filename);
if (file.exists()) {
try (FileInputStream inputStream = new FileInputStream(file)) {
baseline.restoreFrom(inputStream);
} catch (IOException e) {
ApiPlugin.log(e);
}
hasinfos.add(baseline.getName());
}
}
}
public boolean isBaselineLoaded(IApiBaseline baseline) {
return hasinfos.contains(baseline.getName());
}
/**
* Initializes the baseline cache lazily. Only performs work if the current
* cache has not been created yet
*
* @throws FactoryConfigurationError
* @throws ParserConfigurationException
*/
private void initializeStateCache() {
if (baselinecache != null) {
return;
}
if (!ApiPlugin.isRunningInFramework()) {
synchronized (this) {
if (baselinecache == null) {
handlecache = new ConcurrentHashMap<>(8);
hasinfos = ConcurrentHashMap.newKeySet(8);
baselinecache = new ConcurrentHashMap<>(8);
}
}
return;
}
long time = System.currentTimeMillis();
synchronized (this) {
if (baselinecache == null) {
handlecache = new ConcurrentHashMap<>(8);
hasinfos = ConcurrentHashMap.newKeySet(8);
ConcurrentHashMap<String, IApiBaseline> bcache = new ConcurrentHashMap<>(8);
File[] baselines = savelocation.toFile().listFiles((FileFilter) pathname -> pathname.getName().endsWith(BASELINE_FILE_EXTENSION));
if (baselines != null) {
IApiBaseline newbaseline = null;
for (File baseline : baselines) {
if (baseline.exists()) {
newbaseline = new ApiBaseline(new Path(baseline.getName()).removeFileExtension().toString());
handlecache.put(newbaseline.getName(), baseline.getAbsolutePath());
bcache.put(newbaseline.getName(), newbaseline);
}
}
}
String def = getDefaultProfilePref();
if (def != null && bcache.get(def) != null) {
defaultbaseline = def;
} else {
defaultbaseline = null;
}
baselinecache = bcache;
if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
System.out.println("Time to initialize state cache: " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
}
/**
* @return the default API baseline saved in the preferences, or
* <code>null</code> if there isn't one
*/
private String getDefaultProfilePref() {
IPreferencesService service = Platform.getPreferencesService();
return service.getString(ApiPlugin.PLUGIN_ID, DEFAULT_BASELINE, null, new IScopeContext[] { InstanceScope.INSTANCE });
}
/**
* Persists all of the cached elements to individual xml files named with
* the id of the API baseline
*
* @throws IOException
*/
private void persistStateCache() throws CoreException, IOException {
if (savelocation == null) {
return;
}
IEclipsePreferences node = InstanceScope.INSTANCE.getNode(ApiPlugin.PLUGIN_ID);
if (defaultbaseline != null) {
node.put(DEFAULT_BASELINE, defaultbaseline);
} else {
node.remove(DEFAULT_BASELINE);
}
if (baselinecache != null && !hasinfos.isEmpty()) {
File dir = new File(savelocation.toOSString());
Files.createDirectories(dir.toPath());
IApiBaseline baseline = null;
for (Entry<String, IApiBaseline> entry : baselinecache.entrySet()) {
String id = entry.getKey();
baseline = entry.getValue();
if (!isBaselineLoaded(baseline)) {
continue;
}
File file = savelocation.append(id + BASELINE_FILE_EXTENSION).toFile();
if (!file.exists()) {
try {
Files.createFile(file.toPath());
} catch (IOException ioe) {
ApiPlugin.log(new IOException("Unable to save API baseline with id: '" + id + "'", ioe)); //$NON-NLS-1$ //$NON-NLS-2$
continue;
}
}
try (FileOutputStream fout = new FileOutputStream(file)) {
writeBaselineDescription(baseline, fout);
// need to save the api baseline state in order to be able
// to reload it later
handlecache.put(baseline.getName(), file.getAbsolutePath());
fout.flush();
}
}
}
}
/**
* Writes out the current state of the {@link IApiBaseline} as XML to the
* given output stream
*
* @param stream
* @throws CoreException
*/
private void writeBaselineDescription(IApiBaseline baseline, OutputStream stream) throws CoreException {
String xml = getProfileXML(baseline);
try {
stream.write(xml.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
abort("Error writing pofile descrition", e); //$NON-NLS-1$
}
}
/**
* Returns an XML description of the given baseline.
*
* @param baseline the given API baseline
* @return XML string representation of the given baseline
* @throws CoreException if an exception occurs while retrieving the xml
* string representation
*/
private String getProfileXML(IApiBaseline baseline) throws CoreException {
Document document = Util.newDocument();
Element root = document.createElement(IApiXmlConstants.ELEMENT_APIPROFILE);
document.appendChild(root);
root.setAttribute(IApiXmlConstants.ATTR_NAME, baseline.getName());
root.setAttribute(IApiXmlConstants.ATTR_VERSION, IApiXmlConstants.API_PROFILE_CURRENT_VERSION);
String location = baseline.getLocation();
if (location != null) {
root.setAttribute(IApiXmlConstants.ATTR_LOCATION, location);
}
Element celement = null;
IApiComponent[] components = baseline.getApiComponents();
for (IApiComponent component : components) {
Set<IApiComponent> allComponentSet = new HashSet<>();
// if the baseline has multiple versions, persist all versions
Set<IApiComponent> multipleComponents = baseline.getAllApiComponents(component.getSymbolicName());
if (multipleComponents.isEmpty()) {
// no multiple version - add the current component
allComponentSet.add(component);
} else {
allComponentSet.addAll(multipleComponents);
}
for (Iterator<IApiComponent> iterator = allComponentSet.iterator(); iterator.hasNext();) {
IApiComponent iApiComponent = iterator.next();
if (!iApiComponent.isSystemComponent()) {
celement = document.createElement(IApiXmlConstants.ELEMENT_APICOMPONENT);
celement.setAttribute(IApiXmlConstants.ATTR_ID, iApiComponent.getSymbolicName());
celement.setAttribute(IApiXmlConstants.ATTR_VERSION, iApiComponent.getVersion());
celement.setAttribute(IApiXmlConstants.ATTR_LOCATION, new Path(iApiComponent.getLocation()).toPortableString());
root.appendChild(celement);
}
}
// clear the temporary hashset
allComponentSet.clear();
}
return Util.serializeDocument(document);
}
/**
* Throws a core exception with the given message and underlying exception,
* if any.
*
* @param message error message
* @param e underlying exception or <code>null</code>
* @throws CoreException
*/
private static void abort(String message, Throwable e) throws CoreException {
throw new CoreException(Status.error(message, e));
}
/**
* Restore a baseline from the given input stream (persisted baseline).
*
* @param baseline the given baseline to restore
* @param stream the given input stream
* @throws CoreException if unable to restore the baseline
* @return restored baseline components or null if restore didn't work
*/
public IApiComponent[] readBaselineComponents(ApiBaseline baseline, InputStream stream) throws CoreException {
long start = System.currentTimeMillis();
DocumentBuilder parser = null;
IApiComponent[] restored = null;
try {
parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
parser.setErrorHandler(new DefaultHandler());
} catch (ParserConfigurationException | FactoryConfigurationError e) {
abort("Error restoring API baseline", e); //$NON-NLS-1$
}
try {
Document document = parser.parse(stream);
Element root = document.getDocumentElement();
if (root.getNodeName().equals(IApiXmlConstants.ELEMENT_APIPROFILE)) {
String baselineLocation = root.getAttribute(IApiXmlConstants.ATTR_LOCATION);
if (baselineLocation != null && !baselineLocation.equals(Util.EMPTY_STRING)) {
baseline.setLocation(Path.fromPortableString(baselineLocation).toOSString());
}
// un-pooled components
NodeList children = root.getElementsByTagName(IApiXmlConstants.ELEMENT_APICOMPONENT);
List<IApiComponent> components = new ArrayList<>();
for (int j = 0; j < children.getLength(); j++) {
Element componentNode = (Element) children.item(j);
// this also contains components in pools, so don't process
// them
if (componentNode.getParentNode().equals(root)) {
String location = componentNode.getAttribute(IApiXmlConstants.ATTR_LOCATION);
IApiComponent component = ApiModelFactory.newApiComponent(baseline, Path.fromPortableString(location).toOSString());
if (component != null) {
components.add(component);
}
}
}
// pooled components - only for xml file with version <= 1
// since version 2, pools have been removed
children = root.getElementsByTagName(IApiXmlConstants.ELEMENT_POOL);
IApiComponent component = null;
for (int j = 0; j < children.getLength(); j++) {
String location = ((Element) children.item(j)).getAttribute(IApiXmlConstants.ATTR_LOCATION);
IPath poolPath = Path.fromPortableString(location);
NodeList componentNodes = root.getElementsByTagName(IApiXmlConstants.ELEMENT_APICOMPONENT);
for (int i = 0; i < componentNodes.getLength(); i++) {
Element compElement = (Element) componentNodes.item(i);
String id = compElement.getAttribute(IApiXmlConstants.ATTR_ID);
String ver = compElement.getAttribute(IApiXmlConstants.ATTR_VERSION);
StringBuilder name = new StringBuilder();
name.append(id);
name.append('_');
name.append(ver);
File file = poolPath.append(name.toString()).toFile();
if (!file.exists()) {
name.append(".jar"); //$NON-NLS-1$
file = poolPath.append(name.toString()).toFile();
}
component = ApiModelFactory.newApiComponent(baseline, file.getAbsolutePath());
if (component != null) {
components.add(component);
}
}
}
restored = components.toArray(new IApiComponent[components.size()]);
}
} catch (IOException | SAXException e) {
abort("Error restoring API baseline", e); //$NON-NLS-1$
}
if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
System.out.println("Time to restore a persisted baseline : " + (System.currentTimeMillis() - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
return restored;
}
@Override
public void saving(ISaveContext context) throws CoreException {
if (!fNeedsSaving) {
return;
}
try {
persistStateCache();
cleanStateCache();
fNeedsSaving = false;
} catch (IOException e) {
ApiPlugin.log(e);
}
}
/**
* Cleans out all but the default baseline from the in-memory cache of
* baselines
*/
private void cleanStateCache() {
if (baselinecache != null) {
IApiBaseline baseline = null;
for (Entry<String, IApiBaseline> entry : baselinecache.entrySet()) {
baseline = entry.getValue();
if (!baseline.getName().equals(defaultbaseline)) {
baseline.dispose();
hasinfos.remove(baseline.getName());
// iter.remove();
}
}
}
}
/**
* Returns if the given name is an existing baseline name
*
* @param name
* @return true if the given name is an existing baseline name, false
* otherwise
*/
public boolean isExistingProfileName(String name) {
if (baselinecache == null || name == null) {
return false;
}
return baselinecache.containsKey(name);
}
/**
* Cleans up the manager
*/
public void stop() {
try {
Job.getJobManager().cancel(ApiAnalysisJob.class);
if (baselinecache != null) {
// we should first dispose all existing baselines
for (IApiBaseline iApiBaseline : baselinecache.values()) {
iApiBaseline.dispose();
}
baselinecache.clear();
}
synchronized (this) {
if (workspacebaseline != null) {
workspacebaseline.dispose();
}
}
if (handlecache != null) {
handlecache.clear();
}
if (!hasinfos.isEmpty()) {
hasinfos.clear();
}
StubApiComponent.disposeAllCaches();
} finally {
if (ApiPlugin.isRunningInFramework()) {
ApiPlugin.getDefault().removeSaveParticipant(this);
}
}
}
@Override
public void doneSaving(ISaveContext context) {
//
}
@Override
public void prepareToSave(ISaveContext context) throws CoreException {
//
}
@Override
public void rollback(ISaveContext context) {
//
}
@Override
public IApiBaseline getDefaultApiBaseline() {
initializeStateCache();
String defbaseline = defaultbaseline;
if (defbaseline == null) {
return null;
}
return baselinecache.get(defbaseline);
}
@Override
public void setDefaultApiBaseline(String name) {
fNeedsSaving = true;
defaultbaseline = name;
}
@Override
public IApiBaseline getWorkspaceBaseline() {
if (!ApiPlugin.isRunningInFramework()) {
return null;
}
if (this.workspacebaseline == null) {
try {
synchronized (this) {
if (this.workspacebaseline == null) {
this.workspacebaseline = createWorkspaceBaseline();
}
}
} catch (CoreException e) {
ApiPlugin.log(e);
}
}
return this.workspacebaseline;
}
/**
* Disposes the workspace baseline such that a new one will be created on
* the next request.
*/
void disposeWorkspaceBaseline() {
if (workspacebaseline == null) {
return;
}
Job.getJobManager().cancel(ApiAnalysisJob.class);
IApiBaseline oldBaseline = null;
synchronized (this) {
if (workspacebaseline != null) {
if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
System.out.println("disposing workspace baseline"); //$NON-NLS-1$
}
oldBaseline = workspacebaseline;
StubApiComponent.disposeAllCaches();
workspacebaseline = null;
}
}
if (oldBaseline != null) {
oldBaseline.dispose();
}
}
/**
* Creates a workspace {@link IApiBaseline}
*
* @return a new workspace {@link IApiBaseline} or <code>null</code>
*/
private IApiBaseline createWorkspaceBaseline() throws CoreException {
long time = System.currentTimeMillis();
IApiBaseline baseline = null;
try {
baseline = new WorkspaceBaseline();
// populate it with only projects that are API aware
List<IPluginModelBase> models = Arrays.asList(PluginRegistry.getWorkspaceModels());
Set<BundleDescription> bundles = DependencyManager.getSelfAndDependencies(models);
List<IApiComponent> componentsList = new ArrayList<>(bundles.size());
for (BundleDescription bundle : bundles) {
String id = bundle.getSymbolicName();
ModelEntry modelEntry = PluginRegistry.findEntry(id);
IPluginModelBase[] workspaceModels = modelEntry.getWorkspaceModels();
IApiComponent apiComponent = null;
if (workspaceModels.length == 0) {
// external model - reexported case
IPluginModelBase externalModel = PluginRegistry.findModel(id);
if (externalModel != null) {
try {
apiComponent = ApiModelFactory.newApiComponent(baseline, externalModel);
if (apiComponent != null) {
componentsList.add(apiComponent);
}
} catch (CoreException e) {
ApiPlugin.log(e);
}
}
continue;
}
for (IPluginModelBase iPluginModelBase : workspaceModels) {
try {
apiComponent = ApiModelFactory.newApiComponent(baseline, iPluginModelBase);
if (apiComponent != null) {
componentsList.add(apiComponent);
}
} catch (CoreException e) {
ApiPlugin.log(e);
}
}
}
baseline.addApiComponents(componentsList.toArray(new IApiComponent[componentsList.size()]));
} finally {
if (ApiPlugin.DEBUG_BASELINE_MANAGER) {
System.out.println("Time to create a workspace baseline : " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
return baseline;
}
@Override
public IApiComponent getWorkspaceComponent(String symbolicName) {
IApiBaseline baseline = getWorkspaceBaseline();
if (baseline != null) {
return baseline.getApiComponent(symbolicName);
}
return null;
}
}