package net.thucydides.core.requirements;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.thucydides.core.annotations.Narrative;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.model.TestOutcome;
import net.thucydides.core.model.TestTag;
import net.thucydides.core.reflection.ClassFinder;
import net.thucydides.core.requirements.annotations.NarrativeFinder;
import net.thucydides.core.requirements.model.Requirement;
import net.thucydides.core.util.EnvironmentVariables;
import net.thucydides.core.webdriver.Configuration;
import net.thucydides.core.webdriver.SystemPropertiesConfiguration;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.*;
import static net.thucydides.core.ThucydidesSystemProperty.THUCYDIDES_TEST_ROOT;
/**
* A requirements Provider that reads requirement from class or package annotation.
* A class or package needs to be annotated with {@link net.thucydides.core.annotations.Narrative}
* to be a requirement. All package above the class or package will also be considered requirement.
* The root package is defined using {@link net.thucydides.core.ThucydidesSystemProperty#THUCYDIDES_TEST_ROOT}
* It is recommended to change the root package if the {@link FileSystemRequirementsTagProvider} is used.
*
* @see net.thucydides.core.annotations.Narrative
* @see net.thucydides.core.ThucydidesSystemProperty#THUCYDIDES_TEST_ROOT
*/
public class PackageAnnotationBasedTagProvider extends AbstractRequirementsTagProvider implements RequirementsTagProvider, OverridableTagProvider {
private static final String DOT_REGEX = "\\.";
private final static List<String> SUPPORTED_SUFFIXES = ImmutableList.of("story","feature");
private List<Requirement> requirements;
private final Configuration configuration;
private final RequirementPersister persister;
private final String rootPackage;
private List<Requirement> leafRequirements;
SortedMap<String, Requirement> requirementsByPath = Maps.newTreeMap();
Map<Requirement, String> requirementPaths = Maps.newHashMap();
public PackageAnnotationBasedTagProvider() {
this(Injectors.getInjector().getProvider(EnvironmentVariables.class).get() );
}
public PackageAnnotationBasedTagProvider(EnvironmentVariables vars) {
super(vars);
configuration = new SystemPropertiesConfiguration(environmentVariables);
rootPackage = THUCYDIDES_TEST_ROOT.from(environmentVariables, rootDirectory);
persister = new RequirementPersister(configuration.getOutputDirectory(), rootPackage);
leafRequirements = Lists.newArrayList();
}
private Collection<Requirement> getAllRequirements() {
return getRequirementsByPath().values();
}
@Override
public Set<TestTag> getTagsFor(TestOutcome testOutcome) {
Set<TestTag> result = new HashSet<>();
for (Requirement requirement : getAllRequirements()) {
if (isMatchingRequirementFor(testOutcome, requirement)) {
result.add(TestTag.withName(humanReadableVersionOf(requirement.getName())).andType(requirement.getType()));
}
}
return result;
}
private boolean isMatchingRequirementFor(TestOutcome testOutcome, Requirement requirement) {
return (fullPathOf(requirement).matchesOrIsADescendantOf(normalizedPath(testOutcome.getPathId())))
|| (fullPathOf(requirement).matchesOrIsADescendantOf(normalizedPath(testOutcome.getPath())));
}
private String normalizedPath(String path) {
path = path.replaceAll("/",".");
for(String supportedSuffix: SUPPORTED_SUFFIXES) {
if (path.endsWith("." + supportedSuffix)) {
path = path.substring(0, path.lastIndexOf("." + supportedSuffix));
}
}
if (!path.startsWith(rootPackage)) {
path = rootPackage + "." + path;
}
return path;
}
private RequirementPathMatcher fullPathOf(Requirement requirement) {
return new RequirementPathMatcher(requirement);
}
@Override
public List<Requirement> getRequirements() {
if (requirements == null) {
requirements = loadRequirements();
}
return requirements;
}
private List<Requirement> loadRequirements() {
List<Class<?>> classes = loadClasses();
if (classes.isEmpty()) {
requirementsByPath = loadFromJSON();
} else {
loadRequirementsFromClasses(classes);
}
requirementPaths = indexRequirements(requirementsByPath);
List<Requirement> requirementsTree = buildRequirementsTree(requirementsByPath, requirementPaths);
return ImmutableList.copyOf(requirementsTree);
}
protected List<Class<?>> loadClasses() {
return ClassFinder.loadClasses().annotatedWith(Narrative.class).fromPackage(rootPackage);
}
private SortedMap<String, Requirement> loadFromJSON() {
try {
return persister.read();
} catch (IOException e) {
e.printStackTrace();
return new TreeMap<>();
}
}
private Map<Requirement, String> indexRequirements(SortedMap<String, Requirement> requirementsByPath) {
Map<Requirement, String> requirementPaths = Maps.newHashMap();
for(String path : requirementsByPath.keySet()) {
Requirement requirement = requirementsByPath.get(path);
requirementPaths.put(requirement, path);
}
return requirementPaths;
}
private void loadRequirementsFromClasses(List<Class<?>> classes) {
for (Class candidateClass : classes) {
addRequirementTo(requirementsByPath, candidateClass);
}
leafRequirements = findLeafRequirementsIn(requirementsByPath);
persistRequirementsAsJSON(requirementsByPath);
}
private void persistRequirementsAsJSON(SortedMap<String, Requirement> requirementsByPath) {
try {
persister.write(requirementsByPath);
} catch (IOException e) {
e.printStackTrace();
}
}
private List<Requirement> buildRequirementsTree(SortedMap<String, Requirement> requirementsByPath,
Map<Requirement, String> requirementPaths) {
List<Requirement> requirementsTree = Lists.newArrayList();
for (Requirement requirement : requirementsByPath.values()) {
if (isRoot(requirementPaths.get(requirement))) {
List<Requirement> children = findDirectChildrenFor(requirement, requirementsByPath, requirementPaths);
requirementsTree.add(requirement.withChildren(children));
}
}
return requirementsTree;
}
private boolean isRoot(String path) {
return !path.contains(".");
}
private List<Requirement> findDirectChildrenFor(Requirement requirement,
SortedMap<String, Requirement> requirementsByPath,
Map<Requirement, String> requirementPaths) {
List<Requirement> immediateChildren = Lists.newArrayList();
if (!isLeaf(requirement)) {
String requirementPath = requirementPaths.get(requirement);
for (String path : requirementsByPath.keySet()) {
Requirement childRequirement = requirementsByPath.get(path);
if ((childRequirement != requirement) && (isImmediateChild(requirementPath, path))) {
if (isLeaf(childRequirement)) {
immediateChildren.add(childRequirement);
} else {
immediateChildren.add(childRequirement.withChildren(findDirectChildrenFor(childRequirement,
requirementsByPath,
requirementPaths)));
}
}
}
}
return immediateChildren;
}
private boolean isLeaf(Requirement childRequirement) {
return leafRequirements.contains(childRequirement);
}
private boolean isImmediateChild(String requirementPath, String path) {
if (path.startsWith(requirementPath)) {
String trailingPath = path.replaceFirst(requirementPath + ".", "");
return (!StringUtils.isEmpty(trailingPath) && !trailingPath.contains("."));
} else {
return false;
}
}
private String getFullRequirementPath(Class candidateClass) {
return candidateClass.getName().replace(rootPackage + ".", "").replace(".package-info", "");
}
private void addRequirementTo(SortedMap<String, Requirement> requirementsByPath,
Class candidateClass) {
String fullRequirementName = getFullRequirementPath(candidateClass);
String[] packageNames = fullRequirementName.split(DOT_REGEX);
String currentPath = "";
for (int level = 0; level < packageNames.length; level++) {
currentPath = (currentPath.isEmpty()) ? packageNames[level] : Joiner.on(".").join(currentPath, packageNames[level]);
String defaultRequirementType = getDefaultType(level);
Requirement currentRequirement;
if (!requirementsByPath.containsKey(currentPath)) {
if (level < packageNames.length - 1) {
currentRequirement = newParentRequirement(currentPath, packageNames[level], level, defaultRequirementType);
requirementsByPath.put(currentPath, currentRequirement);
} else {
currentRequirement = newRequirement(candidateClass, currentPath, packageNames[level], level, defaultRequirementType);
String fullPath = getFullRequirementPath(candidateClass);
requirementsByPath.put(fullPath, currentRequirement);
}
}
}
}
private List<Requirement> findLeafRequirementsIn(Map<String, Requirement> requirementsByPath) {
List<Requirement> leafRequirements = Lists.newArrayList();
for (String path : requirementsByPath.keySet()) {
if (!longerPathExists(path, requirementsByPath.keySet())) {
leafRequirements.add(requirementsByPath.get(path));
}
}
return leafRequirements;
}
private boolean longerPathExists(String path, Set<String> paths) {
for (String requirementPath : paths) {
if (requirementPath.startsWith(path) && (requirementPath.length() > path.length())) {
return true;
}
}
return false;
}
private Requirement newParentRequirement(String requirementPath,
String packageName,
int level,
String defaultRequirementType) {
String requirementTitle = packageName;
String requirementType = defaultRequirementType;
String narrativeText = "";
String cardNumber = "";
Class candidateClass = null;
Optional<Narrative> narrative = Optional.absent();
try {
candidateClass = Class.forName(rootPackage + "." + requirementPath + ".package-info");
narrative = NarrativeFinder.forClass(candidateClass);
} catch (ClassNotFoundException ignore) {
}
return getRequirement(candidateClass, packageName, level, requirementTitle, requirementType, narrativeText, cardNumber, narrative);
}
private Requirement newRequirement(Class candidateClass,
String currentPath,
String packageName,
int level,
String defaultRequirementType) {
String requirementTitle = packageName;
String requirementType = defaultRequirementType;
String narrativeText = "";
String cardNumber = "";
Optional<Narrative> narrative = NarrativeFinder.forClass(candidateClass);
return getRequirement(candidateClass, packageName, level, requirementTitle, requirementType, narrativeText, cardNumber, narrative);
}
private Requirement getRequirement(Class candidateClass, String packageName, int level, String requirementTitle, String requirementType, String narrativeText, String cardNumber, Optional<Narrative> narrative) {
if (narrative.isPresent()) {
requirementTitle = narrative.get().title();
requirementType = narrative.get().type();
narrativeText = Joiner.on("\n").join(narrative.get().text());
cardNumber = narrative.get().cardNumber();
}
if (StringUtils.isEmpty(requirementType)) {
requirementType = getRequirementType(level, candidateClass);
}
return Requirement.named(humanReadableVersionOf(packageName))
.withOptionalCardNumber(cardNumber)
.withOptionalDisplayName(StringUtils.isEmpty(requirementTitle) ? humanReadableVersionOf(packageName) : requirementTitle)
.withType(requirementType)
.withNarrative(narrativeText);
}
private String getRequirementType(int level, Class candidateClass) {
if ((candidateClass != null) && (candidateClass.getName().endsWith(".package-info"))) {
return getDefaultType(level);
} else {
return "story";
}
}
@Override
public Optional<Requirement> getParentRequirementOf(TestOutcome testOutcome) {
if (testOutcome.getUserStory() == null
|| testOutcome.getUserStory().getStoryClassName() == null) {
return Optional.absent();
}
String name = testOutcome.getUserStory().getStoryClassName().replace(rootPackage + ".", "");
return Optional.fromNullable(getRequirementsByPath().get(name));
}
@Override
public Optional<Requirement> getRequirementFor(TestTag testTag) {
Optional<Requirement> result = Optional.absent();
for (Requirement requirement : getRequirements()) {
if (requirement.matchesTag(testTag)) {
return Optional.of(requirement);
}
}
return result;
}
public SortedMap<String, Requirement> getRequirementsByPath() {
getRequirements();
return requirementsByPath;
}
private class RequirementPathMatcher {
String requirementPath;
public RequirementPathMatcher(Requirement requirement) {
requirementPath = rootPackage + "." + requirementPaths.get(requirement);
}
public boolean matchesOrIsADescendantOf(String path) {
if (StringUtils.isNotEmpty(path)) {
return path.startsWith(requirementPath) || requirementPath.startsWith(path);
} else {
return false;
}
}
}
}