1
2
3
4
5
6 package gov.nist.secauto.metaschema.cli.commands;
7
8 import gov.nist.secauto.metaschema.cli.commands.metapath.MetapathCommand;
9 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
11 import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
12 import gov.nist.secauto.metaschema.cli.processor.command.ICommand;
13 import gov.nist.secauto.metaschema.core.metapath.MetapathException;
14 import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
15 import gov.nist.secauto.metaschema.core.model.IModule;
16 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
17 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
18 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
19 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
20 import gov.nist.secauto.metaschema.core.util.DeleteOnShutdown;
21 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
22 import gov.nist.secauto.metaschema.core.util.UriUtils;
23 import gov.nist.secauto.metaschema.databind.IBindingContext;
24 import gov.nist.secauto.metaschema.databind.io.Format;
25 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
26 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
27 import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat;
28
29 import org.apache.commons.cli.CommandLine;
30 import org.apache.commons.cli.Option;
31
32 import java.io.FileNotFoundException;
33 import java.io.IOException;
34 import java.net.URI;
35 import java.net.URISyntaxException;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.util.Arrays;
40 import java.util.LinkedHashSet;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Set;
44
45 import edu.umd.cs.findbugs.annotations.NonNull;
46
47
48
49
50
51
52
53
54
55
56 @SuppressWarnings("PMD.GodClass")
57 public final class MetaschemaCommands {
58
59
60
61
62 @NonNull
63 public static final List<ICommand> COMMANDS = ObjectUtils.notNull(List.of(
64 new ValidateModuleCommand(),
65 new GenerateSchemaCommand(),
66 new GenerateDiagramCommand(),
67 new ValidateContentUsingModuleCommand(),
68 new ConvertContentUsingModuleCommand(),
69 new MetapathCommand()));
70
71
72
73
74
75
76 @NonNull
77 public static final Option METASCHEMA_REQUIRED_OPTION = ObjectUtils.notNull(
78 Option.builder("m")
79 .hasArg()
80 .argName("FILE_OR_URL")
81 .required()
82 .desc("metaschema resource")
83 .numberOfArgs(1)
84 .build());
85
86
87
88
89
90 @NonNull
91 public static final Option METASCHEMA_OPTIONAL_OPTION = ObjectUtils.notNull(
92 Option.builder("m")
93 .hasArg()
94 .argName("FILE_OR_URL")
95 .desc("metaschema resource")
96 .numberOfArgs(1)
97 .build());
98
99
100
101
102 @NonNull
103 public static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
104 Option.builder()
105 .longOpt("overwrite")
106 .desc("overwrite the destination if it exists")
107 .build());
108
109
110
111
112
113
114 @NonNull
115 public static final Option TO_OPTION = ObjectUtils.notNull(
116 Option.builder()
117 .longOpt("to")
118 .required()
119 .hasArg().argName("FORMAT")
120 .desc("convert to format: " + Arrays.stream(Format.values())
121 .map(Enum::name)
122 .collect(CustomCollectors.joiningWithOxfordComma("or")))
123 .numberOfArgs(1)
124 .build());
125
126
127
128
129
130
131 @NonNull
132 public static final Option AS_FORMAT_OPTION = ObjectUtils.notNull(
133 Option.builder()
134 .longOpt("as")
135 .hasArg()
136 .argName("FORMAT")
137 .desc("source format: " + Arrays.stream(Format.values())
138 .map(Enum::name)
139 .collect(CustomCollectors.joiningWithOxfordComma("or")))
140 .numberOfArgs(1)
141 .build());
142
143
144
145
146
147
148 @NonNull
149 public static final Option AS_SCHEMA_FORMAT_OPTION = ObjectUtils.notNull(
150 Option.builder()
151 .longOpt("as")
152 .required()
153 .hasArg()
154 .argName("FORMAT")
155 .desc("schema format: " + Arrays.stream(SchemaFormat.values())
156 .map(Enum::name)
157 .collect(CustomCollectors.joiningWithOxfordComma("or")))
158 .numberOfArgs(1)
159 .build());
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175 @NonNull
176 public static URI handleSource(
177 @NonNull String pathOrUri,
178 @NonNull URI currentWorkingDirectory) throws CommandExecutionException {
179 try {
180 return getResourceUri(pathOrUri, currentWorkingDirectory);
181 } catch (URISyntaxException ex) {
182 throw new CommandExecutionException(
183 ExitCode.INVALID_ARGUMENTS,
184 String.format(
185 "Cannot load source '%s' as it is not a valid file or URI.",
186 pathOrUri),
187 ex);
188 }
189 }
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208 public static Path handleDestination(
209 @NonNull String path,
210 @NonNull CommandLine commandLine) throws CommandExecutionException {
211 Path retval = Paths.get(path).toAbsolutePath();
212
213 if (Files.exists(retval)) {
214 if (!commandLine.hasOption(OVERWRITE_OPTION)) {
215 throw new CommandExecutionException(
216 ExitCode.INVALID_ARGUMENTS,
217 String.format("The provided destination '%s' already exists and the '%s' option was not provided.",
218 retval,
219 OptionUtils.toArgument(OVERWRITE_OPTION)));
220 }
221 if (!Files.isWritable(retval)) {
222 throw new CommandExecutionException(
223 ExitCode.IO_ERROR,
224 String.format(
225 "The provided destination '%s' is not writable.", retval));
226 }
227 } else {
228 Path parent = retval.getParent();
229 if (parent != null) {
230 try {
231 Files.createDirectories(parent);
232 } catch (IOException ex) {
233 throw new CommandExecutionException(
234 ExitCode.INVALID_TARGET,
235 ex);
236 }
237 }
238 }
239 return retval;
240 }
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 @SuppressWarnings("PMD.PreserveStackTrace")
256 @NonNull
257 public static Format getFormat(
258 @NonNull CommandLine commandLine,
259 @NonNull Option option) throws CommandExecutionException {
260
261 String toFormatText = commandLine.getOptionValue(option);
262 if (toFormatText == null) {
263 throw new CommandExecutionException(
264 ExitCode.INVALID_ARGUMENTS,
265 String.format("The '%s' argument was not provided.",
266 option.hasLongOpt()
267 ? "--" + option.getLongOpt()
268 : "-" + option.getOpt()));
269 }
270 try {
271 return Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
272 } catch (IllegalArgumentException ex) {
273 throw new CommandExecutionException(
274 ExitCode.INVALID_ARGUMENTS,
275 String.format("Invalid '%s' argument. The format must be one of: %s.",
276 option.hasLongOpt()
277 ? "--" + option.getLongOpt()
278 : "-" + option.getOpt(),
279 Arrays.stream(Format.values())
280 .map(Enum::name)
281 .collect(CustomCollectors.joiningWithOxfordComma("or"))));
282 }
283 }
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298 @SuppressWarnings("PMD.PreserveStackTrace")
299 @NonNull
300 public static SchemaFormat getSchemaFormat(
301 @NonNull CommandLine commandLine,
302 @NonNull Option option) throws CommandExecutionException {
303
304 String toFormatText = commandLine.getOptionValue(option);
305 if (toFormatText == null) {
306 throw new CommandExecutionException(
307 ExitCode.INVALID_ARGUMENTS,
308 String.format("Option '%s' not provided.",
309 option.hasLongOpt()
310 ? "--" + option.getLongOpt()
311 : "-" + option.getOpt()));
312 }
313 try {
314 return SchemaFormat.valueOf(toFormatText.toUpperCase(Locale.ROOT));
315 } catch (IllegalArgumentException ex) {
316 throw new CommandExecutionException(
317 ExitCode.INVALID_ARGUMENTS,
318 String.format("Invalid '%s' argument. The schema format must be one of: %s.",
319 option.hasLongOpt()
320 ? "--" + option.getLongOpt()
321 : "-" + option.getOpt(),
322 Arrays.stream(SchemaFormat.values())
323 .map(Enum::name)
324 .collect(CustomCollectors.joiningWithOxfordComma("or"))),
325 ex);
326 }
327 }
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351 @SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.OnlyOneReturn" })
352 @NonNull
353 public static Format determineSourceFormat(
354 @NonNull CommandLine commandLine,
355 @NonNull Option option,
356 @NonNull IBoundLoader loader,
357 @NonNull URI resource) throws CommandExecutionException {
358 if (commandLine.hasOption(option)) {
359
360 return getFormat(commandLine, option);
361 }
362
363
364 try {
365 return loader.detectFormat(resource);
366 } catch (FileNotFoundException ex) {
367
368 throw new CommandExecutionException(
369 ExitCode.IO_ERROR,
370 String.format("The provided source '%s' does not exist.", resource),
371 ex);
372 } catch (IOException ex) {
373 throw new CommandExecutionException(
374 ExitCode.IO_ERROR,
375 String.format("Unable to determine source format. Use '%s' to specify the format. %s",
376 option.hasLongOpt()
377 ? "--" + option.getLongOpt()
378 : "-" + option.getOpt(),
379 ex.getLocalizedMessage()),
380 ex);
381 }
382 }
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402 @NonNull
403 public static IModule loadModule(
404 @NonNull CommandLine commandLine,
405 @NonNull Option option,
406 @NonNull URI currentWorkingDirectory,
407 @NonNull IBindingContext bindingContext) throws CommandExecutionException {
408 String moduleName = commandLine.getOptionValue(option);
409 if (moduleName == null) {
410 throw new CommandExecutionException(
411 ExitCode.INVALID_ARGUMENTS,
412 String.format("Unable to determine the module to load. Use '%s' to specify the module.",
413 option.hasLongOpt()
414 ? "--" + option.getLongOpt()
415 : "-" + option.getOpt()));
416 }
417
418 URI moduleUri;
419 try {
420 moduleUri = UriUtils.toUri(moduleName, currentWorkingDirectory);
421 } catch (URISyntaxException ex) {
422 throw new CommandExecutionException(
423 ExitCode.INVALID_ARGUMENTS,
424 String.format("Cannot load module as '%s' is not a valid file or URL. %s",
425 ex.getInput(),
426 ex.getLocalizedMessage()),
427 ex);
428 }
429 return loadModule(moduleUri, bindingContext);
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450 @NonNull
451 public static IModule loadModule(
452 @NonNull String moduleResource,
453 @NonNull URI currentWorkingDirectory,
454 @NonNull IBindingContext bindingContext) throws CommandExecutionException {
455 try {
456 URI moduleUri = getResourceUri(
457 moduleResource,
458 currentWorkingDirectory);
459 return loadModule(moduleUri, bindingContext);
460 } catch (URISyntaxException ex) {
461 throw new CommandExecutionException(
462 ExitCode.INVALID_ARGUMENTS,
463 String.format("Cannot load module as '%s' is not a valid file or URL. %s",
464 ex.getInput(),
465 ex.getLocalizedMessage()),
466 ex);
467 }
468 }
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483 @NonNull
484 public static IModule loadModule(
485 @NonNull URI moduleResource,
486 @NonNull IBindingContext bindingContext) throws CommandExecutionException {
487
488 try {
489 IBindingModuleLoader loader = bindingContext.newModuleLoader();
490 loader.allowEntityResolution();
491 return loader.load(moduleResource);
492 } catch (IOException | MetaschemaException ex) {
493 throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
494 }
495 }
496
497
498
499
500
501
502
503
504
505
506
507
508 @NonNull
509 public static URI getResourceUri(
510 @NonNull String location,
511 @NonNull URI currentWorkingDirectory) throws URISyntaxException {
512 return UriUtils.toUri(location, currentWorkingDirectory);
513 }
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531 @NonNull
532 public static Set<IConstraintSet> loadConstraintSets(
533 @NonNull CommandLine commandLine,
534 @NonNull Option option,
535 @NonNull URI currentWorkingDirectory) throws CommandExecutionException {
536 Set<IConstraintSet> constraintSets;
537 if (commandLine.hasOption(option)) {
538 IConstraintLoader constraintLoader = IBindingContext.getConstraintLoader();
539 constraintSets = new LinkedHashSet<>();
540 String[] args = commandLine.getOptionValues(option);
541 for (String arg : args) {
542 assert arg != null;
543 try {
544 URI constraintUri = ObjectUtils.requireNonNull(UriUtils.toUri(arg, currentWorkingDirectory));
545 constraintSets.addAll(constraintLoader.load(constraintUri));
546 } catch (URISyntaxException | IOException | MetaschemaException | MetapathException ex) {
547 throw new CommandExecutionException(
548 ExitCode.IO_ERROR,
549 String.format("Unable to process constraint set '%s'. %s",
550 arg,
551 ex.getLocalizedMessage()),
552 ex);
553 }
554 }
555 } else {
556 constraintSets = CollectionUtil.emptySet();
557 }
558 return constraintSets;
559 }
560
561
562
563
564
565
566
567
568
569 @NonNull
570 public static Path newTempDir() throws IOException {
571 Path retval = Files.createTempDirectory("metaschema-cli-");
572 DeleteOnShutdown.register(retval);
573 return ObjectUtils.notNull(retval);
574 }
575
576
577
578
579
580
581
582
583
584
585 @NonNull
586 public static IBindingContext newBindingContextWithDynamicCompilation() throws CommandExecutionException {
587 return newBindingContextWithDynamicCompilation(CollectionUtil.emptySet());
588 }
589
590
591
592
593
594
595
596
597
598
599
600
601
602 @NonNull
603 public static IBindingContext newBindingContextWithDynamicCompilation(@NonNull Set<IConstraintSet> constraintSets)
604 throws CommandExecutionException {
605 try {
606 Path tempDir = newTempDir();
607 return IBindingContext.builder()
608 .compilePath(tempDir)
609 .constraintSet(constraintSets)
610 .build();
611 } catch (IOException ex) {
612 throw new CommandExecutionException(ExitCode.RUNTIME_ERROR,
613 String.format("Unable to initialize the binding context. %s", ex.getLocalizedMessage()),
614 ex);
615 }
616 }
617
618 private MetaschemaCommands() {
619
620 }
621 }