View Javadoc
1   package io.guixer.lang;
2   
3   import static com.google.common.base.Preconditions.checkNotNull;
4   import static com.google.common.collect.Sets.newHashSet;
5   import static io.guixer.lang.GuixerSyntaxErrorType.DUPLICATE_COMMANDS_IN_ATOMIC_STEP;
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.GROUP_STEPS_SHOULD_BE_A_LIST;
9   import static io.guixer.lang.GuixerSyntaxErrorType.ILLEGAL_COMMAND_NAME;
10  import static io.guixer.lang.GuixerSyntaxErrorType.INTENT_SHOULD_BE_AT_STEP_TOP_LEVEL;
11  import static io.guixer.lang.GuixerSyntaxErrorType.SCENARIO_NAME_SHOULD_BE_PRESENT;
12  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_COMMAND_SHOULD_NOT_BE_MIXED;
13  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_SHOULD_BE_A_LIST;
14  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_STEP_SHOULD_BE_A_MAP;
15  import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_STEP_SHOULD_BE_A_SINGLETON_MAP;
16  import static io.guixer.lang.GuixerSyntaxErrorType.STEPS_SHOULD_BE_A_LIST;
17  import static io.guixer.lang.GuixerSyntaxErrorType.STEPS_SHOULD_BE_PRESENT;
18  import static io.guixer.lang.GuixerSyntaxErrorType.STEP_SHOULD_BE_A_MAP;
19  import static io.guixer.lang.GuixerSyntaxErrorType.TAKE_SCREENSHOT_SHOULD_BE_AT_STEP_TOP_LEVEL;
20  import static java.nio.charset.StandardCharsets.UTF_8;
21  import static org.apache.commons.lang3.StringUtils.isBlank;
22  import static org.apache.commons.lang3.StringUtils.substringBefore;
23  import static org.apache.commons.lang3.StringUtils.substringBetween;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.util.Optional;
28  import java.util.Set;
29  
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.lang3.ArrayUtils;
32  
33  import com.avcompris.util.YamlUtils;
34  import com.avcompris.util.Yamled;
35  
36  public class GuixerSyntaxChecker {
37  
38  	public void check(
39  		final File yamlFile
40  	) throws IOException, GuixerSyntaxException {
41  
42  		checkNotNull(yamlFile, "yamlFile");
43  
44  		final Set<String> commandNamesInAtomicStep = newHashSet();
45  
46  		for (final String line : FileUtils.readLines(yamlFile, UTF_8)) {
47  
48  			final String trimmed = line.trim();
49  
50  			if (trimmed.startsWith("#") || trimmed.isEmpty()) {
51  				continue;
52  			}
53  
54  			if (trimmed.startsWith("-")) {
55  				commandNamesInAtomicStep.clear();
56  			}
57  
58  			final String commandName;
59  
60  			if (!line.contains(":")) {
61  				continue;
62  			}
63  
64  			if (trimmed.startsWith("-")) {
65  
66  				commandName = substringBetween(line, "-", ":").trim();
67  
68  			} else {
69  
70  				commandName = substringBefore(line, ":").trim();
71  			}
72  
73  			if (commandNamesInAtomicStep.contains(commandName)) {
74  
75  				throw new GuixerSyntaxException("Duplicate command in AtomicStep: " + commandName, //
76  						DUPLICATE_COMMANDS_IN_ATOMIC_STEP);
77  			}
78  
79  			commandNamesInAtomicStep.add(commandName);
80  		}
81  
82  		final Yamled yaml = YamlUtils.loadYAML(yamlFile);
83  
84  		if (!yaml.has("name")) {
85  			throw new GuixerSyntaxException("The \"name:\" attribute should be present in the YAML scenario file", //
86  					SCENARIO_NAME_SHOULD_BE_PRESENT);
87  		}
88  
89  		if (!yaml.has("steps")) {
90  			throw new GuixerSyntaxException("The \"steps:\" field should be present in the YAML scenario file", //
91  					STEPS_SHOULD_BE_PRESENT);
92  		}
93  
94  		final Yamled steps = yaml.get("steps");
95  
96  		if (!steps.isList()) {
97  			throw new GuixerSyntaxException("\"steps:\" should be a list in the YAML scenario file, but was: " + steps, //
98  					STEPS_SHOULD_BE_A_LIST);
99  		}
100 
101 		for (final Yamled step : steps.items()) {
102 
103 			loadStep(step);
104 		}
105 	}
106 
107 	private static void loadStep(
108 		final Yamled step
109 	) throws GuixerSyntaxException {
110 
111 		if (!step.isMap()) {
112 			throw new GuixerSyntaxException("Step should be a map in the YAML scenario file, but was: " + step, //
113 					STEP_SHOULD_BE_A_MAP);
114 		}
115 
116 		final Set<String> keys = newHashSet();
117 
118 		for (final Yamled item : step.items(false)) {
119 
120 			final String key = item.label();
121 
122 			keys.add(key);
123 		}
124 
125 		final Optional<String> unknownCommandName = keys.stream().filter(key
126 
127 		-> !(ArrayUtils.contains(LEGIT_COMMAND_NAMES, key) //
128 				|| isExtCommand(key))).findAny();
129 
130 		if (unknownCommandName.isPresent()) {
131 
132 			final String commandName = unknownCommandName.get();
133 
134 			if (keys.size() != 1) {
135 				throw new GuixerSyntaxException("Illegal command name with map.size() > 2: " + commandName, //
136 						ILLEGAL_COMMAND_NAME);
137 			}
138 
139 			final String groupName = commandName;
140 
141 			if (isBlank(groupName)) {
142 				throw new GuixerSyntaxException("groupName should not be empty: " + groupName, //
143 						GROUP_NAME_SHOULD_NOT_BE_EMPTY);
144 			}
145 
146 			if (!Character.isUpperCase(groupName.charAt(0))) {
147 				throw new GuixerSyntaxException(
148 						"groupName should start by an uppercase character, but was: " + groupName, //
149 						GROUP_NAME_SHOULD_START_BY_AN_UPPERCASE_CHARACTER);
150 			}
151 
152 			final Yamled groupSteps = step.get(commandName);
153 
154 			if (!groupSteps.isList()) {
155 				throw new GuixerSyntaxException(
156 						"Group steps should be a list in the YAML scenario file, but was: " + groupSteps, //
157 						GROUP_STEPS_SHOULD_BE_A_LIST);
158 			}
159 
160 			for (final Yamled groupStep : groupSteps.items()) {
161 
162 				loadStep(groupStep);
163 			}
164 
165 		} else {
166 
167 			if (keys.contains("seq")) {
168 
169 				final Optional<String> forbiddenCommandName = keys.stream().filter(key
170 
171 				-> !ArrayUtils.contains(ALLOWED_COMMAND_NAMES_WITH_SEQ, key)).findAny();
172 
173 				if (forbiddenCommandName.isPresent()) {
174 
175 					final String commandName = forbiddenCommandName.get();
176 
177 					throw new GuixerSyntaxException("Command name should be inside \"seq:\": " + commandName, //
178 							SEQ_AND_COMMAND_SHOULD_NOT_BE_MIXED);
179 				}
180 
181 				final Yamled seq = step.get("seq");
182 
183 				if (!seq.isList()) {
184 					throw new GuixerSyntaxException(
185 							"\"seq:\" should be a list in the YAML scenario file, but was: " + seq, //
186 							SEQ_SHOULD_BE_A_LIST);
187 				}
188 
189 				for (final Yamled item : seq.items()) {
190 
191 					if (!item.isMap()) {
192 						throw new GuixerSyntaxException(
193 								"Step in \"seq:\" should be a map in the YAML scenario file, but was: " + item, //
194 								SEQ_STEP_SHOULD_BE_A_MAP);
195 					}
196 
197 					final Yamled[] itemSteps = item.items(false);
198 
199 					if (itemSteps.length != 1) {
200 						throw new GuixerSyntaxException(
201 								"Step in \"seq:\" should be a a singleton map in the YAML scenario file, but was: "
202 										+ item, //
203 								SEQ_STEP_SHOULD_BE_A_SINGLETON_MAP);
204 					}
205 
206 					final String stepCommandName = itemSteps[0].label();
207 
208 					if (!ArrayUtils.contains(LEGIT_COMMAND_NAMES, stepCommandName) //
209 							&& !isExtCommand(stepCommandName) //
210 					) {
211 						throw new GuixerSyntaxException("Illegal command in \"seq:\": " + stepCommandName, //
212 								ILLEGAL_COMMAND_NAME);
213 					} else if ("intent".equals(stepCommandName)) {
214 						throw new GuixerSyntaxException(
215 								"\"intent:\" should be at the same level as \"seq:\": " + stepCommandName, //
216 								INTENT_SHOULD_BE_AT_STEP_TOP_LEVEL);
217 					} else if ("takeScreenshot".equals(stepCommandName)) {
218 						throw new GuixerSyntaxException(
219 								"\"takeScreenshot:\" should be at the same level as \"seq:\": " + stepCommandName, //
220 								TAKE_SCREENSHOT_SHOULD_BE_AT_STEP_TOP_LEVEL);
221 					} else if (ArrayUtils.contains(ALLOWED_COMMAND_NAMES_WITH_SEQ, stepCommandName)) {
222 						throw new GuixerSyntaxException("Illegal command in \"seq:\": " + stepCommandName, //
223 								ILLEGAL_COMMAND_NAME);
224 					}
225 				}
226 			}
227 		}
228 	}
229 
230 	private static String[] LEGIT_COMMAND_NAMES = new String[] { //
231 			"assertAbsent", //
232 			"assertFalse", //
233 			"assertPresent", //
234 			"assertTrue", //
235 			"attribute", //
236 			"attribute[RUN]", //
237 			"attribute[STEP]", //
238 			"attribute[UPLOAD]", //
239 			"call", //
240 			"clear", //
241 			"click", //
242 			"executeScript", //
243 			"ext[*]", //
244 			"failure", //
245 			"get", //
246 			"intent", //
247 			"message", //
248 			"sendKeys", //
249 			"seq", //
250 			"setLane", //
251 			"setMaskedVariable", //
252 			"setVariable", //
253 			"sleep", //
254 			"status", //
255 			"success", //
256 			"switchToFrame", //
257 			"tag", //
258 			"tag[RUN]", //
259 			"tag[STEP]", //
260 			"tag[UPLOAD]", //
261 			"takeScreenshot", //
262 			"waitFor", //
263 			"waitForNot", //
264 	};
265 
266 	private static boolean isExtCommand(
267 		final String command
268 	) {
269 
270 		return command.startsWith("ext[") && command.endsWith("]");
271 	}
272 
273 	private static String[] ALLOWED_COMMAND_NAMES_WITH_SEQ = new String[] { //
274 			"intent", //
275 			"seq", //
276 			"takeScreenshot", //
277 	};
278 }