StackWriter.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.util;

import java.io.FilterWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * A helper class for generating formatted text. StackWriter keeps track of
 * nested formatting state like indentation level and quote escaping. Typically,
 * it is inserted between a PrintWriter and the real Writer; directives are
 * passed straight through the PrintWriter via the write method, as in the
 * following example:
 *
 * <blockquote><pre><code>
 *    StringWriter sw = new StringWriter();
 *    StackWriter stackw = new StackWriter(sw, StackWriter.INDENT_SPACE4);
 *    PrintWriter pw = new PrintWriter(stackw);
 *    pw.write(StackWriter.INDENT);
 *    pw.print("execute remote(link_name,");
 *    pw.write(StackWriter.OPEN_SQL_STRING_LITERAL);
 *    pw.println();
 *    pw.write(StackWriter.INDENT);
 *    pw.println("select * from t where c &gt; 'alabama'");
 *    pw.write(StackWriter.OUTDENT);
 *    pw.write(StackWriter.CLOSE_SQL_STRING_LITERAL);
 *    pw.println(");");
 *    pw.write(StackWriter.OUTDENT);
 *    pw.close();
 *    System.out.println(sw.toString());
 * </code></pre></blockquote>
 *
 * <p>which produces the following output:
 *
 * <blockquote><pre><code>
 *      execute remote(link_name,'
 *          select * from t where c &gt; ''alabama''
 *      ');
 * </code></pre></blockquote>
 */
public class StackWriter extends FilterWriter {
  //~ Static fields/initializers ---------------------------------------------

  /**
   * Directive for increasing the indentation level.
   */
  public static final int INDENT = 0xF0000001;

  /**
   * Directive for decreasing the indentation level.
   */
  public static final int OUTDENT = 0xF0000002;

  /**
   * Directive for beginning an SQL string literal.
   */
  public static final int OPEN_SQL_STRING_LITERAL = 0xF0000003;

  /**
   * Directive for ending an SQL string literal.
   */
  public static final int CLOSE_SQL_STRING_LITERAL = 0xF0000004;

  /**
   * Directive for beginning an SQL identifier.
   */
  public static final int OPEN_SQL_IDENTIFIER = 0xF0000005;

  /**
   * Directive for ending an SQL identifier.
   */
  public static final int CLOSE_SQL_IDENTIFIER = 0xF0000006;

  /**
   * Tab indentation.
   */
  public static final String INDENT_TAB = "\t";

  /**
   * Four-space indentation.
   */
  public static final String INDENT_SPACE4 = "    ";
  private static final Character SINGLE_QUOTE = '\'';
  private static final Character DOUBLE_QUOTE = '"';

  //~ Instance fields --------------------------------------------------------

  private int indentationDepth;
  private final String indentation;
  private boolean needIndent;
  private final Deque<Character> quoteStack = new ArrayDeque<>();

  //~ Constructors -----------------------------------------------------------

  /**
   * Creates a new StackWriter on top of an existing Writer, with the
   * specified string to be used for each level of indentation.
   *
   * @param writer      underlying writer
   * @param indentation indentation unit such as {@link #INDENT_TAB} or
   *                    {@link #INDENT_SPACE4}
   */
  public StackWriter(Writer writer, String indentation) {
    super(writer);
    this.indentation = indentation;
  }

  //~ Methods ----------------------------------------------------------------

  private void indentIfNeeded() throws IOException {
    if (needIndent) {
      for (int i = 0; i < indentationDepth; i++) {
        out.write(indentation);
      }
      needIndent = false;
    }
  }

  private void writeQuote(Character quoteChar) throws IOException {
    indentIfNeeded();
    int n = 1;
    for (Character quote : quoteStack) {
      if (quote.equals(quoteChar)) {
        n *= 2;
      }
    }
    for (int i = 0; i < n; i++) {
      out.write(quoteChar);
    }
  }

  private void pushQuote(Character quoteChar) throws IOException {
    writeQuote(quoteChar);
    quoteStack.push(quoteChar);
  }

  private void popQuote(Character quoteChar) throws IOException {
    final Character pop = quoteStack.pop();
    assert pop.equals(quoteChar);
    writeQuote(quoteChar);
  }

  // implement Writer
  @Override public void write(int c) throws IOException {
    switch (c) {
    case INDENT:
      indentationDepth++;
      break;
    case OUTDENT:
      indentationDepth--;
      break;
    case OPEN_SQL_STRING_LITERAL:
      pushQuote(SINGLE_QUOTE);
      break;
    case CLOSE_SQL_STRING_LITERAL:
      popQuote(SINGLE_QUOTE);
      break;
    case OPEN_SQL_IDENTIFIER:
      pushQuote(DOUBLE_QUOTE);
      break;
    case CLOSE_SQL_IDENTIFIER:
      popQuote(DOUBLE_QUOTE);
      break;
    case '\n':
      out.write(c);
      needIndent = true;
      break;
    case '\r':

      // NOTE jvs 3-Jan-2006:  suppress indentIfNeeded() in this case
      // so that we don't get spurious diffs on Windows vs. Linux
      out.write(c);
      break;
    case '\'':
      writeQuote(SINGLE_QUOTE);
      break;
    case '"':
      writeQuote(DOUBLE_QUOTE);
      break;
    default:
      indentIfNeeded();
      out.write(c);
      break;
    }
  }

  // implement Writer
  @Override public void write(char[] cbuf, int off, int len) throws IOException {
    // TODO: something more efficient using searches for
    // special characters
    for (int i = off; i < (off + len); i++) {
      write(cbuf[i]);
    }
  }

  // implement Writer
  @Override public void write(String str, int off, int len) throws IOException {
    // TODO: something more efficient using searches for
    // special characters
    for (int i = off; i < (off + len); i++) {
      write(str.charAt(i));
    }
  }

  /**
   * Writes an SQL string literal.
   *
   * @param pw PrintWriter on which to write
   * @param s  text of literal
   */
  public static void printSqlStringLiteral(PrintWriter pw, String s) {
    pw.write(OPEN_SQL_STRING_LITERAL);
    pw.print(s);
    pw.write(CLOSE_SQL_STRING_LITERAL);
  }

  /**
   * Writes an SQL identifier.
   *
   * @param pw PrintWriter on which to write
   * @param s  identifier
   */
  public static void printSqlIdentifier(PrintWriter pw, String s) {
    pw.write(OPEN_SQL_IDENTIFIER);
    pw.print(s);
    pw.write(CLOSE_SQL_IDENTIFIER);
  }
}