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