001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.cli.commands;
028
029import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
030import gov.nist.secauto.metaschema.cli.processor.ExitCode;
031import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
032import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
033import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
034import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
035import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
036import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
037import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
038import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
039import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
040import gov.nist.secauto.metaschema.core.model.IModule;
041import gov.nist.secauto.metaschema.core.model.MetaschemaException;
042import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
043import gov.nist.secauto.metaschema.core.util.CustomCollectors;
044import gov.nist.secauto.metaschema.core.util.ObjectUtils;
045import gov.nist.secauto.metaschema.databind.io.Format;
046import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator;
047import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat;
048import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature;
049
050import org.apache.commons.cli.CommandLine;
051import org.apache.commons.cli.Option;
052import org.apache.logging.log4j.LogManager;
053import org.apache.logging.log4j.Logger;
054
055import java.io.IOException;
056import java.io.OutputStream;
057import java.nio.file.Files;
058import java.nio.file.Path;
059import java.nio.file.Paths;
060import java.util.Arrays;
061import java.util.Collection;
062import java.util.List;
063import java.util.Locale;
064
065import edu.umd.cs.findbugs.annotations.NonNull;
066
067public class GenerateSchemaCommand
068    extends AbstractTerminalCommand {
069  private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class);
070
071  @NonNull
072  private static final String COMMAND = "generate-schema";
073  @NonNull
074  private static final List<ExtraArgument> EXTRA_ARGUMENTS;
075  @NonNull
076  private static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
077      Option.builder()
078          .longOpt("overwrite")
079          .desc("overwrite the destination if it exists")
080          .build());
081  @NonNull
082  private static final Option AS_OPTION = ObjectUtils.notNull(
083      Option.builder()
084          .longOpt("as")
085          .required()
086          .hasArg()
087          .argName("FORMAT")
088          .desc("source format: xml, json, or yaml")
089          .build());
090  @NonNull
091  private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull(
092      Option.builder()
093          .longOpt("inline-types")
094          .desc("definitions declared inline will be generated as inline types")
095          .build());
096
097  static {
098    EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
099        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}