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 }