001/*
002 * Copyright 2006-2012 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.kuali.rice.vc.test;
018
019import com.predic8.schema.ComplexType;
020import com.predic8.schema.Sequence;
021import com.predic8.soamodel.Difference;
022import com.predic8.wsdl.Definitions;
023import com.predic8.wsdl.Operation;
024import com.predic8.wsdl.PortType;
025import com.predic8.wsdl.WSDLParser;
026import com.predic8.wsdl.diff.WsdlDiffGenerator;
027import org.apache.commons.lang.StringUtils;
028import org.apache.log4j.Logger;
029import org.codehaus.jackson.JsonNode;
030import org.codehaus.jackson.map.ObjectMapper;
031import org.kuali.rice.core.api.config.property.Config;
032import org.kuali.rice.core.api.config.property.ConfigContext;
033import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
034import org.kuali.rice.core.api.lifecycle.Lifecycle;
035import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
036import org.kuali.rice.test.BaselineTestCase;
037
038import javax.xml.namespace.QName;
039import java.io.BufferedReader;
040import java.io.File;
041import java.io.IOException;
042import java.io.InputStreamReader;
043import java.net.MalformedURLException;
044import java.net.URL;
045import java.util.*;
046import java.util.regex.Pattern;
047
048import static org.junit.Assert.assertTrue;
049import static org.junit.Assert.fail;
050
051/*
052*  Compatible Changes
053*   - adding a new WSDL operation definition and associated message definitions
054*   - adding a new WSDL port type definition and associated operation definitions
055*   - adding new WSDL binding and service definitions
056*   - adding a new optional XML Schema element or attribute declaration to a message definition
057*   - reducing the constraint granularity of an XML Schema element or attribute of a message definition type
058*   - adding a new XML Schema wildcard to a message definition type
059*   - adding a new optional WS-Policy assertion
060*   - adding a new WS-Policy alternative
061*
062* Incompatible Changes
063*   - renaming an existing WSDL operation definition
064*   - removing an existing WSDL operation definition
065*   - changing the MEP of an existing WSDL operation definition
066*   - adding a fault message to an existing WSDL operation definition
067*   - adding a new required XML Schema element or attribute declaration to a message definition
068*   - increasing the constraint granularity of an XML Schema element or attribute declaration of a message definition
069*   - renaming an optional or required XML Schema element or attribute in a message definition
070*   - removing an optional or required XML Schema element or attribute or wildcard from a message definition
071*   - adding a new required WS-Policy assertion or expression
072*   - adding a new ignorable WS-Policy expression (most of the time)
073*/
074@BaselineTestCase.BaselineMode(BaselineTestCase.Mode.ROLLBACK)
075public abstract class WsdlCompareTestCase extends BaselineTestCase {
076    private static final Logger LOG = Logger.getLogger(WsdlCompareTestCase.class);
077    private static final String WSDL_URL = "wsdl.test.previous.url";
078    private static final String WSDL_PREVIOUS_VERSION = "wsdl.test.previous.version";
079    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
080
081    private String previousVersion;
082
083    private static final List<String> ignoreBreakageRegexps = Arrays.asList(
084            ".*Position of any changed from .*", // change in position of an 'any' doesn't indicate a breakage for us
085            ".*Position of element null changed.$" // this also indicates an 'any' changing position, ignore it too
086    );
087
088    public WsdlCompareTestCase(String moduleName) {
089        super(moduleName);
090    }
091
092    protected List<String> verifyWsdlDifferences(Difference diff, String level) {
093        List<String> results = new ArrayList<String>();
094
095        if (diff.isBreaks()) {
096            boolean ignore = false;
097            for (String ignoreBreakageRegexp : ignoreBreakageRegexps) {
098                if (diff.getDescription().matches(ignoreBreakageRegexp)) {
099                    ignore = true;
100                    break;
101                }
102            }
103
104            if (ignore) {
105                LOG.info(level + "non-breaking change" + diff.getDescription());
106            } else {
107                LOG.error(level + "breaking change: " + diff.getType() + diff.getDescription());
108                results.add(level + diff.getDescription());
109            }
110        }
111
112
113        //check for operation based sequence changes
114        String opBreakageString = checkForOperationBasedChanges(diff);
115        if (opBreakageString != null) {
116            results.add(level + opBreakageString);
117        }
118
119        for (Difference moreDiff : diff.getDiffs())  {
120            List<String> childBreakages = verifyWsdlDifferences(moreDiff, level + "  ");
121            for (String childBreakage : childBreakages) {
122                if (!diff.getDescription().trim().startsWith("Schema ")) {
123                    results.add(level + diff.getDescription() + LINE_SEPARATOR + childBreakage);
124                } else {
125                    results.add(childBreakage);
126                }
127            }
128        }
129
130        return results;
131    }
132
133    /*
134     * This method is essentially an extra check because java2ws marks parameters on methods as minOccurs=0, which means
135     * as far as the wsdl comparison, adding a new parameter is ok, because it isn't required.
136     *
137     * Unfortunately, that adding the parameter breaks compatibility for us because it invalidates the java interface.
138     *
139     * So, This method goes through, and checks to see if the sequence change is on one of the services Operators.  If it
140     * is on an operator, and there is a difference in type of the operator, we've broken compatibility and should fail.
141     *
142     * returns a string if there is a breakage, null otherwise
143     */
144    private String checkForOperationBasedChanges(Difference diff) {
145        if ("sequence".equals(diff.getType())
146                && diff.getA() != null
147                && diff.getB() != null) {
148            Sequence oldSequence = (Sequence)diff.getA();
149            Sequence newSequence = (Sequence)diff.getB();
150            if (newSequence.getParent() instanceof ComplexType) {
151                ComplexType parent = (ComplexType)newSequence.getParent();
152                String serviceName = newSequence.getSchema().getDefinitions().getName();
153                PortType portType = newSequence.getSchema().getDefinitions().getPortType(serviceName);
154                if (portType != null) {
155                    Operation operation = portType.getOperation(parent.getName());
156
157                    if (operation != null) {
158                        return "Element cannot be added to a sequence if sequence is an Operation " +
159                                diff.getDescription();
160                    }
161//                    assertTrue("Element cannot be added to a sequence if sequence is an Operation " + diff
162//                            .getDescription(), operation == null);
163                }
164            }
165        }
166        return null;
167    }
168
169    protected List<Difference> compareWsdlDefinitions(String oldWsdl, String newWsdl) {
170        WSDLParser parser = new WSDLParser();
171
172        Definitions wsdl1;
173        Definitions wsdl2;
174        try {
175            wsdl1 = parser.parse(oldWsdl);
176        } catch (com.predic8.xml.util.ResourceDownloadException e) {
177            LOG.info("Couldn't download " + oldWsdl + ", maybe the service didn't exist in this version?");
178            return Collections.emptyList();
179        }
180        try {
181            wsdl2 = parser.parse(newWsdl);
182        } catch (com.predic8.xml.util.ResourceDownloadException e) {
183            LOG.info("Couldn't download" + newWsdl + ", maybe the service didn't exist in this version?");
184            return Collections.emptyList();
185        }
186
187        WsdlDiffGenerator diffGen = new WsdlDiffGenerator(wsdl1, wsdl2);
188        return diffGen.compare();
189    }
190
191    protected String getPreviousVersionWsdlUrl(String wsdlFile, MavenVersion previousVersion) {
192
193        StringBuilder oldWsdl = new StringBuilder(buildWsdlUrlPrefix(previousVersion.getOriginalForm()));
194        oldWsdl.append("rice-");
195        oldWsdl.append(getModuleName());
196        oldWsdl.append("-api-");
197        oldWsdl.append(previousVersion.getOriginalForm());
198        oldWsdl.append("-");
199        oldWsdl.append(wsdlFile);
200
201        return oldWsdl.toString();
202    }
203
204    //String oldWsdl = MAVEN_REPO_PREFIX + MODULE + "-api/" + PREVIOUS_VERSION + "/rice-" + MODULE + "-api-" + PREVIOUS_VERSION + "-" + file.getName();
205    private String buildWsdlUrlPrefix(String previousVersion) {
206        String wsdlUrl = ConfigContext.getCurrentContextConfig().getProperty(WSDL_URL);
207
208        if (StringUtils.isNotBlank(wsdlUrl)
209                && StringUtils.isNotBlank(previousVersion)) {
210            StringBuilder urlBuilder = new StringBuilder(wsdlUrl);
211            if (!wsdlUrl.endsWith("/")) {
212                urlBuilder.append("/");
213            }
214            urlBuilder.append("rice-");
215            urlBuilder.append(getModuleName());
216            urlBuilder.append("-api/");
217            urlBuilder.append(previousVersion);
218            urlBuilder.append("/");
219
220            return urlBuilder.toString();
221
222        } else {
223            throw new RuntimeException("Couldn't build wsdl url prefix");
224        }
225    }
226
227    /**
228     * Allows an extending test to specify versions transitions of specific wsdls to omit from testing.  This can be
229     * useful for ignoring version compatibility issues that have already been addressed in previously released
230     * versions.
231     *
232     * @return a Map from wsdl file name (e.g. "DocumentTypeService.wsdl") to a list of {@link MavenVersion}s to filter
233     */
234    protected Map<String, List<VersionTransition>> getWsdlVersionTransitionBlacklists() {
235        return new HashMap<String, List<VersionTransition>>();
236    }
237
238    protected void compareWsdlFiles(File[] wsdlFiles) {
239        List<VersionCompatibilityBreakage> breakages = new ArrayList<VersionCompatibilityBreakage>();
240
241        assertTrue("There should be wsdls to compare", wsdlFiles != null  && wsdlFiles.length > 0);
242
243        MavenVersion previousVersion = new MavenVersion(getPreviousVersion(),
244                "0" /*since this is the oldest version we'll deal with, setting the timestamp to 0 is ok for sorting */);
245        MavenVersion currentVersion = getCurrentMavenVersion();
246        List<MavenVersion> versions = getVersionRange(previousVersion, currentVersion);
247        List<VersionTransition> transitions = generateVersionTransitions(currentVersion, versions);
248
249        for (File wsdlFile : wsdlFiles) { // we're effectively iterating through each service
250            if (wsdlFile.getName().endsWith(".wsdl")) {
251                LOG.info("TESTING WSDL: " + wsdlFile.getAbsolutePath());
252                String newWsdl = wsdlFile.getAbsolutePath();
253
254                // do filtering to avoid testing blacklisted wsdl version transitions
255                List<VersionTransition> wsdlTransitionBlacklist =
256                        getWsdlVersionTransitionBlacklists().get(getServiceNameFromWsdlFile(wsdlFile));
257
258                if (wsdlTransitionBlacklist == null) { wsdlTransitionBlacklist = Collections.emptyList(); }
259
260                for (VersionTransition transition : transitions) if (!wsdlTransitionBlacklist.contains(transition)) {
261                    breakages.addAll(testWsdlVersionTransition(currentVersion, wsdlFile, transition));
262                } else {
263                    LOG.info("Ignoring blacklisted " + transition);
264                }
265            }
266        }
267
268        if (!breakages.isEmpty()) {
269            fail(buildBreakagesSummary(breakages));
270        }
271    }
272
273    // Quick and dirty, and AFAIK very specific to Rice's conventions
274    String getServiceNameFromWsdlFile(File wsdlFile) {
275        String fileName = wsdlFile.getName();
276        int beginIndex = 1 + fileName.lastIndexOf('-');
277        int endIndex = fileName.lastIndexOf('.');
278
279        return fileName.substring(beginIndex, endIndex);
280    }
281
282    /**
283     * find breakages for the given wsdl's version transition
284     * @param currentVersion the current version of Rice
285     * @param wsdlFile the local wsdl file
286     * @param transition the version transition to test
287     * @return any breakages detected
288     */
289    private List<VersionCompatibilityBreakage> testWsdlVersionTransition(MavenVersion currentVersion, File wsdlFile, VersionTransition transition) {
290        List<VersionCompatibilityBreakage> breakages = new ArrayList<VersionCompatibilityBreakage>();
291
292        String fromVersionWsdlUrl = getPreviousVersionWsdlUrl(wsdlFile.getName(), transition.getFromVersion());
293        String toVersionWsdlUrl = getPreviousVersionWsdlUrl(wsdlFile.getName(), transition.getToVersion());
294
295        // current version isn't in the maven repo, use the local file
296        if (transition.getToVersion().equals(currentVersion)) {
297            toVersionWsdlUrl = wsdlFile.getAbsolutePath();
298        }
299
300        getPreviousVersionWsdlUrl(wsdlFile.getName(), transition.getToVersion());
301
302        LOG.info("checking " + transition);
303
304        if (fromVersionWsdlUrl == null) {
305            LOG.warn("SKIPPING check, wsdl not found for " + fromVersionWsdlUrl);
306        } else if (toVersionWsdlUrl == null) {
307            LOG.warn("SKIPPING check, wsdl not found for " + toVersionWsdlUrl);
308        } else {
309            List<Difference> differences = compareWsdlDefinitions(fromVersionWsdlUrl, toVersionWsdlUrl);
310            for (Difference diff : differences) {
311                List<String> breakageStrings = verifyWsdlDifferences(diff, "");
312
313                for (String breakage : breakageStrings) {
314                    breakages.add(new VersionCompatibilityBreakage(
315                            transition.fromVersion, transition.toVersion,
316                            fromVersionWsdlUrl, toVersionWsdlUrl, breakage));
317                }
318            }
319        }
320
321        return breakages;
322    }
323
324    /**
325     * calculate which version transitions to test given the current version, and the list of versions to consider.  The
326     * results should contain a transition from the closest preceeding patch version at each minor version included in
327     * the range to the nearest newer patch version within the current minor version.  That is hard to understand, so
328     * an example is called for:
329     * {@literal
330     *             2.0.0,
331     *             2.0.1,
332     *                         2.1.0,
333     *             2.0.2,
334     * 1.0.4,
335     *                         2.1.1,
336     *                         2.1.2,
337     *                                     2.2.0,
338     *                         2.1.3,
339     *                                     2.2.1,
340     *                         2.1.4,
341     *                                     2.2.2,
342     *                         2.1.5,
343     *                                     2.2.3,
344     * }
345     * So for the above version stream (which is sorted by time) the transitions for the range 1.0.4 to 2.2.3 would be:
346     * {@literal
347     * 1.0.4 -> 2.2.0,
348     * 2.1.2 -> 2.2.0,
349     * 2.1.3 -> 2.2.1,
350     * 2.1.4 -> 2.2.2,
351     * 2.1.5 -> 2.2.3,
352     * }
353     *
354     * @param currentVersion the current version of Rice
355     * @param versions the versions to consider
356     * @return the calculated List of VersionTransitions
357     */
358    protected List<VersionTransition> generateVersionTransitions(MavenVersion currentVersion, List<MavenVersion> versions) {
359        List<VersionTransition> results = new ArrayList<VersionTransition>();
360
361        versions = new ArrayList<MavenVersion>(versions);
362        Collections.sort(versions, mavenVersionTimestampComparator);
363        // We want to iterate through from newest to oldest, so reverse
364        Collections.reverse(versions);
365
366        final MavenVersion currentMinorVersion = trimToMinorVersion(currentVersion);
367        MavenVersion buildingTransitionsTo = currentVersion; // the version we're currently looking at transitions to
368
369        // Keep track of minor versions we've used to build transitions to buildingTransitionsTo
370        // because we want at most one transition from each minor version to any given version
371        Set<MavenVersion> minorVersionsFrom = new HashSet<MavenVersion>();
372
373        for (MavenVersion version : versions) if (version.compareTo(buildingTransitionsTo) < 0) {
374            MavenVersion minorVersion = trimToMinorVersion(version);
375            if (minorVersion.equals(currentMinorVersion)) {
376                // One last transition to add, then start building transitions to this one
377                results.add(new VersionTransition(version, buildingTransitionsTo));
378                buildingTransitionsTo = version;
379                // also, reset the blacklist of versions we can transition from
380                minorVersionsFrom.clear();
381            } else if (!minorVersionsFrom.contains(minorVersion)) {
382                results.add(new VersionTransition(version, buildingTransitionsTo));
383                minorVersionsFrom.add(minorVersion);
384            }
385        }
386
387        // reverse our results so they go from old to new
388        Collections.reverse(results);
389
390        return results;
391    }
392
393    /**
394     * Peel off the patch version and return a MavenVersion that just extends to the minor portion of the given version
395     */
396    private MavenVersion trimToMinorVersion(MavenVersion fullVersion) {
397        return new MavenVersion(""+fullVersion.getNumbers().get(0)+"."+fullVersion.getNumbers().get(1), "0");
398    }
399
400    protected String buildBreakagesSummary(List<VersionCompatibilityBreakage> breakages) {
401        StringBuilder errorsStringBuilder =
402                new StringBuilder(LINE_SEPARATOR + "!!!!! Detected " + breakages.size() + " VC Breakages !!!!!"
403                        + LINE_SEPARATOR);
404
405        MavenVersion lastOldVersion = null;
406        String lastOldWsdlUrl = "";
407
408        for (VersionCompatibilityBreakage breakage : breakages) {
409            // being lazy and using '!=' instead of '!lastOldVersion.equals(...)' to avoid NPEs and extra checks
410            if (lastOldVersion != breakage.oldMavenVersion || lastOldWsdlUrl != breakage.oldWsdlUrl) {
411                lastOldVersion = breakage.oldMavenVersion;
412                lastOldWsdlUrl = breakage.oldWsdlUrl;
413
414                errorsStringBuilder.append(LINE_SEPARATOR + "Old Version: " + lastOldVersion.getOriginalForm()
415                        +", wsdl: " + lastOldWsdlUrl);
416                errorsStringBuilder.append(LINE_SEPARATOR + "New Version: " + breakage.newMavenVersion.getOriginalForm()
417                        +", wsdl: " + breakage.newWsdlUrl + LINE_SEPARATOR + LINE_SEPARATOR);
418            }
419            errorsStringBuilder.append(breakage.breakageMessage + LINE_SEPARATOR);
420        }
421        return errorsStringBuilder.toString();
422    }
423
424    public String getPreviousVersion() {
425        if (StringUtils.isEmpty(this.previousVersion)) {
426            this.previousVersion = ConfigContext.getCurrentContextConfig().getProperty(WSDL_PREVIOUS_VERSION);
427        }
428        return this.previousVersion;
429    }
430
431    public void setPreviousVersion(String previousVersion) {
432        this.previousVersion = previousVersion;
433    }
434
435    @Override
436    protected Lifecycle getLoadApplicationLifecycle() {
437        SpringResourceLoader springResourceLoader = new SpringResourceLoader(new QName("VCTestHarnessResourceLoader"), "classpath:VCTestHarnessSpringBeans.xml", null);
438        springResourceLoader.setParentSpringResourceLoader(getTestHarnessSpringResourceLoader());
439        return springResourceLoader;
440    }
441
442    @Override
443    protected List<Lifecycle> getPerTestLifecycles() {
444        return new ArrayList<Lifecycle>();
445    }
446
447    @Override
448    protected List<Lifecycle> getSuiteLifecycles() {
449        List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
450
451        /**
452         * Initializes Rice configuration from the test harness configuration file.
453         */
454        lifecycles.add(new BaseLifecycle() {
455            @Override
456            public void start() throws Exception {
457                Config config = getTestHarnessConfig();
458                ConfigContext.init(config);
459                super.start();
460            }
461        });
462
463        return lifecycles;
464    }
465
466    /**
467     * Returns the range of versions from previousVersion to currentVersion.  The versions will be in the numerical
468     * range, but will be sorted by timestamp. Note that if either of the given versions aren't in maven central, they
469     * won't be included in the results.
470     * @param lowestVersion the lowest version in the range
471     * @param highestVersion the highest version in the range
472     * @return
473     */
474    protected List<MavenVersion> getVersionRange(MavenVersion lowestVersion, MavenVersion highestVersion) {
475        ArrayList<MavenVersion> results = new ArrayList<MavenVersion>();
476
477        if (highestVersion.compareTo(lowestVersion) <= 0) {
478            throw new IllegalStateException("currentVersion " + highestVersion +
479                    "  is <= previousVersion " + lowestVersion);
480        }
481        List<MavenVersion> riceVersions = getRiceMavenVersions();
482
483        for (MavenVersion riceVersion : riceVersions) {
484            if ( highestVersion.compareTo(riceVersion) > 0 &&
485                    lowestVersion.compareTo(riceVersion) <= 0 &&
486                    "".equals(riceVersion.getQualifier()) ) {
487                results.add(riceVersion);
488            }
489        }
490
491        return results;
492    }
493
494    // "cache" for rice maven versions, since these will not differ between tests and we have to hit
495    // the maven central REST api to get them
496    private static List<MavenVersion> riceMavenVersions = null;
497
498    private static List<MavenVersion> getRiceMavenVersions() {
499        if (riceMavenVersions == null) {
500            String searchContent = getMavenSearchResults();
501            riceMavenVersions = parseSearchResults(searchContent);
502
503            Collections.sort(riceMavenVersions, mavenVersionTimestampComparator);
504
505            LOG.info("Published versions, sorted by timestamp:");
506            for (MavenVersion riceVersion : riceMavenVersions) {
507                LOG.info("" + riceVersion.getTimestamp() + " " + riceVersion.getOriginalForm());
508            }
509
510        }
511        return riceMavenVersions;
512    }
513
514    /**
515     * @return the current version of Rice
516     */
517    private MavenVersion getCurrentMavenVersion() {
518        return new MavenVersion(ConfigContext.getCurrentContextConfig().getProperty("rice.version"),
519                ""+System.currentTimeMillis());
520    }
521
522    private static List<MavenVersion> parseSearchResults(String searchContent) {
523        LinkedList<MavenVersion> riceVersions = new LinkedList<MavenVersion>();
524
525        ObjectMapper mapper = new ObjectMapper();
526        JsonNode rootNode;
527        try {
528            rootNode = mapper.readTree(searchContent);
529        } catch (IOException e) {
530            throw new RuntimeException("Can't parse maven search results", e);
531        }
532        JsonNode docsNode = rootNode.get("response").get("docs");
533
534        for (JsonNode node : docsNode) {
535            String versionStr = node.get("v").toString();
536            String timestampStr = node.get("timestamp").toString();
537            // System.out.println(versionStr);
538            riceVersions.add(new MavenVersion(versionStr.replace(/* strip out surrounding quotes */ "\"",""), timestampStr));
539        }
540
541        Collections.sort(riceVersions);
542        return riceVersions;
543    }
544
545    private static String getMavenSearchResults() {
546        // using the maven search REST api specified here: http://search.maven.org/#api
547        // this query gets all versions of Rice from maven central
548        final String mavenSearchUrlString =
549                "http://search.maven.org/solrsearch/select?q=g:%22org.kuali.rice%22+AND+a:%22rice%22&core=gav&rows=20&wt=json";
550
551        URL mavenSearchUrl;
552
553        try {
554            mavenSearchUrl = new URL(mavenSearchUrlString);
555        } catch (MalformedURLException e) {
556            throw new RuntimeException("can't parse maven search url", e);
557        }
558
559        StringBuilder contentBuilder = new StringBuilder();
560        BufferedReader contentReader;
561        try {
562            contentReader = new BufferedReader(new InputStreamReader(mavenSearchUrl.openStream()));
563            String line;
564            while (null != (line = contentReader.readLine())) {
565                contentBuilder.append(line + LINE_SEPARATOR);
566            }
567        } catch (IOException e) {
568            throw new RuntimeException("Unable to read search results", e);
569        }
570        return contentBuilder.toString();
571    }
572
573    /**
574     * Utility class for parsing and comparing maven versions
575     */
576    protected static class MavenVersion implements Comparable<MavenVersion> {
577        private static final Pattern PERIOD_PATTERN = Pattern.compile("\\.");
578        private final List<Integer> numbers;
579        private final String originalForm;
580        private final String qualifier;
581        private final Long timestamp;
582
583        /**
584         * Constructor that takes just a version string as an argument.  Beware, because 0 will be used as the timestamp!
585         * @param versionString
586         */
587        public MavenVersion(String versionString) {
588            this(versionString, "0");
589        }
590
591        public MavenVersion(String versionString, String timestampString) {
592            originalForm = versionString;
593            if (versionString == null || "".equals(versionString.trim())) {
594                throw new IllegalArgumentException("empty or null version string");
595            }
596            String versionPart;
597            int dashIndex = versionString.indexOf('-');
598            if (dashIndex != -1 && versionString.length()-1 > dashIndex) {
599                qualifier = versionString.substring(dashIndex+1).trim();
600                versionPart = versionString.substring(0,dashIndex);
601            } else {
602                versionPart = versionString;
603                qualifier = "";
604            }
605            String [] versionArray = PERIOD_PATTERN.split(versionPart);
606
607            List<Integer> numbersBuilder = new ArrayList<Integer>(versionArray.length);
608
609            for (String versionParticle : versionArray) {
610                numbersBuilder.add(Integer.valueOf(versionParticle));
611            }
612
613            numbers = Collections.unmodifiableList(numbersBuilder);
614
615            timestamp = Long.valueOf(timestampString);
616        }
617
618        @Override
619        public int compareTo(MavenVersion that) {
620            Iterator<Integer> thisNumbersIter = this.numbers.iterator();
621            Iterator<Integer> thatNumbersIter = that.numbers.iterator();
622
623            while (thisNumbersIter.hasNext()) {
624                // all else being equal, he/she who has the most digits wins
625                if (!thatNumbersIter.hasNext()) return 1;
626
627                int numberComparison = thisNumbersIter.next().compareTo(thatNumbersIter.next());
628
629                // if one is greater than the other, we've established primacy
630                if (numberComparison != 0) return numberComparison;
631            }
632            // all else being equal, he/she who has the most digits wins
633            if (thatNumbersIter.hasNext()) return -1;
634
635            return compareQualifiers(this.qualifier, that.qualifier);
636        }
637
638        private static int compareQualifiers(String thisQ, String thatQ) {
639            // no qualifier is considered greater than a qualifier (e.g. 1.0-SNAPSHOT is less than 1.0)
640            if ("".equals(thisQ)) {
641                if ("".equals(thatQ)) {
642                    return 0;
643                }
644                return 1;
645            } else if ("".equals(thatQ)) {
646                return -1;
647            }
648
649            return thisQ.compareTo(thatQ);
650        }
651
652        public List<Integer> getNumbers() {
653            return Collections.unmodifiableList(numbers);
654        }
655
656        public String getQualifier() {
657            return qualifier;
658        }
659
660        public Long getTimestamp() {
661            return timestamp;
662        }
663
664        public String getOriginalForm() {
665            return originalForm;
666        }
667
668        @Override
669        public String toString() {
670            return "MavenVersion{" +
671                    originalForm +
672                    '}';
673        }
674
675        @Override
676        public boolean equals(Object o) {
677            if (this == o) {
678                return true;
679            }
680            if (o == null || getClass() != o.getClass()) {
681                return false;
682            }
683
684            final MavenVersion that = (MavenVersion) o;
685
686            if (!originalForm.equals(that.originalForm)) {
687                return false;
688            }
689
690            return true;
691        }
692
693        @Override
694        public int hashCode() {
695            return originalForm.hashCode();
696        }
697    }
698
699    /**
700     * Comparator which can be used to sort MavenVersions by timestamp
701     */
702    private static final Comparator<MavenVersion> mavenVersionTimestampComparator = new Comparator<MavenVersion>() {
703        @Override
704        public int compare(MavenVersion o1, MavenVersion o2) {
705            return o1.getTimestamp().compareTo(o2.getTimestamp());
706        }
707    };
708
709    /**
710     * A class representing a transition from one maven version to another
711     */
712    public static class VersionTransition {
713        private final MavenVersion fromVersion;
714        private final MavenVersion toVersion;
715
716        public VersionTransition(MavenVersion fromVersion, MavenVersion toVersion) {
717            this.fromVersion = fromVersion;
718            this.toVersion = toVersion;
719            if (fromVersion == null) throw new IllegalArgumentException("fromVersion must not be null");
720            if (toVersion == null) throw new IllegalArgumentException("toVersion must not be null");
721        }
722
723        public VersionTransition(String fromVersion, String toVersion) {
724            this(new MavenVersion(fromVersion), new MavenVersion(toVersion));
725        }
726
727        private MavenVersion getFromVersion() {
728            return fromVersion;
729        }
730
731        private MavenVersion getToVersion() {
732            return toVersion;
733        }
734
735        @Override
736        public boolean equals(Object o) {
737            if (this == o) return true;
738            if (o == null || getClass() != o.getClass()) return false;
739
740            VersionTransition that = (VersionTransition) o;
741
742            if (!fromVersion.equals(that.fromVersion)) return false;
743            if (!toVersion.equals(that.toVersion)) return false;
744
745            return true;
746        }
747
748        @Override
749        public int hashCode() {
750            int result = fromVersion.hashCode();
751            result = 31 * result + toVersion.hashCode();
752            return result;
753        }
754
755        @Override
756        public String toString() {
757            return "VersionTransition{" +
758                    "fromVersion=" + fromVersion.getOriginalForm() +
759                    "-> toVersion=" + toVersion.getOriginalForm() +
760                    '}';
761        }
762    }
763
764    /**
765     * struct-ish class to hold data about a VC breakage
766     */
767    protected static class VersionCompatibilityBreakage {
768        private final MavenVersion oldMavenVersion;
769        private final MavenVersion newMavenVersion;
770        private final String oldWsdlUrl;
771        private final String newWsdlUrl;
772        private final String breakageMessage;
773
774        public VersionCompatibilityBreakage(MavenVersion oldMavenVersion, MavenVersion newMavenVersion, String oldWsdlUrl, String newWsdlUrl, String breakageMessage) {
775            if (oldMavenVersion == null) throw new IllegalArgumentException("oldMavenVersion must not be null");
776            if (newMavenVersion == null) throw new IllegalArgumentException("newMavenVersion must not be null");
777            if (StringUtils.isEmpty(oldWsdlUrl)) throw new IllegalArgumentException("oldWsdlUrl must not be empty/null");
778            if (StringUtils.isEmpty(newWsdlUrl)) throw new IllegalArgumentException("newWsdlUrl must not be empty/null");
779            if (StringUtils.isEmpty(breakageMessage)) throw new IllegalArgumentException("breakageMessage must not be empty/null");
780            this.oldWsdlUrl = oldWsdlUrl;
781            this.newWsdlUrl = newWsdlUrl;
782            this.oldMavenVersion = oldMavenVersion;
783            this.newMavenVersion = newMavenVersion;
784            this.breakageMessage = breakageMessage;
785        }
786
787        @Override
788        public String toString() {
789            return "VersionCompatibilityBreakage{" +
790                    "oldMavenVersion=" + oldMavenVersion +
791                    ", newMavenVersion=" + newMavenVersion +
792                    ", oldWsdlUrl='" + oldWsdlUrl + '\'' +
793                    ", newWsdlUrl='" + newWsdlUrl + '\'' +
794                    ", breakageMessage='" + breakageMessage + '\'' +
795                    '}';
796        }
797    }
798
799}