/* * Copyright (c) 2012, 2013, 2015, 2016 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("<", "<").replaceAll("\"", """); } 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 += " " + 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>"); } } }