blob: 0f6a96880922ab33515a4143a054ebd9d50539e9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2013 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.api.tools.internal.tasks;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.ApiDescription;
import org.eclipse.pde.api.tools.internal.ApiDescriptionXmlCreator;
import org.eclipse.pde.api.tools.internal.CompilationUnit;
import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
import org.eclipse.pde.api.tools.internal.model.ArchiveApiTypeContainer;
import org.eclipse.pde.api.tools.internal.model.CompositeApiTypeContainer;
import org.eclipse.pde.api.tools.internal.model.DirectoryApiTypeContainer;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiTypeContainer;
import org.eclipse.pde.api.tools.internal.provisional.scanner.TagScanner;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Ant task to generate the .api_description file during the Eclipse build.
*/
public class ApiFileGenerationTask extends Task {
static class APIToolsNatureDefaultHandler extends DefaultHandler {
private static final String NATURE_ELEMENT_NAME = "nature"; //$NON-NLS-1$
boolean isAPIToolsNature = false;
boolean insideNature = false;
StringBuffer buffer;
public void error(SAXParseException e) throws SAXException {
e.printStackTrace();
}
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
if (this.isAPIToolsNature) return;
this.insideNature = NATURE_ELEMENT_NAME.equals(name);
if (this.insideNature) {
this.buffer = new StringBuffer();
}
}
public void characters(char[] ch, int start, int length)
throws SAXException {
if (this.insideNature) {
this.buffer.append(ch, start, length);
}
}
public void endElement(String uri, String localName, String name)
throws SAXException {
if (this.insideNature) {
// check the contents of the characters
String natureName = String.valueOf(this.buffer).trim();
this.isAPIToolsNature = ApiPlugin.NATURE_ID.equals(natureName);
}
this.insideNature = false;
}
public boolean isAPIToolsNature() {
return this.isAPIToolsNature;
}
}
boolean debug;
String projectName;
String projectLocation;
String targetFolder;
String binaryLocations;
String manifests;
String sourceLocations;
boolean allowNonApiProject = false;
/**
* The encoding to read the source files with
* @since 1.0.600
*/
String encoding;
Set apiPackages = new HashSet(0);
/**
* Set the project name.
*
* @param projectName the given project name
*/
public void setProjectName(String projectName) {
this.projectName = projectName;
}
/**
* Set the project location.
*
* <br><br>This is the folder that contains all the source files for the given project.
* <br><br>The location is set using an absolute path.</p>
*
* @param projectLocation the given project location
*/
public void setProject(String projectLocation) {
this.projectLocation = projectLocation;
}
/**
* Set the target location.
*
* <br><br>This is the folder in which the generated files are generated.
* <br><br>The location is set using an absolute path.</p>
*
* @param targetLocation the given target location
*/
public void setTarget(String targetLocation) {
this.targetFolder = targetLocation;
}
/**
* Set the binary locations.
*
* <br><br>This is a list of folders or jar files that contain all the .class files for the given project.
* They are separated by the platform path separator. Each entry must exist.
* <br><br>They should be specified using absolute paths.
*
* @param binaryLocations the given binary locations
*/
public void setBinary(String binaryLocations) {
this.binaryLocations = binaryLocations;
}
/**
* Set if the task should scan the project even if it is not API tools enabled.
* <p>The possible values are: <code>true</code> or <code>false</code></p>
* <p>Default is: <code>false</code>.</p>
* @since 1.2
* @param allow
*/
public void setAllowNonApiProject(String allow) {
this.allowNonApiProject = Boolean.valueOf(allow).booleanValue();
}
/**
* Sets the encoding the task should use when reading text streams
*
* @param encoding
* @since 1.0.600
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Set the debug value.
* <p>The possible values are: <code>true</code>, <code>false</code></p>
* <p>Default is <code>false</code>.</p>
*
* @param debugValue the given debug value
*/
public void setDebug(String debugValue) {
this.debug = Boolean.toString(true).equals(debugValue);
}
/**
* Set the extra manifest files' locations.
*
* <p>This is a list of extra MANIFEST.MF files' locations that can be set to provide more API
* packages to scan. They are separated by the platform path separator. Each entry must exist.</p>
* <p>If the path is not absolute, it will be resolved relative to the current working directory.</p>
* <p>Jar files can be specified instead of MANIFEST.MF file. If a jar file is specified, its MANIFEST.MF file
* will be read if it exists.</p>
*
* @param manifests the given extra manifest files' locations
*/
public void setExtraManifests(String manifests) {
this.manifests = manifests;
}
/**
* Set the extra source locations.
*
* <br><br>This is a list of locations for source files that will be scanned.
* They are separated by the platform path separator. Each entry must exist.
* <br><br>They should be specified using absolute paths.
*
* @param manifests the given extra source locations
*/
public void setExtraSourceLocations(String sourceLocations) {
this.sourceLocations = sourceLocations;
}
/**
* Execute the ant task
*/
public void execute() throws BuildException {
if (this.binaryLocations == null
|| this.projectName == null
|| this.projectLocation == null
|| this.targetFolder == null) {
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
writer.println(
NLS.bind(Messages.api_generation_printArguments,
new String[] {
this.projectName,
this.projectLocation,
this.binaryLocations,
this.targetFolder
})
);
writer.flush();
writer.close();
throw new BuildException(String.valueOf(out.getBuffer()));
}
if (this.debug) {
System.out.println("Project name : " + this.projectName); //$NON-NLS-1$
System.out.println("Encoding: " + this.encoding); //$NON-NLS-1$
System.out.println("Project location : " + this.projectLocation); //$NON-NLS-1$
System.out.println("Binary locations : " + this.binaryLocations); //$NON-NLS-1$
System.out.println("Target folder : " + this.targetFolder); //$NON-NLS-1$
if (this.manifests != null) {
System.out.println("Extra manifest entries : " + this.manifests); //$NON-NLS-1$
}
if (this.sourceLocations != null) {
System.out.println("Extra source locations entries : " + this.sourceLocations); //$NON-NLS-1$
}
}
// collect all compilation units
File root = new File(this.projectLocation);
if (!root.exists() || !root.isDirectory()) {
if (this.debug) {
System.err.println("Must be a directory : " + this.projectLocation); //$NON-NLS-1$
}
throw new BuildException(
NLS.bind(Messages.api_generation_projectLocationNotADirectory, this.projectLocation));
}
// check if the project contains the API tools nature
File dotProjectFile = new File(root, ".project"); //$NON-NLS-1$
if(!this.allowNonApiProject && !isAPIToolsNature(dotProjectFile)) {
System.err.println("The project does not have an API Tools nature so a api_description file will not be generated"); //$NON-NLS-1$
return;
}
// check if the .api_description file exists
File targetProjectFolder = new File(this.targetFolder);
if (!targetProjectFolder.exists()) {
targetProjectFolder.mkdirs();
} else if (!targetProjectFolder.isDirectory()) {
if (this.debug) {
System.err.println("Must be a directory : " + this.targetFolder); //$NON-NLS-1$
}
throw new BuildException(
NLS.bind(Messages.api_generation_targetFolderNotADirectory, this.targetFolder));
}
File apiDescriptionFile = new File(targetProjectFolder, IApiCoreConstants.API_DESCRIPTION_XML_NAME);
if (apiDescriptionFile.exists()) {
System.out.println("API description file already exists: " + apiDescriptionFile.toString()); //$NON-NLS-1$
return;
}
File[] allFiles = null;
Map manifestMap = null;
IApiTypeContainer classFileContainer = null;
if (!this.projectLocation.endsWith(Util.ORG_ECLIPSE_SWT)) {
// create the directory class file container used to resolve signatures during tag scanning
String[] allBinaryLocations = this.binaryLocations.split(File.pathSeparator);
List allContainers = new ArrayList();
IApiTypeContainer container = null;
for (int i = 0; i < allBinaryLocations.length; i++) {
container = getContainer(allBinaryLocations[i]);
if (container == null) {
throw new BuildException(NLS.bind(Messages.api_generation_invalidBinaryLocation, allBinaryLocations[i]));
}
allContainers.add(container);
}
classFileContainer = new CompositeApiTypeContainer(null, allContainers);
File manifestFile = null;
File manifestDir = new File(root, "META-INF"); //$NON-NLS-1$
if (manifestDir.exists() && manifestDir.isDirectory()) {
manifestFile = new File(manifestDir, "MANIFEST.MF"); //$NON-NLS-1$
}
if (manifestFile != null && manifestFile.exists()) {
BufferedInputStream inputStream = null;
try {
inputStream = new BufferedInputStream(new FileInputStream(manifestFile));
manifestMap = ManifestElement.parseBundleManifest(inputStream, null);
this.apiPackages = collectApiPackageNames(manifestMap);
} catch (FileNotFoundException e) {
ApiPlugin.log(e);
} catch (IOException e) {
ApiPlugin.log(e);
} catch (BundleException e) {
ApiPlugin.log(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch(IOException e) {}
}
}
}
if (this.manifests != null) {
String[] allManifestFiles = this.manifests.split(File.pathSeparator);
for (int i = 0, max = allManifestFiles.length; i < max; i++) {
File currentManifest = new File(allManifestFiles[i]);
Set currentApiPackages = null;
if (currentManifest.exists()) {
BufferedInputStream inputStream = null;
ZipFile zipFile = null;
try {
if (isZipJarFile(currentManifest.getName())) {
zipFile = new ZipFile(currentManifest);
final ZipEntry entry = zipFile.getEntry("META-INF/MANIFEST.MF"); //$NON-NLS-1$
if (entry != null) {
inputStream = new BufferedInputStream(zipFile.getInputStream(entry));
}
} else {
inputStream = new BufferedInputStream(new FileInputStream(currentManifest));
}
if (inputStream != null) {
manifestMap = ManifestElement.parseBundleManifest(inputStream, null);
currentApiPackages = collectApiPackageNames(manifestMap);
}
} catch (FileNotFoundException e) {
ApiPlugin.log(e);
} catch (IOException e) {
ApiPlugin.log(e);
} catch (BundleException e) {
ApiPlugin.log(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch(IOException e) {
// ignore
}
}
if (zipFile != null) {
try {
zipFile.close();
}
catch(IOException e) {
// ignore
}
}
}
}
if (currentApiPackages != null) {
if (this.apiPackages == null) {
this.apiPackages = currentApiPackages;
} else {
this.apiPackages.addAll(currentApiPackages);
}
}
}
}
FileFilter fileFilter = new FileFilter() {
public boolean accept(File path) {
return (path.isFile() && Util.isJavaFileName(path.getName()) && isApi(path.getParent())) || path.isDirectory();
}
};
allFiles = Util.getAllFiles(root, fileFilter);
if (this.sourceLocations != null) {
String[] allSourceLocations = this.sourceLocations.split(File.pathSeparator);
for (int i = 0, max = allSourceLocations.length; i < max; i++) {
String currentSourceLocation = allSourceLocations[i];
File[] allFiles2 = Util.getAllFiles(new File(currentSourceLocation), fileFilter);
if (allFiles2 != null) {
if (allFiles == null) {
allFiles = allFiles2;
} else {
int length = allFiles.length;
int length2 = allFiles2.length;
System.arraycopy(allFiles, 0, (allFiles = new File[length + length2]), 0, length);
System.arraycopy(allFiles2, 0, allFiles, length, length2);
}
}
}
}
}
ApiDescription apiDescription = new ApiDescription(this.projectName);
TagScanner tagScanner = TagScanner.newScanner();
if (allFiles != null && allFiles.length != 0) {
Map options = JavaCore.getOptions();
options.put(JavaCore.COMPILER_COMPLIANCE, resolveCompliance(manifestMap));
CompilationUnit unit = null;
for (int i = 0, max = allFiles.length; i < max; i++) {
unit = new CompilationUnit(allFiles[i].getAbsolutePath(), this.encoding);
if (this.debug) {
System.out.println("Unit name[" + i + "] : " + unit.getName()); //$NON-NLS-1$ //$NON-NLS-2$
}
try {
tagScanner.scan(unit, apiDescription, classFileContainer, options, null);
} catch (CoreException e) {
ApiPlugin.log(e);
} finally {
try {
if (classFileContainer != null) {
classFileContainer.close();
}
}
catch (CoreException e) {}
}
}
}
try {
ApiDescriptionXmlCreator xmlVisitor = new ApiDescriptionXmlCreator(this.projectName, this.projectName);
apiDescription.accept(xmlVisitor, null);
String xml = xmlVisitor.getXML();
Util.saveFile(apiDescriptionFile, xml);
} catch (CoreException e) {
ApiPlugin.log(e);
} catch (IOException e) {
ApiPlugin.log(e);
}
}
/**
* Returns if the given path ends with one of the collected API path names
* @param path
* @return true if the given path name ends with one of the collected API package names
*/
boolean isApi(String path) {
String pkg = null;
for(Iterator iter = this.apiPackages.iterator(); iter.hasNext();) {
pkg = (String) iter.next();
if(path.endsWith(pkg.replace('.', File.separatorChar))) {
return true;
}
}
return false;
}
/**
* Collects the names of the packages that are API for the bundle the API description is being created for
* @param manifestmap
* @return the names of the packages that are API for the bundle the API description is being created for
* @throws BundleException if parsing the manifest map to get API package names fail for some reason
*/
private Set/*<String>*/ collectApiPackageNames(Map manifestmap) throws BundleException {
HashSet set = new HashSet();
ManifestElement[] packages = ManifestElement.parseHeader(Constants.EXPORT_PACKAGE, (String) manifestmap.get(Constants.EXPORT_PACKAGE));
if (packages != null) {
for (int i = 0; i < packages.length; i++) {
ManifestElement packageName = packages[i];
Enumeration directiveKeys = packageName.getDirectiveKeys();
if(directiveKeys == null) {
set.add(packageName.getValue());
} else {
boolean include = true;
loop: for (; directiveKeys.hasMoreElements();) {
Object directive = directiveKeys.nextElement();
if ("x-internal".equals(directive)) { //$NON-NLS-1$
String value = packageName.getDirective((String) directive);
if (Boolean.valueOf(value).booleanValue()) {
include = false;
break loop;
}
}
if ("x-friends".equals(directive)) { //$NON-NLS-1$
include = false;
break loop;
}
}
if (include) {
set.add(packageName.getValue());
}
}
}
}
return set;
}
private IApiTypeContainer getContainer(String location) {
File f = new File(location);
if (!f.exists()) return null;
if (isZipJarFile(location)) {
return new ArchiveApiTypeContainer(null, location);
} else {
return new DirectoryApiTypeContainer(null, location);
}
}
/**
* Resolves the compiler compliance based on the BREE entry in the MANIFEST.MF file
* @param manifestmap
* @return The derived {@link JavaCore#COMPILER_COMPLIANCE} from the BREE in the manifest map,
* or {@link JavaCore#VERSION_1_3} if there is no BREE entry in the map or if the BREE entry does not directly map
* to one of {"1.3", "1.4", "1.5", "1.6", "1.7"}.
*/
private String resolveCompliance(Map manifestmap) {
if(manifestmap != null) {
String eename = (String) manifestmap.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT);
if(eename != null) {
if("J2SE-1.4".equals(eename)) { //$NON-NLS-1$
return JavaCore.VERSION_1_4;
}
if("J2SE-1.5".equals(eename)) { //$NON-NLS-1$
return JavaCore.VERSION_1_5;
}
if("JavaSE-1.6".equals(eename)) { //$NON-NLS-1$
return JavaCore.VERSION_1_6;
}
if("JavaSE-1.7".equals(eename)) { //$NON-NLS-1$
return JavaCore.VERSION_1_7;
}
}
}
return JavaCore.VERSION_1_3;
}
/**
* Resolves if the '.project' file belongs to an API enabled project or not
* @param dotProjectFile
* @return true if the '.project' file is for an API enabled project, false otherwise
*/
private boolean isAPIToolsNature(File dotProjectFile) {
if (!dotProjectFile.exists()) return false;
BufferedInputStream stream = null;
try {
stream = new BufferedInputStream(new FileInputStream(dotProjectFile));
String contents = new String(Util.getInputStreamAsCharArray(stream, -1, "UTF-8")); //$NON-NLS-1$
return containsAPIToolsNature(contents);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// ignore
}
}
}
return false;
}
private static boolean isZipJarFile(String fileName) {
String normalizedFileName = fileName.toLowerCase();
return normalizedFileName.endsWith(".zip") //$NON-NLS-1$
|| normalizedFileName.endsWith(".jar"); //$NON-NLS-1$
}
/**
* Check if the given source contains an source extension point.
*
* @param pluginXMLContents the given file contents
* @return true if it contains a source extension point, false otherwise
*/
private boolean containsAPIToolsNature(String pluginXMLContents) {
SAXParserFactory factory = null;
try {
factory = SAXParserFactory.newInstance();
} catch (FactoryConfigurationError e) {
return false;
}
SAXParser saxParser = null;
try {
saxParser = factory.newSAXParser();
} catch (ParserConfigurationException e) {
// ignore
} catch (SAXException e) {
// ignore
}
if (saxParser == null) {
return false;
}
// Parse
InputSource inputSource = new InputSource(new BufferedReader(new StringReader(pluginXMLContents)));
try {
APIToolsNatureDefaultHandler defaultHandler = new APIToolsNatureDefaultHandler();
saxParser.parse(inputSource, defaultHandler);
return defaultHandler.isAPIToolsNature();
} catch (SAXException e) {
// ignore
} catch (IOException e) {
// ignore
}
return false;
}
}