001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.cli.commands; 007 008import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext; 009import gov.nist.secauto.metaschema.cli.processor.ExitCode; 010import gov.nist.secauto.metaschema.cli.processor.ExitStatus; 011import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException; 012import gov.nist.secauto.metaschema.cli.processor.OptionUtils; 013import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 014import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument; 015import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 016import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor; 017import gov.nist.secauto.metaschema.core.model.IModule; 018import gov.nist.secauto.metaschema.core.model.MetaschemaException; 019import gov.nist.secauto.metaschema.core.util.CollectionUtil; 020import gov.nist.secauto.metaschema.core.util.MermaidErDiagramGenerator; 021import gov.nist.secauto.metaschema.core.util.ObjectUtils; 022import gov.nist.secauto.metaschema.core.util.UriUtils; 023 024import org.apache.commons.cli.CommandLine; 025import org.apache.commons.cli.Option; 026import org.apache.logging.log4j.LogManager; 027import org.apache.logging.log4j.Logger; 028 029import java.io.IOException; 030import java.io.PrintWriter; 031import java.io.StringWriter; 032import java.io.Writer; 033import java.net.URI; 034import java.net.URISyntaxException; 035import java.nio.charset.StandardCharsets; 036import java.nio.file.Files; 037import java.nio.file.Path; 038import java.nio.file.Paths; 039import java.nio.file.StandardOpenOption; 040import java.util.Collection; 041import java.util.List; 042 043import edu.umd.cs.findbugs.annotations.NonNull; 044 045public class GenerateDiagramCommand 046 extends AbstractTerminalCommand { 047 private static final Logger LOGGER = LogManager.getLogger(GenerateDiagramCommand.class); 048 049 @NonNull 050 private static final String COMMAND = "generate-diagram"; 051 @NonNull 052 private static final List<ExtraArgument> EXTRA_ARGUMENTS; 053 054 static { 055 EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 056 new DefaultExtraArgument("metaschema-module-file-or-URL", true), 057 new DefaultExtraArgument("destination-diagram-file", false))); 058 } 059 060 @Override 061 public String getName() { 062 return COMMAND; 063 } 064 065 @Override 066 public String getDescription() { 067 return "Generate a diagram for the provided Metaschema module"; 068 } 069 070 @SuppressWarnings("null") 071 @Override 072 public Collection<? extends Option> gatherOptions() { 073 return List.of( 074 MetaschemaCommands.OVERWRITE_OPTION); 075 } 076 077 @Override 078 public List<ExtraArgument> getExtraArguments() { 079 return EXTRA_ARGUMENTS; 080 } 081 082 @Override 083 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 084 List<String> extraArgs = cmdLine.getArgList(); 085 if (extraArgs.isEmpty() || extraArgs.size() > 2) { 086 throw new InvalidArgumentException("Illegal number of arguments."); 087 } 088 } 089 090 @Override 091 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 092 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 093 } 094 095 /** 096 * Execute the diagram generation command. 097 * 098 * @param callingContext 099 * 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}