/*
* JBoss, Home of Professional Open Source
* Copyright 2012, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed 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.jboss.shrinkwrap.resolver.impl.maven.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.jboss.shrinkwrap.resolver.api.CoordinateParseException;
import org.jboss.shrinkwrap.resolver.api.maven.PomEquippedResolveStage;
import org.jboss.shrinkwrap.resolver.api.maven.ScopeType;
import org.junit.Assert;
/**
* Sets a set of files or archives and checks that returned files start with the same names.
*
* @author <a href="kpiwko@redhat.com">Karel Piwko</a>
* @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a>
*/
public class ValidationUtil {
private final Collection<String> requiredFileNamePrefixes;
/**
* Creates a new instance, specifying only the valid file name prefixes permitted in the
* {@link ValidationUtil#validate(File[])} calls.
*
* @param requiredFileNamePrefixes
*/
public ValidationUtil(final String... requiredFileNamePrefixes) {
this.requiredFileNamePrefixes = new ArrayList<String>(requiredFileNamePrefixes.length);
for (final String file : requiredFileNamePrefixes) {
this.requiredFileNamePrefixes.add(file);
}
}
/**
* Validates the current state of the required file names in this instance against the specified dependency tree
* file, in the specified scopes. If no scopes are specified, ALL will be permitted. The root at the specified file
* will be considered.
*
* @param dependencyTree
* @param allowedScopesArray
* @return
*/
public static ValidationUtil fromDependencyTree(File dependencyTree, ScopeType... allowedScopesArray) {
return fromDependencyTree(dependencyTree, true, allowedScopesArray);
}
/**
* Validates the current state of the required file names in this instance against the specified dependency tree
* file, in the specified scopes. If no scopes are specified, ALL will be permitted. If the <code>includeRoot</code>
* flag is set, the root will be added to the list of file prefixes which are required by resolution, else not. For
* instance POM resolution by {@link PomEquippedResolveStage#importRuntimeDependencies()} should not include the
* current artifact in the resolved results, so this flag would be set to false.
*
*
* @param dependencyTree
* @param allowedScopesArray
* @return
* @throws IllegalArgumentException
*/
public static ValidationUtil fromDependencyTree(File dependencyTree, boolean includeRoot,
ScopeType... allowedScopesArray) throws IllegalArgumentException {
List<String> allowedScopes = new ArrayList<String>();
for (ScopeType scope : allowedScopesArray) {
allowedScopes.add(scope.toString());
}
return fromDependencyTree(dependencyTree, includeRoot, allowedScopes);
}
/**
* Validates the current state of the required file names in this instance against the specified dependency tree
* file, in the specified scopes. If no scopes are specified, ALL will be permitted. If the <code>includeRoot</code>
* flag is set, the root will be added to the list of file prefixes which are required by resolution, else not. For
* instance POM resolution by {@link PomEquippedResolveStage#importRuntimeDependencies()} should not include the
* current artifact in the resolved results, so this flag would be set to false.
*
* @param dependencyTree
* @param allowedScopes
* @return
* @throws IllegalArgumentException
*/
public static ValidationUtil fromDependencyTree(final File dependencyTree, boolean includeRoot,
final List<String> allowedScopes) throws IllegalArgumentException {
List<String> files = new ArrayList<String>();
final List<String> realAllowedScopes = new ArrayList<String>();
if (allowedScopes == null || allowedScopes.isEmpty()) {
for (final ScopeType scope : ScopeType.values()) {
realAllowedScopes.add(scope.toString());
}
} else {
realAllowedScopes.addAll(allowedScopes);
}
BufferedReader input = null;
try {
input = new BufferedReader(new FileReader(dependencyTree));
String line = null;
while ((line = input.readLine()) != null) {
final ArtifactMetaData artifact = new ArtifactMetaData(line);
if (artifact.isRoot == true) {
if (!includeRoot) {
// Root of the tree should not be included
continue;
}
if (!"jar".equals(artifact.extension)) {
// skip non-jar from dependency tree
continue;
}
// Add, scope doesn't matter for the root
files.add(artifact.filename());
}
// add artifact if in allowed scope
else if (realAllowedScopes.contains(artifact.scope)) {
files.add(artifact.filename());
}
}
} catch (final IOException e) {
throw new CoordinateParseException(MessageFormat.format(
"Unable to load dependency tree from {0} to verify", dependencyTree));
} finally {
if (input != null) {
try {
input.close();
} catch (final IOException ioe) {
// Swallow
}
}
}
return new ValidationUtil(files.toArray(new String[0]));
}
public void validate(final File single) throws AssertionError {
validate(Collections.singletonList(single));
}
public void validate(final File... files) {
validate(Arrays.asList(files));
}
public void validate(final boolean validateOrder, final File... files) {
validate(true, Arrays.asList(files));
}
public void validate(final boolean validateOrder, final List<File> resolvedFiles) throws AssertionError {
Assert.assertNotNull("There must be some files passed for validation, but the array was null", resolvedFiles);
final Collection<String> resolvedFileNames = new ArrayList<String>(resolvedFiles.size());
for (final File resolvedFile : resolvedFiles) {
resolvedFileNames.add(resolvedFile.getName());
}
final Collection<String> foundNotAllowed = new ArrayList<String>();
final Collection<String> requiredNotFound = new ArrayList<String>();
// Check for resolved files found but not allowed
for (final String resolvedFileName : resolvedFileNames) {
boolean found = false;
for (final String requiredFileName : this.requiredFileNamePrefixes) {
if (resolvedFileName.startsWith(requiredFileName)) {
found = true;
}
}
if (!found) {
foundNotAllowed.add(resolvedFileName);
}
}
// Check for required files not found in those resolved
int i = 0;
Iterator<String> requiredFileNamePrefixesIterator = this.requiredFileNamePrefixes.iterator();
while (requiredFileNamePrefixesIterator.hasNext()) {
final String requiredFileName = requiredFileNamePrefixesIterator.next();
i++;
int j = 0;
boolean found = false;
Iterator<String> resolvedFileNamesIterator = resolvedFileNames.iterator();
while (resolvedFileNamesIterator.hasNext()) {
final String resolvedFileName = resolvedFileNamesIterator.next();
j++;
if (resolvedFileName.startsWith(requiredFileName)) {
if (validateOrder && i == j) {
found = true;
}
else if (!validateOrder) {
found = true;
}
}
}
if (!found) {
requiredNotFound.add(requiredFileName);
}
}
// We're all good in the hood
if (foundNotAllowed.size() == 0 && requiredNotFound.size() == 0) {
// Get outta here
return;
}
// Problems; report 'em
final StringBuilder errorMessage = new StringBuilder().append(requiredFileNamePrefixes.size())
.append(" files required to be resolved, however ").append(resolvedFiles.size())
.append(" files were resolved. ").append("Resolution contains: \n");
if (foundNotAllowed.size() > 0) {
errorMessage.append("\tFound but not allowed:\n\t\t");
errorMessage.append(foundNotAllowed.toString());
errorMessage.append("\n");
}
if (requiredNotFound.size() > 0) {
errorMessage.append("\tRequired but not found:\n\t\t");
errorMessage.append(requiredNotFound.toString());
}
if (validateOrder) {
errorMessage.append("\tOrder of dependencies has been verified as well.");
}
Assert.fail(errorMessage.toString());
}
public void validate(final List<File> resolvedFiles) throws AssertionError {
validate(false, resolvedFiles);
}
/**
* Returns an immutable view of the required file name prefixes configured for this instance
*
* @return
*/
Collection<String> getRequiredFileNamePrefixes() {
return Collections.unmodifiableCollection(this.requiredFileNamePrefixes);
}
/**
* A holder for a line generated from Maven dependency tree plugin
*
* @author <a href="mailto:kpiwko@redhat.com">Karel Piwko</a>
*
*/
private static class ArtifactMetaData {
private static final String SCOPE_ROOT = "";
// Placeholder; we don't read this value back out again, but we do need to remember to pull its token from the
// tokenizer
@SuppressWarnings("unused")
final String groupId;
final String artifactId;
final String extension;
final String classifier;
final String version;
final String scope;
final boolean isRoot;
/**
* Creates an artifact holder from the input lien
*
* @param dependencyCoords
*/
ArtifactMetaData(String dependencyCoords) {
int index = 0;
while (index < dependencyCoords.length()) {
char c = dependencyCoords.charAt(index);
if (c == '\\' || c == '|' || c == ' ' || c == '+' || c == '-') {
index++;
} else {
break;
}
}
for (int testIndex = index, i = 0; i < 4; i++) {
testIndex = dependencyCoords.substring(testIndex).indexOf(":");
if (testIndex == -1) {
throw new IllegalArgumentException("Invalid format of the dependency coordinates for "
+ dependencyCoords);
}
}
StringTokenizer st = new StringTokenizer(dependencyCoords.substring(index), ":");
this.groupId = st.nextToken();
this.artifactId = st.nextToken();
this.extension = st.nextToken();
// this is the root artifact
if (index == 0) {
this.isRoot = true;
if (st.countTokens() == 1) {
this.classifier = "";
this.version = st.nextToken();
} else if (st.countTokens() == 2) {
this.classifier = st.nextToken();
this.version = st.nextToken();
} else {
throw new IllegalArgumentException("Invalid format of the dependency coordinates for "
+ dependencyCoords);
}
this.scope = SCOPE_ROOT;
}
// otherwise
else {
this.isRoot = false;
if (st.countTokens() == 2) {
this.classifier = "";
this.version = st.nextToken();
this.scope = extractScope(st.nextToken());
} else if (st.countTokens() == 3) {
this.classifier = st.nextToken();
this.version = st.nextToken();
this.scope = extractScope(st.nextToken());
} else {
throw new IllegalArgumentException("Invalid format of the dependency coordinates for "
+ dependencyCoords);
}
}
}
public String filename() {
StringBuilder sb = new StringBuilder();
sb.append(artifactId).append("-").append(version);
if (classifier.length() != 0) {
sb.append("-").append(classifier);
}
sb.append(".").append(extension);
return sb.toString();
}
private String extractScope(String scope) {
int lparen = scope.indexOf("(");
int rparen = scope.indexOf(")");
int space = scope.indexOf(" ");
if (lparen == -1 && rparen == -1 && space == -1) {
return scope;
} else if (lparen != -1 && rparen != -1 && space != -1) {
return scope.substring(0, space);
}
throw new IllegalArgumentException("Invalid format of the dependency coordinates for artifact scope: "
+ scope);
}
}
}