View Javadoc
1   /*
2    * Copyright 2006-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.kuali.rice.vc.test;
18  
19  import com.predic8.schema.ComplexType;
20  import com.predic8.schema.Sequence;
21  import com.predic8.soamodel.Difference;
22  import com.predic8.wsdl.Definitions;
23  import com.predic8.wsdl.Operation;
24  import com.predic8.wsdl.PortType;
25  import com.predic8.wsdl.WSDLParser;
26  import com.predic8.wsdl.diff.WsdlDiffGenerator;
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.log4j.Logger;
29  import org.codehaus.jackson.JsonNode;
30  import org.codehaus.jackson.map.ObjectMapper;
31  import org.kuali.rice.core.api.config.property.Config;
32  import org.kuali.rice.core.api.config.property.ConfigContext;
33  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
34  import org.kuali.rice.core.api.lifecycle.Lifecycle;
35  import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
36  import org.kuali.rice.test.BaselineTestCase;
37  
38  import javax.xml.namespace.QName;
39  import java.io.BufferedReader;
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.InputStreamReader;
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  import java.util.*;
46  import java.util.regex.Pattern;
47  
48  import static org.junit.Assert.assertTrue;
49  import static org.junit.Assert.fail;
50  
51  /*
52  *  Compatible Changes
53  *   - adding a new WSDL operation definition and associated message definitions
54  *   - adding a new WSDL port type definition and associated operation definitions
55  *   - adding new WSDL binding and service definitions
56  *   - adding a new optional XML Schema element or attribute declaration to a message definition
57  *   - reducing the constraint granularity of an XML Schema element or attribute of a message definition type
58  *   - adding a new XML Schema wildcard to a message definition type
59  *   - adding a new optional WS-Policy assertion
60  *   - adding a new WS-Policy alternative
61  *
62  * Incompatible Changes
63  *   - renaming an existing WSDL operation definition
64  *   - removing an existing WSDL operation definition
65  *   - changing the MEP of an existing WSDL operation definition
66  *   - adding a fault message to an existing WSDL operation definition
67  *   - adding a new required XML Schema element or attribute declaration to a message definition
68  *   - increasing the constraint granularity of an XML Schema element or attribute declaration of a message definition
69  *   - renaming an optional or required XML Schema element or attribute in a message definition
70  *   - removing an optional or required XML Schema element or attribute or wildcard from a message definition
71  *   - adding a new required WS-Policy assertion or expression
72  *   - adding a new ignorable WS-Policy expression (most of the time)
73  */
74  @BaselineTestCase.BaselineMode(BaselineTestCase.Mode.ROLLBACK)
75  public abstract class WsdlCompareTestCase extends BaselineTestCase {
76      private static final Logger LOG = Logger.getLogger(WsdlCompareTestCase.class);
77      private static final String WSDL_URL = "wsdl.test.previous.url";
78      private static final String WSDL_PREVIOUS_VERSION = "wsdl.test.previous.version";
79      private static final String LINE_SEPARATOR = System.getProperty("line.separator");
80  
81      private String previousVersion;
82  
83      private static final List<String> ignoreBreakageRegexps = Arrays.asList(
84              ".*Position of any changed from .*", // change in position of an 'any' doesn't indicate a breakage for us
85              ".*Position of element null changed.$" // this also indicates an 'any' changing position, ignore it too
86      );
87  
88      public WsdlCompareTestCase(String moduleName) {
89          super(moduleName);
90      }
91  
92      protected List<String> verifyWsdlDifferences(Difference diff, String level) {
93          List<String> results = new ArrayList<String>();
94  
95          if (diff.isBreaks()) {
96              boolean ignore = false;
97              for (String ignoreBreakageRegexp : ignoreBreakageRegexps) {
98                  if (diff.getDescription().matches(ignoreBreakageRegexp)) {
99                      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 }