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.CallingContext;
009import gov.nist.secauto.metaschema.cli.processor.ExitCode;
010import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
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.core.util.AutoCloser;
015import gov.nist.secauto.metaschema.core.util.ObjectUtils;
016import gov.nist.secauto.metaschema.databind.IBindingContext;
017import gov.nist.secauto.metaschema.databind.io.Format;
018import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
019
020import org.apache.commons.cli.CommandLine;
021import org.apache.commons.cli.Option;
022import org.apache.logging.log4j.LogManager;
023import org.apache.logging.log4j.Logger;
024
025import java.io.FileNotFoundException;
026import java.io.IOException;
027import java.io.OutputStreamWriter;
028import java.io.Writer;
029import java.net.URI;
030import java.nio.charset.StandardCharsets;
031import java.nio.file.Files;
032import java.nio.file.Path;
033import java.nio.file.StandardOpenOption;
034import java.util.Collection;
035import java.util.List;
036
037import edu.umd.cs.findbugs.annotations.NonNull;
038
039/**
040 * Used by implementing classes to provide a content conversion command.
041 */
042public abstract class AbstractConvertSubcommand
043    extends AbstractTerminalCommand {
044  private static final Logger LOGGER = LogManager.getLogger(AbstractConvertSubcommand.class);
045
046  @NonNull
047  private static final String COMMAND = "convert";
048  @NonNull
049  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
050      ExtraArgument.newInstance("source-file-or-URL", true),
051      ExtraArgument.newInstance("destination-file", false)));
052
053  @Override
054  public String getName() {
055    return COMMAND;
056  }
057
058  @Override
059  public Collection<? extends Option> gatherOptions() {
060    return ObjectUtils.notNull(List.of(
061        MetaschemaCommands.OVERWRITE_OPTION,
062        MetaschemaCommands.TO_OPTION));
063  }
064
065  @Override
066  public List<ExtraArgument> getExtraArguments() {
067    return EXTRA_ARGUMENTS;
068  }
069
070  /**
071   * Used by implementing classes to provide for execution of a conversion
072   * command.
073   */
074  protected abstract static class AbstractConversionCommandExecutor
075      extends AbstractCommandExecutor {
076
077    /**
078     * Construct a new command executor.
079     *
080     * @param callingContext
081     *          the context of the command execution
082     * @param commandLine
083     *          the parsed command line details
084     */
085    protected AbstractConversionCommandExecutor(
086        @NonNull CallingContext callingContext,
087        @NonNull CommandLine commandLine) {
088      super(callingContext, commandLine);
089    }
090
091    /**
092     * Get the binding context to use for data processing.
093     *
094     * @return the context
095     * @throws CommandExecutionException
096     *           if an error occurred getting the binding context
097     */
098    @NonNull
099    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}