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      @SuppressWarnings("synthetic-access")
117      URI source = MetaschemaCommands.handleSource(
118          ObjectUtils.requireNonNull(extraArgs.get(0)),
119          ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
120
121      Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
122
123      IBindingContext bindingContext = getBindingContext();
124
125      try {
126        IBoundLoader loader = bindingContext.newBoundLoader();
127        if (LOGGER.isInfoEnabled()) {
128          LOGGER.info("Converting '{}'.", source);
129        }
130
131        if (destination == null) {
132          // write to STDOUT
133          try (OutputStreamWriter writer
134              = new OutputStreamWriter(AutoCloser.preventClose(System.out), StandardCharsets.UTF_8)) {
135            handleConversion(source, toFormat, writer, loader);
136          }
137        } else {
138          try (Writer writer = Files.newBufferedWriter(
139              destination,
140              StandardCharsets.UTF_8,
141              StandardOpenOption.CREATE,
142              StandardOpenOption.WRITE,
143              StandardOpenOption.TRUNCATE_EXISTING)) {
144            assert writer != null;
145            handleConversion(source, toFormat, writer, loader);
146          }
147        }
148      } catch (IllegalArgumentException ex) {
149        throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex);
150      } catch (IOException ex) {
151        throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
152      }
153      if (destination != null && LOGGER.isInfoEnabled()) {
154        LOGGER.info("Generated {} file: {}", toFormat.toString(), destination);
155      }
156    }
157
158    /**
159     * Called to perform a content conversion.
160     *
161     * @param source
162     *          the resource to convert
163     * @param toFormat
164     *          the format to convert to
165     * @param writer
166     *          the writer to use to write converted content
167     * @param loader
168     *          the Metaschema loader to use to load the content to convert
169     * @throws FileNotFoundException
170     *           if the requested resource was not found
171     * @throws IOException
172     *           if there was an error reading or writing content
173     */
174    protected abstract void handleConversion(
175        @NonNull URI source,
176        @NonNull Format toFormat,
177        @NonNull Writer writer,
178        @NonNull IBoundLoader loader) throws FileNotFoundException, IOException;
179  }
180}