package net.thucydides.core.requirements.model;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.util.List;
import static net.thucydides.core.requirements.RequirementsPath.fileSystemPathElements;
/**
* Load a narrative text from a directory.
* A narrative is a text file that describes a requirement, feature, or epic, or whatever terms you are using in your
* project. The directory structure itself is used to organize capabilities into features, and so on. At the leaf
* level, the directory will contain story files (e.g. JBehave stories, JUnit test cases, etc). At each level, a
* "narrative.txt" file provides a description.
*
*/
public class NarrativeReader {
private static final String NEW_LINE = System.getProperty("line.separator");
private static final String BACKSLASH = "\\\\";
private static final String FORWARDSLASH = "/";
private final String rootDirectory;
private final List<String> requirementTypes;
protected NarrativeReader(String rootDirectory, List<String> requirementTypes) {
this.rootDirectory = rootDirectory;
this.requirementTypes = ImmutableList.copyOf(requirementTypes);
}
public static NarrativeReader forRootDirectory(String rootDirectory) {
return new NarrativeReader(rootDirectory, RequirementsConfiguration.DEFAULT_CAPABILITY_TYPES);
}
public NarrativeReader withRequirementTypes(List<String> requirementTypes) {
return new NarrativeReader(this.rootDirectory, requirementTypes);
}
public Optional<Narrative> loadFrom(File directory) {
return loadFrom(directory, 0);
}
public Optional<Narrative> loadFrom(File directory, int requirementsLevel) {
File[] narrativeFiles = directory.listFiles(calledNarrativeDotTxt());
if (narrativeFiles.length == 0) {
return Optional.absent();
} else {
return narrativeLoadedFrom(narrativeFiles[0], requirementsLevel);
}
}
public Optional<Narrative> loadFromStoryFile(File storyFile) {
return narrativeLoadedFrom(storyFile, "story");
}
private Optional<Narrative> narrativeLoadedFrom(File narrativeFile, int requirementsLevel) {
String type = directoryLevelInRequirementsHierarchy(narrativeFile, requirementsLevel);
return narrativeLoadedFrom(narrativeFile, type);
}
private Optional<Narrative> narrativeLoadedFrom(File narrativeFile, String type) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(narrativeFile), "UTF-8"));
List<String> lines = readPreambleFrom(reader);
String title = null;
String cardNumber = findCardNumberIn(lines);
List<String> versionNumbers = findVersionNumberIn(lines);
Optional<String> titleLine = readOptionalTitleFrom(lines);
if (titleLine.isPresent()) {
title = titleLine.get();
}
String text = readNarrativeFrom(lines);
reader.close();
return Optional.of(new Narrative(Optional.fromNullable(title),
Optional.fromNullable(cardNumber),
versionNumbers,
type, text));
} catch(IOException ex) {
ex.printStackTrace();
}
return Optional.absent();
}
private String findCardNumberIn(List<String> lines) {
String cardNumber = null;
for(String line: lines) {
String normalizedLine = line.toUpperCase();
if (normalizedLine.startsWith("@ISSUES")) {
String issueList = normalizedLine.replace("@ISSUES","").trim();
List<String> issues = Lists.newArrayList(Splitter.on(",").trimResults().split(issueList));
if (!issues.isEmpty()) {
cardNumber = issues.get(0);
}
} else if (normalizedLine.startsWith("@ISSUE")) {
String issueNumber = normalizedLine.replace("@ISSUE","").trim();
if (!StringUtils.isEmpty(issueNumber)) {
cardNumber = issueNumber;
}
}
}
return cardNumber;
}
private List<String> findVersionNumberIn(List<String> lines) {
for(String line: lines) {
String normalizedLine = line.toUpperCase();
if (normalizedLine.startsWith("@VERSIONS")) {
String versionList = line.substring("@VERSIONS".length()).trim();
return Lists.newArrayList(Splitter.on(",").trimResults().split(versionList));
}
}
return ImmutableList.of();
}
private String readNarrativeFrom(List<String> lines) {
StringBuilder description = new StringBuilder();
for(String line : lines) {
if (!isMarkup(line) && !isAnnotation(line)) {
description.append(line);
description.append(NEW_LINE);
}
}
return description.toString();
}
private boolean isAnnotation(String line) {
return normalizedLine(line).startsWith("@");
}
private Optional<String> readOptionalTitleFrom(List<String> lines) {
if (!lines.isEmpty()) {
String firstLine = lines.get(0);
if (!isMarkup(firstLine)) {
lines.remove(0);
return Optional.of(stripFeatureFrom(firstLine));
}
}
return Optional.absent();
}
private String stripFeatureFrom(String firstLine) {
return (firstLine.toLowerCase().startsWith("feature:")) ? firstLine.substring(8).trim() : firstLine;
}
private boolean isMarkup(String line) {
String normalizedLine = normalizedLine(line);
return normalizedLine.startsWith("narrative:")
|| normalizedLine.startsWith("givenstory:")
|| normalizedLine.startsWith("meta")
|| normalizedLine.startsWith("@")
|| normalizedLine.startsWith("givenstories:");
}
private List<String> readPreambleFrom(BufferedReader reader) throws IOException {
List<String> usefulLines = Lists.newArrayList();
boolean preambleFinished = false;
while (!preambleFinished) {
String nextLine = reader.readLine();
if (nextLine == null) {
preambleFinished = true;
} else {
if (preambleFinishedAt(nextLine)) {
preambleFinished = true;
} else if (thereIsUsefulInformationIn(nextLine)) {
usefulLines.add(nextLine);
}
}
}
return usefulLines;
}
private boolean preambleFinishedAt(String nextLine) {
return normalizedLine(nextLine).startsWith("scenario:");
}
private String normalizedLine(String nextLine) {
return nextLine.trim().toLowerCase();
}
private boolean thereIsUsefulInformationIn(String nextLine) {
String normalizedText = normalizedLine(nextLine);
return !normalizedText.isEmpty()
&& !normalizedText.startsWith("meta:")
&& !(normalizedText.startsWith("@") && (!normalizedText.startsWith("@issue") && (!normalizedText.startsWith("@versions"))));
}
private String directoryLevelInRequirementsHierarchy(File narrativeFile, int requirementsLevel) {
String normalizedNarrativePath = normalized(narrativeFile.getAbsolutePath());
String normalizedRootPath = normalized(rootDirectory);
int rootDirectoryStart = normalizedNarrativePath.lastIndexOf(normalizedRootPath);
int rootDirectoryEnd = (rootDirectoryStart >= 0) ? rootDirectoryStart + normalizedRootPath.length() : 0;
String relativeNarrativePath = normalizedNarrativePath.substring(rootDirectoryEnd);
int directoryCount = fileSystemPathElements(relativeNarrativePath).size() - 1;
int level = requirementsLevel + directoryCount - 1;
return getRequirementTypeForLevel(level);
}
private String normalized(String path) {
return path.replaceAll(BACKSLASH, FORWARDSLASH);
}
private String getRequirementTypeForLevel(int level) {
if (level > requirementTypes.size() - 1) {
return requirementTypes.get(requirementTypes.size() - 1);
} else {
return requirementTypes.get(level);
}
}
private FilenameFilter calledNarrativeDotTxt() {
return new FilenameFilter() {
public boolean accept(File file, String name) {
return name.toLowerCase().equals("narrative.txt");
}
};
}
}