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     */
016    package org.kuali.rice.vc.test;
017    
018    import com.predic8.schema.ComplexType;
019    import com.predic8.schema.Sequence;
020    import com.predic8.soamodel.Difference;
021    import com.predic8.wsdl.Definitions;
022    import com.predic8.wsdl.Operation;
023    import com.predic8.wsdl.PortType;
024    import com.predic8.wsdl.WSDLParser;
025    import com.predic8.wsdl.diff.WsdlDiffGenerator;
026    import org.apache.commons.lang.StringUtils;
027    import org.apache.log4j.Logger;
028    import org.codehaus.jackson.JsonNode;
029    import org.codehaus.jackson.map.ObjectMapper;
030    import org.kuali.rice.core.api.config.property.Config;
031    import org.kuali.rice.core.api.config.property.ConfigContext;
032    import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
033    import org.kuali.rice.core.api.lifecycle.Lifecycle;
034    import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
035    import org.kuali.rice.test.BaselineTestCase;
036    
037    import javax.xml.namespace.QName;
038    import java.io.BufferedReader;
039    import java.io.File;
040    import java.io.IOException;
041    import java.io.InputStreamReader;
042    import java.net.MalformedURLException;
043    import java.net.URL;
044    import java.util.*;
045    import java.util.regex.Pattern;
046    
047    import static org.junit.Assert.assertTrue;
048    import 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)
074    public 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    }