001/** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.vc.test; 017 018import com.predic8.schema.ComplexType; 019import com.predic8.schema.Sequence; 020import com.predic8.soamodel.Difference; 021import com.predic8.wsdl.Definitions; 022import com.predic8.wsdl.Operation; 023import com.predic8.wsdl.PortType; 024import com.predic8.wsdl.WSDLParser; 025import com.predic8.wsdl.diff.WsdlDiffGenerator; 026import org.apache.commons.collections.CollectionUtils; 027import org.apache.commons.lang.StringUtils; 028import org.apache.log4j.Logger; 029import org.codehaus.jackson.JsonNode; 030import org.codehaus.jackson.map.ObjectMapper; 031import org.kuali.rice.core.api.config.property.Config; 032import org.kuali.rice.core.api.config.property.ConfigContext; 033import org.kuali.rice.core.api.lifecycle.BaseLifecycle; 034import org.kuali.rice.core.api.lifecycle.Lifecycle; 035import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader; 036import org.kuali.rice.test.BaselineTestCase; 037 038import javax.xml.namespace.QName; 039import java.io.BufferedReader; 040import java.io.File; 041import java.io.IOException; 042import java.io.InputStreamReader; 043import java.net.MalformedURLException; 044import java.net.URL; 045import java.util.*; 046import java.util.regex.Pattern; 047 048import static org.junit.Assert.assertTrue; 049import static org.junit.Assert.fail; 050 051/* 052* Compatible Changes 053* - adding a new WSDL operation definition and associated message definitions 054* - adding a new WSDL port type definition and associated operation definitions 055* - adding new WSDL binding and service definitions 056* - adding a new optional XML Schema element or attribute declaration to a message definition 057* - reducing the constraint granularity of an XML Schema element or attribute of a message definition type 058* - adding a new XML Schema wildcard to a message definition type 059* - adding a new optional WS-Policy assertion 060* - adding a new WS-Policy alternative 061* 062* Incompatible Changes 063* - renaming an existing WSDL operation definition 064* - removing an existing WSDL operation definition 065* - changing the MEP of an existing WSDL operation definition 066* - adding a fault message to an existing WSDL operation definition 067* - adding a new required XML Schema element or attribute declaration to a message definition 068* - increasing the constraint granularity of an XML Schema element or attribute declaration of a message definition 069* - renaming an optional or required XML Schema element or attribute in a message definition 070* - removing an optional or required XML Schema element or attribute or wildcard from a message definition 071* - adding a new required WS-Policy assertion or expression 072* - adding a new ignorable WS-Policy expression (most of the time) 073*/ 074@BaselineTestCase.BaselineMode(BaselineTestCase.Mode.ROLLBACK) 075public abstract class WsdlCompareTestCase extends BaselineTestCase { 076 private static final Logger LOG = Logger.getLogger(WsdlCompareTestCase.class); 077 private static final String WSDL_URL = "wsdl.test.previous.url"; 078 private static final String WSDL_PREVIOUS_VERSION = "wsdl.test.previous.version"; 079 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 080 081 private String previousVersion; 082 083 private static final List<String> ignoreBreakageRegexps = Arrays.asList( 084 ".*Position of any changed from .*", // change in position of an 'any' doesn't indicate a breakage for us 085 ".*Position of element null changed.$", // this also indicates an 'any' changing position, ignore it too 086 " *ComplexType [^ ]* removed.$", // If a ComplexType that is unused is removed, it isn't a VC breakage 087 088 " *Element [^ ]* removed.$", // If a simpleType that is unused is removed, it isn't a VC breakage, but 089 " *SimpleType [^ ]* removed.$" // it produces both of these errors 090 ); 091 092 public WsdlCompareTestCase(String moduleName) { 093 super(moduleName); 094 } 095 096 protected List<String> verifyWsdlDifferences(Difference diff, String level) { 097 List<String> results = new ArrayList<String>(); 098 099 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}