/*
 * 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.log4j;

import org.apache.log4j.spi.ThrowableRenderer;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.CodeSource;
import java.util.HashMap;
import java.util.Map;

/**
 * Enhanced implementation of ThrowableRenderer. Uses Throwable.getStackTrace if
 * running on JDK 1.4 or later and delegates to DefaultThrowableRenderer.render
 * on earlier virtual machines.
 *
 * @since 1.2.16
 */
public final class EnhancedThrowableRenderer implements ThrowableRenderer {
    /**
     * Throwable.getStackTrace() method.
     */
    private Method getStackTraceMethod;
    /**
     * StackTraceElement.getClassName() method.
     */
    private Method getClassNameMethod;

    /**
     * Construct new instance.
     */
    public EnhancedThrowableRenderer() {
	try {
	    Class[] noArgs = null;
	    getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs);
	    Class ste = Class.forName("java.lang.StackTraceElement");
	    getClassNameMethod = ste.getMethod("getClassName", noArgs);
	} catch (Exception ex) {
	}
    }

    /**
     * {@inheritDoc}
     */
    public String[] doRender(final Throwable throwable) {
	if (getStackTraceMethod != null) {
	    try {
		Object[] noArgs = null;
		Object[] elements = (Object[]) getStackTraceMethod.invoke(throwable, noArgs);
		String[] lines = new String[elements.length + 1];
		lines[0] = throwable.toString();
		Map classMap = new HashMap();
		for (int i = 0; i < elements.length; i++) {
		    lines[i + 1] = formatElement(elements[i], classMap);
		}
		return lines;
	    } catch (Exception ex) {
	    }
	}
	return DefaultThrowableRenderer.render(throwable);
    }

    /**
     * Format one element from stack trace.
     * 
     * @param element  element, may not be null.
     * @param classMap map of class name to location.
     * @return string representation of element.
     */
    private String formatElement(final Object element, final Map classMap) {
	StringBuilder buf = new StringBuilder("\tat ");
	buf.append(element);
	try {
	    String className = getClassNameMethod.invoke(element, (Object[]) null).toString();
	    Object classDetails = classMap.get(className);
	    if (classDetails != null) {
		buf.append(classDetails);
	    } else {
		Class cls = findClass(className);
		int detailStart = buf.length();
		buf.append('[');
		try {
		    CodeSource source = cls.getProtectionDomain().getCodeSource();
		    if (source != null) {
			URL locationURL = source.getLocation();
			if (locationURL != null) {
			    //
			    // if a file: URL
			    //
			    if ("file".equals(locationURL.getProtocol())) {
				String path = locationURL.getPath();
				if (path != null) {
				    //
				    // find the last file separator character
				    //
				    int lastSlash = path.lastIndexOf('/');
				    int lastBack = path.lastIndexOf(File.separatorChar);
				    if (lastBack > lastSlash) {
					lastSlash = lastBack;
				    }
				    //
				    // if no separator or ends with separator (a directory)
				    // then output the URL, otherwise just the file name.
				    //
				    if (lastSlash <= 0 || lastSlash == path.length() - 1) {
					buf.append(locationURL);
				    } else {
					buf.append(path.substring(lastSlash + 1));
				    }
				}
			    } else {
				buf.append(locationURL);
			    }
			}
		    }
		} catch (SecurityException ex) {
		}
		buf.append(':');
		Package pkg = cls.getPackage();
		if (pkg != null) {
		    String implVersion = pkg.getImplementationVersion();
		    if (implVersion != null) {
			buf.append(implVersion);
		    }
		}
		buf.append(']');
		classMap.put(className, buf.substring(detailStart));
	    }
	} catch (Exception ex) {
	}
	return buf.toString();
    }

    /**
     * Find class given class name.
     * 
     * @param className class name, may not be null.
     * @return class, will not be null.
     * @throws ClassNotFoundException thrown if class can not be found.
     */
    private Class findClass(final String className) throws ClassNotFoundException {
	try {
	    return Thread.currentThread().getContextClassLoader().loadClass(className);
	} catch (ClassNotFoundException e) {
	    try {
		return Class.forName(className);
	    } catch (ClassNotFoundException e1) {
		return getClass().getClassLoader().loadClass(className);
	    }
	}
    }

}
