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.command.AbstractTerminalCommand;
11  import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
12  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
13  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
14  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
15  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
16  import gov.nist.secauto.metaschema.core.model.IModule;
17  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
18  import gov.nist.secauto.metaschema.core.util.AutoCloser;
19  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
20  import gov.nist.secauto.metaschema.databind.IBindingContext;
21  import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
22  import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat;
23  import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
24  
25  import org.apache.commons.cli.CommandLine;
26  import org.apache.commons.cli.Option;
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  
30  import java.io.IOException;
31  import java.io.OutputStream;
32  import java.io.OutputStreamWriter;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Path;
35  import java.util.Collection;
36  import java.util.List;
37  
38  import edu.umd.cs.findbugs.annotations.NonNull;
39  import edu.umd.cs.findbugs.annotations.Nullable;
40  
41  /**
42   * This command implementation supports generation of schemas in a variety of
43   * formats based on a provided Metaschema module.
44   */
45  class GenerateSchemaCommand
46      extends AbstractTerminalCommand {
47    private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class);
48  
49    @NonNull
50    private static final String COMMAND = "generate-schema";
51    @NonNull
52    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
53        ExtraArgument.newInstance("metaschema-module-file-or-URL", true),
54        ExtraArgument.newInstance("destination-schema-file", false)));
55  
56    private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull(
57        Option.builder()
58            .longOpt("inline-types")
59            .desc("definitions declared inline will be generated as inline types")
60            .get());
61  
62    @Override
63    public String getName() {
64      return COMMAND;
65    }
66  
67    @Override
68    public String getDescription() {
69      return "Generate a schema for the specified Module module";
70    }
71  
72    @SuppressWarnings("null")
73    @Override
74    public Collection<? extends Option> gatherOptions() {
75      return List.of(
76          MetaschemaCommands.OVERWRITE_OPTION,
77          MetaschemaCommands.AS_SCHEMA_FORMAT_OPTION,
78          INLINE_TYPES_OPTION);
79    }
80  
81    @Override
82    public List<ExtraArgument> getExtraArguments() {
83      return EXTRA_ARGUMENTS;
84    }
85  
86    @Override
87    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
88      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
89    }
90  
91    /**
92     * Execute the schema generation operation.
93     *
94     * @param callingContext
95     *          the context information for the execution
96     * @param cmdLine
97     *          the parsed command line details
98     * @throws CommandExecutionException
99     *           if an error occurred while determining the source format
100    */
101   protected void executeCommand(
102       @NonNull CallingContext callingContext,
103       @NonNull CommandLine cmdLine) throws CommandExecutionException {
104     List<String> extraArgs = cmdLine.getArgList();
105 
106     Path destination = extraArgs.size() > 1
107         ? MetaschemaCommands.handleDestination(
108             ObjectUtils.requireNonNull(extraArgs.get(1)),
109             cmdLine)
110         : null;
111 
112     SchemaFormat asFormat = MetaschemaCommands.getSchemaFormat(cmdLine, MetaschemaCommands.AS_SCHEMA_FORMAT_OPTION);
113 
114     IMutableConfiguration<SchemaGenerationFeature<?>> configuration = createConfiguration(cmdLine, asFormat);
115     generateSchema(extraArgs, destination, asFormat, configuration);
116   }
117 
118   @NonNull
119   private static IMutableConfiguration<SchemaGenerationFeature<?>> createConfiguration(
120       @NonNull CommandLine cmdLine,
121       @NonNull SchemaFormat asFormat) {
122     IMutableConfiguration<SchemaGenerationFeature<?>> configuration = new DefaultConfiguration<>();
123     if (cmdLine.hasOption(INLINE_TYPES_OPTION)) {
124       configuration.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
125       if (SchemaFormat.JSON.equals(asFormat)) {
126         configuration.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
127       } else {
128         configuration.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS);
129       }
130     }
131     return configuration;
132   }
133 
134   private static void generateSchema(
135       @NonNull List<String> extraArgs,
136       @Nullable Path destination,
137       @NonNull SchemaFormat asFormat,
138       @NonNull IMutableConfiguration<SchemaGenerationFeature<?>> configuration) throws CommandExecutionException {
139     IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();
140     IModule module = MetaschemaCommands.loadModule(
141         ObjectUtils.requireNonNull(extraArgs.get(0)),
142         ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()),
143         bindingContext);
144 
145     try {
146       bindingContext.registerModule(module);
147       if (LOGGER.isInfoEnabled()) {
148         LOGGER.info("Generating {} schema for '{}'.", asFormat.name(), extraArgs.get(0));
149       }
150       if (destination == null) {
151         @SuppressWarnings({ "resource", "PMD.CloseResource" }) // not owned
152         OutputStream os = ObjectUtils.notNull(System.out);
153 
154         try (OutputStream out = AutoCloser.preventClose(os)) {
155           try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
156             ISchemaGenerator.generateSchema(module, writer, asFormat, configuration);
157           }
158         }
159       } else {
160         ISchemaGenerator.generateSchema(module, destination, asFormat, configuration);
161       }
162     } catch (IOException | MetaschemaException ex) {
163       throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
164     }
165     if (destination != null && LOGGER.isInfoEnabled()) {
166       LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination);
167     }
168   }
169 }