aboutsummaryrefslogtreecommitdiffstats
path: root/api/src
diff options
context:
space:
mode:
authorJulien Eluard <[email protected]>2010-11-14 22:11:49 +0100
committerJulien Eluard <[email protected]>2010-11-14 22:11:49 +0100
commita06cd8f04ce4fed80d9fa1197c222bafb4624e41 (patch)
tree851bb643bd5d7639d005a0bf96633b7175e8c43f /api/src
first commit
Diffstat (limited to 'api/src')
-rw-r--r--api/src/main/java/org/semver/Checker.java332
-rw-r--r--api/src/main/java/org/semver/Version.java210
-rw-r--r--api/src/main/java/org/semver/jardiff/AccumulatingDiffHandler.java286
-rw-r--r--api/src/site/apt/index.apt43
-rw-r--r--api/src/site/apt/usage.apt.vm63
-rw-r--r--api/src/site/site.xml32
-rw-r--r--api/src/test/java/org/semver/VersionTest.java101
7 files changed, 1067 insertions, 0 deletions
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));
+ }
+
+}