/*
* Copyright Technophobia Ltd 2012
*
* This file is part of Substeps.
*
* Substeps is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Substeps is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Substeps. If not, see <http://www.gnu.org/licenses/>.
*/
package com.technophobia.substeps.runner.syntax;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.technophobia.substeps.model.ParentStep;
import com.technophobia.substeps.model.PatternMap;
import com.technophobia.substeps.model.Step;
import com.technophobia.substeps.model.exception.DuplicatePatternException;
import com.technophobia.substeps.parser.FileContents;
import com.technophobia.substeps.runner.FeatureFileParser;
/**
* @author ian
*
* TODO: failOnDuplicateSubsteps is used inconsistently between
* processDirective and parseSubStepFile. It's also being interpreted as
* 'fail-fast' when it was intended to mean
* parse-and-(run|don't-run)-on-error.
*
*/
public class SubStepDefinitionParser {
private final Logger log = LoggerFactory.getLogger(SubStepDefinitionParser.class);
private ParentStep currentParentStep;
private final PatternMap<ParentStep> parentMap = new PatternMap<ParentStep>();
private final boolean failOnDuplicateSubsteps;
private final SyntaxErrorReporter syntaxErrorReporter;
private FileContents currentFileContents;
public SubStepDefinitionParser(final SyntaxErrorReporter syntaxErrorReporter) {
this(true, syntaxErrorReporter);
}
public SubStepDefinitionParser(final boolean failOnDuplicateSubsteps, final SyntaxErrorReporter syntaxErrorReporter) {
this.failOnDuplicateSubsteps = failOnDuplicateSubsteps;
this.syntaxErrorReporter = syntaxErrorReporter;
}
void parseSubStepFile(final File substepFile) {
this.currentFileContents = new FileContents();
try {
this.currentFileContents.readFile(substepFile);
for (int i = 0; i < this.currentFileContents.getNumberOfLines(); i++) {
// Line numbers are 1-based in FileContents
processLine(i + 1);
}
// add the last scenario in, but only if it has some steps
if (this.currentParentStep != null) {
if (this.currentParentStep.getSteps() != null && !this.currentParentStep.getSteps().isEmpty()) {
try {
storeForPatternOrThrowException(this.currentParentStep.getParent().getPattern(),
this.currentParentStep);
} catch (final DuplicatePatternException ex) {
this.syntaxErrorReporter.reportSubstepsError(ex);
if (this.failOnDuplicateSubsteps) {
throw ex;
}
}
} else {
this.log.warn("Ignoring substep definition [" + this.currentParentStep.getParent().getLine()
+ "] as it has no steps");
}
// we're moving on to another file, so set this to null.
// TODO - pass this around rather than stash the state
this.currentParentStep = null;
}
} catch (final FileNotFoundException e) {
this.log.error(e.getMessage(), e);
} catch (final IOException e) {
this.log.error(e.getMessage(), e);
}
}
public PatternMap<ParentStep> loadSubSteps(final File definitions) {
final List<File> substepsFiles = FileUtils.getFiles(definitions, ".substeps");
for (final File f : substepsFiles) {
parseSubStepFile(f);
}
return this.parentMap;
}
private void processLine(final int lineNumberIdx) {
final String line = this.currentFileContents.getLineAt(lineNumberIdx);
if (this.log.isTraceEnabled()) {
this.log.trace("substep line[" + line + "] @ " + lineNumberIdx + ":"
+ this.currentFileContents.getFile().getName());
}
if (line != null && line.length() > 0) {
// does this line begin with any of annotation values that we're
// interested in ?
// pick out the first word
final String trimmed = FeatureFileParser.stripComments(line.trim());
if (trimmed != null && trimmed.length() > 0 && !trimmed.startsWith("#")) {
processTrimmedLine(trimmed, lineNumberIdx);
}
}
}
private void processTrimmedLine(final String trimmed, final int lineNumberIdx) {
// TODO convert <> into regex wildcards
final int scolon = trimmed.indexOf(':');
boolean lineProcessed = false;
if (scolon > 0) {
// is this a directive line
final String word = trimmed.substring(0, scolon);
final String remainder = trimmed.substring(scolon + 1);
final Directive d = isDirective(word);
if (d != null) {
final String trimmedRemainder = remainder.trim();
if (!Strings.isNullOrEmpty(trimmedRemainder)) {
processDirective(d, remainder, lineNumberIdx);
lineProcessed = true;
}
}
}
if (!lineProcessed) {
if (this.currentParentStep != null) {
final int sourceOffset = this.currentFileContents.getSourceStartOffsetForLineIndex(lineNumberIdx);
// no context at the mo
this.currentParentStep.addStep(new Step(trimmed, true, this.currentFileContents.getFile(),
lineNumberIdx, sourceOffset));
}
}
}
private void processDirective(final Directive d, final String remainder, final int lineNumberIdx) {
this.currentDirective = d;
switch (this.currentDirective) {
case DEFINITION: {
// build up a Step from the remainder
final int sourceOffset = this.currentFileContents.getSourceStartOffsetForLineIndex(lineNumberIdx);
final Step parent = new Step(remainder, true, this.currentFileContents.getFile(), lineNumberIdx,
sourceOffset);
if (this.currentParentStep != null) {
final String newPattern = this.currentParentStep.getParent().getPattern();
try {
storeForPatternOrThrowException(newPattern, this.currentParentStep);
} catch (final DuplicatePatternException ex) {
this.syntaxErrorReporter.reportSubstepsError(ex);
if (this.failOnDuplicateSubsteps) {
throw ex;
}
}
}
this.currentParentStep = new ParentStep(parent);
break;
}
default: // whatever
}
}
/*
* private void processDirective(final Directive d, final String remainder,
* final File source, final int lineNumber) { this.currentDirective = d;
*
* switch (this.currentDirective) {
*
* case DEFINITION: {
*
* // build up a Step from the remainder
*
* final Step parent = new Step(remainder, true, source, lineNumber);
*
* if (this.currentParentStep != null) { final String newPattern =
* this.currentParentStep.getParent() .getPattern(); // check for existing
* values if (this.parentMap.containsPattern(newPattern)) { final ParentStep
* otherValue = this.parentMap .getValueForPattern(newPattern);
*
* this.log.error("duplicate patterns detected: " + newPattern + " in : " +
* otherValue.getSubStepFile() + " and " +
* this.currentParentStep.getSubStepFile());
*
* }
*
* storeForPatternOrReportFailure(source, newPattern,
* this.currentParentStep); }
*
* this.currentParentStep = new ParentStep(parent);
*
* break; } } }
*/
private void storeForPatternOrThrowException(final String newPattern, final ParentStep parentStep)
throws DuplicatePatternException {
if (!this.parentMap.containsPattern(newPattern)) {
this.parentMap.put(newPattern, parentStep);
} else {
throw new DuplicatePatternException(newPattern, this.parentMap.getValueForPattern(newPattern), parentStep);
}
}
private static enum Directive {
// @formatter:off
DEFINITION("Define");
// @formatter:on
Directive(final String name) {
this.name = name;
}
private final String name;
}
private Directive currentDirective = null;
private Directive isDirective(final String word) {
Directive rtn = null;
for (final Directive d : Directive.values()) {
if (word.equalsIgnoreCase(d.name)) {
rtn = d;
break;
}
}
return rtn;
}
}