All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pmw.tinylog.Tokenizer Maven / Gradle / Ivy

/*
 * Copyright 2012 Martin Winandy
 *
 * 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.pmw.tinylog;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;

import org.pmw.tinylog.writers.LogEntryValue;

/**
 * Converts a format pattern for log entries to a list of tokens.
 */
final class Tokenizer {

	private static final String DEFAULT_DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";
	private static final String NEW_LINE = EnvironmentHelper.getNewLine();
	private static final Pattern NEW_LINE_REPLACER = Pattern.compile("\r\n|\\\\r\\\\n|\n|\\\\n|\r|\\\\r");
	private static final String TAB = "\t";
	private static final Pattern TAB_REPLACER = Pattern.compile("\t|\\\\t");

	private final Locale locale;
	private final int maxStackTraceElements;

	private int index;

	/**
	 * @param locale
	 *            Locale for formatting
	 * @param maxStackTraceElements
	 *            Limit of stack traces for exceptions
	 */
	Tokenizer(final Locale locale, final int maxStackTraceElements) {
		this.locale = locale;
		this.maxStackTraceElements = maxStackTraceElements;
	}

	/**
	 * Parse a format pattern.
	 *
	 * @param formatPattern
	 *            Format pattern for log entries
	 *
	 * @return List of tokens
	 */
	List parse(final String formatPattern) {
		List tokens = new ArrayList();
		index = 0;

		while (index < formatPattern.length()) {
			char c = formatPattern.charAt(index);

			int start = index;
			while (c != '{' && c != '}') {
				++index;
				if (index >= formatPattern.length()) {
					tokens.add(getPlainTextToken(formatPattern.substring(start, index)));
					return tokens;
				}
				c = formatPattern.charAt(index);
			}
			if (index > start) {
				tokens.add(getPlainTextToken(formatPattern.substring(start, index)));
			}

			if (c == '{') {
				Token token = parsePartly(formatPattern);
				if (token != null) {
					tokens.add(token);
				}
			} else if (c == '}') {
				InternalLogger.warn("Opening curly brace is missing for: \"{}\"", formatPattern.substring(0, index + 1));
				++index;
			}
		}

		return tokens;
	}

	private Token parsePartly(final String formatPattern) {
		List tokens = new ArrayList();
		int[] options = new int[] { 0 /* minimum size */, 0 /* indent */};
		int offset = index;

		++index;
		while (index < formatPattern.length()) {
			char c = formatPattern.charAt(index);

			int start = index;
			while (c != '{' && c != '|' && c != '}') {
				++index;
				if (index >= formatPattern.length()) {
					InternalLogger.warn("Closing curly brace is missing for: \"{}\"", formatPattern.substring(offset, index));
					tokens.add(getToken(formatPattern.substring(start, index)));
					return combine(tokens, options);
				}
				c = formatPattern.charAt(index);
			}
			if (index > start) {
				if (c == '{') {
					tokens.add(getPlainTextToken(formatPattern.substring(start, index)));
				} else {
					tokens.add(getToken(formatPattern.substring(start, index)));
				}
			}

			if (c == '{') {
				Token token = parsePartly(formatPattern);
				if (token != null) {
					tokens.add(token);
				}
			} else if (c == '|') {
				++index;
				start = index;
				while (c != '{' && c != '}') {
					++index;
					if (index >= formatPattern.length()) {
						InternalLogger.warn("Closing curly brace is missing for: \"{}\"", formatPattern.substring(offset, index));
						options = parseOptions(formatPattern.substring(start));
						return combine(tokens, options);
					}
					c = formatPattern.charAt(index);
				}
				if (index > start) {
					options = parseOptions(formatPattern.substring(start, index));
				}
			} else if (c == '}') {
				++index;
				return combine(tokens, options);
			}
		}

		InternalLogger.warn("Closing curly brace is missing for: \"{}\"", formatPattern.substring(offset, index));
		return combine(tokens, options);
	}

	private Token getToken(final String text) {
		if (text.equals("date")) {
			return getDateToken(DEFAULT_DATE_FORMAT_PATTERN, locale);
		} else if (text.startsWith("date:")) {
			String dateFormatPattern = text.substring(5, text.length()).trim();
			try {
				return getDateToken(dateFormatPattern, locale);
			} catch (IllegalArgumentException ex) {
				InternalLogger.error(ex, "\"{}\" is an invalid date format pattern", dateFormatPattern);
				return getDateToken(DEFAULT_DATE_FORMAT_PATTERN, locale);
			}
		} else if ("pid".equals(text)) {
			return new PlainTextToken(EnvironmentHelper.getRuntimeDialect().getProcessId());
		} else if (text.startsWith("pid:")) {
			InternalLogger.warn("\"{pid}\" does not support parameters");
			return new PlainTextToken(EnvironmentHelper.getRuntimeDialect().getProcessId());
		} else if ("thread".equals(text)) {
			return new ThreadNameToken();
		} else if (text.startsWith("thread:")) {
			InternalLogger.warn("\"{thread}\" does not support parameters");
			return new ThreadNameToken();
		} else if ("thread_id".equals(text)) {
			return new ThreadIdToken();
		} else if (text.startsWith("thread_id:")) {
			InternalLogger.warn("\"{thread_id}\" does not support parameters");
			return new ThreadIdToken();
		} else if (text.equals("context")) {
			InternalLogger.error("\"{context}\" requires a key");
			return getPlainTextToken("");
		} else if (text.startsWith("context:")) {
			String key = text.substring(8, text.length()).trim();
			if (key.length() == 0) {
				InternalLogger.error("\"{context}\" requires a key");
				return getPlainTextToken("");
			} else {
				return new ContextToken(key);
			}
		} else if ("class".equals(text)) {
			return new ClassToken();
		} else if (text.startsWith("class:")) {
			InternalLogger.warn("\"{class}\" does not support parameters");
			return new ClassToken();
		} else if ("class_name".equals(text)) {
			return new ClassNameToken();
		} else if (text.startsWith("class_name:")) {
			InternalLogger.warn("\"{class_name}\" does not support parameters");
			return new ClassNameToken();
		} else if ("package".equals(text)) {
			return new PackageToken();
		} else if (text.startsWith("package:")) {
			InternalLogger.warn("\"{package}\" does not support parameters");
			return new PackageToken();
		} else if ("method".equals(text)) {
			return new MethodToken();
		} else if (text.startsWith("method:")) {
			InternalLogger.warn("\"{method}\" does not support parameters");
			return new MethodToken();
		} else if ("file".equals(text)) {
			return new FileToken();
		} else if (text.startsWith("file:")) {
			InternalLogger.warn("\"{file}\" does not support parameters");
			return new FileToken();
		} else if ("line".equals(text)) {
			return new LineToken();
		} else if (text.startsWith("line:")) {
			InternalLogger.warn("\"{line}\" does not support parameters");
			return new LineToken();
		} else if ("level".equals(text)) {
			return new LevelToken();
		} else if (text.startsWith("level:")) {
			InternalLogger.warn("\"{level}\" does not support parameters");
			return new LevelToken();
		} else if ("message".equals(text)) {
			return new MessageToken(maxStackTraceElements);
		} else if (text.startsWith("message:")) {
			InternalLogger.warn("\"{message}\" does not support parameters");
			return new MessageToken(maxStackTraceElements);
		} else {
			return getPlainTextToken(text);
		}
	}

	private static Token getDateToken(final String pattern, final Locale locale) {
		if (pattern.contains("SSSS") || pattern.contains("n") || pattern.contains("N")) {
			if (EnvironmentHelper.isAtLeastJava9()) {
				return new PreciseDateToken(pattern, locale);
			} else {
				InternalLogger.warn("Java prior version 9 does not support microseconds or nanoseconds");
				return new LegacyDateToken(pattern, locale);
			}
		} else {
			return new LegacyDateToken(pattern, locale);
		}
	}

	private static Token getPlainTextToken(final String text) {
		String plainText = NEW_LINE_REPLACER.matcher(text).replaceAll(NEW_LINE);
		plainText = TAB_REPLACER.matcher(plainText).replaceAll(TAB);
		return new PlainTextToken(plainText);
	}

	private static int[] parseOptions(final String text) {
		int minSize = 0;
		int indent = 0;

		int index = 0;
		while (index < text.length()) {
			char c = text.charAt(index);

			while (c == ',') {
				++index;
				if (index >= text.length()) {
					return new int[] { minSize, indent };
				}
				c = text.charAt(index);
			}

			int start = index;
			while (c != ',') {
				++index;
				if (index >= text.length()) {
					break;
				}
				c = text.charAt(index);
			}
			if (index > start) {
				String parameter = text.substring(start, index);
				int splitter = parameter.indexOf('=');
				if (splitter == -1) {
					parameter = parameter.trim();
					if ("min-size".equals(parameter)) {
						InternalLogger.warn("No value set for \"min-size\"");
					} else if ("indent".equals(parameter)) {
						InternalLogger.warn("No value set for \"indent\"");
					} else  {
						InternalLogger.warn("Unknown option \"{}\"", parameter);
					}
				} else {
					String key = parameter.substring(0, splitter).trim();
					String value = parameter.substring(splitter + 1).trim();
					if ("min-size".equals(key)) {
						if (value.length() == 0) {
							InternalLogger.warn("No value set for \"min-size\"");
						} else {
							try {
								minSize = parsePositiveInt(value);
							} catch (NumberFormatException ex) {
								InternalLogger.warn("\"{}\" is an invalid number for \"min-size\"", value);
							}
						}
					} else if ("indent".equals(key)) {
						if (value.length() == 0) {
							InternalLogger.warn("No value set for \"indent\"");
						} else {
							try {
								indent = parsePositiveInt(value);
							} catch (NumberFormatException ex) {
								InternalLogger.warn("\"{}\" is an invalid number for \"indent\"", value);
							}
						}
					} else {
						InternalLogger.warn("Unknown option \"{}\"", key);
					}
				}
			}
		}

		return new int[] { minSize, indent };
	}

	private static int parsePositiveInt(final String value) throws NumberFormatException {
		int number = Integer.parseInt(value);
		if (number >= 0) {
			return number;
		} else {
			throw new NumberFormatException();
		}
	}

	private static Token combine(final List tokens, final int[] options) {
		int minSize = options[0];
		int indent = options[1];

		if (tokens.isEmpty()) {
			return null;
		} else if (tokens.size() == 1) {
			if (indent > 0) {
				return new IndentToken(tokens.get(0), indent);
			} else if (minSize > 0) {
				return new MinSizeToken(tokens.get(0), minSize);
			} else {
				return tokens.get(0);
			}
		} else {
			if (indent > 0) {
				return new IndentToken(new BundlerToken(tokens), indent);
			} else if (minSize > 0) {
				return new MinSizeToken(new BundlerToken(tokens), minSize);
			} else {
				return new BundlerToken(tokens);
			}
		}
	}

	private static final class BundlerToken implements Token {

		private final List tokens;

		private BundlerToken(final List tokens) {
			this.tokens = tokens;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			Collection values = EnumSet.noneOf(LogEntryValue.class);
			for (Token token : tokens) {
				values.addAll(token.getRequiredLogEntryValues());
			}
			return values;
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			for (Token token : tokens) {
				token.render(logEntry, builder);
			}
		}

	}

	private static final class MinSizeToken implements Token {

		private final Token token;
		private final int minSize;

		private MinSizeToken(final Token token, final int minSize) {
			this.token = token;
			this.minSize = minSize;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return token.getRequiredLogEntryValues();
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			int offset = builder.length();
			token.render(logEntry, builder);
			int size = builder.length() - offset;
			if (size < minSize) {
				char[] spaces = new char[minSize - size];
				Arrays.fill(spaces, ' ');
				builder.append(spaces);
			}
		}

	}

	private static final class IndentToken implements Token {

		private final Token token;
		private final char[] spaces;

		private IndentToken(final Token token, final int indent) {
			this.token = token;
			this.spaces = new char[indent];
			Arrays.fill(spaces, ' ');
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return token.getRequiredLogEntryValues();
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			StringBuilder subBuilder = new StringBuilder(1024);
			token.render(logEntry, subBuilder);

			int i = 0;
			int head = 0;

			if (builder.length() == 0 || builder.charAt(builder.length() - 1) == '\n' || builder.charAt(builder.length() - 1) == '\r') {
				i += addSpaces(builder, subBuilder, i);
				head = i;
			}

			while (i < subBuilder.length()) {
				int lineBreakLength = readLineBreak(subBuilder, i);
				if (lineBreakLength > 0) {
					i += lineBreakLength;
					builder.append(subBuilder, head, i);
					i += addSpaces(builder, subBuilder, i);
					head = i;
				} else {
					++i;
				}
			}

			if (head < subBuilder.length()) {
				builder.append(subBuilder, head, subBuilder.length());
			}
		}

		private int addSpaces(final StringBuilder targetBuilder, final StringBuilder sourceBuilder, final int index) {
			targetBuilder.append(spaces);

			int count = 0;
			for (int i = index; i < sourceBuilder.length() && sourceBuilder.charAt(i) == '\t'; ++i) {
				targetBuilder.append(spaces);
				++count;
			}
			return count;
		}

		private static int readLineBreak(final StringBuilder builder, final int index) {
			char c = builder.charAt(index);
			if (c == '\n') {
				return 1;
			} else if (c == '\r') {
				if (index + 1 < builder.length() && builder.charAt(index + 1) == '\n') {
					return 2;
				} else {
					return 1;
				}
			} else {
				return 0;
			}
		}

	}

	private static final class LegacyDateToken implements Token {

		private final DateFormat formatter;
		private final long divisor;

		private Date lastDate;
		private String lastFormat;

		private LegacyDateToken(final String pattern, final Locale locale) {
			this.formatter = new SimpleDateFormat(pattern, locale);
			this.divisor = pattern.contains("S") ? 1 : pattern.contains("s") ? 1000 : 60000;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.DATE);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(format(logEntry.getDate()));
		}

		private String format(final Date date) {
			synchronized (formatter) {
				if (lastDate != null && date.getTime() / divisor == lastDate.getTime() / divisor) {
					return lastFormat;
				} else {
					lastDate = date;
					lastFormat = formatter.format(date);
					return lastFormat;
				}
			}
		}

	}

	private static final class PreciseDateToken implements Token {
		
		private final DateTimeFormatter formatter;

		private PreciseDateToken(final String pattern, final Locale locale) {
			formatter = DateTimeFormatter.ofPattern(pattern, locale).withZone(ZoneId.systemDefault());
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.PRECISE_DATE);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(formatter.format(((PreciseLogEntry) logEntry).getInstant()));
		}

	}

	private static final class ThreadNameToken implements Token {

		private ThreadNameToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.THREAD);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getThread().getName());
		}

	}

	private static final class ThreadIdToken implements Token {

		private ThreadIdToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.THREAD);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getThread().getId());
		}

	}

	private static final class ContextToken implements Token {

		private final String key;

		private ContextToken(final String key) {
			this.key = key;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.CONTEXT);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getContext().get(key));
		}

	}

	private static final class ClassToken implements Token {

		private ClassToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.CLASS);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getClassName());
		}

	}

	private static final class ClassNameToken implements Token {

		private ClassNameToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.CLASS);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			String fullyQualifiedClassName = logEntry.getClassName();
			int dotIndex = fullyQualifiedClassName.lastIndexOf('.');
			if (dotIndex < 0) {
				builder.append(fullyQualifiedClassName);
			} else {
				builder.append(fullyQualifiedClassName.substring(dotIndex + 1));
			}
		}

	}

	private static final class PackageToken implements Token {

		private PackageToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.CLASS);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			String fullyQualifiedClassName = logEntry.getClassName();
			int dotIndex = fullyQualifiedClassName.lastIndexOf('.');
			if (dotIndex != -1) {
				builder.append(fullyQualifiedClassName.substring(0, dotIndex));
			}
		}

	}

	private static final class MethodToken implements Token {

		private MethodToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.METHOD);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getMethodName());
		}

	}

	private static final class FileToken implements Token {

		private FileToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.FILE);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getFilename());
		}

	}

	private static final class LineToken implements Token {

		private LineToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.LINE);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getLineNumber());
		}

	}

	private static final class LevelToken implements Token {

		private LevelToken() {
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.singletonList(LogEntryValue.LEVEL);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(logEntry.getLevel());
		}

	}

	private static final class MessageToken implements Token {

		private static final String NEW_LINE = EnvironmentHelper.getNewLine();

		private final int maxStackTraceElements;

		private MessageToken(final int maxStackTraceElements) {
			this.maxStackTraceElements = maxStackTraceElements;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return EnumSet.of(LogEntryValue.MESSAGE, LogEntryValue.EXCEPTION);
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			String message = logEntry.getMessage();
			if (message != null) {
				builder.append(message);
			}
			Throwable exception = logEntry.getException();
			if (exception != null) {
				if (message != null) {
					builder.append(": ");
				}
				formatException(builder, exception, maxStackTraceElements);
			}
		}

		private static void formatException(final StringBuilder builder, final Throwable exception, final int maxStackTraceElements) {
			if (maxStackTraceElements == 0) {
				builder.append(exception.getClass().getName());
				String exceptionMessage = exception.getMessage();
				if (exceptionMessage != null) {
					builder.append(": ");
					builder.append(exceptionMessage);
				}
			} else {
				formatExceptionWithStackTrace(builder, exception, maxStackTraceElements);
			}
		}

		private static void formatExceptionWithStackTrace(final StringBuilder builder, final Throwable exception, final int countStackTraceElements) {
			builder.append(exception.getClass().getName());

			String message = exception.getMessage();
			if (message != null) {
				builder.append(": ");
				builder.append(message);
			}

			StackTraceElement[] stackTrace = exception.getStackTrace();
			int length = Math.min(stackTrace.length, Math.max(1, countStackTraceElements));
			for (int i = 0; i < length; ++i) {
				builder.append(NEW_LINE);
				builder.append('\t');
				builder.append("at ");
				builder.append(stackTrace[i]);
			}

			if (stackTrace.length > length) {
				builder.append(NEW_LINE);
				builder.append('\t');
				builder.append("...");
			} else {
				Throwable cause = exception.getCause();
				if (cause != null) {
					builder.append(NEW_LINE);
					builder.append("Caused by: ");
					formatExceptionWithStackTrace(builder, cause, countStackTraceElements - length);
				}
			}
		}

	}

	private static final class PlainTextToken implements Token {

		private final String text;

		private PlainTextToken(final String text) {
			this.text = text;
		}

		@Override
		public Collection getRequiredLogEntryValues() {
			return Collections.emptyList();
		}

		@Override
		public void render(final LogEntry logEntry, final StringBuilder builder) {
			builder.append(text);
		}

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy