Slf4jMessageFormatter.java

/*
 * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Additionally licensed with:
 *
 * Licensed 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.spf4j.base;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import gnu.trove.set.hash.THashSet;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Set;
import javax.annotation.Nonnull;
import org.spf4j.io.ObjectAppenderSupplier;

/**
 * A more flexible implementation of the SLF4j message formatter (org.slf4j.helpers.MessageFormatter). the following
 * improvements:
 *
 * 1) Allow to format to a procvided destination (Appendable) allowing you to reduce the amount of garbage generated in
 * a custom formatter... 2) Lets you know which arguments have been used in the message allowing you to implement extra
 * logic to handle the unused ones 3) Lets you plug custom formatters for argument types. (you can get better
 * performance and more flexibility) 4) Processing arguments that are arrays is sligtly faster than the slf4j formatter.
 *
 * @author zoly
 */
public final class Slf4jMessageFormatter {

  private static final char DELIM_START = '{';
  private static final String DELIM_STR = "{}";
  private static final char ESCAPE_CHAR = '\\';


  public interface ErrorHandler {
    void accept(Object obj, Appendable sbuf, Throwable t) throws IOException;
  }



  private Slf4jMessageFormatter() {
  }


  @SuppressWarnings("checkstyle:regexp")
  public static void exHandle(final Object obj, final Appendable sbuf, final Throwable t) throws IOException {
    String className = obj.getClass().getName();
    synchronized (System.err) {
      System.err.print("SPF4J: Failed toString() invocation on an object of type [");
      System.err.print(className);
      System.err.println(']');
    }
    Throwables.writeTo(t, System.err, Throwables.PackageDetail.SHORT);
    sbuf.append("[FAILED toString() for ");
    sbuf.append(className);
    sbuf.append(']');
  }

  public static String toString(@Nonnull final String messagePattern,
          final Object... argArray) {
    StringBuilder sb = new StringBuilder(messagePattern.length() + argArray.length * 8);
    try {
      int nrUsed = format(sb, messagePattern, argArray);
      if (nrUsed != argArray.length) {
        throw new IllegalArgumentException("Invalid format "
                + messagePattern + ", params " + Arrays.toString(argArray));
      }
    } catch (IOException ex) {
     throw new UncheckedIOException(ex);
    }
    return sb.toString();
  }


  /**
   * Slf4j message formatter.
   *
   * @param to Appendable to put formatted message to.
   * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
   * @param argArray the message arguments.
   * @return the number of arguments used in the message.
   * @throws IOException
   */
  public static int format(@Nonnull final Appendable to, @Nonnull final String messagePattern,
          final Object... argArray) throws IOException {
    return format(to, messagePattern, ObjectAppenderSupplier.TO_STRINGER, argArray);
  }

  /**
   * Slf4j message formatter.
   *
   * @param to Appendable to put formatted message to.
   * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
   * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
   * @param argArray the message arguments.
   * @return the number of arguments used in the message.
   * @throws IOException
   */
  public static int format(@Nonnull final Appendable to,
          @Nonnull final ObjectAppenderSupplier appSupplier, @Nonnull final String messagePattern,
          final Object... argArray) throws IOException {
    return format(to, messagePattern, appSupplier, argArray);
  }

  /**
   * slf4j message formatter.
   *
   * @param to Appendable to put formatted message to.
   * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
   * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
   * @param argArray the message arguments.
   * @return the number of arguments used in the message.
   * @throws IOException something wend wrong while writing to the appendable.
   */
  public static int format(@Nonnull final Appendable to, @Nonnull final String messagePattern,
          @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray) throws IOException {
    return format(0, to, messagePattern, appSupplier, argArray);
  }

  /**
   * Slf4j message formatter.
   *
   * @param to Appendable to put formatted message to.
   * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
   * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
   * @param firstArgIdx the index of the first parameter.
   * @param argArray the message arguments.
   * @return the index of the last arguments used in the message + 1.
   * @throws IOException something wend wrong while writing to the appendable.
   */
  public static int format(final int firstArgIdx, @Nonnull final Appendable to, @Nonnull final String messagePattern,
          @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray) throws IOException {
    return format(Slf4jMessageFormatter::exHandle, firstArgIdx, to, messagePattern, appSupplier, argArray);
  }


  public static int getFormatParameterNumber(@Nonnull final String messagePattern) {
    int nrParams = 0;
    int i = 0;
    int j;
    while ((j = messagePattern.indexOf(DELIM_STR, i)) >= 0) {
        if (isEscapedDelimeter(messagePattern, j)) {
          if (isDoubleEscaped(messagePattern, j)) {
            // The escape character preceding the delimiter start is
            // itself escaped: "abc x:\\{}"
            // we have to consume one backward slash
            nrParams++;
            i = j + 2;
          } else {
            i = j + 1;
          }
        } else {
          // normal case
          nrParams++;
          i = j + 2;
        }
    }
    return nrParams;
  }




  /**
   * Slf4j message formatter.
   *
   * @param safe - if true recoverable exHandle will be caught when writing arguments, and a error will be appended
 instead.
   * @param to Appendable to put formatted message to.
   * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
   * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
   * @param firstArgIdx the index of the first parameter.
   * @param argArray the message arguments.
   * @return the index of the last arguments used in the message + 1.
   * @throws IOException something wend wrong while writing to the appendable.
   */
  public static int format(final ErrorHandler exHandler, final int firstArgIdx,
          @Nonnull final Appendable to, @Nonnull final String messagePattern,
          @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray)
          throws IOException {
    int i = 0;
    final int len = argArray.length;
    int k = firstArgIdx;
    for (; k < len; k++) {
      int j = messagePattern.indexOf(DELIM_STR, i);
      if (j == -1) {
        // no more variables
        break;
      } else {
        if (isEscapedDelimeter(messagePattern, j)) {
          if (isDoubleEscaped(messagePattern, j)) {
            // The escape character preceding the delimiter start is
            // itself escaped: "abc x:\\{}"
            // we have to consume one backward slash
            to.append(messagePattern, i, j - 1);
            deeplyAppendParameter(exHandler, to, argArray[k], new THashSet<>(), appSupplier);
            i = j + 2;
          } else {
            k--; // DELIM_START was escaped, thus should not be incremented
            to.append(messagePattern, i, j - 1);
            to.append(DELIM_START);
            i = j + 1;
          }
        } else {
          // normal case
          to.append(messagePattern, i, j);
          deeplyAppendParameter(exHandler, to, argArray[k], new THashSet<>(), appSupplier);
          i = j + 2;
        }
      }
    }
    // append the characters following the last {} pair.
    to.append(messagePattern, i, messagePattern.length());
    return k;
  }

  private static boolean isEscapedDelimeter(final String messagePattern, final int delimeterStartIndex) {
    if (delimeterStartIndex == 0) {
      return false;
    }
    return messagePattern.charAt(delimeterStartIndex - 1) == ESCAPE_CHAR;
  }

  private static boolean isDoubleEscaped(final String messagePattern, final int delimeterStartIndex) {
    return delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR;
  }

  // special treatment of array values was suggested by 'lizongbo'
  @SuppressFBWarnings("ITC_INHERITANCE_TYPE_CHECKING")
  private static void deeplyAppendParameter(final ErrorHandler exHandler, final Appendable sbuf, final Object o,
          final Set<Object[]> seen, final ObjectAppenderSupplier appSupplier) throws IOException {
    if (o == null) {
      sbuf.append("null");
      return;
    }
    if (!o.getClass().isArray()) {
      safeObjectAppend(exHandler, sbuf, o, appSupplier);
    } else {
      // check for primitive array types because they
      // unfortunately cannot be cast to Object[]
      if (o instanceof boolean[]) {
        booleanArrayAppend(sbuf, (boolean[]) o);
      } else if (o instanceof byte[]) {
        byteArrayAppend(sbuf, (byte[]) o);
      } else if (o instanceof char[]) {
        charArrayAppend(sbuf, (char[]) o);
      } else if (o instanceof short[]) {
        shortArrayAppend(sbuf, (short[]) o);
      } else if (o instanceof int[]) {
        intArrayAppend(sbuf, (int[]) o);
      } else if (o instanceof long[]) {
        longArrayAppend(sbuf, (long[]) o);
      } else if (o instanceof float[]) {
        floatArrayAppend(sbuf, (float[]) o);
      } else if (o instanceof double[]) {
        doubleArrayAppend(sbuf, (double[]) o);
      } else {
        objectArrayAppend(exHandler, sbuf, (Object[]) o, seen, appSupplier);
      }
    }
  }

  @SuppressWarnings("unchecked")
  public static void safeObjectAppend(final ErrorHandler exHandler, final Appendable sbuf, final Object obj,
          final ObjectAppenderSupplier appSupplier) throws IOException {
    try {
      appSupplier.get((Class) obj.getClass()).append(obj, sbuf, appSupplier);
    } catch (IOException | RuntimeException | StackOverflowError t) {
      exHandler.accept(obj, sbuf, t);
    }

  }

  @SuppressFBWarnings("ABC_ARRAY_BASED_COLLECTIONS")
  private static void objectArrayAppend(final ErrorHandler exHandler, final Appendable sbuf,
          final Object[] a, final Set<Object[]> seen,
          final ObjectAppenderSupplier appSupplier) throws IOException {
    sbuf.append('[');
    if (seen.add(a)) {
      final int len = a.length;
      if (len > 0) {
        deeplyAppendParameter(exHandler, sbuf, a[0], seen, appSupplier);
        for (int i = 1; i < len; i++) {
          sbuf.append(", ");
          deeplyAppendParameter(exHandler, sbuf, a[i], seen, appSupplier);
        }
      }
      // allow repeats in siblings
      seen.remove(a);
    } else {
      sbuf.append("...");
    }
    sbuf.append(']');
  }

  private static void booleanArrayAppend(final Appendable sbuf, final boolean[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Boolean.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Boolean.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void byteArrayAppend(final Appendable sbuf, final byte[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Byte.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Byte.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void charArrayAppend(final Appendable sbuf, final char[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(a[0]);
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(a[i]);
      }
    }
    sbuf.append(']');
  }

  private static void shortArrayAppend(final Appendable sbuf, final short[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Short.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Short.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void intArrayAppend(final Appendable sbuf, final int[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Integer.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Integer.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void longArrayAppend(final Appendable sbuf, final long[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Long.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Long.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void floatArrayAppend(final Appendable sbuf, final float[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Float.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Float.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

  private static void doubleArrayAppend(final Appendable sbuf, final double[] a) throws IOException {
    sbuf.append('[');
    final int len = a.length;
    if (len > 0) {
      sbuf.append(Double.toString(a[0]));
      for (int i = 1; i < len; i++) {
        sbuf.append(", ");
        sbuf.append(Double.toString(a[i]));
      }
    }
    sbuf.append(']');
  }

}