package net.thucydides.core.requirements;
import ch.lambdaj.function.convert.Converter;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import net.thucydides.core.ThucydidesSystemProperty;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.model.TestOutcome;
import net.thucydides.core.model.TestTag;
import net.thucydides.core.requirements.model.Narrative;
import net.thucydides.core.requirements.model.NarrativeReader;
import net.thucydides.core.requirements.model.Requirement;
import net.thucydides.core.util.EnvironmentVariables;
import net.thucydides.core.util.Inflector;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Pattern;
import static ch.lambdaj.Lambda.convert;
import static net.thucydides.core.requirements.RequirementsPath.pathElements;
import static net.thucydides.core.requirements.RequirementsPath.stripRootFromPath;
import static net.thucydides.core.util.NameConverter.humanize;
//import javax.persistence.Transient;
/**
* Load a set of requirements (epics/themes,...) from the directory structure.
* This will typically be the directory structure containing the tests (for JUnit) or stories (e.g. for JBehave).
* By default, the tests
*/
public class FileSystemRequirementsTagProvider extends AbstractRequirementsTagProvider implements RequirementsTagProvider, OverridableTagProvider {
private final static String DEFAULT_ROOT_DIRECTORY = "stories";
private final static String FEATURES_ROOT_DIRECTORY = "features";
private final static String DEFAULT_RESOURCE_DIRECTORY = "src/test/resources";
private static final String WORKING_DIR = "user.dir";
private static final List<Requirement> NO_REQUIREMENTS = Lists.newArrayList();
private static final List<TestTag> NO_TEST_TAGS = Lists.newArrayList();
public static final String STORY_EXTENSION = "story";
public static final String FEATURE_EXTENSION = "feature";
private final String rootDirectoryPath;
private final NarrativeReader narrativeReader;
private final int level;
// @Transient
private List<Requirement> requirements;
public FileSystemRequirementsTagProvider() {
this(defaultRootDirectoryPathFrom(Injectors.getInjector().getProvider(EnvironmentVariables.class).get()));
}
public static String defaultRootDirectoryPathFrom(EnvironmentVariables environmentVariables) {
if (ThucydidesSystemProperty.THUCYDIDES_REQUIREMENTS_DIR.isDefinedIn(environmentVariables)) {
return ThucydidesSystemProperty.THUCYDIDES_REQUIREMENTS_DIR.from(environmentVariables);
}
if (ThucydidesSystemProperty.THUCYDIDES_TEST_ROOT.isDefinedIn(environmentVariables)) {
return ThucydidesSystemProperty.THUCYDIDES_TEST_ROOT.from(environmentVariables);
}
Optional<String> resourceDirectory = getResourceDirectory(environmentVariables);
if(resourceDirectory.isPresent()) {
String resourceDir = resourceDirectory.get();
if(new File(resourceDir,DEFAULT_ROOT_DIRECTORY).exists()) {
return DEFAULT_ROOT_DIRECTORY;
} else if(new File(resourceDir, FEATURES_ROOT_DIRECTORY).exists()) {
return FEATURES_ROOT_DIRECTORY;
}
}
return DEFAULT_ROOT_DIRECTORY;
}
public FileSystemRequirementsTagProvider(String rootDirectory, int level) {
this(filePathFormOf(rootDirectory), level, Injectors.getInjector().getProvider(EnvironmentVariables.class).get() );
}
/**
* Convert a package name to a file path if necessary.
*/
private static String filePathFormOf(String rootDirectory) {
if (rootDirectory.contains(".")) {
return rootDirectory.replace(".", "/");
} else {
return rootDirectory;
}
}
public FileSystemRequirementsTagProvider(String rootDirectory, int level, EnvironmentVariables environmentVariables) {
super(environmentVariables);
this.rootDirectoryPath = rootDirectory;
this.level = level;
this.narrativeReader = NarrativeReader.forRootDirectory(rootDirectory)
.withRequirementTypes(getRequirementTypes());
}
public FileSystemRequirementsTagProvider(String rootDirectory) {
this(rootDirectory, 0);
}
/**
* We look for file system requirements in the root directory path (by default, 'stories').
* First, we look on the classpath. If we don't find anything on the classpath (e.g. if the task is
* being run from the Maven plugin), we look in the src/main/resources and src/test/resources directories starting
* at the working directory.
*/
public List<Requirement> getRequirements() {
if (requirements == null) {
try {
Set<Requirement> allRequirements = Sets.newHashSet();
Set<String> directoryPaths = getRootDirectoryPaths();
for(String rootDirectoryPath : directoryPaths) {
File rootDirectory = new File(rootDirectoryPath);
allRequirements.addAll(loadCapabilitiesFrom(rootDirectory.listFiles(thatAreDirectories())));
allRequirements.addAll(loadStoriesFrom(rootDirectory.listFiles(thatAreStories())));
}
requirements = Lists.newArrayList(allRequirements);
Collections.sort(requirements);
} catch (IOException e) {
requirements = NO_REQUIREMENTS;
throw new IllegalArgumentException("Could not load requirements from '" + rootDirectoryPath + "'", e);
}
if (level == 0) {
requirements = addParentsTo(requirements);
}
}
return requirements;
}
private List<Requirement> addParentsTo(List<Requirement> requirements) {
return addParentsTo(requirements, null);
}
private List<Requirement> addParentsTo(List<Requirement> requirements, String parent) {
List<Requirement> augmentedRequirements = Lists.newArrayList();
for (Requirement requirement : requirements) {
List<Requirement> children = requirement.hasChildren()
? addParentsTo(requirement.getChildren(), requirement.getName()) : NO_REQUIREMENTS;
augmentedRequirements.add(requirement.withParent(parent).withChildren(children));
}
return augmentedRequirements;
}
/**
* Find the root directory in the classpath or on the file system from which the requirements will be read.
*/
public Set<String> getRootDirectoryPaths() throws IOException {
if (ThucydidesSystemProperty.THUCYDIDES_TEST_REQUIREMENTS_BASEDIR.isDefinedIn(environmentVariables)) {
return getRootDirectoryFromRequirementsBaseDir().asSet();
} else {
Set<String> rootDirectoryOnClasspath = getRootDirectoryFromClasspath();
if (!rootDirectoryOnClasspath.isEmpty()) {
return rootDirectoryOnClasspath;
} else {
return getRootDirectoryFromWorkingDirectory();
}
}
}
private Set<String> getRootDirectoryFromClasspath() throws IOException {
List<URL> resourceRoots;
try {
Enumeration<URL> requirementResources = getDirectoriesFrom(rootDirectoryPath);
resourceRoots = Collections.list(requirementResources);
} catch (URISyntaxException e) {
throw new IOException(e);
}
return restoreSpacesIn(resourceRoots);
}
private Set<String> restoreSpacesIn(List<URL> resourceRoots) {
Set<String> urlsWithRestoredSpaces = Sets.newHashSet();
for(URL resourceRoot : resourceRoots) {
urlsWithRestoredSpaces.add(withRestoredSpaces(resourceRoot.getPath()));
}
return urlsWithRestoredSpaces;
}
private String withRestoredSpaces(String path) {
try {
return URLDecoder.decode(path, "UTF-8");
} catch (UnsupportedEncodingException e) {
return StringUtils.replace(path, "%20", " ");
}
}
private Set<String> getRootDirectoryFromWorkingDirectory() throws IOException {
return getRootDirectoryFromParentDir(System.getProperty(WORKING_DIR)).asSet();
}
private Optional<String> configuredRelativeRootDirectory;
private Optional<String> getRootDirectoryFromRequirementsBaseDir() {
if (configuredRelativeRootDirectory == null) {
configuredRelativeRootDirectory
= getRootDirectoryFromParentDir(ThucydidesSystemProperty.THUCYDIDES_TEST_REQUIREMENTS_BASEDIR
.from(environmentVariables, ""));
}
return configuredRelativeRootDirectory;
}
private Optional<String> getRootDirectoryFromParentDir(String parentDir) {
File resourceDirectory = getResourceDirectory(environmentVariables).isPresent() ? new File(parentDir, getResourceDirectory(environmentVariables).get()) : new File(parentDir);
File requirementsDirectory = absolutePath(rootDirectoryPath) ? new File(rootDirectoryPath) : new File(resourceDirectory, rootDirectoryPath);
if(!requirementsDirectory.exists()) {
requirementsDirectory = new File(resourceDirectory, FEATURES_ROOT_DIRECTORY); //features
}
if (requirementsDirectory.exists()) {
return Optional.of(requirementsDirectory.getAbsolutePath());
} else {
return Optional.absent();
}
}
private boolean absolutePath(String rootDirectoryPath) {
return (new File(rootDirectoryPath).isAbsolute() || rootDirectoryPath.startsWith("/"));
}
private Enumeration<URL> getDirectoriesFrom(String root) throws IOException, URISyntaxException {
String rootWithEscapedSpaces = root.replaceAll(" ", "%20");
URI rootUri = (isWindowsPath(rootWithEscapedSpaces)) ? new File(root).toPath().toUri() : new URI(rootWithEscapedSpaces);
return getClass().getClassLoader().getResources(rootUri.getPath());
}
private final Pattern WINDOWS_PATH = Pattern.compile("([a-zA-Z]:)?(\\\\[a-zA-Z0-9_-]+)+\\\\?");
private boolean isWindowsPath(String rootWithEscapedSpaces) {
return WINDOWS_PATH.matcher(rootWithEscapedSpaces).find();
}
public Set<TestTag> getTagsFor(final TestOutcome testOutcome) {
Set<TestTag> tags = new HashSet<>();
if (testOutcome.getPath() != null) {
List<String> storyPathElements = stripRootFrom(pathElements(stripRootPathFrom(testOutcome.getPath())));
addStoryTagIfPresent(tags, storyPathElements);
storyPathElements = stripStorySuffixFrom(storyPathElements);
tags.addAll(getMatchingCapabilities(getRequirements(), storyPathElements));
}
return tags;
}
private List<String> stripStorySuffixFrom(List<String> pathElements) {
if ((!pathElements.isEmpty()) && isSupportedFileStoryExtension(last(pathElements))) {
return dropLastElement(pathElements);
} else {
return pathElements;
}
}
private List<String> dropLastElement(List<String> pathElements) {
List<String> strippedPathElements = Lists.newArrayList(pathElements);
strippedPathElements.remove(pathElements.size() - 1);
return strippedPathElements;
}
private void addStoryTagIfPresent(Set<TestTag> tags, List<String> storyPathElements) {
Optional<TestTag> storyTag = storyTagFrom(storyPathElements);
tags.addAll(storyTag.asSet());
}
private Optional<TestTag> storyTagFrom(List<String> storyPathElements) {
if ((!storyPathElements.isEmpty()) && isSupportedFileStoryExtension(last(storyPathElements))) {
String storyName = Lists.reverse(storyPathElements).get(1);
String storyParent = parentElement(storyPathElements);
String qualifiedName = storyParent == null ?
humanize(storyName) : humanize(storyParent).trim() + "/" + humanize(storyName);
TestTag storyTag = TestTag.withName(qualifiedName).andType("story");
return Optional.of(storyTag);
} else {
return Optional.absent();
}
}
private String parentElement(List<String> storyPathElements) {
return storyPathElements.size() > 2 ? Lists.reverse(storyPathElements).get(2) : null;
}
private String last(List<String> list) {
if (list.isEmpty()) {
return null;
} else {
return list.get(list.size() - 1);
}
}
public Optional<Requirement> getParentRequirementOf(final TestOutcome testOutcome) {
if (testOutcome.getPath() != null) {
List<String> storyPathElements = stripStorySuffixFrom(stripRootFrom(pathElements(stripRootPathFrom(testOutcome.getPath()))));
return lastRequirementFrom(storyPathElements);
} else {
return mostSpecificTagRequirementFor(testOutcome);
}
}
private Optional<Requirement> mostSpecificTagRequirementFor(TestOutcome testOutcome) {
Optional<Requirement> mostSpecificRequirement = Optional.absent();
int currentSpecificity = -1;
for (TestTag tag : testOutcome.getTags()) {
Optional<Requirement> matchingRequirement = getRequirementFor(tag);
if (matchingRequirement.isPresent()) {
int specificity = requirementsConfiguration.getRequirementTypes().indexOf(matchingRequirement.get().getType());
if (currentSpecificity < specificity) {
currentSpecificity = specificity;
mostSpecificRequirement = matchingRequirement;
}
}
}
return mostSpecificRequirement;
}
public Optional<Requirement> getRequirementFor(TestTag testTag) {
for (Requirement requirement : getFlattenedRequirements()) {
if (requirement.getName().equalsIgnoreCase(testTag.getName()) && requirement.getType().equalsIgnoreCase(testTag.getType())) {
return Optional.of(requirement);
}
}
return Optional.absent();
}
private List<Requirement> getFlattenedRequirements() {
List<Requirement> allRequirements = Lists.newArrayList();
for (Requirement requirement : getRequirements()) {
allRequirements.add(requirement);
allRequirements.addAll(childRequirementsOf(requirement));
}
return allRequirements;
}
private Collection<Requirement> childRequirementsOf(Requirement requirement) {
List<Requirement> childRequirements = Lists.newArrayList();
for (Requirement childRequirement : requirement.getChildren()) {
childRequirements.add(childRequirement);
childRequirements.addAll(childRequirementsOf(childRequirement));
}
return childRequirements;
}
private Optional<Requirement> lastRequirementFrom(List<String> storyPathElements) {
if (storyPathElements.isEmpty()) {
return Optional.absent();
} else {
return lastRequirementMatchingPath(getRequirements(), storyPathElements);
}
}
private Optional<Requirement> lastRequirementMatchingPath(List<Requirement> requirements, List<String> storyPathElements) {
if (storyPathElements.isEmpty()) {
return Optional.absent();
}
Optional<Requirement> matchingRequirement = findMatchingRequirementIn(next(storyPathElements), requirements);
if (!matchingRequirement.isPresent()) {
return Optional.absent();
}
if (tail(storyPathElements).isEmpty()) {
return matchingRequirement;
}
List<Requirement> childRequrements = matchingRequirement.get().getChildren();
return lastRequirementMatchingPath(childRequrements, tail(storyPathElements));
}
private List<TestTag> getMatchingCapabilities(List<Requirement> requirements, List<String> storyPathElements) {
if (storyPathElements.isEmpty()) {
return NO_TEST_TAGS;
} else {
Optional<Requirement> matchingRequirement = findMatchingRequirementIn(next(storyPathElements), requirements);
if (matchingRequirement.isPresent()) {
TestTag thisTag = matchingRequirement.get().asTag();
List<TestTag> remainingTags = getMatchingCapabilities(matchingRequirement.get().getChildren(), tail(storyPathElements));
return concat(thisTag, remainingTags);
} else {
return NO_TEST_TAGS;
}
}
}
private List<String> stripRootFrom(List<String> storyPathElements) {
return stripRootFromPath(rootDirectoryPath, storyPathElements);
}
private String stripRootPathFrom(String testOutcomePath) {
String rootPath = ThucydidesSystemProperty.THUCYDIDES_TEST_ROOT.from(environmentVariables);
if (rootPath != null && testOutcomePath.startsWith(rootPath) && (!testOutcomePath.equals(rootPath))) {
return testOutcomePath.substring(rootPath.length() + 1);
} else {
return testOutcomePath;
}
}
private List<TestTag> concat(TestTag thisTag, List<TestTag> remainingTags) {
List<TestTag> totalTags = new ArrayList<TestTag>();
totalTags.add(thisTag);
totalTags.addAll(remainingTags);
return totalTags;
}
private <T> T next(List<T> elements) {
return elements.get(0);
}
private <T> List<T> tail(List<T> elements) {
return elements.subList(1, elements.size());
}
private Optional<Requirement> findMatchingRequirementIn(String storyPathElement, List<Requirement> requirements) {
for (Requirement requirement : requirements) {
String normalizedStoryPathElement = Inflector.getInstance().humanize(Inflector.getInstance().underscore(storyPathElement));
if (requirement.getName().equals(normalizedStoryPathElement)) {
return Optional.of(requirement);
}
}
return Optional.absent();
}
private List<Requirement> loadCapabilitiesFrom(File[] requirementDirectories) {
return convert(requirementDirectories, toRequirements());
}
private List<Requirement> loadStoriesFrom(File[] storyFiles) {
return convert(storyFiles, toStoryRequirements());
}
private Converter<File, Requirement> toRequirements() {
return new Converter<File, Requirement>() {
public Requirement convert(File requirementFileOrDirectory) {
return readRequirementFrom(requirementFileOrDirectory);
}
};
}
private Converter<File, Requirement> toStoryRequirements() {
return new Converter<File, Requirement>() {
public Requirement convert(File storyFile) {
return readRequirementsFromStoryFile(storyFile);
}
};
}
private Requirement readRequirementFrom(File requirementDirectory) {
Optional<Narrative> requirementNarrative = narrativeReader.loadFrom(requirementDirectory, level);
if (requirementNarrative.isPresent()) {
return requirementWithNarrative(requirementDirectory,
humanReadableVersionOf(requirementDirectory.getName()),
requirementNarrative.get());
} else {
return requirementFromDirectoryName(requirementDirectory);
}
}
private Requirement readRequirementsFromStoryFile(File storyFile) {
Optional<Narrative> optionalNarrative = narrativeReader.loadFromStoryFile(storyFile);
String storyFileName = storyFile.getName();
String storyName = "";
String storyType = "story";
if(storyFileName.endsWith("." + STORY_EXTENSION)) {
storyName = storyFile.getName().replace("." + STORY_EXTENSION, "");
storyType = "story";
}
else if(storyFileName.endsWith("." + FEATURE_EXTENSION)) {
storyName = storyFile.getName().replace("." + FEATURE_EXTENSION, "");
storyType = "feature";
}
if (optionalNarrative.isPresent()) {
return requirementWithNarrative(storyFile, humanReadableVersionOf(storyName), optionalNarrative.get()).withType(storyType);
} else {
return storyNamed(storyName).withType(storyType);
}
}
private Requirement requirementFromDirectoryName(File requirementDirectory) {
String shortName = humanReadableVersionOf(requirementDirectory.getName());
List<Requirement> children = readChildrenFrom(requirementDirectory);
return Requirement.named(shortName).withType(getDefaultType(level)).withNarrative(shortName).withChildren(children);
}
private Requirement storyNamed(String storyName) {
String shortName = humanReadableVersionOf(storyName);
return Requirement.named(shortName).withType(STORY_EXTENSION).withNarrative(shortName);
}
private Requirement requirementWithNarrative(File requirementDirectory, String shortName, Narrative requirementNarrative) {
String displayName = getTitleFromNarrativeOrDirectoryName(requirementNarrative, shortName);
String cardNumber = requirementNarrative.getCardNumber().orNull();
String type = requirementNarrative.getType();
List<String> releaseVersions = requirementNarrative.getVersionNumbers();
List<Requirement> children = readChildrenFrom(requirementDirectory);
return Requirement.named(shortName)
.withOptionalDisplayName(displayName)
.withOptionalCardNumber(cardNumber)
.withType(type)
.withNarrative(requirementNarrative.getText())
.withReleaseVersions(releaseVersions)
.withChildren(children);
}
private List<Requirement> readChildrenFrom(File requirementDirectory) {
String childDirectory = rootDirectoryPath + "/" + requirementDirectory.getName();
RequirementsTagProvider childReader = new FileSystemRequirementsTagProvider(childDirectory, level + 1, environmentVariables);
return childReader.getRequirements();
}
private String getTitleFromNarrativeOrDirectoryName(Narrative requirementNarrative, String nameIfNoNarrativePresent) {
if (requirementNarrative.getTitle().isPresent()) {
return requirementNarrative.getTitle().get();
} else {
return nameIfNoNarrativePresent;
}
}
private FileFilter thatAreDirectories() {
return new FileFilter() {
public boolean accept(File file) {
return file.isDirectory() && !file.getName().startsWith(".");
}
};
}
private FileFilter thatAreStories() {
return new FileFilter() {
public boolean accept(File file) {
String filename = file.getName().toLowerCase();
if (filename.startsWith("given") || filename.startsWith("precondition")) {
return false;
} else {
return (file.getName().toLowerCase().endsWith("." + STORY_EXTENSION) || file.getName().toLowerCase().endsWith("." + FEATURE_EXTENSION));
}
}
};
}
public static Optional<String> getResourceDirectory(EnvironmentVariables environmentVariables) {
if (ThucydidesSystemProperty.THUCYDIDES_REQUIREMENTS_DIR.isDefinedIn(environmentVariables)) {
return Optional.absent();
} else {
return Optional.of(DEFAULT_RESOURCE_DIRECTORY);
}
}
private boolean isSupportedFileStoryExtension(String storyFileExtension) {
return (storyFileExtension.toLowerCase().equals(FEATURE_EXTENSION) || storyFileExtension.toLowerCase().equals(STORY_EXTENSION));
}
}