1
2
3
4
5
6 package gov.nist.secauto.metaschema.cli.commands;
7
8 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10 import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
11 import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
12 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
13 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
14 import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
15 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
16 import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
17 import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
18 import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
19 import gov.nist.secauto.metaschema.core.model.IModule;
20 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
21 import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
22 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
23 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
24 import gov.nist.secauto.metaschema.core.util.UriUtils;
25 import gov.nist.secauto.metaschema.databind.io.Format;
26 import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
27 import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat;
28 import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
29
30 import org.apache.commons.cli.CommandLine;
31 import org.apache.commons.cli.Option;
32 import org.apache.logging.log4j.LogManager;
33 import org.apache.logging.log4j.Logger;
34
35 import java.io.IOException;
36 import java.io.OutputStream;
37 import java.net.URI;
38 import java.net.URISyntaxException;
39 import java.nio.file.Files;
40 import java.nio.file.Path;
41 import java.nio.file.Paths;
42 import java.util.Arrays;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Locale;
46
47 import edu.umd.cs.findbugs.annotations.NonNull;
48
49 public class GenerateSchemaCommand
50 extends AbstractTerminalCommand {
51 private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class);
52
53 @NonNull
54 private static final String COMMAND = "generate-schema";
55 @NonNull
56 private static final List<ExtraArgument> EXTRA_ARGUMENTS;
57
58 @NonNull
59 private static final Option AS_OPTION = ObjectUtils.notNull(
60 Option.builder()
61 .longOpt("as")
62 .required()
63 .hasArg()
64 .argName("FORMAT")
65 .desc("source format: xml, json, or yaml")
66 .build());
67 @NonNull
68 private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull(
69 Option.builder()
70 .longOpt("inline-types")
71 .desc("definitions declared inline will be generated as inline types")
72 .build());
73
74 static {
75 EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
76 new DefaultExtraArgument("metaschema-module-file-or-URL", true),
77 new DefaultExtraArgument("destination-schema-file", false)));
78 }
79
80 @Override
81 public String getName() {
82 return COMMAND;
83 }
84
85 @Override
86 public String getDescription() {
87 return "Generate a schema for the specified Module module";
88 }
89
90 @SuppressWarnings("null")
91 @Override
92 public Collection<? extends Option> gatherOptions() {
93 return List.of(
94 MetaschemaCommands.OVERWRITE_OPTION,
95 AS_OPTION,
96 INLINE_TYPES_OPTION);
97 }
98
99 @Override
100 public List<ExtraArgument> getExtraArguments() {
101 return EXTRA_ARGUMENTS;
102 }
103
104 @SuppressWarnings("PMD.PreserveStackTrace")
105 @Override
106 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
107 try {
108 String asFormatText = cmdLine.getOptionValue(AS_OPTION);
109 if (asFormatText != null) {
110 SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT));
111 }
112 } catch (IllegalArgumentException ex) {
113 InvalidArgumentException newEx = new InvalidArgumentException(
114 String.format("Invalid '%s' argument. The format must be one of: %s.",
115 OptionUtils.toArgument(AS_OPTION),
116 Arrays.asList(Format.values()).stream()
117 .map(format -> format.name())
118 .collect(CustomCollectors.joiningWithOxfordComma("and"))));
119 newEx.setOption(AS_OPTION);
120 newEx.addSuppressed(ex);
121 throw newEx;
122 }
123
124 List<String> extraArgs = cmdLine.getArgList();
125 if (extraArgs.isEmpty() || extraArgs.size() > 2) {
126 throw new InvalidArgumentException("Illegal number of arguments.");
127 }
128 }
129
130 @Override
131 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
132 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
133 }
134
135
136
137
138
139
140
141
142
143
144 @SuppressWarnings({
145 "PMD.OnlyOneReturn",
146 "unused"
147 })
148 protected ExitStatus executeCommand(
149 @NonNull CallingContext callingContext,
150 @NonNull CommandLine cmdLine) {
151 List<String> extraArgs = cmdLine.getArgList();
152
153 Path destination = null;
154 if (extraArgs.size() > 1) {
155 destination = Paths.get(extraArgs.get(1)).toAbsolutePath();
156 }
157
158 if (destination != null) {
159 if (Files.exists(destination)) {
160 if (!cmdLine.hasOption(MetaschemaCommands.OVERWRITE_OPTION)) {
161 return ExitCode.INVALID_ARGUMENTS.exitMessage(
162 String.format("The provided destination '%s' already exists and the '%s' option was not provided.",
163 destination,
164 OptionUtils.toArgument(MetaschemaCommands.OVERWRITE_OPTION)));
165 }
166 if (!Files.isWritable(destination)) {
167 return ExitCode.IO_ERROR.exitMessage(
168 "The provided destination '" + destination + "' is not writable.");
169 }
170 } else {
171 Path parent = destination.getParent();
172 if (parent != null) {
173 try {
174 Files.createDirectories(parent);
175 } catch (IOException ex) {
176 return ExitCode.INVALID_TARGET.exit().withThrowable(ex);
177 }
178 }
179 }
180 }
181
182 String asFormatText = cmdLine.getOptionValue(AS_OPTION);
183 SchemaFormat asFormat = SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT));
184
185 IMutableConfiguration<SchemaGenerationFeature<?>> configuration = new DefaultConfiguration<>();
186 if (cmdLine.hasOption(INLINE_TYPES_OPTION)) {
187 configuration.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
188 if (SchemaFormat.JSON.equals(asFormat)) {
189 configuration.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
190 } else {
191 configuration.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
192 }
193 }
194
195 String inputName = ObjectUtils.notNull(extraArgs.get(0));
196 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri());
197
198 URI input;
199 try {
200 input = UriUtils.toUri(inputName, cwd);
201 } catch (URISyntaxException ex) {
202 return ExitCode.IO_ERROR.exitMessage(
203 String.format("Unable to load '%s' as it is not a valid file or URI.", inputName)).withThrowable(ex);
204 }
205 assert input != null;
206 try {
207 ModuleLoader loader = new ModuleLoader();
208 loader.allowEntityResolution();
209 IModule module = loader.load(input);
210
211 if (LOGGER.isInfoEnabled()) {
212 LOGGER.info("Generating {} schema for '{}'.", asFormat.name(), input);
213 }
214 if (destination == null) {
215 @SuppressWarnings({ "resource", "PMD.CloseResource" })
216 OutputStream os = ObjectUtils.notNull(System.out);
217 ISchemaGenerator.generateSchema(module, os, asFormat, configuration);
218 } else {
219 ISchemaGenerator.generateSchema(module, destination, asFormat, configuration);
220 }
221 } catch (IOException | MetaschemaException ex) {
222 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
223 }
224 if (destination != null && LOGGER.isInfoEnabled()) {
225 LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination);
226 }
227 return ExitCode.OK.exit();
228 }
229 }