View Javadoc
1   package io.guixer.lang;
2   
3   import static com.google.common.base.Preconditions.checkNotNull;
4   import static com.google.common.collect.Lists.newArrayList;
5   import static com.google.common.collect.Maps.newHashMap;
6   import static io.guixer.lang.GuixerSyntaxErrorType.GROUP_NAME_SHOULD_NOT_BE_EMPTY;
7   import static io.guixer.lang.GuixerSyntaxErrorType.GROUP_NAME_SHOULD_START_BY_AN_UPPERCASE_CHARACTER;
8   import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_ASSERT_ABSENT_SHOULD_NOT_BE_MIXED;
9   import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_ASSERT_PRESENT_SHOULD_NOT_BE_MIXED;
10  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_CALL_SHOULD_NOT_BE_MIXED;
11  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_CLEAR_SHOULD_NOT_BE_MIXED;
12  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_CLICK_SHOULD_NOT_BE_MIXED;
13  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_GET_SHOULD_NOT_BE_MIXED;
14  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_SEND_KEYS_SHOULD_NOT_BE_MIXED;
15  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_SLEEP_SHOULD_NOT_BE_MIXED;
16  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_SWITCH_TO_FRAME_SHOULD_NOT_BE_MIXED;
17  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_WAIT_FOR_NOT_SHOULD_NOT_BE_MIXED;
18  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_WAIT_FOR_SHOULD_NOT_BE_MIXED;
19  import static net.avcompris.commons3.databeans.DataBeans.instantiate;
20  import static org.apache.commons.lang3.StringUtils.isBlank;
21  import static org.apache.commons.lang3.StringUtils.join;
22  import static org.apache.commons.lang3.StringUtils.substringAfter;
23  import static org.apache.commons.lang3.StringUtils.substringBefore;
24  import static org.apache.commons.lang3.StringUtils.substringBetween;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.util.List;
29  import java.util.Map;
30  
31  import javax.annotation.Nullable;
32  
33  import org.apache.commons.lang3.NotImplementedException;
34  
35  import com.google.common.collect.ImmutableList;
36  
37  import io.guixer.lang.command.AssertAbsentCommand;
38  import io.guixer.lang.command.AssertFalseCommand;
39  import io.guixer.lang.command.AssertPresentCommand;
40  import io.guixer.lang.command.AssertTrueCommand;
41  import io.guixer.lang.command.AttributeCommand;
42  import io.guixer.lang.command.CallCommand;
43  import io.guixer.lang.command.ClearCommand;
44  import io.guixer.lang.command.ClickCommand;
45  import io.guixer.lang.command.Command;
46  import io.guixer.lang.command.ExecuteScriptCommand;
47  import io.guixer.lang.command.ExtCommand;
48  import io.guixer.lang.command.FailureCommand;
49  import io.guixer.lang.command.GetCommand;
50  import io.guixer.lang.command.MessageCommand;
51  import io.guixer.lang.command.SendKeysCommand;
52  import io.guixer.lang.command.SetMaskedVariableCommand;
53  import io.guixer.lang.command.SetVariableCommand;
54  import io.guixer.lang.command.SleepCommand;
55  import io.guixer.lang.command.SuccessCommand;
56  import io.guixer.lang.command.SwitchToFrameCommand;
57  import io.guixer.lang.command.TagCommand;
58  import io.guixer.lang.command.WaitForCommand;
59  import io.guixer.lang.command.WaitForNotCommand;
60  import io.guixer.types.AttributeScope;
61  
62  public abstract class AbstractGuixerScenarioLoader {
63  
64  	public static GuixerScenario load(
65  		final File yamlFile,
66  		final MyYamlStrategy yamlStrategy
67  	) throws IOException, GuixerSyntaxException {
68  
69  		checkNotNull(yamlFile, "yamlFile");
70  
71  		new GuixerSyntaxChecker().check(yamlFile);
72  
73  		checkNotNull(yamlStrategy, "yamlStrategy");
74  
75  		final MyYaml yaml = yamlStrategy.loadYaml(yamlFile);
76  
77  		final String scenarioName = yaml.get("name").asString();
78  
79  		final Map<String, String> environmentVariables = newHashMap();
80  
81  		if (yaml.has("environment")) {
82  
83  			final MyYaml environmentYaml = yaml.get("environment");
84  
85  			for (final String key : environmentYaml.keysAsStrings()) {
86  
87  				final String value = environmentYaml.get(key).asString();
88  
89  				environmentVariables.put(key, value);
90  			}
91  		}
92  
93  		final Map<String, String> attributes = newHashMap();
94  
95  		if (yaml.has("attributes")) {
96  
97  			final MyYaml attributesYaml = yaml.get("attributes");
98  
99  			for (final String key : attributesYaml.keysAsStrings()) {
100 
101 				@Nullable
102 				final String value = attributesYaml.get(key).isArray() //
103 						? null // empty array -> tag
104 						: attributesYaml.get(key).asString();
105 
106 				attributes.put(key, value);
107 			}
108 		}
109 
110 		final MutableGuixerScenario scenario = instantiate(MutableGuixerScenario.class) //
111 				.setName(scenarioName) //
112 				.setEnvironmentVariables(environmentVariables) //
113 				.setAttributes(attributes);
114 
115 		for (final Step step : loadSteps(yaml.get("steps"))) {
116 
117 			scenario.addToSteps(step);
118 		}
119 
120 		return scenario;
121 	}
122 
123 	private interface MutableGuixerScenario extends GuixerScenario {
124 
125 		MutableGuixerScenario setName(
126 			String name
127 		);
128 
129 		MutableGuixerScenario addToSteps(
130 			Step step
131 		);
132 
133 		MutableGuixerScenario setEnvironmentVariables(
134 			Map<String, String> environmentVariables
135 		);
136 
137 		MutableGuixerScenario setAttributes(
138 			Map<String, String> attributes
139 		);
140 	}
141 
142 	private interface MutableGroupStep extends GroupStep {
143 
144 		MutableGroupStep setName(
145 			String name
146 		);
147 
148 		MutableGroupStep addToSteps(
149 			Step step
150 		);
151 	}
152 
153 	private interface MutableStatusStep extends StatusStep {
154 
155 		MutableStatusStep setLabel(
156 			String label
157 		);
158 	}
159 
160 	private interface MutableSetLaneStep extends SetLaneStep {
161 
162 		MutableSetLaneStep setLaneId(
163 			String laneId
164 		);
165 	}
166 
167 	private interface MutableAtomicStep extends AtomicStep {
168 
169 		MutableAtomicStep setIntent(
170 			String intent
171 		);
172 
173 		MutableAtomicStep setGetCommand(
174 			@Nullable GetCommand get
175 		);
176 
177 		MutableAtomicStep setClearCommand(
178 			@Nullable ClearCommand clear
179 		);
180 
181 		MutableAtomicStep setSendKeysCommand(
182 			@Nullable SendKeysCommand sendKeys
183 		);
184 
185 		MutableAtomicStep setClickCommand(
186 			@Nullable ClickCommand click
187 		);
188 
189 		MutableAtomicStep setCallCommand(
190 			@Nullable CallCommand call
191 		);
192 
193 		MutableAtomicStep setExecuteScriptCommand(
194 			@Nullable ExecuteScriptCommand executeScript
195 		);
196 
197 		MutableAtomicStep setSwitchToFrameCommand(
198 			@Nullable SwitchToFrameCommand switchToFrame
199 		);
200 
201 		MutableAtomicStep setSleepCommand(
202 			@Nullable SleepCommand sleep
203 		);
204 
205 		MutableAtomicStep setWaitForCommand(
206 			@Nullable WaitForCommand waitFor
207 		);
208 
209 		MutableAtomicStep setWaitForNotCommand(
210 			@Nullable WaitForNotCommand waitForNot
211 		);
212 
213 		MutableAtomicStep setFailureCommand(
214 			@Nullable FailureCommand failure
215 		);
216 
217 		MutableAtomicStep setSuccessCommand(
218 			@Nullable SuccessCommand success
219 		);
220 
221 		MutableAtomicStep setMessageCommand(
222 			@Nullable MessageCommand message
223 		);
224 
225 		MutableAtomicStep setAssertPresentCommand(
226 			@Nullable AssertPresentCommand assertPresent
227 		);
228 
229 		MutableAtomicStep setAssertAbsentCommand(
230 			@Nullable AssertAbsentCommand assertAbsent
231 		);
232 
233 		MutableAtomicStep setAssertTrueCommand(
234 			@Nullable AssertTrueCommand assertTrue
235 		);
236 
237 		MutableAtomicStep setAssertFalseCommand(
238 			@Nullable AssertFalseCommand assertFalse
239 		);
240 
241 		MutableAtomicStep setSetVariableCommand(
242 			@Nullable SetVariableCommand setVariable
243 		);
244 
245 		MutableAtomicStep setSetMaskedVariableCommand(
246 			@Nullable SetMaskedVariableCommand setMaskedVariable
247 		);
248 
249 		MutableAtomicStep setTagCommand(
250 			@Nullable TagCommand tag
251 		);
252 
253 		MutableAtomicStep setAttributeCommand(
254 			@Nullable AttributeCommand attribute
255 		);
256 
257 		MutableAtomicStep setExtCommand(
258 			@Nullable ExtCommand ext
259 		);
260 
261 		MutableAtomicStep setTakeScreenshot(
262 			boolean takeScreenshot
263 		);
264 
265 		MutableAtomicStep addToSeq(
266 			Command command
267 		);
268 	}
269 
270 	private static ImmutableList<Step> loadSteps(
271 		final MyYaml yaml
272 	) throws GuixerSyntaxException {
273 
274 		checkNotNull(yaml, "yaml");
275 
276 		if (!yaml.isArray()) {
277 
278 			throw new IllegalArgumentException("\"steps\" should be an array in YAML, but was: " + yaml);
279 		}
280 
281 		final List<Step> steps = newArrayList();
282 
283 		for (final MyYaml item : yaml.items()) {
284 
285 			final Step step = loadStep(item);
286 
287 			steps.add(step);
288 		}
289 
290 		return ImmutableList.copyOf(steps);
291 	}
292 
293 	@Nullable
294 	private static String getExtOrNull(
295 		final MyYaml yaml
296 	) {
297 
298 		for (final String key : yaml.keysAsStrings()) {
299 
300 			if (key.startsWith("ext[") && key.endsWith("]")) {
301 
302 				return yaml.get(key).asString();
303 			}
304 		}
305 
306 		return null;
307 	}
308 
309 	@Nullable
310 	private static String getExtDirectiveOrNull(
311 		final MyYaml yaml
312 	) {
313 
314 		for (final String key : yaml.keysAsStrings()) {
315 
316 			if (key.startsWith("ext[") && key.endsWith("]")) {
317 
318 				return key;
319 			}
320 		}
321 
322 		return null;
323 	}
324 
325 	private static Step loadStep(
326 		final MyYaml yaml
327 	) throws GuixerSyntaxException {
328 
329 		if (!yaml.isMap()) {
330 
331 			throw new IllegalStateException("step should be a map in YAML, but was: " + yaml);
332 		}
333 
334 		@Nullable
335 		final String intent = getStringOrNull(yaml, "intent");
336 
337 		@Nullable
338 		final String setLane = getStringOrNull(yaml, "setLane");
339 
340 		@Nullable
341 		final String get = getStringOrNull(yaml, "get");
342 
343 		@Nullable
344 		final String clear = getStringOrNull(yaml, "clear");
345 
346 		@Nullable
347 		final String sendKeys = getStringOrNull(yaml, "sendKeys");
348 
349 		@Nullable
350 		final String click = getStringOrNull(yaml, "click");
351 
352 		@Nullable
353 		final String call = getStringOrNull(yaml, "call");
354 
355 		@Nullable
356 		final String executeScript = getStringOrNull(yaml, "executeScript");
357 
358 		@Nullable
359 		final String switchToFrame = getStringOrNull(yaml, "switchToFrame");
360 
361 		@Nullable
362 		final String sleep = getStringOrNull(yaml, "sleep");
363 
364 		@Nullable
365 		final String waitFor = getStringOrNull(yaml, "waitFor");
366 
367 		@Nullable
368 		final String waitForNot = getStringOrNull(yaml, "waitForNot");
369 
370 		@Nullable
371 		final String failure = getStringOrNull(yaml, "failure");
372 
373 		@Nullable
374 		final String success = getStringOrNull(yaml, "success");
375 
376 		@Nullable
377 		final String message = getStringOrNull(yaml, "message");
378 
379 		@Nullable
380 		final String assertPresent = getStringOrNull(yaml, "assertPresent");
381 
382 		@Nullable
383 		final String assertAbsent = getStringOrNull(yaml, "assertAbsent");
384 
385 		@Nullable
386 		final String setVariable = getStringOrNull(yaml, "setVariable");
387 
388 		@Nullable
389 		final String setMaskedVariable = getStringOrNull(yaml, "setMaskedVariable");
390 
391 		@Nullable
392 		final String tag = getStringOrNull(yaml, "tag", //
393 				"tag[STEP]", "tag[RUN]", "tag[UPLOAD]");
394 
395 		@Nullable
396 		final String attribute = getStringOrNull(yaml, "attribute", //
397 				"attribute[STEP]", "attribute[RUN]", "attribute[UPLOAD]");
398 
399 		@Nullable
400 		final String ext = getExtOrNull(yaml);
401 
402 		@Nullable
403 		final Boolean takeScreenshot = getBooleanOrNull(yaml, "takeScreenshot");
404 
405 		@Nullable
406 		final String status = getStringOrNull(yaml, "status");
407 
408 		if (status != null) {
409 
410 			return instantiate(MutableStatusStep.class) //
411 					.setLabel(status);
412 
413 		} else if (setLane != null) {
414 
415 			return instantiate(MutableSetLaneStep.class) //
416 					.setLaneId(setLane);
417 
418 		} else if (intent == null //
419 				&& get == null //
420 				&& clear == null //
421 				&& sendKeys == null //
422 				&& click == null //
423 				&& call == null //
424 				&& executeScript == null //
425 				&& switchToFrame == null //
426 				&& sleep == null //
427 				&& waitFor == null //
428 				&& waitForNot == null //
429 				&& success == null //
430 				&& failure == null //
431 				&& message == null //
432 				&& assertPresent == null //
433 				&& assertAbsent == null //
434 				&& setVariable == null //
435 				&& setMaskedVariable == null //
436 				&& tag == null //
437 				&& attribute == null //
438 				&& ext == null //
439 				&& takeScreenshot == null) {
440 
441 			final String groupName = yaml.uniqueKey();
442 
443 			if (isBlank(groupName)) {
444 
445 				throw new GuixerSyntaxException("groupName should not be empty: " + groupName, //
446 						GROUP_NAME_SHOULD_NOT_BE_EMPTY);
447 			}
448 
449 			if (!Character.isUpperCase(groupName.charAt(0))) {
450 
451 				throw new GuixerSyntaxException(
452 						"groupName should start by an uppercase character, but was: " + groupName, //
453 						GROUP_NAME_SHOULD_START_BY_AN_UPPERCASE_CHARACTER);
454 			}
455 
456 			final MutableGroupStep group = instantiate(MutableGroupStep.class) //
457 					.setName(groupName);
458 
459 			for (final Step step : loadSteps(yaml.get(groupName))) {
460 
461 				group.addToSteps(step);
462 			}
463 
464 			return group;
465 		}
466 
467 		final MutableAtomicStep step = instantiate(MutableAtomicStep.class) //
468 				.setIntent(intent) //
469 				.setGetCommand(loadGetCommand(get)) //
470 				.setClearCommand(loadClearCommand(clear)) //
471 				.setSendKeysCommand(loadSendKeysCommand(sendKeys)) //
472 				.setClickCommand(loadClickCommand(click)) //
473 				.setCallCommand(loadCallCommand(call)) //
474 				.setExecuteScriptCommand(loadExecuteScriptCommand(executeScript)) //
475 				.setSwitchToFrameCommand(loadSwitchToFrameCommand(switchToFrame)) //
476 				.setSleepCommand(loadSleepCommand(sleep)) //
477 				.setWaitForCommand(loadWaitForCommand(waitFor)) //
478 				.setWaitForNotCommand(loadWaitForNotCommand(waitForNot)) //
479 				.setFailureCommand(loadFailureCommand(failure)) //
480 				.setSuccessCommand(loadSuccessCommand(success)) //
481 				.setMessageCommand(loadMessageCommand(message)) //
482 				.setAssertPresentCommand(loadAssertPresentCommand(assertPresent)) //
483 				.setAssertAbsentCommand(loadAssertAbsentCommand(assertAbsent)) //
484 				.setSetVariableCommand(loadSetVariableCommand(setVariable)) //
485 				.setSetMaskedVariableCommand(loadSetMaskedVariableCommand(setMaskedVariable)) //
486 				.setTagCommand(loadTagCommand(getTagDirectiveOrNull(yaml), tag)) //
487 				.setAttributeCommand(loadAttributeCommand(getAttributeDirectiveOrNull(yaml), attribute)) //
488 				.setExtCommand(loadExtCommand(getExtDirectiveOrNull(yaml), ext)) //
489 				.setTakeScreenshot(takeScreenshot == null ? false : takeScreenshot);
490 
491 		if (yaml.has("seq")) {
492 
493 			if (get != null) {
494 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"get\" should not be mixed.", //
495 						SEQ_AND_GET_SHOULD_NOT_BE_MIXED);
496 			} else if (clear != null) {
497 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"clear\" should not be mixed.", //
498 						SEQ_AND_CLEAR_SHOULD_NOT_BE_MIXED);
499 			} else if (sendKeys != null) {
500 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"sendKeys\" should not be mixed.", //
501 						SEQ_AND_SEND_KEYS_SHOULD_NOT_BE_MIXED);
502 			} else if (click != null) {
503 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"click\" should not be mixed.", //
504 						SEQ_AND_CLICK_SHOULD_NOT_BE_MIXED);
505 			} else if (call != null) {
506 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"call\" should not be mixed.", //
507 						SEQ_AND_CALL_SHOULD_NOT_BE_MIXED);
508 			} else if (switchToFrame != null) {
509 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"switchToFrame\" should not be mixed.", //
510 						SEQ_AND_SWITCH_TO_FRAME_SHOULD_NOT_BE_MIXED);
511 			} else if (sleep != null) {
512 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"sleep\" should not be mixed.", //
513 						SEQ_AND_SLEEP_SHOULD_NOT_BE_MIXED);
514 			} else if (waitFor != null) {
515 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"waitFor\" should not be mixed.", //
516 						SEQ_AND_WAIT_FOR_SHOULD_NOT_BE_MIXED);
517 			} else if (waitForNot != null) {
518 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"waitForNot\" should not be mixed.", //
519 						SEQ_AND_WAIT_FOR_NOT_SHOULD_NOT_BE_MIXED);
520 			} else if (assertPresent != null) {
521 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"assertPresent\" should not be mixed.", //
522 						SEQ_AND_ASSERT_PRESENT_SHOULD_NOT_BE_MIXED);
523 			} else if (assertAbsent != null) {
524 				throw new GuixerSyntaxException("Mixed declaration: \"seq\" and \"assertAbsent\" should not be mixed.", //
525 						SEQ_AND_ASSERT_ABSENT_SHOULD_NOT_BE_MIXED);
526 			}
527 
528 			for (final MyYaml commandYaml : yaml.get("seq").items()) {
529 
530 				final String commandLabel = commandYaml.uniqueKey();
531 
532 				final MyYaml commandValueYaml = commandYaml.get(commandLabel);
533 
534 				final String commandValue;
535 
536 				if (commandValueYaml.isArray()) {
537 
538 					final List<String> itemsAsStrings = newArrayList();
539 
540 					commandValueYaml.items().forEach(item ->
541 
542 					itemsAsStrings.add(item.asString()));
543 
544 					commandValue = join(itemsAsStrings, " ");
545 
546 				} else {
547 
548 					commandValue = commandValueYaml.asString();
549 				}
550 
551 				final Command command;
552 
553 				if ("get".equals(commandLabel)) {
554 					command = loadGetCommand(commandValue);
555 				} else if ("clear".equals(commandLabel)) {
556 					command = loadClearCommand(commandValue);
557 				} else if ("sendKeys".equals(commandLabel)) {
558 					command = loadSendKeysCommand(commandValue);
559 				} else if ("click".equals(commandLabel)) {
560 					command = loadClickCommand(commandValue);
561 				} else if ("call".equals(commandLabel)) {
562 					command = loadCallCommand(commandValue);
563 				} else if ("executeScript".equals(commandLabel)) {
564 					command = loadExecuteScriptCommand(commandValue);
565 				} else if ("switchToFrame".equals(commandLabel)) {
566 					command = loadSwitchToFrameCommand(commandValue);
567 				} else if ("sleep".equals(commandLabel)) {
568 					command = loadSleepCommand(commandValue);
569 				} else if ("waitFor".equals(commandLabel)) {
570 					command = loadWaitForCommand(commandValue);
571 				} else if ("waitForNot".equals(commandLabel)) {
572 					command = loadWaitForNotCommand(commandValue);
573 				} else if ("failure".equals(commandLabel)) {
574 					command = loadFailureCommand(commandValue);
575 				} else if ("success".equals(commandLabel)) {
576 					command = loadSuccessCommand(commandValue);
577 				} else if ("message".equals(commandLabel)) {
578 					command = loadMessageCommand(commandValue);
579 				} else if ("assertPresent".equals(commandLabel)) {
580 					command = loadAssertPresentCommand(commandValue);
581 				} else if ("assertAbsent".equals(commandLabel)) {
582 					command = loadAssertAbsentCommand(commandValue);
583 				} else if ("assertTrue".equals(commandLabel)) {
584 					command = loadAssertTrueCommand(commandValue);
585 				} else if ("assertFalse".equals(commandLabel)) {
586 					command = loadAssertFalseCommand(commandValue);
587 				} else if ("setVariable".equals(commandLabel)) {
588 					command = loadSetVariableCommand(commandValue);
589 				} else if ("setMaskedVariable".equals(commandLabel)) {
590 					command = loadSetMaskedVariableCommand(commandValue);
591 				} else if ("tag".equals(commandLabel) //
592 						|| "tag[STEP]".equals(commandLabel) //
593 						|| "tag[RUN]".equals(commandLabel) //
594 						|| "tag[UPLOAD]".equals(commandLabel)) {
595 					command = loadTagCommand(commandLabel, commandValue);
596 				} else if ("attribute".equals(commandLabel) //
597 						|| "attribute[STEP]".equals(commandLabel) //
598 						|| "attribute[RUN]".equals(commandLabel) //
599 						|| "attribute[UPLOAD]".equals(commandLabel)) {
600 					command = loadAttributeCommand(commandLabel, commandValue);
601 				} else if (commandLabel.startsWith("ext[")) {
602 					command = loadExtCommand(commandLabel, commandValue);
603 				} else {
604 					throw new NotImplementedException("commandLabel: " + commandLabel);
605 				}
606 
607 				step.addToSeq(command);
608 			}
609 		}
610 
611 		return step;
612 	}
613 
614 	@Nullable
615 	private static String getTagDirectiveOrNull(
616 		final MyYaml yaml
617 	) {
618 
619 		for (final String directive : new String[] { //
620 				"tag", //
621 				"tag[STEP]", //
622 				"tag[RUN]", //
623 				"tag[UPLOAD]" }) {
624 
625 			if (yaml.has(directive)) {
626 
627 				return directive;
628 			}
629 		}
630 
631 		return null;
632 	}
633 
634 	@Nullable
635 	private static String getAttributeDirectiveOrNull(
636 		final MyYaml yaml
637 	) {
638 
639 		for (final String directive : new String[] { //
640 				"attribute", //
641 				"attribute[STEP]", //
642 				"attribute[RUN]", //
643 				"attribute[UPLOAD]" }) {
644 
645 			if (yaml.has(directive)) {
646 
647 				return directive;
648 			}
649 		}
650 
651 		return null;
652 	}
653 
654 	@Nullable
655 	private static String getStringOrNull(
656 		final MyYaml yaml,
657 		final String name,
658 		final String... alternateNames
659 	) {
660 
661 		if (yaml.has(name)) {
662 
663 			return yaml.get(name).asString();
664 		}
665 
666 		for (final String alternateName : alternateNames) {
667 
668 			if (yaml.has(alternateName)) {
669 
670 				return yaml.get(alternateName).asString();
671 			}
672 		}
673 
674 		return null;
675 	}
676 
677 	@Nullable
678 	private static Boolean getBooleanOrNull(
679 		final MyYaml yaml,
680 		final String name
681 	) {
682 
683 		return yaml.has(name) //
684 				? yaml.get(name).asBoolean() //
685 				: null;
686 	}
687 
688 	@Nullable
689 	private static GetCommand loadGetCommand(
690 		@Nullable final String url
691 	) {
692 
693 		if (url == null) {
694 
695 			return null;
696 
697 		} else {
698 
699 			return new GetCommand(url);
700 		}
701 	}
702 
703 	@Nullable
704 	private static ClickCommand loadClickCommand(
705 		@Nullable final String locator
706 	) {
707 
708 		if (locator == null) {
709 
710 			return null;
711 
712 		} else {
713 
714 			return new ClickCommand(locator);
715 		}
716 	}
717 
718 	@Nullable
719 	private static SleepCommand loadSleepCommand(
720 		@Nullable final String secondsAsString
721 	) {
722 
723 		if (secondsAsString == null) {
724 
725 			return null;
726 
727 		} else {
728 
729 			final int seconds;
730 
731 			try {
732 
733 				seconds = Integer.parseInt(secondsAsString);
734 
735 			} catch (final NumberFormatException e) {
736 
737 				throw new RuntimeException("Cannot parse seconds: " + secondsAsString, e);
738 			}
739 
740 			return new SleepCommand(seconds);
741 		}
742 	}
743 
744 	@Nullable
745 	private static ClearCommand loadClearCommand(
746 		@Nullable final String locator
747 	) {
748 
749 		if (locator == null) {
750 
751 			return null;
752 
753 		} else {
754 
755 			return new ClearCommand(locator);
756 		}
757 	}
758 
759 	@Nullable
760 	private static CallCommand loadCallCommand(
761 		@Nullable final String call
762 	) {
763 
764 		if (call == null) {
765 
766 			return null;
767 
768 		} else {
769 
770 			return new CallCommand(call);
771 		}
772 	}
773 
774 	@Nullable
775 	private static ExecuteScriptCommand loadExecuteScriptCommand(
776 		@Nullable final String script
777 	) {
778 
779 		if (script == null) {
780 
781 			return null;
782 
783 		} else {
784 
785 			return new ExecuteScriptCommand(script);
786 		}
787 	}
788 
789 	@Nullable
790 	private static SwitchToFrameCommand loadSwitchToFrameCommand(
791 		@Nullable final String frameName
792 	) {
793 
794 		if (frameName == null) {
795 
796 			return null;
797 
798 		} else {
799 
800 			return new SwitchToFrameCommand(frameName);
801 		}
802 	}
803 
804 	@Nullable
805 	private static WaitForCommand loadWaitForCommand(
806 		@Nullable final String locator
807 	) {
808 
809 		if (locator == null) {
810 
811 			return null;
812 
813 		} else {
814 
815 			return new WaitForCommand(locator);
816 		}
817 	}
818 
819 	@Nullable
820 	private static WaitForNotCommand loadWaitForNotCommand(
821 		@Nullable final String locator
822 	) {
823 
824 		if (locator == null) {
825 
826 			return null;
827 
828 		} else {
829 
830 			return new WaitForNotCommand(locator);
831 		}
832 	}
833 
834 	@Nullable
835 	private static FailureCommand loadFailureCommand(
836 		@Nullable final String message
837 	) {
838 
839 		if (message == null) {
840 
841 			return null;
842 
843 		} else {
844 
845 			return new FailureCommand(message);
846 		}
847 	}
848 
849 	@Nullable
850 	private static SuccessCommand loadSuccessCommand(
851 		@Nullable final String message
852 	) {
853 
854 		if (message == null) {
855 
856 			return null;
857 
858 		} else {
859 
860 			return new SuccessCommand(message);
861 		}
862 	}
863 
864 	@Nullable
865 	private static MessageCommand loadMessageCommand(
866 		@Nullable final String text
867 	) {
868 
869 		if (text == null) {
870 
871 			return null;
872 
873 		} else {
874 
875 			return new MessageCommand(text);
876 		}
877 	}
878 
879 	@Nullable
880 	private static AssertPresentCommand loadAssertPresentCommand(
881 		@Nullable final String locator
882 	) {
883 
884 		if (locator == null) {
885 
886 			return null;
887 
888 		} else {
889 
890 			return new AssertPresentCommand(locator);
891 		}
892 	}
893 
894 	@Nullable
895 	private static AssertAbsentCommand loadAssertAbsentCommand(
896 		@Nullable final String locator
897 	) {
898 
899 		if (locator == null) {
900 
901 			return null;
902 
903 		} else {
904 
905 			return new AssertAbsentCommand(locator);
906 		}
907 	}
908 
909 	@Nullable
910 	private static AssertTrueCommand loadAssertTrueCommand(
911 		@Nullable final String expression
912 	) {
913 
914 		if (expression == null) {
915 
916 			return null;
917 
918 		} else {
919 
920 			return new AssertTrueCommand(expression);
921 		}
922 	}
923 
924 	@Nullable
925 	private static AssertFalseCommand loadAssertFalseCommand(
926 		@Nullable final String expression
927 	) {
928 
929 		if (expression == null) {
930 
931 			return null;
932 
933 		} else {
934 
935 			return new AssertFalseCommand(expression);
936 		}
937 	}
938 
939 	@Nullable
940 	private static SendKeysCommand loadSendKeysCommand(
941 		@Nullable final String s
942 	) {
943 
944 		if (s == null) {
945 
946 			return null;
947 
948 		} else {
949 
950 			final String locator = extractLocator(s);
951 
952 			final String value = extractValue(s, locator);
953 
954 			return new SendKeysCommand(locator, removeQuotes(value));
955 		}
956 	}
957 
958 	@Nullable
959 	private static SetMaskedVariableCommand loadSetMaskedVariableCommand(
960 		@Nullable final String s
961 	) {
962 
963 		if (s == null) {
964 
965 			return null;
966 		}
967 
968 		final String name = substringBefore(s, " ");
969 
970 		final String value = substringAfter(s, " ").trim();
971 
972 		return new SetMaskedVariableCommand(name, removeQuotes(value));
973 	}
974 
975 	@Nullable
976 	private static TagCommand loadTagCommand(
977 		@Nullable final String directive,
978 		@Nullable final String s
979 	) {
980 
981 		if (s == null) {
982 
983 			return null;
984 		}
985 
986 		final AttributeScope scope = extractAttributeScope(directive);
987 
988 		final String name = s;
989 
990 		return new TagCommand(scope, removeQuotes(name));
991 	}
992 
993 	@Nullable
994 	private static AttributeCommand loadAttributeCommand(
995 		@Nullable final String directive,
996 		@Nullable final String s
997 	) {
998 
999 		if (s == null) {
1000 
1001 			return null;
1002 		}
1003 
1004 		final AttributeScope scope = extractAttributeScope(directive);
1005 
1006 		final String name = s.startsWith("\"") //
1007 				? substringBetween(s, "\"") //
1008 				: substringBefore(s, " ");
1009 
1010 		final String value = substringAfter(substringAfter(s, name), " ").trim();
1011 
1012 		return new AttributeCommand(scope, name, removeQuotes(value));
1013 	}
1014 
1015 	private static AttributeScope extractAttributeScope(
1016 		final String s
1017 	) {
1018 
1019 		return s.contains("[") //
1020 				? AttributeScope.valueOf(substringBetween(s, "[", "]")) //
1021 				: AttributeScope.STEP;
1022 	}
1023 
1024 	@Nullable
1025 	private static ExtCommand loadExtCommand(
1026 		@Nullable final String directive,
1027 		@Nullable final String s
1028 	) {
1029 
1030 		if (s == null) {
1031 
1032 			return null;
1033 		}
1034 
1035 		final String namespace = substringBetween(directive, "[", "]");
1036 
1037 		return new ExtCommand(namespace, removeQuotes(s));
1038 	}
1039 
1040 	@Nullable
1041 	private static SetVariableCommand loadSetVariableCommand(
1042 		@Nullable final String s
1043 	) {
1044 
1045 		if (s == null) {
1046 
1047 			return null;
1048 		}
1049 
1050 		final String name = substringBefore(s, " ");
1051 
1052 		final String value = substringAfter(s, " ").trim();
1053 
1054 		return new SetVariableCommand(name, removeQuotes(value));
1055 	}
1056 
1057 	private static String removeQuotes(
1058 		final String s
1059 	) {
1060 
1061 		if (s.startsWith("'")) {
1062 
1063 			return s.substring(1, s.length() - 1);
1064 
1065 		} else if (s.startsWith("\"")) {
1066 
1067 			return s.substring(1, s.length() - 1);
1068 
1069 		} else {
1070 
1071 			return s;
1072 		}
1073 	}
1074 
1075 	private static String extractLocator(
1076 		final String s
1077 	) {
1078 
1079 		final StringBuilder sb = new StringBuilder();
1080 
1081 		for (final char c : s.toCharArray()) {
1082 
1083 			if (c == ' ') {
1084 
1085 				break;
1086 			}
1087 
1088 			sb.append(c);
1089 
1090 			if (c == '[') {
1091 
1092 				sb.append(parseToEndingSquareBracket(s.substring(sb.length())));
1093 
1094 				break;
1095 			}
1096 		}
1097 
1098 		return removeQuotes(sb.toString());
1099 	}
1100 
1101 	private static String extractValue(
1102 		final String s,
1103 		final String locator
1104 	) {
1105 
1106 		return s.substring( //
1107 
1108 				s.startsWith("'") ? locator.length() + 2 : locator.length()
1109 
1110 		).trim();
1111 	}
1112 
1113 	private static String parseToEndingSquareBracket(
1114 		final String s
1115 	) {
1116 
1117 		final StringBuilder sb = new StringBuilder();
1118 
1119 		for (final char c : s.toCharArray()) {
1120 
1121 			sb.append(c);
1122 
1123 			if (c == ']') {
1124 
1125 				return sb.toString();
1126 			}
1127 		}
1128 
1129 		throw new IllegalStateException("Cannot parse end of locator: " + s);
1130 	}
1131 }