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.IOException;
014import java.io.PrintWriter;
015import java.io.StringWriter;
016import java.io.Writer;
017import java.net.URI;
018import java.net.URISyntaxException;
019import java.nio.charset.StandardCharsets;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.StandardOpenOption;
023import java.util.Collection;
024import java.util.List;
025
026import dev.metaschema.cli.processor.CallingContext;
027import dev.metaschema.cli.processor.ExitCode;
028import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
029import dev.metaschema.cli.processor.command.CommandExecutionException;
030import dev.metaschema.cli.processor.command.ExtraArgument;
031import dev.metaschema.cli.processor.command.ICommandExecutor;
032import dev.metaschema.core.model.IModule;
033import dev.metaschema.core.model.util.MermaidErDiagramGenerator;
034import dev.metaschema.core.util.ObjectUtils;
035import dev.metaschema.databind.IBindingContext;
036import edu.umd.cs.findbugs.annotations.NonNull;
037import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
038
039/**
040 * This command implementation supports generation of a diagram depicting the
041 * objects and relationships within a provided Metaschema module.
042 */
043public class GenerateDiagramCommand
044    extends AbstractTerminalCommand {
045  private static final Logger LOGGER = LogManager.getLogger(GenerateDiagramCommand.class);
046
047  @NonNull
048  private static final String COMMAND = "generate-diagram";
049  @NonNull
050  private static final List<ExtraArgument> EXTRA_ARGUMENTS;
051
052  static {
053    EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
054        ExtraArgument.newInstance("metaschema-module-file-or-URL", true),
055        ExtraArgument.newInstance("destination-diagram-file", false)));
056  }
057
058  @Override
059  public String getName() {
060    return COMMAND;
061  }
062
063  @Override
064  public String getDescription() {
065    return "Generate a diagram for the provided Metaschema module";
066  }
067
068  @SuppressWarnings("null")
069  @Override
070  public Collection<? extends Option> gatherOptions() {
071    return List.of(MetaschemaCommands.OVERWRITE_OPTION);
072  }
073
074  @Override
075  public List<ExtraArgument> getExtraArguments() {
076    return EXTRA_ARGUMENTS;
077  }
078
079  @Override
080  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
081    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
082  }
083
084  /**
085   * Execute the diagram generation command.
086   *
087   * @param callingContext
088   *          information about the calling context
089   * @param cmdLine
090   *          the parsed command line details
091   * @throws CommandExecutionException
092   *           if an error occurred while executing the command
093   */
094  @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION",
095      justification = "Catching generic exception for CLI error handling")
096  protected void executeCommand(
097      @NonNull CallingContext callingContext,
098      @NonNull CommandLine cmdLine) throws CommandExecutionException {
099
100    List<String> extraArgs = cmdLine.getArgList();
101
102    Path destination = null;
103    if (extraArgs.size() > 1) {
104      destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
105    }
106
107    IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();
108
109    URI moduleUri;
110    try {
111      moduleUri = resolveAgainstCWD(ObjectUtils.requireNonNull(extraArgs.get(0)));
112    } catch (URISyntaxException ex) {
113      throw new CommandExecutionException(
114          ExitCode.INVALID_ARGUMENTS,
115          String.format("Cannot load module as '%s' is not a valid file or URL. %s",
116              extraArgs.get(0),
117              ex.getLocalizedMessage()),
118          ex);
119    }
120    IModule module = MetaschemaCommands.loadModule(moduleUri, bindingContext);
121
122    if (destination == null) {
123      Writer stringWriter = new StringWriter();
124      try (PrintWriter writer = new PrintWriter(stringWriter)) {
125        MermaidErDiagramGenerator.generate(module, writer);
126      }
127
128      // Print the result
129      if (LOGGER.isInfoEnabled()) {
130        LOGGER.info(stringWriter.toString());
131      }
132    } else {
133      try (Writer writer = Files.newBufferedWriter(
134          destination,
135          StandardCharsets.UTF_8,
136          StandardOpenOption.CREATE,
137          StandardOpenOption.WRITE,
138          StandardOpenOption.TRUNCATE_EXISTING)) {
139        try (PrintWriter printWriter = new PrintWriter(writer)) {
140          MermaidErDiagramGenerator.generate(module, printWriter);
141        }
142      } catch (IOException ex) {
143        throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
144      }
145    }
146  }
147}