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