blob: 507f0e27d204ce94094042f6061274ed10e763b4 [file] [log] [blame]
/*
* Copyright (c) 2012, 2013, 2015 Eike Stepper (Berlin, Germany) 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:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.emf.cdo.releng;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Eike Stepper
*/
public class Api2Html extends DefaultHandler
{
private static final String ANNOTATION = "annotation";
private static final String ENUM = "enum";
private static final String INTERFACE = "interface";
private static final String CLASS = "class";
private static final String PLUS = "plus.gif";
private static final String MINUS = "minus.gif";
private static final String NO_DOCS = "";
private static final Pattern VERSION_CHANGED = Pattern.compile(
"The ([^ ]+) version has been changed for the api component ([^ ]+) \\(from version ([^ ]+) to ([^ ]+)\\)");
private int lastNodeID;
private Category breaking = new Category("Breaking API Changes");
private Category compatible = new Category("Compatible API Changes");
private Category reexports = new Category("Re-Exported API Changes");
private Map<String, String> docProjects = new HashMap<String, String>();
private ClassLoader classLoader;
private String buildQualifier;
private File pluginsFolder;
private File tpFolder;
public Api2Html(File folder, String buildQualifier, File pluginsFolder, File tpFolder) throws Exception
{
this.buildQualifier = buildQualifier;
this.pluginsFolder = pluginsFolder;
this.tpFolder = tpFolder;
File xmlFile = new File(folder, "api.xml");
InputStream in = new FileInputStream(xmlFile);
try
{
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(in, this);
}
finally
{
in.close();
}
File htmlFile = new File(folder, "api.html");
generate(htmlFile);
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
if ("delta".equalsIgnoreCase(qName))
{
try
{
String componentVersion = null;
String componentChange = null;
String componentID = attributes.getValue("componentId");
String typeName = attributes.getValue("type_name");
String elementType = attributes.getValue("element_type");
String kind = attributes.getValue("kind");
String message = attributes.getValue("message");
if (componentID == null || componentID.length() == 0)
{
if (message.startsWith("The API component "))
{
componentID = message.substring("The API component ".length());
componentID = componentID.substring(0, componentID.indexOf(' '));
if (message.endsWith("added"))
{
componentChange = "The plugin has been added";
componentVersion = readComponentVersion(componentID);
}
else if (message.endsWith("removed"))
{
componentChange = "The plugin has been removed";
}
else
{
System.out.println("No componentID: " + message);
return;
}
}
}
if (componentChange == null && (typeName == null || typeName.length() == 0))
{
Matcher matcher = VERSION_CHANGED.matcher(message);
if (matcher.matches())
{
componentChange = "The " + matcher.group(1) + " version has been changed from " + matcher.group(3) + " to "
+ matcher.group(4);
}
}
int pos = componentID.indexOf('(');
if (pos != -1)
{
componentVersion = componentID.substring(pos + 1, componentID.length() - 1);
componentID = componentID.substring(0, pos);
}
message = remove(message, typeName + ".");
message = remove(message, " in an interface that is tagged with '@noimplement'");
message = remove(message, " for interface " + typeName);
message = remove(message, " for class " + typeName);
if (!message.contains("modifier has been"))
{
message = remove(message, " to " + typeName);
}
if (message != null && message.startsWith("The deprecation modifiers has"))
{
message = "The deprecation modifier has" + message.substring("The deprecation modifiers has".length());
}
Category category;
if (message.startsWith("The re-exported type"))
{
componentChange = message;
category = reexports;
}
else
{
category = "true".equals(attributes.getValue("compatible")) ? compatible : breaking;
}
Map<String, Component> components = category.getComponents();
Component component = components.get(componentID);
if (component == null)
{
component = new Component(componentID);
components.put(componentID, component);
}
if (componentVersion != null)
{
component.setComponentVersion(componentVersion);
}
if (componentChange != null)
{
component.getChanges().add(new Change(componentChange, kind));
}
else
{
if (typeName == null || typeName.length() == 0)
{
System.out.println("No typeName: " + message);
return;
}
Type type = component.getTypes().get(typeName);
if (type == null)
{
type = new Type(component, typeName);
component.getTypes().put(typeName, type);
}
type.setElementType(elementType);
type.getChanges().add(new Change(message, kind));
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
private String readComponentVersion(String componentID) throws Exception
{
File plugin = new File(pluginsFolder, componentID);
File metaInf = new File(plugin, "META-INF");
File manifestFile = new File(metaInf, "MANIFEST.MF");
if (manifestFile.isFile())
{
FileInputStream in = new FileInputStream(manifestFile);
try
{
Manifest manifest = new Manifest(in);
java.util.jar.Attributes attributes = manifest.getMainAttributes();
return attributes.getValue("Bundle-Version");
}
finally
{
in.close();
}
}
return null;
}
private String getDocProject(String componentID) throws Exception
{
String docProject = docProjects.get(componentID);
if (docProject == NO_DOCS)
{
return null;
}
if (docProject == null)
{
docProject = NO_DOCS;
File plugin = new File(pluginsFolder, componentID);
if (plugin.isDirectory())
{
File buildProperties = new File(plugin, "build.properties");
FileInputStream in = new FileInputStream(buildProperties);
try
{
Properties properties = new Properties();
properties.load(in);
docProject = properties.getProperty("doc.project", NO_DOCS);
}
finally
{
in.close();
}
}
}
docProjects.put(componentID, docProject);
return docProject;
}
private ClassLoader createClassLoader() throws MalformedURLException
{
List<URL> urls = new ArrayList<URL>();
for (File plugin : pluginsFolder.listFiles())
{
if (plugin.isDirectory())
{
File bin = new File(plugin, "bin");
if (bin.isDirectory())
{
urls.add(bin.toURI().toURL());
}
}
else if (plugin.getName().endsWith(".jar"))
{
urls.add(plugin.toURI().toURL());
}
}
for (File plugin : tpFolder.listFiles())
{
urls.add(plugin.toURI().toURL());
}
return new URLClassLoader(urls.toArray(new URL[urls.size()]));
}
private void generate(File htmlFile) throws Exception
{
PrintStream out = new PrintStream(htmlFile);
try
{
out.println("<!DOCTYPE HTML>");
out.println("<html>");
out.println("<head>");
out.println("<title>API Evolution Report for CDO " + buildQualifier + "</title>");
out.println("<link rel=stylesheet type='text/css' href='api.css'>");
out.println("<base href='http://www.eclipse.org/cdo/images/api/'>");
out.println("<script type='text/javascript'>");
out.println(" function toggle(id)");
out.println(" {");
out.println(" e = document.getElementById(id);");
out.println(" e.style.display = (e.style.display == '' ? 'none' : '');");
out.println(" img = document.getElementById('img_' + id);");
out.println(" img.src = (e.style.display == 'none' ? '" + PLUS + "' : '" + MINUS + "');");
out.println(" }");
out.println("</script>");
out.println("</head>");
out.println("<body>");
out.println("<h1>API Evolution Report for CDO <a href='http://www.eclipse.org/cdo/downloads/#"
+ buildQualifier.replace('-', '_') + "'>" + buildQualifier + "</a></h1>");
breaking.generate(out, "");
out.println("<p/>");
compatible.generate(out, "");
out.println("<p/>");
reexports.generate(out, "");
out.println("</body>");
out.println("</html>");
}
finally
{
out.close();
}
}
private List<String> sortedKeys(Map<String, ?> map)
{
List<String> list = new ArrayList<String>(map.keySet());
Collections.sort(list);
return list;
}
private String remove(String string, String remove)
{
if (string != null)
{
int pos = string.indexOf(remove);
if (pos != -1)
{
string = string.substring(0, pos) + string.substring(pos + remove.length());
}
}
return string;
}
public static void main(String[] args) throws Exception
{
if (args.length == 0)
{
// Just for local testing!
args = new String[] { "/develop", "R20120918-0947", "/develop/git/cdo/plugins",
"/develop/ws/cdo/.buckminster/tp/plugins" };
}
new Api2Html(new File(args[0]), args[1], new File(args[2]), new File(args[3]));
}
/**
* @author Eike Stepper
*/
public static final class Version implements Comparable<Version>
{
private static final String SEPARATOR = ".";
private int major = 0;
private int minor = 0;
private int micro = 0;
public Version(String version)
{
StringTokenizer st = new StringTokenizer(version, SEPARATOR, true);
major = Integer.parseInt(st.nextToken());
if (st.hasMoreTokens())
{
st.nextToken();
minor = Integer.parseInt(st.nextToken());
if (st.hasMoreTokens())
{
st.nextToken();
micro = Integer.parseInt(st.nextToken());
}
}
}
@Override
public String toString()
{
return major + SEPARATOR + minor + SEPARATOR + micro;
}
public int compareTo(Version o)
{
if (o == this)
{
return 0;
}
int result = major - o.major;
if (result != 0)
{
return result;
}
result = minor - o.minor;
if (result != 0)
{
return result;
}
result = micro - o.micro;
if (result != 0)
{
return result;
}
return 0;
}
}
/**
* @author Eike Stepper
*/
protected abstract class AbstractNode
{
private final String text;
public AbstractNode(String text)
{
this.text = text;
}
public String getText()
{
return text.replaceAll("<", "&lt;").replaceAll("\"", "&quot;");
}
public String getIcon()
{
return "";
}
public void generate(PrintStream out, String indent) throws Exception
{
String href = getHref();
out.print(indent + getIcon() + " " + (href != null ? "<a href='" + href + "' target='_blank'>" : "") + getText()
+ (href != null ? "</a>" : ""));
}
protected String getHref() throws Exception
{
return null;
}
}
/**
* @author Eike Stepper
*/
protected abstract class AbstractTreeNode extends AbstractNode
{
private int id;
public AbstractTreeNode(String text)
{
super(text);
id = ++lastNodeID;
}
@Override
public void generate(PrintStream out, String indent) throws Exception
{
out.print(
indent + "<div class='" + getClass().getSimpleName().toLowerCase() + "'><a href=\"javascript:toggle('node"
+ id + "')\"><img src='" + (isCollapsed() ? PLUS : MINUS) + "' id='img_node" + id + "'></a>");
super.generate(out, "");
out.println("</div>");
out.println(indent + "<div id=\"node" + id + "\" style='" + (isCollapsed() ? "display:none; " : "")
+ "margin-left:20px;'>");
generateChildren(out, indent + " ");
out.println(indent + "</div>");
}
protected abstract void generateChildren(PrintStream out, String indent) throws Exception;
protected boolean isCollapsed()
{
return true;
}
}
/**
* @author Eike Stepper
*/
private final class Category extends AbstractTreeNode
{
private final Map<String, Component> components = new HashMap<String, Component>();
public Category(String text)
{
super(text);
}
public Map<String, Component> getComponents()
{
return components;
}
@Override
protected void generateChildren(PrintStream out, String indent) throws Exception
{
if (components.isEmpty())
{
out.println(indent + "<em>There are no " + getText().toLowerCase() + ".</em>");
}
else
{
for (String key : sortedKeys(components))
{
Component component = components.get(key);
component.generate(out, indent);
}
}
}
@Override
protected boolean isCollapsed()
{
return false;
}
}
/**
* @author Eike Stepper
*/
private final class Component extends AbstractTreeNode
{
private final List<Change> changes = new ArrayList<Change>();
private final Map<String, Type> types = new HashMap<String, Type>();
private Version componentVersion;
public Component(String componentID)
{
super(componentID);
}
public String getComponentID()
{
return super.getText();
}
public void setComponentVersion(String componentVersion)
{
Version version = new Version(componentVersion);
if (this.componentVersion == null || this.componentVersion.compareTo(version) < 0)
{
this.componentVersion = version;
}
}
@Override
public String getText()
{
String componentID = getComponentID();
if (componentVersion != null)
{
componentID += "&nbsp;" + componentVersion;
}
return componentID;
}
@Override
public String getIcon()
{
return "<img src='plugin.gif'>";
}
public List<Change> getChanges()
{
return changes;
}
public Map<String, Type> getTypes()
{
return types;
}
@Override
protected void generateChildren(PrintStream out, String indent) throws Exception
{
for (Change change : changes)
{
change.generate(out, indent);
}
for (String key : sortedKeys(types))
{
Type type = types.get(key);
type.generate(out, indent);
}
}
@Override
protected String getHref() throws Exception
{
String componentID = getComponentID();
String docProject = getDocProject(componentID);
if (docProject == null)
{
return null;
}
return "http://download.eclipse.org/modeling/emf/cdo/drops/" + buildQualifier + "/help/" + docProject
+ "/javadoc/" + componentID.replace('.', '/') + "/package-summary.html";
}
}
/**
* @author Eike Stepper
*/
private final class Type extends AbstractTreeNode
{
private final List<Change> changes = new ArrayList<Change>();
private final Component component;
private String elementType;
public Type(Component component, String text)
{
super(text);
this.component = component;
}
public String getTypeName()
{
return super.getText();
}
@Override
public String getText()
{
String typeName = getTypeName();
return typeName.replace('$', '.');
}
@Override
public String getIcon()
{
try
{
return "<img src='" + getElementType() + ".gif'>";
}
catch (Exception ex)
{
return super.getIcon();
}
}
public List<Change> getChanges()
{
return changes;
}
public void setElementType(String elementType)
{
if ("CLASS_ELEMENT_TYPE".equals(elementType))
{
this.elementType = CLASS;
}
else if ("INTERFACE_ELEMENT_TYPE".equals(elementType))
{
this.elementType = INTERFACE;
}
else if ("ENUM_ELEMENT_TYPE".equals(elementType))
{
this.elementType = ENUM;
}
else if ("ANNOTATION_ELEMENT_TYPE".equals(elementType))
{
this.elementType = ANNOTATION;
}
}
public String getElementType() throws Exception
{
if (elementType == null)
{
String typeName = getTypeName();
elementType = determineElementType(typeName);
}
return elementType;
}
@Override
protected void generateChildren(PrintStream out, String indent) throws Exception
{
for (Change change : changes)
{
change.generate(out, indent);
}
}
@Override
protected String getHref() throws Exception
{
String componentID = component.getComponentID();
String docProject = getDocProject(componentID);
if (docProject == null)
{
return null;
}
return "http://download.eclipse.org/modeling/emf/cdo/drops/" + buildQualifier + "/help/" + docProject
+ "/javadoc/" + getTypeName().replace('.', '/').replace('$', '.') + ".html";
}
private String determineElementType(String typeName) throws MalformedURLException
{
if (classLoader == null)
{
classLoader = createClassLoader();
}
try
{
Class<?> c = classLoader.loadClass(typeName);
if (c.isAnnotation())
{
return ANNOTATION;
}
if (c.isEnum())
{
return ENUM;
}
if (c.isInterface())
{
return INTERFACE;
}
}
catch (Throwable ex)
{
//$FALL-THROUGH$
}
return CLASS;
}
}
/**
* @author Eike Stepper
*/
private final class Change extends AbstractNode
{
private final String kind;
public Change(String text, String kind)
{
super(text);
if ("REMOVED".equals(kind))
{
this.kind = "removal";
}
else if ("ADDED".equals(kind))
{
this.kind = "addition";
}
else
{
this.kind = "change";
}
}
@Override
public String getIcon()
{
try
{
return "<img src='" + kind + ".gif'>";
}
catch (Exception ex)
{
return super.getIcon();
}
}
@Override
public void generate(PrintStream out, String indent) throws Exception
{
out.print(indent + "<img src='empty.gif'>");
super.generate(out, "");
out.println("<br>");
}
}
}