1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.commands;
7   
8   import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9   import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
11  import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
12  import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
13  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
14  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
15  import gov.nist.secauto.metaschema.core.model.IModule;
16  import gov.nist.secauto.metaschema.core.model.util.MermaidErDiagramGenerator;
17  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
18  import gov.nist.secauto.metaschema.databind.IBindingContext;
19  
20  import org.apache.commons.cli.CommandLine;
21  import org.apache.commons.cli.Option;
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  
25  import java.io.IOException;
26  import java.io.PrintWriter;
27  import java.io.StringWriter;
28  import java.io.Writer;
29  import java.net.URI;
30  import java.net.URISyntaxException;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.nio.file.StandardOpenOption;
35  import java.util.Collection;
36  import java.util.List;
37  
38  import edu.umd.cs.findbugs.annotations.NonNull;
39  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
40  
41  /**
42   * This command implementation supports generation of a diagram depicting the
43   * objects and relationships within a provided Metaschema module.
44   */
45  class GenerateDiagramCommand
46      extends AbstractTerminalCommand {
47    private static final Logger LOGGER = LogManager.getLogger(GenerateDiagramCommand.class);
48  
49    @NonNull
50    private static final String COMMAND = "generate-diagram";
51    @NonNull
52    private static final List<ExtraArgument> EXTRA_ARGUMENTS;
53  
54    static {
55      EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
56          new DefaultExtraArgument("metaschema-module-file-or-URL", true),
57          new DefaultExtraArgument("destination-diagram-file", false)));
58    }
59  
60    @Override
61    public String getName() {
62      return COMMAND;
63    }
64  
65    @Override
66    public String getDescription() {
67      return "Generate a diagram for the provided Metaschema module";
68    }
69  
70    @SuppressWarnings("null")
71    @Override
72    public Collection<? extends Option> gatherOptions() {
73      return List.of(MetaschemaCommands.OVERWRITE_OPTION);
74    }
75  
76    @Override
77    public List<ExtraArgument> getExtraArguments() {
78      return EXTRA_ARGUMENTS;
79    }
80  
81    @Override
82    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
83      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
84    }
85  
86    /**
87     * Execute the diagram generation command.
88     *
89     * @param callingContext
90     *          information about the calling context
91     * @param cmdLine
92     *          the parsed command line details
93     * @throws CommandExecutionException
94     *           if an error occurred while executing the command
95     */
96    @SuppressWarnings({
97        "PMD.OnlyOneReturn", // readability
98        "PMD.AvoidCatchingGenericException"
99    })
100   @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION",
101       justification = "Catching generic exception for CLI error handling")
102   protected void executeCommand(
103       @NonNull CallingContext callingContext,
104       @NonNull CommandLine cmdLine) throws CommandExecutionException {
105 
106     List<String> extraArgs = cmdLine.getArgList();
107 
108     Path destination = null;
109     if (extraArgs.size() > 1) {
110       destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
111     }
112 
113     IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();
114 
115     URI moduleUri;
116     try {
117       moduleUri = resolveAgainstCWD(ObjectUtils.requireNonNull(extraArgs.get(0)));
118     } catch (URISyntaxException ex) {
119       throw new CommandExecutionException(
120           ExitCode.INVALID_ARGUMENTS,
121           String.format("Cannot load module as '%s' is not a valid file or URL. %s",
122               extraArgs.get(0),
123               ex.getLocalizedMessage()),
124           ex);
125     }
126     IModule module = MetaschemaCommands.loadModule(moduleUri, bindingContext);
127 
128     if (destination == null) {
129       Writer stringWriter = new StringWriter();
130       try (PrintWriter writer = new PrintWriter(stringWriter)) {
131         MermaidErDiagramGenerator.generate(module, writer);
132       }
133 
134       // Print the result
135       if (LOGGER.isInfoEnabled()) {
136         LOGGER.info(stringWriter.toString());
137       }
138     } else {
139       try (Writer writer = Files.newBufferedWriter(
140           destination,
141           StandardCharsets.UTF_8,
142           StandardOpenOption.CREATE,
143           StandardOpenOption.WRITE,
144           StandardOpenOption.TRUNCATE_EXISTING)) {
145         try (PrintWriter printWriter = new PrintWriter(writer)) {
146           MermaidErDiagramGenerator.generate(module, printWriter);
147         }
148       } catch (IOException ex) {
149         throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
150       }
151     }
152   }
153 }