001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.cli.commands;
007
008import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
009import gov.nist.secauto.metaschema.cli.processor.ExitCode;
010import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
011import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
012import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
013import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
014import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
015import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
016import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
017import gov.nist.secauto.metaschema.core.model.IModule;
018import gov.nist.secauto.metaschema.core.model.MetaschemaException;
019import gov.nist.secauto.metaschema.core.util.CollectionUtil;
020import gov.nist.secauto.metaschema.core.util.MermaidErDiagramGenerator;
021import gov.nist.secauto.metaschema.core.util.ObjectUtils;
022import gov.nist.secauto.metaschema.core.util.UriUtils;
023
024import org.apache.commons.cli.CommandLine;
025import org.apache.commons.cli.Option;
026import org.apache.logging.log4j.LogManager;
027import org.apache.logging.log4j.Logger;
028
029import java.io.IOException;
030import java.io.PrintWriter;
031import java.io.StringWriter;
032import java.io.Writer;
033import java.net.URI;
034import java.net.URISyntaxException;
035import java.nio.charset.StandardCharsets;
036import java.nio.file.Files;
037import java.nio.file.Path;
038import java.nio.file.Paths;
039import java.nio.file.StandardOpenOption;
040import java.util.Collection;
041import java.util.List;
042
043import edu.umd.cs.findbugs.annotations.NonNull;
044
045public class GenerateDiagramCommand
046    extends AbstractTerminalCommand {
047  private static final Logger LOGGER = LogManager.getLogger(GenerateDiagramCommand.class);
048
049  @NonNull
050  private static final String COMMAND = "generate-diagram";
051  @NonNull
052  private static final List<ExtraArgument> EXTRA_ARGUMENTS;
053
054  static {
055    EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
056        new DefaultExtraArgument("metaschema-module-file-or-URL", true),
057        new DefaultExtraArgument("destination-diagram-file", false)));
058  }
059
060  @Override
061  public String getName() {
062    return COMMAND;
063  }
064
065  @Override
066  public String getDescription() {
067    return "Generate a diagram for the provided Metaschema module";
068  }
069
070  @SuppressWarnings("null")
071  @Override
072  public Collection<? extends Option> gatherOptions() {
073    return List.of(
074        MetaschemaCommands.OVERWRITE_OPTION);
075  }
076
077  @Override
078  public List<ExtraArgument> getExtraArguments() {
079    return EXTRA_ARGUMENTS;
080  }
081
082  @Override
083  public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
084    List<String> extraArgs = cmdLine.getArgList();
085    if (extraArgs.isEmpty() || extraArgs.size() > 2) {
086      throw new InvalidArgumentException("Illegal number of arguments.");
087    }
088  }
089
090  @Override
091  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
092    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
093  }
094
095  /**
096   * Execute the diagram generation command.
097   *
098   * @param callingContext
099   *          information about the calling context
100   * @param cmdLine
101   *          the parsed command line details
102   * @return the execution result
103   */
104  @SuppressWarnings({
105      "PMD.OnlyOneReturn", // readability
106  })
107  protected ExitStatus executeCommand(
108      @NonNull CallingContext callingContext,
109      @NonNull CommandLine cmdLine) {
110
111    List<String> extraArgs = cmdLine.getArgList();
112
113    Path destination = null;
114    if (extraArgs.size() > 1) {
115      destination = Paths.get(extraArgs.get(1)).toAbsolutePath();
116    }
117
118    if (destination != null) {
119      if (Files.exists(destination)) {
120        if (!cmdLine.hasOption(MetaschemaCommands.OVERWRITE_OPTION)) {
121          return ExitCode.INVALID_ARGUMENTS.exitMessage( // NOPMD readability
122              String.format("The provided destination '%s' already exists and the '%s' option was not provided.",
123                  destination,
124                  OptionUtils.toArgument(MetaschemaCommands.OVERWRITE_OPTION)));
125        }
126        if (!Files.isWritable(destination)) {
127          return ExitCode.IO_ERROR.exitMessage( // NOPMD readability
128              "The provided destination '" + destination + "' is not writable.");
129        }
130      } else {
131        Path parent = destination.getParent();
132        if (parent != null) {
133          try {
134            Files.createDirectories(parent);
135          } catch (IOException ex) {
136            return ExitCode.INVALID_TARGET.exit().withThrowable(ex); // NOPMD readability
137          }
138        }
139      }
140    }
141
142    URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri());
143
144    IModule module;
145    try {
146      URI moduleUri = UriUtils.toUri(ObjectUtils.requireNonNull(extraArgs.get(0)), cwd);
147      module = MetaschemaCommands.handleModule(moduleUri, CollectionUtil.emptyList());
148    } catch (URISyntaxException ex) {
149      return ExitCode.INVALID_ARGUMENTS
150          .exitMessage(
151              String.format("Cannot load module as '%s' is not a valid file or URL.", ex.getInput()))
152          .withThrowable(ex);
153    } catch (IOException | MetaschemaException ex) {
154      return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
155    }
156
157    try {
158      if (destination == null) {
159        Writer stringWriter = new StringWriter();
160        try (PrintWriter writer = new PrintWriter(stringWriter)) {
161          MermaidErDiagramGenerator.generate(module, writer);
162        }
163
164        // Print the result
165        if (LOGGER.isInfoEnabled()) {
166          LOGGER.info(stringWriter.toString());
167        }
168      } else {
169        try (Writer writer = Files.newBufferedWriter(
170            destination,
171            StandardCharsets.UTF_8,
172            StandardOpenOption.CREATE,
173            StandardOpenOption.WRITE,
174            StandardOpenOption.TRUNCATE_EXISTING)) {
175          try (PrintWriter printWriter = new PrintWriter(writer)) {
176            MermaidErDiagramGenerator.generate(module, printWriter);
177          }
178        }
179      }
180
181      return ExitCode.OK.exit();
182    } catch (Exception ex) {
183      return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
184    }
185  }
186}