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