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}