1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.oscal.tools.cli.core.commands;
7   
8   import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9   import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10  import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
11  import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
12  import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
13  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
14  import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
15  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
16  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
17  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
18  
19  import org.apache.commons.cli.CommandLine;
20  import org.apache.commons.cli.Option;
21  import org.apache.logging.log4j.LogManager;
22  import org.apache.logging.log4j.Logger;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.Collection;
30  import java.util.List;
31  
32  import javax.xml.transform.TransformerException;
33  
34  import edu.umd.cs.findbugs.annotations.NonNull;
35  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
36  
37  public abstract class AbstractRenderCommand
38      extends AbstractTerminalCommand {
39    private static final Logger LOGGER = LogManager.getLogger(AbstractRenderCommand.class);
40  
41    @NonNull
42    private static final String COMMAND = "render";
43    @NonNull
44    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
45        new DefaultExtraArgument("source file", true),
46        new DefaultExtraArgument("destination file", false)));
47  
48    @NonNull
49    private static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
50        Option.builder()
51            .longOpt("overwrite")
52            .desc("overwrite the destination if it exists")
53            .build());
54  
55    @Override
56    public String getName() {
57      return COMMAND;
58    }
59  
60    @SuppressWarnings("null")
61    @Override
62    public Collection<? extends Option> gatherOptions() {
63      return List.of(OVERWRITE_OPTION);
64    }
65  
66    @Override
67    @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "unmodifiable collection and immutable item")
68    public List<ExtraArgument> getExtraArguments() {
69      return EXTRA_ARGUMENTS;
70    }
71  
72    @Override
73    public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
74      List<String> extraArgs = cmdLine.getArgList();
75      if (extraArgs.size() != 2) {
76        throw new InvalidArgumentException("Both a source and destination argument must be provided.");
77      }
78  
79      File source = new File(extraArgs.get(0));
80      if (!source.exists()) {
81        throw new InvalidArgumentException("The provided source '" + source.getPath() + "' does not exist.");
82      }
83      if (!source.canRead()) {
84        throw new InvalidArgumentException("The provided source '" + source.getPath() + "' is not readable.");
85      }
86    }
87  
88    @Override
89    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
90      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
91    }
92  
93    @SuppressWarnings({
94        "PMD.OnlyOneReturn", // readability
95        "unused"
96    })
97    protected ExitStatus executeCommand(
98        @NonNull CallingContext callingContext,
99        @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 }