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.ExitStatus;
11  import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
12  import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
13  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
14  import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
15  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
16  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
17  import gov.nist.secauto.metaschema.core.model.IModule;
18  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
19  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
20  import gov.nist.secauto.metaschema.core.util.MermaidErDiagramGenerator;
21  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
22  import gov.nist.secauto.metaschema.core.util.UriUtils;
23  
24  import org.apache.commons.cli.CommandLine;
25  import org.apache.commons.cli.Option;
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  
29  import java.io.IOException;
30  import java.io.PrintWriter;
31  import java.io.StringWriter;
32  import java.io.Writer;
33  import java.net.URI;
34  import java.net.URISyntaxException;
35  import java.nio.charset.StandardCharsets;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.nio.file.Paths;
39  import java.nio.file.StandardOpenOption;
40  import java.util.Collection;
41  import java.util.List;
42  
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  
45  public 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(
74          MetaschemaCommands.OVERWRITE_OPTION);
75    }
76  
77    @Override
78    public List<ExtraArgument> getExtraArguments() {
79      return EXTRA_ARGUMENTS;
80    }
81  
82    @Override
83    public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
84      List<String> extraArgs = cmdLine.getArgList();
85      if (extraArgs.isEmpty() || extraArgs.size() > 2) {
86        throw new InvalidArgumentException("Illegal number of arguments.");
87      }
88    }
89  
90    @Override
91    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
92      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
93    }
94  
95    /**
96     * Execute the diagram generation command.
97     *
98     * @param callingContext
99     *          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 }