View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
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") // intended
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( // NOPMD - intentional
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", // readability
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( // NOPMD readability
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( // NOPMD readability
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); // NOPMD readability
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" }) // not owned
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); // NOPMD readability
236     }
237     if (destination != null && LOGGER.isInfoEnabled()) {
238       LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination);
239     }
240     return ExitCode.OK.exit();
241   }
242 }