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