/* * Lilith - a log event viewer. * Copyright (C) 2007-2016 Joern Huxhorn * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 Lesser 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, see <http://www.gnu.org/licenses/>. */ /* * Copyright 2007-2016 Joern Huxhorn * * 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 de.huxhorn.lilith.data.logging; import de.huxhorn.sulky.formatting.SafeString; import java.util.Arrays; /** * <p>Replacement for org.slf4j.helpers.MessageFormatter.</p> * <p> * In contrast to the mentioned class, the formatting of message pattern and arguments into the actual message * is split into three parts: * </p> * <ol> * <li>Counting of placeholders in the message pattern (cheap)</li> * <li>Conversion of argument array into an ArgumentResult, containing the arguments converted to String as well as * an optional Throwable if available (relatively cheap)</li> * <li>Replacement of placeholders in a message pattern with arguments given as String[]. (most expensive)</li> * </ol> * <p> * That way only the first two steps have to be done during event creation while the most expensive part, i.e. the * actual construction of the message, is only done on demand. * </p> */ public class MessageFormatter { private static final char DELIM_START = '{'; private static final char DELIM_STOP = '}'; private static final char ESCAPE_CHAR = '\\'; /** * Replace placeholders in the given messagePattern with arguments. * * @param messagePattern the message pattern containing placeholders. * @param arguments the arguments to be used to replace placeholders. * @return the formatted message. */ public static String format(String messagePattern, String[] arguments) { if(messagePattern == null || arguments == null || arguments.length == 0) { return messagePattern; } StringBuilder result = new StringBuilder(); int escapeCounter = 0; int currentArgument = 0; for(int i = 0; i < messagePattern.length(); i++) { char curChar = messagePattern.charAt(i); if(curChar == ESCAPE_CHAR) { escapeCounter++; } else { if(curChar == DELIM_START) { if(i < messagePattern.length() - 1) { if(messagePattern.charAt(i + 1) == DELIM_STOP) { // write escaped escape chars int escapedEscapes = escapeCounter / 2; for(int j = 0; j < escapedEscapes; j++) { result.append(ESCAPE_CHAR); } if(escapeCounter % 2 == 1) { // i.e. escaped // write escaped escape chars result.append(DELIM_START); result.append(DELIM_STOP); } else { // unescaped if(currentArgument < arguments.length) { result.append(arguments[currentArgument]); } else { result.append(DELIM_START).append(DELIM_STOP); } currentArgument++; } // this is an optimization: charAt(i+1) has already been checked. // @cs-: ModifiedControlVariable i++; escapeCounter = 0; continue; } } } // any other char beside ESCAPE or DELIM_START/STOP-combo // write unescaped escape chars if(escapeCounter > 0) { for(int j = 0; j < escapeCounter; j++) { result.append(ESCAPE_CHAR); } escapeCounter = 0; } result.append(curChar); } } return result.toString(); } /** * Counts the number of unescaped placeholders in the given messagePattern. * * @param messagePattern the message pattern to be analyzed. * @return the number of unescaped placeholders. */ public static int countArgumentPlaceholders(String messagePattern) { if(messagePattern == null) { return 0; } if(-1 == messagePattern.indexOf(DELIM_START)) { // Special case: no placeholders at all. // This is an optimization because charAt checks bounds for every // single call while indexOf(char) isn't. // Big messages without placeholders will benefit from this shortcut. // the result of indexOf can't be used as start index in the loop // below because it could still be escaped. return 0; } int result = 0; boolean isEscaped = false; for(int i = 0; i < messagePattern.length(); i++) { char curChar = messagePattern.charAt(i); if(curChar == ESCAPE_CHAR) { isEscaped = !isEscaped; } else if(curChar == DELIM_START) { if(!isEscaped) { if(i < messagePattern.length() - 1) { if(messagePattern.charAt(i + 1) == DELIM_STOP) { result++; // this is an optimization: charAt(i+1) has already been checked. // @cs-: ModifiedControlVariable i++; } } } isEscaped = false; } else { isEscaped = false; } } return result; } /** * <p>This method returns a MessageFormatter.ArgumentResult which contains the arguments converted to String * as well as an optional Throwable.</p> * * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned * in MessageFormatter.ArgumentResult.getThrowable() and won't be contained in the created String[].</p> * <p>If it is used up getThrowable will return null even if the last argument was a Throwable!</p> * * @param messagePattern the message pattern that to be checked for placeholders. * @param arguments the argument array to be converted. * @return a MessageFormatter.ArgumentResult containing the converted formatted message and optionally a Throwable. */ public static ArgumentResult evaluateArguments(String messagePattern, Object[] arguments) { if(arguments == null) { return null; } int argsCount = countArgumentPlaceholders(messagePattern); int resultArgCount = arguments.length; Throwable throwable = null; if(argsCount < arguments.length) { if(arguments[arguments.length - 1] instanceof Throwable) { throwable = (Throwable) arguments[arguments.length - 1]; resultArgCount--; } } String[] stringArgs; if(argsCount == 1 && throwable == null && arguments.length > 1) { // special case stringArgs = new String[1]; stringArgs[0] = SafeString.toString(arguments, SafeString.StringWrapping.CONTAINED, SafeString.StringStyle.GROOVY, SafeString.MapStyle.GROOVY); } else { stringArgs = new String[resultArgCount]; for(int i = 0; i < stringArgs.length; i++) { stringArgs[i] = SafeString.toString(arguments[i], SafeString.StringWrapping.CONTAINED, SafeString.StringStyle.GROOVY, SafeString.MapStyle.GROOVY); } } return new ArgumentResult(stringArgs, throwable); } /** * <p>This is just a simple class containing the result of an evaluateArgument call. It's necessary because we need to * return two results, i.e. the resulting String[] and the optional Throwable.</p> * * <p>This class is not Serializable because serializing a Throwable is generally a bad idea if the data is supposed * to leave the current VM since it may result in ClassNotFoundExceptions if the given Throwable is not * available/different in the deserializing VM.</p> */ public static class ArgumentResult { private final String[] arguments; private final Throwable throwable; public ArgumentResult(String[] arguments, Throwable throwable) { this.arguments = arguments; this.throwable = throwable; } public String[] getArguments() { return arguments; } public Throwable getThrowable() { return throwable; } @Override public String toString() { final StringBuilder result = new StringBuilder("ArgumentResult{"); result.append("arguments="); if(arguments == null) { result.append("null"); } else { result.append('['); boolean isFirst = true; for (String current : arguments) { if (!isFirst) { result.append(", "); } else { isFirst = false; } if (current != null) { result.append('"').append(current).append('"'); } else { result.append("null"); } } result.append(']'); } result.append(", throwable=").append(throwable); result.append('}'); return result.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ArgumentResult that = (ArgumentResult) o; if (!Arrays.equals(arguments, that.arguments)) return false; return throwable != null ? throwable.equals(that.throwable) : that.throwable == null; } @Override public int hashCode() { int result = Arrays.hashCode(arguments); result = 31 * result + (throwable != null ? throwable.hashCode() : 0); return result; } } }