blob: 45cdbb1e08a1a83b75391e0eb11047fc38ef07bc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2013 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.search;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
import org.eclipse.pde.api.tools.internal.SynchronizedOverflowingLRUCache;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.util.FileManager;
import org.eclipse.pde.api.tools.internal.util.Util;
public class UseScanManager {
private static UseScanCache fApiComponentCache;
private static UseScanManager fUseScanProcessor;
private static String tempLocation = "${workspace_loc}/.metadata/.plugins/" + ApiPlugin.PLUGIN_ID + "/ApiUseScans/"; //$NON-NLS-1$ //$NON-NLS-2$
public static final String STATE_DELIM = "*"; //$NON-NLS-1$
public static final String LOCATION_DELIM = "|"; //$NON-NLS-1$
public static final String ESCAPE_REGEX = "\\"; //$NON-NLS-1$
/**
* Regular expression for finding use scan directories <br>
* <br>
* Value is: <code>^.* \\(.*\\)$</code>
*/
public static final Pattern NAME_REGEX = Pattern.compile("^.* \\(.*\\)$"); //$NON-NLS-1$
/**
* Number of entries to cache in the {@link UseScanCache}
*/
public static final int DEFAULT_CACHE_SIZE = 1000;
/**
* Cache to maintain the list of least recently used
* <code>UseScanReferences</code>
*/
private static class UseScanCache extends SynchronizedOverflowingLRUCache<IApiComponent, IReferenceCollection> {
public UseScanCache(int size) {
super(size);
}
public UseScanCache(int size, int overflow) {
super(size, overflow);
}
@Override
protected boolean close(LRUCacheEntry<IApiComponent, IReferenceCollection> entry) {
IReferenceCollection references = entry.value;
references.clear();
return true;
}
@Override
protected UseScanCache newInstance(int size, int newOverflow) {
return new UseScanCache(size, newOverflow);
}
}
private String[] fLocations = null;
/**
* {@link FileFilter} for finding use scan directories
*/
static FileFilter USESCAN_FILTER = pathname -> {
if (NAME_REGEX.matcher(pathname.getName()).matches()) {
throw new RuntimeException(pathname.getName());
}
return false;
};
// Singleton
private UseScanManager() {
}
/**
* Returns a handle to the singleton instance
*
* @return the singleton {@link UseScanManager}
*/
public synchronized static UseScanManager getInstance() {
if (fUseScanProcessor == null) {
fUseScanProcessor = new UseScanManager();
fApiComponentCache = new UseScanCache(DEFAULT_CACHE_SIZE);
}
return fUseScanProcessor;
}
/**
* Returns the references for a given <code>IApiComponent</code>. If it can
* not find them in cache, they will be fetched from the API Use Scans and
* stored.
*
* @param apiComponent component whose references have to be fetched
* @param refTypes reference types for which the references has to be
* computed in the given <code>IApiComponent</code>. If
* <code>null</code> or empty, all references will be returned
* @param monitor
* @return the array of reference descriptors
*/
public IReferenceDescriptor[] getExternalDependenciesFor(IApiComponent apiComponent, String[] apiUseTypes, IProgressMonitor monitor) {
IReferenceCollection references = fApiComponentCache.get(apiComponent);
if (references == null) {
references = apiComponent.getExternalDependencies();
}
SubMonitor localmonitor = SubMonitor.convert(monitor, SearchMessages.collecting_external_dependencies, 10);
ArrayList<String> unavailableMembers = new ArrayList<>();
if (apiUseTypes != null && apiUseTypes.length > 0) {
for (int i = 0; i < apiUseTypes.length; i++) {
if (!references.hasReferencesTo(apiUseTypes[i])) {
unavailableMembers.add(apiUseTypes[i]);
}
}
if (unavailableMembers.size() > 0) {
fetch(apiComponent, unavailableMembers.toArray(new String[unavailableMembers.size()]), references,
localmonitor.split(9));
}
localmonitor.setWorkRemaining(1).split(1);
return references.getExternalDependenciesTo(apiUseTypes);
} else {
// full build has been triggered so re-fetch
fetch(apiComponent, null, references, localmonitor.split(9));
localmonitor.split(1);
return references.getAllExternalDependencies();
}
}
/**
* fetches the references from the API Use Scan locations
*
* @param apiComponent
* @param member
* @param references
* @param monitor
*/
private void fetch(IApiComponent apiComponent, String[] types, IReferenceCollection references, IProgressMonitor monitor) {
UseScanParser parser = new UseScanParser();
UseScanReferenceVisitor visitor = new UseScanReferenceVisitor(apiComponent, types, references);
SubMonitor localmonitor = SubMonitor.convert(monitor, SearchMessages.load_external_dependencies, 10);
try {
String[] locations;
if (fLocations == null) {
locations = getReportLocations();
} else {
locations = fLocations;
}
if (locations != null) {
IStringVariableManager stringManager = null;
localmonitor.setWorkRemaining(locations.length);
for (int i = 0; i < locations.length; i++) {
SubMonitor iterationMonitor = localmonitor.split(1);
File file = new File(locations[i]);
if (!file.exists()) {
continue;
}
if (file.isFile()) {
if (Util.isArchive(file.getName())) {
String destDirPath = tempLocation + file.getName() + '.' + file.getAbsolutePath().hashCode();
if (stringManager == null) {
stringManager = VariablesPlugin.getDefault().getStringVariableManager();
}
destDirPath = stringManager.performStringSubstitution(destDirPath);
locations[i] = destDirPath + '/' + file.lastModified();
File unzipDirLoc = new File(destDirPath);
if (unzipDirLoc.exists()) {
String[] childDirs = unzipDirLoc.list();
for (int j = 0; j < childDirs.length; j++) {
if (!childDirs[j].equals(String.valueOf(file.lastModified()))) {
FileManager.getManager().recordTempFileRoot(destDirPath + '/' + childDirs[j]);
}
}
} else {
Util.unzip(file.getPath(), locations[i]);
}
} else {
continue;
}
}
try {
locations[i] = getExactScanLocation(locations[i]);
if (locations[i] == null) {
String message;
if (file.isDirectory()) {
message = NLS.bind(SearchMessages.UseScanManager_InvalidDir, file.getAbsolutePath());
} else {
message = NLS.bind(SearchMessages.UseScanManager_InvalidArchive, file.getAbsolutePath());
}
throw new Exception(message);
}
parser.parse(locations[i], iterationMonitor, visitor);
} catch (Exception e) {
ApiPlugin.log(e); // log the exception and continue with
// next location
}
}
fApiComponentCache.remove(apiComponent); // remove current value
// so that it only
// doesn't gets
// purged if size
// limit is reached
fApiComponentCache.put(apiComponent, references);
}
} catch (Exception e) {
ApiPlugin.log(e);
}
}
/**
* Returns the scan
*
* @param location
* @return
*/
public static String getExactScanLocation(String location) {
File file = new File(location);
if (isValidDirectory(file)) {
return location;
}
file = new File(location, IApiCoreConstants.XML);
if (isValidDirectory(file)) {
return file.getAbsolutePath();
}
return null;
}
/**
* Validate if the given {@link File} is a folder that contains a use scan. <br>
* <br>
* The {@link File} is considered valid iff:
* <ul>
* <li>it is a folder</li>
* <li>the folder has child folder that matches the name pattern
* <code>^.* (.*)$</code></li>
* <li>the previous child directory has its own child directory that matches
* the name pattern <code>^.* (.*)$</code></li>
* </ul>
*
* @param file
* @return <code>true</code> is the sub folders match the patterns,
* <code>false</code> otherwise
*/
public static boolean isValidDirectory(File file) {
if (file.exists() && file.isDirectory()) {
try {
file.listFiles(USESCAN_FILTER);
} catch (RuntimeException rte) {
File f = new File(file, rte.getMessage());
try {
if (f.exists() && f.isDirectory()) {
f.listFiles(USESCAN_FILTER);
}
} catch (RuntimeException re) {
return true;
}
}
}
return false;
}
/**
* Validate if the given {@link File} is an archive that contains a use
* scan. <br>
* <br>
* The {@link File} is considered valid iff:
* <ul>
* <li>it has an XML folder</li>
* <li>the XML folder has child folder that matches the name pattern
* <code>{@link #NAME_REGEX}</code></li>
* <li>the previous child directory has its own child directory that matches
* the name pattern <code>{@link #NAME_REGEX}</code></li>
* </ul>
*
* @param file
* @return <code>true</code> is the sub folders match the patterns,
* <code>false</code> otherwise
*/
public static boolean isValidArchive(File file) {
String fname = file.getName().toLowerCase();
if (file.exists() && Util.isArchive(fname)) {
Enumeration<? extends ZipEntry> entries = null;
if (fname.endsWith(Util.DOT_JAR)) {
try (JarFile jfile = new JarFile(file)) {
entries = jfile.entries();
} catch (IOException ioe) {
return false;
}
} else if (fname.endsWith(Util.DOT_ZIP)) {
try (ZipFile zfile = new ZipFile(file)) {
entries = zfile.entries();
} catch (IOException e) {
return false;
}
}
if (entries != null) {
while (entries.hasMoreElements()) {
ZipEntry o = entries.nextElement();
if (o.isDirectory()) {
IPath path = new Path(o.getName());
int count = path.segmentCount();
if (count > 2) {
return NAME_REGEX.matcher(path.segment(0)).matches() || NAME_REGEX.matcher(path.segment(1)).matches();
}
}
}
}
}
return false;
}
/**
* Returns if the scan if a valid API use scan
*
* @param location
* @return true if the scan is valid false otherwise
*/
public static boolean isValidScanLocation(String location) {
if (location != null && location.length() > 0) {
IPath path = new Path(location);
File file = path.toFile();
return isValidDirectory(file) || isValidArchive(file);
}
return false;
}
/**
* Returns the report locations from the preferences
*
* @return
*/
public String[] getReportLocations() {
IEclipsePreferences node = InstanceScope.INSTANCE.getNode(ApiPlugin.PLUGIN_ID);
String apiUseScanPaths = node.get(IApiCoreConstants.API_USE_SCAN_LOCATION, null);
if (apiUseScanPaths == null || apiUseScanPaths.length() == 0) {
return new String[0];
}
String[] locations = apiUseScanPaths.split(ESCAPE_REGEX + LOCATION_DELIM);
ArrayList<String> locationList = new ArrayList<>(locations.length);
for (String location : locations) {
String values[] = location.split(ESCAPE_REGEX + STATE_DELIM);
if (Boolean.parseBoolean(values[1])) {
locationList.add(values[0]);
}
}
return locationList.toArray(new String[locationList.size()]);
}
/**
* Sets the report locations to be used. Once set, these locations will be
* used instead of ones in the preference. When set to <code>null</code>,
* the locations in preference will be used.
*
* @param locations
*/
public void setReportLocations(String[] locations) {
fLocations = locations;
}
/**
* Sets the cache size
*
* @param size The total number of references that can be held in memory
*/
public void setCacheSize(int size) {
fApiComponentCache.setSpaceLimit(size);
}
/**
* Purges all reference information
*/
public void clearCache() {
List<IReferenceCollection> elements = fApiComponentCache.elementsSnapshot();
for (IReferenceCollection reference : elements) {
reference.clear();
}
fApiComponentCache.flush();
}
}