View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.cli.commands;
28  
29  import gov.nist.secauto.metaschema.cli.processor.CLIProcessor;
30  import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
31  import gov.nist.secauto.metaschema.cli.processor.ExitCode;
32  import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
33  import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
34  import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
35  import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
36  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
37  import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
38  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
39  import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
40  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
41  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
42  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
43  import gov.nist.secauto.metaschema.core.model.xml.ConstraintLoader;
44  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
45  import gov.nist.secauto.metaschema.core.util.CustomCollectors;
46  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
47  import gov.nist.secauto.metaschema.databind.IBindingContext;
48  import gov.nist.secauto.metaschema.databind.IBindingContext.IValidationSchemaProvider;
49  import gov.nist.secauto.metaschema.databind.io.Format;
50  import gov.nist.secauto.metaschema.databind.io.FormatDetector;
51  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
52  
53  import org.apache.commons.cli.CommandLine;
54  import org.apache.commons.cli.Option;
55  import org.apache.logging.log4j.LogManager;
56  import org.apache.logging.log4j.Logger;
57  import org.xml.sax.SAXException;
58  
59  import java.io.FileNotFoundException;
60  import java.io.IOException;
61  import java.nio.file.Files;
62  import java.nio.file.Path;
63  import java.nio.file.Paths;
64  import java.util.Arrays;
65  import java.util.Collection;
66  import java.util.LinkedHashSet;
67  import java.util.List;
68  import java.util.Locale;
69  import java.util.Set;
70  
71  import edu.umd.cs.findbugs.annotations.NonNull;
72  
73  public abstract class AbstractValidateContentCommand
74      extends AbstractTerminalCommand {
75    private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
76    @NonNull
77    private static final String COMMAND = "validate";
78    @NonNull
79    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
80        new DefaultExtraArgument("file to validate", true)));
81  
82    @NonNull
83    private static final Option AS_OPTION = ObjectUtils.notNull(
84        Option.builder()
85            .longOpt("as")
86            .hasArg()
87            .argName("FORMAT")
88            .desc("source format: xml, json, or yaml")
89            .build());
90    @NonNull
91    private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
92        Option.builder("c")
93            .hasArg()
94            .argName("FILE")
95            .desc("additional constraint definitions")
96            .build());
97  
98    @Override
99    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 }