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}