/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2018 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.glassfish.pfl.test ;

import org.glassfish.pfl.basic.reflection.FieldValueHelper;

import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.Permission;
import java.security.ProtectionDomain;
import java.security.CodeSource;
import java.security.Principal;
import java.security.PermissionCollection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.Map.Entry;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.Properties;
import java.util.IdentityHashMap;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger ;
import java.math.BigDecimal ;

/** General object related utilities.  This class must not depend on
 * generated log wrappers.
 */
public final class ObjectUtility {
    private boolean useToString ;
    private boolean isIndenting ;
    private int initialLevel ;
    private int increment ;
    private ClassMap classToPrinter = new ClassMap() ;

    private static ObjectUtility standard = new ObjectUtility( false, true, 
	0, 4 ) ;
    private static ObjectUtility compact = new ObjectUtility( true, false, 
	0, 4 ) ;

    private ObjectUtility( boolean useToString, boolean isIndenting, 
	int initialLevel, int increment )
    {
	this.useToString = useToString ;
	this.isIndenting = isIndenting ;
	this.initialLevel = initialLevel ;
	this.increment = increment ;
	classToPrinter.put( Properties.class, propertiesPrinter ) ;
	classToPrinter.put( Collection.class, collectionPrinter ) ;
	classToPrinter.put( Map.class, mapPrinter ) ;
    }

    /** Construct an Utility instance with the desired objectToString
    * behavior.
    */
    public static ObjectUtility make( boolean useToString, boolean isIndenting, 
	int initialLevel, int increment )
    {
	return new ObjectUtility( useToString, isIndenting, initialLevel, 
	    increment ) ;
    }

    /** Construct an Utility instance with the desired objectToString
    * behavior.
    */
    public static ObjectUtility make( boolean useToString, boolean isIndenting )
    {
	return new ObjectUtility( useToString, isIndenting, 0, 4 ) ;
    }

    /** Get the standard Utility object that supports objectToString with
    * indented display and no use of toString() methods.
    */
    public static ObjectUtility make()
    {
	return standard ;
    }
    
    /** A convenience method that gives the default behavior: use indenting
    * to display the object's structure and do not use built-in toString
    * methods.
    */
    public static String defaultObjectToString( java.lang.Object object )
    {
	return standard.objectToString( object ) ;
    }

    public static String compactObjectToString( java.lang.Object object )
    {
	return compact.objectToString( object ) ;
    }

    /** objectToString handles display of arbitrary objects.  It correctly
    * handles objects whose elements form an arbitrary graph.  It uses
    * reflection to display the contents of any kind of object. 
    * An object's toString() method may optionally be used, but the default
    * is to ignore all toString() methods except for those defined for
    * primitive types, primitive type wrappers, and strings.
    */
    public String objectToString(java.lang.Object obj)
    {
	IdentityHashMap printed = new IdentityHashMap() ;
	ObjectWriter result = ObjectWriter.make( isIndenting, initialLevel, 
	    increment ) ;
	objectToStringHelper( printed, result, obj ) ;
	return result.toString() ;
    }

    // Perform a deep structural equality comparison of the two objects.
    // This handles all arrays, maps, and sets specially, otherwise
    // it just calls the object's equals() method.
    public static boolean equals( java.lang.Object obj1, java.lang.Object obj2 )
    {
	// Set of pairs of objects that have been (or are being) considered for
	// equality.  Such pairs are presumed to be equals.  If they are not, 
	// this will be detected eventually and the equals method will return 
	// false.
	Set considered = new HashSet() ;    

	// Map that gives the corresponding component of obj2 for a component 
	// of obj1.  This is used to check for the same aliasing and use of 
	// equal objects in both objects.
	Map counterpart = new IdentityHashMap() ;

	return equalsHelper( counterpart, considered, obj1, obj2 ) ;
    }

    /** If arr1 and arr2 are both arrays of the same component type,
     * return an array of that component type that consists of the
     * elements of arr1 followed by the elements of arr2.
     * Throws IllegalArgumentException otherwise.
     */
    public static Object concatenateArrays( Object arr1, Object arr2 )
    {
	Class comp1 = arr1.getClass().getComponentType() ;
	Class comp2 = arr2.getClass().getComponentType() ;
	int len1 = Array.getLength( arr1 ) ;
	int len2 = Array.getLength( arr2 ) ;

	if ((comp1 == null) || (comp2 == null))	
	    throw new IllegalStateException( "Arguments must be arrays" ) ;
	if (!comp1.equals( comp2 ))
	    throw new IllegalStateException( 
		"Arguments must be arrays with the same component type" ) ;

	Object result = Array.newInstance( comp1, len1 + len2 ) ;

	int index = 0 ;

	for (int ctr=0; ctr<len1; ctr++)
	    Array.set( result, index++, Array.get( arr1, ctr ) ) ;

	for (int ctr=0; ctr<len2; ctr++)
	    Array.set( result, index++, Array.get( arr2, ctr ) ) ;

	return result ;
    }

//===========================================================================
//  Implementation
//===========================================================================

    private void objectToStringHelper( IdentityHashMap printed, 
	ObjectWriter result, java.lang.Object obj)
    {
	if (obj==null) {
	    result.append( "null" ) ;
	    result.endElement() ;
	} else {
	    Class cls = obj.getClass() ;
	    result.startObject( obj ) ;

	    if (printed.keySet().contains( obj )) {
		result.endObject( "*VISITED*" ) ;
	    } else {
		printed.put( obj, null ) ;

		if (mustUseToString(cls)) {
		    result.endObject( obj.toString() ) ;
		} else {
		    // First, handle any classes that have special printer 
		    // methods defined.  This is useful when the class 
		    // overrides toString with something that
		    // is not sufficiently detailed.
		    ObjectPrinter printer = (ObjectPrinter)(classToPrinter.get(
			cls )) ;
		    if (printer != null) {
			printer.print( printed, result, obj ) ;
			result.endObject() ;
		    } else {
			Class compClass = cls.getComponentType() ;

			if (compClass == null)
			    // handleObject always calls endObject
			    handleObject( printed, result, obj ) ;
			else {
			    handleArray( printed, result, obj ) ;
			    result.endObject() ;
			}
		    }
		}
	    }
	}
    }

    private static interface ObjectPrinter {
	void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj ) ;
    }

    private ObjectPrinter propertiesPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Properties)) 
		throw new Error() ;

	    Properties props = (Properties)obj ;
	    Enumeration keys = props.propertyNames() ;
	    while (keys.hasMoreElements()) {
		String key = (String)(keys.nextElement()) ;
		String value = props.getProperty( key ) ;
		buff.startElement() ;
		buff.append( key ) ;
		buff.append( "=" ) ;
		buff.append( value ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private ObjectPrinter collectionPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Collection)) 
		throw new Error() ;
	    
	    Collection coll = (Collection)obj ;
	    Iterator iter = coll.iterator() ;
	    while (iter.hasNext()) {
		java.lang.Object element = iter.next() ;
		buff.startElement() ;
		objectToStringHelper( printed, buff, element ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private ObjectPrinter mapPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Map)) 
		throw new Error() ;

	    Map map = (Map)obj ;
	    Iterator iter = map.entrySet().iterator() ;
	    while (iter.hasNext()) {
		Entry entry = (Entry)(iter.next()) ;
		buff.startElement() ;
		objectToStringHelper( printed, buff, entry.getKey() ) ;
		buff.append( "=>" ) ;
		objectToStringHelper( printed, buff, entry.getValue() ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private static class ClassMap {
	ArrayList data ;

	public ClassMap()
	{
	    data = new ArrayList() ;
	}

	/** Return the first element of the ClassMap that is assignable to cls.
	* The order is determined by the order in which the put method was 
	* called.  Returns null if there is no match.
	*/
	public java.lang.Object get( Class cls )
	{
	    Iterator iter = data.iterator() ;
	    while (iter.hasNext()) {
		java.lang.Object[] arr = (java.lang.Object[])(iter.next()) ;
		Class key = (Class)(arr[0]) ;
		if (key.isAssignableFrom( cls ))
		    return arr[1] ;
	    }

	    return null ;
	}

	/** Add obj to the map with key cls.  Note that order matters,
	 * as the first match is returned.
	 */
	public void put( Class cls, java.lang.Object obj )
	{
	    java.lang.Object[] pair = { cls, obj } ;
	    data.add( pair ) ;
	}
    }

    private boolean mustUseToString( Class cls )
    {
	// These probably never occur
	if (cls.isPrimitive())
	    return true ;

	// We must use toString for all primitive wrappers, since 
	// otherwise the code recurses endlessly (access value field
	// inside Integer, returns another Integer through reflection).
	if ((cls == Integer.class) ||
	    (cls == BigInteger.class) ||
	    (cls == BigDecimal.class) ||
	    (cls == String.class) ||
	    (cls == StringBuffer.class) ||
	    (cls == Long.class) ||
	    (cls == Short.class) ||
	    (cls == Byte.class) ||
	    (cls == Character.class) ||
	    (cls == Float.class) ||
	    (cls == Double.class) ||
	    (cls == Boolean.class))
	    return true ;

	if (useToString) {
	    try {
		cls.getDeclaredMethod( "toString" ) ;
		return true ;
	    } catch (Exception exc) {
		return false ;
	    }
	}

	return false ;
    }
    
    // Determine whether or not the package of class cls is 
    // accessible.  Handlers arrays as well as normal classes.
    private void checkPackageAccess( Class cls ) {
	SecurityManager sm = System.getSecurityManager() ;
	if (sm != null) {
	    String cname = cls.getName().replace( '/', '.' ) ;
	    if (cname.startsWith( "[" )) {
		final int lastBracket = cname.lastIndexOf( "[" ) + 2 ;
		if (lastBracket > 1 && lastBracket < cname.length()) {
		    cname = cname.substring( lastBracket ) ;
		}

		final int lastDot = cname.lastIndexOf( '.' ) ;
		if (lastDot != -1) {
		    final String pname = cname.substring( 0, lastDot ) ;
		    sm.checkPackageAccess( pname ) ;
		}
	    }
	}
    }

    private Field[] getDeclaredFields( Class cls ) {
	checkPackageAccess( cls ) ;
	return cls.getDeclaredFields() ;
    }

    private void handleObject( IdentityHashMap printed, ObjectWriter result,
	java.lang.Object obj ) 
    {
	Class cls = obj.getClass() ;

	try {
	    Field[] fields;  
	    // If the security manager is not null, throw a security exception
	    // if cls is NOT accessible from the caller.
	    SecurityManager security = System.getSecurityManager();  
	    if (security != null && !Modifier.isPublic(cls.getModifiers())) {  
		fields = new Field[0];  
	    } else {  
		fields = getDeclaredFields(cls);  
            }  

	    for (int ctr=0; ctr<fields.length; ctr++ ) {
		final Field fld = fields[ctr] ;
		int modifiers = fld.getModifiers() ;

		// Do not display field if it is static, since these fields
		// are always the same for every instances.  This could
		// be made configurable, but I don't think it is 
		// useful to do so.
		if (!Modifier.isStatic( modifiers )) {
		    if (security != null) {
			if (!Modifier.isPublic(modifiers))
			    continue;
                    }
		    result.startElement() ;
		    result.append( fld.getName() ) ;
		    result.append( ":" ) ;

                    // Make sure that we can read the field if it is
                    // not public
                    AccessController.doPrivileged( new PrivilegedAction() {
                        public Object run() {
                            fld.setAccessible( true ) ;
                            return null ;
                        }
                    } ) ;

                    java.lang.Object value = fld.get( obj ) ;
                    objectToStringHelper( printed, result, value ) ;

		    result.endElement() ;
		}
	    }

	    result.endObject() ;
	} catch (Exception exc2) {
	    result.endObject( obj.toString() ) ;
	}
    }

    private void handleArray( IdentityHashMap printed, ObjectWriter result,
	java.lang.Object obj ) 
    {
	Class compClass = obj.getClass().getComponentType() ;
	if (compClass == boolean.class) {
	    boolean[] arr = (boolean[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == byte.class) {
	    byte[] arr = (byte[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == short.class) {
	    short[] arr = (short[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == int.class) {
	    int[] arr = (int[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == long.class) {
	    long[] arr = (long[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == char.class) {
	    char[] arr = (char[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == float.class) {
	    float[] arr = (float[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == double.class) {
	    double[] arr = (double[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else { // array of object
	    java.lang.Object[] arr = (java.lang.Object[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		objectToStringHelper( printed, result, arr[ctr] ) ;
		result.endElement() ;
	    }
	}
    }

    private static class Pair 
    {
	private java.lang.Object obj1 ;
	private java.lang.Object obj2 ;

	Pair( java.lang.Object obj1, java.lang.Object obj2 )
	{
	    this.obj1 = obj1 ;
	    this.obj2 = obj2 ;
	}

	public boolean equals( java.lang.Object obj )
	{
	    if (!(obj instanceof Pair))
		return false ;

	    Pair other = (Pair)obj ;
	    return other.obj1 == obj1 && other.obj2 == obj2 ;
	}

	public int hashCode()
	{
	    return System.identityHashCode( obj1 ) ^ 
		System.identityHashCode( obj2 ) ;
	}
    }

    private static boolean equalsHelper( Map counterpart, Set considered, 
	java.lang.Object obj1, java.lang.Object obj2 )
    {
	if ((obj1 == null) || (obj2 == null))
	    return obj1 == obj2 ;

	java.lang.Object other2 = counterpart.get( obj1 ) ;
	if (other2 == null) {
	    other2 = obj2 ;
	    counterpart.put( obj1, other2 ) ;
	}

	if (obj1 == other2)
	    return true ;

	if (obj2 != other2)
	    return false ;
	
	Pair pair = new Pair( obj1, obj2 ) ;
	if (considered.contains( pair ))
	    return true ;
	else
	    considered.add( pair ) ;

	if (obj1 instanceof java.lang.Object[] && 
	    obj2 instanceof java.lang.Object[])
	    return equalArrays( counterpart, considered, 
		(java.lang.Object[])obj1, (java.lang.Object[])obj2 ) ;
	else if (obj1 instanceof Map && obj2 instanceof Map)
	    return equalMaps( counterpart, considered, 
		(Map)obj1, (Map)obj2 ) ;
	else if (obj1 instanceof Set && obj2 instanceof Set)
	    return equalSets( counterpart, considered, 
		(Set)obj1, (Set)obj2 ) ;
	else if (obj1 instanceof List && obj2 instanceof List)
	    return equalLists( counterpart, considered, 
		(List)obj1, (List)obj2 ) ;
	else if (obj1 instanceof boolean[] && obj2 instanceof boolean[])
	    return Arrays.equals( (boolean[])obj1, (boolean[])obj2 ) ;
	else if (obj1 instanceof byte[] && obj2 instanceof byte[])
	    return Arrays.equals( (byte[])obj1, (byte[])obj2 ) ;
	else if (obj1 instanceof char[] && obj2 instanceof char[])
	    return Arrays.equals( (char[])obj1, (char[])obj2 ) ;
	else if (obj1 instanceof double[] && obj2 instanceof double[])
	    return Arrays.equals( (double[])obj1, (double[])obj2 ) ;
	else if (obj1 instanceof float[] && obj2 instanceof float[])
	    return Arrays.equals( (float[])obj1, (float[])obj2 ) ;
	else if (obj1 instanceof int[] && obj2 instanceof int[])
	    return Arrays.equals( (int[])obj1, (int[])obj2 ) ;
	else if (obj1 instanceof long[] && obj2 instanceof long[])
	    return Arrays.equals( (long[])obj1, (long[])obj2 ) ;
	else {
	    Class cls = obj1.getClass() ;
	    if (cls != obj2.getClass())
		return obj1.equals( obj2 ) ;
	    else
		return equalsObject( counterpart, considered, cls, obj1, obj2 ) ;
	}
    }

    private static boolean equalsObject( Map counterpart, Set considered, 
	Class cls, java.lang.Object obj1, java.lang.Object obj2 ) 
    {
	Class objectClass = java.lang.Object.class ;
	if (cls == objectClass)
	    return true ;

	try {
	    Method equalsMethod = cls.getDeclaredMethod( "equals", 
		objectClass ) ;
	    return obj1.equals( obj2 ) ;
	} catch (Exception exc) {
	    if (equalsObjectFields( counterpart, considered, 
		    cls, obj1, obj2 ))
		return equalsObject( counterpart, considered, 
		    cls.getSuperclass(), obj1, obj2 ) ;
	    else
		return false ;
	}
    }

	private static boolean equalsObjectFields(Map counterpart, Set considered, Class cls, Object obj1, Object obj2) {
		Field[] fields = cls.getDeclaredFields();
		for (Field field : fields) {
			try {
				if (!Modifier.isStatic(field.getModifiers())) { // Ignore static fields
					Object value1 = FieldValueHelper.getFieldValue(obj1, field);
					Object value2 = FieldValueHelper.getFieldValue(obj2, field);
					if (!equalsHelper(counterpart, considered, value1, value2)) return false;
				}
			} catch (IllegalAccessException exc) {
				return false;
			}
		}

		return true;
	}

	private static boolean equalArrays(Map counterpart, Set considered,
									   java.lang.Object[] arr1, java.lang.Object[] arr2) {
		int len = arr1.length;
		if (len != arr2.length)
			return false;

		for (int ctr = 0; ctr < len; ctr++)
			if (!equalsHelper(counterpart, considered, arr1[ctr], arr2[ctr]))
				return false;

		return true;
	}

	private static boolean equalMaps(Map counterpart, Set considered, Map map1, Map map2) {
		if (map2.size() != map1.size())
			return false;

		try {
			for (Object o : map1.entrySet()) {
				Entry e = (Entry) o;
				Object key = e.getKey();
				Object value = e.getValue();
				if (value == null) {
					if (!(map2.get(key) == null && map2.containsKey(key)))
						return false;
				} else {
					if (!equalsHelper(counterpart, considered, value, map2.get(key)))
						return false;
				}
			}
		} catch (ClassCastException | NullPointerException unused) {
			return false;
		}

		return true;
	}

    // Obviously this is an inefficient quadratic algorithm.
    // This is taken pretty directly from AbstractSet and AbstractCollection
    // in the JDK.
    // For HashSet, an O(n) (with a good hash function) algorithm
    // is possible, and likewise TreeSet, since it is 
    // ordered, is O(n).  But this is not worth the effort here.
    // Note that the inner loop uses equals, not equalsHelper.
    // This is needed because of the searching behavior of this test.
    // However, note that this will NOT correctly handle sets that
    // contain themselves as members, or that have members that reference
    // themselves.  These cases will cause infinite regress!
    private static boolean equalSets( Map counterpart, Set considered,
	Set set1, Set set2 )
    {
	if (set1.size() != set2.size())
	    return false ;

	Iterator e1 = set1.iterator() ;
	while (e1.hasNext()) {
	    java.lang.Object obj1 = e1.next() ;

	    boolean found = false ;
	    Iterator e2 = set2.iterator() ;
	    while (e2.hasNext() && !found) {
		java.lang.Object obj2 = e2.next() ;
		found = equals( obj1, obj2 ) ;
	    }

	    if (!found)
		return false ;
	}

	return true ;
    }

    private static boolean equalLists( Map counterpart, Set considered,
	List list1, List list2 )
    {
	ListIterator e1 = list1.listIterator();
	ListIterator e2 = list2.listIterator();
	while(e1.hasNext() && e2.hasNext()) {
	    java.lang.Object o1 = e1.next();
	    java.lang.Object o2 = e2.next();
	    if (!(o1==null ? o2==null : equalsHelper( 
		counterpart, considered, o1, o2)))
		return false;
	}
	return !(e1.hasNext() || e2.hasNext());
    }

    private static void sop( String msg ) {
	System.out.println( msg ) ;
    }

    public static void displayException( Throwable thr ) {
	Throwable current = thr ;
	while (current != null) {
	    if (current != thr) {
		sop( "Caused By:" ) ;
	    }

	    StackTraceElement[] elems = thr.getStackTrace() ;
	    if (elems == null) {
		sop( "\tNo stack trace available" ) ;
	    } else {
		for (final StackTraceElement elem : elems) {
		    sop( "\t" + elem ) ;
		    try {
			AccessController.doPrivileged( 
			    new PrivilegedAction() {
				public Object run() {
				    try {
					Class<?> cls = Class.forName( elem.getClassName() ) ;
					ClassLoader loader = cls.getClassLoader() ;
					// display ClassLoader
					sop( "\t\tClassLoader = " + loader ) ;
					
					ProtectionDomain pd = cls.getProtectionDomain() ;
					CodeSource cs = pd.getCodeSource() ;
					// display CodeSource
					sop( "\t\tCodeSource contents:" ) ;
					// Don't need Certificate[]
					// Don't need CodeSigner[]
					sop( "\t\t\tLocation = " + cs.getLocation() ) ;

					PermissionCollection pcoll = pd.getPermissions() ;
					// display Permissions
					sop( "\t\t\tPermissions:" ) ;
					Enumeration<Permission> perms = pcoll.elements() ;
					while (perms.hasMoreElements()) {
					    Permission perm = perms.nextElement() ;
					    sop( "\t\t\t\t" + perm ) ;
					}
					
					Principal[] principals = pd.getPrincipals() ;
					// display Principals
					sop( "\t\t\tPrincipals:" ) ;
					for (Principal p : principals) {
					    sop( "\t\t\t\t" + p ) ;
					}
				    } catch (Exception exc) {
					sop( "\t\tCould not load class " + elem.getClassName() ) ;
				    }

				    return null ;
				} 
			    } 
			) ;
		    } catch (Exception exc) {
			sop( "\t\tError: could not get information for stack trace element" ) ;
		    }
		}
	    }

	    current = current.getCause() ;
	}
    }

    private static void a() {
	b() ;
    }

    private static void b() {
	c() ;
    }

    private static void c() {
	d() ;
    }

    private static void d() {
	e() ;
    }

    private static void e() {
	throw new RuntimeException() ;
    }

    public static void main( String[] args ) {
	try {
	    a() ;
	} catch (Exception exc) {
	    displayException( exc ) ;
	}
    }
}
