StoryTimeouts.java

  1. package org.jbehave.core.embedder;

  2. import static java.util.Arrays.asList;
  3. import static java.util.regex.Pattern.compile;

  4. import java.util.ArrayList;
  5. import java.util.HashMap;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.regex.Matcher;
  9. import java.util.regex.Pattern;
  10. import java.util.regex.PatternSyntaxException;

  11. import org.apache.commons.lang3.builder.ToStringBuilder;
  12. import org.apache.commons.lang3.builder.ToStringStyle;
  13. import org.codehaus.plexus.util.StringUtils;
  14. import org.jbehave.core.model.Story;

  15. public class StoryTimeouts {

  16.     private static final String COMMA = ",";
  17.     private EmbedderControls embedderControls;
  18.     private EmbedderMonitor embedderMonitor;
  19.     private List<TimeoutParser> parsers = new ArrayList<>();

  20.     public StoryTimeouts(EmbedderControls embedderControls,
  21.             EmbedderMonitor embedderMonitor) {
  22.         this.embedderControls = embedderControls;
  23.         this.embedderMonitor = embedderMonitor;
  24.         configureDefaultParsers();
  25.     }

  26.     private void configureDefaultParsers() {
  27.         this.parsers.addAll(asList(new SimpleTimeoutParser(), new DigitTimeoutParser()));
  28.     }

  29.     public StoryTimeouts withParsers(TimeoutParser...parsers) {
  30.         this.parsers.addAll(0, asList(parsers));
  31.         return this;
  32.     }
  33.    
  34.     public long getTimeoutInSecs(Story story) {
  35.         Map<String, StoryTimeout> timeouts = asMap(embedderControls
  36.                 .storyTimeouts());
  37.        
  38.         // look for timeout by path
  39.         for (StoryTimeout timeout : timeouts.values()) {
  40.             if (timeout.allowedByPath(story.getPath())) {
  41.                 long timeoutInSecs = timeout.getTimeoutInSecs();
  42.                 embedderMonitor.usingTimeout(story.getName(), timeoutInSecs);
  43.                 return timeoutInSecs;
  44.             }
  45.         }

  46.         // look for default timeout
  47.         for (StoryTimeout timeout : timeouts.values()) {
  48.             if (timeout.isDefault()) {
  49.                 long timeoutInSecs = timeout.getTimeoutInSecs();
  50.                 embedderMonitor.usingTimeout(story.getName(), timeoutInSecs);
  51.                 return timeoutInSecs;
  52.             }
  53.         }

  54.         // default to 300 for backward compatibility
  55.         long timeoutInSecs = 300;
  56.         embedderMonitor.usingTimeout(story.getName(), timeoutInSecs);
  57.         return timeoutInSecs;
  58.     }

  59.     private Map<String, StoryTimeout> asMap(String timeoutsAsString) {
  60.         Map<String, StoryTimeout> timeouts = new HashMap<>();
  61.         if (StringUtils.isBlank(timeoutsAsString)) {
  62.             return timeouts;
  63.         }        
  64.         for (String timeoutAsString : timeoutsAsString.split(COMMA)) {
  65.             StoryTimeout timeout = new StoryTimeout(timeoutAsString, parsers);
  66.             timeouts.put(timeout.getPathPattern(), timeout);
  67.         }
  68.         return timeouts;
  69.     }

  70.     public static class StoryTimeout {
  71.         private static final String COLON = ":";
  72.         private boolean isDefault;
  73.         private String pathPattern = "";
  74.         private String timeout = "0";
  75.         private String timeoutAsString;
  76.         private List<TimeoutParser> parsers;
  77.    
  78.         public StoryTimeout(String timeoutAsString, List<TimeoutParser> parsers) {
  79.             this.timeoutAsString = timeoutAsString;
  80.             this.parsers = parsers;
  81.             if (timeoutAsString.contains(COLON)) {
  82.                 String[] timeoutByPath = timeoutAsString.split(COLON);
  83.                 pathPattern = timeoutByPath[0];
  84.                 timeout = timeoutByPath[1];
  85.             } else {
  86.                 isDefault = true;
  87.                 timeout = timeoutAsString;
  88.             }
  89.         }

  90.         public boolean allowedByPath(String path) {
  91.             if (path != null) {
  92.                 return path.matches(regexOf(pathPattern));
  93.             }
  94.             return false;
  95.         }

  96.         public boolean isDefault() {
  97.             return isDefault;
  98.         }

  99.         public String getPathPattern() {
  100.             return pathPattern;
  101.         }

  102.         public long getTimeoutInSecs() {
  103.             for (TimeoutParser parser : parsers) {
  104.                 if (parser.isValid(timeout)) {
  105.                     return parser.asSeconds(timeout);
  106.                 }
  107.             }
  108.             throw new TimeoutFormatException("No format found for timeout: " + timeout);
  109.         }

  110.         public String getTimeoutAsString() {
  111.             return timeoutAsString;
  112.         }

  113.         private String regexOf(String pattern) {
  114.             try {
  115.                 // check if pattern is already a valid regex
  116.                 Pattern.compile(pattern);
  117.                 return pattern;
  118.             } catch (PatternSyntaxException e) {
  119.                 // assume Ant-style pattern: **/path/*.story
  120.                 return pattern.replace("*", ".*");
  121.             }
  122.         }

  123.         @Override
  124.         public String toString() {
  125.             return ToStringBuilder.reflectionToString(this,
  126.                     ToStringStyle.SHORT_PREFIX_STYLE);
  127.         }
  128.     }

  129.     public static interface TimeoutParser {

  130.         boolean isValid(String timeout);

  131.         long asSeconds(String timeout);

  132.     }

  133.     /**
  134.      * A simple parser for timeouts of format: 1d 2h 30m 15s.
  135.      */
  136.     public static class SimpleTimeoutParser implements TimeoutParser {

  137.         private static final String UNIT_PATTERN = "[a-zA-Z]+";
  138.         private static final Pattern TIMEOUT_PATTERN = compile("(\\d+)\\s*("
  139.                 + UNIT_PATTERN + ")");
  140.         private Map<String, Long> units = new HashMap<>();

  141.         public SimpleTimeoutParser() {
  142.             addUnit("d", 24 * 3600).addUnit("h", 3600).addUnit("m", 60)
  143.                     .addUnit("s", 1);
  144.         }

  145.         private SimpleTimeoutParser addUnit(String unit, long value) {
  146.             if (!unit.matches(UNIT_PATTERN)) {
  147.                 throw new TimeoutFormatException("Unit '" + unit
  148.                         + "' must be a non-numeric word");
  149.             }
  150.             if (value < 0) {
  151.                 throw new TimeoutFormatException("Unit value '" + value
  152.                         + "' cannot be negative");
  153.             }
  154.             units.put(unit, Long.valueOf(value));
  155.             return this;
  156.         }

  157.         @Override
  158.         public boolean isValid(String timeout) {
  159.             return TIMEOUT_PATTERN.matcher(timeout).find();
  160.         }

  161.         @Override
  162.         public long asSeconds(String timeout) {
  163.             long total = 0;
  164.             Matcher matcher = TIMEOUT_PATTERN.matcher(timeout);
  165.             while (matcher.find()) {
  166.                 long value = Long.parseLong(matcher.group(1));
  167.                 String unit = matcher.group(2);
  168.                 if (!units.containsKey(unit)) {
  169.                     throw new TimeoutFormatException("Unrecognized unit: "
  170.                             + unit);
  171.                 }
  172.                 total += units.get(unit).longValue() * value;
  173.             }
  174.             return total;
  175.         }

  176.     }

  177.     /**
  178.      * A digit parser for timeouts
  179.      */
  180.     public static class DigitTimeoutParser implements TimeoutParser {

  181.         private static final Pattern TIMEOUT_PATTERN = compile("(\\d+)");

  182.         @Override
  183.         public boolean isValid(String timeout) {
  184.             return TIMEOUT_PATTERN.matcher(timeout).find();
  185.         }

  186.         @Override
  187.         public long asSeconds(String timeout) {
  188.             return Long.parseLong(timeout);
  189.         }

  190.     }

  191.     @SuppressWarnings("serial")
  192.     public static class TimeoutFormatException extends IllegalArgumentException {

  193.         public TimeoutFormatException(String message) {
  194.             super(message);
  195.         }

  196.     }
  197. }