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