diff --git a/japicmp-testbase/pom.xml b/japicmp-testbase/pom.xml index 549313a7c..b64377954 100644 --- a/japicmp-testbase/pom.xml +++ b/japicmp-testbase/pom.xml @@ -39,15 +39,6 @@ true - - - org.jsoup - jsoup - 1.15.3 - test - - - diff --git a/japicmp/src/main/java/japicmp/exception/JApiCmpException.java b/japicmp/src/main/java/japicmp/exception/JApiCmpException.java index 584d9a9a3..7ef9cddcb 100644 --- a/japicmp/src/main/java/japicmp/exception/JApiCmpException.java +++ b/japicmp/src/main/java/japicmp/exception/JApiCmpException.java @@ -16,7 +16,8 @@ public enum Reason { IllegalState, IllegalArgument, XsltError, - IncompatibleChange + IncompatibleChange, + ResourceNotFound } public JApiCmpException(Reason reason, String msg) { diff --git a/japicmp/src/main/java/japicmp/output/html/HtmlOutput.java b/japicmp/src/main/java/japicmp/output/html/HtmlOutput.java new file mode 100644 index 000000000..0ff32c810 --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/html/HtmlOutput.java @@ -0,0 +1,13 @@ +package japicmp.output.html; + +public class HtmlOutput { + private final String html; + + public HtmlOutput(String html) { + this.html = html; + } + + public String getHtml() { + return html; + } +} diff --git a/japicmp/src/main/java/japicmp/output/html/HtmlOutputGenerator.java b/japicmp/src/main/java/japicmp/output/html/HtmlOutputGenerator.java new file mode 100644 index 000000000..6e17b71ce --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/html/HtmlOutputGenerator.java @@ -0,0 +1,625 @@ +package japicmp.output.html; + +import japicmp.config.Options; +import japicmp.exception.JApiCmpException; +import japicmp.model.*; +import japicmp.output.OutputFilter; +import japicmp.output.OutputGenerator; +import japicmp.util.Streams; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static japicmp.util.StringHelper.filtersAsString; + +public class HtmlOutputGenerator extends OutputGenerator { + + private final HtmlOutputGeneratorOptions htmlOutputGeneratorOptions; + private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + public HtmlOutputGenerator(List jApiClasses, Options options, HtmlOutputGeneratorOptions htmlOutputGeneratorOptions) { + super(options, jApiClasses); + this.htmlOutputGeneratorOptions = htmlOutputGeneratorOptions; + } + + @Override + public HtmlOutput generate() { + OutputFilter outputFilter = new OutputFilter(options); + outputFilter.filter(jApiClasses); + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("\n"); + sb.append("").append(getTitle()).append("\n"); + sb.append("\n"); + sb.append("\n"); + sb.append("\n"); + sb.append("").append(getTitle()).append("\n"); + sb.append("
"); + metaInformation(sb); + warningMissingClasses(sb); + toc(sb); + explanations(sb); + classes(sb); + sb.append("\n"); + sb.append("\n"); + return new HtmlOutput(sb.toString()); + } + + private void classes(StringBuilder sb) { + sb.append(jApiClasses.stream() + .map(jApiClass -> loadAndFillTemplate("/html/class-entry.html", mapOf( + "fullyQualifiedName", jApiClass.getFullyQualifiedName(), + "outputChangeStatus", outputChangeStatus(jApiClass), + "javaObjectSerializationCompatibleClass", javaObjectSerializationCompatibleClass(jApiClass), + "javaObjectSerializationCompatible", javaObjectSerializationCompatible(jApiClass), + "modifiers", modifiers(jApiClass), + "classType", classType(jApiClass), + "compatibilityChanges", compatibilityChanges(jApiClass), + "classFileFormatVersion", classFileFormatVersion(jApiClass), + "genericTemplates", genericTemplates(jApiClass), + "superclass", superclass(jApiClass), + "interfaces", interfaces(jApiClass), + "serialVersionUid", serialVersionUid(jApiClass), + "fields", fields(jApiClass), + "constructors", constructors(jApiClass) + ))) + .collect(Collectors.joining())); + } + + private String constructors(JApiClass jApiClass) { + if (!jApiClass.getConstructors().isEmpty()) { + return loadAndFillTemplate("/html/constructors.html", mapOf( + "tbody", constructors(jApiClass.getConstructors()) + )); + } + return ""; + } + + private String constructors(List constructors) { + return constructors.stream() + .map(constructor -> "\n" + + "" + outputChangeStatus(constructor) + "\n" + + "" + modifiers(constructor) + "\n" + + "" + genericTemplates(constructor) + "\n" + + "" + constructor.getName() + "(" + parameters(constructor) + ")" + annotations(constructor.getAnnotations()) + "\n" + + "" + exceptions(constructor) + "\n" + + "" + compatibilityChanges(constructor) + "\n" + + "" + + loadAndFillTemplate("/html/line-numbers.html", mapOf( + "oldLineNumber", constructor.getOldLineNumberAsString(), + "newLineNumber", constructor.getNewLineNumberAsString())) + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String exceptions(JApiConstructor constructor) { + if (!constructor.getExceptions().isEmpty()) { + return loadAndFillTemplate("/html/exceptions.html", mapOf( + "tbody", exceptionsTBody(constructor.getExceptions()) + )); + } + return ""; + } + + private String exceptionsTBody(List exceptions) { + return exceptions.stream() + .map(exc -> "\n" + + "" + outputChangeStatus(exc) + "\n" + + "" + exc.getName() + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String parameters(JApiConstructor constructor) { + return constructor.getParameters().stream() + .map(parameter -> "" + + parameter.getType() + + genericParameterTypes(parameter) + + binaryAndSourceCompatibility(parameter) + + "") + .collect(Collectors.joining(", ")); + } + + private String fields(JApiClass jApiClass) { + if (!jApiClass.getFields().isEmpty()) { + return loadAndFillTemplate("/html/fields.html", mapOf( + "tbody", fields(jApiClass.getFields()) + )); + } + return ""; + } + + private String fields(List fields) { + return fields.stream() + .map(field -> "\n" + + "" + outputChangeStatus(field) + "\n" + + "" + modifiers(field) + "\n" + + "" + type(field) + "\n" + + "" + field.getName() + annotations(field.getAnnotations()) + "\n" + + "" + compatibilityChanges(field) + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String type(JApiField field) { + return "" + + typeValue(field) + + ""; + } + + private String typeValue(JApiField field) { + JApiType type = field.getType(); + switch (type.getChangeStatus()) { + case NEW: + case UNCHANGED: + return type.getNewValue() + genericParameterTypes(field); + case REMOVED: + return type.getOldValue() + genericParameterTypes(field); + case MODIFIED: + return type.getNewValue() + " (<- " + type.getOldValue() + genericParameterTypes(field); + } + return ""; + } + + private String annotations(List annotations) { + if (!annotations.isEmpty()) { + return loadAndFillTemplate("/html/annotations.html", mapOf( + "tbody", annotationsTBody(annotations) + )); + } + return ""; + } + + private String annotationsTBody(List annotations) { + return annotations.stream() + .map(annotation -> "\n" + + "" + outputChangeStatus(annotation) + "\n" + + "" + annotation.getFullyQualifiedName() + "\n" + + "" + elements(annotation) + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String elements(JApiAnnotation annotation) { + if (!annotation.getElements().isEmpty()) { + return loadAndFillTemplate("/html/annotation-elements.html", mapOf( + "tbody", annotationElements(annotation.getElements()) + )); + } else { + return "n.a."; + } + } + + private String annotationElements(List elements) { + return elements.stream() + .map(element -> "\n" + + "" + outputChangeStatus(element) + "\n" + + "" + element.getName() + "\n" + + "" + element.getOldElementValues().stream() + .map(this::valueToString) + .collect(Collectors.joining()) + + "\n" + + "" + element.getNewElementValues().stream() + .map(this::valueToString) + .collect(Collectors.joining()) + + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String valueToString(JApiAnnotationElementValue value) { + switch (value.getType()) { + case Annotation: + return "@" + value.getFullyQualifiedName() + "(" + values(value) + ")"; + case Array: + return "{" + values(value) + "}"; + case Enum: + return value.getFullyQualifiedName() + "." + value.getValue(); + default: + return String.valueOf(value); + } + } + + private String values(JApiAnnotationElementValue value) { + return value.getValues().stream() + .map(this::valueToString) + .collect(Collectors.joining()); + } + + private String serialVersionUid(JApiClass jApiClass) { + if (jApiClass.getSerialVersionUid().isSerializableOld() || jApiClass.getSerialVersionUid().isSerializableNew()) { + return loadAndFillTemplate("/html/serial-version-uid.html", mapOf( + "tbody", serialVersionUidTBody(jApiClass.getSerialVersionUid()) + )); + } + return ""; + } + + private String serialVersionUidTBody(JApiSerialVersionUid serialVersionUid) { + return "\n" + + "Old" + + "" + serialVersionUid.isSerializableOld() + "\n" + + "" + serialVersionUid.getSerialVersionUidDefaultOldAsString() + "\n" + + "" + serialVersionUid.getSerialVersionUidInClassOldAsString() + "\n" + + "\n" + + "\n" + + "New" + + "" + serialVersionUid.isSerializableNew() + "\n" + + "" + serialVersionUid.getSerialVersionUidDefaultNewAsString() + "\n" + + "" + serialVersionUid.getSerialVersionUidInClassNewAsString() + "\n" + + "\n"; + } + + private String interfaces(JApiClass jApiClass) { + if (!jApiClass.getInterfaces().isEmpty()) { + return loadAndFillTemplate("/html/interfaces.html", mapOf( + "tbody", interfacesTBody(jApiClass.getInterfaces()) + )); + } + return ""; + } + + private String interfacesTBody(List interfaces) { + return interfaces.stream() + .map(interfaze -> "\n" + + "" + outputChangeStatus(interfaze) + "\n" + + "" + interfaze.getFullyQualifiedName() + "\n" + + "" + compatibilityChanges(interfaze) + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String superclass(JApiClass jApiClass) { + JApiSuperclass superclass = jApiClass.getSuperclass(); + if ((superclass.getOldSuperclass().isPresent() || superclass.getNewSuperclass().isPresent()) && + ((superclass.getChangeStatus() == JApiChangeStatus.NEW && !superclass.getSuperclassNew().equalsIgnoreCase("java.lang.Object")) || + (superclass.getChangeStatus() == JApiChangeStatus.REMOVED && !superclass.getSuperclassOld().equalsIgnoreCase("java.lang.Object")) || + (superclass.getChangeStatus() == JApiChangeStatus.MODIFIED) || + (superclass.getChangeStatus() == JApiChangeStatus.UNCHANGED && !superclass.getSuperclassOld().equalsIgnoreCase("java.lang.Object")) + ) + ) { + return loadAndFillTemplate("/html/superclass.html", mapOf( + "tbody", superclassTBody(jApiClass.getSuperclass()) + )); + } + return ""; + } + + private String superclassTBody(JApiSuperclass superclass) { + return "\n" + + "" + outputChangeStatus(superclass) + "\n" + + "" + superclassName(superclass) + "\n" + + "" + compatibilityChanges(superclass) + "\n" + + "\n"; + } + + private String superclassName(JApiSuperclass superclass) { + switch (superclass.getChangeStatus()) { + case NEW: + case UNCHANGED: + return superclass.getSuperclassNew(); + case REMOVED: + return superclass.getSuperclassOld(); + case MODIFIED: + return superclass.getSuperclassNew() + "(<- " + superclass.getSuperclassOld() + ")"; + } + return ""; + } + + private String genericTemplates(JApiHasGenericTemplates jApiHasGenericTemplates) { + List genericTemplates = jApiHasGenericTemplates.getGenericTemplates(); + if (!genericTemplates.isEmpty()) { + return "Generic Templates:\n" + + loadAndFillTemplate("/html/generic-templates.html", mapOf( + "tbody", genericTemplatesTBody(genericTemplates) + )); + } + return ""; + } + + private String genericTemplatesTBody(List genericTemplates) { + return genericTemplates.stream() + .map(jApiGenericTemplate -> "\n" + + "" + outputChangeStatus(jApiGenericTemplate) + "\n" + + "" + jApiGenericTemplate.getName() + "\n" + + "" + jApiGenericTemplate.getOldType() + interfaceTypes(jApiGenericTemplate.getOldInterfaceTypes()) + "\n" + + "" + jApiGenericTemplate.getNewType() + "\n" + + "" + genericParameterTypes(jApiGenericTemplate) + "\n" + + "\n") + .collect(Collectors.joining()); + } + + private String interfaceTypes(List interfaceTypes) { + if (!interfaceTypes.isEmpty()) { + return interfaceTypes.stream() + .map(interfaceType -> "& " + interfaceType.getType() + genericParameterTypesRecursive(interfaceType)) + .collect(Collectors.joining()); + } + return ""; + } + + private String genericParameterTypesRecursive(JApiGenericType jApiGenericType) { + if (!jApiGenericType.getGenericTypes().isEmpty()) { + return "<" + jApiGenericType.getGenericTypes().stream() + .map(jApiGenericType1 -> genericParameterWithWildcard(jApiGenericType1) + genericParameterTypesRecursive(jApiGenericType1)) + .collect(Collectors.joining(",")) + + ">"; + } + return ""; + } + + private String genericParameterWithWildcard(JApiGenericType jApiGenericType1) { + switch (jApiGenericType1.getGenericWildCard()) { + case NONE: + return jApiGenericType1.getType(); + case EXTENDS: + return "? extends " + jApiGenericType1.getType(); + case SUPER: + return "? super " + jApiGenericType1.getType(); + case UNBOUNDED: + return "? " + jApiGenericType1.getType(); + } + return ""; + } + + private String genericParameterTypes(JApiHasGenericTypes jApiHasGenericTypes) { + if (!jApiHasGenericTypes.getNewGenericTypes().isEmpty() || !jApiHasGenericTypes.getOldGenericTypes().isEmpty()) { + return "
" + + "<..>" + + "
" + + "" + + genericTypes("New", jApiHasGenericTypes.getNewGenericTypes()) + + genericTypes("Old", jApiHasGenericTypes.getOldGenericTypes()) + + "
" + + "
" + + "
"; + } + return ""; + } + + private String genericTypes(String header, List genericTypes) { + if (!genericTypes.isEmpty()) { + return "" + + "" + header + ":" + + genericTypes.stream() + .map(genericType -> "" + + genericParameterWithWildcard(genericType) + + genericParameterTypesRecursive(genericType) + + "") + .collect(Collectors.joining()) + + ""; + } + return ""; + } + + private String classFileFormatVersion(JApiClass jApiClass) { + if (jApiClass.getClassFileFormatVersion().getChangeStatus() == JApiChangeStatus.MODIFIED) { + return loadAndFillTemplate("/html/class-file-format-version.html", mapOf( + "tbody", classFileFormatVersionTBody(jApiClass) + )); + } + return ""; + } + + private String classFileFormatVersionTBody(JApiClass jApiClass) { + JApiClassFileFormatVersion classFileFormatVersion = jApiClass.getClassFileFormatVersion(); + return "\n" + + "" + outputChangeStatus(classFileFormatVersion) + "\n" + + "" + classFileFormatVersionString(classFileFormatVersion.getMajorVersionOld(), classFileFormatVersion.getMinorVersionOld()) + "\n" + + "" + classFileFormatVersionString(classFileFormatVersion.getMajorVersionNew(), classFileFormatVersion.getMinorVersionNew()) + "\n" + + "\n"; + } + + private String classFileFormatVersionString(int majorVersion, int minorVersion) { + if (majorVersion >= 0 && minorVersion >= 0) { + return majorVersion + "." + minorVersion; + } + return "n.a."; + } + + private String compatibilityChanges(JApiCompatibility jApiClass) { + if (!jApiClass.getCompatibilityChanges().isEmpty()) { + return loadAndFillTemplate("/html/compatibility-changes.html", mapOf( + "tbody", jApiClass.getCompatibilityChanges().stream() + .map(this::compatibilityChange) + .collect(Collectors.joining()) + )); + } + return ""; + } + + private String compatibilityChange(JApiCompatibilityChange jApiCompatibilityChange) { + return "" + jApiCompatibilityChange.getType() + "\n"; + } + + private String classType(JApiClass jApiClass) { + return "" + + classTypeValue(jApiClass.getClassType()) + + "\n"; + } + + private String classTypeValue(JApiClassType classType) { + if (classType.getChangeStatus() == JApiChangeStatus.MODIFIED) { + return classType.getNewType().toLowerCase() + " (<- " + classType.getOldType().toLowerCase() + ")"; + } else if (classType.getChangeStatus() == JApiChangeStatus.NEW || classType.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + return classType.getNewType().toLowerCase(); + } else if (classType.getChangeStatus() == JApiChangeStatus.REMOVED) { + return classType.getOldType().toLowerCase(); + } + return ""; + } + + private String modifiers(JApiHasModifiers jApiHasModifiers) { + return jApiHasModifiers.getModifiers().stream() + .map(jApiModifier -> { + String modifier = modifier(jApiModifier); + if (!modifier.trim().isEmpty()) { + return "" + + modifier + + "\n"; + } + return ""; + }) + .collect(Collectors.joining()); + } + + private String modifier(JApiModifier>> jApiModifier) { + if (jApiModifier.getChangeStatus() == JApiChangeStatus.MODIFIED) { + return jApiModifier.getValueNew() + " (<- " + jApiModifier.getValueOld() + ") "; + } else if (jApiModifier.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + return jApiModifier.getValueNew().toLowerCase().startsWith("non") || jApiModifier.getValueNew().equalsIgnoreCase("package_protected") ? "" : jApiModifier.getValueNew().toLowerCase(); + } else if (jApiModifier.getChangeStatus() == JApiChangeStatus.NEW) { + return jApiModifier.getValueNew().toLowerCase().startsWith("non") || jApiModifier.getValueNew().equalsIgnoreCase("package_protected") ? "" : jApiModifier.getValueNew().toLowerCase(); + } else if (jApiModifier.getChangeStatus() == JApiChangeStatus.REMOVED) { + return jApiModifier.getValueOld().toLowerCase().startsWith("non") || jApiModifier.getValueOld().equalsIgnoreCase("package_protected") ? "" : jApiModifier.getValueOld().toLowerCase(); + } + return ""; + } + + private String javaObjectSerializationCompatibleClass(JApiClass jApiClass) { + if (jApiClass.getJavaObjectSerializationCompatible() == JApiJavaObjectSerializationCompatibility.JApiJavaObjectSerializationChangeStatus.NOT_SERIALIZABLE) { + return ""; + } else if (jApiClass.getJavaObjectSerializationCompatible() == JApiJavaObjectSerializationCompatibility.JApiJavaObjectSerializationChangeStatus.SERIALIZABLE_COMPATIBLE) { + return "new"; + } else { + return "removed"; + } + } + + private String javaObjectSerializationCompatible(JApiClass jApiClass) { + if (jApiClass.getJavaObjectSerializationCompatible() == JApiJavaObjectSerializationCompatibility.JApiJavaObjectSerializationChangeStatus.NOT_SERIALIZABLE) { + return ""; + } else if (jApiClass.getJavaObjectSerializationCompatible() == JApiJavaObjectSerializationCompatibility.JApiJavaObjectSerializationChangeStatus.SERIALIZABLE_COMPATIBLE) { + return " (Serializable compatible) "; + } else { + return " (Serializable incompatible(!): " + jApiClass.getJavaObjectSerializationCompatibleAsString() + ") "; + } + } + + private void explanations(StringBuilder sb) { + sb.append("
\n" + + "Binary incompatible changes are marked with (!) while source incompatible changes are marked with (*).\n" + + "
\n"); + } + + private void toc(StringBuilder sb) { + if (!jApiClasses.isEmpty()) { + sb.append("
    \n"); + sb.append("
  • \n"); + sb.append("Classes\n"); + sb.append("
  • \n"); + sb.append("
\n"); + sb.append(loadAndFillTemplate("/html/toc.html", mapOf( + "tbody", tocEntries() + ))); + } + } + + private String tocEntries() { + return jApiClasses.stream() + .map(jApiClass -> loadAndFillTemplate("/html/toc-entry.html", mapOf( + "outputChangeStatus", outputChangeStatus(jApiClass), + "fullyQualifiedName", jApiClass.getFullyQualifiedName() + ))) + .collect(Collectors.joining()); + } + + private String outputChangeStatus(JApiHasChangeStatus jApiHasChangeStatus) { + return "" + + jApiHasChangeStatus.getChangeStatus().name() + + (jApiHasChangeStatus instanceof JApiCompatibility ? binaryAndSourceCompatibility((JApiCompatibility) jApiHasChangeStatus) : "") + + ""; + } + + private String binaryAndSourceCompatibility(JApiCompatibility jApiCompatibility) { + if (!jApiCompatibility.isBinaryCompatible()) { + return " (!)"; + } else if (jApiCompatibility.isBinaryCompatible() && !jApiCompatibility.isSourceCompatible()) { + return " (*)"; + } + return ""; + } + + private void warningMissingClasses(StringBuilder sb) { + if (options.getIgnoreMissingClasses().isIgnoreAllMissingClasses()) { + sb.append("
\n" + + "\n" + + "WARNING: You are using the option '--ignore-missing-classes', i.e. superclasses and\n" + + "interfaces that could not be found on the classpath are ignored. Hence changes\n" + + "caused by these superclasses and interfaces are not reflected in the output.\n" + + "\n" + + "
" + ); + } + } + + private String getStyle() { + String styleSheet; + if (options.getHtmlStylesheet().isPresent()) { + try { + InputStream inputStream = new FileInputStream(options.getHtmlStylesheet().get()); + styleSheet = Streams.asString(inputStream); + } catch (FileNotFoundException e) { + throw new JApiCmpException(JApiCmpException.Reason.IoException, "Failed to load stylesheet: " + e.getMessage(), e); + } + } else { + styleSheet = loadTemplate("/style.css"); + } + return styleSheet; + } + + private void metaInformation(StringBuilder sb) { + sb.append(loadAndFillTemplate("/html/meta-information.html", mapOf( + "oldJar", options.joinOldArchives(), + "newJar", options.joinNewArchives(), + "newJar", options.joinNewArchives(), + "creationTimestamp", DATE_FORMAT.format(new Date()), + "accessModifier", options.getAccessModifier().name(), + "onlyModifications", String.valueOf(options.isOutputOnlyModifications()), + "onlyBinaryIncompatibleModifications", String.valueOf(options.isOutputOnlyBinaryIncompatibleModifications()), + "ignoreMissingClasses", String.valueOf(options.getIgnoreMissingClasses().isIgnoreAllMissingClasses()), + "packagesInclude", filtersAsString(options.getIncludes(), true), + "packagesExclude", filtersAsString(options.getExcludes(), false), + "semanticVersioning", htmlOutputGeneratorOptions.getSemanticVersioningInformation() + ))).append("\n"); + } + + private Map mapOf(String... args) { + Map map = new HashMap<>(); + int count = args.length / 2; + for (int i = 0; i < count; i++) { + map.put(args[i * 2], args[(i * 2) + 1]); + } + return map; + } + + private String loadAndFillTemplate(String path, Map params) { + String template = loadTemplate(path); + for (Map.Entry entry : params.entrySet()) { + template = template.replace("${" + entry.getKey() + "}", entry.getValue()); + } + return template; + } + + private String loadTemplate(String path) { + InputStream resourceAsStream = HtmlOutputGenerator.class.getResourceAsStream(path); + if (resourceAsStream == null) { + throw new JApiCmpException(JApiCmpException.Reason.ResourceNotFound, "Failed to load: " + path); + } + return Streams.asString(resourceAsStream); + } + + private String getTitle() { + if (this.htmlOutputGeneratorOptions.getTitle().isPresent()) { + return this.htmlOutputGeneratorOptions.getTitle().get(); + } + return "japicmp-Report"; + } +} diff --git a/japicmp/src/main/java/japicmp/output/html/HtmlOutputGeneratorOptions.java b/japicmp/src/main/java/japicmp/output/html/HtmlOutputGeneratorOptions.java new file mode 100644 index 000000000..2f8d96afa --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/html/HtmlOutputGeneratorOptions.java @@ -0,0 +1,24 @@ +package japicmp.output.html; + +import japicmp.util.Optional; + +public class HtmlOutputGeneratorOptions { + private Optional title = Optional.absent(); + private String semanticVersioningInformation = "n.a."; + + public Optional getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = Optional.fromNullable(title); + } + + public String getSemanticVersioningInformation() { + return semanticVersioningInformation; + } + + public void setSemanticVersioningInformation(String semanticVersioningInformation) { + this.semanticVersioningInformation = semanticVersioningInformation; + } +} diff --git a/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java b/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java index 0024d9502..5a765d4d6 100644 --- a/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java +++ b/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java @@ -1,38 +1,25 @@ package japicmp.output.xml; -import com.google.common.base.Joiner; -import japicmp.util.Optional; import japicmp.config.Options; import japicmp.exception.JApiCmpException; import japicmp.exception.JApiCmpException.Reason; -import japicmp.filter.Filter; import japicmp.model.JApiClass; import japicmp.output.OutputFilter; import japicmp.output.OutputGenerator; import japicmp.output.extapi.jpa.JpaAnalyzer; import japicmp.output.extapi.jpa.model.JpaTable; import japicmp.output.xml.model.JApiCmpXmlRoot; +import japicmp.util.Optional; import japicmp.util.Streams; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.SchemaOutputResolver; -import javax.xml.transform.Result; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; +import javax.xml.transform.*; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; +import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; @@ -44,6 +31,8 @@ import java.util.logging.Logger; import java.util.regex.Pattern; +import static japicmp.util.StringHelper.filtersAsString; + public class XmlOutputGenerator extends OutputGenerator { private static final String XSD_FILENAME = "japicmp.xsd"; private static final String XML_SCHEMA = XSD_FILENAME; @@ -240,17 +229,5 @@ private String regExAsString(List ignoreMissingClassRegularExpression) return sb.toString(); } - private String filtersAsString(List filters, boolean include) { - String join; - if (filters.size() == 0) { - if (include) { - join = "all"; - } else { - join = "n.a."; - } - } else { - join = Joiner.on(";").skipNulls().join(filters); - } - return join; - } + } diff --git a/japicmp/src/main/java/japicmp/util/StringHelper.java b/japicmp/src/main/java/japicmp/util/StringHelper.java new file mode 100644 index 000000000..e14fd0580 --- /dev/null +++ b/japicmp/src/main/java/japicmp/util/StringHelper.java @@ -0,0 +1,27 @@ +package japicmp.util; + +import com.google.common.base.Joiner; +import japicmp.filter.Filter; + +import java.util.List; + +public class StringHelper { + + private StringHelper() { + // private constructor + } + + public static String filtersAsString(List filters, boolean include) { + String join; + if (filters.isEmpty()) { + if (include) { + join = "all"; + } else { + join = "n.a."; + } + } else { + join = Joiner.on(";").skipNulls().join(filters); + } + return join; + } +} diff --git a/japicmp/src/main/resources/html/annotation-elements.html b/japicmp/src/main/resources/html/annotation-elements.html new file mode 100644 index 000000000..4f5e0d11c --- /dev/null +++ b/japicmp/src/main/resources/html/annotation-elements.html @@ -0,0 +1,13 @@ + + + + + + + + + + + ${tbody} + +
Status:Name:Old element values:New element values:
diff --git a/japicmp/src/main/resources/html/annotations.html b/japicmp/src/main/resources/html/annotations.html new file mode 100644 index 000000000..fb3744d76 --- /dev/null +++ b/japicmp/src/main/resources/html/annotations.html @@ -0,0 +1,15 @@ +
+ Annotations: + + + + + + + + + + ${tbody} + +
Status:Fully Qualified Name:Elements:
+
diff --git a/japicmp/src/main/resources/html/class-entry.html b/japicmp/src/main/resources/html/class-entry.html new file mode 100644 index 000000000..d32806fe6 --- /dev/null +++ b/japicmp/src/main/resources/html/class-entry.html @@ -0,0 +1,57 @@ +
+
+
+ + + ${outputChangeStatus} + ${javaObjectSerializationCompatible} + ${modifiers} + ${classType} ${fullyQualifiedName} + + top +
+ ${compatibilityChanges} + ${classFileFormatVersion} +
+ ${genericTemplates} +
+
+ ${superclass} +
+
+ ${interfaces} +
+ ${serialVersionUid} +
+ ${fields} +
+
+ ${constructors} +
+
+ + Methods: + + + + + + + + + + + + + + + + + + +
StatusModifierGeneric TemplatesTypeMethodExceptionsCompatibility Changes:Line Number
+
+
+ +
+
diff --git a/japicmp/src/main/resources/html/class-file-format-version.html b/japicmp/src/main/resources/html/class-file-format-version.html new file mode 100644 index 000000000..6a5e66542 --- /dev/null +++ b/japicmp/src/main/resources/html/class-file-format-version.html @@ -0,0 +1,15 @@ +
+ class File Format Version: + + + + + + + + + + ${tbody} + +
StatusOld VersionNew Version
+
diff --git a/japicmp/src/main/resources/html/compatibility-changes.html b/japicmp/src/main/resources/html/compatibility-changes.html new file mode 100644 index 000000000..3b3fe5546 --- /dev/null +++ b/japicmp/src/main/resources/html/compatibility-changes.html @@ -0,0 +1,13 @@ +
+ Compatibility Changes: + + + + + + + + ${tbody} + +
Change
+
diff --git a/japicmp/src/main/resources/html/constructors.html b/japicmp/src/main/resources/html/constructors.html new file mode 100644 index 000000000..5d5c547a0 --- /dev/null +++ b/japicmp/src/main/resources/html/constructors.html @@ -0,0 +1,17 @@ +Constructors: + + + + + + + + + + + + + + ${tbody} + +
StatusModifierGeneric TemplatesConstructorExceptionsCompatibility Changes:Line Number
diff --git a/japicmp/src/main/resources/html/exceptions.html b/japicmp/src/main/resources/html/exceptions.html new file mode 100644 index 000000000..c5718a54c --- /dev/null +++ b/japicmp/src/main/resources/html/exceptions.html @@ -0,0 +1,11 @@ + + + + + + + + + ${tbody} + +
Status:Name:
diff --git a/japicmp/src/main/resources/html/fields.html b/japicmp/src/main/resources/html/fields.html new file mode 100644 index 000000000..f012bbc5a --- /dev/null +++ b/japicmp/src/main/resources/html/fields.html @@ -0,0 +1,15 @@ +Fields: + + + + + + + + + + + + ${tbody} + +
StatusModifierTypeFieldCompatibility Changes:
diff --git a/japicmp/src/main/resources/html/generic-templates.html b/japicmp/src/main/resources/html/generic-templates.html new file mode 100644 index 000000000..41ac159f3 --- /dev/null +++ b/japicmp/src/main/resources/html/generic-templates.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + ${tbody} + +
Change StatusNameOld TypeNew TypeGenerics
diff --git a/japicmp/src/main/resources/html/interfaces.html b/japicmp/src/main/resources/html/interfaces.html new file mode 100644 index 000000000..210336b65 --- /dev/null +++ b/japicmp/src/main/resources/html/interfaces.html @@ -0,0 +1,13 @@ +Interfaces: + + + + + + + + + + ${tbody} + +
StatusInterfaceCompatibility Changes
diff --git a/japicmp/src/main/resources/html/line-numbers.html b/japicmp/src/main/resources/html/line-numbers.html new file mode 100644 index 000000000..41fd971b7 --- /dev/null +++ b/japicmp/src/main/resources/html/line-numbers.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + +
Old fileNew file
+ ${oldLineNumber} + + ${newLineNumber} +
diff --git a/japicmp/src/main/resources/html/meta-information.html b/japicmp/src/main/resources/html/meta-information.html new file mode 100644 index 000000000..d1cc506a3 --- /dev/null +++ b/japicmp/src/main/resources/html/meta-information.html @@ -0,0 +1,64 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Old: + ${oldJar} +
New: + ${newJar} +
Created: + ${creationTimestamp} +
Access modifier filter: + ${accessModifier} +
Only modifications: + ${onlyModifications} +
Only binary incompatible modifications: + ${onlyBinaryIncompatibleModifications} +
Ignore missing classes: + ${ignoreMissingClasses} +
Includes: + ${packagesInclude} +
Excludes: + ${packagesExclude} +
Semantic Versioning: + ${semanticVersioning} +
+
diff --git a/japicmp/src/main/resources/html/serial-version-uid.html b/japicmp/src/main/resources/html/serial-version-uid.html new file mode 100644 index 000000000..14d0c1022 --- /dev/null +++ b/japicmp/src/main/resources/html/serial-version-uid.html @@ -0,0 +1,15 @@ +
+ + + + + + + + + + + ${tbody} + +
Serializabledefault serialVersionUIDserialVersionUID in class
+
diff --git a/japicmp/src/main/resources/html/superclass.html b/japicmp/src/main/resources/html/superclass.html new file mode 100644 index 000000000..c76087faa --- /dev/null +++ b/japicmp/src/main/resources/html/superclass.html @@ -0,0 +1,13 @@ +Superclass: + + + + + + + + + + ${tbody} + +
StatusSuperclassCompatibility Changes
diff --git a/japicmp/src/main/resources/html/toc-entry.html b/japicmp/src/main/resources/html/toc-entry.html new file mode 100644 index 000000000..25188ef6e --- /dev/null +++ b/japicmp/src/main/resources/html/toc-entry.html @@ -0,0 +1,10 @@ + + + ${outputChangeStatus} + + + + ${fullyQualifiedName} + + + diff --git a/japicmp/src/main/resources/html/toc.html b/japicmp/src/main/resources/html/toc.html new file mode 100644 index 000000000..68b3e24b1 --- /dev/null +++ b/japicmp/src/main/resources/html/toc.html @@ -0,0 +1,14 @@ +
+ Classes: + + + + + + + + + ${tbody} + +
StatusFully Qualified Name
+
diff --git a/japicmp/src/test/java/japicmp/output/html/HtmlOutputGeneratorTest.java b/japicmp/src/test/java/japicmp/output/html/HtmlOutputGeneratorTest.java new file mode 100644 index 000000000..3267313e7 --- /dev/null +++ b/japicmp/src/test/java/japicmp/output/html/HtmlOutputGeneratorTest.java @@ -0,0 +1,63 @@ +package japicmp.output.html; + +import japicmp.cmp.ClassesHelper; +import japicmp.cmp.JarArchiveComparatorOptions; +import japicmp.config.Options; +import japicmp.model.JApiClass; +import japicmp.util.*; +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.NotFoundException; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.StringContains.containsString; + +public class HtmlOutputGeneratorTest { + + @Test + public void testHtmlReport() throws Exception { + JarArchiveComparatorOptions options = new JarArchiveComparatorOptions(); + options.setIncludeSynthetic(true); + List jApiClasses = ClassesHelper.compareClasses(options, new ClassesHelper.ClassesGenerator() { + @Override + public List createOldClasses(ClassPool classPool) throws CannotCompileException { + CtClass ctClass = CtClassBuilder.create().name("japicmp.Test").addToClassPool(classPool); + CtMethodBuilder.create().name("toBeRemoved").returnType(CtClass.booleanType).addToClass(ctClass); + return Collections.singletonList(ctClass); + } + + @Override + public List createNewClasses(ClassPool classPool) throws CannotCompileException, NotFoundException { + CtClass newInterface = CtInterfaceBuilder.create().name("NewInterface").addToClassPool(classPool); + CtClass superclass = CtClassBuilder.create().name("japicmp.Superclass").addToClassPool(classPool); + CtClass ctClass = CtClassBuilder.create().name("japicmp.Test").withSuperclass(superclass).implementsInterface(newInterface).addToClassPool(classPool); + CtMethodBuilder.create().name("newMethod").returnType(CtClass.booleanType).addToClass(ctClass); + CtFieldBuilder.create().type(CtClass.booleanType).name("bField").addToClass(ctClass); + CtConstructorBuilder.create().publicAccess().parameters(new CtClass[] {CtClass.intType, CtClass.booleanType}).exceptions(new CtClass[] {classPool.get("java.lang.Exception")}).addToClass(ctClass); + return Arrays.asList(ctClass, superclass); + } + }); + Options reportOptions = Options.newDefault(); + reportOptions.setIgnoreMissingClasses(true); + HtmlOutputGenerator generator = new HtmlOutputGenerator(jApiClasses, reportOptions, new HtmlOutputGeneratorOptions()); + + HtmlOutput htmlOutput = generator.generate(); + + Files.write(Paths.get(System.getProperty("user.dir"), "target", "report.html"), htmlOutput.getHtml().getBytes(StandardCharsets.UTF_8)); + Document document = Jsoup.parse(htmlOutput.getHtml()); + assertThat(document.select("#meta-accessmodifier-value").text(), is("PROTECTED")); + assertThat(document.select("#warning-missingclasses").text(), containsString("WARNING")); + } +} diff --git a/pom.xml b/pom.xml index 06e3a5841..5ce02ee3c 100644 --- a/pom.xml +++ b/pom.xml @@ -171,6 +171,12 @@ 4.3.1 test + + org.jsoup + jsoup + 1.15.3 + test +