Skip to content

Instantly share code, notes, and snippets.

@lukaseder
Created March 5, 2020 16:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save lukaseder/4d2ae9984d8c5ba1e854afea84c87ee9 to your computer and use it in GitHub Desktop.
Save lukaseder/4d2ae9984d8c5ba1e854afea84c87ee9 to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2009-2015, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* This work is dual-licensed
* - under the Apache Software License 2.0 (the "ASL")
* - under the jOOQ License and Maintenance Agreement (the "jOOQ License")
* =============================================================================
* You may choose which license applies to you:
*
* - If you're using this work with Open Source databases, you may choose
* either ASL or jOOQ License.
* - If you're using this work with at least one commercial database, you must
* choose jOOQ License
*
* For more information, please visit http://www.jooq.org/licenses
*
* Apache Software License 2.0:
* -----------------------------------------------------------------------------
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* jOOQ License and Maintenance Agreement:
* -----------------------------------------------------------------------------
* Data Geekery grants the Customer the non-exclusive, timely limited and
* non-transferable license to install and use the Software under the terms of
* the jOOQ License and Maintenance Agreement.
*
* This library is distributed with a LIMITED WARRANTY. See the jOOQ License
* and Maintenance Agreement for more details: http://www.jooq.org/licensing
*/
package org.jooq.web;
import static java.util.Collections.nCopies;
import static java.util.stream.Collectors.joining;
import static org.jooq.web.ApiDiff.Modification.added;
import static org.jooq.web.ApiDiff.Modification.contravariance;
import static org.jooq.web.ApiDiff.Modification.deprecated;
import static org.jooq.web.ApiDiff.Modification.pulledup;
import static org.jooq.web.ApiDiff.Modification.removed;
import static org.jooq.web.Versions.API_DIFF;
import static org.jooq.web.Versions.GROUP_ID;
import static org.jooq.web.Versions.JAVADOC;
import static org.jooq.web.Versions.MINOR;
import static org.jooq.web.Versions.PATCH;
import static org.jooq.web.Versions.VERSIONS;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jooq.lambda.Seq;
import org.jooq.tools.StringUtils;
import org.apache.commons.io.IOUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
/**
* @author Lukas Eder
*/
public class ApiDiff {
static final int MAX_DEGREE = 5;
final String repository;
final Version oldVersion;
final Version newVersion;
final Map<String, List<String>> parameters;
final Map<String, List<String>> parametersShortenTypes;
public ApiDiff(String repository, Version oldVersion, Version newVersion) {
this.repository = repository;
this.oldVersion = oldVersion;
this.newVersion = newVersion;
this.parameters = new HashMap<>();
this.parametersShortenTypes = new HashMap<>();
}
private void run() throws Exception {
System.out.println("API diff generation from " + oldVersion + " to " + newVersion);
oldVersion.init();
newVersion.init();
try (PrintStream out = new PrintStream(new FileOutputStream(new File("jooq.org/api-diff/" + oldVersion.minor + "-" + newVersion.minor + ".php")))) {
Iterator<ClassNode> i1 = classes(oldVersion.classes);
Iterator<ClassNode> i2 = classes(newVersion.classes);
out.println(
"<?php\n"
+ "// The following content has been generated by ApiDiff.java\n"
+ "// Please do not edit this content manually\n"
+ "require '../frame.php';\n"
+ "function getH1() {\n"
+ " return 'API diff between " + oldVersion + " and " + newVersion + "';\n"
+ "}\n"
+ "function getActiveMenu() {\n"
+ " return 'learn';\n"
+ "}\n"
+ "function printTheme() {\n"
+ " noTheme();\n"
+ "}\n"
+ "function printContent() {\n"
+ " global $root;\n"
+ "?>\n"
+ "<div class='row col col-100 col-white headline'>\n"
+ "<h1><?=getH1()?></h1>\n"
+ "</div>\n"
+ "<style>\n"
+ "<?php require 'styles.css'; ?>\n"
+ "</style>\n"
+ "<div class='row col col-100 col-white'>\n"
+ "<table border='0' cellpadding='0' cellspacing='0' class='api-diff'>\n"
+ "<tr><th>Object</th>" + (newVersion.proAnnotationAvailable() ? "<th>Edition</th>" : "") + "<th>Modification</th></tr>\n");
Set<String> packagesPrinted = new HashSet<>();
BiConsumer<ClassNode, Modification> classWithPackageHeader = (c, modification) -> {
if (packagesPrinted.add(packageName(c)))
out.println("<tr><td class='api-diff-package'><span class='package-name'>" + formatPackage(c, modification) + "</span></td>" + (newVersion.proAnnotationAvailable() ? "<td></td>" : "") + "<td class='api-diff-modification'></td></tr>");
out.println("<tr><td class='api-diff-class api-diff-" + modification + "'><span class='" + classType(c) + "-name'>" + formatClass(c, modification) + "</span></td>" + (newVersion.proAnnotationAvailable() ? "<td>" + edition(c) + "</td>" : "") + "<td class='api-diff-modification api-diff-modification-" + modification + "'><a href='#legend'>" + (modification != null ? modification : "") + "</a></td></tr>");
};
append(i1, i2, CLASS_COMP,
c -> classWithPackageHeader.accept(c, added),
c -> classWithPackageHeader.accept(c, removed),
(c1, c2) -> {
Modification classModification = !deprecated(c1) && deprecated(c2)
? deprecated
: null;
AtomicBoolean classPrinted = new AtomicBoolean();
BiConsumer<MethodNode, Modification> methodWithClassHeader = (m, modification) -> {
if (classPrinted.compareAndSet(false, true))
classWithPackageHeader.accept(c2, classModification);
out.println("<tr><td class='api-diff-method api-diff-" + modification + "'><span class='method-name'>" + formatMethod(modification == removed ? c1 : c2, m, modification, modification == removed ? oldVersion : newVersion) + "</span></td>" + (newVersion.proAnnotationAvailable() ? "<td>" + edition(m) + "</td>" : "") + "<td class='api-diff-modification api-diff-modification-" + modification + "'><a href='#legend'>" + (modification != null ? modification : "") + "</a></td></tr>");
};
append(methods(c1), methods(c2), METHOD_COMP,
m -> methodWithClassHeader.accept(m, addedOrPushedDown(c2, m)),
m -> methodWithClassHeader.accept(m, removedOrPulledUp(c2, m)),
(m1, m2) -> {
Modification methodModification =
!deprecated(m1) && deprecated(m2)
? deprecated
: increasedContravariance(m1, m2)
? contravariance
: null;
if (methodModification != null)
methodWithClassHeader.accept(m2, methodModification);
}
);
}
);
out.println(
"</table>\n"
+ "</div>\n"
+ "<?php\n"
+ "require 'legend.php';\n"
+ "?>\n"
+ "<div class='row col col-100 col-white'>\n"
+ "Eclipse icons copyright by <a href='http://www.eclipse.org/'>Eclipse</a> licensed under <a href='http://www.eclipse.org/legal/epl-v10.html'>EPL</a>. Inspiration taken from <a href='https://javaalmanac.io/'>https://javaalmanac.io/</a>"
+ "</div>");
out.println("<?php } ?>");
}
}
private boolean increasedContravariance(MethodNode m1, MethodNode m2) {
return increasedContravariance(parameters(m1), parameters(m2));
}
private boolean increasedContravariance(List<String> p1, List<String> p2) {
if (p1.size() != p2.size())
return false;
boolean result = false;
for (int i = 0; i < p1.size(); i++) {
String s1 = p1.get(i).replaceAll("\\.\\.\\.", "[]");
String s2 = p2.get(i).replaceAll("\\.\\.\\.", "[]");
while (s1.endsWith("[]") && s2.endsWith("[]")) {
s1 = s1.replaceFirst("\\[\\]", "");
s2 = s2.replaceFirst("\\[\\]", "");
}
if (!s1.equals(s2)) {
if (!subtype(newVersion.classesBySourceName.get(s1), newVersion.classesBySourceName.get(s2)))
return false;
else
result = true;
}
}
return result;
}
private boolean subtype(ClassNode c1, ClassNode c2) {
if (c1 == null || c2 == null)
return false;
if (c1.name.equals(c2.name))
return true;
if (subtype(newVersion.classes.get(c1.superName), c2))
return true;
if (c1.interfaces != null)
for (String i : c1.interfaces)
if (subtype(newVersion.classes.get(i), c2))
return true;
return false;
}
private String classType(ClassNode c) {
if ((c.access & Opcodes.ACC_ENUM) != 0)
return "enum";
else if ((c.access & Opcodes.ACC_INTERFACE) != 0)
return "interface";
else if ((c.access & Opcodes.ACC_ANNOTATION) != 0)
return "annotation";
else
return "class";
}
private boolean deprecated(ClassNode c) {
return deprecated(c.visibleAnnotations);
}
private boolean deprecated(MethodNode m) {
return deprecated(m.visibleAnnotations);
}
private boolean deprecated(List<AnnotationNode> annotations) {
return annotations != null && annotations.stream().anyMatch(a -> a.desc.equals("Ljava/lang/Deprecated;"));
}
private Modification addedOrPushedDown(ClassNode c2, MethodNode m) {
// TODO: This pusheddown algorithm is not yet correct. If the overridden method
// is also new, then there was no push down!
return override(c2, m) ? added : added;
}
private boolean override(ClassNode c2, MethodNode m) {
return override(c2, m, false);
}
private boolean override(ClassNode c2, MethodNode m, boolean checkMethods) {
if (c2 == null)
return false;
if (override(newVersion.classes.get(c2.superName), m, true))
return true;
if (c2.interfaces != null)
for (String i : c2.interfaces)
if (override(newVersion.classes.get(i), m, true))
return true;
if (checkMethods)
for (MethodNode m2 : c2.methods)
if (m2.name.equals(m.name) && m2.desc.equals(m.desc))
return true;
return false;
}
private Modification removedOrPulledUp(ClassNode c2, MethodNode m) {
if (c2 == null)
return removed;
if (removedOrPulledUp(newVersion.classes.get(c2.superName), m) == pulledup)
return pulledup;
if (c2.interfaces != null)
for (String i : c2.interfaces)
if (removedOrPulledUp(newVersion.classes.get(i), m) == pulledup)
return pulledup;
for (MethodNode m2 : c2.methods)
if (m2.name.equals(m.name) && m2.desc.equals(m.desc))
return pulledup;
return removed;
}
private static String sourceName(String bytecodeName) {
return bytecodeName.replace("/", ".").replace("$", ".");
}
private String version(Modification modification) {
return modification == removed ? oldVersion.version : newVersion.version;
}
private String packagePath(ClassNode c) {
return c.name.replaceAll("(.*)/.*", "$1");
}
private String className(ClassNode c) {
return sourceName(c.name.replaceAll(".*/", "")).replaceAll("1$", "1 - 22");
}
private String packageName(ClassNode c) {
return sourceName(packagePath(c));
}
private String formatPackage(ClassNode c, Modification modification) {
return "<a href='<?=$root?>/javadoc/" + version(modification) + "/org.jooq/" + packagePath(c) + "/package-summary.html'>" + packageName(c) + "</a>";
}
private String formatClass(ClassNode c, Modification modification) {
return "<a href='<?=$root?>/javadoc/" + version(modification) + "/org.jooq/" + c.name + ".html'>" + className(c) + "</a>";
}
private String formatMethod(ClassNode c, MethodNode m, Modification modification, Version version) {
return "<a href='<?=$root?>/javadoc/" + version(modification) + "/org.jooq/" + c.name + ".html#" + ("8".equals(version.javadoc) ? methodName(m, false).replaceAll("[(), ]+", "-") : methodName(m, false)) + "'>" + escape(methodName(m, true)) + "</a>" + degreeSuffix(m, MAX_DEGREE - 1);
}
private String degreeSuffix(MethodNode m, int degree) {
return degreeNorMore(parameters(m), degree) ? " <sub>... and more overloads</sub>" : "";
}
private String methodName(MethodNode m, boolean shortenTypes) {
return m.desc == null && StringUtils.isBlank(m.desc)
? m.name + "()"
: m.name + "(" + parameters(m, shortenTypes).stream().collect(Collectors.joining(", ")) + ")";
}
private String edition(ClassNode c) {
return edition(c.visibleAnnotations);
}
private String edition(MethodNode m) {
return edition(m.visibleAnnotations);
}
private String edition(List<AnnotationNode> annotations) {
return annotations != null && annotations.stream().anyMatch(a -> a.desc.contains("Pro")) ? "Pro" : "All";
}
private String escape(String string) {
return string.replace("<", "&lt;").replace(">", "&gt;");
}
private final <N> void append(
Iterator<? extends N> i1,
Iterator<? extends N> i2,
Comparator<? super N> comp,
Consumer<N> create,
Consumer<N> drop,
BiConsumer<N, N> merge
) {
N n1 = null;
N n2 = null;
for (;;) {
if (n1 == null && i1.hasNext())
n1 = i1.next();
if (n2 == null && i2.hasNext())
n2 = i2.next();
if (n1 == null && n2 == null)
break;
int c = n1 == null
? 1
: n2 == null
? -1
: comp.compare(n1, n2);
if (c < 0) {
if (drop != null)
drop.accept(n1);
n1 = null;
}
else if (c > 0) {
if (create != null)
create.accept(n2);
n2 = null;
}
else {
if (merge != null)
merge.accept(n1, n2);
n1 = n2 = null;
}
}
}
private static final Pattern P_PARAM_DESC = Pattern.compile("\\(([^)]*)\\).*?");
private List<String> parameters(MethodNode m) {
return parameters(m, false);
}
private List<String> parameters(MethodNode m, boolean shortenTypes) {
return parameters(m.desc, shortenTypes, (m.access & Opcodes.ACC_VARARGS) != 0);
}
private List<String> parameters(String desc, boolean shortenTypes, boolean varargs) {
return (shortenTypes ? parametersShortenTypes : parameters).computeIfAbsent(desc, s -> {
List<String> result = new ArrayList<>();
Matcher matcher = P_PARAM_DESC.matcher(desc);
if (matcher.find()) {
String content = matcher.group(1);
int dimensions = 0;
contentLoop:
for (int i = 0; i < content.length(); i++) {
switch (content.charAt(i)) {
case '[': dimensions++; continue contentLoop;
case 'Z': result.add(array("boolean", dimensions, varargs)); break;
case 'B': result.add(array("byte", dimensions, varargs)); break;
case 'C': result.add(array("char", dimensions, varargs)); break;
case 'D': result.add(array("double", dimensions, varargs)); break;
case 'F': result.add(array("float", dimensions, varargs)); break;
case 'I': result.add(array("int", dimensions, varargs)); break;
case 'J': result.add(array("long", dimensions, varargs)); break;
case 'S': result.add(array("short", dimensions, varargs)); break;
case 'V': result.add(array("void", dimensions, varargs)); break;
case 'L': {
String type = content.substring(i + 1, content.indexOf(';', i));
result.add(array(shortenTypes ? sourceName(type.replaceAll(".*/", "")) : sourceName(type), dimensions, varargs));
i += type.length() + 1;
break;
}
}
dimensions = 0;
}
}
return result;
});
}
private String array(String type, int dimensions, boolean varargs) {
for (int i = 0; i < dimensions; i++)
type = type + (varargs && i == dimensions - 1 ? "..." : "[]");
return type;
}
private final Comparator<MethodNode> METHOD_COMP = Comparator
.<MethodNode, String>comparing(m -> m.name)
.thenComparing((m1, m2) -> {
List<String> p1 = parameters(m1);
List<String> p2 = parameters(m2);
int s1 = p1.size();
int s2 = p2.size();
int c = s1 - s2;
if (c != 0)
return c;
for (int i = 0; i < s1; i++) {
c = p1.get(i).compareTo(p2.get(i));
if (c != 0)
if (increasedContravariance(p1, p2))
return 0;
else
return c;
}
return 0;
});
private final Comparator<ClassNode> CLASS_COMP = Comparator.comparing(c -> c.name);
private boolean degreeNorMore(List<String> p, int degree) {
if (p.size() < degree)
return false;
return nCopies(p.size() - (degree - 1), p.get(p.size() - 1)).equals(p.subList((degree - 1), p.size()));
}
private Iterator<MethodNode> methods(ClassNode n) {
return Seq.seq(n.methods)
.filter(m -> (m.access & Opcodes.ACC_PUBLIC) != 0)
.filter(m -> Seq.seq(m.visibleAnnotations).noneMatch(a -> "Lorg/jooq/Internal;".equals(a.desc)))
.filter(m -> !degreeNorMore(parameters(m), MAX_DEGREE))
.sorted(METHOD_COMP)
.iterator();
}
private Iterator<ClassNode> classes(Map<String, ClassNode> cache) {
return cache.values()
.stream()
.sorted(CLASS_COMP)
.iterator();
}
public static void main(String[] args) throws Exception {
String repository = "C:/users/lukas/.m2/repository";
Version[] versions = new Version[VERSIONS.length];
for (int i = 0; i < versions.length; i++)
versions[i] = new Version(repository, VERSIONS[i][GROUP_ID], VERSIONS[i][MINOR], VERSIONS[i][PATCH], VERSIONS[i][JAVADOC]);
System.out.println("API diff overview generation");
try (PrintStream out = new PrintStream(new FileOutputStream(new File("jooq.org/api-diff/index.php")))) {
out.println(
"<?php\n"
+ "// The following content has been generated by ApiDiff.java\n"
+ "// Please do not edit this content manually\n"
+ "require '../frame.php';\n"
+ "function getH1() {\n"
+ " return 'API diff between all jOOQ versions';\n"
+ "}\n"
+ "function getActiveMenu() {\n"
+ " return 'learn';\n"
+ "}\n"
+ "function printTheme() {\n"
+ " noTheme();\n"
+ "}\n"
+ "function printContent() {\n"
+ " global $root;\n"
+ "?>\n"
+ "<div class='row col col-100 col-white headline'>\n"
+ "<h1><?=getH1()?></h1>\n"
+ "</div>\n"
+ "<style>\n"
+ "<?php require 'styles.css'; ?>\n"
+ "</style>\n"
+ "<div class='row col col-100 col-white'>\n"
+ "<table border='0' cellpadding='0' cellspacing='0' class='api-diff api-diff-overview'>\n"
+ "<tr><th>From</th>" +
Stream.of(VERSIONS)
.filter(v -> "true".equals(v[API_DIFF]))
.skip(1)
.map(v -> "<th>To " + v[MINOR] + "</th>")
.collect(joining())
+ "</tr>\n");
for (int i1 = 0; i1 < VERSIONS.length; i1++) {
int i = i1;
if ("true".equals(VERSIONS[i1][API_DIFF])
&& Seq.of(VERSIONS)
.zipWithIndex()
.filter(v -> "true".equals(v.v1[API_DIFF]))
.skip(1)
.anyMatch(v -> i < v.v2)
) {
out.println("<tr><td>" + VERSIONS[i1][MINOR] + "</td>" +
Seq.of(VERSIONS)
.zipWithIndex()
.filter(v -> "true".equals(v.v1[API_DIFF]))
.skip(1)
.map(v -> "<td>" + (i < v.v2 ? ("<a href='<?=$root?>/api-diff/" + VERSIONS[i][MINOR] + "-" + v.v1[MINOR] + "'>Diff</a>") : "") + "</td>")
.collect(joining())
+ "</tr>\n");
}
}
out.println(
"</table>\n"
+ "</div>");
out.println("<?php } ?>");
}
for (int i1 = 0; i1 < VERSIONS.length; i1++)
for (int i2 = i1 + 1; i2 < VERSIONS.length; i2++)
if ("true".equals(VERSIONS[i1][API_DIFF]) && "true".equals(VERSIONS[i2][API_DIFF]))
new ApiDiff(repository, versions[i1], versions[i2]).run();
}
static class Version {
static final Pattern P_TUPLE_TYPE = Pattern.compile("^org/jooq/.*(1\\d|2\\d|[2-9])$");
final String repository;
final String groupId;
final String minor;
final String version;
final String javadoc;
final File jarFile;
final Map<String, ClassNode> classes;
final Map<String, ClassNode> classesBySourceName;
boolean init;
Boolean proAnnotationAvailable;
Version(String repository, String groupId, String minor, String version, String javadoc) {
this.repository = repository;
this.groupId = groupId;
this.minor = minor;
this.version = version;
this.javadoc = javadoc;
this.jarFile = lookupJarFile();
this.classes = new HashMap<>();
this.classesBySourceName = new HashMap<>();
}
private File lookupJarFile() {
return new File(repository + "/" + groupId.replace(".", "/") + "/jooq/" + version + "/jooq-" + version + ".jar");
}
private void init() throws IOException {
if (!init) {
init = true;
try (JarFile j = new JarFile(jarFile)) {
j.stream()
.filter(e -> e.getName().endsWith(".class"))
.map(e -> new ClassReader(readEntry(j, e)))
.map(c -> {
ClassNode node = new ClassNode();
c.accept(node, 0);
return node;
})
.filter(c -> (c.access & Opcodes.ACC_PUBLIC) != 0)
.filter(c -> Seq.seq(c.visibleAnnotations).noneMatch(a -> "Lorg/jooq/Internal;".equals(a.desc)))
.filter(c -> !P_TUPLE_TYPE.matcher(c.name).matches())
.forEach(c -> {
classes.put(c.name, c);
classesBySourceName.put(sourceName(c.name), c);
});
}
}
}
private byte[] readEntry(JarFile jar, JarEntry e) {
try (InputStream is = jar.getInputStream(e)) {
return IOUtils.toByteArray(is);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private boolean proAnnotationAvailable() {
if (proAnnotationAvailable == null)
proAnnotationAvailable = classesBySourceName.containsKey("org.jooq.Pro");
return proAnnotationAvailable;
}
@Override
public String toString() {
return version;
}
}
enum Modification {
added,
removed,
pulledup("pulled up"),
pusheddown("pushed down"),
contravariance("contravariance"),
covariance("covariance"),
deprecated,
;
final String label;
private Modification() {
this(null);
}
private Modification(String label) {
this.label = label == null ? name() : label;
}
@Override
public String toString() {
return label;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment