BoundedAppender.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.hadoop.yarn.util;

import org.apache.hadoop.classification.VisibleForTesting;
import org.apache.hadoop.util.Preconditions;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;

/**
 * A {@link CharSequence} appender that considers its {@link #limit} as upper
 * bound.
 * <p>
 * When {@link #limit} would be reached on append, past messages will be
 * truncated from head, and a header telling the user about truncation will be
 * prepended, with ellipses in between header and messages.
 * <p>
 * Note that header and ellipses are not counted against {@link #limit}.
 * <p>
 * An example:
 *
 * <pre>
 * {@code
 *   // At the beginning it's an empty string
 *   final Appendable shortAppender = new BoundedAppender(80);
 *   // The whole message fits into limit
 *   shortAppender.append(
 *       "message1 this is a very long message but fitting into limit\n");
 *   // The first message is truncated, the second not
 *   shortAppender.append("message2 this is shorter than the previous one\n");
 *   // The first message is deleted, the second truncated, the third
 *   // preserved
 *   shortAppender.append("message3 this is even shorter message, maybe.\n");
 *   // The first two are deleted, the third one truncated, the last preserved
 *   shortAppender.append("message4 the shortest one, yet the greatest :)");
 *   // Current contents are like this:
 *   // Diagnostic messages truncated, showing last 80 chars out of 199:
 *   // ...s is even shorter message, maybe.
 *   // message4 the shortest one, yet the greatest :)
 * }
 * </pre>
 * <p>
 * Note that <code>null</code> values are {@link #append(CharSequence) append}ed
 * just like in {@link StringBuilder#append(CharSequence) original
 * implementation}.
 * <p>
 * Note that this class is not thread safe.
 */

@InterfaceAudience.Public
@InterfaceStability.Unstable
@VisibleForTesting
public class BoundedAppender {
  @VisibleForTesting
  public static final String TRUNCATED_MESSAGES_TEMPLATE =
      "Diagnostic messages truncated, showing last "
          + "%d chars out of %d:%n...%s";

  private final int limit;
  private final StringBuilder messages = new StringBuilder();
  private int totalCharacterCount = 0;

  public BoundedAppender(final int limit) {
    Preconditions.checkArgument(limit > 0, "limit should be positive");

    this.limit = limit;
  }

  /**
   * Append a {@link CharSequence} considering {@link #limit}, truncating
   * from the head of {@code csq} or {@link #messages} when necessary.
   *
   * @param csq the {@link CharSequence} to append
   * @return this
   */
  public BoundedAppender append(final CharSequence csq) {
    appendAndCount(csq);
    checkAndCut();

    return this;
  }

  private void appendAndCount(final CharSequence csq) {
    final int before = messages.length();
    messages.append(csq);
    final int after = messages.length();
    totalCharacterCount += after - before;
  }

  private void checkAndCut() {
    if (messages.length() > limit) {
      final int newStart = messages.length() - limit;
      messages.delete(0, newStart);
    }
  }

  /**
   * Get current length of messages considering truncates
   * without header and ellipses.
   *
   * @return current length
   */
  public int length() {
    return messages.length();
  }

  public int getLimit() {
    return limit;
  }

  /**
   * Get a string representation of the actual contents, displaying also a
   * header and ellipses when there was a truncate.
   *
   * @return String representation of the {@link #messages}
   */
  @Override
  public String toString() {
    if (messages.length() < totalCharacterCount) {
      return String.format(TRUNCATED_MESSAGES_TEMPLATE, messages.length(),
          totalCharacterCount, messages.toString());
    }

    return messages.toString();
  }
}