package org.springframework.roo.project;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* Simplified immutable representation of a dependency.
* <p>
* Structured after the model used by Maven. This may be replaced in a
* future release with a more OSGi-centric model.
* <p>
* According to the Maven docs, "the minimal set of information for matching a
* dependency reference against a dependencyManagement section is actually
* {groupId, artifactId, type, classifier}"; see
* http://maven.apache.org/guides/introduction
* /introduction-to-dependency-mechanism.html#Dependency_Scope
*
* @author Ben Alex
* @author Stefan Schmidt
* @author Alan Stewart
* @author Andrew Swan
* @author Juan Carlos GarcĂa
* @since 1.0
*/
public class Dependency implements Comparable<Dependency> {
// Known dependency types in increasing containment order
private static final List<String> TYPE_HIERARCHY = Arrays.asList("jar", "war", "ear", "pom");
/**
* Indicates whether one dependency type is at a higher logical level than
* another.
*
* @param type1 the first dependency type to compare (required)
* @param type2 the second dependency type to compare (required)
* @return <code>false</code> if they are at the same level or the first is
* at a lower level
* @since 1.2.1
*/
public static boolean isHigherLevel(final String type1, final String type2) {
final int type1Index = TYPE_HIERARCHY.indexOf(type1.toLowerCase());
final int type2Index = TYPE_HIERARCHY.indexOf(type2.toLowerCase());
return type2Index >= 0 && type1Index > type2Index;
}
private final String artifactId;
private final String classifier;
private final String optional;
private final List<Dependency> exclusions = new ArrayList<Dependency>();
// -- Identifying
private final String groupId;
// -- Non-identifying
private final DependencyScope scope;
private final String systemPath;
private final DependencyType type;
private final String version;
/**
* Constructs a {@link Dependency} from a Maven-style <dependency>
* element.
*
* @param dependency to parse (required)
*/
public Dependency(final Element dependency) {
// Test if it has Maven format
if (dependency.hasChildNodes() && dependency.getElementsByTagName("artifactId").getLength() > 0) {
groupId = dependency.getElementsByTagName("groupId").item(0).getTextContent().trim();
artifactId = dependency.getElementsByTagName("artifactId").item(0).getTextContent().trim();
final NodeList versionElements = dependency.getElementsByTagName("version");
if (versionElements.getLength() > 0) {
version = versionElements.item(0).getTextContent();
} else {
version = "";
}
// POM attributes supported in Maven 3.1
type = DependencyType.getType(dependency);
// POM attributes supported in Maven 3.1
scope = DependencyScope.getScope(dependency);
if (scope == DependencyScope.SYSTEM) {
if (XmlUtils.findFirstElement("systemPath", dependency) != null) {
systemPath = XmlUtils.findFirstElement("systemPath", dependency).getTextContent().trim();
} else {
throw new IllegalArgumentException("Missing <systemPath> declaration for system scope");
}
} else {
systemPath = null;
}
classifier = DomUtils.getChildTextContent(dependency, "classifier");
optional = DomUtils.getChildTextContent(dependency, "optional");
// Parsing for exclusions
final List<Element> exclusionList = XmlUtils.findElements("exclusions/exclusion", dependency);
if (exclusionList.size() > 0) {
for (final Element exclusion : exclusionList) {
final Element exclusionE = XmlUtils.findFirstElement("groupId", exclusion);
String exclusionId = "";
if (exclusionE != null) {
exclusionId = exclusionE.getTextContent();
}
final Element exclusionArtifactE = XmlUtils.findFirstElement("artifactId", exclusion);
String exclusionArtifactId = "";
if (exclusionArtifactE != null) {
exclusionArtifactId = exclusionArtifactE.getTextContent();
}
if (!(exclusionArtifactId.length() < 1) && !(exclusionId.length() < 1)) {
exclusions.add(new Dependency(exclusionId, exclusionArtifactId, "ignored"));
}
}
}
} else {
throw new IllegalStateException(
"Dependency XML format not supported or is missing a mandatory node ('" + dependency
+ "')");
}
}
/**
* Constructor for a dependency with the given attributes.
*
* @param gav the coordinates to use (required)
* @param type the dependency type (required)
* @param scope the dependency scope (required)
* @since 1.2.1
*/
public Dependency(final GAV gav, final DependencyType type, final DependencyScope scope) {
this(gav.getGroupId(), gav.getArtifactId(), gav.getVersion(), type, scope);
}
/**
* Constructs a compile-scoped JAR dependency.
*
* @param groupId the group ID (required)
* @param artifactId the artifact ID (required)
* @param version the version (required)
*/
public Dependency(final String groupId, final String artifactId, final String version) {
this(groupId, artifactId, version, DependencyType.JAR, DependencyScope.COMPILE);
}
/**
* Constructs a compile-scoped JAR dependency with optional exclusions.
*
* @param groupId the group ID (required)
* @param artifactId the artifact ID (required)
* @param version the version ID (required)
* @param exclusions the exclusions for this dependency (can be null)
*/
public Dependency(final String groupId, final String artifactId, final String version,
final Collection<? extends Dependency> exclusions) {
this(groupId, artifactId, version, DependencyType.JAR, DependencyScope.COMPILE);
if (exclusions != null) {
this.exclusions.addAll(exclusions);
}
}
/**
* Constructs a dependency with the given type and scope.
*
* @param groupId the group ID (required)
* @param artifactId the artifact ID (required)
* @param version the version ID (required)
* @param type the dependency type (required)
* @param scope the dependency scope (required)
*/
public Dependency(final String groupId, final String artifactId, final String version,
final DependencyType type, final DependencyScope scope) {
this(groupId, artifactId, version, type, scope, "");
}
/**
* Creates an immutable {@link Dependency}.
*
* @param groupId the group ID (required)
* @param artifactId the artifact ID (required)
* @param version the version ID (required)
* @param type the dependency type (required)
* @param scope the dependency scope (required)
* @param classifier the dependency classifier (required)
*/
public Dependency(final String groupId, final String artifactId, final String version,
final DependencyType type, final DependencyScope scope, final String classifier) {
XmlUtils.assertElementLegal(groupId);
XmlUtils.assertElementLegal(artifactId);
Validate.notNull(scope, "Dependency scope required");
Validate.notNull(type, "Dependency type required");
this.artifactId = artifactId;
this.classifier = classifier;
this.groupId = groupId;
this.scope = scope;
systemPath = null;
this.type = type;
this.version = version;
this.optional = null;
}
/**
* Adds the given exclusion to this dependency
*
* @param exclusionGroupId the groupId of the dependency to exclude
* (required)
* @param exclusionArtifactId the artifactId of the dependency to exclude
* (required)
*/
public void addExclusion(final String exclusionGroupId, final String exclusionArtifactId) {
Validate.notBlank(exclusionGroupId, "Excluded groupId required");
Validate.notBlank(exclusionArtifactId, "Excluded artifactId required");
exclusions.add(new Dependency(exclusionGroupId, exclusionArtifactId, "ignored"));
}
/**
* Compares this dependency's identifying coordinates (i.e. not the version)
* to those of the given dependency
*
* @param other the dependency being compared to (required)
* @return see {@link Comparable#compareTo(Object)}
*/
private int compareCoordinates(final Dependency other) {
Validate.notNull(other, "Dependency being compared to cannot be null");
int result = groupId.compareTo(other.getGroupId());
if (result == 0) {
result = artifactId.compareTo(other.getArtifactId());
}
if (result == 0) {
result =
StringUtils.stripToEmpty(classifier).compareTo(
StringUtils.stripToEmpty(other.getClassifier()));
}
if (result == 0 && type != null) {
result = type.compareTo(other.getType());
}
return result;
}
public int compareTo(final Dependency o) {
final int result = compareCoordinates(o);
if (result != 0) {
return result;
}
return version.compareTo(o.getVersion());
}
@Override
public boolean equals(final Object obj) {
return obj instanceof Dependency && compareTo((Dependency) obj) == 0;
}
public String getArtifactId() {
return artifactId;
}
public String getClassifier() {
return classifier;
}
/**
* Returns the XML element for this dependency
*
* @param document the parent XML document
* @return a non-<code>null</code> element
* @since 1.2.0
*/
public Element getElement(final Document document) {
final Element dependencyElement = document.createElement("dependency");
dependencyElement.appendChild(XmlUtils.createTextElement(document, "groupId", groupId));
dependencyElement.appendChild(XmlUtils.createTextElement(document, "artifactId", artifactId));
dependencyElement.appendChild(XmlUtils.createTextElement(document, "version", version));
if (type != null && type != DependencyType.JAR) {
// Keep the XML short, we don't need "JAR" given it's the default
String typeContent = "";
if (type == DependencyType.TESTJAR) {
typeContent = "test-jar";
} else {
typeContent = type.toString().toLowerCase();
}
final Element typeElement = XmlUtils.createTextElement(document, "type", typeContent);
dependencyElement.appendChild(typeElement);
}
// Keep the XML short, we don't need "compile" given it's the default
if (scope != null && scope != DependencyScope.COMPILE) {
dependencyElement.appendChild(XmlUtils.createTextElement(document, "scope", scope.toString()
.toLowerCase()));
if (scope == DependencyScope.SYSTEM && StringUtils.isNotBlank(systemPath)) {
dependencyElement.appendChild(XmlUtils
.createTextElement(document, "systemPath", systemPath));
}
}
if (StringUtils.isNotBlank(classifier)) {
dependencyElement.appendChild(XmlUtils.createTextElement(document, "classifier", classifier));
}
if (StringUtils.isNotBlank(optional)) {
dependencyElement.appendChild(XmlUtils.createTextElement(document, "optional", optional));
}
// Add exclusions if any
if (!exclusions.isEmpty()) {
final Element exclusionsElement =
DomUtils.createChildElement("exclusions", dependencyElement, document);
for (final Dependency exclusion : exclusions) {
final Element exclusionElement =
DomUtils.createChildElement("exclusion", exclusionsElement, document);
exclusionElement.appendChild(XmlUtils.createTextElement(document, "groupId",
exclusion.getGroupId()));
exclusionElement.appendChild(XmlUtils.createTextElement(document, "artifactId",
exclusion.getArtifactId()));
}
}
return dependencyElement;
}
/**
* @return list of exclusions (never null)
*/
public List<Dependency> getExclusions() {
return exclusions;
}
public String getGroupId() {
return groupId;
}
public DependencyScope getScope() {
return scope;
}
/**
* @return a simple description, as would be used for console output
*/
public String getSimpleDescription() {
return groupId + ":" + artifactId + ":" + version
+ (StringUtils.isNotBlank(classifier) ? ":" + classifier : "");
}
public String getSystemPath() {
return systemPath;
}
public DependencyType getType() {
return type;
}
public String getVersion() {
return version;
}
@Deprecated
public String getVersionId() {
return version;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (artifactId == null ? 0 : artifactId.hashCode());
result = prime * result + (groupId == null ? 0 : groupId.hashCode());
result = prime * result + (classifier == null ? 0 : classifier.hashCode());
result = prime * result + (type == null ? 0 : type.hashCode());
return result;
}
/**
* Indicates whether the given {@link Dependency} has the same Maven
* coordinates as this one; this is not necessarily the same as calling
* {@link #equals(Object)}, which may compare more fields beyond the basic
* coordinates.
*
* @param dependency the dependency to check (can be <code>null</code>)
* @return <code>false</code> if any coordinates are different
*/
public boolean hasSameCoordinates(final Dependency dependency) {
return dependency != null && compareCoordinates(dependency) == 0;
}
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);
builder.append("groupId", groupId);
builder.append("artifactId", artifactId);
builder.append("version", version);
builder.append("type", type);
builder.append("scope", scope);
if (classifier != null) {
builder.append("classifier", classifier);
}
return builder.toString();
}
}