1   /*
2    * The MIT License
3    *
4    * Copyright (c) <2010> <tap4j>
5    * 
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   * 
13   * The above copyright notice and this permission notice shall be included in
14   * all copies or substantial portions of the Software.
15   * 
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22   * THE SOFTWARE.
23   */
24  package org.tap4j.parser;
25  
26  import java.io.File;
27  import java.util.Scanner;
28  import java.util.regex.Matcher;
29  
30  import org.apache.commons.lang.StringUtils;
31  import org.tap4j.model.BailOut;
32  import org.tap4j.model.Comment;
33  import org.tap4j.model.Directive;
34  import org.tap4j.model.Footer;
35  import org.tap4j.model.Header;
36  import org.tap4j.model.Plan;
37  import org.tap4j.model.SkipPlan;
38  import org.tap4j.model.TestResult;
39  import org.tap4j.model.TestSet;
40  import org.tap4j.model.Text;
41  import org.tap4j.util.DirectiveValues;
42  import org.tap4j.util.StatusValues;
43  
44  /**
45   * TAP 13 parser.
46   * 
47   * @since 1.0
48   */
49  public class Tap13Parser implements Parser {
50  
51      private boolean isFirstLine = true;
52  
53      private boolean planBeforeTestResult = false;
54  
55      // Helper String to check the Footer
56      private String lastLine = null;
57  
58      /**
59       * Test Set.
60       */
61      private TestSet testSet;
62  
63      /**
64       * Default constructor. Calls the init method.
65       */
66      public Tap13Parser() {
67          super();
68          this.init();
69      }
70  
71      /**
72       * Called from the constructor and everytime a new TAP Stream (file or
73       * string) is processed.
74       */
75      public final void init() {
76          this.isFirstLine = true;
77          this.planBeforeTestResult = false;
78          this.lastLine = null;
79          this.testSet = new TestSet();
80      }
81  
82      /*
83       * (non-Javadoc)
84       * @see org.tap4j.TapConsumer#getTestSet()
85       */
86      public TestSet getTestSet() {
87          return this.testSet;
88      }
89  
90      /*
91       * (non-Javadoc)
92       * @see org.tap4j.TapConsumer#parseLine(java.lang.String)
93       */
94      public void parseLine(String tapLine) {
95          if (StringUtils.isEmpty(tapLine)) {
96              return;
97          }
98  
99          Matcher matcher = null;
100 
101         // Comment
102         matcher = COMMENT_PATTERN.matcher(tapLine);
103         if (matcher.matches()) {
104             this.extractComment(matcher);
105             return;
106         }
107 
108         // Last line that is not a comment.
109         lastLine = tapLine;
110 
111         // Header
112         matcher = HEADER_PATTERN.matcher(tapLine);
113         if (matcher.matches()) {
114 
115             this.checkTAPHeaderParsingLocationAndDuplicity();
116 
117             this.extractHeader(matcher);
118             this.isFirstLine = false;
119             return;
120         }
121 
122         // Plan
123         matcher = PLAN_PATTERN.matcher(tapLine);
124         if (matcher.matches()) {
125 
126             this.checkTAPPlanDuplicity();
127 
128             this.checkIfTAPPlanIsSetBeforeTestResultsOrBailOut();
129 
130             this.extractPlan(matcher);
131             this.isFirstLine = false;
132             return;
133         }
134 
135         // Test Result
136         matcher = TEST_RESULT_PATTERN.matcher(tapLine);
137         if (matcher.matches()) {
138             this.extractTestResult(matcher);
139             return;
140         }
141 
142         // Bail Out
143         matcher = BAIL_OUT_PATTERN.matcher(tapLine);
144         if (matcher.matches()) {
145             this.extractBailOut(matcher);
146             return;
147         }
148 
149         // Footer
150         matcher = FOOTER_PATTERN.matcher(tapLine);
151         if (matcher.matches()) {
152             this.extractFooter(matcher);
153             return;
154         }
155 
156         // Any text. It should not be parsed by the consumer.
157         final Text text = new Text(tapLine);
158         this.testSet.getTapLines().add(text);
159     }
160 
161     /**
162      * Checks if the TAP Plan is set before any Test Result or Bail Out.
163      */
164     protected void checkIfTAPPlanIsSetBeforeTestResultsOrBailOut() {
165         if (this.testSet.getTestResults().size() <= 0 &&
166             this.testSet.getBailOuts().size() <= 0) {
167             this.planBeforeTestResult = true;
168         }
169     }
170 
171     /**
172      * Checks the Header location and duplicity. The Header must be the first
173      * element and cannot occurs more than on time. However the Header is
174      * optional.
175      */
176     protected void checkTAPHeaderParsingLocationAndDuplicity() {
177         if (this.testSet.getHeader() != null) {
178             throw new ParserException("Duplicated TAP Header found.");
179         }
180         if (!isFirstLine) {
181             throw new ParserException(
182                                       "Invalid position of TAP Header. It must be the first element (apart of Comments) in the TAP Stream.");
183         }
184     }
185 
186     /**
187      * Checks if there are more than one TAP Plan in the TAP Stream.
188      */
189     protected void checkTAPPlanDuplicity() {
190         if (this.testSet.getPlan() != null) {
191             throw new ParserException("Duplicated TAP Plan found.");
192         }
193     }
194 
195     /**
196      * This method is called after the TAP Stream has already been parsed. So we
197      * just check if the plan was found before test result or bail outs. If so,
198      * skip this check. Otherwise, we shall check if the last line is the TAP
199      * Plan.
200      * 
201      * @deprecated
202      */
203     protected void checkTAPPlanPosition() {
204         if (!this.planBeforeTestResult) {
205             Matcher matcher = PLAN_PATTERN.matcher(lastLine);
206 
207             if (matcher.matches()) {
208                 return; // OK
209             }
210 
211             throw new ParserException("Invalid position of TAP Plan.");
212         }
213     }
214 
215     /**
216      * Checks if TAP Plan has been set.
217      * 
218      * @throws ParserException if TAP Plan has not been set.
219      */
220     protected void checkTAPPlanIsSet() {
221         if (this.testSet.getPlan() == null) {
222             throw new ParserException("Missing TAP Plan.");
223         }
224     }
225 
226     /**
227      * Extracts the Header from a TAP Line.
228      * 
229      * @param matcher REGEX Matcher.
230      */
231     protected void extractHeader(Matcher matcher) {
232         final Integer version = Integer.parseInt(matcher.group(1));
233 
234         final Header header = new Header(version);
235 
236         final String commentToken = matcher.group(2);
237 
238         if (commentToken != null) {
239             String text = matcher.group(3);
240             final Comment comment = new Comment(text);
241             header.setComment(comment);
242         }
243 
244         this.testSet.setHeader(header);
245     }
246 
247     /**
248      * @param matcher REGEX Matcher.
249      */
250     protected void extractPlan(Matcher matcher) {
251         Integer initialTest = Integer.parseInt(matcher.group(1));
252         Integer lastTest = Integer.parseInt(matcher.group(3));
253 
254         Plan plan = null;
255         plan = new Plan(initialTest, lastTest);
256 
257         String skipToken = matcher.group(4);
258         if (skipToken != null) {
259             String reason = matcher.group(5);
260             final SkipPlan skip = new SkipPlan(reason);
261             plan.setSkip(skip);
262         }
263 
264         String commentToken = matcher.group(6);
265         if (commentToken != null) {
266             String text = matcher.group(7);
267             final Comment comment = new Comment(text);
268             plan.setComment(comment);
269         }
270 
271         this.testSet.setPlan(plan);
272     }
273 
274     /**
275      * @param matcher REGEX Matcher.
276      */
277     protected void extractTestResult(Matcher matcher) {
278         TestResult testResult = null;
279 
280         final String okOrNotOk = matcher.group(1);
281         StatusValues status = null;
282         if (okOrNotOk.trim().equals("ok")) {
283             status = StatusValues.OK;
284         } else // regex mate...
285         {
286             status = StatusValues.NOT_OK;
287         }
288 
289         Integer testNumber = this.getTestNumber(matcher.group(2));
290 
291         testResult = new TestResult(status, testNumber);
292 
293         testResult.setDescription(matcher.group(3));
294 
295         String directiveToken = matcher.group(4);
296         if (directiveToken != null) {
297             String directiveText = matcher.group(5);
298             DirectiveValues directiveValue = null;
299             if (directiveText.trim().equalsIgnoreCase("todo")) {
300                 directiveValue = DirectiveValues.TODO;
301             } else {
302                 directiveValue = DirectiveValues.SKIP;
303             }
304             String reason = matcher.group(6);
305             Directive directive = new Directive(directiveValue, reason);
306             testResult.setDirective(directive);
307         }
308 
309         String commentToken = matcher.group(7);
310         if (commentToken != null) {
311             String text = matcher.group(8);
312             final Comment comment = new Comment(text);
313             comment.setInline(Boolean.TRUE);
314             testResult.addComment(comment);
315         }
316 
317         this.testSet.addTestResult(testResult);
318         this.testSet.addTapLine(testResult);
319     }
320 
321     /**
322      * Returns the test number out from an input String. If the string is null
323      * or equals "" this method returns the next test result number. Otherwise
324      * it will return the input String value parsed to an Integer.
325      * 
326      * @param testNumber
327      * @return
328      */
329     private Integer getTestNumber(String testNumber) {
330         Integer integerTestNumber = null;
331         if (StringUtils.isEmpty(testNumber)) {
332             integerTestNumber = (this.testSet.getTestResults().size() + 1);
333         } else {
334             integerTestNumber = Integer.parseInt(testNumber);
335         }
336         return integerTestNumber;
337     }
338 
339     /**
340      * @param matcher REGEX Matcher.
341      */
342     protected void extractBailOut(Matcher matcher) {
343         String reason = matcher.group(1);
344 
345         BailOut bailOut = new BailOut(reason);
346 
347         String commentToken = matcher.group(2);
348 
349         if (commentToken != null) {
350             String text = matcher.group(3);
351             Comment comment = new Comment(text);
352             bailOut.setComment(comment);
353         }
354 
355         this.testSet.addBailOut(bailOut);
356         this.testSet.addTapLine(bailOut);
357     }
358 
359     /**
360      * @param matcher REGEX Matcher.
361      */
362     protected void extractComment(Matcher matcher) {
363         String text = matcher.group(1);
364         Comment comment = new Comment(text);
365 
366         this.testSet.addComment(comment);
367         this.testSet.addTapLine(comment);
368     }
369 
370     /**
371      * Simply extracts the footer from the TAP line.
372      * 
373      * @param matcher REGEX Matcher.
374      */
375     protected void extractFooter(Matcher matcher) {
376         String text = matcher.group(1);
377         Footer footer = new Footer(text);
378 
379         final String commentToken = matcher.group(2);
380 
381         if (commentToken != null) {
382             String commentText = matcher.group(3);
383             final Comment comment = new Comment(commentText);
384             footer.setComment(comment);
385         }
386 
387         this.testSet.setFooter(footer);
388     }
389 
390     /*
391      * (non-Javadoc)
392      * @see org.tap4j.TapConsumer#parseTapStream(java.lang.String)
393      */
394     public TestSet parseTapStream(String tapStream) {
395 
396         this.init();
397 
398         Scanner scanner = null;
399 
400         try {
401             scanner = new Scanner(tapStream);
402             String line = null;
403 
404             while (scanner.hasNextLine()) {
405                 line = scanner.nextLine();
406                 if (StringUtils.isNotEmpty(line)) {
407                     this.parseLine(line);
408                 }
409             }
410             this.postProcess();
411         } catch (Exception e) {
412             throw new ParserException("Error parsing TAP Stream: " +
413                                       e.getMessage(), e);
414         } finally {
415             if (scanner != null) {
416                 scanner.close();
417             }
418         }
419 
420         return this.getTestSet();
421 
422     }
423 
424     /*
425      * (non-Javadoc)
426      * @see org.tap4j.TapConsumer#parseFile(java.io.File)
427      */
428     public TestSet parseFile(File tapFile) {
429 
430         this.init();
431 
432         Scanner scanner = null;
433 
434         try {
435             scanner = new Scanner(tapFile);
436             String line = null;
437 
438             while (scanner.hasNextLine()) {
439                 line = scanner.nextLine();
440                 if (StringUtils.isNotBlank(line)) {
441                     this.parseLine(line);
442                 }
443             }
444             this.postProcess();
445         } catch (Exception e) {
446             throw new ParserException("Error parsing TAP Stream: " +
447                                       e.getMessage(), e);
448         } finally {
449             if (scanner != null) {
450                 scanner.close();
451             }
452         }
453 
454         return this.getTestSet();
455     }
456 
457     /**
458      * @throws org.tap4j.consumer.TapConsumerException
459      */
460     protected void postProcess() {
461         // deprecated for better interoperability with Perl done_testing()
462         // this.checkTAPPlanPosition();
463         this.checkTAPPlanIsSet();
464     }
465 
466 }