package com.fujitsu.iflowqa.testframe;

import com.fujitsu.iflow.common.Utils;
import com.fujitsu.iflow.model.wfcore.ListSpec;
import com.fujitsu.iflow.model.workflow.DataItem;
import com.fujitsu.iflow.model.workflow.DataItemMappingElement;
import com.fujitsu.iflow.model.workflow.DataItemRef;
import com.fujitsu.iflow.model.workflow.Filter;
import com.fujitsu.iflow.model.workflow.Node;
import com.fujitsu.iflow.model.workflow.NodeInstance;
import com.fujitsu.iflow.model.workflow.Plan;
import com.fujitsu.iflow.model.workflow.ProcessInstance;
import com.fujitsu.iflow.model.workflow.TimerStatus;
import com.fujitsu.iflow.model.workflow.WFObjectFactory;
import com.fujitsu.iflow.model.workflow.WFObjectList;
import com.fujitsu.iflow.model.workflow.WFSession;
import com.fujitsu.iflow.model.workflow.WorkItem;
import com.fujitsu.iflow.model.util.IflowEnumeration;
import java.io.File;
import java.util.Hashtable;
import java.util.Random;
import java.util.Vector;


public class FlowPath extends BPMTestBase
{
    // holds a map between user name and their passwords so that it
    // can log in an complete work items for that user.
    Hashtable passwordMap;
    Hashtable runtimeSubprocessGuard;
    Random    randNo;

    /*
     * Zero argument constructor.
     */
    public FlowPath() {
        super();
        passwordMap = new Hashtable();
        randNo = new Random();
        runtimeSubprocessGuard = new Hashtable();
    }

    /*
     * This method runs the rest.
     */
    public void runTests(TestRecorder recorder) throws Exception
    {
        init(recorder);

        String filename = null;
        long piId = -1;

        try {
            login();
            // the fist argument from the tr will be the xpdl file name.
            String[] commandLineArgs = tr.getCommandLineArgs();
            if (commandLineArgs==null) {
                throw new Exception("commandLineArgs is null, must have the name of the XPDL files to import in the command line args.");
            }
            filename = commandLineArgs[1];
            if (filename==null) {
                throw new Exception("filename is null, must have the name of the XPDL files to import in the command line args.");
            }

            if (filename.indexOf("\\") > -1) {
                throw new Exception("A file name was specified with back-slashes, this is not portable, and not necessary.  Please try again with forward slashes (which works fine on Windows).");
            }

            filePath = "";  //in case the file is in the 'root'

            int lastSlashPos = filename.lastIndexOf("/");
            if (lastSlashPos>-1) {
                filePath = filename.substring(0, lastSlashPos+1);
            }

            Plan plan = importXPDL(filename);
            ProcessInstance pi = createProcess(plan);

            //Mark this as a FlowPath test so it can be easily deleted
            pi.startEdit();
            pi.setTitle("%%FlowPath");
            pi.commitEdit();

            SafeSleep sleeper = new SafeSleep("SafeSleeping till process id " + pi.getId() + " goes to running state");
            while(pi.getState() != ProcessInstance.STATE_RUNNING
                  && pi.getState() != ProcessInstance.STATE_ERROR) {
                debug("Process instance "+pi.getId()+" is not running yet, still waiting...");
                sleeper.nap();
            }

            getPasswordMap(pi);
            handleAllWorkItems(pi);
            determineResult(pi);

        } catch (Exception e) {
            throw new ExceptionWrapper("Exception caught in main FlowPath test level", e);
        } finally {
            try {
                logOut();
            } catch (Exception e) {
                // have to ignore this exception
            }
        }
    }


    /**
    * in order to handle multiple users, we need to have a list of
    * such users and their passwords.  If a UDA named PasswordMap is
    * present then read from it usernames and passwords in this form:
    *     user1:pass1,user2:pass2,user3:pass3
    */
    private void getPasswordMap(ProcessInstance pi)
    {
        //always add in the main user and password so that always is OK
        passwordMap.put(username, password);

        String votingList = tr.getProperty("PasswordMap", "iflow:iflow");
        String[] usersList = Utils.splitString(votingList);

        for (int i = 0; i < usersList.length; i++) {

            String aUser = usersList[i].trim();
            FlowPath test = new FlowPath();
            int index = aUser.indexOf(':');
            String uName = aUser.substring(0, index);
            String uPass = aUser.substring(index + 1);

            passwordMap.put(uName, uPass);
        }
    }


    /**
    * If a process uses another process as a subprocess, then that subprocess
    * definition must be loaded into engine before use.  A process can accomplish
    * this by defining a variable "SubProcessXPDL" which is a comma delimited
    * list of XPDL files that must be imported before running the process.
    */
    private void loadSubProcessXPDL(ProcessInstance pi)
        throws Exception
    {
        //some processes have multiple settings to import the same
        //process definition, but importing onces will suffice
        Hashtable duplicateGuard = new Hashtable();

        DataItem di = pi.getDataItem("SubProcessXPDL");
        if (di!=null) {
            String xpdlList = di.getValue();
            String[] xpdlFiles = Utils.splitString(xpdlList);
            for (int i = 0; i < xpdlFiles.length; i++) {
                String aFile = filePath + xpdlFiles[i].trim();
                if (duplicateGuard.get(aFile)==null) {
                    tr.log("Loading XPDL: "+aFile);
                    importXPDL(aFile);
                    duplicateGuard.put(aFile,aFile);
                }
            }
        }

        //an older scheme put an expression into the activity node
        //descriptions, so walk through all the activities here, and if there
        //are any with a description like this, import it:
        //
        //     createChildPlan=FileName.xpdl
        //
        Plan plan = pi.getPlan();
        Node[] nodes = plan.getNodes();

        for (int i=0; i<nodes.length; i++) {
            String desc = nodes[i].getDesc();
            if (desc.startsWith("createChildPlan=")) {
                String bFile = filePath + desc.substring(16);
                if (duplicateGuard.get(bFile)==null) {
                    tr.log("Loading XPDL: "+bFile);
                    importXPDL(bFile);
                    duplicateGuard.put(bFile,bFile);
                }
            }
        }
    }

    private void determineResult(ProcessInstance pi)
        throws Exception
    {

        DataItem di = pi.getDataItem("TestResult");
        if (di == null) {
            throw new Exception("The UDA named 'TestResult' is not present in this process.");
        }

        //if this is a negative test, then a UDA __ErrorMessage must be defined
        DataItem expDi = pi.getDataItem("__ErrorMessage");
        if (expDi == null) {

            //not negative test, so report result
            testAndReport("FlowPath Result", di.getValue(), "Passed");
            return;
        }

        //is a negative test, so check that the exception recorded
        DataItem actDi = pi.getDataItem("_ProcessException");
        if (actDi == null) {
            throw new Exception(
                "Negative test case identified because '__ErrorMessage' is defined, but invalid negative test because '_ProcessException' is not defined.");
        }

        String expectedValue = expDi.getValue();
        String actualValue = actDi.getValue();

        if (actualValue.indexOf(expectedValue) != -1) {
            markPassed("FlowPath Negative Result");
        } else {
            markFailed("FlowPath Negative Result", "Expected '" + expectedValue
                    + "' but actually got '" + actualValue+"'.");
        }

    }

    /**
    * The point is that we want to pick workitems in a random order
    * but we may not have all the users loaded in the user map, so
    * we have to ignore them, and we may have to find the single one
    * that we can handle.  Most efficient way to get randomness but
    * also be sure to try all is to shuffle, and then test linearly.
    * Then, after trying all of them, you can give up and exit.
    */
    private void shuffleWorkItems(Object[] list)
    {
        int shufCount = list.length + 3;
        for (int i=0; i<shufCount; i++) {
            int pos1 = randNo.nextInt(list.length);
            int pos2 = randNo.nextInt(list.length);
            Object holder = list[pos1];
            list[pos1] = list[pos2];
            list[pos2] = holder;
        }
    }

    /*
     * This method loops every 100ms checking to see if the process is
     * completed.  If not complete, it picks a random workitem from the
     * process and calls handleOneWorkItem.
     */
    public void handleAllWorkItems(ProcessInstance pi)
            throws Exception {
        tr.log("handleAllWorkItems of process instance "+pi.getId());

        //record the starting time so we can be sure to try for at least
        //a minimum amount of time.
        long lastWorkItemProcessTime = System.currentTimeMillis();

        //records whether we were stopped last time, so we can report a change
        boolean isStopped = false;

        //remember if you have already done a runtime subprocess, don't do it twice.
        boolean alreadyHandledRuntimeSubprocess = false;

        //check to see if this process needs any related XPDL loaded
        loadSubProcessXPDL(pi);

        try {

            // DriverControlLimit can be optionally set to the number of seconds
            // that the process can be in "stop" mode. If this variable is not
            // set, then the default value is 10 seconds limit.

            int noWorkListGuard = 300;
            int stopTimeMax = 30;
            DataItem controlLimit = pi.getDataItem("__DriverControlLimit");
            if (controlLimit != null) {
                stopTimeMax = Integer.parseInt(controlLimit.getValue());
            }
            int stopTimeMaxMillis = stopTimeMax*1000;

            while (true) {

                //everytime through the loop, we sleep automatically for .1 seconds
                //this keeps it from spinning in the same state too quickly
                Thread.sleep(100);

                //if the process is completed, then we can leave
                if (pi.getState() == pi.STATE_CLOSED) {
                    tr.log("Process instance "+pi.getId()+" is now completed");
                    return;
                }
                //if the process is in error, then no point in further processing
                if (pi.getState() == pi.STATE_ERROR) {
                    tr.log("Process instance "+pi.getId()+" has hit an error, aborting test");
                    displayErrorFromHistory(pi);
                    return;
                }


                //setting __DriverControl to stop pauses the workitem handling
                DataItem control = pi.getDataItem("__DriverControl");
                if (control != null) {
                    if (control.getValue().equalsIgnoreCase("stop")) {
                        //produce some log output about the fact that __DriverControl=STOP
                        if (!isStopped) {
                            tr.log("__DriverControl=STOP, waiting for timer or something, workitems handling stopped for up to "+stopTimeMax+" seconds");
                            isStopped = true;
                        }
                        if (System.currentTimeMillis() - lastWorkItemProcessTime > stopTimeMaxMillis) {
                            tr.log("__DriverControl=STOP for too long (more than "+stopTimeMax+" seconds) giving up");
                            return;
                        }
                        continue;
                    }
                    else{
                        if (isStopped) {
                            tr.log("__DriverControl=GO, will start handling workitems now");
                            isStopped = false;
                        }
                    }
                }

                //Note from Keith:  I am not sure this is really proper here.  Leaving it for
                //now because I don't want to break anything.  The problem is that this loop is
                //done many many times for a single process instance.  Handling the workitems for
                //a subprocess are not any different than handling for the main process, in fact
                //you could mix the workitems together without any difficulty.  The main process
                //and subprocess run in parallel, so there is no reason to go an complete
                //the subprocess completely before handling anything in the main process.
                //It would be a better test if it would actually run the two in parallel,
                //picking workitems from either process.  The complication means that that
                //you need to get workitems from the parent process and all child processes
                //instead of a single process.  Worry about this sometime.
                if ((pi.getDescription().equalsIgnoreCase("HasSubProcess"))) {
                    handleChildProcess(pi.getId(), false);
                }

                //handle chainedprocess workitem
                if ((pi.getDescription().equalsIgnoreCase("HasChainedProcess"))) {
                    handleChildProcess(pi.getId(), true);
                }

                Object[] workItemList = getProcessWorkitems(
                        Filter.AllWorkItems, null, pi.getId());

                if (workItemList == null) {
                    throw new Exception("Programming error: got a null back for the worklist.  This should never happen.");
                }
                WorkItem workItem = null;

                if (workItemList.length > 0) {

                    //debug dump ... to see what they look like
                    for (int i=0; i<workItemList.length; i++) {
                        WorkItem wi = (WorkItem) workItemList[i];
                        tr.log("  -- work item: (" + wi.getId() + ") "+wi.getName()+" @ "+wi.getAssignee());
                    }

                    shuffleWorkItems(workItemList);

                    //OK, we have workitems, so find one for a user that we know the password for
                    String wiUser = null;
                    String wiPass = null;
                    for (int i=0; i<workItemList.length; i++) {
                        WorkItem wi2 = (WorkItem) workItemList[i];
                        wiUser = wi2.getAssignee();
                        wiPass = (String) passwordMap.get(wiUser);
                        if (wiPass != null) {
                            workItem = wi2;
                            break;
                        }
                    }
                }

                //check for the case that no acceptable workitem was found
                if (workItem==null) {

                    // getting here means there are no workitems for this process instance
                    // but that may simply because we have to wait for them, so if it has
                    // been less than 30 seconds, just loop back and keep checking.
                    if (System.currentTimeMillis()-lastWorkItemProcessTime < 30000) {
                        continue;
                    }

                    //Have waited too long for workitem, must exit
                    tr.log("There have been no items on the worklist for more than 30 seconds, giving up");
                    return;
                }

                if (handleOneWorkItem(workItem)) {
                    //write down the current time so when we can know how long it has been
                    //since the last workitem was successfully handled
                    lastWorkItemProcessTime = System.currentTimeMillis();
                }

            }
        } catch (Exception e) {
            throw new ExceptionWrapper(
                "handleAllWorkItems() - Possibly because of WorkItem.makeChoice()",e);
        }
    }


    //Take the selected workitem, and make a choice on it
    //after logging in as the user that the workitem is assigned to.
    private boolean handleOneWorkItem(WorkItem originalWi)
        throws Exception
    {
        WFSession newSession = null;
        WorkItem wi = originalWi;

        try {
            String wiUser = wi.getAssignee();
            String wiPass = (String) passwordMap.get(wiUser);

            //here we need to complete the work item logged in as someone else
            if (!username.equals(wiUser)) {
                newSession = WFObjectFactory.getWFSession();
                newSession.initForApplication(null, tr.getProps());
                newSession.logIn(server + "Flow", wiUser, wiPass);
                wi = WFObjectFactory.getWorkItem(wi.getId(), newSession);
            }

            NodeInstance nInstance = wi.getNodeInstance();

            /*
            //DO WE NEED THIS????
            //Timers are normally handled simply by setting the __DriverControl == STOP
            //and then let the time switch it back.   Waiting is accomplished
            //not by calculating the time of the timer to finish, but simply
            //spinning in this main loop until __DriverControl is set back to GO.
            if (control == null) {
                TimerStatus[] tStatus = nInstance.getStatusOfTimers();
                for (int j = 0; j < tStatus.length; j++) {
                    waitForTimerToExpire(tStatus[j]
                            .getTimerExpirationTime());
                }
            }
            */

            //Check if it is a candidate for runtime subprocess
            String actDesc = nInstance.getDesc();
            if (actDesc.startsWith("runTimeSubProcessPlan")) {
                String activityName = nInstance.getName();

                //make a subprocess only once for a given activity
                if (runtimeSubprocessGuard.get(activityName)==null) {
                    handleRuntimeSubProc(originalWi, nInstance);
                    runtimeSubprocessGuard.put(activityName,activityName);
                    return true;
                }
            }

            String[] choices = wi.getChoices();
            String chosenOne = choices[randNo.nextInt(choices.length)];
            tr.log("Choosing '"+chosenOne+"' on workitem '"+wi.getName()+"' logged in as '"+wiUser+"'.");
            wi.makeChoice(chosenOne);
        }
        catch (Exception e) {
            // There is a possibility that the code picked up a workitem, and then
            // before getting the chance to make a choice on it, due to a
            // background process, the workitem is no longer available.
            // This is a standard multiple user conflict scenario, and does
            // not necessarily indicate that the server is wrong.
            // The resolution is to ignore this error, and return to
            // the main loop to go back and select a different workitem.
            if (isWorkItemNotAvailableException(e)) {
                tr.log("The workitem apparently has gone away since the time that we got the worklist.  Work item: ("
                    + wi.getId() + ") "+wi.getName()+" @ "+wi.getAssignee()+ " exception: "+e);
                return false;
            }
            throw e;
        }
        finally {
            //now logout if necessary
            if (newSession != null) {
                newSession.logOut();
            }
        }
        return true;
    }

    // Need to determine if this is one specific error: you had a workitem id
    // and went to look in the database, and you did not find it there.
    // Presumably this is because you are asking about a workitem that has
    // been deleted in the mean time.
    private boolean isWorkItemNotAvailableException(Exception e)
    {
        String msg = e.toString();
        //can probably come up with a better check which walks through the
        //list of exceptions and checks the specific class and error number.
        //The result would be a test that would work in any language.
        //But for now this check works only in english.
        if (msg.indexOf("Database retrieve request failed")>0
            && msg.indexOf("Could not retrieve the workitem")>0) {
            return true;
        }
        return false;
    }



    /*
     * This method logs in the specified user before handing all the workitems.
     * Not used in this class, but might be used by other classes.
     */
    public boolean logInAndHandleWorkItems(TestRecorder trt, String uName,
            String uPass, ProcessInstance pi, String choice) throws Exception {
        init(trt);
        username = uName;
        password = uPass;
        login();
        //handleWorkItems(pi, choice);
        logOutWithoutException();
        return false;
    }

    private void handleRuntimeSubProc(WorkItem wi, NodeInstance ni)
        throws Exception
    {
        String desc = ni.getDesc();

        String childPalnName = desc.substring(desc.indexOf("=") + 1);
        if (filePath != null) {
            childPalnName = filePath + childPalnName;
        }
        Plan childPlan = importXPDL(childPalnName);
        long planid = childPlan.getId();

        mapDataItemsToSubprocess(childPlan, wi);
        wi.setSubPlanId(planid);
        long subProcId = wi.startSubProcess();

        ProcessInstance cPi = WFObjectFactory.getProcessInstance(subProcId,
                wfSession);
        ProcessInstance pPi = ni.getProcessInstance();

        SafeSleep sleeper = new SafeSleep("SafeSleeping till runtime subprocess id "
                + subProcId + " goes to running state");
        while(cPi.getState() != ProcessInstance.STATE_RUNNING) {
            tr.log("Runtime subprocess instance "+subProcId+" is not running yet, still waiting...");
            sleeper.nap();
        }

        tr.log("Runtime Subprocess created and running. piid="+subProcId+" from plan=" + childPalnName);
        validateSubProcessUDA(pPi, cPi, "To Child");

        handleAllWorkItems(cPi);
        sleeper = new SafeSleep("SafeSleeping till node id "
                + ni.getNodeInstanceId() + " goes to running state");
        while (ni.getState() != NodeInstance.STATE_RUNNING) {
            tr.log("Runtime node instance "+ni.getNodeInstanceId()+" is not running yet, still waiting...");
            sleeper.nap();
        }

        validateSubProcessUDA(pPi, cPi, "To Parent");
    }



    //this makes a data mapping between every variable in the parent that starts with
    //"MAP_" to every variable in the child plan that starts with "MAP_"
    //that has the same type.  So you had better have only one of a given type...
    private void mapDataItemsToSubprocess(Plan childPlan, WorkItem wi)
        throws Exception
    {
        DataItemRef[] drefs = childPlan.getDataItemRefs();

        DataItem[] ditms = wi.getProcessInstance().getDataItems();

        for (int i = 0; i < drefs.length; i++) {
            String childVarName = drefs[i].getName();
            if (!childVarName.startsWith("MAP_")) {
                continue;
            }
            for (int j = 0; j < ditms.length; j++) {
                String parentVarName = ditms[j].getName();
                if (parentVarName.startsWith("MAP_")
                        && drefs[i].getType().equals(ditms[j].getType())) {
                    wi.addDataMappingElement(parentVarName, childVarName, DataItemMappingElement.INOUT);
                    break;
                }
            }
        }
    }


    //Checks to make sure that the variables that should have been mapped
    //from parent to child, have, in fact the same values in them.
    private void validateSubProcessUDA(ProcessInstance ppi, ProcessInstance cpi,
            String msg)
        throws Exception
    {
        DataItemRef[] cdtitms = cpi.getDataItems();
        DataItem[] pdtitms = ppi.getDataItems();

        for (int i = 0; i < cdtitms.length; i++) {
            DataItemRef childRef = cdtitms[i];
            String childVarName = childRef.getName();
            if (!childVarName.startsWith("MAP_")) {
                continue;
            }
            for (int j = 0; j < pdtitms.length; j++) {
                DataItem parentVar = pdtitms[j];
                String parentVarName = parentVar.getName();
                if (!parentVarName.startsWith("MAP_")) {
                    continue;
                }
                if (!childRef.getType().equals(parentVar.getType())) {
                    continue;
                }
                //at this point the values should match
                testAndReport("Data Mapping between parent."+parentVarName+" and child."+childVarName,
                              parentVar.getValue(), childRef.getValue());
            }
        }
    }

    /**
    * This method is called if the process has an indication that it MIGHT
    * have a subprocess.  OF course, at the time of calling, this subprocess
    * mightnot be started yet, or it might already have been completed.
    * So if no subprocess is found, then this is no cause for alarm.
    * It might be found next time.
    *
    * This approach is a little bit flawed, because if it find a subprocess
    * it handles it to completion, which is not realistic.  It would be better
    * to handle just one workitem and return to possibly handle a workitem
    * in the parent process, so the processes are running in parallel.
    * But for now, the child process is handled to completion before anything
    * else on the parent is handled.
    *
    * This routine should be called multiple times ... every main loop on the
    * parent because subprocesses will come and go.  There might be multiple
    * child processes, but completing one, might cause the cancellation of
    * others, so this routine handles only one child, and expects to be called
    * again for the other children if they still exist.
    */
    private void handleChildProcess(long parentProcessId, boolean isChained)
            throws Exception
    {
        WFObjectList wfol = WFObjectFactory.getWFObjectList(wfSession);
        wfol.addFilter(WFObjectList.LISTFIELD_PROCESSINSTANCE_PARENTID,
                "=", "'" + parentProcessId + "'");
        wfol.openBatchedList(Filter.MyActiveProcesses);
        Object[] processList = wfol.getNextBatch(0, 99);
        if (processList == null || processList.length == 0) {
            //no child processes, so just return.
            return;
        }

        //handle only the first one, the rest can be handled on subsequent
        //calls ... if they still exist.
        ProcessInstance pi = (ProcessInstance) processList[0];
        if (parentProcessId != pi.getParentProcessId()) {
            throw new Exception("Strange...asked for processes that are a child of "
                +parentProcessId+", but this appears instead to be a child of "
                +pi.getParentProcessId());
        }

        //run this child process to completion
        handleAllWorkItems(pi);

        /*
         * Code added by SecF Code to check Chained Child
         * UDA in case of Chained Process as in chained
         * process the child process is not dependent on
         * parent.
         */
        determineResult(pi);
    }


    private void displayErrorFromHistory(ProcessInstance pi)
        throws Exception
    {

        int _processHistoryFields[] = {
            ProcessInstance.HISTORY_ID,
            ProcessInstance.HISTORY_TIME_STAMP,
            ProcessInstance.HISTORY_EVENT_TYPE,
            ProcessInstance.HISTORY_RESPONSIBLE,
            ProcessInstance.HISTORY_PRODUCER_TYPE,
            ProcessInstance.HISTORY_PRODUCER_ID,
            ProcessInstance.HISTORY_CONSUMER_TYPE,
            ProcessInstance.HISTORY_CONSUMER_ID,
            ProcessInstance.HISTORY_PROCESSINSTANCE_ID,
            ProcessInstance.HISTORY_ISHANDLED,
            ProcessInstance.HISTORY_EVENTDATA
            };

        IflowEnumeration sElements = pi.getHistory(_processHistoryFields);
        Integer timeStampKey = new Integer(ListSpec.PROCESSHISTORY_TIMESTAMP);
        Integer consumerIdKey = new Integer(ListSpec.PROCESSHISTORY_CONSUMERID);
        Integer consumerTypeKey = new Integer(ListSpec.PROCESSHISTORY_CONSUMERTYPE);
        Integer producerIdKey = new Integer(ProcessInstance.HISTORY_PRODUCER_ID);
        Integer producerTypeKey = new Integer(ProcessInstance.HISTORY_PRODUCER_TYPE);
        Integer responisbleKey = new Integer(ListSpec.PROCESSHISTORY_RESPONSIBLE);
        Integer eventTypeKey = new Integer(ListSpec.PROCESSHISTORY_EVENTTYPE);
        Integer isHandledKey = new Integer(ListSpec.PROCESSHISTORY_ISHANDLED);
        Integer eventDataKey = new Integer(ListSpec.PROCESSHISTORY_EVENTDATA);

        while(sElements.hasMoreElements()) {
            Hashtable historyData = (Hashtable) sElements.nextElement();

            Long timeRepresentationInLong = (Long) historyData.get(timeStampKey);
            //String timeStamp = printDate(timeRepresentationInLong.longValue());
            //long consumerType = ((Long) historyData.get(consumerTypeKey)).longValue();
            //long consumerId = ((Long) historyData.get(consumerIdKey)).longValue();
            //long producerType = ((Long) historyData.get(producerTypeKey)).longValue();
            //long producerId = ((Long) historyData.get(producerIdKey)).longValue();
            //String eventType = (String) historyData.get(eventTypeKey);
            String message = (String) historyData.get(eventDataKey);
            //String responsible = (String) historyData.get(responisbleKey);
            //String isHandled = (String) historyData.get(isHandledKey);

            if (message != null && message.length()>0) {
                deparenthesize(message);
            }
        }
    }


    // converts a string with multiple nested parenthises into
    // a nested hierarchy of lists ... much easier to read
    private void deparenthesize(String input)
    {
        try {
            int pos = 0;
            int leftover=1;
            int last = input.length();
            while (pos < last) {
                int spos = input.indexOf('{', pos);
                int epos = input.indexOf('}', pos);
                int jpos = input.indexOf("nested exception is:", pos);
                int kpos = input.indexOf("because:", pos);
                int npos = last;
                int option = 0;
                int skip = 0;
                if (spos >= 0 && spos < npos) {
                    npos = spos;
                    option = 1;
                    skip = 1;
                }
                if (epos >= 0 && epos < npos) {
                    npos = epos;
                    option = 2;
                    skip = 1;
                }
                if (jpos >= 0 && jpos < npos) {
                    npos = jpos;
                    option = 3;
                    skip = 20;
                }
                if (kpos >= 0 && kpos < npos) {
                    npos = kpos;
                    option = 3;
                    skip = 8;
                }

                // if the character at the position is the searched char,
                // then both pos and npos will be the same
                if (npos > pos) {
                    tr.log("ERROR: * "+input.substring(pos, npos));
                }
                pos = npos + skip;

                // skip any following white space to avoid empty outline entries
                while (pos < last && input.charAt(pos) == ' ') {
                    pos++;
                }
            }
        }
        catch (Exception ee) {
            tr.log("Deparenthesize got this error:"+ee);
        }
    }

}
