/* * Lilith - a log event viewer. * Copyright (C) 2007-2017 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-2017 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 java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; /** * Replacement for java.lang.StackTraceElement containing additional info about * version and code location of the given class. */ public class ExtendedStackTraceElement implements Serializable, Cloneable { private static final long serialVersionUID = -6954579590347369344L; public static final int UNKNOWN_SOURCE_LINE_NUMBER = -1; public static final int NATIVE_METHOD_LINE_NUMBER = -2; private static final String NATIVE_METHOD_STRING = "Native Method"; private static final String UNKNOWN_SOURCE_STRING = "Unknown Source"; private static final String NA_PLACEHOLDER = "na"; private static final String EXTENDED_EXACT_PREFIX = "["; private static final String EXTENDED_INEXACT_PREFIX = "~["; private static final String EXTENDED_POSTFIX = "]"; private static final char SEPARATOR_CHAR = ':'; private static final char MODULE_SEPARATOR_CHAR = '/'; private static final char MODULE_VERSION_SEPARATOR_CHAR = '@'; private String className; private String methodName; private String fileName; private int lineNumber; // Logback extended info private String codeLocation; private String version; private boolean exact; // Java 9 private String classLoaderName; private String moduleName; private String moduleVersion; public ExtendedStackTraceElement() { lineNumber = UNKNOWN_SOURCE_LINE_NUMBER; } public ExtendedStackTraceElement(StackTraceElement stackTraceElement) { Objects.requireNonNull(stackTraceElement, "stackTraceElement must not be null!"); this.className = stackTraceElement.getClassName(); this.methodName = stackTraceElement.getMethodName(); this.fileName = stackTraceElement.getFileName(); this.lineNumber = stackTraceElement.getLineNumber(); this.classLoaderName = getClassLoaderNameFrom(stackTraceElement); this.moduleName = getModuleNameFrom(stackTraceElement); this.moduleVersion= getModuleVersionFrom(stackTraceElement); } public ExtendedStackTraceElement(String className, String methodName, String fileName, int lineNumber) { this(className, methodName, fileName, lineNumber, null, null, false); } public ExtendedStackTraceElement(String className, String methodName, String fileName, int lineNumber, String codeLocation, String version, boolean exact) { this.className = className; this.methodName = methodName; this.fileName = fileName; this.lineNumber = lineNumber; this.codeLocation = codeLocation; this.version = version; this.exact = exact; } public boolean isNativeMethod() { return lineNumber == NATIVE_METHOD_LINE_NUMBER; } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public int getLineNumber() { return lineNumber; } public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } public String getCodeLocation() { return codeLocation; } public void setCodeLocation(String codeLocation) { this.codeLocation = codeLocation; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public boolean isExact() { return exact; } public void setExact(boolean exact) { this.exact = exact; } public String getClassLoaderName() { return classLoaderName; } public void setClassLoaderName(String classLoaderName) { this.classLoaderName = classLoaderName; } public String getModuleName() { return moduleName; } public void setModuleName(String moduleName) { this.moduleName = moduleName; } public String getModuleVersion() { return moduleVersion; } public void setModuleVersion(String moduleVersion) { this.moduleVersion = moduleVersion; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ExtendedStackTraceElement that = (ExtendedStackTraceElement) o; if (lineNumber != that.lineNumber) return false; if (exact != that.exact) return false; if (className != null ? !className.equals(that.className) : that.className != null) return false; if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) return false; if (fileName != null ? !fileName.equals(that.fileName) : that.fileName != null) return false; if (codeLocation != null ? !codeLocation.equals(that.codeLocation) : that.codeLocation != null) return false; if (version != null ? !version.equals(that.version) : that.version != null) return false; if (classLoaderName != null ? !classLoaderName.equals(that.classLoaderName) : that.classLoaderName != null) return false; if (moduleName != null ? !moduleName.equals(that.moduleName) : that.moduleName != null) return false; return moduleVersion != null ? moduleVersion.equals(that.moduleVersion) : that.moduleVersion == null; } @Override public int hashCode() { int result = className != null ? className.hashCode() : 0; result = 31 * result + (methodName != null ? methodName.hashCode() : 0); result = 31 * result + (fileName != null ? fileName.hashCode() : 0); result = 31 * result + lineNumber; result = 31 * result + (codeLocation != null ? codeLocation.hashCode() : 0); result = 31 * result + (version != null ? version.hashCode() : 0); result = 31 * result + (exact ? 1 : 0); result = 31 * result + (classLoaderName != null ? classLoaderName.hashCode() : 0); result = 31 * result + (moduleName != null ? moduleName.hashCode() : 0); result = 31 * result + (moduleVersion != null ? moduleVersion.hashCode() : 0); return result; } /** * @return the basic StackTraceElement this instance represents or null if either className or methodName are null. */ public StackTraceElement getStackTraceElement() { if(className == null || methodName == null) { return null; } return createStackTraceElement(classLoaderName, moduleName, moduleVersion, className, methodName, fileName, lineNumber); } public ExtendedStackTraceElement clone() throws CloneNotSupportedException { return (ExtendedStackTraceElement) super.clone(); } public String getExtendedString() { if(codeLocation != null || version != null) { return appendExtended(new StringBuilder()).toString(); } return null; } private StringBuilder appendExtended(StringBuilder stringBuilder) { if (exact) { stringBuilder.append(EXTENDED_EXACT_PREFIX); } else { stringBuilder.append(EXTENDED_INEXACT_PREFIX); } if (codeLocation != null) { stringBuilder.append(codeLocation); } else { stringBuilder.append(NA_PLACEHOLDER); } stringBuilder.append(SEPARATOR_CHAR); if (version != null) { stringBuilder.append(version); } else { stringBuilder.append(NA_PLACEHOLDER); } stringBuilder.append(EXTENDED_POSTFIX); return stringBuilder; } /** * Returns the string representation of this instance, but without extended info. * * Shortcut for toString(false). * * @return String representation of this instance, but without extended info. */ public String toString() { return toString(false); } /** * Returns the string representation of this instance. * Extended info will be included if the parameter extended is true and info is available. * * @param extended Whether or not extended info should be included, if available. * @return String representation of this instance. */ public String toString(boolean extended) { return appendTo(new StringBuilder(), extended).toString(); } /** * Appends this instance to the given StringBuilder. * * @param stringBuilder the StringBuilder to append this instance to. * @param extended Whether or not extended info should be included, if available. * @return the given StringBuilder instance. * @throws NullPointerException if stringBuilder is null. */ public StringBuilder appendTo(StringBuilder stringBuilder, boolean extended) { Objects.requireNonNull(stringBuilder, "stringBuilder must not be null!"); boolean separatorRequired = false; if(classLoaderName != null && !classLoaderName.isEmpty()) { stringBuilder.append(classLoaderName).append(MODULE_SEPARATOR_CHAR); separatorRequired = true; } if(moduleName != null && !moduleName.isEmpty()) { stringBuilder.append(moduleName); if(moduleVersion != null && !moduleVersion.isEmpty()) { stringBuilder.append(MODULE_VERSION_SEPARATOR_CHAR).append(moduleVersion); } separatorRequired = true; } if(separatorRequired) { stringBuilder.append(MODULE_SEPARATOR_CHAR); } stringBuilder.append(className).append(".").append(methodName); if(isNativeMethod()) { stringBuilder.append("(").append(NATIVE_METHOD_STRING).append(")"); } else if(fileName != null) { stringBuilder.append("(").append(fileName); if(lineNumber >= 0) { stringBuilder.append(SEPARATOR_CHAR).append(lineNumber); } stringBuilder.append(")"); } else { stringBuilder.append("(").append(UNKNOWN_SOURCE_STRING).append(")"); } if(extended) { if(codeLocation != null || version != null) { stringBuilder.append(' '); appendExtended(stringBuilder); } } return stringBuilder; } public static ExtendedStackTraceElement parseStackTraceElement(final String ste) { if(ste == null) { return null; } int idx = ste.lastIndexOf('('); if(idx < 0) { return null; // invalid } int endIdx = ste.lastIndexOf(')'); if(endIdx < 0) { return null; // invalid } String classAndMethod = ste.substring(0, idx); String source = ste.substring(idx + 1, endIdx); idx = classAndMethod.lastIndexOf('.'); if(idx < 0) { return null; // invalid } String clazz = classAndMethod.substring(0, idx); String method = classAndMethod.substring(idx + 1, classAndMethod.length()); String classLoaderName = null; String moduleName = null; String moduleVersion = null; idx = clazz.lastIndexOf(MODULE_SEPARATOR_CHAR); if(idx > -1) { String loaderModule = clazz.substring(0, idx); clazz = clazz.substring(idx + 1); idx = loaderModule.indexOf(MODULE_SEPARATOR_CHAR); if(idx > -1) { classLoaderName = loaderModule.substring(0, idx); moduleName = loaderModule.substring(idx + 1); } else { moduleName = loaderModule; } idx = moduleName.indexOf(MODULE_VERSION_SEPARATOR_CHAR); if(idx > -1) { moduleVersion = moduleName.substring(idx + 1); moduleName = moduleName.substring(0, idx); } } if(classLoaderName != null && classLoaderName.isEmpty()) { classLoaderName = null; } if(moduleName != null && moduleName.isEmpty()) { moduleName = null; } if(moduleVersion != null && moduleVersion.isEmpty()) { moduleVersion = null; } idx = source.lastIndexOf(SEPARATOR_CHAR); String file = null; int lineNumber = UNKNOWN_SOURCE_LINE_NUMBER; if(idx != -1) { file = source.substring(0, idx); String numberString = source.substring(idx + 1, source.length()); try { lineNumber = Integer.parseInt(numberString); } catch(NumberFormatException ex) { return null; // invalid } } else { if(source.equals(NATIVE_METHOD_STRING)) { lineNumber = ExtendedStackTraceElement.NATIVE_METHOD_LINE_NUMBER; } else if(!source.equals(UNKNOWN_SOURCE_STRING)) { file = source; } } ExtendedStackTraceElement result = null; if(endIdx + 2 < ste.length()) { String remainder = ste.substring(endIdx + 2); int vEndIdx = remainder.lastIndexOf(']'); if(vEndIdx >= 0) { boolean exact = false; String versionStr = null; if (remainder.startsWith(EXTENDED_EXACT_PREFIX)) { exact = true; versionStr = remainder.substring(EXTENDED_EXACT_PREFIX.length(), vEndIdx); } else if (remainder.startsWith(EXTENDED_INEXACT_PREFIX)) { exact = false; versionStr = remainder.substring(EXTENDED_INEXACT_PREFIX.length(), vEndIdx); } if (versionStr != null) { int colonIdx = versionStr.indexOf(SEPARATOR_CHAR); if (colonIdx > -1) { String codeLocation = versionStr.substring(0, colonIdx); String version = versionStr.substring(colonIdx + 1); if (codeLocation.isEmpty() || NA_PLACEHOLDER.equals(codeLocation)) { codeLocation = null; } if (version.isEmpty() || NA_PLACEHOLDER.equals(version)) { version = null; } result = new ExtendedStackTraceElement(clazz, method, file, lineNumber, codeLocation, version, exact); } } } } if(result == null) { result = new ExtendedStackTraceElement(clazz, method, file, lineNumber); } result.setClassLoaderName(classLoaderName); result.setModuleName(moduleName); result.setModuleVersion(moduleVersion); return result; } private static final Method GET_CLASS_LOADER_NAME; private static final Method GET_MODULE_NAME; private static final Method GET_MODULE_VERSION; private static final Constructor<StackTraceElement> FULL_CTOR; static { { Method method = null; try { method = StackTraceElement.class.getMethod("getClassLoaderNameFrom"); } catch (NoSuchMethodException e) { // ignore } GET_CLASS_LOADER_NAME = method; } { Method method = null; try { method = StackTraceElement.class.getMethod("getModuleName"); } catch (NoSuchMethodException e) { // ignore } GET_MODULE_NAME = method; } { Method method = null; try { method = StackTraceElement.class.getMethod("getModuleVersion"); } catch (NoSuchMethodException e) { // ignore } GET_MODULE_VERSION = method; } { Constructor<StackTraceElement> ctor = null; try { ctor = StackTraceElement.class.getConstructor( String.class, // classLoaderName String.class, // moduleName String.class, // moduleVersion String.class, // declaringClass String.class, // methodName String.class, // fileName int.class // lineNumber ); } catch (NoSuchMethodException e) { // ignore } FULL_CTOR = ctor; } } private static String getClassLoaderNameFrom(StackTraceElement ste) { return getValueFrom(GET_CLASS_LOADER_NAME, ste); } private static String getModuleNameFrom(StackTraceElement ste) { return getValueFrom(GET_MODULE_NAME, ste); } private static String getModuleVersionFrom(StackTraceElement ste) { return getValueFrom(GET_MODULE_VERSION, ste); } private static String getValueFrom(Method method, StackTraceElement ste) { if(method == null) { return null; } try { String result = (String) method.invoke(ste); if(result != null && !result.isEmpty()) { return result; } } catch (IllegalAccessException | InvocationTargetException e) { // ignore } return null; } private static StackTraceElement createStackTraceElement( String classLoaderName, String moduleName, String moduleVersion, String declaringClass, String methodName, String fileName, int lineNumber) { if(FULL_CTOR != null) { try { return FULL_CTOR.newInstance( classLoaderName, moduleName, moduleVersion, declaringClass, methodName, fileName, lineNumber ); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { // ignore } } return new StackTraceElement(declaringClass, methodName, fileName, lineNumber); } }