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.configuration.DefaultConfiguration; 018import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration; 019import gov.nist.secauto.metaschema.core.model.IModule; 020import gov.nist.secauto.metaschema.core.model.MetaschemaException; 021import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader; 022import gov.nist.secauto.metaschema.core.util.CustomCollectors; 023import gov.nist.secauto.metaschema.core.util.ObjectUtils; 024import gov.nist.secauto.metaschema.core.util.UriUtils; 025import gov.nist.secauto.metaschema.databind.io.Format; 026import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator; 027import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat; 028import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature; 029 030import org.apache.commons.cli.CommandLine; 031import org.apache.commons.cli.Option; 032import org.apache.logging.log4j.LogManager; 033import org.apache.logging.log4j.Logger; 034 035import java.io.IOException; 036import java.io.OutputStream; 037import java.net.URI; 038import java.net.URISyntaxException; 039import java.nio.file.Files; 040import java.nio.file.Path; 041import java.nio.file.Paths; 042import java.util.Arrays; 043import java.util.Collection; 044import java.util.List; 045import java.util.Locale; 046 047import edu.umd.cs.findbugs.annotations.NonNull; 048 049public class GenerateSchemaCommand 050 extends AbstractTerminalCommand { 051 private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class); 052 053 @NonNull 054 private static final String COMMAND = "generate-schema"; 055 @NonNull 056 private static final List<ExtraArgument> EXTRA_ARGUMENTS; 057 058 @NonNull 059 private static final Option AS_OPTION = ObjectUtils.notNull( 060 Option.builder() 061 .longOpt("as") 062 .required() 063 .hasArg() 064 .argName("FORMAT") 065 .desc("source format: xml, json, or yaml") 066 .build()); 067 @NonNull 068 private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull( 069 Option.builder() 070 .longOpt("inline-types") 071 .desc("definitions declared inline will be generated as inline types") 072 .build()); 073 074 static { 075 EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 076 new DefaultExtraArgument("metaschema-module-file-or-URL", true), 077 new DefaultExtraArgument("destination-schema-file", false))); 078 } 079 080 @Override 081 public String getName() { 082 return COMMAND; 083 } 084 085 @Override 086 public String getDescription() { 087 return "Generate a schema for the specified Module module"; 088 } 089 090 @SuppressWarnings("null") 091 @Override 092 public Collection<? extends Option> gatherOptions() { 093 return List.of( 094 MetaschemaCommands.OVERWRITE_OPTION, 095 AS_OPTION, 096 INLINE_TYPES_OPTION); 097 } 098 099 @Override 100 public List<ExtraArgument> getExtraArguments() { 101 return EXTRA_ARGUMENTS; 102 } 103 104 @SuppressWarnings("PMD.PreserveStackTrace") // intended 105 @Override 106 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 107 try { 108 String asFormatText = cmdLine.getOptionValue(AS_OPTION); 109 if (asFormatText != null) { 110 SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT)); 111 } 112 } catch (IllegalArgumentException ex) { 113 InvalidArgumentException newEx = new InvalidArgumentException( // NOPMD - intentional 114 String.format("Invalid '%s' argument. The format must be one of: %s.", 115 OptionUtils.toArgument(AS_OPTION), 116 Arrays.asList(Format.values()).stream() 117 .map(format -> format.name()) 118 .collect(CustomCollectors.joiningWithOxfordComma("and")))); 119 newEx.setOption(AS_OPTION); 120 newEx.addSuppressed(ex); 121 throw newEx; 122 } 123 124 List<String> extraArgs = cmdLine.getArgList(); 125 if (extraArgs.isEmpty() || extraArgs.size() > 2) { 126 throw new InvalidArgumentException("Illegal number of arguments."); 127 } 128 } 129 130 @Override 131 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 132 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 133 } 134 135 /** 136 * Called to execute the schema generation. 137 * 138 * @param callingContext 139 * the context information for the execution 140 * @param cmdLine 141 * the parsed command line details 142 * @return the execution result 143 */ 144 @SuppressWarnings({ 145 "PMD.OnlyOneReturn", // readability 146 "unused" 147 }) 148 protected ExitStatus executeCommand( 149 @NonNull CallingContext callingContext, 150 @NonNull CommandLine cmdLine) { 151 List<String> extraArgs = cmdLine.getArgList(); 152 153 Path destination = null; 154 if (extraArgs.size() > 1) { 155 destination = Paths.get(extraArgs.get(1)).toAbsolutePath(); 156 } 157 158 if (destination != null) { 159 if (Files.exists(destination)) { 160 if (!cmdLine.hasOption(MetaschemaCommands.OVERWRITE_OPTION)) { 161 return ExitCode.INVALID_ARGUMENTS.exitMessage( // NOPMD readability 162 String.format("The provided destination '%s' already exists and the '%s' option was not provided.", 163 destination, 164 OptionUtils.toArgument(MetaschemaCommands.OVERWRITE_OPTION))); 165 } 166 if (!Files.isWritable(destination)) { 167 return ExitCode.IO_ERROR.exitMessage( // NOPMD readability 168 "The provided destination '" + destination + "' is not writable."); 169 } 170 } else { 171 Path parent = destination.getParent(); 172 if (parent != null) { 173 try { 174 Files.createDirectories(parent); 175 } catch (IOException ex) { 176 return ExitCode.INVALID_TARGET.exit().withThrowable(ex); // NOPMD readability 177 } 178 } 179 } 180 } 181 182 String asFormatText = cmdLine.getOptionValue(AS_OPTION); 183 SchemaFormat asFormat = SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT)); 184 185 IMutableConfiguration<SchemaGenerationFeature<?>> configuration = new DefaultConfiguration<>(); 186 if (cmdLine.hasOption(INLINE_TYPES_OPTION)) { 187 configuration.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 188 if (SchemaFormat.JSON.equals(asFormat)) { 189 configuration.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 190 } else { 191 configuration.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 192 } 193 } 194 195 String inputName = ObjectUtils.notNull(extraArgs.get(0)); 196 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri()); 197 198 URI input; 199 try { 200 input = UriUtils.toUri(inputName, cwd); 201 } catch (URISyntaxException ex) { 202 return ExitCode.IO_ERROR.exitMessage( 203 String.format("Unable to load '%s' as it is not a valid file or URI.", inputName)).withThrowable(ex); 204 } 205 assert input != null; 206 try { 207 ModuleLoader loader = new ModuleLoader(); 208 loader.allowEntityResolution(); 209 IModule module = loader.load(input); 210 211 if (LOGGER.isInfoEnabled()) { 212 LOGGER.info("Generating {} schema for '{}'.", asFormat.name(), input); 213 } 214 if (destination == null) { 215 @SuppressWarnings({ "resource", "PMD.CloseResource" }) // not owned 216 OutputStream os = ObjectUtils.notNull(System.out); 217 ISchemaGenerator.generateSchema(module, os, asFormat, configuration); 218 } else { 219 ISchemaGenerator.generateSchema(module, destination, asFormat, configuration); 220 } 221 } catch (IOException | MetaschemaException ex) { 222 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); // NOPMD readability 223 } 224 if (destination != null && LOGGER.isInfoEnabled()) { 225 LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination); 226 } 227 return ExitCode.OK.exit(); 228 } 229}