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