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.processor.CLIProcessor.CallingContext;
009import gov.nist.secauto.metaschema.cli.processor.ExitCode;
010import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
011import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
012import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
013import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
014import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
015import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
016import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
017import gov.nist.secauto.metaschema.core.util.ObjectUtils;
018
019import org.apache.commons.cli.CommandLine;
020import org.apache.commons.cli.Option;
021import org.apache.logging.log4j.LogManager;
022import org.apache.logging.log4j.Logger;
023
024import java.io.File;
025import java.io.IOException;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.nio.file.Paths;
029import java.util.Collection;
030import java.util.List;
031
032import javax.xml.transform.TransformerException;
033
034import edu.umd.cs.findbugs.annotations.NonNull;
035import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
036
037public abstract class AbstractRenderCommand
038    extends AbstractTerminalCommand {
039  private static final Logger LOGGER = LogManager.getLogger(AbstractRenderCommand.class);
040
041  @NonNull
042  private static final String COMMAND = "render";
043  @NonNull
044  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
045      new DefaultExtraArgument("source file", true),
046      new DefaultExtraArgument("destination file", false)));
047
048  @NonNull
049  private static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
050      Option.builder()
051          .longOpt("overwrite")
052          .desc("overwrite the destination if it exists")
053          .build());
054
055  @Override
056  public String getName() {
057    return COMMAND;
058  }
059
060  @SuppressWarnings("null")
061  @Override
062  public Collection<? extends Option> gatherOptions() {
063    return List.of(OVERWRITE_OPTION);
064  }
065
066  @Override
067  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "unmodifiable collection and immutable item")
068  public List<ExtraArgument> getExtraArguments() {
069    return EXTRA_ARGUMENTS;
070  }
071
072  @Override
073  public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
074    List<String> extraArgs = cmdLine.getArgList();
075    if (extraArgs.size() != 2) {
076      throw new InvalidArgumentException("Both a source and destination argument must be provided.");
077    }
078
079    File source = new File(extraArgs.get(0));
080    if (!source.exists()) {
081      throw new InvalidArgumentException("The provided source '" + source.getPath() + "' does not exist.");
082    }
083    if (!source.canRead()) {
084      throw new InvalidArgumentException("The provided source '" + source.getPath() + "' is not readable.");
085    }
086  }
087
088  @Override
089  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
090    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
091  }
092
093  @SuppressWarnings({
094      "PMD.OnlyOneReturn", // readability
095      "unused"
096  })
097  protected ExitStatus executeCommand(
098      @NonNull CallingContext callingContext,
099      @NonNull CommandLine cmdLine) {
100    List<String> extraArgs = cmdLine.getArgList();
101    Path destination = resolvePathAgainstCWD(ObjectUtils.notNull(Paths.get(extraArgs.get(1)))); // .toAbsolutePath();
102
103    if (Files.exists(destination)) {
104      if (!cmdLine.hasOption(OVERWRITE_OPTION)) {
105        return ExitCode.INVALID_ARGUMENTS.exitMessage(
106            String.format("The provided destination '%s' already exists and the '%s' option was not provided.",
107                destination,
108                OptionUtils.toArgument(OVERWRITE_OPTION)));
109      }
110      if (!Files.isWritable(destination)) {
111        return ExitCode.IO_ERROR.exitMessage("The provided destination '" + destination + "' is not writable.");
112      }
113    }
114
115    Path input = resolvePathAgainstCWD(ObjectUtils.notNull(Paths.get(extraArgs.get(0))));
116    try {
117      performRender(input, destination);
118    } catch (IOException | TransformerException ex) {
119      return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
120    }
121
122    if (LOGGER.isInfoEnabled()) {
123      LOGGER.info("Generated HTML file: " + destination.toString());
124    }
125    return ExitCode.OK.exit();
126  }
127
128  protected abstract void performRender(Path input, Path result) throws IOException, TransformerException;
129}