blob: 1595f9644fafd5677caacb62dd053d424009bb33 [file] [log] [blame]
Thomas Vachuskaf9c84362015-04-15 11:20:45 -07001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Thomas Vachuskaf9c84362015-04-15 11:20:45 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onlab.stc;
17
18import com.google.common.collect.ImmutableList;
19import com.google.common.collect.ImmutableSet;
20import com.google.common.collect.Lists;
21import com.google.common.collect.Maps;
22import com.google.common.collect.Sets;
23import org.apache.commons.configuration.HierarchicalConfiguration;
Thomas Vachuska1c152872015-08-26 18:41:10 -070024import org.onlab.graph.DepthFirstSearch;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070025
26import java.io.File;
27import java.io.FileInputStream;
28import java.io.IOException;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32
Thomas Vachuska1c152872015-08-26 18:41:10 -070033import static com.google.common.base.Preconditions.*;
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -070034import static com.google.common.base.Strings.isNullOrEmpty;
Thomas Vachuska664f29e2015-12-08 12:58:16 -080035import static java.lang.Integer.parseInt;
Thomas Vachuska1c152872015-08-26 18:41:10 -070036import static org.onlab.graph.DepthFirstSearch.EdgeType.BACK_EDGE;
37import static org.onlab.graph.GraphPathSearch.ALL_PATHS;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070038import static org.onlab.stc.Scenario.loadScenario;
39
40/**
41 * Entity responsible for loading a scenario and producing a redy-to-execute
42 * process flow graph.
43 */
44public class Compiler {
45
Thomas Vachuska229dced2015-10-14 14:21:13 -070046 private static final String DEFAULT_LOG_DIR = "${WORKSPACE}/tmp/stc/";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070047
48 private static final String IMPORT = "import";
49 private static final String GROUP = "group";
50 private static final String STEP = "step";
51 private static final String PARALLEL = "parallel";
Thomas Vachuska4be30542015-10-21 18:15:52 -070052 private static final String SEQUENTIAL = "sequential";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070053 private static final String DEPENDENCY = "dependency";
54
55 private static final String LOG_DIR = "[@logDir]";
56 private static final String NAME = "[@name]";
57 private static final String COMMAND = "[@exec]";
Thomas Vachuska4bfccd542015-05-30 00:35:25 -070058 private static final String ENV = "[@env]";
59 private static final String CWD = "[@cwd]";
Thomas Vachuska664f29e2015-12-08 12:58:16 -080060 private static final String DELAY = "[@delay]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070061 private static final String REQUIRES = "[@requires]";
62 private static final String IF = "[@if]";
63 private static final String UNLESS = "[@unless]";
64 private static final String VAR = "[@var]";
Thomas Vachuska4be30542015-10-21 18:15:52 -070065 private static final String STARTS = "[@starts]";
66 private static final String ENDS = "[@ends]";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070067 private static final String FILE = "[@file]";
68 private static final String NAMESPACE = "[@namespace]";
69
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -070070 static final String PROP_START = "${";
71 static final String PROP_END = "}";
Thomas Vachuska4be30542015-10-21 18:15:52 -070072
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070073 private static final String HASH = "#";
Thomas Vachuska4be30542015-10-21 18:15:52 -070074 private static final String HASH_PREV = "#-1";
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070075
76 private final Scenario scenario;
77
78 private final Map<String, Step> steps = Maps.newHashMap();
79 private final Map<String, Step> inactiveSteps = Maps.newHashMap();
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -070080 private final Map<String, String> requirements = Maps.newHashMap();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070081 private final Set<Dependency> dependencies = Sets.newHashSet();
Thomas Vachuska4be30542015-10-21 18:15:52 -070082 private final List<Integer> clonables = Lists.newArrayList();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070083
84 private ProcessFlow processFlow;
85 private File logDir;
86
Thomas Vachuska18571b02015-05-31 22:30:06 -070087 private String previous = null;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -070088 private String pfx = "";
89 private boolean debugOn = System.getenv("debug") != null;
90
91 /**
92 * Creates a new compiler for the specified scenario.
93 *
94 * @param scenario scenario to be compiled
95 */
96 public Compiler(Scenario scenario) {
97 this.scenario = scenario;
98 }
99
100 /**
101 * Returns the scenario being compiled.
102 *
103 * @return test scenario
104 */
105 public Scenario scenario() {
106 return scenario;
107 }
108
109 /**
110 * Compiles the specified scenario to produce a final process flow graph.
111 */
112 public void compile() {
113 compile(scenario.definition(), null, null);
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700114 compileRequirements();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700115
116 // Produce the process flow
117 processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
118 ImmutableSet.copyOf(dependencies));
119
Thomas Vachuska1c152872015-08-26 18:41:10 -0700120 scanForCycles();
121
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700122 // Extract the log directory if there was one specified
Thomas Vachuskaa48e3d12015-06-02 09:43:29 -0700123 String defaultPath = DEFAULT_LOG_DIR + scenario.name();
124 String path = scenario.definition().getString(LOG_DIR, defaultPath);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700125 logDir = new File(expand(path));
126 }
127
128 /**
129 * Returns the step with the specified name.
130 *
131 * @param name step or group name
132 * @return test step or group
133 */
134 public Step getStep(String name) {
135 return steps.get(name);
136 }
137
138 /**
139 * Returns the process flow generated from this scenario definition.
140 *
141 * @return process flow as a graph
142 */
143 public ProcessFlow processFlow() {
144 return processFlow;
145 }
146
147 /**
148 * Returns the log directory where scenario logs should be kept.
Thomas Vachuska18571b02015-05-31 22:30:06 -0700149 *
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700150 * @return scenario logs directory
151 */
152 public File logDir() {
153 return logDir;
154 }
155
156 /**
157 * Recursively elaborates this definition to produce a final process flow graph.
158 *
159 * @param cfg hierarchical definition
160 * @param namespace optional namespace
161 * @param parentGroup optional parent group
162 */
163 private void compile(HierarchicalConfiguration cfg,
164 String namespace, Group parentGroup) {
165 String opfx = pfx;
166 pfx = pfx + ">";
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700167 print("pfx=%s namespace=%s", pfx, namespace);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700168
169 // Scan all imports
170 cfg.configurationsAt(IMPORT)
171 .forEach(c -> processImport(c, namespace, parentGroup));
172
173 // Scan all steps
174 cfg.configurationsAt(STEP)
175 .forEach(c -> processStep(c, namespace, parentGroup));
176
177 // Scan all groups
178 cfg.configurationsAt(GROUP)
179 .forEach(c -> processGroup(c, namespace, parentGroup));
180
181 // Scan all parallel groups
182 cfg.configurationsAt(PARALLEL)
183 .forEach(c -> processParallelGroup(c, namespace, parentGroup));
184
Thomas Vachuska4be30542015-10-21 18:15:52 -0700185 // Scan all sequential groups
186 cfg.configurationsAt(SEQUENTIAL)
187 .forEach(c -> processSequentialGroup(c, namespace, parentGroup));
188
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700189 // Scan all dependencies
190 cfg.configurationsAt(DEPENDENCY)
191 .forEach(c -> processDependency(c, namespace));
192
193 pfx = opfx;
194 }
195
196 /**
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700197 * Compiles requirements for all steps and groups accrued during the
198 * overall compilation process.
199 */
200 private void compileRequirements() {
201 requirements.forEach((name, requires) ->
202 compileRequirements(getStep(name), requires));
203 }
204
205 private void compileRequirements(Step src, String requires) {
206 split(requires).forEach(n -> {
207 boolean isSoft = n.startsWith("~");
208 String name = n.replaceFirst("^~", "");
209 Step dst = getStep(name);
210 if (dst != null) {
211 dependencies.add(new Dependency(src, dst, isSoft));
212 }
213 });
214 }
215
216 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700217 * Processes an import directive.
218 *
219 * @param cfg hierarchical definition
220 * @param namespace optional namespace
221 * @param parentGroup optional parent group
222 */
223 private void processImport(HierarchicalConfiguration cfg,
224 String namespace, Group parentGroup) {
225 String file = checkNotNull(expand(cfg.getString(FILE)),
226 "Import directive must specify 'file'");
227 String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
228 print("import file=%s namespace=%s", file, newNamespace);
229 try {
230 Scenario importScenario = loadScenario(new FileInputStream(file));
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700231 compile(importScenario.definition(), newNamespace, parentGroup);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700232 } catch (IOException e) {
233 throw new IllegalArgumentException("Unable to import scenario", e);
234 }
235 }
236
237 /**
238 * Processes a step directive.
239 *
240 * @param cfg hierarchical definition
241 * @param namespace optional namespace
242 * @param parentGroup optional parent group
243 */
244 private void processStep(HierarchicalConfiguration cfg,
245 String namespace, Group parentGroup) {
246 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700247 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700248 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
249 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuska664f29e2015-12-08 12:58:16 -0800250 int delay = parseInt(expand(cfg.getString(DELAY, parentGroup != null ? "" + parentGroup.delay() : "0")));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700251
Thomas Vachuska664f29e2015-12-08 12:58:16 -0800252 print("step name=%s command=%s env=%s cwd=%s delay=%d", name, command, env, cwd, delay);
253 Step step = new Step(name, command, env, cwd, parentGroup, delay);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700254 registerStep(step, cfg, namespace, parentGroup);
255 }
256
257 /**
258 * Processes a group directive.
259 *
260 * @param cfg hierarchical definition
261 * @param namespace optional namespace
262 * @param parentGroup optional parent group
263 */
264 private void processGroup(HierarchicalConfiguration cfg,
265 String namespace, Group parentGroup) {
266 String name = expand(prefix(cfg.getString(NAME), namespace));
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700267 String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
Thomas Vachuska4bfccd542015-05-30 00:35:25 -0700268 String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
269 String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
Thomas Vachuska664f29e2015-12-08 12:58:16 -0800270 int delay = parseInt(expand(cfg.getString(DELAY, parentGroup != null ? "" + parentGroup.delay() : "0")));
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700271
Thomas Vachuska664f29e2015-12-08 12:58:16 -0800272 print("group name=%s command=%s env=%s cwd=%s delay=%d", name, command, env, cwd, delay);
273 Group group = new Group(name, command, env, cwd, parentGroup, delay);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700274 if (registerStep(group, cfg, namespace, parentGroup)) {
275 compile(cfg, namespace, group);
276 }
277 }
278
279 /**
280 * Registers the specified step or group.
281 *
282 * @param step step or group
283 * @param cfg hierarchical definition
284 * @param namespace optional namespace
285 * @param parentGroup optional parent group
286 * @return true of the step or group was registered as active
287 */
288 private boolean registerStep(Step step, HierarchicalConfiguration cfg,
289 String namespace, Group parentGroup) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700290 checkState(!steps.containsKey(step.name()), "Step %s already exists", step.name());
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700291 String ifClause = expand(cfg.getString(IF));
292 String unlessClause = expand(cfg.getString(UNLESS));
293
294 if ((ifClause != null && ifClause.length() == 0) ||
295 (unlessClause != null && unlessClause.length() > 0) ||
296 (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
297 inactiveSteps.put(step.name(), step);
298 return false;
299 }
300
301 if (parentGroup != null) {
302 parentGroup.addChild(step);
303 }
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700304
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700305 steps.put(step.name(), step);
306 processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
Thomas Vachuska18571b02015-05-31 22:30:06 -0700307 previous = step.name();
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700308 return true;
309 }
310
311 /**
312 * Processes a parallel clone group directive.
313 *
314 * @param cfg hierarchical definition
315 * @param namespace optional namespace
316 * @param parentGroup optional parent group
317 */
318 private void processParallelGroup(HierarchicalConfiguration cfg,
319 String namespace, Group parentGroup) {
320 String var = cfg.getString(VAR);
321 print("parallel var=%s", var);
322
323 int i = 1;
324 while (condition(var, i).length() > 0) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700325 clonables.add(0, i);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700326 compile(cfg, namespace, parentGroup);
Thomas Vachuska4be30542015-10-21 18:15:52 -0700327 clonables.remove(0);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700328 i++;
329 }
330 }
331
332 /**
Thomas Vachuska4be30542015-10-21 18:15:52 -0700333 * Processes a sequential clone group directive.
334 *
335 * @param cfg hierarchical definition
336 * @param namespace optional namespace
337 * @param parentGroup optional parent group
338 */
339 private void processSequentialGroup(HierarchicalConfiguration cfg,
340 String namespace, Group parentGroup) {
341 String var = cfg.getString(VAR);
342 String starts = cfg.getString(STARTS);
343 String ends = cfg.getString(ENDS);
344 print("sequential var=%s", var);
345
346 int i = 1;
347 while (condition(var, i).length() > 0) {
348 clonables.add(0, i);
349 compile(cfg, namespace, parentGroup);
350 if (i > 1) {
351 processSequentialRequirements(starts, ends, namespace);
352 }
353 clonables.remove(0);
354 i++;
355 }
356 }
357
358 /**
359 * Hooks starts of this sequence tier to the previous tier.
360 *
361 * @param starts comma-separated list of start steps
362 * @param ends comma-separated list of end steps
363 * @param namespace optional namespace
364 */
365 private void processSequentialRequirements(String starts, String ends,
366 String namespace) {
367 for (String s : split(starts)) {
368 String start = expand(prefix(s, namespace));
369 String reqs = requirements.get(s);
370 for (String n : split(ends)) {
371 boolean isSoft = n.startsWith("~");
372 String name = n.replaceFirst("^~", "");
373 name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
374 reqs = reqs == null ? name : (reqs + "," + name);
375 }
376 requirements.put(start, reqs);
377 }
378 }
379
380 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700381 * Returns the elaborated repetition construct conditional.
382 *
383 * @param var repetition var property
384 * @param i index to elaborate
385 * @return elaborated string
386 */
387 private String condition(String var, Integer i) {
388 return expand(var.replaceFirst("#", i.toString())).trim();
389 }
390
391 /**
392 * Processes a dependency directive.
393 *
394 * @param cfg hierarchical definition
395 * @param namespace optional namespace
396 */
397 private void processDependency(HierarchicalConfiguration cfg, String namespace) {
398 String name = expand(prefix(cfg.getString(NAME), namespace));
399 String requires = expand(cfg.getString(REQUIRES));
400
401 print("dependency name=%s requires=%s", name, requires);
402 Step step = getStep(name, namespace);
Thomas Vachuska177ece62015-06-26 00:18:21 -0700403 if (!inactiveSteps.containsValue(step)) {
404 processRequirements(step, requires, namespace);
405 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700406 }
407
408 /**
409 * Processes the specified requiremenst string and adds dependency for
410 * each requirement of the given step.
411 *
412 * @param src source step
413 * @param requires comma-separated list of required steps
414 * @param namespace optional namespace
415 */
416 private void processRequirements(Step src, String requires, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700417 String reqs = requirements.get(src.name());
418 for (String n : split(requires)) {
Thomas Vachuska18571b02015-05-31 22:30:06 -0700419 boolean isSoft = n.startsWith("~");
420 String name = n.replaceFirst("^~", "");
421 name = previous != null && name.equals("^") ? previous : name;
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700422 name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
423 reqs = reqs == null ? name : (reqs + "," + name);
424 }
425 requirements.put(src.name(), reqs);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700426 }
427
428 /**
429 * Retrieves the step or group with the specified name.
430 *
431 * @param name step or group name
432 * @param namespace optional namespace
433 * @return step or group; null if none found in active or inactive steps
434 */
435 private Step getStep(String name, String namespace) {
436 String dName = prefix(name, namespace);
437 Step step = steps.get(dName);
438 step = step != null ? step : inactiveSteps.get(dName);
439 checkArgument(step != null, "Unknown step %s", dName);
440 return step;
441 }
442
443 /**
444 * Prefixes the specified name with the given namespace.
445 *
446 * @param name name of a step or a group
447 * @param namespace optional namespace
448 * @return composite name
449 */
450 private String prefix(String name, String namespace) {
Thomas Vachuskaae0fbe82015-07-15 17:42:17 -0700451 return isNullOrEmpty(namespace) ? name : namespace + "." + name;
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700452 }
453
454 /**
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700455 * Expands any environment variables in the specified string. These are
456 * specified as ${property} tokens.
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700457 *
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700458 * @param string string to be processed
459 * @param keepTokens true if the original unresolved tokens should be kept
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700460 * @return original string with expanded substitutions
461 */
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700462 private String expand(String string, boolean... keepTokens) {
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700463 if (string == null) {
464 return null;
465 }
466
467 String pString = string;
468 StringBuilder sb = new StringBuilder();
469 int start, end, last = 0;
470 while ((start = pString.indexOf(PROP_START, last)) >= 0) {
471 end = pString.indexOf(PROP_END, start + PROP_START.length());
472 checkArgument(end > start, "Malformed property in %s", pString);
473 sb.append(pString.substring(last, start));
474 String prop = pString.substring(start + PROP_START.length(), end);
475 String value;
476 if (prop.equals(HASH)) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700477 value = Integer.toString(clonables.get(0));
478 } else if (prop.equals(HASH_PREV)) {
479 value = Integer.toString(clonables.get(0) - 1);
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700480 } else if (prop.endsWith(HASH)) {
Thomas Vachuska4be30542015-10-21 18:15:52 -0700481 pString = pString.replaceFirst("#}", clonables.get(0) + "}");
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700482 last = start;
483 continue;
484 } else {
485 // Try system property first, then fall back to env. variable.
486 value = System.getProperty(prop);
487 if (value == null) {
488 value = System.getenv(prop);
489 }
490 }
Thomas Vachuskab51b8bc2015-07-27 08:37:12 -0700491 if (value == null && keepTokens.length == 1 && keepTokens[0]) {
492 sb.append("${").append(prop).append("}");
493 } else {
494 sb.append(value != null ? value : "");
495 }
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700496 last = end + 1;
497 }
498 sb.append(pString.substring(last));
Thomas Vachuska0ec6ff42015-07-17 11:00:02 -0700499 return sb.toString().replace('\n', ' ').replace('\r', ' ');
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700500 }
501
502 /**
503 * Splits the comma-separated string into a list of strings.
504 *
505 * @param string string to split
506 * @return list of strings
507 */
508 private List<String> split(String string) {
509 ImmutableList.Builder<String> builder = ImmutableList.builder();
510 String[] fields = string != null ? string.split(",") : new String[0];
511 for (String field : fields) {
512 builder.add(field.trim());
513 }
514 return builder.build();
515 }
516
517 /**
Thomas Vachuska1c152872015-08-26 18:41:10 -0700518 * Scans the process flow graph for cyclic dependencies.
519 */
520 private void scanForCycles() {
521 DepthFirstSearch<Step, Dependency> dfs = new DepthFirstSearch<>();
522 // Use a brute-force method of searching paths from all vertices.
523 processFlow().getVertexes().forEach(s -> {
524 DepthFirstSearch<Step, Dependency>.SpanningTreeResult r =
525 dfs.search(processFlow, s, null, null, ALL_PATHS);
526 r.edges().forEach((e, et) -> checkArgument(et != BACK_EDGE,
527 "Process flow has a cycle involving dependency from %s to %s",
528 e.src().name, e.dst().name));
529 });
530 }
531
532
533 /**
Thomas Vachuskaf9c84362015-04-15 11:20:45 -0700534 * Prints formatted output.
535 *
536 * @param format printf format string
537 * @param args arguments to be printed
538 */
539 private void print(String format, Object... args) {
540 if (debugOn) {
541 System.err.println(pfx + String.format(format, args));
542 }
543 }
544
545}