| /* |
| * 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("<", "<").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>"); |
| } |
| } |
| } |