1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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") // intended
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( // NOPMD - intentional
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    * Called to execute the schema generation.
137    *
138    * @param callingContext
139    *          the context information for the execution
140    * @param cmdLine
141    *          the parsed command line details
142    * @return the execution result
143    */
144   @SuppressWarnings({
145       "PMD.OnlyOneReturn", // readability
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( // NOPMD readability
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( // NOPMD readability
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); // NOPMD readability
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" }) // not owned
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); // NOPMD readability
223     }
224     if (destination != null && LOGGER.isInfoEnabled()) {
225       LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination);
226     }
227     return ExitCode.OK.exit();
228   }
229 }