1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.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.command.AbstractCommandExecutor;
11  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
12  import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
13  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
14  import gov.nist.secauto.metaschema.core.util.AutoCloser;
15  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
16  import gov.nist.secauto.metaschema.databind.IBindingContext;
17  import gov.nist.secauto.metaschema.databind.io.Format;
18  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
19  
20  import org.apache.commons.cli.CommandLine;
21  import org.apache.commons.cli.Option;
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  
25  import java.io.FileNotFoundException;
26  import java.io.IOException;
27  import java.io.OutputStreamWriter;
28  import java.io.Writer;
29  import java.net.URI;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.StandardOpenOption;
34  import java.util.Collection;
35  import java.util.List;
36  
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  
39  /**
40   * Used by implementing classes to provide a content conversion command.
41   */
42  public abstract class AbstractConvertSubcommand
43      extends AbstractTerminalCommand {
44    private static final Logger LOGGER = LogManager.getLogger(AbstractConvertSubcommand.class);
45  
46    @NonNull
47    private static final String COMMAND = "convert";
48    @NonNull
49    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
50        ExtraArgument.newInstance("source-file-or-URL", true),
51        ExtraArgument.newInstance("destination-file", false)));
52  
53    @Override
54    public String getName() {
55      return COMMAND;
56    }
57  
58    @Override
59    public Collection<? extends Option> gatherOptions() {
60      return ObjectUtils.notNull(List.of(
61          MetaschemaCommands.OVERWRITE_OPTION,
62          MetaschemaCommands.TO_OPTION));
63    }
64  
65    @Override
66    public List<ExtraArgument> getExtraArguments() {
67      return EXTRA_ARGUMENTS;
68    }
69  
70    /**
71     * Used by implementing classes to provide for execution of a conversion
72     * command.
73     */
74    protected abstract static class AbstractConversionCommandExecutor
75        extends AbstractCommandExecutor {
76  
77      /**
78       * Construct a new command executor.
79       *
80       * @param callingContext
81       *          the context of the command execution
82       * @param commandLine
83       *          the parsed command line details
84       */
85      protected AbstractConversionCommandExecutor(
86          @NonNull CallingContext callingContext,
87          @NonNull CommandLine commandLine) {
88        super(callingContext, commandLine);
89      }
90  
91      /**
92       * Get the binding context to use for data processing.
93       *
94       * @return the context
95       * @throws CommandExecutionException
96       *           if an error occurred getting the binding context
97       */
98      @NonNull
99      protected abstract IBindingContext getBindingContext() throws CommandExecutionException;
100 
101     @SuppressWarnings({
102         "PMD.OnlyOneReturn", // readability
103         "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" // reasonable
104     })
105     @Override
106     public void execute() throws CommandExecutionException {
107       CommandLine cmdLine = getCommandLine();
108 
109       List<String> extraArgs = cmdLine.getArgList();
110 
111       Path destination = null;
112       if (extraArgs.size() > 1) {
113         destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
114       }
115 
116       URI source = MetaschemaCommands.handleSource(
117           ObjectUtils.requireNonNull(extraArgs.get(0)),
118           ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
119 
120       Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
121 
122       IBindingContext bindingContext = getBindingContext();
123 
124       try {
125         IBoundLoader loader = bindingContext.newBoundLoader();
126         if (LOGGER.isInfoEnabled()) {
127           LOGGER.info("Converting '{}'.", source);
128         }
129 
130         if (destination == null) {
131           // write to STDOUT
132           try (OutputStreamWriter writer
133               = new OutputStreamWriter(AutoCloser.preventClose(System.out), StandardCharsets.UTF_8)) {
134             handleConversion(source, toFormat, writer, loader);
135           }
136         } else {
137           try (Writer writer = Files.newBufferedWriter(
138               destination,
139               StandardCharsets.UTF_8,
140               StandardOpenOption.CREATE,
141               StandardOpenOption.WRITE,
142               StandardOpenOption.TRUNCATE_EXISTING)) {
143             assert writer != null;
144             handleConversion(source, toFormat, writer, loader);
145           }
146         }
147       } catch (IllegalArgumentException ex) {
148         throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
149       } catch (IOException ex) {
150         throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
151       }
152       if (destination != null && LOGGER.isInfoEnabled()) {
153         LOGGER.info("Generated {} file: {}", toFormat.toString(), destination);
154       }
155     }
156 
157     /**
158      * Called to perform a content conversion.
159      *
160      * @param source
161      *          the resource to convert
162      * @param toFormat
163      *          the format to convert to
164      * @param writer
165      *          the writer to use to write converted content
166      * @param loader
167      *          the Metaschema loader to use to load the content to convert
168      * @throws FileNotFoundException
169      *           if the requested resource was not found
170      * @throws IOException
171      *           if there was an error reading or writing content
172      */
173     protected abstract void handleConversion(
174         @NonNull URI source,
175         @NonNull Format toFormat,
176         @NonNull Writer writer,
177         @NonNull IBoundLoader loader) throws FileNotFoundException, IOException;
178   }
179 }