001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.oscal.tools.cli.core.commands;
007
008import gov.nist.secauto.metaschema.cli.commands.MetaschemaCommands;
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.AbstractTerminalCommand;
012import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
013import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
014import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
015import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
016import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
017import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
018import gov.nist.secauto.metaschema.core.util.ObjectUtils;
019import gov.nist.secauto.metaschema.databind.IBindingContext;
020import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
021import gov.nist.secauto.metaschema.databind.io.Format;
022import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
023import gov.nist.secauto.metaschema.databind.io.ISerializer;
024import gov.nist.secauto.oscal.lib.OscalBindingContext;
025import gov.nist.secauto.oscal.lib.model.Catalog;
026import gov.nist.secauto.oscal.lib.model.Profile;
027import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException;
028import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
029
030import org.apache.commons.cli.CommandLine;
031import org.apache.commons.cli.Option;
032
033import java.io.IOException;
034import java.io.PrintStream;
035import java.net.URI;
036import java.nio.file.Path;
037import java.util.Collection;
038import java.util.List;
039
040import edu.umd.cs.findbugs.annotations.NonNull;
041
042/**
043 * A command implementation supporting the resolution of an OSCAL profile.
044 */
045public abstract class AbstractResolveCommand
046    extends AbstractTerminalCommand {
047  @NonNull
048  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
049      ExtraArgument.newInstance("URI to resolve", true),
050      ExtraArgument.newInstance("destination file", false)));
051  @NonNull
052  private static final List<Option> OPTIONS = ObjectUtils.notNull(
053      List.of(
054          MetaschemaCommands.AS_FORMAT_OPTION,
055          MetaschemaCommands.TO_OPTION,
056          MetaschemaCommands.OVERWRITE_OPTION));
057
058  @Override
059  public String getDescription() {
060    return "Resolve the specified OSCAL Profile";
061  }
062
063  @Override
064  public Collection<? extends Option> gatherOptions() {
065    return OPTIONS;
066  }
067
068  @Override
069  public List<ExtraArgument> getExtraArguments() {
070    return EXTRA_ARGUMENTS;
071  }
072
073  @SuppressWarnings({
074      "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable
075      "PMD.PreserveStackTrace" // intended
076  })
077
078  @Override
079  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
080    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
081  }
082
083  /**
084   * Process the command line arguments and execute the profile resolution
085   * operation.
086   *
087   * @param callingContext
088   *          the context information for the execution
089   * @param cmdLine
090   *          the parsed command line details
091   * @throws CommandExecutionException
092   *           if an error occurred while determining the source format
093   */
094  @SuppressWarnings({
095      "PMD.OnlyOneReturn", // readability
096      "PMD.CyclomaticComplexity"
097  })
098  protected void executeCommand(
099      @NonNull CallingContext callingContext,
100      @NonNull CommandLine cmdLine) throws CommandExecutionException {
101    List<String> extraArgs = cmdLine.getArgList();
102
103    URI source = MetaschemaCommands.handleSource(
104        ObjectUtils.requireNonNull(extraArgs.get(0)),
105        ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
106
107    IBindingContext bindingContext = OscalBindingContext.instance();
108    IBoundLoader loader = bindingContext.newBoundLoader();
109    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
110
111    // attempt to determine the format
112    Format asFormat = MetaschemaCommands.determineSourceFormat(
113        cmdLine,
114        MetaschemaCommands.AS_FORMAT_OPTION,
115        loader,
116        source);
117
118    IDocumentNodeItem document;
119    try {
120      document = loader.loadAsNodeItem(asFormat, source);
121    } catch (IOException ex) {
122      throw new CommandExecutionException(
123          ExitCode.IO_ERROR,
124          String.format("Unable to load content '%s'. %s",
125              source,
126              ex.getMessage()),
127          ex);
128    }
129
130    Object object = document.getValue();
131    if (object == null) {
132      throw new CommandExecutionException(
133          ExitCode.INVALID_ARGUMENTS,
134          String.format("The source document '%s' contained no data.", source));
135    }
136
137    if (object instanceof Catalog) {
138      // this is a catalog
139      throw new CommandExecutionException(
140          ExitCode.OK,
141          String.format("The source '%s' is already a catalog.", source));
142    }
143
144    if (!(object instanceof Profile)) {
145      // this is something else
146      throw new CommandExecutionException(
147          ExitCode.INVALID_ARGUMENTS,
148          String.format("The source '%s' is not a profile.", source));
149    }
150
151    Path destination = null;
152    if (extraArgs.size() > 1) {
153      destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
154    }
155
156    // this is a profile
157    DynamicContext dynamicContext = new DynamicContext(document.getStaticContext());
158    dynamicContext.setDocumentLoader(loader);
159    ProfileResolver resolver = new ProfileResolver(dynamicContext);
160
161    IDocumentNodeItem resolvedProfile;
162    try {
163      resolvedProfile = resolver.resolve(document);
164    } catch (IOException | ProfileResolutionException ex) {
165      throw new CommandExecutionException(
166          ExitCode.PROCESSING_ERROR,
167          String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()),
168          ex);
169    }
170
171    // DefaultConstraintValidator validator = new
172    // DefaultConstraintValidator(dynamicContext);
173    // ((IBoundXdmNodeItem)resolvedProfile).validate(validator);
174    // validator.finalizeValidation();
175
176    Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
177    ISerializer<Catalog> serializer = bindingContext.newSerializer(toFormat, Catalog.class);
178    try {
179      if (destination == null) {
180        @SuppressWarnings({ "resource", "PMD.CloseResource" })
181        PrintStream stdOut = ObjectUtils.notNull(System.out);
182        serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut);
183      } else {
184        serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination);
185      }
186    } catch (IOException ex) {
187      throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
188    }
189  }
190}