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