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