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.AbstractCommandExecutor; 014import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 015import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument; 016import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 017import gov.nist.secauto.metaschema.core.util.CustomCollectors; 018import gov.nist.secauto.metaschema.core.util.ObjectUtils; 019import gov.nist.secauto.metaschema.core.util.UriUtils; 020import gov.nist.secauto.metaschema.databind.IBindingContext; 021import gov.nist.secauto.metaschema.databind.io.Format; 022import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 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.FileNotFoundException; 030import java.io.IOException; 031import java.io.OutputStreamWriter; 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; 042import java.util.Locale; 043 044import edu.umd.cs.findbugs.annotations.NonNull; 045 046public abstract class AbstractConvertSubcommand 047 extends AbstractTerminalCommand { 048 private static final Logger LOGGER = LogManager.getLogger(AbstractConvertSubcommand.class); 049 050 @NonNull 051 private static final String COMMAND = "convert"; 052 @NonNull 053 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 054 new DefaultExtraArgument("source-file-or-URL", true), 055 new DefaultExtraArgument("destination-file", false))); 056 057 @NonNull 058 private static final Option OVERWRITE_OPTION = ObjectUtils.notNull( 059 Option.builder() 060 .longOpt("overwrite") 061 .desc("overwrite the destination if it exists") 062 .build()); 063 @NonNull 064 private static final Option TO_OPTION = ObjectUtils.notNull( 065 Option.builder() 066 .longOpt("to") 067 .required() 068 .hasArg().argName("FORMAT") 069 .desc("convert to format: xml, json, or yaml") 070 .build()); 071 072 @Override 073 public String getName() { 074 return COMMAND; 075 } 076 077 @Override 078 public Collection<? extends Option> gatherOptions() { 079 return ObjectUtils.notNull(List.of( 080 OVERWRITE_OPTION, 081 TO_OPTION)); 082 } 083 084 @Override 085 public List<ExtraArgument> getExtraArguments() { 086 return EXTRA_ARGUMENTS; 087 } 088 089 @SuppressWarnings("PMD.PreserveStackTrace") // intended 090 @Override 091 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 092 093 try { 094 String toFormatText = cmdLine.getOptionValue(TO_OPTION); 095 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 096 } catch (IllegalArgumentException ex) { 097 InvalidArgumentException newEx = new InvalidArgumentException( 098 String.format("Invalid '%s' argument. The format must be one of: %s.", 099 OptionUtils.toArgument(TO_OPTION), 100 Format.names().stream() 101 .collect(CustomCollectors.joiningWithOxfordComma("and")))); 102 newEx.setOption(TO_OPTION); 103 newEx.addSuppressed(ex); 104 throw newEx; 105 } 106 107 List<String> extraArgs = cmdLine.getArgList(); 108 if (extraArgs.isEmpty() || extraArgs.size() > 2) { 109 throw new InvalidArgumentException("Illegal number of arguments."); 110 } 111 } 112 113 protected abstract static class AbstractConversionCommandExecutor 114 extends AbstractCommandExecutor { 115 116 /** 117 * Construct a new command executor. 118 * 119 * @param callingContext 120 * the context of the command execution 121 * @param commandLine 122 * the parsed command line details 123 */ 124 protected AbstractConversionCommandExecutor( 125 @NonNull CallingContext callingContext, 126 @NonNull CommandLine commandLine) { 127 super(callingContext, commandLine); 128 } 129 130 /** 131 * Get the binding context to use for data processing. 132 * 133 * @return the context 134 */ 135 @NonNull 136 protected abstract IBindingContext getBindingContext(); 137 138 @SuppressWarnings({ 139 "PMD.OnlyOneReturn", // readability 140 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" // reasonable 141 }) 142 @Override 143 public ExitStatus execute() { 144 CommandLine cmdLine = getCommandLine(); 145 146 List<String> extraArgs = cmdLine.getArgList(); 147 148 Path destination = null; 149 if (extraArgs.size() > 1) { 150 destination = Paths.get(extraArgs.get(1)).toAbsolutePath(); 151 } 152 153 if (destination != null) { 154 if (Files.exists(destination)) { 155 if (!cmdLine.hasOption(OVERWRITE_OPTION)) { 156 return ExitCode.INVALID_ARGUMENTS.exitMessage( 157 String.format("The provided destination '%s' already exists and the '%s' option was not provided.", 158 destination, 159 OptionUtils.toArgument(OVERWRITE_OPTION))); 160 } 161 if (!Files.isWritable(destination)) { 162 return ExitCode.IO_ERROR.exitMessage( 163 "The provided destination '" + destination + "' is not writable."); 164 } 165 } else { 166 Path parent = destination.getParent(); 167 if (parent != null) { 168 try { 169 Files.createDirectories(parent); 170 } catch (IOException ex) { 171 return ExitCode.INVALID_TARGET.exit().withThrowable(ex); // NOPMD readability 172 } 173 } 174 } 175 } 176 177 String sourceName = ObjectUtils.notNull(extraArgs.get(0)); 178 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri()); 179 180 URI source; 181 try { 182 source = UriUtils.toUri(sourceName, cwd); 183 } catch (URISyntaxException ex) { 184 return ExitCode.IO_ERROR.exitMessage("Cannot load source '%s' as it is not a valid file or URI.") 185 .withThrowable(ex); 186 } 187 assert source != null; 188 189 String toFormatText = cmdLine.getOptionValue(TO_OPTION); 190 Format toFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 191 192 IBindingContext bindingContext = getBindingContext(); 193 try { 194 IBoundLoader loader = bindingContext.newBoundLoader(); 195 if (LOGGER.isInfoEnabled()) { 196 LOGGER.info("Converting '{}'.", source); 197 } 198 199 if (destination == null) { 200 // write to STDOUT 201 OutputStreamWriter writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8); 202 handleConversion(source, toFormat, writer, loader); 203 } else { 204 try (Writer writer = Files.newBufferedWriter( 205 destination, 206 StandardCharsets.UTF_8, 207 StandardOpenOption.CREATE, 208 StandardOpenOption.WRITE, 209 StandardOpenOption.TRUNCATE_EXISTING)) { 210 assert writer != null; 211 handleConversion(source, toFormat, writer, loader); 212 } 213 } 214 } catch (IOException | IllegalArgumentException ex) { 215 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); // NOPMD readability 216 } 217 if (destination != null && LOGGER.isInfoEnabled()) { 218 LOGGER.info("Generated {} file: {}", toFormat.toString(), destination); 219 } 220 return ExitCode.OK.exit(); 221 } 222 223 /** 224 * Called to perform a content conversion. 225 * 226 * @param source 227 * the resource to convert 228 * @param toFormat 229 * the format to convert to 230 * @param writer 231 * the writer to use to write converted content 232 * @param loader 233 * the Metaschema loader to use to load the content to convert 234 * @throws FileNotFoundException 235 * if the requested resource was not found 236 * @throws IOException 237 * if there was an error reading or writing content 238 */ 239 protected abstract void handleConversion( 240 @NonNull URI source, 241 @NonNull Format toFormat, 242 @NonNull Writer writer, 243 @NonNull IBoundLoader loader) throws FileNotFoundException, IOException; 244 } 245}