blob: 02c9e9d6e7b010fbc3f4bc59cd07ea98542e2303 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.validation;
import org.apache.ivy.Ivy;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.report.ResolveReport;
import org.apache.ivy.core.resolve.ResolveOptions;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.plugins.conflict.NoConflictManager;
import org.apache.lucene.dependencies.InterpolatedProperties;
import org.apache.lucene.validation.ivyde.IvyNodeElement;
import org.apache.lucene.validation.ivyde.IvyNodeElementAdapter;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.Resources;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An Ant task to verify that the '/org/name' keys in ivy-versions.properties
* are sorted lexically and are neither duplicates nor orphans, and that all
* dependencies in all ivy.xml files use rev="${/org/name}" format.
*/
public class LibVersionsCheckTask extends Task {
private static final String IVY_XML_FILENAME = "ivy.xml";
private static final Pattern COORDINATE_KEY_PATTERN = Pattern.compile("(/([^/ \t\f]+)/([^=:/ \t\f]+))");
private static final Pattern BLANK_OR_COMMENT_LINE_PATTERN = Pattern.compile("[ \t\f]*(?:[#!].*)?");
private static final Pattern TRAILING_BACKSLASH_PATTERN = Pattern.compile("[^\\\\]*(\\\\+)$");
private static final Pattern LEADING_WHITESPACE_PATTERN = Pattern.compile("[ \t\f]+(.*)");
private static final Pattern WHITESPACE_GOODSTUFF_WHITESPACE_BACKSLASH_PATTERN
= Pattern.compile("[ \t\f]*(.*?)(?:(?<!\\\\)[ \t\f]*)?\\\\");
private static final Pattern TRAILING_WHITESPACE_BACKSLASH_PATTERN
= Pattern.compile("(.*?)(?:(?<!\\\\)[ \t\f]*)?\\\\");
private static final Pattern MODULE_NAME_PATTERN = Pattern.compile("\\smodule\\s*=\\s*[\"']([^\"']+)[\"']");
private static final Pattern MODULE_DIRECTORY_PATTERN
= Pattern.compile(".*[/\\\\]((?:lucene|solr)[/\\\\].*)[/\\\\].*");
private Ivy ivy;
/**
* All ivy.xml files to check.
*/
private Resources ivyXmlResources = new Resources();
/**
* Centralized Ivy versions properties file: ivy-versions.properties
*/
private File centralizedVersionsFile;
/**
* Centralized Ivy ignore conflicts file: ivy-ignore-conflicts.properties
*/
private File ignoreConflictsFile;
/**
* Ivy settings file: top-level-ivy-settings.xml
*/
private File topLevelIvySettingsFile;
/**
* Location of common build dir: lucene/build/
*/
private File commonBuildDir;
/**
* Location of ivy cache resolution directory.
*/
private File ivyResolutionCacheDir;
/**
* Artifact lock strategy that Ivy should use.
*/
private String ivyLockStrategy;
/**
* A logging level associated with verbose logging.
*/
private int verboseLevel = Project.MSG_VERBOSE;
/**
* All /org/name keys found in ivy-versions.properties,
* mapped to info about direct dependence and what would
* be conflicting indirect dependencies if Lucene/Solr
* were to use transitive dependencies.
*/
private Map<String,Dependency> directDependencies = new LinkedHashMap<>();
/**
* All /org/name keys found in ivy-ignore-conflicts.properties,
* mapped to the set of indirect dependency versions that will
* be ignored, i.e. not trigger a conflict.
*/
private Map<String,HashSet<String>> ignoreConflictVersions = new HashMap<>();
private static class Dependency {
String org;
String name;
String directVersion;
String latestVersion;
boolean directlyReferenced = false;
LinkedHashMap<IvyNodeElement,Set<String>> conflictLocations = new LinkedHashMap<>(); // dependency path -> moduleNames
Dependency(String org, String name, String directVersion) {
this.org = org;
this.name = name;
this.directVersion = directVersion;
}
}
/**
* Adds a set of ivy.xml resources to check.
*/
public void add(ResourceCollection rc) {
ivyXmlResources.add(rc);
}
public void setVerbose(boolean verbose) {
verboseLevel = (verbose ? Project.MSG_INFO : Project.MSG_VERBOSE);
}
public void setCentralizedVersionsFile(File file) {
centralizedVersionsFile = file;
}
public void setTopLevelIvySettingsFile(File file) {
topLevelIvySettingsFile = file;
}
public void setIvyResolutionCacheDir(File dir) {
ivyResolutionCacheDir = dir;
}
public void setIvyLockStrategy(String strategy) {
this.ivyLockStrategy = strategy;
}
public void setCommonBuildDir(File file) {
commonBuildDir = file;
}
public void setIgnoreConflictsFile(File file) {
ignoreConflictsFile = file;
}
/**
* Execute the task.
*/
@Override
public void execute() throws BuildException {
log("Starting scan.", verboseLevel);
long start = System.currentTimeMillis();
setupIvy();
int numErrors = 0;
if ( ! verifySortedCoordinatesPropertiesFile(centralizedVersionsFile)) {
++numErrors;
}
if ( ! verifySortedCoordinatesPropertiesFile(ignoreConflictsFile)) {
++numErrors;
}
collectDirectDependencies();
if ( ! collectVersionConflictsToIgnore()) {
++numErrors;
}
int numChecked = 0;
@SuppressWarnings("unchecked")
Iterator<Resource> iter = (Iterator<Resource>)ivyXmlResources.iterator();
while (iter.hasNext()) {
final Resource resource = iter.next();
if ( ! resource.isExists()) {
throw new BuildException("Resource does not exist: " + resource.getName());
}
if ( ! (resource instanceof FileResource)) {
throw new BuildException("Only filesystem resources are supported: "
+ resource.getName() + ", was: " + resource.getClass().getName());
}
File ivyXmlFile = ((FileResource)resource).getFile();
try {
if ( ! checkIvyXmlFile(ivyXmlFile)) {
++numErrors;
}
if ( ! resolveTransitively(ivyXmlFile)) {
++numErrors;
}
if ( ! findLatestConflictVersions()) {
++numErrors;
}
} catch (Exception e) {
throw new BuildException("Exception reading file " + ivyXmlFile.getPath() + " - " + e.toString(), e);
}
++numChecked;
}
log("Checking for orphans in " + centralizedVersionsFile.getName(), verboseLevel);
for (Map.Entry<String,Dependency> entry : directDependencies.entrySet()) {
String coordinateKey = entry.getKey();
if ( ! entry.getValue().directlyReferenced) {
log("ORPHAN coordinate key '" + coordinateKey + "' in " + centralizedVersionsFile.getName()
+ " is not found in any " + IVY_XML_FILENAME + " file.",
Project.MSG_ERR);
++numErrors;
}
}
int numConflicts = emitConflicts();
int messageLevel = numErrors > 0 ? Project.MSG_ERR : Project.MSG_INFO;
log("Checked that " + centralizedVersionsFile.getName() + " and " + ignoreConflictsFile.getName()
+ " have lexically sorted '/org/name' keys and no duplicates or orphans.",
messageLevel);
log("Scanned " + numChecked + " " + IVY_XML_FILENAME + " files for rev=\"${/org/name}\" format.",
messageLevel);
log("Found " + numConflicts + " indirect dependency version conflicts.");
log(String.format(Locale.ROOT, "Completed in %.2fs., %d error(s).",
(System.currentTimeMillis() - start) / 1000.0, numErrors),
messageLevel);
if (numConflicts > 0 || numErrors > 0) {
throw new BuildException("Lib versions check failed. Check the logs.");
}
}
private boolean findLatestConflictVersions() {
boolean success = true;
StringBuilder latestIvyXml = new StringBuilder();
latestIvyXml.append("<ivy-module version=\"2.0\">\n");
latestIvyXml.append(" <info organisation=\"org.apache.lucene\" module=\"core-tools-find-latest-revision\"/>\n");
latestIvyXml.append(" <configurations>\n");
latestIvyXml.append(" <conf name=\"default\" transitive=\"false\"/>\n");
latestIvyXml.append(" </configurations>\n");
latestIvyXml.append(" <dependencies>\n");
for (Map.Entry<String, Dependency> directDependency : directDependencies.entrySet()) {
Dependency dependency = directDependency.getValue();
if (dependency.conflictLocations.entrySet().isEmpty()) {
continue;
}
latestIvyXml.append(" <dependency org=\"");
latestIvyXml.append(dependency.org);
latestIvyXml.append("\" name=\"");
latestIvyXml.append(dependency.name);
latestIvyXml.append("\" rev=\"latest.release\" conf=\"default->*\"/>\n");
}
latestIvyXml.append(" </dependencies>\n");
latestIvyXml.append("</ivy-module>\n");
File buildDir = new File(commonBuildDir, "ivy-transitive-resolve");
if ( ! buildDir.exists() && ! buildDir.mkdirs()) {
throw new BuildException("Could not create temp directory " + buildDir.getPath());
}
File findLatestIvyXmlFile = new File(buildDir, "find.latest.conflicts.ivy.xml");
try {
try (Writer writer = new OutputStreamWriter(new FileOutputStream(findLatestIvyXmlFile), StandardCharsets.UTF_8)) {
writer.write(latestIvyXml.toString());
}
ResolveOptions options = new ResolveOptions();
options.setDownload(false); // Download only module descriptors, not artifacts
options.setTransitive(false); // Resolve only direct dependencies
options.setUseCacheOnly(false); // Download the internet!
options.setOutputReport(false); // Don't print to the console
options.setLog(LogOptions.LOG_QUIET); // Don't log to the console
options.setConfs(new String[] {"*"}); // Resolve all configurations
ResolveReport resolveReport = ivy.resolve(findLatestIvyXmlFile.toURI().toURL(), options);
IvyNodeElement root = IvyNodeElementAdapter.adapt(resolveReport);
for (IvyNodeElement element : root.getDependencies()) {
String coordinate = "/" + element.getOrganization() + "/" + element.getName();
Dependency dependency = directDependencies.get(coordinate);
if (null == dependency) {
log("ERROR: the following coordinate key does not appear in "
+ centralizedVersionsFile.getName() + ": " + coordinate, Project.MSG_ERR);
success = false;
} else {
dependency.latestVersion = element.getRevision();
}
}
} catch (IOException e) {
log("Exception writing to " + findLatestIvyXmlFile.getPath() + ": " + e.toString(), Project.MSG_ERR);
success = false;
} catch (ParseException e) {
log("Exception parsing filename " + findLatestIvyXmlFile.getPath() + ": " + e.toString(), Project.MSG_ERR);
success = false;
}
return success;
}
/**
* Collects indirect dependency version conflicts to ignore
* in ivy-ignore-conflicts.properties, and also checks for orphans
* (coordinates not included in ivy-versions.properties).
*
* Returns true if no orphans are found.
*/
private boolean collectVersionConflictsToIgnore() {
log("Checking for orphans in " + ignoreConflictsFile.getName(), verboseLevel);
boolean orphansFound = false;
InterpolatedProperties properties = new InterpolatedProperties();
try (InputStream inputStream = new FileInputStream(ignoreConflictsFile);
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (IOException e) {
throw new BuildException("Exception reading " + ignoreConflictsFile + ": " + e.toString(), e);
}
for (Object obj : properties.keySet()) {
String coordinate = (String)obj;
if (COORDINATE_KEY_PATTERN.matcher(coordinate).matches()) {
if ( ! directDependencies.containsKey(coordinate)) {
orphansFound = true;
log("ORPHAN coordinate key '" + coordinate + "' in " + ignoreConflictsFile.getName()
+ " is not found in " + centralizedVersionsFile.getName(),
Project.MSG_ERR);
} else {
String versionsToIgnore = properties.getProperty(coordinate);
List<String> ignore = Arrays.asList(versionsToIgnore.trim().split("\\s*,\\s*|\\s+"));
ignoreConflictVersions.put(coordinate, new HashSet<>(ignore));
}
}
}
return ! orphansFound;
}
private void collectDirectDependencies() {
InterpolatedProperties properties = new InterpolatedProperties();
try (InputStream inputStream = new FileInputStream(centralizedVersionsFile);
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
properties.load(reader);
} catch (IOException e) {
throw new BuildException("Exception reading " + centralizedVersionsFile + ": " + e.toString(), e);
}
for (Object obj : properties.keySet()) {
String coordinate = (String)obj;
Matcher matcher = COORDINATE_KEY_PATTERN.matcher(coordinate);
if (matcher.matches()) {
String org = matcher.group(2);
String name = matcher.group(3);
String directVersion = properties.getProperty(coordinate);
Dependency dependency = new Dependency(org, name, directVersion);
directDependencies.put(coordinate, dependency);
}
}
}
/**
* Transitively resolves all dependencies in the given ivy.xml file,
* looking for indirect dependencies with versions that conflict
* with those of direct dependencies. Dependency conflict when a
* direct dependency's version is older than that of an indirect
* dependency with the same /org/name.
*
* Returns true if no version conflicts are found and no resolution
* errors occurred, false otherwise.
*/
private boolean resolveTransitively(File ivyXmlFile) {
boolean success = true;
ResolveOptions options = new ResolveOptions();
options.setDownload(false); // Download only module descriptors, not artifacts
options.setTransitive(true); // Resolve transitively, if not already specified in the ivy.xml file
options.setUseCacheOnly(false); // Download the internet!
options.setOutputReport(false); // Don't print to the console
options.setLog(LogOptions.LOG_QUIET); // Don't log to the console
options.setConfs(new String[] {"*"}); // Resolve all configurations
// Rewrite the ivy.xml, replacing all 'transitive="false"' with 'transitive="true"'
// The Ivy API is file-based, so we have to write the result to the filesystem.
String moduleName = "unknown";
String ivyXmlContent = xmlToString(ivyXmlFile);
Matcher matcher = MODULE_NAME_PATTERN.matcher(ivyXmlContent);
if (matcher.find()) {
moduleName = matcher.group(1);
}
ivyXmlContent = ivyXmlContent.replaceAll("\\btransitive\\s*=\\s*[\"']false[\"']", "transitive=\"true\"");
File transitiveIvyXmlFile = null;
try {
File buildDir = new File(commonBuildDir, "ivy-transitive-resolve");
if ( ! buildDir.exists() && ! buildDir.mkdirs()) {
throw new BuildException("Could not create temp directory " + buildDir.getPath());
}
matcher = MODULE_DIRECTORY_PATTERN.matcher(ivyXmlFile.getCanonicalPath());
if ( ! matcher.matches()) {
throw new BuildException("Unknown ivy.xml module directory: " + ivyXmlFile.getCanonicalPath());
}
String moduleDirPrefix = matcher.group(1).replaceAll("[/\\\\]", ".");
transitiveIvyXmlFile = new File(buildDir, "transitive." + moduleDirPrefix + ".ivy.xml");
try (Writer writer = new OutputStreamWriter(new FileOutputStream(transitiveIvyXmlFile), StandardCharsets.UTF_8)) {
writer.write(ivyXmlContent);
}
ResolveReport resolveReport = ivy.resolve(transitiveIvyXmlFile.toURI().toURL(), options);
IvyNodeElement root = IvyNodeElementAdapter.adapt(resolveReport);
for (IvyNodeElement directDependency : root.getDependencies()) {
String coordinate = "/" + directDependency.getOrganization() + "/" + directDependency.getName();
Dependency dependency = directDependencies.get(coordinate);
if (null == dependency) {
log("ERROR: the following coordinate key does not appear in "
+ centralizedVersionsFile.getName() + ": " + coordinate);
success = false;
} else {
dependency.directlyReferenced = true;
if (collectConflicts(directDependency, directDependency, moduleName)) {
success = false;
}
}
}
} catch (ParseException | IOException e) {
if (null != transitiveIvyXmlFile) {
log("Exception reading " + transitiveIvyXmlFile.getPath() + ": " + e.toString());
}
success = false;
}
return success;
}
/**
* Recursively finds indirect dependencies that have a version conflict with a direct dependency.
* Returns true if one or more conflicts are found, false otherwise
*/
private boolean collectConflicts(IvyNodeElement root, IvyNodeElement parent, String moduleName) {
boolean conflicts = false;
for (IvyNodeElement child : parent.getDependencies()) {
String coordinate = "/" + child.getOrganization() + "/" + child.getName();
Dependency dependency = directDependencies.get(coordinate);
if (null != dependency) { // Ignore this indirect dependency if it's not also a direct dependency
String indirectVersion = child.getRevision();
if (isConflict(coordinate, dependency.directVersion, indirectVersion)) {
conflicts = true;
Set<String> moduleNames = dependency.conflictLocations.get(root);
if (null == moduleNames) {
moduleNames = new HashSet<>();
dependency.conflictLocations.put(root, moduleNames);
}
moduleNames.add(moduleName);
}
conflicts |= collectConflicts(root, child, moduleName);
}
}
return conflicts;
}
/**
* Copy-pasted from Ivy's
* org.apache.ivy.plugins.latest.LatestRevisionStrategy
* with minor modifications
*/
private static final Map<String,Integer> SPECIAL_MEANINGS;
static {
SPECIAL_MEANINGS = new HashMap<>();
SPECIAL_MEANINGS.put("dev", -1);
SPECIAL_MEANINGS.put("rc", 1);
SPECIAL_MEANINGS.put("final", 2);
}
/**
* Copy-pasted from Ivy's
* org.apache.ivy.plugins.latest.LatestRevisionStrategy.MridComparator
* with minor modifications
*/
private static class LatestVersionComparator implements Comparator<String> {
@Override
public int compare(String rev1, String rev2) {
rev1 = rev1.replaceAll("([a-zA-Z])(\\d)", "$1.$2");
rev1 = rev1.replaceAll("(\\d)([a-zA-Z])", "$1.$2");
rev2 = rev2.replaceAll("([a-zA-Z])(\\d)", "$1.$2");
rev2 = rev2.replaceAll("(\\d)([a-zA-Z])", "$1.$2");
String[] parts1 = rev1.split("[-._+]");
String[] parts2 = rev2.split("[-._+]");
int i = 0;
for (; i < parts1.length && i < parts2.length; i++) {
if (parts1[i].equals(parts2[i])) {
continue;
}
boolean is1Number = isNumber(parts1[i]);
boolean is2Number = isNumber(parts2[i]);
if (is1Number && !is2Number) {
return 1;
}
if (is2Number && !is1Number) {
return -1;
}
if (is1Number && is2Number) {
return Long.valueOf(parts1[i]).compareTo(Long.valueOf(parts2[i]));
}
// both are strings, we compare them taking into account special meaning
Integer sm1 = SPECIAL_MEANINGS.get(parts1[i].toLowerCase(Locale.ROOT));
Integer sm2 = SPECIAL_MEANINGS.get(parts2[i].toLowerCase(Locale.ROOT));
if (sm1 != null) {
sm2 = sm2 == null ? 0 : sm2;
return sm1.compareTo(sm2);
}
if (sm2 != null) {
return Integer.valueOf(0).compareTo(sm2);
}
return parts1[i].compareTo(parts2[i]);
}
if (i < parts1.length) {
return isNumber(parts1[i]) ? 1 : -1;
}
if (i < parts2.length) {
return isNumber(parts2[i]) ? -1 : 1;
}
return 0;
}
private static final Pattern IS_NUMBER = Pattern.compile("\\d+");
private static boolean isNumber(String str) {
return IS_NUMBER.matcher(str).matches();
}
}
private static LatestVersionComparator LATEST_VERSION_COMPARATOR = new LatestVersionComparator();
/**
* Returns true if directVersion is less than indirectVersion, and
* coordinate=indirectVersion is not present in ivy-ignore-conflicts.properties.
*/
private boolean isConflict(String coordinate, String directVersion, String indirectVersion) {
boolean isConflict = LATEST_VERSION_COMPARATOR.compare(directVersion, indirectVersion) < 0;
if (isConflict) {
Set<String> ignoredVersions = ignoreConflictVersions.get(coordinate);
if (null != ignoredVersions && ignoredVersions.contains(indirectVersion)) {
isConflict = false;
}
}
return isConflict;
}
/**
* Returns the number of direct dependencies in conflict with indirect
* dependencies.
*/
private int emitConflicts() {
int conflicts = 0;
StringBuilder builder = new StringBuilder();
for (Map.Entry<String,Dependency> directDependency : directDependencies.entrySet()) {
String coordinate = directDependency.getKey();
Set<Map.Entry<IvyNodeElement,Set<String>>> entrySet
= directDependency.getValue().conflictLocations.entrySet();
if (entrySet.isEmpty()) {
continue;
}
++conflicts;
Map.Entry<IvyNodeElement,Set<String>> first = entrySet.iterator().next();
int notPrinted = entrySet.size() - 1;
builder.append("VERSION CONFLICT: transitive dependency in module(s) ");
boolean isFirst = true;
for (String moduleName : first.getValue()) {
if (isFirst) {
isFirst = false;
} else {
builder.append(", ");
}
builder.append(moduleName);
}
builder.append(":\n");
IvyNodeElement element = first.getKey();
builder.append('/').append(element.getOrganization()).append('/').append(element.getName())
.append('=').append(element.getRevision()).append('\n');
emitConflict(builder, coordinate, first.getKey(), 1);
if (notPrinted > 0) {
builder.append("... and ").append(notPrinted).append(" more\n");
}
builder.append("\n");
}
if (builder.length() > 0) {
log(builder.toString());
}
return conflicts;
}
private boolean emitConflict(StringBuilder builder, String conflictCoordinate, IvyNodeElement parent, int depth) {
for (IvyNodeElement child : parent.getDependencies()) {
String indirectCoordinate = "/" + child.getOrganization() + "/" + child.getName();
if (conflictCoordinate.equals(indirectCoordinate)) {
Dependency dependency = directDependencies.get(conflictCoordinate);
String directVersion = dependency.directVersion;
if (isConflict(conflictCoordinate, directVersion, child.getRevision())) {
for (int i = 0 ; i < depth - 1 ; ++i) {
builder.append(" ");
}
builder.append("+-- ");
builder.append(indirectCoordinate).append("=").append(child.getRevision());
builder.append(" <<< Conflict (direct=").append(directVersion);
builder.append(", latest=").append(dependency.latestVersion).append(")\n");
return true;
}
} else if (hasConflicts(conflictCoordinate, child)) {
for (int i = 0 ; i < depth -1 ; ++i) {
builder.append(" ");
}
builder.append("+-- ");
builder.append(indirectCoordinate).append("=").append(child.getRevision()).append("\n");
if (emitConflict(builder, conflictCoordinate, child, depth + 1)) {
return true;
}
}
}
return false;
}
private boolean hasConflicts(String conflictCoordinate, IvyNodeElement parent) {
// the element itself will never be in conflict, since its coordinate is different
for (IvyNodeElement child : parent.getDependencies()) {
String indirectCoordinate = "/" + child.getOrganization() + "/" + child.getName();
if (conflictCoordinate.equals(indirectCoordinate)) {
Dependency dependency = directDependencies.get(conflictCoordinate);
if (isConflict(conflictCoordinate, dependency.directVersion, child.getRevision())) {
return true;
}
} else if (hasConflicts(conflictCoordinate, child)) {
return true;
}
}
return false;
}
private String xmlToString(File ivyXmlFile) {
StringWriter writer = new StringWriter();
try {
StreamSource inputSource = new StreamSource(new FileInputStream(ivyXmlFile.getPath()));
Transformer serializer = TransformerFactory.newInstance().newTransformer();
serializer.transform(inputSource, new StreamResult(writer));
} catch (TransformerException | IOException e) {
throw new BuildException("Exception reading " + ivyXmlFile.getPath() + ": " + e.toString(), e);
}
return writer.toString();
}
private void setupIvy() {
IvySettings ivySettings = new IvySettings();
try {
ivySettings.setVariable("common.build.dir", commonBuildDir.getAbsolutePath());
ivySettings.setVariable("ivy.exclude.types", "source|javadoc");
ivySettings.setVariable("ivy.resolution-cache.dir", ivyResolutionCacheDir.getAbsolutePath());
ivySettings.setVariable("ivy.lock-strategy", ivyLockStrategy);
ivySettings.setVariable("ivysettings.xml", getProject().getProperty("ivysettings.xml")); // nested settings file
ivySettings.setBaseDir(commonBuildDir);
ivySettings.setDefaultConflictManager(new NoConflictManager());
ivy = Ivy.newInstance(ivySettings);
ivy.configure(topLevelIvySettingsFile);
} catch (Exception e) {
throw new BuildException("Exception reading " + topLevelIvySettingsFile.getPath() + ": " + e.toString(), e);
}
}
/**
* Returns true if the "/org/name" coordinate keys in the given
* properties file are lexically sorted and are not duplicates.
*/
private boolean verifySortedCoordinatesPropertiesFile(File coordinatePropertiesFile) {
log("Checking for lexically sorted non-duplicated '/org/name' keys in: " + coordinatePropertiesFile, verboseLevel);
boolean success = true;
String line = null;
String currentKey = null;
String previousKey = null;
try (InputStream stream = new FileInputStream(coordinatePropertiesFile);
Reader reader = new InputStreamReader(stream, StandardCharsets.ISO_8859_1);
BufferedReader bufferedReader = new BufferedReader(reader)) {
while (null != (line = readLogicalPropertiesLine(bufferedReader))) {
final Matcher keyMatcher = COORDINATE_KEY_PATTERN.matcher(line);
if ( ! keyMatcher.lookingAt()) {
continue; // Ignore keys that don't look like "/org/name"
}
currentKey = keyMatcher.group(1);
if (null != previousKey) {
int comparison = currentKey.compareTo(previousKey);
if (0 == comparison) {
log("DUPLICATE coordinate key '" + currentKey + "' in " + coordinatePropertiesFile.getName(),
Project.MSG_ERR);
success = false;
} else if (comparison < 0) {
log("OUT-OF-ORDER coordinate key '" + currentKey + "' in " + coordinatePropertiesFile.getName(),
Project.MSG_ERR);
success = false;
}
}
previousKey = currentKey;
}
} catch (IOException e) {
throw new BuildException("Exception reading " + coordinatePropertiesFile.getPath() + ": " + e.toString(), e);
}
return success;
}
/**
* Builds up logical {@link java.util.Properties} lines, composed of one non-blank,
* non-comment initial line, either:
*
* 1. without a non-escaped trailing slash; or
* 2. with a non-escaped trailing slash, followed by
* zero or more lines with a non-escaped trailing slash, followed by
* one or more lines without a non-escaped trailing slash
*
* All leading non-escaped whitespace and trailing non-escaped whitespace +
* non-escaped slash are trimmed from each line before concatenating.
*
* After composing the logical line, escaped characters are un-escaped.
*
* null is returned if there are no lines left to read.
*/
private String readLogicalPropertiesLine(BufferedReader reader) throws IOException {
final StringBuilder logicalLine = new StringBuilder();
String line;
do {
line = reader.readLine();
if (null == line) {
return null;
}
} while (BLANK_OR_COMMENT_LINE_PATTERN.matcher(line).matches());
Matcher backslashMatcher = TRAILING_BACKSLASH_PATTERN.matcher(line);
// Check for a non-escaped backslash
if (backslashMatcher.find() && 1 == (backslashMatcher.group(1).length() % 2)) {
final Matcher firstLineMatcher = TRAILING_WHITESPACE_BACKSLASH_PATTERN.matcher(line);
if (firstLineMatcher.matches()) {
logicalLine.append(firstLineMatcher.group(1)); // trim trailing backslash and any preceding whitespace
}
line = reader.readLine();
while (null != line
&& (backslashMatcher = TRAILING_BACKSLASH_PATTERN.matcher(line)).find()
&& 1 == (backslashMatcher.group(1).length() % 2)) {
// Trim leading whitespace, the trailing backslash and any preceding whitespace
final Matcher goodStuffMatcher = WHITESPACE_GOODSTUFF_WHITESPACE_BACKSLASH_PATTERN.matcher(line);
if (goodStuffMatcher.matches()) {
logicalLine.append(goodStuffMatcher.group(1));
}
line = reader.readLine();
}
if (null != line) {
// line can't have a non-escaped trailing backslash
final Matcher leadingWhitespaceMatcher = LEADING_WHITESPACE_PATTERN.matcher(line);
if (leadingWhitespaceMatcher.matches()) {
line = leadingWhitespaceMatcher.group(1); // trim leading whitespace
}
logicalLine.append(line);
}
} else {
logicalLine.append(line);
}
// trim non-escaped leading whitespace
final Matcher leadingWhitespaceMatcher = LEADING_WHITESPACE_PATTERN.matcher(logicalLine);
final CharSequence leadingWhitespaceStripped = leadingWhitespaceMatcher.matches()
? leadingWhitespaceMatcher.group(1)
: logicalLine;
// unescape all chars in the logical line
StringBuilder output = new StringBuilder();
final int numChars = leadingWhitespaceStripped.length();
for (int pos = 0 ; pos < numChars - 1 ; ++pos) {
char ch = leadingWhitespaceStripped.charAt(pos);
if (ch == '\\') {
ch = leadingWhitespaceStripped.charAt(++pos);
}
output.append(ch);
}
if (numChars > 0) {
output.append(leadingWhitespaceStripped.charAt(numChars - 1));
}
return output.toString();
}
/**
* Check a single ivy.xml file for dependencies' versions in rev="${/org/name}"
* format. Returns false if problems are found, true otherwise.
*/
private boolean checkIvyXmlFile(File ivyXmlFile)
throws ParserConfigurationException, SAXException, IOException {
log("Scanning: " + ivyXmlFile.getPath(), verboseLevel);
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
DependencyRevChecker revChecker = new DependencyRevChecker(ivyXmlFile);
xmlReader.setContentHandler(revChecker);
xmlReader.setErrorHandler(revChecker);
xmlReader.parse(new InputSource(ivyXmlFile.getAbsolutePath()));
return ! revChecker.fail;
}
private class DependencyRevChecker extends DefaultHandler {
private final File ivyXmlFile;
private final Stack<String> tags = new Stack<>();
public boolean fail = false;
public DependencyRevChecker(File ivyXmlFile) {
this.ivyXmlFile = ivyXmlFile;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (localName.equals("dependency") && insideDependenciesTag()) {
String org = attributes.getValue("org");
boolean foundAllAttributes = true;
if (null == org) {
log("MISSING 'org' attribute on <dependency> in " + ivyXmlFile.getPath(), Project.MSG_ERR);
fail = true;
foundAllAttributes = false;
}
String name = attributes.getValue("name");
if (null == name) {
log("MISSING 'name' attribute on <dependency> in " + ivyXmlFile.getPath(), Project.MSG_ERR);
fail = true;
foundAllAttributes = false;
}
String rev = attributes.getValue("rev");
if (null == rev) {
log("MISSING 'rev' attribute on <dependency> in " + ivyXmlFile.getPath(), Project.MSG_ERR);
fail = true;
foundAllAttributes = false;
}
if (foundAllAttributes) {
String coordinateKey = "/" + org + '/' + name;
String expectedRev = "${" + coordinateKey + '}';
if ( ! rev.equals(expectedRev)) {
log("BAD <dependency> 'rev' attribute value '" + rev + "' - expected '" + expectedRev + "'"
+ " in " + ivyXmlFile.getPath(), Project.MSG_ERR);
fail = true;
}
if ( ! directDependencies.containsKey(coordinateKey)) {
log("MISSING key '" + coordinateKey + "' in " + centralizedVersionsFile.getPath(), Project.MSG_ERR);
fail = true;
}
}
}
tags.push(localName);
}
@Override
public void endElement (String uri, String localName, String qName) throws SAXException {
tags.pop();
}
private boolean insideDependenciesTag() {
return tags.size() == 2 && tags.get(0).equals("ivy-module") && tags.get(1).equals("dependencies");
}
}
}