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.IOException;
14  import java.io.PrintWriter;
15  import java.io.StringWriter;
16  import java.io.Writer;
17  import java.net.URI;
18  import java.net.URISyntaxException;
19  import java.nio.charset.StandardCharsets;
20  import java.nio.file.Files;
21  import java.nio.file.Path;
22  import java.nio.file.StandardOpenOption;
23  import java.util.Collection;
24  import java.util.List;
25  
26  import dev.metaschema.cli.processor.CallingContext;
27  import dev.metaschema.cli.processor.ExitCode;
28  import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
29  import dev.metaschema.cli.processor.command.CommandExecutionException;
30  import dev.metaschema.cli.processor.command.ExtraArgument;
31  import dev.metaschema.cli.processor.command.ICommandExecutor;
32  import dev.metaschema.core.model.IModule;
33  import dev.metaschema.core.model.util.MermaidErDiagramGenerator;
34  import dev.metaschema.core.util.ObjectUtils;
35  import dev.metaschema.databind.IBindingContext;
36  import edu.umd.cs.findbugs.annotations.NonNull;
37  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38  
39  /**
40   * This command implementation supports generation of a diagram depicting the
41   * objects and relationships within a provided Metaschema module.
42   */
43  public class GenerateDiagramCommand
44      extends AbstractTerminalCommand {
45    private static final Logger LOGGER = LogManager.getLogger(GenerateDiagramCommand.class);
46  
47    @NonNull
48    private static final String COMMAND = "generate-diagram";
49    @NonNull
50    private static final List<ExtraArgument> EXTRA_ARGUMENTS;
51  
52    static {
53      EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
54          ExtraArgument.newInstance("metaschema-module-file-or-URL", true),
55          ExtraArgument.newInstance("destination-diagram-file", false)));
56    }
57  
58    @Override
59    public String getName() {
60      return COMMAND;
61    }
62  
63    @Override
64    public String getDescription() {
65      return "Generate a diagram for the provided Metaschema module";
66    }
67  
68    @SuppressWarnings("null")
69    @Override
70    public Collection<? extends Option> gatherOptions() {
71      return List.of(MetaschemaCommands.OVERWRITE_OPTION);
72    }
73  
74    @Override
75    public List<ExtraArgument> getExtraArguments() {
76      return EXTRA_ARGUMENTS;
77    }
78  
79    @Override
80    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
81      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
82    }
83  
84    /**
85     * Execute the diagram generation command.
86     *
87     * @param callingContext
88     *          information about the calling context
89     * @param cmdLine
90     *          the parsed command line details
91     * @throws CommandExecutionException
92     *           if an error occurred while executing the command
93     */
94    @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION",
95        justification = "Catching generic exception for CLI error handling")
96    protected void executeCommand(
97        @NonNull CallingContext callingContext,
98        @NonNull CommandLine cmdLine) throws CommandExecutionException {
99  
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 }