001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.metaschema.cli.commands; 028 029import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext; 030import gov.nist.secauto.metaschema.cli.processor.ExitCode; 031import gov.nist.secauto.metaschema.cli.processor.ExitStatus; 032import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException; 033import gov.nist.secauto.metaschema.cli.processor.OptionUtils; 034import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 035import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument; 036import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 037import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor; 038import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; 039import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration; 040import gov.nist.secauto.metaschema.core.model.IModule; 041import gov.nist.secauto.metaschema.core.model.MetaschemaException; 042import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader; 043import gov.nist.secauto.metaschema.core.util.CustomCollectors; 044import gov.nist.secauto.metaschema.core.util.ObjectUtils; 045import gov.nist.secauto.metaschema.databind.io.Format; 046import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator; 047import gov.nist.secauto.metaschema.schemagen.ISchemaGenerator.SchemaFormat; 048import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature; 049 050import org.apache.commons.cli.CommandLine; 051import org.apache.commons.cli.Option; 052import org.apache.logging.log4j.LogManager; 053import org.apache.logging.log4j.Logger; 054 055import java.io.IOException; 056import java.io.OutputStream; 057import java.nio.file.Files; 058import java.nio.file.Path; 059import java.nio.file.Paths; 060import java.util.Arrays; 061import java.util.Collection; 062import java.util.List; 063import java.util.Locale; 064 065import edu.umd.cs.findbugs.annotations.NonNull; 066 067public class GenerateSchemaCommand 068 extends AbstractTerminalCommand { 069 private static final Logger LOGGER = LogManager.getLogger(GenerateSchemaCommand.class); 070 071 @NonNull 072 private static final String COMMAND = "generate-schema"; 073 @NonNull 074 private static final List<ExtraArgument> EXTRA_ARGUMENTS; 075 @NonNull 076 private static final Option OVERWRITE_OPTION = ObjectUtils.notNull( 077 Option.builder() 078 .longOpt("overwrite") 079 .desc("overwrite the destination if it exists") 080 .build()); 081 @NonNull 082 private static final Option AS_OPTION = ObjectUtils.notNull( 083 Option.builder() 084 .longOpt("as") 085 .required() 086 .hasArg() 087 .argName("FORMAT") 088 .desc("source format: xml, json, or yaml") 089 .build()); 090 @NonNull 091 private static final Option INLINE_TYPES_OPTION = ObjectUtils.notNull( 092 Option.builder() 093 .longOpt("inline-types") 094 .desc("definitions declared inline will be generated as inline types") 095 .build()); 096 097 static { 098 EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 099 new DefaultExtraArgument("metaschema-module-file", true), 100 new DefaultExtraArgument("destination-schema-file", false))); 101 } 102 103 @Override 104 public String getName() { 105 return COMMAND; 106 } 107 108 @Override 109 public String getDescription() { 110 return "Generate a schema for the specified Module module"; 111 } 112 113 @SuppressWarnings("null") 114 @Override 115 public Collection<? extends Option> gatherOptions() { 116 return List.of( 117 OVERWRITE_OPTION, 118 AS_OPTION, 119 INLINE_TYPES_OPTION); 120 } 121 122 @Override 123 public List<ExtraArgument> getExtraArguments() { 124 return EXTRA_ARGUMENTS; 125 } 126 127 @SuppressWarnings("PMD.PreserveStackTrace") // intended 128 @Override 129 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 130 try { 131 String asFormatText = cmdLine.getOptionValue(AS_OPTION); 132 if (asFormatText != null) { 133 SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT)); 134 } 135 } catch (IllegalArgumentException ex) { 136 InvalidArgumentException newEx = new InvalidArgumentException( // NOPMD - intentional 137 String.format("Invalid '%s' argument. The format must be one of: %s.", 138 OptionUtils.toArgument(AS_OPTION), 139 Arrays.asList(Format.values()).stream() 140 .map(format -> format.name()) 141 .collect(CustomCollectors.joiningWithOxfordComma("and")))); 142 newEx.setOption(AS_OPTION); 143 newEx.addSuppressed(ex); 144 throw newEx; 145 } 146 147 List<String> extraArgs = cmdLine.getArgList(); 148 if (extraArgs.isEmpty() || extraArgs.size() > 2) { 149 throw new InvalidArgumentException("Illegal number of arguments."); 150 } 151 152 Path module = Paths.get(extraArgs.get(0)); 153 if (!Files.exists(module)) { 154 throw new InvalidArgumentException("The provided metaschema module '" + module + "' does not exist."); 155 } 156 if (!Files.isReadable(module)) { 157 throw new InvalidArgumentException("The provided metaschema module '" + module + "' is not readable."); 158 } 159 } 160 161 @Override 162 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 163 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 164 } 165 166 @SuppressWarnings({ 167 "PMD.OnlyOneReturn", // readability 168 "unused" 169 }) 170 protected ExitStatus executeCommand( 171 @NonNull CallingContext callingContext, 172 @NonNull CommandLine cmdLine) { 173 List<String> extraArgs = cmdLine.getArgList(); 174 175 Path destination = null; 176 if (extraArgs.size() > 1) { 177 destination = Paths.get(extraArgs.get(1)).toAbsolutePath(); 178 } 179 180 if (destination != null) { 181 if (Files.exists(destination)) { 182 if (!cmdLine.hasOption(OVERWRITE_OPTION)) { 183 return ExitCode.INVALID_ARGUMENTS.exitMessage( // NOPMD readability 184 String.format("The provided destination '%s' already exists and the '%s' option was not provided.", 185 destination, 186 OptionUtils.toArgument(OVERWRITE_OPTION))); 187 } 188 if (!Files.isWritable(destination)) { 189 return ExitCode.IO_ERROR.exitMessage( // NOPMD readability 190 "The provided destination '" + destination + "' is not writable."); 191 } 192 } else { 193 Path parent = destination.getParent(); 194 if (parent != null) { 195 try { 196 Files.createDirectories(parent); 197 } catch (IOException ex) { 198 return ExitCode.INVALID_TARGET.exit().withThrowable(ex); // NOPMD readability 199 } 200 } 201 } 202 } 203 204 String asFormatText = cmdLine.getOptionValue(AS_OPTION); 205 SchemaFormat asFormat = SchemaFormat.valueOf(asFormatText.toUpperCase(Locale.ROOT)); 206 207 IMutableConfiguration<SchemaGenerationFeature<?>> configuration = new DefaultConfiguration<>(); 208 if (cmdLine.hasOption(INLINE_TYPES_OPTION)) { 209 configuration.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS); 210 if (SchemaFormat.JSON.equals(asFormat)) { 211 configuration.disableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 212 } else { 213 configuration.enableFeature(SchemaGenerationFeature.INLINE_CHOICE_DEFINITIONS); 214 } 215 } 216 217 Path input = Paths.get(extraArgs.get(0)); 218 assert input != null; 219 try { 220 ModuleLoader loader = new ModuleLoader(); 221 loader.allowEntityResolution(); 222 IModule module = loader.load(input); 223 224 if (LOGGER.isInfoEnabled()) { 225 LOGGER.info("Generating {} schema for '{}'.", asFormat.name(), input); 226 } 227 if (destination == null) { 228 @SuppressWarnings({ "resource", "PMD.CloseResource" }) // not owned 229 OutputStream os = ObjectUtils.notNull(System.out); 230 ISchemaGenerator.generateSchema(module, os, asFormat, configuration); 231 } else { 232 ISchemaGenerator.generateSchema(module, destination, asFormat, configuration); 233 } 234 } catch (IOException | MetaschemaException ex) { 235 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); // NOPMD readability 236 } 237 if (destination != null && LOGGER.isInfoEnabled()) { 238 LOGGER.info("Generated {} schema file: {}", asFormat.toString(), destination); 239 } 240 return ExitCode.OK.exit(); 241 } 242}