This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to annotated tag japicmp-base-0.0.1 in repository https://gitbox.apache.org/repos/asf/commons-vfs.git
commit 0ed6bdc36c762c04f724510102237a1c5760b59e Author: mmois <martin.m...@googlemail.com> AuthorDate: Sun Sep 29 13:31:27 2013 +0200 Initial commit --- .gitignore | 8 ++ japicmp/pom.xml | 56 +++++++++++ japicmp/src/main/java/japicmp/JApiCmp.java | 62 +++++++++++++ japicmp/src/main/java/japicmp/cli/CliParser.java | 63 +++++++++++++ .../src/main/java/japicmp/cmp/ClassComparator.java | 72 ++++++++++++++ .../main/java/japicmp/cmp/ClassesComparator.java | 64 +++++++++++++ .../java/japicmp/cmp/JarArchiveComparator.java | 83 +++++++++++++++++ japicmp/src/main/java/japicmp/config/Options.java | 42 +++++++++ .../main/java/japicmp/model/JApiChangeStatus.java | 5 + japicmp/src/main/java/japicmp/model/JApiClass.java | 83 +++++++++++++++++ .../src/main/java/japicmp/model/JApiMethod.java | 61 ++++++++++++ .../src/main/java/japicmp/model/JApiParameter.java | 16 ++++ .../java/japicmp/output/OutputTransformer.java | 55 +++++++++++ .../output/stdout/StdoutOutputGenerator.java | 71 ++++++++++++++ .../japicmp/output/xml/XmlOutputGenerator.java | 24 +++++ .../japicmp/output/xml/model/JApiCmpXmlRoot.java | 43 +++++++++ .../main/java/japicmp/util/SignatureParser.java | 103 +++++++++++++++++++++ .../java/japicmp/util/StringArrayEnumeration.java | 27 ++++++ japicmp/src/main/resources/log4j.properties | 8 ++ .../src/test/java/japicmp/cli/CliParserTest.java | 40 ++++++++ .../java/japicmp/util/SignatureParserTest.java | 99 ++++++++++++++++++++ .../japicmp/util/StringArrayEnumerationTest.java | 37 ++++++++ pom.xml | 43 +++++++++ 23 files changed, 1165 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2e9750 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target +.idea +httpclient*.jar +*.iml +test.xml +output.xml +japicmp/guava-10.0.1.jar +japicmp/guava-14.0.1.jar diff --git a/japicmp/pom.xml b/japicmp/pom.xml new file mode 100644 index 0000000..f26ebbf --- /dev/null +++ b/japicmp/pom.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>japicmp</groupId> + <artifactId>japicmp-base</artifactId> + <version>0.0.1-SNAPSHOT</version> + </parent> + + <artifactId>japicmp</artifactId> + + <dependencies> + <dependency> + <groupId>javassist</groupId> + <artifactId>javassist</artifactId> + <version>3.12.1.GA</version> + </dependency> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <version>1.2.17</version> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>14.0.1</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>cobertura-maven-plugin</artifactId> + <version>2.5.2</version> + <configuration> + <formats> + <format>html</format> + </formats> + </configuration> + <executions> + <execution> + <phase>verify</phase> + <goals> + <goal>clean</goal> + <goal>cobertura</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> \ No newline at end of file diff --git a/japicmp/src/main/java/japicmp/JApiCmp.java b/japicmp/src/main/java/japicmp/JApiCmp.java new file mode 100644 index 0000000..70c861f --- /dev/null +++ b/japicmp/src/main/java/japicmp/JApiCmp.java @@ -0,0 +1,62 @@ +package japicmp; + +import japicmp.cli.CliParser; +import japicmp.cmp.JarArchiveComparator; +import japicmp.config.Options; +import japicmp.model.JApiClass; +import japicmp.output.OutputTransformer; +import japicmp.output.stdout.StdoutOutputGenerator; +import japicmp.output.xml.XmlOutputGenerator; + +import java.io.File; +import java.util.List; + +public class JApiCmp { + + public static void main(String[] args) { + Options options = parseCliOptions(args); + File oldArchive = new File(options.getOldArchive()); + File newArchive = new File(options.getNewArchive()); + verifyFilesExist(oldArchive, newArchive); + JarArchiveComparator jarArchiveComparator = new JarArchiveComparator(); + List<JApiClass> jApiClasses = jarArchiveComparator.compare(oldArchive, newArchive); + generateOutput(options, oldArchive, newArchive, jApiClasses); + } + + private static void generateOutput(Options options, File oldArchive, File newArchive, List<JApiClass> jApiClasses) { + OutputTransformer.sortClassesAndMethods(jApiClasses); + if(options.getXmlOutputFile().isPresent()) { + XmlOutputGenerator xmlGenerator = new XmlOutputGenerator(); + xmlGenerator.generate(oldArchive, newArchive, jApiClasses, options); + } + StdoutOutputGenerator stdoutOutputGenerator = new StdoutOutputGenerator(); + String output = stdoutOutputGenerator.generate(oldArchive, newArchive, jApiClasses, options); + System.out.println(output); + } + + private static Options parseCliOptions(String[] args) { + Options options = new Options(); + try { + CliParser cliParser = new CliParser(); + options = cliParser.parse(args); + } catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); + System.exit(-1); + } catch (Exception e) { + System.err.println("Failed to parse command line options: " + e.getMessage()); + System.exit(-1); + } + return options; + } + + private static void verifyFilesExist(File oldArchive, File newArchive) { + if (!oldArchive.exists()) { + System.err.println(String.format("File '%s' does not exist.", oldArchive.getAbsolutePath())); + System.exit(-1); + } + if (!newArchive.exists()) { + System.err.println(String.format("File '%s' does not exist.", newArchive.getAbsolutePath())); + System.exit(-1); + } + } +} diff --git a/japicmp/src/main/java/japicmp/cli/CliParser.java b/japicmp/src/main/java/japicmp/cli/CliParser.java new file mode 100644 index 0000000..1e1f788 --- /dev/null +++ b/japicmp/src/main/java/japicmp/cli/CliParser.java @@ -0,0 +1,63 @@ +package japicmp.cli; + +import com.google.common.base.Optional; +import japicmp.config.Options; +import japicmp.util.StringArrayEnumeration; + +public class CliParser { + + public Options parse(String[] args) throws IllegalArgumentException { + Options options = new Options(); + StringArrayEnumeration sae = new StringArrayEnumeration(args); + while (sae.hasMoreElements()) { + String arg = sae.nextElement(); + if ("-n".equals(arg)) { + String newArchive = getOptionWithArgument("-n", sae); + options.setNewArchive(newArchive); + } + if ("-o".equals(arg)) { + String oldArchive = getOptionWithArgument("-o", sae); + options.setOldArchive(oldArchive); + } + if ("-x".equals(arg)) { + String xmlOutputFile = getOptionWithArgument("-x", sae); + options.setXmlOutputFile(Optional.of(xmlOutputFile)); + } + if ("-m".equals(arg)) { + options.setOutputOnlyModifications(true); + } + if ("-h".equals(arg)) { + System.out.println("Available parameters:"); + System.out.println("-h Prints this help."); + System.out.println("-o <pathToOldVersionJar> Provides the path to the old version of the jar."); + System.out.println("-n <pathToNewVersionJar> Provides the path to the new version of the jar."); + System.out.println("-x <pathToXmlOutputFile> Provides the path to the xml output file. If not given, stdout is used."); + System.out.println("-m Outputs only modified classes/methods. If not given, all classes and methods are printed."); + System.exit(0); + } + } + checkForMandatoryOptions(options); + return options; + } + + private void checkForMandatoryOptions(Options options) { + if (options.getOldArchive() == null || options.getOldArchive().length() == 0) { + throw new IllegalArgumentException("Missing option for old version: -o <pathToOldVersionJar>"); + } + if (options.getNewArchive() == null || options.getNewArchive().length() == 0) { + throw new IllegalArgumentException("Missing option for new version: -n <pathToNewVersionJar>"); + } + } + + private String getOptionWithArgument(String option, StringArrayEnumeration sae) { + if (sae.hasMoreElements()) { + String value = sae.nextElement(); + if(value.startsWith("-")) { + throw new IllegalArgumentException(String.format("Missing argument for option %s.", option)); + } + return value; + } else { + throw new IllegalArgumentException(String.format("Missing argument for option %s.", option)); + } + } +} diff --git a/japicmp/src/main/java/japicmp/cmp/ClassComparator.java b/japicmp/src/main/java/japicmp/cmp/ClassComparator.java new file mode 100644 index 0000000..92838e7 --- /dev/null +++ b/japicmp/src/main/java/japicmp/cmp/ClassComparator.java @@ -0,0 +1,72 @@ +package japicmp.cmp; + +import com.google.common.base.Optional; +import japicmp.model.JApiChangeStatus; +import japicmp.model.JApiClass; +import japicmp.model.JApiMethod; +import japicmp.model.JApiParameter; +import japicmp.util.SignatureParser; +import javassist.CtClass; +import javassist.CtMethod; + +import java.util.HashMap; +import java.util.Map; + +public class ClassComparator { + + public void compare(JApiClass jApiClass) { + Map<String, CtMethod> oldMethodsMap = createMethodMap(jApiClass.getOldClass()); + Map<String, CtMethod> newMethodsMap = createMethodMap(jApiClass.getNewClass()); + sortMethodsIntoLists(oldMethodsMap, newMethodsMap, jApiClass); + } + + private void sortMethodsIntoLists(Map<String, CtMethod> oldMethodsMap, Map<String, CtMethod> newMethodsMap, JApiClass jApiClass) { + SignatureParser signatureParser = new SignatureParser(); + for (CtMethod ctMethod : oldMethodsMap.values()) { + String longName = ctMethod.getLongName(); + signatureParser.parse(ctMethod.getSignature()); + CtMethod foundMethod = newMethodsMap.get(longName); + if (foundMethod == null) { + JApiMethod jApiMethod = new JApiMethod(ctMethod.getName(), JApiChangeStatus.REMOVED, Optional.of(ctMethod), Optional.<CtMethod>absent(), signatureParser.getReturnType()); + addParametersToMethod(signatureParser, jApiMethod); + jApiClass.addMethod(jApiMethod); + if(jApiClass.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + jApiClass.setChangeStatus(JApiChangeStatus.MODIFIED); + } + } else { + JApiMethod jApiMethod = new JApiMethod(ctMethod.getName(), JApiChangeStatus.UNCHANGED, Optional.of(ctMethod), Optional.of(foundMethod), signatureParser.getReturnType()); + addParametersToMethod(signatureParser, jApiMethod); + jApiClass.addMethod(jApiMethod); + } + } + for (CtMethod ctMethod : newMethodsMap.values()) { + String longName = ctMethod.getLongName(); + signatureParser.parse(ctMethod.getSignature()); + CtMethod foundMethod = oldMethodsMap.get(longName); + if (foundMethod == null) { + JApiMethod jApiMethod = new JApiMethod(ctMethod.getName(), JApiChangeStatus.NEW, Optional.<CtMethod>absent(), Optional.of(ctMethod), signatureParser.getReturnType()); + addParametersToMethod(signatureParser, jApiMethod); + jApiClass.addMethod(jApiMethod); + if(jApiClass.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + jApiClass.setChangeStatus(JApiChangeStatus.MODIFIED); + } + } + } + } + + private void addParametersToMethod(SignatureParser signatureParser, JApiMethod jApiMethod) { + for (String param : signatureParser.getParameters()) { + jApiMethod.addParameter(new JApiParameter(param)); + } + } + + private Map<String, CtMethod> createMethodMap(Optional<CtClass> ctClass) { + Map<String, CtMethod> methods = new HashMap<String, CtMethod>(); + if (ctClass.isPresent()) { + for (CtMethod ctMethod : ctClass.get().getMethods()) { + methods.put(ctMethod.getLongName(), ctMethod); + } + } + return methods; + } +} diff --git a/japicmp/src/main/java/japicmp/cmp/ClassesComparator.java b/japicmp/src/main/java/japicmp/cmp/ClassesComparator.java new file mode 100644 index 0000000..ee02733 --- /dev/null +++ b/japicmp/src/main/java/japicmp/cmp/ClassesComparator.java @@ -0,0 +1,64 @@ +package japicmp.cmp; + +import com.google.common.base.Optional; +import japicmp.model.JApiChangeStatus; +import japicmp.model.JApiClass; +import javassist.CtClass; +import javassist.Modifier; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class ClassesComparator { + private List<JApiClass> classes = new LinkedList<JApiClass>(); + + public void compare(List<CtClass> oldClassesArg, List<CtClass> newClassesArg) { + classes = new LinkedList<JApiClass>(); + Map<String, CtClass> oldClassesMap = createClassMap(oldClassesArg); + Map<String, CtClass> newClassesMap = createClassMap(newClassesArg); + sortIntoLists(oldClassesMap, newClassesMap); + } + + private void sortIntoLists(Map<String, CtClass> oldClassesMap, Map<String, CtClass> newClassesMap) { + for(CtClass ctClass : oldClassesMap.values()) { + CtClass foundClass = newClassesMap.get(ctClass.getName()); + if(foundClass == null) { + classes.add(new JApiClass(ctClass.getName(), Optional.<CtClass>of(ctClass), Optional.<CtClass>absent(), JApiChangeStatus.REMOVED, getType(ctClass))); + } else { + classes.add(new JApiClass(ctClass.getName(), Optional.<CtClass>of(ctClass), Optional.<CtClass>of(foundClass), JApiChangeStatus.UNCHANGED, getType(ctClass))); + } + } + for(CtClass ctClass : newClassesMap.values()) { + CtClass foundClass = oldClassesMap.get(ctClass.getName()); + if(foundClass == null) { + classes.add(new JApiClass(ctClass.getName(), Optional.<CtClass>absent(), Optional.<CtClass>of(ctClass), JApiChangeStatus.NEW, getType(ctClass))); + } + } + } + + private JApiClass.Type getType(CtClass ctClass) { + if(ctClass.isAnnotation()) { + return JApiClass.Type.ANNOTATION; + } else if(ctClass.isEnum()) { + return JApiClass.Type.ENUM; + } else if(ctClass.isInterface()) { + return JApiClass.Type.INTERFACE; + } else { + return JApiClass.Type.CLASS; + } + } + + private Map<String, CtClass> createClassMap(List<CtClass> oldClassesArg) { + Map<String, CtClass> oldClassesMap = new HashMap<String, CtClass>(); + for(CtClass ctClass : oldClassesArg) { + oldClassesMap.put(ctClass.getName(), ctClass); + } + return oldClassesMap; + } + + public List<JApiClass> getClasses() { + return classes; + } +} diff --git a/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java b/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java new file mode 100644 index 0000000..0f150c2 --- /dev/null +++ b/japicmp/src/main/java/japicmp/cmp/JarArchiveComparator.java @@ -0,0 +1,83 @@ +package japicmp.cmp; + +import japicmp.model.JApiClass; +import javassist.ClassPool; +import javassist.CtClass; +import org.apache.log4j.Logger; + +import java.io.File; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarArchiveComparator { + private static final Logger logger = Logger.getLogger(JarArchiveComparator.class); + + public List<JApiClass> compare(File oldArchive, File newArchive) { + ClassPool classPool = new ClassPool(); + try { + ClassesComparator classesComparator = compareClassLists(oldArchive, newArchive, classPool); + List<JApiClass> classList = classesComparator.getClasses(); + compareClasses(classList); + return classList; + } catch (Exception e) { + System.err.println(String.format("Processing jar files '%s' and '%s' failed: %s.", oldArchive.getAbsolutePath(), newArchive.getAbsolutePath(), e.getMessage())); + return new LinkedList<JApiClass>(); + } + } + + private void compareClasses(List<JApiClass> classList) { + for (JApiClass jApiClass : classList) { + ClassComparator classComparator = new ClassComparator(); + classComparator.compare(jApiClass); + } + } + + private ClassesComparator compareClassLists(File oldArchive, File newArchive, ClassPool classPool) throws Exception { + List<CtClass> oldClasses = createListOfCtClasses(oldArchive, classPool); + List<CtClass> newClasses = createListOfCtClasses(newArchive, classPool); + ClassesComparator classesComparator = new ClassesComparator(); + classesComparator.compare(oldClasses, newClasses); + if (logger.isDebugEnabled()) { + for (JApiClass jApiClass : classesComparator.getClasses()) { + logger.debug(jApiClass); + } + } + return classesComparator; + } + + private List<CtClass> createListOfCtClasses(File archive, ClassPool classPool) throws Exception { + List<CtClass> classes = new LinkedList<CtClass>(); + JarFile oldJar = null; + try { + oldJar = new JarFile(archive); + Enumeration<JarEntry> entryEnumeration = oldJar.entries(); + while (entryEnumeration.hasMoreElements()) { + JarEntry jarEntry = entryEnumeration.nextElement(); + String name = jarEntry.getName(); + if (name.endsWith(".class")) { + CtClass ctClass = null; + try { + ctClass = classPool.makeClass(oldJar.getInputStream(jarEntry)); + } catch (Exception e) { + logger.error(String.format("Failed to load file from jar '%s' as class file: %s.", name, e.getMessage())); + throw e; + } + classes.add(ctClass); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Adding class '%s' with jar name '%s' to list.", ctClass.getName(), name)); + } + } else { + logger.debug(String.format("Skipping file '%s' because filename does not end with '.class'.", name)); + } + } + } finally { + if (oldJar != null) { + oldJar.close(); + } + } + return classes; + } +} diff --git a/japicmp/src/main/java/japicmp/config/Options.java b/japicmp/src/main/java/japicmp/config/Options.java new file mode 100644 index 0000000..4576f72 --- /dev/null +++ b/japicmp/src/main/java/japicmp/config/Options.java @@ -0,0 +1,42 @@ +package japicmp.config; + +import com.google.common.base.Optional; + +public class Options { + private String oldArchive; + private String newArchive; + private boolean outputOnlyModifications = false; + private Optional<String> xmlOutputFile = Optional.<String>absent(); + + public String getNewArchive() { + return newArchive; + } + + public void setNewArchive(String newArchive) { + this.newArchive = newArchive; + } + + public String getOldArchive() { + return oldArchive; + } + + public void setOldArchive(String oldArchive) { + this.oldArchive = oldArchive; + } + + public boolean isOutputOnlyModifications() { + return outputOnlyModifications; + } + + public void setOutputOnlyModifications(boolean outputOnlyModifications) { + this.outputOnlyModifications = outputOnlyModifications; + } + + public Optional<String> getXmlOutputFile() { + return xmlOutputFile; + } + + public void setXmlOutputFile(Optional<String> xmlOutputFile) { + this.xmlOutputFile = xmlOutputFile; + } +} diff --git a/japicmp/src/main/java/japicmp/model/JApiChangeStatus.java b/japicmp/src/main/java/japicmp/model/JApiChangeStatus.java new file mode 100644 index 0000000..8222e7c --- /dev/null +++ b/japicmp/src/main/java/japicmp/model/JApiChangeStatus.java @@ -0,0 +1,5 @@ +package japicmp.model; + +public enum JApiChangeStatus { + NEW, REMOVED, UNCHANGED, MODIFIED; +} diff --git a/japicmp/src/main/java/japicmp/model/JApiClass.java b/japicmp/src/main/java/japicmp/model/JApiClass.java new file mode 100644 index 0000000..e1d429f --- /dev/null +++ b/japicmp/src/main/java/japicmp/model/JApiClass.java @@ -0,0 +1,83 @@ +package japicmp.model; + +import com.google.common.base.Optional; +import javassist.CtClass; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlTransient; +import java.util.LinkedList; +import java.util.List; + +public class JApiClass { + private final String fullyQualifiedName; + private final Optional<CtClass> oldClass; + private final Optional<CtClass> newClass; + private List<JApiMethod> methods = new LinkedList<JApiMethod>(); + private JApiChangeStatus changeStatus; + private final Type type; + + public enum Type { + ANNOTATION, INTERFACE, CLASS, ENUM + } + + public JApiClass(String fullyQualifiedName, Optional<CtClass> oldClass, Optional<CtClass> newClass, JApiChangeStatus changeStatus, Type type) { + this.changeStatus = changeStatus; + this.fullyQualifiedName = fullyQualifiedName; + this.newClass = newClass; + this.oldClass = oldClass; + this.type = type; + } + + public void addMethod(JApiMethod jApiMethod) { + methods.add(jApiMethod); + } + + @XmlAttribute + public JApiChangeStatus getChangeStatus() { + return changeStatus; + } + + @XmlAttribute + public String getFullyQualifiedName() { + return fullyQualifiedName; + } + + @XmlTransient + public Optional<CtClass> getNewClass() { + return newClass; + } + + @XmlTransient + public Optional<CtClass> getOldClass() { + return oldClass; + } + + public void setChangeStatus(JApiChangeStatus changeStatus) { + this.changeStatus = changeStatus; + } + + @XmlElement(name = "method") + public List<JApiMethod> getMethods() { + return methods; + } + + public void setMethods(List<JApiMethod> methods) { + this.methods = methods; + } + + @XmlAttribute + public Type getType() { + return type; + } + + @Override + public String toString() { + return "JApiClass{" + + "changeStatus=" + changeStatus + + ", fullyQualifiedName='" + fullyQualifiedName + '\'' + + ", oldClass=" + oldClass + + ", newClass=" + newClass + + '}'; + } +} diff --git a/japicmp/src/main/java/japicmp/model/JApiMethod.java b/japicmp/src/main/java/japicmp/model/JApiMethod.java new file mode 100644 index 0000000..ab04e1f --- /dev/null +++ b/japicmp/src/main/java/japicmp/model/JApiMethod.java @@ -0,0 +1,61 @@ +package japicmp.model; + +import com.google.common.base.Optional; +import javassist.CtMethod; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlTransient; +import java.util.LinkedList; +import java.util.List; + +public class JApiMethod { + private final String name; + private final JApiChangeStatus changeStatus; + private final Optional<CtMethod> oldMethod; + private final Optional<CtMethod> newMethod; + private final String returnType; + private final List<JApiParameter> parameters = new LinkedList<JApiParameter>(); + + public JApiMethod(String name, JApiChangeStatus changeStatus, Optional<CtMethod> oldClass, Optional<CtMethod> newClass, String returnType) { + this.name = name; + this.changeStatus = changeStatus; + this.oldMethod = oldClass; + this.newMethod = newClass; + this.returnType = returnType; + } + + @XmlAttribute + public JApiChangeStatus getChangeStatus() { + return changeStatus; + } + + @XmlAttribute + public String getName() { + return name; + } + + @XmlTransient + public Optional<CtMethod> getNewMethod() { + return newMethod; + } + + @XmlTransient + public Optional<CtMethod> getOldMethod() { + return oldMethod; + } + + @XmlAttribute + public String getReturnType() { + return returnType; + } + + @XmlElement(name = "parameter") + public List<JApiParameter> getParameters() { + return parameters; + } + + public void addParameter(JApiParameter jApiParameter) { + parameters.add(jApiParameter); + } +} diff --git a/japicmp/src/main/java/japicmp/model/JApiParameter.java b/japicmp/src/main/java/japicmp/model/JApiParameter.java new file mode 100644 index 0000000..56f2a81 --- /dev/null +++ b/japicmp/src/main/java/japicmp/model/JApiParameter.java @@ -0,0 +1,16 @@ +package japicmp.model; + +import javax.xml.bind.annotation.XmlAttribute; + +public class JApiParameter { + private final String type; + + public JApiParameter(String type) { + this.type = type; + } + + @XmlAttribute + public String getType() { + return type; + } +} diff --git a/japicmp/src/main/java/japicmp/output/OutputTransformer.java b/japicmp/src/main/java/japicmp/output/OutputTransformer.java new file mode 100644 index 0000000..5b31b27 --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/OutputTransformer.java @@ -0,0 +1,55 @@ +package japicmp.output; + +import japicmp.model.JApiChangeStatus; +import japicmp.model.JApiClass; +import japicmp.model.JApiMethod; + +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class OutputTransformer { + + private OutputTransformer() { + + } + + public static void removeUnchanged(List<JApiClass> jApiClasses) { + List<JApiClass> classesToRemove = new LinkedList<JApiClass>(); + for (JApiClass jApiClass : jApiClasses) { + if (jApiClass.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + classesToRemove.add(jApiClass); + } else { + List<JApiMethod> methodsToRemove = new LinkedList<JApiMethod>(); + List<JApiMethod> methods = jApiClass.getMethods(); + for (JApiMethod jApiMethod : methods) { + if (jApiMethod.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + methodsToRemove.add(jApiMethod); + } + } + for (JApiMethod jApiMethod : methodsToRemove) { + methods.remove(jApiMethod); + } + } + } + for (JApiClass jApiClass : classesToRemove) { + jApiClasses.remove(jApiClass); + } + } + + public static void sortClassesAndMethods(List<JApiClass> jApiClasses) { + Collections.sort(jApiClasses, new Comparator<JApiClass>() { + public int compare(JApiClass o1, JApiClass o2) { + return o1.getFullyQualifiedName().compareToIgnoreCase(o2.getFullyQualifiedName()); + } + }); + for(JApiClass jApiClass : jApiClasses) { + Collections.sort(jApiClass.getMethods(), new Comparator<JApiMethod>() { + public int compare(JApiMethod o1, JApiMethod o2) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + }); + } + } +} diff --git a/japicmp/src/main/java/japicmp/output/stdout/StdoutOutputGenerator.java b/japicmp/src/main/java/japicmp/output/stdout/StdoutOutputGenerator.java new file mode 100644 index 0000000..cead423 --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/stdout/StdoutOutputGenerator.java @@ -0,0 +1,71 @@ +package japicmp.output.stdout; + +import japicmp.config.Options; +import japicmp.model.JApiChangeStatus; +import japicmp.model.JApiClass; +import japicmp.model.JApiMethod; +import japicmp.model.JApiParameter; +import japicmp.output.OutputTransformer; + +import java.io.File; +import java.util.List; + +public class StdoutOutputGenerator { + + public String generate(File oldArchive, File newArchive, List<JApiClass> jApiClasses, Options options) { + if (options.isOutputOnlyModifications()) { + OutputTransformer.removeUnchanged(jApiClasses); + } + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Comparing %s with %s:\n", oldArchive.getAbsolutePath(), newArchive.getAbsolutePath())); + for (JApiClass jApiClass : jApiClasses) { + processClass(sb, jApiClass); + processMethods(sb, jApiClass); + } + return sb.toString(); + } + + private void processMethods(StringBuilder sb, JApiClass jApiClass) { + List<JApiMethod> methods = jApiClass.getMethods(); + for (JApiMethod jApiMethod : methods) { + if (jApiMethod.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + appendMethod(sb, "===", jApiMethod); + } else if (jApiMethod.getChangeStatus() == JApiChangeStatus.NEW) { + appendMethod(sb, "+++", jApiMethod); + } else if (jApiMethod.getChangeStatus() == JApiChangeStatus.REMOVED) { + appendMethod(sb, "---", jApiMethod); + } else if (jApiMethod.getChangeStatus() == JApiChangeStatus.MODIFIED) { + appendMethod(sb, "***", jApiMethod); + } + } + } + + private void processClass(StringBuilder sb, JApiClass jApiClass) { + if (jApiClass.getChangeStatus() == JApiChangeStatus.UNCHANGED) { + appendClass(sb, "===", jApiClass); + } else if (jApiClass.getChangeStatus() == JApiChangeStatus.NEW) { + appendClass(sb, "+++", jApiClass); + } else if (jApiClass.getChangeStatus() == JApiChangeStatus.REMOVED) { + appendClass(sb, "---", jApiClass); + } else if (jApiClass.getChangeStatus() == JApiChangeStatus.MODIFIED) { + appendClass(sb, "***", jApiClass); + } + } + + private void appendMethod(StringBuilder sb, String signs, JApiMethod jApiMethod) { + sb.append("\t" + signs + " " + jApiMethod.getChangeStatus() + " METHOD " + jApiMethod.getName() + "("); + int paramCount = 0; + for (JApiParameter jApiParameter : jApiMethod.getParameters()) { + if (paramCount > 0) { + sb.append(","); + } + sb.append(jApiParameter.getType()); + paramCount++; + } + sb.append(")\n"); + } + + private void appendClass(StringBuilder sb, String signs, JApiClass jApiClass) { + sb.append(signs + " " + jApiClass.getChangeStatus() + " " + jApiClass.getType() + " " + jApiClass.getFullyQualifiedName() + "\n"); + } +} diff --git a/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java b/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java new file mode 100644 index 0000000..939c6cb --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/xml/XmlOutputGenerator.java @@ -0,0 +1,24 @@ +package japicmp.output.xml; + +import japicmp.config.Options; +import japicmp.model.JApiClass; +import japicmp.output.OutputTransformer; +import japicmp.output.xml.model.JApiCmpXmlRoot; + +import javax.xml.bind.JAXB; +import java.io.File; +import java.util.List; + +public class XmlOutputGenerator { + + public void generate(File oldArchive, File newArchive, List<JApiClass> jApiClasses, Options options) { + JApiCmpXmlRoot jApiCmpXmlRoot = new JApiCmpXmlRoot(); + jApiCmpXmlRoot.setOldJar(oldArchive.getAbsolutePath()); + jApiCmpXmlRoot.setNewJar(newArchive.getAbsolutePath()); + jApiCmpXmlRoot.setClasses(jApiClasses); + if (options.isOutputOnlyModifications()) { + OutputTransformer.removeUnchanged(jApiClasses); + } + JAXB.marshal(jApiCmpXmlRoot, new File(options.getXmlOutputFile().get())); + } +} diff --git a/japicmp/src/main/java/japicmp/output/xml/model/JApiCmpXmlRoot.java b/japicmp/src/main/java/japicmp/output/xml/model/JApiCmpXmlRoot.java new file mode 100644 index 0000000..77beafe --- /dev/null +++ b/japicmp/src/main/java/japicmp/output/xml/model/JApiCmpXmlRoot.java @@ -0,0 +1,43 @@ +package japicmp.output.xml.model; + +import japicmp.model.JApiClass; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.LinkedList; +import java.util.List; + +@XmlRootElement(name = "japicmp") +public class JApiCmpXmlRoot { + private String oldJar = ""; + private String newJar = ""; + private List<JApiClass> classes = new LinkedList<JApiClass>(); + + @XmlElement(name = "class") + public List<JApiClass> getClasses() { + return classes; + } + + public void setClasses(List<JApiClass> classes) { + this.classes = classes; + } + + @XmlAttribute + public String getNewJar() { + return newJar; + } + + public void setNewJar(String newJar) { + this.newJar = newJar; + } + + @XmlAttribute + public String getOldJar() { + return oldJar; + } + + public void setOldJar(String oldJar) { + this.oldJar = oldJar; + } +} diff --git a/japicmp/src/main/java/japicmp/util/SignatureParser.java b/japicmp/src/main/java/japicmp/util/SignatureParser.java new file mode 100644 index 0000000..9d4d96d --- /dev/null +++ b/japicmp/src/main/java/japicmp/util/SignatureParser.java @@ -0,0 +1,103 @@ +package japicmp.util; + +import java.util.LinkedList; +import java.util.List; + +public class SignatureParser { + private List<String> parameters = new LinkedList<String>(); + private String returnType = "void"; + + public void parse(String signature) { + int parenthesisCloseIndex = signature.indexOf(')'); + if(parenthesisCloseIndex > -1) { + parseParameters(signature, parenthesisCloseIndex); + parseReturnValue(signature, parenthesisCloseIndex); + } + } + + private void parseReturnValue(String signature, int parenthesisCloseIndex) { + String retValPart = signature.substring(parenthesisCloseIndex+1); + List<String> retValTypes = parseTypes(retValPart); + returnType = retValTypes.get(0); + } + + private void parseParameters(String signature, int parenthesisCloseIndex) { + String paramPart = signature.substring(1, parenthesisCloseIndex); + List<String> paramTypes = parseTypes(paramPart); + parameters.clear(); + parameters.addAll(paramTypes); + } + + private List<String> parseTypes(String paramPart) { + List<String> types = new LinkedList<String>(); + boolean arrayNotation = false; + for(int i=0; i<paramPart.length(); i++) { + char c = paramPart.charAt(i); + String type = "void"; + switch(c) { + case 'Z': + type = "boolean"; + break; + case 'B': + type = "byte"; + break; + case 'C': + type = "char"; + break; + case 'S': + type = "short"; + break; + case 'I': + type = "int"; + break; + case 'J': + type = "long"; + break; + case 'F': + type = "float"; + break; + case 'D': + type = "double"; + break; + case 'V': + type = "void"; + break; + case '[': + arrayNotation = true; + continue; + case 'L': + StringBuilder fqn = new StringBuilder(); + i++; + while(i<paramPart.length()) { + c = paramPart.charAt(i); + if(c == ';') { + break; + } else if(c == '/') { + fqn.append('.'); + } else { + fqn.append(c); + } + i++; + } + type = fqn.toString(); + break; + default: + throw new IllegalStateException("Unknown type signature: '"+c+"'"); + } + if(arrayNotation) { + type += "[]"; + arrayNotation = false; + } + types.add(type); + } + return types; + } + + public List<String> getParameters() { + return parameters; + } + + public String getReturnType() { + return returnType; + } +} diff --git a/japicmp/src/main/java/japicmp/util/StringArrayEnumeration.java b/japicmp/src/main/java/japicmp/util/StringArrayEnumeration.java new file mode 100644 index 0000000..82c7eb6 --- /dev/null +++ b/japicmp/src/main/java/japicmp/util/StringArrayEnumeration.java @@ -0,0 +1,27 @@ +package japicmp.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class StringArrayEnumeration implements Enumeration<String> { + private final String[] array; + private int pos = 0; + + public StringArrayEnumeration(String[] array) { + this.array = array; + } + + @Override + public boolean hasMoreElements() { + return pos < array.length; + } + + @Override + public String nextElement() { + if(hasMoreElements()) { + return array[pos++]; + } else { + throw new NoSuchElementException(); + } + } +} diff --git a/japicmp/src/main/resources/log4j.properties b/japicmp/src/main/resources/log4j.properties new file mode 100644 index 0000000..71255bb --- /dev/null +++ b/japicmp/src/main/resources/log4j.properties @@ -0,0 +1,8 @@ +# Root logger option +log4j.rootLogger=DEBUG, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/japicmp/src/test/java/japicmp/cli/CliParserTest.java b/japicmp/src/test/java/japicmp/cli/CliParserTest.java new file mode 100644 index 0000000..8dafe7f --- /dev/null +++ b/japicmp/src/test/java/japicmp/cli/CliParserTest.java @@ -0,0 +1,40 @@ +package japicmp.cli; + +import japicmp.config.Options; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class CliParserTest { + private CliParser subject; + + @Before + public void before() { + subject = new CliParser(); + } + + @Test + public void testAllOptions() { + Options options = subject.parse(new String[]{"-n", "npath", "-o", "opath", "-m", "-x", "xpath"}); + assertThat(options.getXmlOutputFile().get(), is("xpath")); + assertThat(options.getNewArchive(), is("npath")); + assertThat(options.getOldArchive(), is("opath")); + assertThat(options.isOutputOnlyModifications(), is(true)); + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingArgumentForN() { + subject.parse(new String[]{"-n", "-o", "opath"}); + } + + @Test + public void testOnlyNAndO() { + Options options = subject.parse(new String[]{"-n", "npath", "-o", "opath",}); + assertThat(options.getXmlOutputFile().isPresent(), is(false)); + assertThat(options.getNewArchive(), is("npath")); + assertThat(options.getOldArchive(), is("opath")); + assertThat(options.isOutputOnlyModifications(), is(false)); + } +} diff --git a/japicmp/src/test/java/japicmp/util/SignatureParserTest.java b/japicmp/src/test/java/japicmp/util/SignatureParserTest.java new file mode 100644 index 0000000..183fd8a --- /dev/null +++ b/japicmp/src/test/java/japicmp/util/SignatureParserTest.java @@ -0,0 +1,99 @@ +package japicmp.util; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class SignatureParserTest { + private SignatureParser subject; + + @Before + public void before() throws Exception { + subject = new SignatureParser(); + } + + @Test + public void testNoParamsReturnsVoid() { + subject.parse("()V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(0)); + } + + @Test + public void testTwoReferenceParamsReturnsReference() { + subject.parse("(Lorg/apache/http/conn/routing/HttpRoute;Ljava/lang/Object;)Lorg/apache/http/conn/ManagedClientConnection;"); + assertThat(subject.getReturnType(), is("org.apache.http.conn.ManagedClientConnection")); + assertThat(subject.getParameters().size(), is(2)); + assertThat(subject.getParameters(), hasItem("org.apache.http.conn.routing.HttpRoute")); + assertThat(subject.getParameters(), hasItem("java.lang.Object")); + } + + @Test + public void testOneReferenceOnePrimParamsReturnsVoid() { + subject.parse("(JLjava/util/concurrent/TimeUnit;)V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(2)); + assertThat(subject.getParameters(), hasItem("long")); + assertThat(subject.getParameters(), hasItem("java.util.concurrent.TimeUnit")); + } + + @Test + public void testArrayTwoPrimParamsReturnsVoid() { + subject.parse("([BII)V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(3)); + assertThat(subject.getParameters(), hasItem("byte[]")); + assertThat(subject.getParameters(), hasItem("int")); + } + + @Test + public void testArrayPrimParamReturnsVoid() { + subject.parse("([B)V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(1)); + assertThat(subject.getParameters(), hasItem("byte[]")); + } + + @Test + public void testArrayRefParamReturnsVoid() { + subject.parse("([Lorg/apache/http/cookie/Cookie;)V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(1)); + assertThat(subject.getParameters(), hasItem("org.apache.http.cookie.Cookie[]")); + } + + @Test + public void testOneReferenceParamsReturnsVoid() { + subject.parse("(Lorg/apache/http/impl/conn/tsccm/BasicPoolEntry;)V"); + assertThat(subject.getReturnType(), is("void")); + assertThat(subject.getParameters().size(), is(1)); + assertThat(subject.getParameters(), hasItem("org.apache.http.impl.conn.tsccm.BasicPoolEntry")); + } + + @Test + public void testOneReferenceParamsReturnsOneReference() { + subject.parse("(Ljava/util/List;)Ljava/util/List;"); + assertThat(subject.getReturnType(), is("java.util.List")); + assertThat(subject.getParameters().size(), is(1)); + assertThat(subject.getParameters(), hasItem("java.util.List")); + } + + @Test + public void testNoParamsReturnsReference() { + subject.parse("()Lorg/apache/http/conn/scheme/SchemeRegistry;"); + assertThat(subject.getReturnType(), is("org.apache.http.conn.scheme.SchemeRegistry")); + assertThat(subject.getParameters().size(), is(0)); + } + + @Test + public void testNoParamsReturnsI() { + subject.parse("()I"); + assertThat(subject.getReturnType(), is("int")); + assertThat(subject.getParameters().size(), is(0)); + } +} diff --git a/japicmp/src/test/java/japicmp/util/StringArrayEnumerationTest.java b/japicmp/src/test/java/japicmp/util/StringArrayEnumerationTest.java new file mode 100644 index 0000000..853d71e --- /dev/null +++ b/japicmp/src/test/java/japicmp/util/StringArrayEnumerationTest.java @@ -0,0 +1,37 @@ +package japicmp.util; + +import org.junit.Before; +import org.junit.Test; + +import java.util.NoSuchElementException; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class StringArrayEnumerationTest { + + @Test(expected = NoSuchElementException.class) + public void testEmptyArray() { + StringArrayEnumeration sae = new StringArrayEnumeration(new String[]{}); + assertThat(sae.hasMoreElements(), is(false)); + sae.nextElement(); + } + + @Test + public void testOneElementArray() { + StringArrayEnumeration sae = new StringArrayEnumeration(new String[]{"1"}); + assertThat(sae.hasMoreElements(), is(true)); + assertThat(sae.nextElement(), is("1")); + assertThat(sae.hasMoreElements(), is(false)); + } + + @Test + public void testTwoElementsArray() { + StringArrayEnumeration sae = new StringArrayEnumeration(new String[]{"1","2"}); + assertThat(sae.hasMoreElements(), is(true)); + assertThat(sae.nextElement(), is("1")); + assertThat(sae.hasMoreElements(), is(true)); + assertThat(sae.nextElement(), is("2")); + assertThat(sae.hasMoreElements(), is(false)); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a25f751 --- /dev/null +++ b/pom.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>japicmp</groupId> + <artifactId>japicmp-base</artifactId> + <version>0.0.1-SNAPSHOT</version> + <packaging>pom</packaging> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <modules> + <module>japicmp</module> + </modules> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.1</version> + <configuration> + <source>1.5</source> + <target>1.5</target> + </configuration> + </plugin> + </plugins> + </build> + +</project> \ No newline at end of file