Processes.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.adapter.os;

import org.apache.calcite.linq4j.AbstractEnumerable;
import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Enumerator;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

/**
 * Utilities regarding operating system processes.
 *
 * <p>WARNING: Spawning processes is not secure.
 * Use this class with caution.
 * This class is in the "plus" module because "plus" is not used by default.
 * Do not move this class to the "core" module.
 */
public class Processes {
  private Processes() {}

  /** Executes a command and returns its result as an enumerable of lines. */
  static Enumerable<String> processLines(String... args) {
    return processLines(' ', args);
  }

  /** Executes a command and returns its result as an enumerable of lines. */
  static Enumerable<String> processLines(char sep, String... args) {
    return processLines(sep, processSupplier(args));
  }

  /** Executes a command and returns its result as an enumerable of lines.
   *
   * @param sep Separator character
   * @param processSupplier Command and its arguments
   */
  private static Enumerable<String> processLines(char sep,
      Supplier<Process> processSupplier) {
    if (sep != ' ') {
      return new SeparatedLinesEnumerable(processSupplier, sep);
    } else {
      return new ProcessLinesEnumerator(processSupplier);
    }
  }

  private static Supplier<Process> processSupplier(final String... args) {
    return new ProcessFactory(args);
  }

  /** Enumerator that executes a process and returns each line as an element. */
  private static class ProcessLinesEnumerator
      extends AbstractEnumerable<String> {
    private final Supplier<Process> processSupplier;

    ProcessLinesEnumerator(Supplier<Process> processSupplier) {
      this.processSupplier = requireNonNull(processSupplier, "processSupplier");
    }

    @Override public Enumerator<String> enumerator() {
      final Process process = processSupplier.get();
      final InputStream is = process.getInputStream();
      final BufferedInputStream bis =
          new BufferedInputStream(is);
      final InputStreamReader isr =
          new InputStreamReader(bis, StandardCharsets.UTF_8);
      final BufferedReader br = new BufferedReader(isr);
      return new Enumerator<String>() {
        private String line;

        @Override public String current() {
          return line;
        }

        @Override public boolean moveNext() {
          try {
            line = br.readLine();
            return line != null;
          } catch (IOException e) {
            throw new RuntimeException(e);
          }
        }

        @Override public void reset() {
          throw new UnsupportedOperationException();
        }

        @Override public void close() {
          try {
            br.close();
          } catch (IOException e) {
            throw new RuntimeException("while running " + processSupplier, e);
          }
          process.destroy();
        }
      };
    }
  }

  /** Enumerator that executes a process and returns each line as an element. */
  private static class SeparatedLinesEnumerable
      extends AbstractEnumerable<String> {
    private final Supplier<Process> processSupplier;
    private final int sep;

    SeparatedLinesEnumerable(Supplier<Process> processSupplier, char sep) {
      this.processSupplier = processSupplier;
      this.sep = sep;
    }

    @Override public Enumerator<String> enumerator() {
      final Process process = processSupplier.get();
      final InputStream is = process.getInputStream();
      final BufferedInputStream bis =
          new BufferedInputStream(is);
      final InputStreamReader isr =
          new InputStreamReader(bis, StandardCharsets.UTF_8);
      final BufferedReader br = new BufferedReader(isr);
      return new Enumerator<String>() {
        private final StringBuilder b = new StringBuilder();
        private String line;

        @Override public String current() {
          return line;
        }

        @Override public boolean moveNext() {
          try {
            for (;;) {
              int c = br.read();
              if (c < 0) {
                return false;
              }
              if (c == sep) {
                line = b.toString();
                b.setLength(0);
                return true;
              }
              b.append((char) c);
            }
          } catch (IOException e) {
            throw new RuntimeException(e);
          }
        }

        @Override public void reset() {
          throw new UnsupportedOperationException();
        }

        @Override public void close() {
          try {
            br.close();
          } catch (IOException e) {
            throw new RuntimeException("while running " + processSupplier, e);
          }
          process.destroy();
        }
      };
    }
  }

  /** Creates processes. */
  private static class ProcessFactory implements Supplier<Process> {
    private final String[] args;

    ProcessFactory(String... args) {
      this.args = args;
    }

    @Override public Process get() {
      try {
        return new ProcessBuilder().command(args).start();
      } catch (IOException e) {
        throw new RuntimeException("while creating process: "
            + Arrays.toString(args), e);
      }
    }

    @Override public String toString() {
      return args[0];
    }
  }
}