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;
009import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
010import gov.nist.secauto.metaschema.cli.processor.ExitCode;
011import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
012import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
013import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
014import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
015import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
016import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
017import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
018import gov.nist.secauto.metaschema.core.metapath.MetapathException;
019import gov.nist.secauto.metaschema.core.model.IModule;
020import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
021import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
022import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
023import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
024import gov.nist.secauto.metaschema.core.util.IVersionInfo;
025import gov.nist.secauto.metaschema.core.util.ObjectUtils;
026import gov.nist.secauto.metaschema.databind.IBindingContext;
027import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider;
028import gov.nist.secauto.metaschema.databind.io.Format;
029import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
030import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler;
031
032import org.apache.commons.cli.CommandLine;
033import org.apache.commons.cli.Option;
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036
037import java.io.FileNotFoundException;
038import java.io.IOException;
039import java.net.URI;
040import java.net.UnknownHostException;
041import java.nio.file.Path;
042import java.nio.file.Paths;
043import java.util.Collection;
044import java.util.List;
045import java.util.Set;
046
047import edu.umd.cs.findbugs.annotations.NonNull;
048import edu.umd.cs.findbugs.annotations.Nullable;
049
050/**
051 * Used by implementing classes to provide a content validation command.
052 */
053public abstract class AbstractValidateContentCommand
054    extends AbstractTerminalCommand {
055  private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
056  @NonNull
057  private static final String COMMAND = "validate";
058  @NonNull
059  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
060      ExtraArgument.newInstance("file-or-URI-to-validate", true)));
061
062  @NonNull
063  private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
064      Option.builder("c")
065          .hasArgs()
066          .argName("URL")
067          .desc("additional constraint definitions")
068          .build());
069  @NonNull
070  private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull(
071      Option.builder("o")
072          .hasArg()
073          .argName("FILE")
074          .desc("write SARIF results to the provided FILE")
075          .numberOfArgs(1)
076          .build());
077  @NonNull
078  private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull(
079      Option.builder()
080          .longOpt("sarif-include-pass")
081          .desc("include pass results in SARIF")
082          .build());
083  @NonNull
084  private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull(
085      Option.builder()
086          .longOpt("disable-schema-validation")
087          .desc("do not perform schema validation")
088          .build());
089  @NonNull
090  private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull(
091      Option.builder()
092          .longOpt("disable-constraint-validation")
093          .desc("do not perform constraint validation")
094          .build());
095
096  @Override
097  public String getName() {
098    return COMMAND;
099  }
100
101  @SuppressWarnings("null")
102  @Override
103  public Collection<? extends Option> gatherOptions() {
104    return List.of(
105        MetaschemaCommands.AS_FORMAT_OPTION,
106        CONSTRAINTS_OPTION,
107        SARIF_OUTPUT_FILE_OPTION,
108        SARIF_INCLUDE_PASS_OPTION,
109        NO_SCHEMA_VALIDATION_OPTION,
110        NO_CONSTRAINT_VALIDATION_OPTION);
111  }
112
113  @Override
114  public List<ExtraArgument> getExtraArguments() {
115    return EXTRA_ARGUMENTS;
116  }
117
118  /**
119   * Drives the validation execution.
120   */
121  protected abstract class AbstractValidationCommandExecutor
122      extends AbstractCommandExecutor {
123
124    /**
125     * Construct a new command executor.
126     *
127     * @param callingContext
128     *          the context of the command execution
129     * @param commandLine
130     *          the parsed command line details
131     */
132    public AbstractValidationCommandExecutor(
133        @NonNull CallingContext callingContext,
134        @NonNull CommandLine commandLine) {
135      super(callingContext, commandLine);
136    }
137
138    /**
139     * Get the binding context to use for data processing.
140     *
141     * @param constraintSets
142     *          the constraints to configure in the resulting binding context
143     * @return the context
144     * @throws CommandExecutionException
145     *           if a error occurred while getting the binding context
146     */
147    @NonNull
148    protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets)
149        throws CommandExecutionException;
150
151    /**
152     * Get the module to use for validation.
153     * <p>
154     * This module is used to generate schemas and as a source of built-in
155     * constraints.
156     *
157     * @param commandLine
158     *          the provided command line argument information
159     * @param bindingContext
160     *          the context used to access Metaschema module information based on
161     *          Java class bindings
162     * @return the loaded Metaschema module
163     * @throws CommandExecutionException
164     *           if an error occurred while loading the module
165     */
166    @NonNull
167    protected abstract IModule getModule(
168        @NonNull CommandLine commandLine,
169        @NonNull IBindingContext bindingContext)
170        throws CommandExecutionException;
171
172    /**
173     * Get the schema validation implementation requested based on the provided
174     * command line arguments.
175     * <p>
176     * It is typical for this call to result in the dynamic generation of a schema
177     * to use for validation.
178     *
179     * @param module
180     *          the Metaschema module to generate the schema from
181     * @param commandLine
182     *          the provided command line argument information
183     * @param bindingContext
184     *          the context used to access Metaschema module information based on
185     *          Java class bindings
186     * @return the provider
187     */
188    @NonNull
189    protected abstract ISchemaValidationProvider getSchemaValidationProvider(
190        @NonNull IModule module,
191        @NonNull CommandLine commandLine,
192        @NonNull IBindingContext bindingContext);
193
194    /**
195     * Execute the validation operation.
196     */
197    @SuppressWarnings("PMD.OnlyOneReturn") // readability
198    @Override
199    public void execute() throws CommandExecutionException {
200      CommandLine cmdLine = getCommandLine();
201      URI currentWorkingDirectory = ObjectUtils.notNull(getCurrentWorkingDirectory().toUri());
202
203      Set<IConstraintSet> constraintSets = MetaschemaCommands.loadConstraintSets(
204          cmdLine,
205          CONSTRAINTS_OPTION,
206          currentWorkingDirectory);
207
208      List<String> extraArgs = cmdLine.getArgList();
209
210      URI source = MetaschemaCommands.handleSource(
211          ObjectUtils.requireNonNull(extraArgs.get(0)),
212          currentWorkingDirectory);
213
214      IBindingContext bindingContext = getBindingContext(constraintSets);
215      IBoundLoader loader = bindingContext.newBoundLoader();
216      Format asFormat = MetaschemaCommands.determineSourceFormat(
217          cmdLine,
218          MetaschemaCommands.AS_FORMAT_OPTION,
219          loader,
220          source);
221
222      IValidationResult validationResult = validate(source, asFormat, cmdLine, bindingContext);
223      handleOutput(source, validationResult, cmdLine, bindingContext);
224
225      if (validationResult == null || validationResult.isPassing()) {
226        if (LOGGER.isInfoEnabled()) {
227          LOGGER.info("The file '{}' is valid.", source);
228        }
229      } else if (LOGGER.isErrorEnabled()) {
230        LOGGER.error("The file '{}' is invalid.", source);
231      }
232
233      if (validationResult != null && !validationResult.isPassing()) {
234        throw new CommandExecutionException(ExitCode.FAIL);
235      }
236    }
237
238    @SuppressWarnings("PMD.CyclomaticComplexity")
239    @Nullable
240    private IValidationResult validate(
241        @NonNull URI source,
242        @NonNull Format asFormat,
243        @NonNull CommandLine commandLine,
244        @NonNull IBindingContext bindingContext) throws CommandExecutionException {
245
246      if (LOGGER.isInfoEnabled()) {
247        LOGGER.info("Validating '{}' as {}.", source, asFormat.name());
248      }
249
250      IValidationResult validationResult = null;
251      try {
252        // get the module, but don't register it
253        IModule module = getModule(commandLine, bindingContext);
254        if (!commandLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) {
255          // perform schema validation
256          validationResult = getSchemaValidationProvider(module, commandLine, bindingContext)
257              .validateWithSchema(source, asFormat, bindingContext);
258        }
259
260        if (!commandLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION)) {
261          IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>();
262          if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && commandLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) {
263            configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS);
264          }
265
266          // perform constraint validation
267          bindingContext.registerModule(module); // ensure the module is registered
268          IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration);
269          validationResult = validationResult == null
270              ? constraintValidationResult
271              : AggregateValidationResult.aggregate(validationResult, constraintValidationResult);
272        }
273      } catch (FileNotFoundException ex) {
274        throw new CommandExecutionException(
275            ExitCode.IO_ERROR,
276            String.format("Resource not found at '%s'", source),
277            ex);
278      } catch (UnknownHostException ex) {
279        throw new CommandExecutionException(
280            ExitCode.IO_ERROR,
281            String.format("Unknown host for '%s'.", source),
282            ex);
283      } catch (IOException ex) {
284        throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
285      } catch (MetapathException ex) {
286        throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex.getLocalizedMessage(), ex);
287      }
288      return validationResult;
289    }
290
291    private void handleOutput(
292        @NonNull URI source,
293        @Nullable IValidationResult validationResult,
294        @NonNull CommandLine commandLine,
295        @NonNull IBindingContext bindingContext) throws CommandExecutionException {
296      if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) {
297        Path sarifFile = ObjectUtils.notNull(Paths.get(commandLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION)));
298
299        IVersionInfo version
300            = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION);
301
302        try {
303          SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version);
304          if (validationResult != null) {
305            sarifHandler.addFindings(validationResult.getFindings());
306          }
307          sarifHandler.write(sarifFile, bindingContext);
308        } catch (IOException ex) {
309          throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
310        }
311      } else if (validationResult != null && !validationResult.getFindings().isEmpty()) {
312        LOGGER.info("Validation identified the following issues:");
313        LoggingValidationHandler.instance().handleResults(validationResult);
314      }
315
316    }
317  }
318}