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