package jeffaschenk.commons.frameworks.cnxidx.admin;

import jeffaschenk.commons.frameworks.cnxidx.utility.ldap.*;

import java.util.*;
import java.io.*;
import javax.naming.*;
import javax.naming.directory.*;

/**
 * Java Daemon Server thread.
 *
 * @author jeff.schenk
 * @version 2.0 $Revision
 * Developed 2001-2002
 */

/**
 * IRRChangePoller
 * Class to run MetaLink Trigger Poller Thread.
 */
class IRRChangePoller implements Runnable, idxCMDReturnCodes {

    /**
     * IRRChangePoller
     * Class to Interface to MetaLink Triggers.
     */
    Thread t;

    private BufferedWriter CWQ;

    private IRRChangeStatus ChangeStatus;

    private static String MP = "IRRChangePoller: ";

    private static final String XMLHDR = "<?xml version='1.0' encoding='utf-8'?>";
    private static final String EOPYES = XMLHDR + "<irrchangeloggerprocess endofcycle='no' endofprocess='yes'/>" + "\n";
    private static final String EOCYES = XMLHDR + "<irrchangeloggerprocess endofcycle='yes' endofprocess='no'/>" + "\n";

    private static idxManageContext IRRSource = null;

    private static String IRRHost = null;
    private static String IRRPrincipal = null;
    private static String IRRCredentials = null;
    private static String IRRMLTuid = null;

    private static String OUTPUT_DIRECTORY = null;

    private static boolean VERBOSE = false;
    private static boolean DEBUG = false;

    private idxTimeStamp CurrentTimeStamp = new idxTimeStamp();

    private boolean ExitOnException = false;

    private boolean RUNNING = true;

    /**
     * IRRChangePoller Contructor class driven.
     *
     * @param BPQout            Pipe Queue.
     * @param _ChangeStatus     Object.
     * @param _IRRHost          Source IRR LDAP URL.
     * @param _IRRPrincipal     Source IRR Principal.
     * @param _IRRCredentials   Source IRR Credentials.
     * @param _IRRMLTuid        MetaLink Trigger UID.
     * @param _OUTPUT_DIRECTORY Output Directory Name for Trapping a Stop.
     * @param _VERBOSE          Indicate Verbosity.
     * @param _DEBUG            Indicate DEBUGGING.
     * @param _ExitOnException  Indicate Exit on Exceptions.
     */
    IRRChangePoller(Writer BPQout,
                    IRRChangeStatus _ChangeStatus,
                    String _IRRHost,
                    String _IRRPrincipal,
                    String _IRRCredentials,
                    String _IRRMLTuid,
                    String _OUTPUT_DIRECTORY,
                    boolean _VERBOSE,
                    boolean _DEBUG,
                    boolean _ExitOnException) {

        // ****************************************
        // Set My Incoming Parameters.
        //
        IRRHost = _IRRHost;
        IRRPrincipal = _IRRPrincipal;
        IRRCredentials = _IRRCredentials;
        IRRMLTuid = _IRRMLTuid;
        OUTPUT_DIRECTORY = _OUTPUT_DIRECTORY;
        VERBOSE = _VERBOSE;
        DEBUG = _DEBUG;
        ExitOnException = _ExitOnException;

        ChangeStatus = _ChangeStatus;

        // ****************************************
        // Ready the Synchronized Object and start
        // the Thread.
        //
        try {
            this.CWQ = new BufferedWriter(BPQout);
            t = new Thread(this, "IRRChangePoller");
            t.start(); // Start the Thread.
        } catch (Exception e) {
            // TODO Handle
        }
    } // End of Contructor.

    public void run() {

        // ***********************************************
        // Initialize
        long memfree;
        long CollectorCycle = 0;
        long CollectorChangesDetected = 0;
        String tname = Thread.currentThread().getName();
        CurrentTimeStamp.enableLocalTime();  // Show Local Time Not GMT.
        System.out.println(MP + CurrentTimeStamp.getFTS() + " Thread Established for:[" + tname + "]");

        // ***********************************************
        // Initialize my LAP Timers
        idxLapTime LP_OBTAINING_TRIGGER = new idxLapTime();
        idxLapTime LP_PROCESSING_TRIGGER_LIST = new idxLapTime();
        idxLapTime LP_ENTRY_TO_PIPE = new idxLapTime();

        // **************************************************
        // Obtain Runtime Object.
        Runtime rt = Runtime.getRuntime();

        // **************************************************
        // Shutdown Indicator Filename.
        String SHUTDOWN_FILENAME = OUTPUT_DIRECTORY + System.getProperty("file.separator") +
                "IRRCHGLOG_SHUTDOWN_PROCESS";

        // ***********************************************
        // Now initiate a Connection to the Directory
        // for a LDAP Source Context for obtain leaf entries.
        System.out.println(MP + CurrentTimeStamp.getFTS() +
                " Attempting Aux Object Directory Connection to Host URL:[ldap://" + IRRHost + "]");

        IRRSource = new idxManageContext("ldap://" + IRRHost,
                IRRPrincipal,
                IRRCredentials,
                "IRRChangePoller Aux Source");

        // ************************************************
        // Exit on all Exceptions.
        IRRSource.setExitOnException(false);

        // ************************************************
        // Now Try to Open and Obtain Context.
        try {
            IRRSource.open();
        } catch (Exception e) {
            System.err.println(MP + e);
            ChangeStatus.setError(EXIT_IRR_CLOSE_FAILURE);
            return;
        } // End of exception

        // *****************************************
        // Disable the Factories.
        try {
            IRRSource.disableDSAEFactories();
        } catch (Exception e) {
            System.err.println(MP + e);
            ChangeStatus.setError(EXIT_GENERIC_FAILURE);
            return;
        } // End of exception  

        // ****************************************
        // Indicate the Poller is starting.
        System.out.println(MP + CurrentTimeStamp.getFTS() + " Starting MetaLink Poller...");

        // ****************************************
        // Now Enter our LOOP to obtain changes
        // from the MetaLink Trigger...
        while (RUNNING) {
            // ***********************************************
            // Now Determine if I should stop the process
            // based upon the existenance of a "SHUTDOWN" file
            // in the Output Directory.
            //
            File sFile = new File(SHUTDOWN_FILENAME);
            if (sFile.exists()) {
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Detected Stop Request," +
                        " Shutdown of Change Log Process in progress.");
                break;
            }

            // ***********************************************
            // Now initiate a Connection to the Directory
            // to obtain the MLT Connection Context.
            CollectorCycle++;
            if (VERBOSE) {
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Cycle:[" + CollectorCycle +
                        "], MetaLink Poller Directory Connecting to Host:[" + IRRHost + "]");
            }

            // *******************************************
            // Establish MetaLink Trigger Token.
            //MLTrigConnectionTokenSimple MLTtoken = new MLTrigConnectionTokenSimple( IRRHost,
            //				                 IRRPrincipal,
            //                                             IRRCredentials);

            // ***********************************************
            // Now initiate a Connection to the Directory
            // for a Trigger Context
            //MLTrigConnection MLTcontext = new MLTrigConnection();
            //try {
            //MLTcontext.start( MLTtoken, IRRMLTuid );
            //} catch (MLTrigException e) {
            //	System.err.println(MP+"MLT Exception Starting Connection, "+e);
            //	ChangeStatus.setError( EXIT_IRR_UNABLE_TO_OBTAIN_CONTEXT );

            // ***************************************
            // Tell the LDIF Thread to Finish.
            //	try {
            //		CWQ.write( EOPYES );
            //		CWQ.flush();
            //	} catch(Exception xe) {}
            // **********************
            // End Thread.
            // return; // End Thread.
            //} // End of Exception

            // ****************************************
            // If Debug, show total Memory.
            if (DEBUG) {
                System.out.println(MP + "Total Memory: [" +
                        rt.totalMemory() + "].");

                memfree = rt.freeMemory();

                System.out.println(MP + "Current Free Memory: [" +
                        memfree + "].");
            }

            // ***********************************************
            // Wait for a Change Trigger to be popped
            System.out.println(MP + CurrentTimeStamp.getFTS() + " Cycle:[" + CollectorCycle + "], Waiting for Changes....");
            //try {
            LP_OBTAINING_TRIGGER.Start();
            //MLTrigChangeList lchangelist = MLTcontext.getNextChanges();
            LP_OBTAINING_TRIGGER.Stop();

            // ***************************************
            // Ok, we have a change list, process
            LP_PROCESSING_TRIGGER_LIST.Start();
            // TODO -- this while will not go into effect...
            //while( lchangelist.hasMoreChanges() )
            //{
            CollectorChangesDetected++;
            String TypeofChange;
            //MLTrigChange lchange = lchangelist.getNextChange();
            //MLTrigChangeRename lRchange = null;
            /**
             switch( lchange.getOperation() ) {

             case MLTrigChange.ADD:
             TypeofChange = "ADD";
             break;

             case MLTrigChange.MODIFY:
             TypeofChange = "MODIFY";
             break;

             case MLTrigChange.DELETE:
             TypeofChange = "DELETE";
             break;

             case MLTrigChange.RENAME:
             TypeofChange = "RENAME";
             lRchange = (MLTrigChangeRename)lchange;
             break;

             default:
             TypeofChange = "UNKNOWN";
             break;
             } // End of Switch
             **/
            TypeofChange = "UNKNOWN";
            // *********************************************
            // Show the Change Information
            // And construct a XML Document to send over the
            // Pipe to our other Thread Buddy.
            if (VERBOSE) {
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Operation Perform on Entry:[" +
                        TypeofChange + "]");
            }
            /**
             if ( lchange.getOperation() == MLTrigChange.RENAME )
             {
             if (VERBOSE)
             {
             System.out.println(MP+CurrentTimeStamp.getFTS()+"  olddn: " + lchange.getDN() );
             System.out.println(MP+CurrentTimeStamp.getFTS()+"  dn: " + lRchange.getNewDN() );
             }
             LP_ENTRY_TO_PIPE.Start();
             CWQ.write( XMLHDR+"<irrchange "+
             "typename=\042"+TypeofChange+"\042 "+
             "type=\042"+lchange.getOperation()+"\042 "+
             "dn=\042"+lRchange.getNewDN()+"\042   "+
             "olddn=\042"+lchange.getDN()+"\042 "+
             "/>\n" );
             LP_ENTRY_TO_PIPE.Stop();
             CWQ.flush();

             // *************************************
             // Now we need to feed the pipe with
             // any leaf entries that were affected
             // by the rename of a parent.
             //
             TreeMap LEAFmap = new TreeMap();
             try {
             LEAFmap = obtainLeafEntries( IRRSource.irrctx,
             lRchange.getNewDN(), lchange.getDN());
             } catch(Exception e) {
             System.out.println(MP+CurrentTimeStamp.getFTS()+
             "Exception obtaining leaf Entries, Ignoring Exception:["+e+"]");
             } // End of Exception.

             // *************************************
             // Now iterate through the Treemap
             // and produce additional rename
             // operations for leaf entries.
             //
             if ( (LEAFmap != null) && (LEAFmap.size() > 0 ) )
             {
             Set _Rset = LEAFmap.entrySet();
             Iterator iterator = _Rset.iterator();
             while (iterator.hasNext())
             {
             Map.Entry _Rentry = (Map.Entry)iterator.next();
             String _X500newDN = (String)_Rentry.getKey();
             String _oldDN = (String)LEAFmap.get(_X500newDN);

             LP_ENTRY_TO_PIPE.Start();
             CWQ.write( XMLHDR+"<irrchange "+
             "typename=\042"+TypeofChange+"\042 "+
             "type=\042"+lchange.getOperation()+"\042 "+
             "dn=\042"+convertX500NameToLDAPName(_X500newDN)+"\042   "+
             "olddn=\042"+_oldDN+"\042 "+
             "/>\n" );
             LP_ENTRY_TO_PIPE.Stop();
             CWQ.flush();
             if (VERBOSE)
             {
             System.out.println(MP+CurrentTimeStamp.getFTS()+
             "  leaf olddn: " + _oldDN);
             System.out.println(MP+CurrentTimeStamp.getFTS()+
             "  leaf dn: " + convertX500NameToLDAPName(_X500newDN) );
             } // End of If.

             } // End of While.
             } else {
             System.out.println(MP+CurrentTimeStamp.getFTS()+
             " No Leaf Entries for Rename to be Processed.");
             } // End of Else.

             // ***********************************
             // All other operations here.
             } else {
             if (VERBOSE)
             { System.out.println(MP+CurrentTimeStamp.getFTS()+"  dn: " + lchange.getDN() ); }
             LP_ENTRY_TO_PIPE.Start();
             CWQ.write( XMLHDR+"<irrchange "+
             "typename=\042"+TypeofChange+"\042 "+
             "type=\042"+lchange.getOperation()+"\042 "+
             "dn=\042"+lchange.getDN()+"\042 "+
             "/>\n" );
             LP_ENTRY_TO_PIPE.Stop();
             CWQ.flush();
             } // End of Else.
             **/
            // } // End of ChangeList Inner While.
            LP_PROCESSING_TRIGGER_LIST.Stop();

            // *****************************************
            // Now we have scanned through a change
            // List, tell the MLT Facility to stop
            // and that we have processed all.
            //
            System.out.println(MP + CurrentTimeStamp.getFTS() +
                    " Closing MetaLink Trigger Context, Changed Collected for this cycle:[" +
                    CollectorChangesDetected +
                    "].");
            //MLTcontext.stop(true);
            CollectorChangesDetected = 0;

            // ***************************************
            // Show the Lap Timings.
            if (VERBOSE) {
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Cycle:[" + CollectorCycle +
                        "], Lap Time for Pipe Communications: " + LP_ENTRY_TO_PIPE);
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Cycle:[" + CollectorCycle +
                        "], Lap Time for Obtaining a Trigger List: " + LP_OBTAINING_TRIGGER);
                System.out.println(MP + CurrentTimeStamp.getFTS() + " Cycle:[" + CollectorCycle +
                        "], Lap Time for Processing a Trigger List: " + LP_PROCESSING_TRIGGER_LIST);
            } // End of If.

            // ***********************************************
            // Reset my LAP Timers for Next Cycle.
            LP_OBTAINING_TRIGGER.Reset();
            LP_PROCESSING_TRIGGER_LIST.Reset();
            LP_ENTRY_TO_PIPE.Reset();

            // ****************************************
            // Tell the Writer Thread to CheckPoint
            // to next output file.
            //
            try {
                CWQ.write(EOCYES);
                CWQ.flush();
            } catch (Exception e) {
                System.err.println(MP + "IRR Exception Writing to Thread Pipe,\n" + e);
                ChangeStatus.setError(EXIT_GENERIC_FAILURE);
                return; // End Thread.
            } // End Of Exception.

            /**
             } catch (MLTrigException e) {
             // **************************************
             // If we have an ReturnCode of a Server
             // Operation Failure, then we simply
             // need to fall through and obtain
             // a connection again.
             // If a different error, then we
             // fail.
             //
             if ( e.getErrorCode() != MLTrigException.SERVER_OP_FAILED )
             {
             System.err.println(MP+"MLT Exception on getNextChanges RC:[" + e.getErrorCode() + "],"+ e);
             e.printStackTrace();
             ChangeStatus.setError( EXIT_GENERIC_FAILURE );

             // ************************
             // Tell other thread to end
             try {
             CWQ.write( EOPYES );
             CWQ.flush();
             } catch (Exception xe) {}
             return; // End of Thread.
             } // End of If.
             else {
             if (VERBOSE)
             { System.out.println(MP+CurrentTimeStamp.getFTS()+
             " Cycle:["+CollectorCycle+"], MetaLink Poller Ended, will retry Cycle.");
             }
             CollectorCycle--;
             } // End of Else.
             } catch (Exception e) {
             System.err.println(MP+"IRR Exception on Obtaining Child Entries,\n" + e);
             e.printStackTrace();
             ChangeStatus.setError( EXIT_GENERIC_FAILURE );

             // ************************
             // Tell other thread to end
             try {
             CWQ.write( EOPYES );
             CWQ.flush();
             } catch (Exception xe) {}

             return; // End Thread.
             } // End of exception
             **/
        } // End of Outer While.

        // ***************************************
        // Tell the LDIF Thread to Finish.
        try {
            CWQ.write(EOPYES);
            CWQ.flush();

            // *******************************
            // Wait for LDIF Thread to Finish.
            //
            Thread.sleep(10000); // 10 Seconds.

        } catch (Exception e) {
            System.err.println(MP + "IRR Exception Writing to Thread Pipe,\n" + e);
            ChangeStatus.setError(EXIT_GENERIC_FAILURE);
            return; // End Thread.
        } // End of Exception

        // ***************************************
        // Done.
        return;

    } // End of run.

    /**
     * obtainLeafEntries
     * Method to obtain all current leaf entries for the
     * existing New DN.
     * <p/>
     * Since if a Rename occurrs that affects it's leaf entries
     * we must generate the necessary rename for those leafs
     * as well.
     *
     * @param _irrctx      of IRR Instance.
     * @param _ParentNewDN Parent New DN.
     * @param _ParentOldDN Parent Old DN.
     * @return TreeMap of Entries.
     */
    private TreeMap obtainLeafEntries(DirContext _irrctx,
                                      String _ParentNewDN,
                                      String _ParentOldDN)
            throws Exception {

        // ***********************************
        // Obtain the Notification List File
        // Information.
        //
        TreeMap<String,String> LEAFmap = new TreeMap<>();

        // ***********************************
        // Set up the Search Controls.
        String[] NO_AttrIDs = {"1.1"};
        SearchControls SUBTREE_OS_ctls = new SearchControls();
        SUBTREE_OS_ctls.setReturningAttributes(NO_AttrIDs);
        SUBTREE_OS_ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);

        // *****************************************
        // Parse the Destination Entry DN to be sure
        // we have any Quotes....
        idxParseDN Naming_Source = new idxParseDN(_ParentNewDN);

        // *****************************************
        // Obtain the Namespace.
        String NameSpace = null;
        try {
            NameSpace = _irrctx.getNameInNamespace();
            if (NameSpace.equals("")) {
                NameSpace = _ParentNewDN;
            }
        } catch (Exception e) {
            NameSpace = _ParentNewDN;
        } // End of exception

        // *****************************************
        // Now obtain the Leaf Entries.
        //
        try {
            NamingEnumeration sl = _irrctx.search(_ParentNewDN, "(objectclass=*)", SUBTREE_OS_ctls);

            // ********************************
            // If the Result is Null, Ignore.
            if (sl == null) {
                return (LEAFmap);
            }

            // ***********************************
            // Loop to obtain all Leaf Entry DNs
            long Rename_DNcount = 0;
            while (sl.hasMore()) {
                SearchResult si = (SearchResult) sl.next();

                String DN = null;
                if (NameSpace.equals("")) {
                    DN = si.getName();
                } else if (si.getName().equals("")) {
                    DN = NameSpace;
                } else {
                    DN = si.getName() + "," + NameSpace;
                }

                // ************************************
                // Parse the Current DN.
                idxParseDN pDN = new idxParseDN(DN);

                // ********************************
                // Formulate the Old DN.
                //
                Rename_DNcount++;
                String formulatedOldDN = _ParentOldDN;
                if (Rename_DNcount != 1) {
                    // ****************************************
                    // Calculate the depth we need to obtain
                    // with Parent and Current DN.
                    //
                    int PrefixSize = ((pDN.depth() - Naming_Source.depth()) - 1);
                    String childRDN = "";

                    // ***************************************************************
                    // Parse for a specific depth of the DN.
                    //
                    idxParseDN XDN = new idxParseDN(pDN.getDN());
                    for (int zI = 0; zI <= PrefixSize; zI++) {
                        if ((XDN.getPDN() == null) ||
                                ("".equalsIgnoreCase(XDN.getPDN()))) {
                            break;
                        }

                        if ((childRDN == null) ||
                                (childRDN.equalsIgnoreCase(""))) {
                            childRDN = XDN.getRDN();
                        } else {
                            childRDN = childRDN + "," + XDN.getRDN();
                        }

                        XDN = new idxParseDN(XDN.getPDN());
                    } // End of For Loop.

                    // **************************************
                    // Fully Formulated Old Child DN.
                    formulatedOldDN = childRDN + "," + _ParentOldDN;
                    ;

                    // *******************************************
                    // Place Entry into TreeMap.
                    LEAFmap.put(pDN.getX500Name(), formulatedOldDN);

                } // End of Inner If.
            } // End of While Loop.

        } catch (NameNotFoundException nfe) {
            return (LEAFmap);
        } // End of exception

        // ********************************
        // Return the TreeMap.
        return (LEAFmap);

    } // End of obtainLeafEntries.

    /**
     * convertX500NameToLDAPName.
     *
     * @param _x500name X500Name
     * @return String LDAPName
     */
    private String convertX500NameToLDAPName(String _x500name) {

        // ***************************************
        // Initialize.
        String _ldapname = "";

        // ***************************************
        // Now Parse out the X500 Domains to
        // Create the GLuE Nodes.
        //
        StringTokenizer NODES = new StringTokenizer(_x500name, "/");
        while (NODES.hasMoreTokens()) {
            String node = (String) NODES.nextToken();
            if ((node == null) || (node.equals(""))) {
                continue;
            }

            // **********************************
            // Place the Node at the Begining of
            // the LDAP Name.
            if (_ldapname.equals("")) {
                _ldapname = node;
            } else {
                _ldapname = node + "," + _ldapname;
            }
        } // End of While.

        // ***************************************
        // Return the LDAP Name.
        return (_ldapname);
    } // End of convertX500NameToLDAPName.

} // End of Class IRRChangePoller
