Abbreviator.java
package io.jstach.rainbowgum.pattern.format;
import java.util.concurrent.ConcurrentHashMap;
interface Abbreviator {
static final char DOT = '.';
public String abbreviate(String in);
/**
* Creates the abbreviator for the given target length - uncached. Callers that want
* caching wrap the result with {@link #cache(Abbreviator)} themselves, driven by
* {@link PatternConfig#abbreviatorCache()}.
* @param length target length, non-positive means class-name-only.
* @return abbreviator.
*/
public static Abbreviator of(int length) {
if (length <= 0) {
return StandardAbbreviator.CLASS_NAME_ONLY;
}
return new TargetLengthBasedClassNameAbbreviator(length);
}
/**
* Wraps an abbreviator with a cache.
* @param a abbreviator to wrap.
* @return cached abbreviator.
*/
public static Abbreviator cache(Abbreviator a) {
return new CacheAbbreviator(a);
}
/*
* Why a plain ConcurrentHashMap and not an LRU or Logback's cache:
*
* The key is the logger name, and logger names are as bounded as the loggers
* themselves, which the logging facades keep for the life of the process anyway. So
* this map never holds more than one short string per logger that already exists. An
* LRU only protects against an unbounded key set; here eviction would just recompute
* abbreviations for loggers that are still in use.
*
* Logger names that are not bounded, for example a request id used as a logger name,
* are a bug in the application that would leak loggers first. Rainbow Gum reports
* them through LogMetrics.LOGGER_NAMES_METRIC instead of guarding every cache.
*
* Logback's NamedConverter cache, which this replaced, took a lock on every call (hit
* or miss) to count calls, and grew or disabled itself based on its miss rate; all of
* that exists for the unbounded case. Here a hit is a lock free read and a miss locks
* only its own bin in computeIfAbsent.
*/
final class CacheAbbreviator implements Abbreviator {
private final Abbreviator abbreviator;
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
CacheAbbreviator(Abbreviator abbreviator) {
this.abbreviator = abbreviator;
}
@Override
public String abbreviate(String in) {
String abbreviated = cache.get(in);
if (abbreviated != null) {
return abbreviated;
}
return cache.computeIfAbsent(in, abbreviator::abbreviate);
}
}
enum StandardAbbreviator implements Abbreviator {
CLASS_NAME_ONLY {
@Override
public String abbreviate(String fqClassName) {
// we ignore the fact that the separator character can also be a
// dollar
// If the inner class is org.good.AClass#Inner, returning
// AClass#Inner seems most appropriate
int lastIndex = fqClassName.lastIndexOf(DOT);
if (lastIndex != -1) {
return fqClassName.substring(lastIndex + 1, fqClassName.length());
}
else {
return fqClassName;
}
}
};
}
class TargetLengthBasedClassNameAbbreviator implements Abbreviator {
final int targetLength;
public TargetLengthBasedClassNameAbbreviator(int targetLength) {
this.targetLength = targetLength;
}
@Override
public String abbreviate(String fqClassName) {
int inLen = fqClassName.length();
if (inLen < targetLength) {
return fqClassName;
}
StringBuilder buf = new StringBuilder(inLen);
int rightMostDotIndex = fqClassName.lastIndexOf(DOT);
if (rightMostDotIndex == -1)
return fqClassName;
// length of last segment including the dot
int lastSegmentLength = inLen - rightMostDotIndex;
int leftSegments_TargetLen = targetLength - lastSegmentLength;
if (leftSegments_TargetLen < 0)
leftSegments_TargetLen = 0;
int leftSegmentsLen = inLen - lastSegmentLength;
// maxPossibleTrim denotes the maximum number of characters we aim
// to trim
// the actual number of character trimmed may be higher since
// segments, when
// reduced, are reduced to just one character
int maxPossibleTrim = leftSegmentsLen - leftSegments_TargetLen;
int trimmed = 0;
boolean inDotState = true;
int i = 0;
for (; i < rightMostDotIndex; i++) {
char c = fqClassName.charAt(i);
if (c == DOT) {
// if trimmed too many characters, let us stop
if (trimmed >= maxPossibleTrim)
break;
buf.append(c);
inDotState = true;
}
else {
if (inDotState) {
buf.append(c);
inDotState = false;
}
else {
trimmed++;
}
}
}
// append from the position of i which may include the last seen DOT
buf.append(fqClassName.substring(i));
return buf.toString();
}
}
}