/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.profiler.jvmti;


import gnu.trove.TLongObjectHashMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
import java.util.WeakHashMap;

import org.jboss.logging.Logger;



/**
 * @author Clebert Suconic
 */
public class JVMTIInterface implements JVMTIInterfaceMBean
{
    private static final Logger log = Logger.getLogger(JVMTIInterface.class);
    
	private static boolean isLoaded=true;
    static {
        try {
            System.loadLibrary("jbossAgent");
        }
        catch (Throwable e) {
        	isLoaded=false;
            log.error("The DLL couldn't be loaded, you won't be able to use any JVMTIInterface feature",e);
        }
    }


    private void addTo(Collection coll, Field[] obj)
    {
        for (int i=0;i<obj.length;i++)
        {
            coll.add(obj[i]);
            obj[i].setAccessible(true);
        }
    }

    public Field[] retrieveAllFields(Class clazz)
    {

        ArrayList list = new ArrayList();



        for (Class superClass = clazz;superClass!=null;superClass=superClass.getSuperclass())
        {
            addTo(list,superClass.getDeclaredFields());
        }

        return  (Field[]) list.toArray(new Field[list.size()]);

    }

    /** Force a GC. This method doesn't use System.gc. If JVMTI is enabled this will really cause a FullGC by calling a JVMTI function. */
    public native void forceGC();

    /** returns the first class found with a given name */
    public Class getClassByName(String className)
    {
    	Class classes[] = getLoadedClasses();
    	
    	for (int i=0;i<classes.length;i++)
    	{
	    	if (classes[i].getName().equals(className))
	    	{
	    		return classes[i];
	    	}
    	}
    	
    	return null;
    }

    /** Will release internal tags used by previous methods. 
     * All the navigations through JVMTI are done through tagging. Calling this method will release any tagging done by previous methods. */
    public native void releaseTags();
    
    /** This method will keep every object tagged for later usage. This method is going to tag objects. */
    public native void notifyInventory(boolean notifyOnClasses, String temporaryFileReferences,  String temporaryFileObjects, JVMTICallBack callback);
    
    public void notifyOnReferences(String temporaryFile, JVMTICallBack callback)
    {
    	notifyInventory(false,temporaryFile,null,callback);
    }
    
    /** Will get all the objects holding references to these objects passed by parameter. This method is going to release tags, be careful if you are on the middle of navigations. */
    public native Object[] getReferenceHolders(Object [] objects);
    public native Class[] getLoadedClasses();
    /** Will return all methods of a give class. This will change tags, be careful if you are on the middle of navitations. */
    public native Object[] getAllObjects(Class clazz);
    /** Will return the tag of an object */
    public native long getTagOnObject(Object obj);
    /** Will return the object on a tag*/
    public native Object getObjectOnTag(long tag);
    private native boolean internalIsConfiguredProperly();
    
    /** Returns true if -agentlib:jbossAgent was configured properly */
    public boolean isActive()
    {
    	if (!isLoaded)
    	{
    		return false;
    	}
    	else
    	{
    		return internalIsConfiguredProperly();
    	}
    }
    
    /** Returns the field represted by the FieldId. This is used on field relationships according to the rule determined by JVMTI documentation. */
    public Field getObjectField(Class clazz, int fieldId)
    {
    	ArrayList list = new ArrayList();
    	list.add(clazz);
    	while ((clazz = clazz.getSuperclass())!=null)
    	{
    		list.add(clazz);
    	}
    	
    	for (int i=list.size()-1;i>=0;i--)
    	{
    		Field fields[] = ((Class)list.get(i)).getDeclaredFields();
    		if (fieldId<fields.length)
    		{
    			return fields[fieldId];
    		}
    		fieldId -= fields.length;
    	}
    	return null;
    }
    
    public native String getMethodName(long methodId);
    public native String getMethodSignature(long methodId);
    public native Class getMethodClass(long methodId);
    
    

    /** @deprecated use getLoadedClasses */
    public Class[] retrieveLoadedClasses()
    {
        return getLoadedClasses();
    }

    protected static native void heapSnapshot(String classesFileName, String referencesFileName, String objectsFileName);

    /** Will call {@link JVMTIInterface.heapSnapshot(String,String,String)} passing "_classes, _references, _objects in the name of the files */
    public void heapSnapshot(String basicFileName, String suffix) {
        forceGC();
    	heapSnapshot(basicFileName + "_classes"+ "." + suffix,
    			basicFileName + "_references" + "." + suffix,
				basicFileName + "_objects" + "." + suffix);
    }

    /** Return every single object on a give class by its name. This method will look for every single class with this name, and if more than one classLoader is loading a class with this name, this method will return objects for all the respective classes.
     *  For example if you look for a Structs Action Form, this will return every ActionForm defined on the current JVM. */
    public Object[] getAllObjects(String clazz) {
        ArrayList list = new ArrayList();

        Class [] classes = this.getLoadedClasses();
        for (int i=0;i<classes.length;i++)
        {
            if (classes[i].getName().equals(clazz))
            {
                Object objs[] = this.getAllObjects(classes[i]);
                for (int count=0;count<objs.length;count++)
                {
                    list.add(objs[count]);
                }
            }
        }


        return list.toArray();
    }
    
    static class ClassSorterByClassLoader implements Comparator
    {

		public int compare(Object o1, Object o2)
		{
			Class left = (Class)o1;
			Class right = (Class)o2;
			
			int compare=0;
			
			if ((compare=compareClassLoader(left.getClassLoader(),right.getClassLoader()))!=0) return compare;
			
			return left.getName().compareTo(right.getName());
		}
		
		public int compareClassLoader(ClassLoader left, ClassLoader right)
		{
			if (left==null || right==null)
			{
				if (left==right)
				{
					return 0;
				}
				else if (left==null)
				{
					return -1;
				}
				else 
				{
					return 1;
				}
			}
			else
			{
				return left.toString().compareTo(right.toString());
			}
		}
    	
    }
    
    static class ClassSorterByClassName implements Comparator
    {

		public int compare(Object o1, Object o2)
		{
			Class left = (Class)o1;
			Class right = (Class)o2;
			int compare = left.getName().compareTo(right.getName());
			/*if (compare==0)
			{
				if (o1==o2) 
				{
					return 0;
				}
				else
				{
					return 1;
				}
			}
			else
			{
				return compare;
			}*/
			return compare;
		}
    }
    
    private String callToString(Object obj, boolean callToString)
    {
    	
    	try
    	{
    		if(obj==null)
    		{
    			return "null";
    		}
    		else
    		{
    			if (callToString)
    			{
    				return obj.toString();
    			}
    			else
    			{
    				if (obj instanceof Class)
    				{
    					return obj.toString();
    				}
    				else
    				{
    					return  obj.getClass().getName() + "@" + System.identityHashCode(obj);
    				}
    			}
    		}
    			
    	}
    	catch (Throwable e)
    	{
    		return obj.getClass().getName() + " toString had an Exception ";
    	}
    }
    
    /** Explore references recursevely */
    private void exploreObject(PrintWriter out, Object source,  int currentLevel, final int maxLevel,boolean useToString,  boolean weakAndSoft,
    		                                HashMap mapDataPoints,  
    		                                HashSet alreadyExplored)
    {
    	String level = null;
    	{
	    	StringBuffer levelStr = new StringBuffer();
	    	for (int i=0;i<=currentLevel;i++)
	    	{
	    		levelStr.append("!--");
	    	}
	    	level = levelStr.toString();
    	}
    	
    	if (maxLevel>=0 && currentLevel>=maxLevel)
    	{
    		out.println("<br>" + level + "<b>MaxLevel</b>");
    		return;
    	}
    	Integer index = new Integer(System.identityHashCode(source));
    	
    	if (alreadyExplored.contains(index))
    	{
            if (source instanceof Class)
            {
               out.println("<br>" + level + " object instanceOf " + source + "@" + index + " was already described before on this report");
            } else
            {
               out.println("<br>" + level + " object instanceOf " + source.getClass() + "@" + index + " was already described before on this report");
            }
    		return;
    	}
    	
    	alreadyExplored.add(index);
    	
    	
    	log.info("resolving references of " + callToString(source,useToString) + "...");
    	Long sourceTag = new Long(this.getTagOnObject(source));
    	ArrayList listPoints = (ArrayList)mapDataPoints.get(sourceTag);
    	if (listPoints ==null)
    	{
           log.info("didn't find references");
    		return;
    	}
    	
        log.info("References found");
    	
    	Iterator iter = listPoints.iterator();
    	
    	while (iter.hasNext())
    	{
    		ReferenceDataPoint point = (ReferenceDataPoint)iter.next();
    		
    		Object nextReference = treatReference(level, out, point, useToString);
    		
    		if (nextReference!=null && !weakAndSoft)
    		{
    			if (nextReference instanceof WeakReference || nextReference instanceof SoftReference)
    			{
    				nextReference=null;
    			}
    		}
    		
    		if (nextReference!=null)
    		{
    			exploreObject(out,nextReference,currentLevel+1,maxLevel,useToString,weakAndSoft,mapDataPoints,alreadyExplored);
    		}
    	}
    	
    }
    /** This is used by JSPs to have access to internal features formating results according to the navigations. That's the only reason this method is public. 
     * This is not intended to be used as part of the public API.  
     * @urlBaseToFollow will be concatenated objId=3> obj </a> to the outputStream*/
	public Object treatReference(String level, PrintWriter out, ReferenceDataPoint point, boolean useToString)
	{
		Object referenceHolder = null;
		if (point.getReferenceHolder()==0 || point.getReferenceHolder()==-1)
		{
			referenceHolder=null;
		}
		else
		{
			referenceHolder = this.getObjectOnTag(point.getReferenceHolder());
		}
		Object nextReference = null;
		switch (point.getReferenceType())
		{
		case JVMTICallBack.JVMTI_REFERENCE_CLASS:;// Reference from an object to its class.
		    out.print("<br>" + level + "InstanceOfReference:");
	    	out.println("ToString=" + callToString(referenceHolder,useToString));
	    	
		    
		    nextReference = referenceHolder;
		    break;
		case JVMTICallBack.JVMTI_REFERENCE_FIELD://Reference from an object to the value of one of its instance fields. For references of this kind the referrer_index parameter to the jvmtiObjectReferenceCallback is the index of the the instance field. The index is based on the order of all the object's fields. This includes all fields of the directly declared static and instance fields in the class, and includes all fields (both public and private) fields declared in superclasses and superinterfaces. The index is thus calculated by summing the index of field in the directly declared class (see GetClassFields), with the total number of fields (both public and private) declared in all superclasses and superinterfaces. The index starts at zero.
		{
			
			String fieldName = null;

			if (referenceHolder == null)
			{
				fieldName = "Reference GONE";
			}
			else
			{
				Class clazz = referenceHolder.getClass();
				Field field = this.getObjectField(clazz,(int)point.getIndex());
				if (field==null)
				{
					fieldName = "UndefinedField@" + referenceHolder;
				}
				else
				{
					fieldName = field.toString();
				}
			}
			out.print("<br>" + level + " FieldReference " + fieldName + "=" + callToString(referenceHolder,useToString));
			nextReference = referenceHolder;
			break;
		}
		case JVMTICallBack.JVMTI_REFERENCE_ARRAY_ELEMENT://Reference from an array to one of its elements. For references of this kind the referrer_index parameter to the jvmtiObjectReferenceCallback is the array index.
            if (referenceHolder==null)
            {
               out.println("<br>" + level +" arrayRef Position " + point.getIndex()  + " is gone");
            }
            else
            {
               out.println("<br>" + level +" arrayRef " + referenceHolder.getClass().getName() + "[" + point.getIndex() + "] id=@" + System.identityHashCode(referenceHolder) );
            }
            nextReference = referenceHolder;
			break;
		case JVMTICallBack.JVMTI_REFERENCE_CLASS_LOADER://	Reference from a class to its class loader.
			out.println("<br>" + level +"ClassLoaderReference @ " + callToString(referenceHolder,useToString));
			nextReference = referenceHolder;
			break;
		case JVMTICallBack.JVMTI_REFERENCE_SIGNERS://Reference from a class to its signers array.
			out.println("<br>" + level +"ReferenceSigner@" + callToString(referenceHolder,useToString) );
			nextReference = referenceHolder;
			break;
		case JVMTICallBack.JVMTI_REFERENCE_PROTECTION_DOMAIN://Reference from a class to its protection domain.
			out.println("<br>" + level +"ProtectionDomain@" + callToString(referenceHolder,useToString) );
			nextReference = referenceHolder;
			break;
		case JVMTICallBack.JVMTI_REFERENCE_INTERFACE://Reference from a class to one of its interfaces.
			out.println("<br>" + level +"ReferenceInterface@" +  callToString(referenceHolder,useToString) );
			nextReference = referenceHolder;
			break;
		case JVMTICallBack.JVMTI_REFERENCE_STATIC_FIELD://Reference from a class to the value of one of its static fields. For references of this kind the referrer_index parameter to the jvmtiObjectReferenceCallback is the index of the static field. The index is based on the order of the directly declared static and instance fields in the class (not inherited fields), starting at zero. See GetClassFields.
		{
			Class clazz = (Class)referenceHolder;
			Field field = this.getObjectField(clazz,(int)point.getIndex());
			String fieldName = null;
			if (field==null)
			{
				fieldName = "UndefinedField@" + referenceHolder;
			}
			else
			{
				fieldName = field.toString();
			}
			out.println("<br>" + level +" StaticFieldReference " + fieldName);
			nextReference = null;
			break;
		}
		case JVMTICallBack.JVMTI_REFERENCE_CONSTANT_POOL://Reference from a class to a resolved entry in the constant pool. For references of this kind the referrer_index parameter to the jvmtiObjectReferenceCallback is the index into constant pool table of the class, starting at 1. See The Constant Pool in the Java Virtual Machine Specification.
			out.println("<br>" + level +"ReferenceInterface@" +  callToString(referenceHolder,useToString)  );
			nextReference = referenceHolder;
			break;
		case JVMTICallBack.ROOT_REFERENCE :
			out.println("<br>" + level +"Root");
			nextReference = null;
			break;
		case JVMTICallBack.THREAD_REFERENCE:;

		   Class methodClass = this.getMethodClass(point.getMethod());
		   if (methodClass!=null)
		   {
			   String className= null;
			   if (methodClass!=null)
			   {
				   className = methodClass.getName();
			   }
			
				Thread.yield();
			
		   // this is weird but without this sleep here, the JVM crashes. 
		    /*try
			{
				Thread.sleep(10);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}*/
			
			   
			   String methodName = this.getMethodName(point.getMethod());
			   out.println("<br>" + level + " Reference inside a method - " + className+ "::" + methodName);
		   }
		   nextReference = null;
		   break;
	   default:
			   log.warn("unexpected reference " + point);
		}
		return nextReference;
	}
   
    /** This method tags the JVM and return an index. You can navigate through references using this returned HashMap. 
     * This method can't be exposed through JMX as it would serialize a huge amount of data.
     * @return HashMap<Long objectId,ArrayList<ReferenceDataPoint> referencees>
     *  */
    public HashMap createIndexMatrix() throws IOException
    {
    	final HashMap referencesMap = new HashMap();
	    	File tmpFile = File.createTempFile("tmpRefs",".tmp");
	    	this.notifyOnReferences(tmpFile.getAbsolutePath(),new JVMTICallBack()
	    			{
	    		       int count=0;
						public void notifyReference(long referenceHolder, long referencedObject, long classTag, long index, long method, byte referenceType)
						{
							if (count==0)
							{
								log.info("started receiving references");
							}
							count++;
							if (count%1000==0)
							{
                               log.info(count + " references received");
							}
							ReferenceDataPoint dataPoint = new ReferenceDataPoint(referenceHolder, referencedObject, classTag, index, method, referenceType);
							Long indexLong = new Long(referencedObject);
							ArrayList arrayList = (ArrayList)referencesMap.get(indexLong);
							if (arrayList==null)
							{
								arrayList = new ArrayList();
								referencesMap.put(indexLong,arrayList);
							}
							arrayList.add(dataPoint);
						}
						public void notifyClass(long classTag, Class clazz) {
						}
						public void notifyObject(long classTag, long objectId, long bytes) {
						}
	    	});
	    	
	    	tmpFile.delete();
	    	
	    	return referencesMap;
    }
    
    
    /**
     * Show the reference holders tree of an object
     * @param className The name of the class to explore
     * @param  maxLevel The number of levels to explode. Be careful as if you put this number too high, you migh endup in a forever loop, specially if your object is referencing something too generic
     * @param solveReferencesOnClass Will expose the tree on the class
     * @param solveReferencesOnClassLoader Will expode the tree on the classLoader (I mostly recommend to only look for classLoader's references)
     * @param useToString If true, will use toString when an object is printed. If False will use className@<System.identityHashCode(object)>
     * @param weakAndSoft If false, won't detail references on Weak and Soft References
     * @param printObject If true, Will print (with toString) every single instance of the object passed as parameter 
     */
    public String exploreClassReferences(String className, int maxLevel, boolean solveReferencesOnClasses, boolean solveReferencesOnClassLoaders, boolean useToString, boolean weakAndSoft, boolean printObjects)
    {
    	this.forceGC();
    	if (!solveReferencesOnClasses && !solveReferencesOnClassLoaders && !printObjects)
    	{
    		return "<b> you have to select at least solveReferences || solveClassLoaders || printObjects </b>";
    	}

    	HashMap referencesMap = null;
    	try
    	{
	    	referencesMap = createIndexMatrix();
		}
		catch (Exception e)
		{
	    	CharArrayWriter charArray = new CharArrayWriter();
	    	PrintWriter out = new PrintWriter(charArray);
			e.printStackTrace(out);
			return charArray.toString();
		}
    	

		try
		{
			return exploreClassReferences(className, maxLevel, solveReferencesOnClasses, solveReferencesOnClassLoaders, useToString, weakAndSoft, printObjects, referencesMap);
		}
		finally
		{
	    	referencesMap.clear();
	    	this.releaseTags();
		}
    }

    /** This is an overload to reuse the matrix index in case you already have indexed the JVM  */
	public String exploreClassReferences(String className, int maxLevel, boolean solveReferencesOnClasses, boolean solveReferencesOnClassLoaders, boolean useToString, boolean weakAndSoft, boolean printObjects, HashMap referencesMap) {
		CharArrayWriter charArray = new CharArrayWriter();
    	PrintWriter out = new PrintWriter(charArray);
	    	
    	try
    	{
	    	
	    	Class[] loadClasses = this.getLoadedClasses();
	    	
	    	for (int i=0;i<loadClasses.length;i++)
	    	{
	    		if (loadClasses[i].getName().equals(className))
	    		{
	    			out.println("<br><br><br><b>References to " + loadClasses[i] + "</b>");
	    			if (solveReferencesOnClasses)exploreObject(out,loadClasses[i],0,maxLevel,useToString,weakAndSoft,referencesMap,new HashSet());
	    			if (solveReferencesOnClassLoaders)
	    			{
		    			if (loadClasses[i].getClassLoader()!=null)
		    			{
			    			out.println("<br><b><i>references to its classloader " + loadClasses[i].getClassLoader() +"</i></b>");
			    			exploreObject(out,loadClasses[i].getClassLoader(),0,maxLevel,useToString,weakAndSoft,referencesMap,new HashSet());
		    			}
	    			}
	    			out.println("<br>");
	    			
	    			if (printObjects)
	    			{
	    				
	    				Object objects[] = this.getAllObjects(loadClasses[i]);
	    				for (int j=0;j<objects.length;j++)
	    				{
	    					out.println("obj[" + j + "]=" + objects[j]);
	    				}
	    			}
	    		}
	    	}
	    	
	    	loadClasses = null;
	    	
	    	return charArray.toString();
    	}
    	catch (Exception e)
    	{
        	charArray = new CharArrayWriter();
        	out = new PrintWriter(charArray);
    		e.printStackTrace(out);
    		return charArray.toString();
    	}
	}

    /**
     * Show the reference holders tree of an object. This method is also exposed through MBean.
     */
    public String exploreObjectReferences(String className, int maxLevel, boolean useToString)
    {
    	this.forceGC();

    	Object obj[] = this.getAllObjects(className);

    	HashMap referencesMap = null;
    	try
    	{
	    	referencesMap = createIndexMatrix();
		}
		catch (Exception e)
		{
	    	CharArrayWriter charArray = new CharArrayWriter();
	    	PrintWriter out = new PrintWriter(charArray);
			e.printStackTrace(out);
			return charArray.toString();
		}
    	
    	CharArrayWriter charArray = new CharArrayWriter();
    	PrintWriter out = new PrintWriter(charArray);

		try
		{
			if (obj.length>100)
			{
				out.println("obj List too big (>100)=" + obj.length);
				return charArray.toString();
			}
			for (int i=0;i<obj.length;i++)
			{
				out.println("<br><b>References to obj["+i+"]=" + (useToString?obj[i].toString():obj[i].getClass().getName()));
				out.println(exploreObjectReferences(referencesMap,obj[i],maxLevel,useToString));
			}
			return charArray.toString();
		}
		finally
		{
	    	referencesMap.clear();
	    	this.releaseTags();
		}
    }

    /**
     * Show the reference holders tree of an object. This returns a report you can visualize through MBean.
     */
    public String exploreObjectReferences(HashMap referencesMap, Object thatObject, int maxLevel, boolean useToString)
    {
    	CharArrayWriter charArray = new CharArrayWriter();
    	PrintWriter out = new PrintWriter(charArray);

	    	
    	try
    	{
			exploreObject(out,thatObject,0,maxLevel,useToString,false,referencesMap,new HashSet());
			out.println("<br>");
	    	return charArray.toString();
    	}
    	catch (Exception e)
    	{
        	charArray = new CharArrayWriter();
        	out = new PrintWriter(charArray);
    		e.printStackTrace(out);
    		return charArray.toString();
    	}
    }
    
    /** Forces an OutOfMemoryError and releases the memory immediatly. This will force SoftReferences to go away. */
    public void forceReleaseOnSoftReferences()
    {
    	SoftReference reference = new SoftReference(new Object());
    	
    	ArrayList list = new ArrayList();
    	int i=0;
    	try
    	{
    		while(true)
    		{
	    		list.add("A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String " + (i++));
	    		if (i%1000==0) // doing the check on each 100 elements
	    		{
		    		if (reference.get()==null)
		    		{
		    			System.out.println("Break as the soft reference was gone");
		    			break;
		    		}
	    		}
    		}
    	}
    	catch (Throwable e)
    	{
    	}
    	
    	list.clear();
    	try
    	{
	    	ByteArrayOutputStream byteout = new ByteArrayOutputStream();
	    	ObjectOutputStream out = new ObjectOutputStream(byteout);
	    	
	    	out.writeObject(new Dummy());
	    	
	    	ByteArrayInputStream byteInput = new ByteArrayInputStream(byteout.toByteArray());
	    	ObjectInputStream input = new ObjectInputStream(byteInput);
	    	input.readObject();
	    	
    	}
    	catch (Exception e)
    	{
    		e.printStackTrace();
    	}
    	
    	this.forceGC();
    }
    
    /** Used just to serialize anything and release SoftCache on java Serialization */
    static class Dummy implements Serializable
    {
		private static final long serialVersionUID = 1L;
    }

    /** 
     * Will show a report of every class loaded on this JVM. At the beggining of the report you will see duplicated classes (classes loaded in more than one classLoader)
     */
    public String listClassesHTMLReport() throws Exception
    {
    	try
    	{
    		forceGC();
	    	CharArrayWriter charArray = new CharArrayWriter();
	    	PrintWriter out = new PrintWriter(charArray);
	    	
	    	
	    	Collection classSet = createTreeSet(new ClassSorterByClassName());
	    	
	    	boolean printedHeader=false;
	    	
	    	ClassLoader systemClassLoaderDummy = new ClassLoader(){public String toString(){return "SystemClassLoader";}};
	    	ArrayList classLoaderDuplicates = new ArrayList();
	    	Iterator iter = classSet.iterator();
	    	String currentName=null;
	    	Class currentClass = null;
	    	while (iter.hasNext())
	    	{
	    		currentClass = (Class)iter.next();
	    		if (currentName!=currentClass.getName())
	    		{
	    			if (classLoaderDuplicates.size()>1)
	    			{
	    		    	if (!printedHeader)
	    		    	{
	    		    		out.println("<br><b>List of duplicated classes</b>");
	    		    		printedHeader=true;
	    		    	}

	    				out.println("<br>" + "<b> Class " + currentName +" was loaded on these classLoaders:</b>");
	    				Iterator iterClassLoader = classLoaderDuplicates.iterator();
	    				while (iterClassLoader.hasNext())
	    				{
	    					ClassLoader loader = (ClassLoader) iterClassLoader.next();
	    					out.println("<br>" + loader.toString());
	    				}
	    				
	    			}
	    			currentName = currentClass.getName();
	    			classLoaderDuplicates.clear();
	    		}

	    		ClassLoader loader = currentClass.getClassLoader();
    			if (loader==null)
    			{
    				loader = systemClassLoaderDummy;
    			}
    			classLoaderDuplicates.add(loader );
	    		
	    		currentName = currentClass.getName();
	    	}
	    	
			if (classLoaderDuplicates.size()>1)
			{
				out.println("<br>" + "<b> Class " + currentName +" was loaded on these classLoaders:</b>");
				Iterator iterClassLoader = classLoaderDuplicates.iterator();
				while (iterClassLoader.hasNext())
				{
					ClassLoader loader = (ClassLoader) iterClassLoader.next();
					out.println("<br>" + loader.toString());
				}
				
			}
	    	
	    	out.println("<br><b>List of classes by ClassLoader</b>");
	    	classSet = retrieveLoadedClassesByClassLoader();
	    	
	    	// I will need a dummy reference, as the first classLoader on the iterator will be null
	    	ClassLoader currentClassLoader = new ClassLoader(){};
	    	out.println("<br>");
	    	
	    	iter = classSet.iterator();
	    	while (iter.hasNext())
	    	{
	    		Class clazz = (Class)iter.next();
	    		if (currentClassLoader!=clazz.getClassLoader())
	    		{
	    			currentClassLoader = clazz.getClassLoader();
	    			out.println("<br><b>ClassLoader = " + (currentClassLoader==null?"System Class Loader":(currentClassLoader.toString())) + "</b>");
	    		}
	    		out.println("Class = " + clazz.getName());
	    	}
	    	
	    	return new String(charArray.toCharArray());
    	}
    	catch (Exception e)
    	{
    		e.printStackTrace();
    		throw e;
    	}
    }

    /** Used by JSPs and JMX to report */
	public Collection retrieveLoadedClassesByClassName()
	{
		Collection classSet;
		classSet =  createTreeSet(new ClassSorterByClassName());
		return classSet;
	}
    
    /** Used by JSPs and JMX to report */
	public Collection retrieveLoadedClassesByClassLoader()
	{
		Collection classSet;
		classSet = createTreeSet(new ClassSorterByClassLoader());
		return classSet;
	}

	private Collection createTreeSet(Comparator comparator)
	{
		Class[] classes = this.getLoadedClasses();
		
		ArrayList classSet = new ArrayList();
		
		for (int i=0;i<classes.length;i++)
		{
			classSet.add(classes[i]);
		}
		
		Collections.sort(classSet,comparator);
		return classSet;
	}

	static class InnerCallBack implements JVMTICallBack
	{

		TLongObjectHashMap classesMap = new TLongObjectHashMap();
		WeakHashMap maps = new WeakHashMap();
		public void notifyClass(long classTag, Class clazz) {
			classesMap.put(classTag,clazz);			
		}

		public void notifyObject(long classTag, long objectId, long bytes) {
			Class clazz = (Class)classesMap.get(classTag);
			
			if (clazz!=null) // this is not supposed to happen, but just in case I keep this if here
			{
				InventoryDataPoint point = (InventoryDataPoint)maps.get(clazz);
				if (point==null)
				{
					point = new InventoryDataPoint(clazz);
					maps.put(clazz,point);
				}
				
				point.bytes+=bytes;
				point.instances++;
			}
			
			
		}

		public void notifyReference(long referenceHolder, long referencedObject, long classTag, long index, long method, byte referenceType) {
		}
		
	}
	

	/**
	 * It will return true if the comparisson didn't represent any changes.
	 * This can be used by JUnitTests to validate the consumption of the memory is on the expected results.
	 * @param reportOutput You could set System.out here. The location where logging information is going to be sent.
	 * @param map1 The first snapshot.
	 * @param map2 The second snapshot.
	 * @param ignoredClasses Classes you want to ignore on the comparisson. Used to ignore things you know are going to be produced and you don't have control over the testcase.
	 * @param prefixesToIgnore Same thing as classes, but every classes starting with these prefixes are going to be ignored.
	 * @param expectedIncreases An array of InventoryDataPoint with the maximum number of instances each class could be generating.
	 * @return true if the assertion is okay
	 */
	public boolean compareInventories(PrintStream reportOutput, Map map1, Map map2, Class[]ignoredClasses, String[]prefixesToIgnore,InventoryDataPoint[] expectedIncreases)
	{
		HashSet ignoredItems = new HashSet();
		if (ignoredClasses!=null)
		{
			for (int i=0;i<ignoredClasses.length;i++)
			{
				ignoredItems.add(ignoredClasses[i]);
			}
		}
		
		HashMap expectedIncreasesHash = new HashMap();
		if (expectedIncreases!=null)
		{
			for (int i=0;i<expectedIncreases.length;i++)
			{
				Class clazz = expectedIncreases[i].getClazz();
				expectedIncreasesHash.put(clazz,expectedIncreases[i]);
			}
		}

    // expected increase based on map1's size
    addExpectedIncrease(expectedIncreasesHash,"java.lang.ref.ReferenceQueue$Lock",1);
    addExpectedIncrease(expectedIncreasesHash,"java.util.WeakHashMap",1);
    addExpectedIncrease(expectedIncreasesHash,"java.lang.ref.ReferenceQueue",1);
    addExpectedIncrease(expectedIncreasesHash,"[Ljava.util.WeakHashMap$Entry;",1);
    addExpectedIncrease(expectedIncreasesHash,"java.lang.ref.WeakReference",map1.size());
    addExpectedIncrease(expectedIncreasesHash,"java.util.WeakHashMap$Entry",map1.size());
    
		boolean reportOK=true;
		
		
		Iterator iterMap1 = map1.entrySet().iterator();
		while (iterMap1.hasNext())
		{
			Map.Entry entry = (Map.Entry)iterMap1.next();
			Class clazz = (Class)entry.getKey();

			boolean isIgnoredPrefix=false;
			if (prefixesToIgnore!=null)
			{
				for (int i=0;i<prefixesToIgnore.length;i++)
				{
					if (clazz.getName().startsWith(prefixesToIgnore[i]))
					{
						isIgnoredPrefix=true;
						break;
					}
				}
			}
			if (!isIgnoredPrefix && !ignoredItems.contains(entry.getKey()))
			{
				InventoryDataPoint point1 = (InventoryDataPoint)entry.getValue();
				InventoryDataPoint point2 = (InventoryDataPoint)map2.get(clazz);
				if (point2!=null)
				{
					if (point2.getInstances()>point1.getInstances())
					{
						InventoryDataPoint expectedIncrease = (InventoryDataPoint)expectedIncreasesHash.get(clazz);
						boolean failed=true;
						if (expectedIncrease!=null)
						{
							if ((point2.getInstances() - point1.getInstances())<=expectedIncrease.getInstances())
							{
								failed=false;
							}
						}
						if (failed)
						{
              int expected=0;
              if (expectedIncrease!=null)
              {
                expected = expectedIncrease.getInstances();
              }
							reportOK=false;
							reportOutput.println("<br> Class " + clazz.getName() + " had an increase of " + ((point2.getInstances() - point1.getInstances())-expected) + " instances represented by " + (point2.getBytes() - point1.getBytes()) + " bytes");
              if (expectedIncrease!=null)
              {
                reportOutput.print("<br> " + ((point2.getInstances() - point1.getInstances())-expectedIncrease.getInstances()) + " higher than expected");
              }
							
						}
					}
				}
			}
		}
		
		return reportOK;
	}

  private void addExpectedIncrease(HashMap expectedIncreasesHash,String name, int numberOfInstances) {
    Class tmpClass = getClassByName(name);
    if (tmpClass!=null)
    {
      expectedIncreasesHash.put(tmpClass,new InventoryDataPoint(tmpClass,numberOfInstances));
    }
  }
	
	/** Returns a WeakHashMap<Class,InventoryDataPoint> summarizing the current JVM's inventory.
	 *  */
	public Map produceInventory() throws IOException
	{
		this.forceGC();
		InnerCallBack callBack = new InnerCallBack();
		File tmpFileObjects = File.createTempFile("delete-me",".objects");
		try
		{
			notifyInventory(true,null,tmpFileObjects.getAbsolutePath(),callBack);
		}
		finally
		{
			if (tmpFileObjects.exists())
			{
				try
				{
					tmpFileObjects.delete();
				}
				catch (Exception ignored)
				{
				}
			}
		}
		
		return callBack.maps;
	}

	/** Will list the current memory inventory. Exposed through JMX.  */
	public String inventoryReport() throws Exception {
		Map map = produceInventory();
		
		TreeSet valuesSet = new TreeSet(map.values());
		Iterator iterDataPoints = valuesSet.iterator();
    	CharArrayWriter charArray = new CharArrayWriter();
    	PrintWriter out = new PrintWriter(charArray);

		out.println("<table><tr><td>Class</td><td>#Instances</td><td>#Bytes</td></tr>");

    	while (iterDataPoints.hasNext())
		{
			InventoryDataPoint point = (InventoryDataPoint)iterDataPoints.next();
			out.println("<tr><td>" + point.getClazz().getName() + "</td><td>" + point.getInstances() + "</td><td>" + point.getBytes() + "</td></tr>");
		}
    	out.println("</table>");
		
		return charArray.toString();
	}

	/** Will print a report of every instance of the class passed by parameter. Exposed through JMX .  */
    public String printObjects(String className) throws Exception
    {
    	CharArrayWriter charArray = new CharArrayWriter();
    	PrintWriter out = new PrintWriter(charArray);

    	Object objects[] = this.getAllObjects(className);

    	out.println("<table>");
    	for (int i=0;i<objects.length;i++)
    	{
    		out.println("<tr><td>");
    		out.println(objects[i]);
    		if (objects[i] instanceof Object[])
    		{
        		out.println("</td><td>");
        		out.println("array of " + ((Object[])objects[i]).length);
        		out.println("</td></tr>");
    		}
    	}
    	
    	out.println("</table>");
    	
    	
    	return charArray.toString();
    }
	
}
