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