001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.cli.commands;
007
008import org.apache.commons.cli.CommandLine;
009import org.apache.commons.cli.Option;
010import org.apache.logging.log4j.LogManager;
011import org.apache.logging.log4j.Logger;
012
013import java.io.File;
014import java.io.FileNotFoundException;
015import java.io.IOException;
016import java.io.OutputStreamWriter;
017import java.io.Writer;
018import java.net.URI;
019import java.nio.charset.StandardCharsets;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.StandardOpenOption;
023import java.util.Collection;
024import java.util.List;
025
026import dev.metaschema.cli.processor.CallingContext;
027import dev.metaschema.cli.processor.ExitCode;
028import dev.metaschema.cli.processor.command.AbstractCommandExecutor;
029import dev.metaschema.cli.processor.command.AbstractTerminalCommand;
030import dev.metaschema.cli.processor.command.CommandExecutionException;
031import dev.metaschema.cli.processor.command.ExtraArgument;
032import dev.metaschema.core.model.MetaschemaException;
033import dev.metaschema.core.util.AutoCloser;
034import dev.metaschema.core.util.ObjectUtils;
035import dev.metaschema.databind.IBindingContext;
036import dev.metaschema.databind.io.Format;
037import dev.metaschema.databind.io.IBoundLoader;
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      ExtraArgument.newInstance("source-file-or-URL", true, URI.class),
052      ExtraArgument.newInstance("destination-file", false, File.class)));
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     * @throws MetaschemaException
099     *           if an error occurred while setting up the binding context, such as
100     *           pre-loading any needed modules
101     */
102    @NonNull
103    protected abstract IBindingContext getBindingContext() throws CommandExecutionException, MetaschemaException;
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      try {
124        IBindingContext bindingContext = getBindingContext();
125        // Use permissive loader since convert is not a validation command
126        IBoundLoader loader = bindingContext.newPermissiveBoundLoader();
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 | MetaschemaException 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}