diff options
author | Julien Eluard <[email protected]> | 2010-11-14 22:11:49 +0100 |
---|---|---|
committer | Julien Eluard <[email protected]> | 2010-11-14 22:11:49 +0100 |
commit | a06cd8f04ce4fed80d9fa1197c222bafb4624e41 (patch) | |
tree | 851bb643bd5d7639d005a0bf96633b7175e8c43f /api |
first commit
Diffstat (limited to 'api')
-rw-r--r-- | api/pom.xml | 62 | ||||
-rw-r--r-- | api/src/main/java/org/semver/Checker.java | 332 | ||||
-rw-r--r-- | api/src/main/java/org/semver/Version.java | 210 | ||||
-rw-r--r-- | api/src/main/java/org/semver/jardiff/AccumulatingDiffHandler.java | 286 | ||||
-rw-r--r-- | api/src/site/apt/index.apt | 43 | ||||
-rw-r--r-- | api/src/site/apt/usage.apt.vm | 63 | ||||
-rw-r--r-- | api/src/site/site.xml | 32 | ||||
-rw-r--r-- | api/src/test/java/org/semver/VersionTest.java | 101 |
8 files changed, 1129 insertions, 0 deletions
diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..5fe88c7 --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,62 @@ +<?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> + + <artifactId>api</artifactId> + <packaging>jar</packaging> + + <name>API</name> + + <parent> + <groupId>org.semantic-versioning</groupId> + <artifactId>parent</artifactId> + <version>0.9-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <dependencies> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <version>1.3.9</version> + </dependency> + <dependency> + <groupId>jardiff</groupId> + <artifactId>jardiff</artifactId> + <version>0.2</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.8.2</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.3.2</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + </configuration> + </plugin> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifest> + <mainClass>org.semver.Checker</mainClass> + </manifest> + </archive> + </configuration> + </plugin> + </plugins> + </build> + +</project> + diff --git a/api/src/main/java/org/semver/Checker.java b/api/src/main/java/org/semver/Checker.java new file mode 100644 index 0000000..d63b980 --- /dev/null +++ b/api/src/main/java/org/semver/Checker.java @@ -0,0 +1,332 @@ +/** + * This software is licensed under the Apache 2 license, quoted below. + * + * Copyright 2010 Julien Eluard + * + * 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. + */ + +package org.semver; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; + +import org.osjava.jardiff.AbstractInfo; +import org.osjava.jardiff.ClassInfo; +import org.osjava.jardiff.DiffException; +import org.osjava.jardiff.JarDiff; +import org.osjava.jardiff.SimpleDiffCriteria; +import org.semver.jardiff.AccumulatingDiffHandler; +import org.semver.jardiff.AccumulatingDiffHandler.Difference; + +/** + * + * Allows to compare content of JARs. Provides convenient methods to validate that chosen {@link Version} are correct. + * + */ +public class Checker { + + /** + * Library compatibility type. From most compatible to less compatible. + */ + public enum CompatibilityType { + + BACKWARD_COMPATIBLE_IMPLEMENTER, + + BACKWARD_COMPATIBLE_USER, + + NON_BACKWARD_COMPATIBLE + } + + /** + * + * Infers next {@link Version} depending on provided {@link CompatibilityType}. + * + * @param version + * @param compatibilityType + * @return + */ + public static Version inferNextVersion(@Nonnull final Version version, @Nonnull final CompatibilityType compatibilityType) { + switch (compatibilityType) { + case BACKWARD_COMPATIBLE_IMPLEMENTER: + return version.next(Version.Type.PATCH); + case BACKWARD_COMPATIBLE_USER: + return version.next(Version.Type.MINOR); + case NON_BACKWARD_COMPATIBLE: + return version.next(Version.Type.MAJOR); + default: + throw new IllegalArgumentException("Unknown type <"+compatibilityType+">"); + } + } + + /** + * @param compatibilityType + * @param type + * @return true if specified {@link CompatibilityType} + */ + public static boolean isTypeCompatible(final CompatibilityType compatibilityType, final Version.Type type) { + switch (compatibilityType) { + case BACKWARD_COMPATIBLE_USER: + return type.isAtLeast(Version.Type.PATCH); + case BACKWARD_COMPATIBLE_IMPLEMENTER: + return type.isAtLeast(Version.Type.MINOR); + case NON_BACKWARD_COMPATIBLE: + return type.isAtLeast(Version.Type.MAJOR); + default: + throw new IllegalArgumentException("Unknown type <"+compatibilityType+">"); + } + } + + /** + * @param previousJAR + * @param currentJAR + * @param includes + * @param excludes + * @return all {@link Difference} between both JARs + * @throws IOException + * @throws FileNotFoundException + */ + public final Set<AccumulatingDiffHandler.Difference> diff(final File previousJAR, final File currentJAR, final Set<String> includes, final Set<String> excludes) throws IOException, FileNotFoundException { + if (!previousJAR.isFile()) { + throw new IllegalArgumentException("<"+previousJAR+"> is not a valid file"); + } + if (!currentJAR.isFile()) { + throw new IllegalArgumentException("<"+currentJAR+"> is not a valid file"); + } + + try { + final JarDiff jarDiff = new JarDiff(); + jarDiff.loadOldClasses(previousJAR); + jarDiff.loadNewClasses(currentJAR); + final AccumulatingDiffHandler handler = new AccumulatingDiffHandler(includes, excludes); + jarDiff.diff(handler, new SimpleDiffCriteria()); + return handler.getDifferences(); + } catch (DiffException e) { + throw new RuntimeException(e); + } + } + + protected final String extractActionType(final Difference difference) { + final String actionType = difference.getClass().getSimpleName(); + return actionType.endsWith("e")?actionType+"d":actionType+"ed"; + } + + protected final String extractInfoType(final AbstractInfo info) { + final String simpleClassName = info.getClass().getSimpleName(); + return simpleClassName.substring(0, simpleClassName.indexOf("Info")); + } + + protected final String extractDetails(final Difference difference) { + if (difference instanceof AccumulatingDiffHandler.Change) { + final AccumulatingDiffHandler.Change change = (AccumulatingDiffHandler.Change) difference; + return extractDetails(difference.getInfo())+" "+extractAccessDetails(difference.getInfo(), change.getModifiedInfo()); + } else { + return extractDetails(difference.getInfo()); + } + } + + protected final String extractDetails(final AbstractInfo info) { + final StringBuilder builder = new StringBuilder(); + if (!(info instanceof ClassInfo)) { + builder.append(info.getName()); + } + return builder.toString(); + } + + protected final void accumulateAccessDetails(final String access, final boolean previousAccess, final boolean currentAccess, final List<String> added, final List<String> removed) { + if (previousAccess != currentAccess) { + if (previousAccess) { + removed.add(access); + } else { + added.add(access); + } + } + } + + protected final String extractAccessDetails(final AbstractInfo previousInfo, final AbstractInfo currentInfo) { + final List<String> added = new LinkedList<String>(); + final List<String> removed = new LinkedList<String>(); + accumulateAccessDetails("abstract", previousInfo.isAbstract(), currentInfo.isAbstract(), added, removed); + accumulateAccessDetails("annotation", previousInfo.isAnnotation(), currentInfo.isAnnotation(), added, removed); + accumulateAccessDetails("bridge", previousInfo.isBridge(), currentInfo.isBridge(), added, removed); + accumulateAccessDetails("deprecated", previousInfo.isDeprecated(), currentInfo.isDeprecated(), added, removed); + accumulateAccessDetails("enum", previousInfo.isEnum(), currentInfo.isEnum(), added, removed); + accumulateAccessDetails("final", previousInfo.isFinal(), currentInfo.isFinal(), added, removed); + accumulateAccessDetails("interface", previousInfo.isInterface(), currentInfo.isInterface(), added, removed); + accumulateAccessDetails("native", previousInfo.isNative(), currentInfo.isNative(), added, removed); + accumulateAccessDetails("package-private", previousInfo.isPackagePrivate(), currentInfo.isPackagePrivate(), added, removed); + accumulateAccessDetails("private", previousInfo.isPrivate(), currentInfo.isPrivate(), added, removed); + accumulateAccessDetails("protected", previousInfo.isProtected(), currentInfo.isProtected(), added, removed); + accumulateAccessDetails("public", previousInfo.isPublic(), currentInfo.isPublic(), added, removed); + accumulateAccessDetails("static", previousInfo.isStatic(), currentInfo.isStatic(), added, removed); + accumulateAccessDetails("strict", previousInfo.isStrict(), currentInfo.isStrict(), added, removed); + accumulateAccessDetails("super", previousInfo.isSuper(), currentInfo.isSuper(), added, removed); + accumulateAccessDetails("synchronized", previousInfo.isSynchronized(), currentInfo.isSynchronized(), added, removed); + accumulateAccessDetails("synthetic", previousInfo.isSynthetic(), currentInfo.isSynthetic(), added, removed); + accumulateAccessDetails("transcient", previousInfo.isTransient(), currentInfo.isTransient(), added, removed); + accumulateAccessDetails("varargs", previousInfo.isVarargs(), currentInfo.isVarargs(), added, removed); + accumulateAccessDetails("volatile", previousInfo.isVolatile(), currentInfo.isVolatile(), added, removed); + final StringBuilder details = new StringBuilder(); + if (!added.isEmpty()) { + details.append("added: "); + for (final String access : added) { + details.append(access).append(" "); + } + } + if (!removed.isEmpty()) { + details.append("removed: "); + for (final String access : removed) { + details.append(access).append(" "); + } + } + return details.toString().trim(); + } + + /** + * + * Dumps on {@link System#out} all differences between both JARs. + * + * @param previousJAR + * @param currentJAR + * @param includes + * @param excludes + * @throws IOException + * @throws FileNotFoundException + */ + public final void dumpDiff(final File previousJAR, final File currentJAR, final Set<String> includes, final Set<String> excludes) throws IOException, FileNotFoundException { + final Set<AccumulatingDiffHandler.Difference> differences = diff(previousJAR, currentJAR, includes, excludes); + final List<AccumulatingDiffHandler.Difference> sortedDifferences = new LinkedList<AccumulatingDiffHandler.Difference>(differences); + Collections.sort(sortedDifferences); + String currentClassName = ""; + for (final AccumulatingDiffHandler.Difference difference : sortedDifferences) { + if (!currentClassName.equals(difference.getClassName())) { + System.out.println("Class "+difference.getClassName()); + } + System.out.println(" "+extractActionType(difference)+" "+extractInfoType(difference.getInfo())+" "+extractDetails(difference)); + currentClassName = difference.getClassName(); + } + } + + protected final boolean contains(final Set<AccumulatingDiffHandler.Difference> differences, final Class<? extends Difference> type) { + for (final Difference difference : differences) { + if (type.isInstance(difference)) { + return false; + } + } + return true; + } + + /** + * @param differences + * @return {@link CompatibilityType} based on specified {@link Difference} + */ + protected final CompatibilityType computeCompatibilityType(final Set<AccumulatingDiffHandler.Difference> differences) { + if (!contains(differences, AccumulatingDiffHandler.Change.class) && + !contains(differences, AccumulatingDiffHandler.Remove.class)) { + return CompatibilityType.NON_BACKWARD_COMPATIBLE; + } else if (!contains(differences, AccumulatingDiffHandler.Add.class)) { + return CompatibilityType.BACKWARD_COMPATIBLE_USER; + } else { + return CompatibilityType.BACKWARD_COMPATIBLE_IMPLEMENTER; + } + } + + public final CompatibilityType check(final File previousJAR, final File currentJAR, final Set<String> includes, final Set<String> excludes) throws IOException, FileNotFoundException { + return computeCompatibilityType(diff(previousJAR, currentJAR, includes, excludes)); + } + + /** + * @param previous + * @param previousJAR + * @param currentJAR + * @param includes + * @param excludes + * @return an inferred {@link Version} for current JAR based on previous JAR content/version. + * @throws IOException + */ + public final Version infer(final Version previous, final File previousJAR, final File currentJAR, final Set<String> includes, final Set<String> excludes) throws IOException { + final CompatibilityType compatibilityType = new Checker().check(previousJAR, currentJAR, includes, excludes); + return Checker.inferNextVersion(previous, compatibilityType); + } + + /** + * @param previous + * @param previousJAR + * @param current + * @param currentJAR + * @param includes + * @param excludes + * @return true if {@link Version} provided for current JAR is compatible with previous JAR content/version. + * @throws IOException + */ + public final boolean validate(final Version previous, final File previousJAR, final Version current, final File currentJAR, final Set<String> includes, final Set<String> excludes) throws IOException { + final CompatibilityType compatibilityType = new Checker().check(previousJAR, currentJAR, includes, excludes); + return isTypeCompatible(compatibilityType, previous.delta(current)); + } + + private static void failIfNotEnoughArguments(final String[] arguments, final int minimalSize, final String message) { + if (arguments.length < minimalSize) { + System.out.println(message); + System.exit(-1); + } + } + + private static final String DIFF_ACTION = "diff"; + private static final String CHECK_ACTION = "check"; + private static final String INFER_ACTION = "infer"; + private static final String VALIDATE_ACTION = "validate"; + + private static Set<String> extractFiltersIfAny(final String[] arguments, final int position) { + try { + final String filters = arguments[position]; + return new HashSet<String>(Arrays.asList(filters.split(";"))); + } catch (IndexOutOfBoundsException e) { + return Collections.emptySet(); + } + } + + public static void main(final String[] arguments) throws Exception { + Checker.failIfNotEnoughArguments(arguments, 3, "Usage: ["+DIFF_ACTION+"|"+CHECK_ACTION+"|"+INFER_ACTION+"|"+VALIDATE_ACTION+"] (previousVersion) previousJar (currentVersion) currentJar (includes) (excludes)"); + + final String action = arguments[0]; + if (DIFF_ACTION.equals(action)) { + Checker.failIfNotEnoughArguments(arguments, 3, "Usage: "+DIFF_ACTION+" previousJar currentJar (includes) (excludes)"); + + new Checker().dumpDiff(new File(arguments[1]), new File(arguments[2]), extractFiltersIfAny(arguments, 3), extractFiltersIfAny(arguments, 4)); + } else if (CHECK_ACTION.equals(action)) { + Checker.failIfNotEnoughArguments(arguments, 3, "Usage: "+CHECK_ACTION+" previousJar currentJar (includes) (excludes)"); + + System.out.println(new Checker().check(new File(arguments[1]), new File(arguments[2]), extractFiltersIfAny(arguments, 3), extractFiltersIfAny(arguments, 4))); + } else if (INFER_ACTION.equals(action)) { + Checker.failIfNotEnoughArguments(arguments, 4, "Usage: "+INFER_ACTION+" previousVersion previousJar currentJar (includes) (excludes)"); + + System.out.println(new Checker().infer(Version.parse(arguments[1]), new File(arguments[2]), new File(arguments[3]), extractFiltersIfAny(arguments, 4), extractFiltersIfAny(arguments, 5))); + } else if (VALIDATE_ACTION.equals(action)) { + Checker.failIfNotEnoughArguments(arguments, 5, "Usage: "+VALIDATE_ACTION+" previousVersion previousJar currentVersion currentJar (includes) (excludes)"); + + System.out.println(new Checker().validate(Version.parse(arguments[1]), new File(arguments[2]), Version.parse(arguments[3]), new File(arguments[4]), extractFiltersIfAny(arguments, 5), extractFiltersIfAny(arguments, 6))); + } else { + System.out.println("First argument must be one of ["+DIFF_ACTION+"|"+CHECK_ACTION+"|"+INFER_ACTION+"|"+VALIDATE_ACTION+"]"); + System.exit(-1); + } + } + +} diff --git a/api/src/main/java/org/semver/Version.java b/api/src/main/java/org/semver/Version.java new file mode 100644 index 0000000..45f4833 --- /dev/null +++ b/api/src/main/java/org/semver/Version.java @@ -0,0 +1,210 @@ +/** + * This software is licensed under the Apache 2 license, quoted below. + * + * Copyright 2010 Julien Eluard + * + * 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. + */ + +package org.semver; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * + * Version following semantic defined by <a href="http://semver.org/">Semantic Versioning</a> document. + * + */ +public class Version implements Comparable<Version> { + + /** + * {@link Version} element type. From most meaningful to less meaningful. + */ + public enum Type { + MAJOR, MINOR, PATCH, SPECIAL; + + public boolean isAtLeast(@Nonnull final Version.Type type) { + return compareTo(type) <= 0; + } + + } + + private final static String FORMAT = "(\\d)\\.(\\d)\\.(\\d)([A-Za-z][0-9A-Za-z-]*)?"; + private final static Pattern PATTERN = Pattern.compile(Version.FORMAT); + + private final int major; + private final int minor; + private final int patch; + private final String special; + + public Version(@Nonnegative final int major, @Nonnegative final int minor, @Nonnegative final int patch) { + this(major, minor, patch, null); + } + + public Version(@Nonnegative final int major, @Nonnegative final int minor, @Nonnegative final int patch, @Nonnull final String special) { + if (major < 0) { + throw new IllegalArgumentException(Type.MAJOR+" must be positive"); + } + if (minor < 0) { + throw new IllegalArgumentException(Type.MINOR+" must be positive"); + } + if (patch < 0) { + throw new IllegalArgumentException(Type.PATCH+" must be positive"); + } + + this.major = major; + this.minor = minor; + this.patch = patch; + this.special = special; + } + + /** + * + * Creates a Version from a string representation. Must match Version#FORMAT. + * + * @param version + * @return + */ + public static Version parse(@Nonnull final String version) { + final Matcher matcher = Version.PATTERN.matcher(version); + if (!matcher.matches()) { + throw new IllegalArgumentException("<"+version+"> does not match format "+Version.FORMAT); + } + + final int major = Version.parseElement(matcher.group(1), Type.MAJOR); + final int minor = Version.parseElement(matcher.group(2), Type.MINOR); + final int patch = Version.parseElement(matcher.group(3), Type.PATCH); + + if (matcher.groupCount() == 4) { + return new Version(major, minor, patch, matcher.group(4)); + } else { + return new Version(major, minor, patch); + } + } + + /** + * @param number + * @param type + * @return int representation of provided number + */ + private static @Nonnegative int parseElement(@Nonnull final String number, @Nonnull final Version.Type type) { + try { + return Integer.valueOf(number); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(type+" must be an integer", e); + } + } + + /** + * @param type + * @return next {@link Version} regarding specified {@link Version.Type} + */ + public Version next(@Nonnull final Version.Type type) { + switch (type) { + case MAJOR: + return new Version(this.major+1, 0, 0); + case MINOR: + return new Version(this.major, this.minor+1, 0); + case PATCH: + return new Version(this.major, this.minor, this.patch+1); + default: + throw new IllegalArgumentException("Unknown type <"+type+">"); + } + } + + /** + * @param other + * @return most important differing {@link Version.Type} component between this and another {@link Version}, null if both are same. + */ + public Version.Type delta(@Nonnull final Version other) { + if (this.major != other.major) { + return Version.Type.MAJOR; + } else if (this.minor != other.minor) { + return Version.Type.MINOR; + } else if (this.patch != other.patch) { + return Version.Type.PATCH; + } else if (this.special.equals(other.special)) { + return null; + } else { + return null; + } + } + + public boolean isInDevelopment() { + return this.major == 0; + } + + public boolean isStable() { + return !isInDevelopment(); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 43 * hash + this.major; + hash = 43 * hash + this.minor; + hash = 43 * hash + this.patch; + hash = 43 * hash + (this.special != null ? this.special.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (!(object instanceof Version)) { + return false; + } + + final Version other = (Version) object; + if (other.major != this.major || other.minor != this.minor || other.patch != this.patch) { + return false; + } + return (this.special == null) ? other.special == null : this.special.equals(other.special); + } + + @Override + public int compareTo(final Version other) { + if (equals(other)) { + return 0; + } + + if (other.major > this.major) { + return 1; + } else if (other.major == this.major) { + if (other.minor > this.minor) { + return 1; + } else if (other.minor == this.minor) { + if (other.patch > this.patch) { + return 1; + } else if (other.special != null) { + return other.special.compareTo(this.special); + } + } + } + return -1; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(this.major).append(".").append(this.minor).append(".").append(this.patch); + if (this.special != null) { + builder.append(this.special); + } + return builder.toString(); + } + +} diff --git a/api/src/main/java/org/semver/jardiff/AccumulatingDiffHandler.java b/api/src/main/java/org/semver/jardiff/AccumulatingDiffHandler.java new file mode 100644 index 0000000..5bbbd42 --- /dev/null +++ b/api/src/main/java/org/semver/jardiff/AccumulatingDiffHandler.java @@ -0,0 +1,286 @@ +/** + * This software is licensed under the Apache 2 license, quoted below. + * + * Copyright 2010 Julien Eluard + * + * 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. + */ + +package org.semver.jardiff; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nonnull; + +import org.osjava.jardiff.AbstractDiffHandler; +import org.osjava.jardiff.AbstractInfo; +import org.osjava.jardiff.ClassInfo; +import org.osjava.jardiff.DiffException; +import org.osjava.jardiff.DiffHandler; +import org.osjava.jardiff.FieldInfo; +import org.osjava.jardiff.MethodInfo; + +/** + * + * {@link DiffHandler} implementation accumulating changes. + * + */ +public class AccumulatingDiffHandler extends AbstractDiffHandler { + + public static class Difference implements Comparable<Difference> { + + private final String className; + private final AbstractInfo info; + + public Difference(@Nonnull final String className, @Nonnull final AbstractInfo info) { + this.className = className; + this.info = info; + } + + public String getClassName() { + return this.className; + } + + public AbstractInfo getInfo() { + return info; + } + + @Override + public int compareTo(final Difference other) { + return getClassName().compareTo(other.getClassName()); + } + + } + + public static class Add extends Difference { + + public Add(@Nonnull final String className, @Nonnull final AbstractInfo info) { + super(className, info); + } + + } + + public static class Change extends Difference { + + private final AbstractInfo modifiedInfo; + + public Change(@Nonnull final String className, @Nonnull final AbstractInfo info, @Nonnull final AbstractInfo modifiedInfo) { + super(className, info); + + this.modifiedInfo = modifiedInfo; + } + + public AbstractInfo getModifiedInfo() { + return this.modifiedInfo; + } + + } + + public class Remove extends Difference { + + public Remove(@Nonnull final String className, @Nonnull final AbstractInfo info) { + super(className, info); + } + + } + + private String currentClassName; + private final Set<String> includes; + private final Set<String> excludes; + private final Set<Difference> differences = new HashSet<Difference>(); + + public AccumulatingDiffHandler() { + this(Collections.<String>emptySet(), Collections.<String>emptySet()); + } + + public AccumulatingDiffHandler(@Nonnull final Set<String> includes, @Nonnull final Set<String> excludes) { + this.includes = includes; + this.excludes = excludes; + } + + @Override + public void startDiff(final String previous, final String current) throws DiffException { + } + + @Override + public void endDiff() throws DiffException { + } + + @Override + public void startOldContents() throws DiffException { + } + + @Override + public void endOldContents() throws DiffException { + } + + @Override + public void startNewContents() throws DiffException { + } + + @Override + public void endNewContents() throws DiffException { + } + + @Override + public void contains(final ClassInfo classInfo) throws DiffException { + } + + @Override + public void startAdded() throws DiffException { + } + + @Override + public void classAdded(final ClassInfo classInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Add(getClassName(classInfo.getName()), classInfo)); + } + + @Override + public void fieldAdded(final FieldInfo fieldInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Add(this.currentClassName, fieldInfo)); + } + + @Override + public void methodAdded(final MethodInfo methodInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Add(this.currentClassName, methodInfo)); + } + + @Override + public void endAdded() throws DiffException { + } + + @Override + public void startChanged() throws DiffException { + } + + @Override + public void startClassChanged(final String className) throws DiffException { + this.currentClassName = getClassName(className); + } + + @Override + public void classChanged(final ClassInfo oldClassInfo, final ClassInfo newClassInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Change(this.currentClassName, oldClassInfo, newClassInfo)); + } + + @Override + public void fieldChanged(final FieldInfo oldFieldInfo, final FieldInfo newFieldInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Change(this.currentClassName, oldFieldInfo, newFieldInfo)); + } + + @Override + public void methodChanged(final MethodInfo oldMethodInfo, final MethodInfo newMethodInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Change(this.currentClassName, oldMethodInfo, newMethodInfo)); + } + + @Override + public void endClassChanged() throws DiffException { + } + + @Override + public void endChanged() throws DiffException { + } + + @Override + public void startRemoved() throws DiffException { + } + + @Override + public void classRemoved(final ClassInfo classInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Remove(this.currentClassName, classInfo)); + } + + @Override + public void fieldRemoved(final FieldInfo fieldInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Remove(this.currentClassName, fieldInfo)); + } + + @Override + public void methodRemoved(final MethodInfo methodInfo) throws DiffException { + if (!isConsidered()) { + return; + } + + this.differences.add(new Remove(this.currentClassName, methodInfo)); + } + + @Override + public void endRemoved() throws DiffException { + } + + /** + * + * Is considered a class whose package: + * * is included + * * is not excluded + * + * If includes are provided then package must be defined here. + * + * @return + */ + protected final boolean isConsidered() { + for (final String exclude : this.excludes) { + if (this.currentClassName.startsWith(exclude)) { + return false; + } + } + + if (!this.includes.isEmpty()) { + for (final String include : this.includes) { + if (this.currentClassName.startsWith(include)) { + return true; + } + } + return false; + } + return true; + } + + public Set<Difference> getDifferences() { + return Collections.unmodifiableSet(this.differences); + } + +} diff --git a/api/src/site/apt/index.apt b/api/src/site/apt/index.apt new file mode 100644 index 0000000..05e1a2a --- /dev/null +++ b/api/src/site/apt/index.apt @@ -0,0 +1,43 @@ +~~ +~~ This software is licensed under the Apache 2 license, quoted below. +~~ +~~ Copyright 2010 Julien Eluard +~~ +~~ 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. +~~ + +Semantic Versioning + + Semantic Versioning provides tools to automatically validate if your project's version number follows Semantic Versioning principles as defined by {{{http://semver.org}Semantic Versioning}}. + +* CLI overview + + The CLI has the following functionality: + + * {{{usage.html}Diff}} between two specifed JARs + + * {{{usage.html}Check}} compatibility type between two specifed JARs + + * {{{usage.html}Infer}} correct version of your JAR based on an previous version + + * {{{usage.html}Validate}} if your JAR's version is compatible with a previous one + + [] + +* API overview + + Semantic Versioning also provides an API for programmatically validating your project's version number. This API is used by + {{{../enforcer-rule} the maven enforcer rule}}. If these tool is not sufficient to your needs, then + the entry point to the API is {{{apidocs/org/semver/Checker.html}Checker}}. + + []
\ No newline at end of file diff --git a/api/src/site/apt/usage.apt.vm b/api/src/site/apt/usage.apt.vm new file mode 100644 index 0000000..95b3f2d --- /dev/null +++ b/api/src/site/apt/usage.apt.vm @@ -0,0 +1,63 @@ +#* + * This software is licensed under the Apache 2 license, quoted below. + * + * Copyright 2010 Julien Eluard + * + * 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. + *# + +What is this? + + This simple command line tool looks at Java JAR files and determine inspect API changes. + +Diff + + Dumps all changes between two JARs on standard output. + +----------------------------- +% java -jar target/semantic-versioning-${project.version}.jar previousJar currentJar (includes) (excludes) +Class org.project.MyClass + Added Class +Class org.project.MyClass2 + Added Method method1 + Removed Field field1 + Changed Field field2 removed: final + +----------------------------- + +Check + + Checks compatibility type between two JARs. + +----------------------------- +% java -jar target/semantic-versioning-${project.version}.jar previousJar currentJar (includes) (excludes) +BACKWARD_COMPATIBLE_IMPLEMENTER +----------------------------- + +Infer + + Infers JAR version based on a previously versioned JAR. + +----------------------------- +% java -jar target/semantic-versioning-${project.version}.jar previousVersion previousJar currentJar (includes) (excludes) +1.0.0 +----------------------------- + +Validate + + Validates JAR version based on a previously versioned JAR. + +----------------------------- +% java -jar target/semantic-versioning-${project.version}.jar previousVersion previousJar currentVersion currentJar (includes) (excludes) +true +-----------------------------
\ No newline at end of file diff --git a/api/src/site/site.xml b/api/src/site/site.xml new file mode 100644 index 0000000..5934f1a --- /dev/null +++ b/api/src/site/site.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + This software is licensed under the Apache 2 license, quoted below. + + Copyright 2010 Julien Eluard + + 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. + +--> + +<project> + <body> + <menu ref="parent"/> + <menu name="Overview"> + <item name="Introduction" href="index.html" /> + <item name="Usage" href="usage.html" /> + </menu> + <menu ref="reports" /> + </body> +</project> + diff --git a/api/src/test/java/org/semver/VersionTest.java b/api/src/test/java/org/semver/VersionTest.java new file mode 100644 index 0000000..4197f8a --- /dev/null +++ b/api/src/test/java/org/semver/VersionTest.java @@ -0,0 +1,101 @@ +/** + * This software is licensed under the Apache 2 license, quoted below. + * + * Copyright 2010 Julien Eluard + * + * 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. + */ + +package org.semver; + +import org.junit.Assert; +import org.junit.Test; + +public class VersionTest { + + @Test + public void shouldNegativVersionBeRejected() { + try { + new Version(-1, 0, 0); + Assert.fail(); + } catch (IllegalArgumentException e) { + } + try { + new Version(0, -1, 0); + Assert.fail(); + } catch (IllegalArgumentException e) { + } + try { + new Version(0, 0, -1); + Assert.fail(); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void shouldValidVersionAreParsed() { + Version.parse("1.2.3"); + Version.parse("1.2.3beta"); + } + + @Test(expected=IllegalArgumentException.class) + public void shouldInvalidVersion1NotBeParsed() { + Version.parse("invalid"); + } + + @Test(expected=IllegalArgumentException.class) + public void shouldInvalidVersion2NotBeParsed() { + Version.parse("1.2.3.4"); + } + + @Test(expected=IllegalArgumentException.class) + public void shouldInvalidVersion3NotBeParsed() { + Version.parse("1.2.3.beta"); + } + + @Test + public void shouldDevelopmentBeInDevelopment() { + Assert.assertTrue(Version.parse("0.1.1").isInDevelopment()); + Assert.assertFalse(Version.parse("1.1.1").isInDevelopment()); + } + + @Test + public void shouldStableVersionBeStable() { + Assert.assertTrue(Version.parse("1.1.1").isStable()); + Assert.assertFalse(Version.parse("0.1.1").isStable()); + } + + @Test + public void isNewer() { + Assert.assertTrue(Version.parse("1.0.0").compareTo(Version.parse("0.0.0")) < 0); + Assert.assertTrue(Version.parse("1.1.0").compareTo(Version.parse("1.0.0")) < 0); + Assert.assertTrue(Version.parse("1.0.1").compareTo(Version.parse("1.0.0")) < 0); + Assert.assertTrue(Version.parse("1.0.0Beta").compareTo(Version.parse("1.0.0Alpha")) < 0); + Assert.assertFalse(Version.parse("0.0.0").compareTo(Version.parse("0.0.0")) < 0); + Assert.assertFalse(Version.parse("0.0.0").compareTo(Version.parse("0.0.1")) < 0); + } + + @Test + public void isAtLeast() { + Assert.assertTrue(Version.Type.MAJOR.isAtLeast(Version.Type.MAJOR)); + Assert.assertTrue(Version.Type.MAJOR.isAtLeast(Version.Type.MINOR)); + Assert.assertTrue(Version.Type.MAJOR.isAtLeast(Version.Type.PATCH)); + Assert.assertFalse(Version.Type.MINOR.isAtLeast(Version.Type.MAJOR)); + Assert.assertTrue(Version.Type.MINOR.isAtLeast(Version.Type.MINOR)); + Assert.assertTrue(Version.Type.MINOR.isAtLeast(Version.Type.PATCH)); + Assert.assertFalse(Version.Type.PATCH.isAtLeast(Version.Type.MAJOR)); + Assert.assertFalse(Version.Type.PATCH.isAtLeast(Version.Type.MINOR)); + Assert.assertTrue(Version.Type.PATCH.isAtLeast(Version.Type.PATCH)); + } + +} |