View Javadoc

1   /**
2    * Copyright 2005-2013 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  package org.kuali.rice.vc.test;
17  
18  import com.predic8.schema.ComplexType;
19  import com.predic8.schema.Sequence;
20  import com.predic8.soamodel.Difference;
21  import com.predic8.wsdl.Definitions;
22  import com.predic8.wsdl.Operation;
23  import com.predic8.wsdl.PortType;
24  import com.predic8.wsdl.WSDLParser;
25  import com.predic8.wsdl.diff.WsdlDiffGenerator;
26  import org.apache.commons.lang.StringUtils;
27  import org.apache.log4j.Logger;
28  import org.codehaus.jackson.JsonNode;
29  import org.codehaus.jackson.map.ObjectMapper;
30  import org.kuali.rice.core.api.config.property.Config;
31  import org.kuali.rice.core.api.config.property.ConfigContext;
32  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
33  import org.kuali.rice.core.api.lifecycle.Lifecycle;
34  import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
35  import org.kuali.rice.test.BaselineTestCase;
36  
37  import javax.xml.namespace.QName;
38  import java.io.BufferedReader;
39  import java.io.File;
40  import java.io.IOException;
41  import java.io.InputStreamReader;
42  import java.net.MalformedURLException;
43  import java.net.URL;
44  import java.util.*;
45  import java.util.regex.Pattern;
46  
47  import static org.junit.Assert.assertTrue;
48  import static org.junit.Assert.fail;
49  
50  /*
51  *  Compatible Changes
52  *   - adding a new WSDL operation definition and associated message definitions
53  *   - adding a new WSDL port type definition and associated operation definitions
54  *   - adding new WSDL binding and service definitions
55  *   - adding a new optional XML Schema element or attribute declaration to a message definition
56  *   - reducing the constraint granularity of an XML Schema element or attribute of a message definition type
57  *   - adding a new XML Schema wildcard to a message definition type
58  *   - adding a new optional WS-Policy assertion
59  *   - adding a new WS-Policy alternative
60  *
61  * Incompatible Changes
62  *   - renaming an existing WSDL operation definition
63  *   - removing an existing WSDL operation definition
64  *   - changing the MEP of an existing WSDL operation definition
65  *   - adding a fault message to an existing WSDL operation definition
66  *   - adding a new required XML Schema element or attribute declaration to a message definition
67  *   - increasing the constraint granularity of an XML Schema element or attribute declaration of a message definition
68  *   - renaming an optional or required XML Schema element or attribute in a message definition
69  *   - removing an optional or required XML Schema element or attribute or wildcard from a message definition
70  *   - adding a new required WS-Policy assertion or expression
71  *   - adding a new ignorable WS-Policy expression (most of the time)
72  */
73  @BaselineTestCase.BaselineMode(BaselineTestCase.Mode.ROLLBACK)
74  public abstract class WsdlCompareTestCase extends BaselineTestCase {
75      private static final Logger LOG = Logger.getLogger(WsdlCompareTestCase.class);
76      private static final String WSDL_URL = "wsdl.test.previous.url";
77      private static final String WSDL_PREVIOUS_VERSION = "wsdl.test.previous.version";
78      private static final String LINE_SEPARATOR = System.getProperty("line.separator");
79  
80      private String previousVersion;
81  
82      private static final List<String> ignoreBreakageRegexps = Arrays.asList(
83              ".*Position of any changed from .*", // change in position of an 'any' doesn't indicate a breakage for us
84              ".*Position of element null changed.$" // this also indicates an 'any' changing position, ignore it too
85      );
86  
87      public WsdlCompareTestCase(String moduleName) {
88          super(moduleName);
89      }
90  
91      protected List<String> verifyWsdlDifferences(Difference diff, String level) {
92          List<String> results = new ArrayList<String>();
93  
94          if (diff.isBreaks()) {
95              boolean ignore = false;
96              for (String ignoreBreakageRegexp : ignoreBreakageRegexps) {
97                  if (diff.getDescription().matches(ignoreBreakageRegexp)) {
98                      ignore = true;
99                      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 }