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; 030import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext; 031import gov.nist.secauto.metaschema.cli.processor.ExitCode; 032import gov.nist.secauto.metaschema.cli.processor.ExitStatus; 033import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException; 034import gov.nist.secauto.metaschema.cli.processor.OptionUtils; 035import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor; 036import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 037import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument; 038import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 039import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler; 040import gov.nist.secauto.metaschema.core.model.MetaschemaException; 041import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet; 042import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; 043import gov.nist.secauto.metaschema.core.model.xml.ConstraintLoader; 044import gov.nist.secauto.metaschema.core.util.CollectionUtil; 045import gov.nist.secauto.metaschema.core.util.CustomCollectors; 046import gov.nist.secauto.metaschema.core.util.ObjectUtils; 047import gov.nist.secauto.metaschema.databind.IBindingContext; 048import gov.nist.secauto.metaschema.databind.IBindingContext.IValidationSchemaProvider; 049import gov.nist.secauto.metaschema.databind.io.Format; 050import gov.nist.secauto.metaschema.databind.io.FormatDetector; 051import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 052 053import org.apache.commons.cli.CommandLine; 054import org.apache.commons.cli.Option; 055import org.apache.logging.log4j.LogManager; 056import org.apache.logging.log4j.Logger; 057import org.xml.sax.SAXException; 058 059import java.io.FileNotFoundException; 060import java.io.IOException; 061import java.nio.file.Files; 062import java.nio.file.Path; 063import java.nio.file.Paths; 064import java.util.Arrays; 065import java.util.Collection; 066import java.util.LinkedHashSet; 067import java.util.List; 068import java.util.Locale; 069import java.util.Set; 070 071import edu.umd.cs.findbugs.annotations.NonNull; 072 073public abstract class AbstractValidateContentCommand 074 extends AbstractTerminalCommand { 075 private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class); 076 @NonNull 077 private static final String COMMAND = "validate"; 078 @NonNull 079 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 080 new DefaultExtraArgument("file to validate", true))); 081 082 @NonNull 083 private static final Option AS_OPTION = ObjectUtils.notNull( 084 Option.builder() 085 .longOpt("as") 086 .hasArg() 087 .argName("FORMAT") 088 .desc("source format: xml, json, or yaml") 089 .build()); 090 @NonNull 091 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull( 092 Option.builder("c") 093 .hasArg() 094 .argName("FILE") 095 .desc("additional constraint definitions") 096 .build()); 097 098 @Override 099 public String getName() { 100 return COMMAND; 101 } 102 103 @SuppressWarnings("null") 104 @Override 105 public Collection<? extends Option> gatherOptions() { 106 return List.of( 107 AS_OPTION, 108 CONSTRAINTS_OPTION); 109 } 110 111 @Override 112 public List<ExtraArgument> getExtraArguments() { 113 return EXTRA_ARGUMENTS; 114 } 115 116 @SuppressWarnings("PMD.PreserveStackTrace") // intended 117 @Override 118 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 119 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) { 120 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION); 121 for (String arg : args) { 122 Path constraint = Paths.get(arg); 123 if (!Files.exists(constraint)) { 124 throw new InvalidArgumentException( 125 "The provided external constraint file '" + constraint + "' does not exist."); 126 } 127 if (!Files.isRegularFile(constraint)) { 128 throw new InvalidArgumentException( 129 "The provided external constraint file '" + constraint + "' is not a file."); 130 } 131 if (!Files.isReadable(constraint)) { 132 throw new InvalidArgumentException( 133 "The provided external constraint file '" + constraint + "' is not readable."); 134 } 135 } 136 } 137 138 List<String> extraArgs = cmdLine.getArgList(); 139 if (extraArgs.size() != 1) { 140 throw new InvalidArgumentException("The source to validate must be provided."); 141 } 142 143 Path source = Paths.get(extraArgs.get(0)); 144 if (!Files.exists(source)) { 145 throw new InvalidArgumentException("The provided source file '" + source + "' does not exist."); 146 } 147 if (!Files.isReadable(source)) { 148 throw new InvalidArgumentException("The provided source file '" + source + "' is not readable."); 149 } 150 151 if (cmdLine.hasOption(AS_OPTION)) { 152 try { 153 String toFormatText = cmdLine.getOptionValue(AS_OPTION); 154 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 155 } catch (IllegalArgumentException ex) { 156 InvalidArgumentException newEx = new InvalidArgumentException( 157 String.format("Invalid '%s' argument. The format must be one of: %s.", 158 OptionUtils.toArgument(AS_OPTION), 159 Arrays.asList(Format.values()).stream() 160 .map(format -> format.name()) 161 .collect(CustomCollectors.joiningWithOxfordComma("and")))); 162 newEx.addSuppressed(ex); 163 throw newEx; 164 } 165 } 166 } 167 168 protected abstract class AbstractValidationCommandExecutor 169 extends AbstractCommandExecutor 170 implements IValidationSchemaProvider { 171 172 public AbstractValidationCommandExecutor( 173 @NonNull CallingContext callingContext, 174 @NonNull CommandLine commandLine) { 175 super(callingContext, commandLine); 176 } 177 178 @NonNull 179 protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets) 180 throws MetaschemaException, IOException; 181 182 @SuppressWarnings("PMD.OnlyOneReturn") // readability 183 @Override 184 public ExitStatus execute() { 185 CommandLine cmdLine = getCommandLine(); 186 187 Set<IConstraintSet> constraintSets; 188 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) { 189 ConstraintLoader constraintLoader = new ConstraintLoader(); 190 constraintSets = new LinkedHashSet<>(); 191 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION); 192 for (String arg : args) { 193 Path constraintPath = Paths.get(arg); 194 assert constraintPath != null; 195 try { 196 constraintSets.add(constraintLoader.load(constraintPath)); 197 } catch (IOException | MetaschemaException ex) { 198 return ExitCode.IO_ERROR.exitMessage("Unable to load constraint set '" + arg + "'.").withThrowable(ex); 199 } 200 } 201 } else { 202 constraintSets = CollectionUtil.emptySet(); 203 } 204 IBindingContext bindingContext; 205 try { 206 bindingContext = getBindingContext(constraintSets); 207 } catch (IOException | MetaschemaException ex) { 208 return ExitCode.PROCESSING_ERROR 209 .exitMessage("Unable to get binding context. " + ex.getMessage()) 210 .withThrowable(ex); 211 } 212 213 IBoundLoader loader = bindingContext.newBoundLoader(); 214 215 List<String> extraArgs = cmdLine.getArgList(); 216 @SuppressWarnings("null") Path source = resolvePathAgainstCWD(Paths.get(extraArgs.get(0))); 217 assert source != null; 218 219 Format asFormat; 220 if (cmdLine.hasOption(AS_OPTION)) { 221 try { 222 String toFormatText = cmdLine.getOptionValue(AS_OPTION); 223 asFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 224 } catch (IllegalArgumentException ex) { 225 return ExitCode.IO_ERROR 226 .exitMessage("Invalid '--as' argument. The format must be one of: " 227 + Arrays.stream(Format.values()) 228 .map(format -> format.name()) 229 .collect(CustomCollectors.joiningWithOxfordComma("or"))) 230 .withThrowable(ex); 231 } 232 } else { 233 // attempt to determine the format 234 FormatDetector.Result formatResult; 235 try { 236 formatResult = loader.detectFormat(source); 237 } catch (FileNotFoundException ex) { 238 // this case was already checked for 239 return ExitCode.IO_ERROR.exitMessage("The provided source file '" + source + "' does not exist."); 240 } catch (IOException ex) { 241 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); 242 } catch (IllegalArgumentException ex) { 243 return ExitCode.IO_ERROR.exitMessage( 244 "Source file has unrecognizable format. Use '--as' to specify the format. The format must be one of: " 245 + Arrays.stream(Format.values()) 246 .map(format -> format.name()) 247 .collect(CustomCollectors.joiningWithOxfordComma("or"))); 248 } 249 asFormat = formatResult.getFormat(); 250 } 251 252 if (LOGGER.isInfoEnabled()) { 253 LOGGER.info("Validating '{}' as {}.", source, asFormat.name()); 254 } 255 256 IValidationResult validationResult; 257 try { 258 validationResult = bindingContext.validate(source, asFormat, this); 259 } catch (IOException | SAXException ex) { 260 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); 261 } 262 263 if (LOGGER.isInfoEnabled()) { 264 LOGGER.info("Validation identified the following in file '{}'.", source); 265 } 266 267 LoggingValidationHandler.instance().handleValidationResults(validationResult); 268 269 if (validationResult.isPassing() && !cmdLine.hasOption(CLIProcessor.QUIET_OPTION) && LOGGER.isInfoEnabled()) { 270 LOGGER.info("The file '{}' is valid.", source); 271 } 272 273 return (validationResult.isPassing() ? ExitCode.OK : ExitCode.FAIL).exit(); 274 } 275 276 } 277}