001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.cli.commands;
007
008import org.apache.commons.cli.CommandLine;
009import org.apache.commons.cli.Option;
010import org.apache.logging.log4j.LogManager;
011import org.apache.logging.log4j.Logger;
012
013import java.io.File;
014import java.io.IOException;
015import java.io.OutputStream;
016import java.io.OutputStreamWriter;
017import java.net.URI;
018import java.nio.charset.StandardCharsets;
019import java.nio.file.Path;
020import java.util.Collection;
021import java.util.List;
022
023import dev.metaschema.cli.processor.CallingContext;
024import dev.metaschema.cli.processor.ExitCode;
025import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
026import dev.metaschema.cli.processor.command.CommandExecutionException;
027import dev.metaschema.cli.processor.command.ExtraArgument;
028import dev.metaschema.cli.processor.command.ICommandExecutor;
029import dev.metaschema.core.configuration.DefaultConfiguration;
030import dev.metaschema.core.configuration.IMutableConfiguration;
031import dev.metaschema.core.model.IModule;
032import dev.metaschema.core.model.MetaschemaException;
033import dev.metaschema.core.util.AutoCloser;
034import dev.metaschema.core.util.ObjectUtils;
035import dev.metaschema.databind.IBindingContext;
036import dev.metaschema.schemagen.ISchemaGenerator;
037import dev.metaschema.schemagen.ISchemaGenerator.SchemaFormat;
038import dev.metaschema.schemagen.SchemaGenerationFeature;
039import edu.umd.cs.findbugs.annotations.NonNull;
040import edu.umd.cs.findbugs.annotations.Nullable;
041
042/**
043 * This command implementation supports generation of schemas in a variety of
044 * formats based on a provided Metaschema module.
045 */
046public class GenerateSchemaCommand
047    extends AbstractTerminalCommand {
048  private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class);
049
050  @NonNull
051  private static final String COMMAND = "generate-schema";
052  @NonNull
053  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
054      ExtraArgument.newInstance("metaschema-module-file-or-URL", true, URI.class),
055      ExtraArgument.newInstance("destination-schema-file", false, File.class)));
056
057  private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull(
058      Option.builder()
059          .longOpt("inline-types")
060          .desc("definitions declared inline will be generated as inline types")
061          .get());
062
063  @Override
064  public String getName() {
065    return COMMAND;
066  }
067
068  @Override
069  public String getDescription() {
070    return "Generate a schema for the specified Module module";
071  }
072
073  @SuppressWarnings("null")
074  @Override
075  public Collection<? extends Option> gatherOptions() {
076    return List.of(
077        MetaschemaCommands.OVERWRITE_OPTION,
078        MetaschemaCommands.AS_SCHEMA_FORMAT_OPTION,
079        INLINE_TYPES_OPTION);
080  }
081
082  @Override
083  public List<ExtraArgument> getExtraArguments() {
084    return EXTRA_ARGUMENTS;
085  }
086
087  @Override
088  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
089    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
090  }
091
092  /**
093   * Execute the schema generation operation.
094   *
095   * @param callingContext
096   *          the context information for the execution
097   * @param cmdLine
098   *          the parsed command line details
099   * @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}