/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */
#include "xp.h"

#include "abcinfo.h"
#include "abpane2.h"
#include "abpicker.h"
#include "abmodel.h" 
#include "dirprefs.h"
#include "xpgetstr.h"
#include "prefapi.h"
#include "msgurlq.h"
#include "ldap.h"
#include "addrbook.h"
#include "addrprsr.h"

extern "C" {
extern int XP_BKMKS_IMPORT_ADDRBOOK;
extern int XP_BKMKS_SAVE_ADDRBOOK;
extern int MK_MSG_ADDRESS_BOOK;
extern int MK_ADDR_MOVE_ENTRIES;
extern int MK_ADDR_COPY_ENTRIES;
extern int MK_ADDR_DELETE_ENTRIES;

#if !defined(MOZADDRSTANDALONE)
extern int	MK_ADDR_FIRST_LAST_SEP;
/* extern int	MK_ADDR_LAST_FIRST_SEP; */
#endif
}

// eventually we'll want to apply some smart heuristic to increase these values during import...
// but for now we'll just use constants...
#define AB_kImportRowAmount 100  /* # of entries to import in a time slice.. */
#define AB_kImportFileByteCount 5000 /* file byte count limit */
#define AB_kExportRowCount  100
#define AB_kExportFileByteCount 5000

#define AB_kCopyInfoAmount 5   /* # of entries to copy/move/delete at a time */

AB_ReferencedObject::AB_ReferencedObject()
{
	m_refCount = 1;
	m_objectState = AB_ObjectOpen;
}

AB_ReferencedObject::~AB_ReferencedObject()
{
	XP_ASSERT(m_refCount == 0);
}

int32 AB_ReferencedObject::Acquire()
{
	return ++ m_refCount;
}

int32 AB_ReferencedObject::Release()
{
	int32 refCount = --m_refCount;
	if (m_refCount <= 0)
	{
		if (m_objectState == AB_ObjectOpen)
		{
			Close();
			GoAway();  /* do not call delete here...if the object is deletable, GoAway will do so */
		}
	}
	return refCount;
}

int AB_ReferencedObject::Close() 
{ 
	m_objectState = AB_ObjectClosed;
	return AB_SUCCESS;
}

AB_ContainerInfo::AB_ContainerInfo(MWContext * context) : ab_View(ab_Usage::kHeap, ab_Change_kAllRowChanges | ab_Change_kClosing), AB_ReferencedObject()
{
	m_context = context;
	m_sortAscending = TRUE;
	m_sortedBy = AB_attribDisplayName;
	m_pageSize = 0; 
	m_IsCached = TRUE;  // by default, all ctrs are cached. If you develop a ctr that is not cached, set this flag in its constructor
	m_isImporting = FALSE;
	m_isExporting = FALSE;

	m_db = (AB_Database *)XP_CALLOC(1, sizeof(AB_Database));
}

AB_ContainerInfo::~AB_ContainerInfo()
{
	XP_ASSERT(m_db == NULL);
}

void AB_ContainerInfo::GoAway()
{
	// this is only called when our ref count is zero!!!
	XP_ASSERT(m_refCount <= 0);   // this insures that close has been called
	AB_Env * env = AB_Env_New();
	env->sEnv_DoTrace = 0;
	ab_Env * ev = ab_Env::AsThis(env);
	ab_View::ReleaseObject(ev); 
	AB_Env_Release(env);
}

/* static */ int AB_ContainerInfo::Create(MWContext * context, AB_ContainerInfo * parentContainer, ABID entryID, AB_ContainerInfo ** newCtr)
{
	// re-direct this because we know we want to create a non-directory container with this create call.....
	return AB_NonDirectoryContainerInfo::Create(context, parentContainer, entryID, newCtr);
}


/* static */ int AB_ContainerInfo::Create(MWContext * context, DIR_Server * server, AB_ContainerInfo ** newCtr)
{
	// re-direct this because we know we want to create a directory container. Why? 'cause we have a DIR_Server
	return AB_DirectoryContainerInfo::Create(context, server, newCtr);
}

int AB_ContainerInfo::Close()
{
	// this basically acts like the destructor! the destructor assumes that Close has always been called.
	// it will yell at us through assertion failures if we have not come through here first!

	m_objectState = AB_ObjectClosing;  // no longer open for business!!
	NotifyAnnouncerGoingAway(this);
	if (m_db)
	{
		CloseDatabase(m_db);

		XP_ASSERT(m_refCount <= 0);
		XP_ASSERT(m_db->db_row == NULL);
		XP_ASSERT(m_db->db_table == NULL);
		XP_ASSERT(m_db->db_env == NULL);
		XP_ASSERT(m_db->db_store == NULL);

		XP_FREE(m_db);
		m_db = NULL;
	}

	

	// we aren't responsible for anything else on closing...
	m_objectState = AB_ObjectClosed;  // put a fork in this container....it's done...
	return AB_SUCCESS;
}


void AB_ContainerInfo::CloseDatabase(AB_Database *db)		// releases all of our database objects, saves the content, etc. 
{
	if (db->db_row)
	{
		AB_Row_Release(db->db_row, db->db_env);
		db->db_row = NULL;
	}

	// (1) release the table
	if (db->db_table)
	{
		AB_Env_ForgetErrors(db->db_env);
		if ( ( (ab_Table *) db->db_table)->IsOpenObject() )
			((ab_Table *) db->db_table)->CutView(ab_Env::AsThis(db->db_env), this);
		
		AB_Table_Close(db->db_table, db->db_env);
		AB_Table_Release(db->db_table, db->db_env);

		// check errors...
		db->db_table = NULL;
	}

	if (db->db_listTable)
	{
		AB_Env_ForgetErrors(db->db_env);
		AB_Table_Close(db->db_listTable, db->db_env);
		AB_Table_Release(db->db_listTable, db->db_env);
		db->db_listTable = NULL;
	}

	// (2) release the store
	if (db->db_store)
	{
		AB_Store_SaveStoreContent(db->db_store, db->db_env); /* commit */
		// report any errors on the commit??? 
		AB_Env_ForgetErrors(db->db_env);
		AB_Store_CloseStoreContent(db->db_store, db->db_env);
		AB_Store_Release(db->db_store, db->db_env);
		AB_Env_ForgetErrors(db->db_env);
		db->db_store = NULL;
	}

	if (db->db_env)
	{
		AB_Env_Release(db->db_env);
		db->db_env = NULL;
	}
}

uint32 AB_ContainerInfo::GetNumEntries() // right now returns total # known entries.
{
	if (m_db->db_table && m_db->db_env)
		return AB_Table_CountRows(m_db->db_table, m_db->db_env);
	else
		return 0;
}

MSG_ViewIndex AB_ContainerInfo::GetIndexForABID(ABID entryID)
{
	MSG_ViewIndex index = MSG_VIEWINDEXNONE;
	if (m_db->db_env && m_db->db_table && entryID != AB_ABIDUNKNOWN)
	{
		AB_Env_ForgetErrors(m_db->db_env);
		ab_row_pos rowPos = AB_Table_RowPos(m_db->db_table, m_db->db_env, (ab_row_uid) entryID);
		AB_Env_ForgetErrors(m_db->db_env); // we can forget any errors 'cause if it failed, we'll return MSG_VIEWINDEXNONE
		if (rowPos) // 1 based index...
			index = (MSG_ViewIndex) (rowPos - 1); // - 1 because database is one indexed...our world is zero indexed.
	}

	return index;
}

char * AB_ContainerInfo::GetDescription() // caller must free the char *
{
	if (m_description)
		return XP_STRDUP(m_description);
	else
		return NULL;
}

AB_LDAPContainerInfo * AB_ContainerInfo::GetLDAPContainerInfo()
{ 
	return NULL;
}

AB_PABContainerInfo * AB_ContainerInfo::GetPABContainerInfo()
{
	return NULL;
}

AB_MListContainerInfo * AB_ContainerInfo::GetMListContainerInfo()
{
	return NULL;
}

XP_Bool AB_ContainerInfo::IsInContainer(ABID entryID)
{
	if (entryID != AB_ABIDUNKNOWN)
		return TRUE;
	else
		return FALSE;
}

XP_Bool AB_ContainerInfo::IsIndexInContainer(MSG_ViewIndex index)
{
	if (index != MSG_VIEWINDEXNONE)
	{
		if (index + 1 <= AB_ContainerInfo::GetNumEntries())
			return TRUE;
	}
	
	return FALSE;
}

/* CompareSelections
 *
 * This is sort of a virtual selection comparison function.  We must implement
 * it this way because the compare function must be a static function.
 */
int AB_ContainerInfo::CompareSelections(const void *e1, const void *e2)
{
 	/* Currently, only the LDAP container implements extended selection, so
	 * we can just call its comparison function.  In the future, if we have
	 * several classes that support extended selection the first element
	 * in the selection entry could be a type and we could switch on that
	 * for the proper comparison function to call.
	 */
	return AB_LDAPContainerInfo::CompareSelections(e1, e2);
}

ABID AB_ContainerInfo::GetABIDForIndex(const MSG_ViewIndex index)
{
	ABID entryID = AB_ABIDUNKNOWN;
	if (IsOpen() && m_db->db_env && m_db->db_table && index != MSG_VIEWINDEXNONE && index < GetNumEntries())
	{
		AB_Env_ForgetErrors(m_db->db_env);  // in case someone didn't clear any previous errors
		ab_row_uid rowUID = AB_Table_GetRowAt(m_db->db_table, m_db->db_env, (ab_row_pos) (index + 1));  // + 1 because db is one indexed, we are zero indexed
		AB_Env_ForgetErrors(m_db->db_env);  // we can ignore error codes because if it failed, then index not in table..
		if (rowUID)  // is it in the table i.e. > 0
			entryID = (ABID) rowUID;
	}

	return entryID;
}

void AB_ContainerInfo::UpdatePageSize(uint32 pageSize)
{
	if (pageSize > m_pageSize)
		m_pageSize = pageSize;
	
	// probably need to notify whoever is running the virtual list stuff that the page size has changed..
}

int AB_ContainerInfo::GetNumColumnsForContainer()
{
	// for right now, we have a fixed number of columns...0 based...
	return AB_NumberOfColumns;   // enumerated columnsID type
}

int AB_ContainerInfo::GetColumnAttribIDs(AB_AttribID * attribIDs, int * numAttribs)
{
	// this is just for starters...eventually we want to hook into the DIR_Server and update ourselves
	// with any changes....

	// okay this is hacky and not good since I fail completely if the array isn't big enough...
	// but the code should only be temporary =)

	if (*numAttribs == AB_NumberOfColumns)
	{
		attribIDs[0] = AB_attribEntryType;
		attribIDs[1] = /* AB_attribFullName */ AB_attribDisplayName;
		attribIDs[2] = AB_attribEmailAddress;
		attribIDs[3] = AB_attribCompanyName;
		attribIDs[4] = AB_attribWorkPhone;
		attribIDs[5] = AB_attribLocality;
		attribIDs[6] = AB_attribNickName;
	}

	else
		*numAttribs = 0;

	return AB_SUCCESS;
}


AB_ColumnInfo * AB_ContainerInfo::GetColumnInfo(AB_ColumnID columnID)
{
	AB_ColumnInfo * columnInfo = (AB_ColumnInfo *) XP_ALLOC(sizeof(AB_ColumnInfo));
	if (columnInfo)
	{
		switch (columnID)
		{
			case AB_ColumnID0:
				columnInfo->attribID = AB_attribEntryType;
				columnInfo->displayString = NULL;
				break;
			case AB_ColumnID1:
				columnInfo->attribID= AB_attribDisplayName;
				columnInfo->displayString = XP_STRDUP("Name");
				break;
			case AB_ColumnID2:
				columnInfo->attribID = AB_attribEmailAddress;
				columnInfo->displayString = XP_STRDUP("Email Address");
				break;
			case AB_ColumnID3:
				columnInfo->attribID = AB_attribCompanyName;
				columnInfo->displayString = XP_STRDUP("Organization");
				break;
			case AB_ColumnID4:
				columnInfo->attribID = AB_attribWorkPhone;
				columnInfo->displayString = XP_STRDUP("Phone");
				break;
			case AB_ColumnID5:
				columnInfo->attribID = AB_attribLocality;
				columnInfo->displayString = XP_STRDUP("City");
				break;
			case AB_ColumnID6:
				columnInfo->attribID = AB_attribNickName;
				columnInfo->displayString = XP_STRDUP("NickName");
				break;
			default:
				XP_ASSERT(0);
				XP_FREE(columnInfo);
				columnInfo = NULL;
		}

		if (columnInfo)
			columnInfo->sortable = IsEntryAttributeSortable(columnInfo->attribID);
	}

	return columnInfo;
}

// Methods for obtaining a container's child containers.
int32 AB_ContainerInfo::GetNumChildContainers()  // returns # of child containers inside this container...
{
	int childContainerCount = 0;
	if (m_db->db_listTable)
	{
		childContainerCount =  AB_Table_CountRows(m_db->db_listTable, m_db->db_env);
		if (AB_Env_Good(m_db->db_env))
			return childContainerCount;
		AB_Env_ForgetErrors(m_db->db_env);
	}

	return childContainerCount;
}

int AB_ContainerInfo::AcquireChildContainers(AB_ContainerInfo ** arrayOfContainers /* allocated by caller */, 
										int32 * numContainers /* in - size of array, out - # containers added */)
// All acquired child containers are reference counted here! Caller does not need to do it...but the do need to
// release these containers when they are done!
{
	int status = AB_FAILURE;
	int32 numAdded = 0;

	// enumerate through the list table getting the ABID for each index. Make a ctr from each one
	if (numContainers && *numContainers > 0 && m_db->db_listTable && m_db->db_env)
	{
		for (int32 index = 0; index < GetNumChildContainers()  && index < *numContainers; index++)
		{
			arrayOfContainers[index] = NULL;  // always make sure the entry will have some value...
			ab_row_uid rowUID = AB_Table_GetRowAt(m_db->db_listTable, m_db->db_env, (ab_row_pos) (index + 1));  // + 1 because db is one indexed, we are zero indexed
			if (rowUID)
				AB_ContainerInfo::Create(m_context,this, (ABID) rowUID, &(arrayOfContainers[index]));

		}

		numAdded = *numContainers; // we actually stored something for every value
		status = AB_SUCCESS;
	}

	if (numContainers)
		*numContainers = numAdded;

	return status;
}

XP_Bool AB_ContainerInfo::IsEntryAContainer(ABID entryID)
{
	// right now a mailing list is our only container entry in a container....
	AB_AttributeValue * value = NULL;
	XP_Bool containerEntry = FALSE;
	GetEntryAttribute(NULL, entryID, AB_attribEntryType, &value);
	if (value)
	{
		if (value->u.entryType == AB_MailingList)
			containerEntry = TRUE;
		AB_FreeEntryAttributeValue(value);
	}
	return containerEntry;
}

// Adding Entries to the Container
int AB_ContainerInfo::AddEntry(AB_AttributeValue * values, uint16 numItems, ABID * entryID /* our return value */)
{
	int status = AB_FAILURE;
	if (m_db->db_row && m_db->db_env)
	{
		// clear the row out...
		AB_Row_ClearAllCells(m_db->db_row, m_db->db_env);
		const char * content;
		// now write the attributes for our new value into the row....
		for (uint16 i = 0; i < numItems; i++)
		{
			content = ConvertAttribValueToDBase(&values[i]);
			AB_Row_WriteCell(m_db->db_row, m_db->db_env, content, AB_Attrib_AsStdColUid(ConvertAttribToDBase(values[i].attrib)));
		}

		// once all the cells have been written, write the row to the table
		ab_row_uid RowUid = AB_Row_NewTableRowAt(m_db->db_row, m_db->db_env, 0);
		AB_Env_ForgetErrors(m_db->db_env);

		if (RowUid > 0) // did it succeed?
		{
			if (entryID)
				*entryID = (ABID) RowUid;
			status = AB_SUCCESS;
		}

		AB_Env_ForgetErrors(m_db->db_env);
	}

	return status;
}

int AB_ContainerInfo::AddUserWithUI(MSG_Pane * srcPane, AB_AttributeValue * values,  uint16 numItems, XP_Bool /* lastOneToAdd */)
{
	int status = AB_SUCCESS;
	// create a person pane, initialize it with our values, 
	if (srcPane)
	{
		AB_PersonPane * personPane = new AB_PersonPane(srcPane->GetContext(), srcPane->GetMaster(), this, AB_ABIDUNKNOWN, NULL);
		if (personPane)
		{
			personPane->InitializeWithAttributes(values, numItems);
			AB_ShowPropertySheetForEntryFunc * func = srcPane->GetShowPropSheetForEntryFunc();
			if (func)
				func (personPane, srcPane->GetContext());
		}
		else
			status = AB_FAILURE;

	}
	else
		status = ABError_NullPointer;
	return status;
}

int AB_ContainerInfo::AddSender(char * /* author */, char * /* url */)
{
	return AB_SUCCESS;
}

/*******************************************
	Asynchronous import/export methods...
********************************************/
int AB_ContainerInfo::ImportBegin(MWContext * /* context */, const char * fileName, void ** importCookie, XP_Bool * importFinished)
{
	if (importCookie)
		*importCookie = NULL;
	int status = AB_SUCCESS;
	if (/* !m_isImporting && */ m_db->db_store && m_db->db_env && fileName)
	{
//		m_isImporting = TRUE;
		AB_ImportState * importState = (AB_ImportState *) XP_ALLOC(sizeof(AB_ImportState));
		if (importState)
		{
			importState->db_file = AB_Store_NewImportFile(m_db->db_store, m_db->db_env, fileName);
			importState->db_thumb = AB_Store_NewThumb(m_db->db_store, m_db->db_env, AB_kImportRowAmount, AB_kImportFileByteCount /* file byte count limit */);
			if (AB_Env_Good(m_db->db_env))
			{
				if (importCookie)
					*importCookie = (void *) (importState);
				status = ImportMore(importCookie, importFinished);
			}
			else
			{
				AB_Env_ForgetErrors(m_db->db_env);
				// need some suitable warning to the user saying that the import failed.....
			}
		}
		else
			status = AB_OUT_OF_MEMORY;
	}
	else
		status = AB_FAILURE;

	return status;
}

int AB_ContainerInfo::ImportMore(void ** importCookie, XP_Bool * importFinished)
// if we are finished, import cookie is released...
{
	int status = AB_SUCCESS;
	XP_Bool keepImporting = FALSE;
	if (importCookie && *importCookie && m_db->db_store && m_db->db_env)
	{
		AB_ImportState * importState = (AB_ImportState *) (*importCookie);
		keepImporting = AB_Store_ContinueImport(m_db->db_store, m_db->db_env, importState->db_file, importState->db_thumb);
		if (AB_Env_Bad(m_db->db_env))
		{
			// display some suitable error message....need more info...
			AB_Env_ForgetErrors(m_db->db_env);
		}

		if (importFinished)
			*importFinished = !keepImporting;
	}

	return status;
}

int AB_ContainerInfo::ImportInterrupt(void ** importCookie) // free import state and termineate import
{

	// right now, import always means finish import...i.e. we always stop the import on interrupt...
	return ImportFinish(importCookie);
}

int AB_ContainerInfo::ImportFinish(void ** importCookie)
{
	AB_ImportState * importState = NULL;
	if (importCookie)
		importState = (AB_ImportState *) (*importCookie);

	if (importState)
	{
		AB_File_Release(importState->db_file, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env);
		AB_Thumb_Release(importState->db_thumb, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env);
		XP_FREE(importState);
	}

	if (importCookie) 
		*importCookie = NULL; // we have freed the cookie...

	m_isImporting = FALSE; // we are no longer attempting to import a file...

	return AB_SUCCESS;
}

/* used to determine progress on the import...*/
int AB_ContainerInfo::ImportProgress(void * importCookie, uint32 * position, uint32 * fileLength, uint32 * passCount)
{
	AB_ImportState * importState = (AB_ImportState *) importCookie;
	int status = AB_SUCCESS;
	if (importState && m_db->db_env)
	{
		uint32 pos = AB_Thumb_ImportProgress(importState->db_thumb, m_db->db_env, (ab_pos *) fileLength, (ab_count *) passCount);
		if (position)
			*position = pos;
		AB_Env_ForgetErrors(m_db->db_env);
	}
	else
		status = ABError_NullPointer;

	return status;
}

int AB_ContainerInfo::ExportBegin(MWContext * context, const char * fileName, void ** exportCookie, XP_Bool * exportFinished)
{
	if (exportCookie)
		*exportCookie = NULL;
	int status = AB_SUCCESS;
	if (m_db->db_store && m_db->db_env && fileName)
	{
		AB_ImportState * exportState = (AB_ImportState *) XP_ALLOC(sizeof(AB_ImportState));
		if (exportState)
		{
			exportState->db_file = AB_Store_NewExportFile(m_db->db_store, m_db->db_env, fileName);
			exportState->db_thumb = AB_Store_NewThumb(m_db->db_store, m_db->db_env, AB_kExportRowCount, AB_kExportFileByteCount);
			if (AB_Env_Good(m_db->db_env))
			{
				if (exportCookie)
					*exportCookie = (void *) (exportState);
				status = ExportMore(exportCookie, exportFinished);
			}
			else
			{
				AB_Env_ForgetErrors(m_db->db_env);
				// need some suitable warning to the user saying that the export failed.....
			}
		}
		else
			status = AB_OUT_OF_MEMORY;
	}
	else
		status = AB_FAILURE;

	return status;
}

int AB_ContainerInfo::ExportMore(void ** exportCookie, XP_Bool * exportFinished)
{
	int status = AB_SUCCESS;
	XP_Bool keepExporting = FALSE;
	if (exportCookie && *exportCookie && m_db->db_store && m_db->db_env)
	{
		AB_ImportState * exportState = (AB_ImportState *) (*exportCookie);
		keepExporting = AB_Store_ContinueExport(m_db->db_store, m_db->db_env, exportState->db_file, exportState->db_thumb);
		if (AB_Env_Bad(m_db->db_env))
		{
			// display some suitable error message....need more info...
			AB_Env_ForgetErrors(m_db->db_env);
		}

		if (exportFinished)
			*exportFinished = !keepExporting;
	}

	return status;
}

int AB_ContainerInfo::ExportInterrupt(void ** exportCookie)
{

	// right now, import always means finish import...i.e. we always stop the import on interrupt...
	return ExportFinish(exportCookie);
}

int AB_ContainerInfo::ExportProgress(void * exportCookie, uint32 * numberExported, uint32 * totalEntries)
{
	AB_ImportState * exportState = (AB_ImportState *) exportCookie;
	int status = AB_SUCCESS;
	if (exportState && m_db->db_env)
	{
		uint32 num = AB_Thumb_ExportProgress(exportState->db_thumb, m_db->db_env, (ab_row_count *) totalEntries);
		if (numberExported)
			*numberExported = num;
		AB_Env_ForgetErrors(m_db->db_env);
	}
	else
		status = ABError_NullPointer;

	return status;
}

int AB_ContainerInfo::ExportFinish(void ** exportCookie)
{
	AB_ImportState * exportState = NULL;
	if (exportCookie)
		exportState = (AB_ImportState *) (*exportCookie);
	if (exportState)
	{
		AB_File_Release(exportState->db_file, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env);
		AB_Thumb_Release(exportState->db_thumb, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env);
		XP_FREE(exportState);
	}
	if (exportCookie)
		*exportCookie = NULL; 

	m_isExporting = FALSE;

	return AB_SUCCESS;
}

/*****************************************************************

  Basic import methods......(i.e. those called by panes, FEs, etc.

******************************************************************/


/* static */ void AB_ContainerInfo::ImportFileCallback(MWContext * context, char * fileName, void * closure)
{
	// notice that we don't ref count the ctr inside the closure....control eventually returns to the
	// caller so it is responsible for acquiring/releasing the ctr as necessary..
	AB_ImportClosure * importClosure = (AB_ImportClosure *) closure;
	if (importClosure)
	{
		if (importClosure->container)
			importClosure->container->ImportFile(importClosure->pane, context, fileName);
	}

	XP_FREEIF(fileName);
}

int AB_ContainerInfo::ImportFile(MSG_Pane * pane, MWContext * /* context */, const char * fileName)
{
	if (!m_isImporting && m_db->db_store && m_db->db_env)
	{
		m_isImporting = TRUE;
		// now create an import URL and add it to the queue
		char * url = PR_smprintf("%s%s", "addbook:import?file=", fileName);
		URL_Struct *url_struct = NET_CreateURLStruct(url, NET_DONT_RELOAD);
		if (url_struct)
		{
			Acquire();  // add a ref count for the URL copy 
			url_struct->owner_data = (void *) this;
			if (pane)
				MSG_UrlQueue::AddUrlToPane(url_struct, NULL, pane, FALSE, NET_DONT_RELOAD);
			else
				NET_GetURL(url_struct, FO_PRESENT, m_context, NULL);
		}

		XP_FREEIF(url);

	}
	return AB_SUCCESS;
}
	
int AB_ContainerInfo::ImportData(MSG_Pane * pane, const char * buffer, int32 /* bufSize */, AB_ImportExportType dataType)
{
	int status = AB_SUCCESS;
	AB_ImportClosure * closure = NULL;
	if (m_db->db_env && m_db->db_store)
	{
		switch (dataType)
		{
			case AB_PromptForFileName:
				closure = (AB_ImportClosure *) XP_ALLOC(sizeof(AB_ImportClosure));
				if (closure)
				{
					closure->container = this; // note: we aren't ref counting this...
					closure->pane = pane;
					FE_PromptForFileName(m_context,XP_GetString(XP_BKMKS_IMPORT_ADDRBOOK),0, TRUE, FALSE, AB_ContainerInfo::ImportFileCallback, closure);
					XP_FREE(closure);
				}
				else
					status = AB_OUT_OF_MEMORY;
				break;

			case AB_Vcard:
				status = AB_ImportVCards(this, buffer); /* buffer should be NULL terminated */
				break;

			/* we'll add comma list and raw data here as they come on line...for now...*/
			default:
				/* don't call the database here....FEs pass in prompt for file name.....what if the type is vcard? 
				   that is a non - db type... */
				/* AB_Store_ImportWithPromptForFileName(m_db->db_store, m_db->db_env, m_context); */
				break;
		}
	}
	return status;
}

XP_Bool AB_ContainerInfo::ImportDataStatus(AB_ImportExportType /* dataType */)
{
	// Keep it simple for now....PABs and Mailing lists will accept importing from any type (filename,vcard, raw data, etc.)
	if (GetType() != AB_LDAPContainer)
		return TRUE;
	else
		return FALSE;
}

int AB_ContainerInfo::ExportData(MSG_Pane * pane, char ** /* buffer */, int32 * /* bufSize */, AB_ImportExportType dataType)
{
	int status = AB_SUCCESS;
	AB_ImportClosure * closure = NULL;
	if (m_db->db_env && m_db->db_store)
	{
		switch (dataType)
		{
			case AB_PromptForFileName:
				closure = (AB_ImportClosure *) XP_ALLOC(sizeof(AB_ImportClosure));
				if (closure)
				{
					closure->container = this;
					closure->pane = pane;
					FE_PromptForFileName(m_context, XP_GetString(XP_BKMKS_SAVE_ADDRBOOK), 0, FALSE, FALSE, AB_ContainerInfo::ExportFileCallback, closure);
					XP_FREE(closure);
				}
				else
					status = AB_OUT_OF_MEMORY;
				break;
			default:
				/* don't call dbase....some types (like vcard) when I fill them in don't invole dbase's export APIs...*/
				break;
		}
	}
	return status;
}
/* static */ void AB_ContainerInfo::ExportFileCallback(MWContext * context, char * fileName, void * closure)
{
	// notice that we don't ref count the ctr inside the closure....control eventually returns to the
	// caller so it is responsible for acquiring/releasing the ctr as necessary..
	AB_ImportClosure * importClosure = (AB_ImportClosure *) closure;
	if (importClosure)
	{
		if (importClosure->container)
			importClosure->container->ExportFile(importClosure->pane, context, fileName);
	}

	XP_FREEIF(fileName);
}

int AB_ContainerInfo::ExportFile(MSG_Pane * pane, MWContext * /* context */, const char * fileName)
{
	if (!m_isExporting && m_db->db_store && m_db->db_env)
	{
		m_isExporting = TRUE;
		
		// now create an export URL and add it to the queue
		char * url = PR_smprintf("%s%s", "addbook:export?file=", fileName);
		URL_Struct *url_struct = NET_CreateURLStruct(url, NET_DONT_RELOAD);
		if (url_struct)
		{
			Acquire();  // add a ref count for the URL copy 
			url_struct->owner_data = (void *) this;
			if (pane)
				pane->GetURL(url_struct, TRUE); // launch the url...
			else
				NET_GetURL(url_struct, FO_PRESENT, m_context, NULL);
		}

		XP_FREEIF(url);

	}
	return AB_SUCCESS;
}


int AB_ContainerInfo::UpdateDIRServer() // FE has changed DIR_Server, update self and notify any listeners
{
	return AB_SUCCESS;
}
/*
	Getting / Setting Container Attributes
*/

int AB_ContainerInfo::GetAttribute(AB_ContainerAttribute attrib, AB_ContainerAttribValue * value /* already allocated */)
{
	if (value)
	{
		value->attrib = attrib;
		switch(attrib)
		{
		case attribContainerType:
			value->u.containerType = GetType();
			break;
		case attribName:
			if (m_description)
				value->u.string = XP_STRDUP(m_description);
			else
				value->u.string = XP_STRDUP(""); // assign empty string as a default value? 
			break;
		case attribNumChildren:
			value->u.number = GetNumChildContainers();
			break;
		case attribDepth:
			value->u.number = m_depth;
			break;
		case attribContainerInfo:
			value->u.container = this;
			break;
		default:   // most unexpected this is...
			return AB_INVALID_ATTRIBUTE;
		}

		return AB_SUCCESS;
	}

	return AB_FAILURE;
}

// Container Attributes
int AB_ContainerInfo::GetAttribute(AB_ContainerAttribute attrib, AB_ContainerAttribValue ** newValue)
{
	AB_ContainerAttribValue * value = new AB_ContainerAttribValue;
	if (!value)
		return AB_OUT_OF_MEMORY;
	
	*newValue = value;  // point to our new attribute value
	return GetAttribute(attrib, value);
}

int AB_ContainerInfo::SetAttribute(AB_ContainerAttribValue * value)
{
	if (value)
	{
		switch(value->attrib)
		{
		case attribName:
			// WARNING! What do we do with the old name? free it?
			XP_FREE(m_description);
			m_description = XP_STRDUP(value->u.string);
			break;
		default: // otherwise the attribute is not setable!
			return AB_INVALID_ATTRIBUTE;  
		}
		
		return AB_SUCCESS;
	}

	return AB_FAILURE;
}


int AB_ContainerInfo::GetAttributes(AB_ContainerAttribute * attribArray, AB_ContainerAttribValue ** valueArray, uint16 * numItems)
{
	int errorCode = AB_SUCCESS;
	int error = AB_SUCCESS; // error returned from calling another function
	*valueArray = NULL;
	if (*numItems > 0)
	{
		AB_ContainerAttribValue * values = (AB_ContainerAttribValue *) XP_ALLOC(sizeof(AB_ContainerAttribValue) * (*numItems));
		if (values)
		{
			for (uint16 i = 0; i < *numItems; i++)
				if ( (error = GetAttribute(attribArray[i], &values[i])) != AB_SUCCESS)
					errorCode = error;
			*valueArray = values;
		}
		else
			errorCode = AB_OUT_OF_MEMORY;
	}

	return errorCode;
}

int AB_ContainerInfo::SetAttributes(AB_ContainerAttribValue * valuesArray, uint16 numItems)
{
	int errorCode = AB_SUCCESS;
	int error = AB_SUCCESS;

	// set each attribute...return last non-success error message
	for (uint16 i = 0; i < numItems; i++)
		if ((error = SetAttribute(&valuesArray[i])) != AB_SUCCESS)
			errorCode = error;

	return errorCode;
}

// assumes destValue has already been allocated
int AB_ContainerInfo::AssignEntryValues(AB_AttributeValue *srcValue, AB_AttributeValue * destValue)
{
	return AB_CopyEntryAttributeValue(srcValue, destValue);
}

int AB_ContainerInfo::AssignEmptyEntryValue(AB_AttribID attrib, AB_AttributeValue * value /* already allocated */)
{
	return AB_CopyDefaultAttributeValue(attrib, value);
}

// Entry Attribute Accessors 
int AB_ContainerInfo::GetEntryAttribute(MSG_Pane * srcPane, ABID entryID, AB_AttribID attrib, AB_AttributeValue ** newValue)
{
	uint16 numItems = 1;
	return GetEntryAttributes(srcPane, entryID, &attrib, newValue, &numItems); 
}

XP_Bool AB_ContainerInfo::IsDatabaseAttribute(AB_AttribID attrib)
{
	switch (attrib)
	{
		case AB_attribFullAddress:
		case AB_attribVCard:
			return FALSE;
		default:
			return TRUE;
	}

}

int AB_ContainerInfo::GetNonDBEntryAttributeForIndex(MSG_Pane * srcPane, MSG_ViewIndex index, AB_AttribID attrib, AB_AttributeValue * value /* must already be allocated!!!*/)
{
	ABID entryID = GetABIDForIndex(index);
	return GetNonDBEntryAttribute(srcPane, entryID, attrib, value);
}

int AB_ContainerInfo::GetNonDBEntryAttribute(MSG_Pane * srcPane, ABID entryID, AB_AttribID attrib, AB_AttributeValue * value /* must already be allocated!!!*/)
{
	int status = AB_SUCCESS;

	if (value)
	{
		value->attrib = attrib;
		switch (attrib)
		{
		case AB_attribFullAddress:
				value->u.string = GetFullAddress(srcPane, entryID);
				break;
			case AB_attribVCard:
				value->u.string = GetVCard(srcPane, entryID);
				break;
			default:
				AB_CopyDefaultAttributeValue(attrib,value);
				break;
		}
	}
	else
		status = AB_FAILURE;

	return status;
}

int AB_ContainerInfo::GetEntryAttribute(MSG_Pane * srcPane, ABID entryID, AB_AttribID attrib, AB_AttributeValue * value /* must already be allocated!!!*/)
{	
	int status = AB_FAILURE;
	if (IsDatabaseAttribute(attrib))
	{
		if (m_db->db_row && value)
		{
			if (AB_Row_ReadTableRow(m_db->db_row, m_db->db_env, entryID))  // read in the row from the table
			{
				const AB_Cell * cell = AB_Row_GetColumnCell(m_db->db_row, m_db->db_env, AB_Attrib_AsStdColUid(ConvertAttribToDBase(attrib)));
				AB_Env_ForgetErrors(m_db->db_env);
				if (cell)
				{
					value->attrib = attrib;
					status = ConvertDBaseValueToAttribValue(cell->sCell_Content, value, attrib);
				}
			}
		}
		// if we had a problem, return a default attribute
		if (status == AB_FAILURE)
			status = AB_CopyDefaultAttributeValue(attrib,value);
	}
	else
		status = GetNonDBEntryAttribute(srcPane, entryID, attrib, value);

	return status;
}

int AB_ContainerInfo::GetEntryAttributeForIndex(MSG_Pane * srcPane, MSG_ViewIndex index, AB_AttribID attrib, AB_AttributeValue ** value)
{
	uint16 numItems = 1;
	return GetEntryAttributesForIndex(srcPane, index, &attrib, value, &numItems /* special case of get entry attributes */);
}
	

int AB_ContainerInfo::GetEntryAttributesForIndex(MSG_Pane * srcPane, MSG_ViewIndex index, AB_AttribID * attribs, AB_AttributeValue ** valuesArray, uint16 * numItems)
{
	uint32 numEntries = GetNumEntries();

	if (index != MSG_VIEWINDEXNONE && index < numEntries)
	{
		int status = AB_SUCCESS;
		// okay, row positions in the database are ONE based. MSG_ViewIndices are ZERO based...need to convert to a row position..
		ab_row_pos rowPos = (ab_row_pos) (index + 1);
		uint16 numItemsAdded = 0; 
		AB_AttributeValue * values = NULL;

		if (numItems && *numItems > 0)
		{
			values = (AB_AttributeValue *) XP_ALLOC(sizeof(AB_AttributeValue) * (*numItems));
			if (values)
			{
				if (m_db->db_row)
				{
					AB_Row_ClearAllCells(m_db->db_row, m_db->db_env);

					if (AB_Env_Good(m_db->db_env) && AB_Row_ReadTableRowAt(m_db->db_row, m_db->db_env, rowPos))
					{
						for (uint16 i = 0; i < *numItems; i++) // for each attribute, process it
						{
							int loopStatus = AB_FAILURE;

							if (IsDatabaseAttribute(attribs[i])) // try to read in the database attribute
							{
								const AB_Cell * cell = AB_Row_GetColumnCell(m_db->db_row, m_db->db_env,AB_Attrib_AsStdColUid(ConvertAttribToDBase(attribs[i])));
								AB_Env_ForgetErrors(m_db->db_env); // come back later and pick up errors...
								values[numItemsAdded].attrib = attribs[numItemsAdded];
								if (cell)
									loopStatus = ConvertDBaseValueToAttribValue(cell->sCell_Content, &values[numItemsAdded], attribs[i]);
								else // if we had a problem, return a default attribute
									loopStatus = AB_CopyDefaultAttributeValue(attribs[numItemsAdded], &values[numItemsAdded]);
							} // if not a database attribute, generate it
							else
								loopStatus = GetNonDBEntryAttributeForIndex(srcPane, index, attribs[i], &values[numItemsAdded]);

							if (loopStatus == AB_SUCCESS) // then we succeeded in adding the entry
								numItemsAdded++;
						} /* for loop */
					} /* reading the row */
				}
			} /* values exists? */
			else
				status = AB_OUT_OF_MEMORY;
		}

		if (status != AB_SUCCESS && values)
		{
			XP_FREE(values); 
			values = NULL;
		}

		*valuesArray = values;
		*numItems = numItemsAdded;
		return status;
	}
	else
	{
		*valuesArray = NULL;
		return AB_FAILURE;  // we had an invalid index
	}
}

int AB_ContainerInfo::GetEntryAttributes(MSG_Pane * srcPane, ABID entryID, AB_AttribID * attribs, AB_AttributeValue ** valuesArray, uint16 * numItems)
{
	MSG_ViewIndex index = GetIndexForABID(entryID);
	return GetEntryAttributesForIndex(srcPane, index, attribs, valuesArray, numItems);
}

/*
	Setting entry attributes
*/

int AB_ContainerInfo::SetEntryAttributeForIndex(MSG_ViewIndex index, AB_AttributeValue * value)
{
	return SetEntryAttributesForIndex(index, value, 1);
}

int AB_ContainerInfo::SetEntryAttributesForIndex(MSG_ViewIndex index, AB_AttributeValue * valuesArray, uint16 numItems)
{
	// setting converts to an ABID and performs the set. Right now, database does not allow us to set attributes
	// based on row position. 
	ABID entryID = GetABIDForIndex(index);
	return SetEntryAttributes(entryID, valuesArray, numItems);
}
int AB_ContainerInfo::SetEntryAttribute(ABID entryID, AB_AttributeValue * value)
{
	return SetEntryAttributes(entryID, value, 1);
}

int AB_ContainerInfo::SetEntryAttributes(ABID entryID, AB_AttributeValue * valuesArray, uint16 numItems)
{
	int status = AB_FAILURE;
	AB_Env_ForgetErrors(m_db->db_env);

	if (m_db->db_row)
	{
		if (AB_Row_ReadTableRow(m_db->db_row, m_db->db_env, entryID)) // read in the row from the table
		{
			AB_Env_ForgetErrors(m_db->db_env);  // come back later and check for errors...
			const char * content = NULL;
			status = AB_SUCCESS;
			for (int i = 0; i < numItems && status == AB_SUCCESS; i++)
			{
				content = ConvertAttribValueToDBase(&valuesArray[i]);
				if (!AB_Row_WriteCell(m_db->db_row, m_db->db_env, content, AB_Attrib_AsStdColUid(ConvertAttribToDBase(valuesArray[i].attrib))))
					status = AB_FAILURE;  // it didn't write!!!
			}
			AB_Env_ForgetErrors(m_db->db_env);  // status would have caught the write cell error
			if (status == AB_SUCCESS)
			{
				AB_Row_UpdateTableRow(m_db->db_row, m_db->db_env, entryID);  // commit changes back to the database
				if (AB_Env_Bad(m_db->db_env))
					status = AB_FAILURE;
			}
		}
	}

	return status;
}

int AB_ContainerInfo::SortByAttribute(AB_AttribID attrib, XP_Bool ascending)
{
	m_sortedBy = attrib;
	m_sortAscending = ascending;
	return AB_SUCCESS;
}

int AB_ContainerInfo::SortByAttribute(AB_AttribID attrib)
{
	m_sortedBy = attrib;
	return AB_SUCCESS;
}

void AB_ContainerInfo::SetSortAscending(XP_Bool ascending)
{
	m_sortAscending = ascending;
}

int AB_ContainerInfo::GetSortedBy(AB_AttribID * attrib)
{
	*attrib = m_sortedBy;
	return AB_SUCCESS;
}

char * AB_ContainerInfo::GetVCard(MSG_Pane * /*srcPane*/, ABID entryID)		  // caller must free null terminated string returned
{
	char * VCardString = NULL;

	if (m_db && m_db->db_row && m_db->db_env && entryID != AB_ABIDUNKNOWN)
	{
		AB_Row_ClearAllCells(m_db->db_row, m_db->db_env);
		if ( AB_Env_Good(m_db->db_env) && AB_Row_ReadTableRow(m_db->db_row, m_db->db_env, (ab_row_pos) entryID) )
		{
			VCardString = AB_Row_AsVCardString(m_db->db_row, m_db->db_env);
			if (AB_Env_Bad(m_db->db_env))
			{
				if (VCardString)
				{
					XP_FREE(VCardString);
					VCardString = NULL;
				}

				AB_Env_ForgetErrors(m_db->db_env);
			}
		}
	}

	return VCardString;
}


/* The expandedString returned has been parsed, quoted, domain names added and can be used to send the message */
/* static */ int AB_ContainerInfo::GetExpandedHeaderForEntry(char ** expandedString, MSG_Pane * srcPane, AB_ContainerInfo * ctr, ABID entryID)
{
	// call the recursive version....we also need an IDArray to make sure we don't add duplicates as we visit all of the
	// entries on our journey...

	AB_ABIDArray idsAlreadyAdded; // we'll eventually want to make this a sorted array for performance gains on the look ups..
	int status = AB_SUCCESS;

	if (ctr)
		status = ctr->GetExpandedHeaderForEntry(expandedString, srcPane, entryID, idsAlreadyAdded);

	idsAlreadyAdded.RemoveAll();
	return status;
}

int AB_ContainerInfo::GetExpandedHeaderForEntry(char ** expandedString, MSG_Pane * srcPane, ABID entryID, AB_ABIDArray idsAlreadyAdded)
{
	// this is a recursive function because if we are a mailing list, we need to expand its header entries...
	int status = AB_FAILURE; // i'm feeling pessimistic...
	AB_AttributeValue * value = NULL;

	// make sure we haven't already added this ABID....
	if (idsAlreadyAdded.FindIndex(0, (void *) entryID) != -1 /* make sure we can't find it */)
		return AB_SUCCESS; // we've already visited this entry 

	// we have 2 cases:
	// (1) the base case is a person entry...format the person, and add them to the expand string
	// (2) the recursive call...for mailing lists...create a mailing list ctr, add each entry in the mlist ctr

	if (entryID != AB_ABIDUNKNOWN)
	{
		GetEntryAttribute(srcPane, entryID, AB_attribEntryType, &value);
		if (value)
		{
			// we have two cases: the base case is a person entry...make sure the person was not added already, 
			// and 
			if (value->u.entryType == AB_Person)
			{
				char * fullAddress = GetFullAddress(srcPane, entryID);
				if (fullAddress)
				{
					char * name = NULL;
					char * address = NULL;
					MSG_ParseRFC822Addresses(fullAddress, &name, &address);
					AB_AddressParser * parser = NULL;
					AB_AddressParser::Create(NULL, &parser);
					if (parser)
					{
						parser->FormatAndAddToAddressString(expandedString, name, address, TRUE /* ensure domain name */);
						parser->Release();
						parser = NULL;
						status = AB_SUCCESS;
					}
					XP_FREEIF(name);
					XP_FREEIF(address);
					XP_FREEIF(fullAddress);
				}
			}
			else	// we are a mailing list ctr
			{
				AB_ContainerInfo * mListCtr = NULL;
				AB_ContainerInfo::Create(m_context, this, entryID, &mListCtr);
				if (mListCtr)
				{
					for (MSG_ViewIndex index = 0; index < mListCtr->GetNumEntries(); index++)
					{
						ABID memberID = mListCtr->GetABIDForIndex(index);
						if (memberID != AB_ABIDUNKNOWN)
							mListCtr->GetExpandedHeaderForEntry(expandedString,srcPane, memberID, idsAlreadyAdded);
					}
					mListCtr->Release();
					status = AB_SUCCESS;
				}
				else
					status = AB_FAILURE;
			}
			AB_FreeEntryAttributeValue(value);
		}
		else
			status = ABError_NullPointer;
	}
	else
		status = ABError_NullPointer;

	return status;
}

char * AB_ContainerInfo::GetFullAddress(MSG_Pane * srcPane, ABID entryID)
{
	AB_AttributeValue * emailAddress = NULL;
	AB_AttributeValue * displayName = NULL;
	char * displayString = NULL;
	char * email = NULL;

	// get necessary attributes
	GetEntryAttribute(srcPane, entryID, AB_attribEmailAddress, &emailAddress);
	GetEntryAttribute(srcPane, entryID, AB_attribDisplayName, &displayName);

	if (displayName)
		displayString = displayName->u.string;
	if (emailAddress)
		email = emailAddress->u.string;

	char * fullAddress = MSG_MakeFullAddress(displayString, email);
	if (!fullAddress) // then try our best...
		if (displayString)
			fullAddress = XP_STRDUP(displayString);
		else
			if (email)
				fullAddress = XP_STRDUP(email);
			

	AB_FreeEntryAttributeValue(emailAddress);
	AB_FreeEntryAttributeValue(displayName);

	return fullAddress;
}

AB_Column_eAttribute AB_ContainerInfo::ConvertAttribToDBase(AB_AttribID attrib)
{
	// this stuff really should be just temporary. Once things settle down, we want the FEs to use the database
	// attribute types. When they do, then we won't need to convert...
	switch(attrib)
	{
		case AB_attribEntryType:
			return AB_Attrib_kIsPerson;
		case AB_attribEntryID:
			return AB_Attrib_kUid;
		case AB_attribFullName:
			return AB_Attrib_kFullName;
		case AB_attribNickName:
			return AB_Attrib_kNickname;
		case AB_attribGivenName:
			return AB_Attrib_kGivenName;
		case AB_attribMiddleName:
			return AB_Attrib_kMiddleName;
		case AB_attribFamilyName:
			return AB_Attrib_kFamilyName;
		case AB_attribCompanyName:
			return AB_Attrib_kCompanyName;
		case AB_attribLocality:
			return AB_Attrib_kLocality;
		case AB_attribRegion:
			return AB_Attrib_kRegion;
		case AB_attribEmailAddress:
			return AB_Attrib_kEmail;
		case AB_attribInfo:
			return AB_Attrib_kInfo;
		case AB_attribHTMLMail:
			return AB_Attrib_kHtmlMail;
		case AB_attribExpandedName:
			return AB_Attrib_kExpandedName;
		case AB_attribTitle:
			return AB_Attrib_kTitle;
		case AB_attribPOAddress:
			return AB_Attrib_kPostalAddress;
		case AB_attribStreetAddress:
			return AB_Attrib_kStreetAddress;
		case AB_attribZipCode:
			return AB_Attrib_kZip;
		case AB_attribCountry:
			return AB_Attrib_kCountry;
		case AB_attribWorkPhone:
			return AB_Attrib_kWorkPhone;
		case AB_attribFaxPhone:
			return AB_Attrib_kFax;
		case AB_attribHomePhone:
			return AB_Attrib_kHomePhone;
		case AB_attribDistName:
			return AB_Attrib_kDistName;
		case AB_attribSecurity:
			return AB_Attrib_kSecurity;
		case AB_attribCoolAddress:
			return AB_Attrib_kCoolAddress;
		case AB_attribUseServer:
			return AB_Attrib_kUseServer;
		case AB_attribDisplayName:
/*			return AB_Attrib_kDisplayName; */
			return AB_Attrib_kFullName;
		case AB_attribWinCSID:
			return AB_Attrib_kCharSet;
			/* WARNING: RIGHT NOW DBASE DOES NOT SUPPORT PAGER AND CELL PHONE YET THE FE CODE IS TRYING TO SET THEM....
			THIS IS CAUSING SOME BAD PROBLEMS. TO FIX THIS, WE ARE GOING TO MAP THESE TO A VALID ATTRIBUTE: 

  */
		case AB_attribPager:
/*			return AB_Attrib_kPager; */
			return AB_Attrib_kDistName;
		case AB_attribCellularPhone:
/*			return AB_Attrib_kCellPhone; */
			return AB_Attrib_kDistName;
			
		default:
			XP_ASSERT(0);
			return AB_Attrib_kNumColumnAttributes;   // we need an invalid attribute enum from the database for here!!
	}
}

// Database related methods (Note: each subclass has its own behavior for dbase interaction...
const char * AB_ContainerInfo::ConvertAttribValueToDBase(AB_AttributeValue * value)
{
	// all database values are strings! So we must map any of our attributeValues that are not strings, to strings...
	// if you add a non-string attribute here, make sure you also modify AB_CopyEntryAttributeValue in abglue.cpp
	if (value)
	{
		switch (value->attrib)
		{
		case AB_attribHTMLMail:
			if (value->u.boolValue)
				return "t";
			else
				return "f";
		case AB_attribEntryType:
			if (value->u.entryType == AB_Person)
				return "t";
			else
				return "f";
		case AB_attribWinCSID:
		case AB_attribSecurity:
		case AB_attribUseServer:
			// convert short value to a string...
			// for now just return NULL
			return NULL;
		case AB_attribDisplayName:
			// HACK ALERT!!! Right now we have a database corruption at 20 characters....until this if fixed we are going
			// to truncate long display names to prevent the corruption....
			if (value->u.string && XP_STRLEN(value->u.string) >= 18)
				value->u.string[17] = '\0'; // insert the null termination early!!!!!
		default: // string attribute
			if (AB_IsStringEntryAttributeValue(value))
				return value->u.string;

		}
	}

	return NULL;  // we received an invalid attribute!
}

int AB_ContainerInfo::ConvertDBaseValueToAttribValue(const char * dbaseValue, AB_AttributeValue * value /* already allocated */, AB_AttribID attrib)
// if you call this function, make sure value is a fresh value because I do not free any string attribute values that might be inside it.
// Why? Because I have no idea if you are passing me an unitialized value or a previously used value so I can't really ask the value
// its type. Caller is responsible for making sure it is a fresh value.
{
	if (dbaseValue)
	{

		value->attrib = attrib;

		switch(attrib)
		{
		case AB_attribHTMLMail:
			if (XP_STRCMP(dbaseValue, "t") == 0)
				value->u.boolValue = TRUE;
			else
				value->u.boolValue = FALSE;
			break;
		case AB_attribEntryType:
			if (XP_STRCMP(dbaseValue, "t") == 0)
				value->u.entryType = AB_Person;
			else
				value->u.entryType = AB_MailingList;
			break;
		case AB_attribWinCSID:
		case AB_attribSecurity:
		case AB_attribUseServer:
			// convert string to integer
			// HACK JUST FOR NOW...
			value->u.shortValue = 0;
			break;
		default:  // as always, default is a string attribute
			if (XP_STRLEN(dbaseValue) > 0)
				value->u.string = XP_STRDUP(dbaseValue);
			else
				value->u.string = XP_STRDUP("");
			break;
		}

		return AB_SUCCESS;

	}
	else
		return AB_FAILURE;  // we didn't have a dbaseValue to convert
}

int AB_ContainerInfo::TypedownSearch(AB_Pane * requester, const char * typeDownValue, MSG_ViewIndex startIndex)
{
	// begin a database search on the type down value. This gives us a table with rows for all of the matches.
	// For now, ignore the start Index (LDAP needs this not us). Get the row_uid for the first row in the 
	// search results table. Turn around and get the index for this row in the big dbaseTable. This is our type down
	// index!!

	int status = AB_SUCCESS;
	MSG_ViewIndex typedownIndex = startIndex;

	AB_Env_ForgetErrors(m_db->db_env);
	if (m_db->db_table)
	{
		ab_column_uid colID = AB_Table_GetSortColumn(m_db->db_table, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env); // come back and pick up errors later

		ab_row_uid rowUID = AB_Table_FindFirstRowWithPrefix(m_db->db_table, m_db->db_env, typeDownValue, colID);
		// convert the row_uid we got back into a row positon
		if (AB_Env_Good(m_db->db_env) && rowUID)
		{
			ab_row_pos rowPos = AB_Table_RowPos(m_db->db_table, m_db->db_env, rowUID);
			if (rowPos)
				typedownIndex = (MSG_ViewIndex) rowPos - 1; // row positions are 1 based. viewIndices are 0 based
		}
	}

	AB_Env_ForgetErrors(m_db->db_env);

	if (requester)
		FE_PaneChanged(requester, TRUE, MSG_PaneNotifyTypeDownCompleted, typedownIndex);

	return status;
}

/* Okay, the policy on database notifications is that the base class will ignore them!!! It is up to the 
   container subclasses to decide what actions they wish to take. */

void AB_ContainerInfo::SeeBeginModelFlux(ab_Env* /* ev */, ab_Model* /* m */)
{
}

void AB_ContainerInfo::SeeEndModelFlux(ab_Env* /* ev */, ab_Model* /* m */)
{
}

void AB_ContainerInfo::SeeChangedModel(ab_Env* /* ev */, ab_Model* /* m */, const ab_Change* /* c */)
{
}
    
void AB_ContainerInfo::SeeClosingModel(ab_Env* /* ev */, ab_Model* /* m */, const ab_Change* /* c */)  // database is going away
{
	if (m_objectState == AB_ObjectOpen)  // if we aren't already closing or closed...
		Close();
}

/*******************************************************************************************

  Asynchronous copy methods -> begin, more, interrupt and finish

*********************************************************************************************/
/* static */ char * AB_ContainerInfo::BuildCopyUrl() // url string for a addbook-copy url
{
	return XP_STRDUP("addbook:copy");
}

/* static */ void AB_ContainerInfo::PostAddressBookCopyUrlExitFunc (URL_Struct *URL_s, int /* status */, MWContext * /* window_id */)
{
	// it is also possible to enter this routine if the url was interrupted!!!

	// address book copy urls always have an AB_AddressBookCopyInfo in the owner_data field...we need to free this.
	 if (URL_s)
	 {
		 AB_AddressBookCopyInfo * copyInfo = (AB_AddressBookCopyInfo *) URL_s->owner_data;
		 AB_FreeAddressBookCopyInfo(copyInfo);
		 URL_s->owner_data = NULL;
	 }

	 // url struct gets freed in netlib....
}

int AB_ContainerInfo::BeginCopyEntry(MWContext * context, AB_AddressBookCopyInfo * abCopyInfo, void ** copyCookie, XP_Bool * copyFinished)
{
	// (1) set status text based on action
	// (2) set copyCookie to 0 because the first id we want to look at
	int status = AB_SUCCESS;
	if (abCopyInfo)
	{
		char * statusText = NULL;
		char * destDescription = NULL;
		if (abCopyInfo->destContainer)
			destDescription = abCopyInfo->destContainer->GetDescription();

		switch (abCopyInfo->state)
		{
			case AB_CopyInfoCopy:
				if (destDescription)
					statusText = PR_smprintf (XP_GetString(MK_ADDR_COPY_ENTRIES), destDescription);
				else
					statusText = PR_smprintf (XP_GetString(MK_ADDR_COPY_ENTRIES), XP_GetString(MK_MSG_ADDRESS_BOOK));
				break;
			case AB_CopyInfoMove:
				if (destDescription)
					statusText = PR_smprintf (XP_GetString(MK_ADDR_MOVE_ENTRIES), destDescription);
				else
					statusText = PR_smprintf (XP_GetString(MK_ADDR_MOVE_ENTRIES), XP_GetString(MK_MSG_ADDRESS_BOOK));
				break;
			case AB_CopyInfoDelete:
				if (m_description)
					statusText = PR_smprintf (XP_GetString(MK_ADDR_DELETE_ENTRIES), m_description);
				else
					statusText = PR_smprintf (XP_GetString(MK_ADDR_DELETE_ENTRIES), XP_GetString(MK_MSG_ADDRESS_BOOK));
				break;
			default:
				XP_ASSERT(FALSE);
				break;
		}

		// now set the status text
		if (statusText)
		{
			FE_Progress (context, statusText);
			XP_FREE(statusText);
		}

		XP_FREEIF(destDescription);
		// now initialize the progress to 0....since we are just starting
		FE_SetProgressBarPercent (context, 0);
		
		// intialize our copyInfo state
		if (copyCookie)
		{
			*copyCookie = (void *) 0;  // we always start from 0;
			MoreCopyEntry(context, abCopyInfo, copyCookie, copyFinished);
		}
		else
			status = ABError_NullPointer;
	}
	else
		status = ABError_NullPointer;

	return status;
}

int AB_ContainerInfo::MoreCopyEntry(MWContext * context, AB_AddressBookCopyInfo * abCopyInfo, void ** copyCookie, XP_Bool * copyFinished)
{
	int32 actionPosition = 0;
	int32 numToCopy = 0;
	int status = AB_SUCCESS;
	XP_Bool finished = FALSE;

	if (copyCookie && abCopyInfo)
	{
		actionPosition = (int32) (*copyCookie); // get the position we are in...
		if ( abCopyInfo->numItems - actionPosition > 0)
		{
			numToCopy = AB_kCopyInfoAmount < (abCopyInfo->numItems - actionPosition) ? AB_kCopyInfoAmount : abCopyInfo->numItems - actionPosition;
			switch (abCopyInfo->state)
			{
				case AB_CopyInfoCopy:
					status = CopyEntriesTo(abCopyInfo->destContainer, &abCopyInfo->idArray[actionPosition], numToCopy, FALSE);
					break;
				case AB_CopyInfoMove:
					status = MoveEntriesTo(abCopyInfo->destContainer, &abCopyInfo->idArray[actionPosition], numToCopy);
					break;
				case AB_CopyInfoDelete:
					status = DeleteEntries(&abCopyInfo->idArray[actionPosition], numToCopy, NULL);
					break;
				default:
					XP_ASSERT(FALSE);
					status = AB_FAILURE;
					break;
			}

			// update progress bar...
			actionPosition += numToCopy;
			int32 percent = (int32) (100 * (((double) (actionPosition+1)) / ((double) abCopyInfo->numItems)));
			FE_SetProgressBarPercent (context, percent);
			// update action position to mark ourselves as done...

			*copyCookie = (void *) actionPosition; // update our position for the next pass...
			if (actionPosition == abCopyInfo->numItems)
				finished = TRUE;
		}
		else
			finished = TRUE;
	}
	else
		status = ABError_NullPointer;

	if (copyFinished)
		*copyFinished = finished;

	return status;
}

int AB_ContainerInfo::FinishCopyEntry(MWContext * /* context */, AB_AddressBookCopyInfo * /* copyInfo */, void ** /* copyCookie */)
{
	return AB_SUCCESS;

}

int AB_ContainerInfo::InterruptCopyEntry(MWContext * context, AB_AddressBookCopyInfo * copyInfo, void ** copyCookie)
{
	return FinishCopyEntry(context, copyInfo, copyCookie);
}

int AB_ContainerInfo::DeleteEntries(ABID * /* ids */, int32 /* numIndices */, AB_ContainerListener * /* instigator*/ )
{
	// what do I know as a basic container about deleting? not much....
	return AB_SUCCESS;
}

int AB_ContainerInfo::CopyEntriesTo(AB_ContainerInfo * destContainer, ABID * idArray, int32 numItems, XP_Bool deleteAfterCopy)
{
	// base class implementation, again assumes the src ctr uses a database...
	// we could rewrite this stuff to not copy rows from dbase tables, instead generating an array of attributes
	// and calling addUser on those attributes....
	if (destContainer && destContainer != this && m_db->db_table && destContainer->GetTable())
	{
		for (int32 i = 0; i < numItems; i++)
		{
			if (idArray[i] != AB_ABIDUNKNOWN)
			{
				AB_Env_ForgetErrors(m_db->db_env);
				ab_row_uid rowUid = AB_Table_CopyRow(destContainer->GetTable(),m_db->db_env,m_db->db_table, (ab_row_uid) idArray[i]);

				// what should we do if an individual copy fails????
				if (deleteAfterCopy && rowUid && AB_Env_Good(m_db->db_env)) // no errors, copy succeeded and we need to delete after copy
					DeleteEntries(&idArray[i], 1, NULL);
			}
		}
	}
	return AB_SUCCESS;
}

int AB_ContainerInfo::MoveEntriesTo(AB_ContainerInfo * destContainer, ABID * idArray, int32 numItems)
{
	return CopyEntriesTo(destContainer, idArray, numItems, TRUE /* remove after copy */);
}


// added to aid debugging with the databae.
char* AB_ContainerInfo::ObjectAsString(ab_Env* ev, char* outXmlBuf) const
{
        AB_USED_PARAMS_1(ev);
		char * description = "";
		if (m_description)
			description = m_description;
#if AB_CONFIG_TRACE_orDEBUG_orPRINT
        sprintf(outXmlBuf,
                "<AB_ContainerInfo me=\"^%lX\" describe=\"%.64s\"/>",
                (long) this,                        // me=\"^%lX\"
                description              // describe=\"%.64s\"
                );
#else
        *outXmlBuf = 0; /* empty string */
#endif /*AB_CONFIG_TRACE_orDEBUG_orPRINT*/
        return outXmlBuf;
}

/* static */ int AB_ContainerInfo::CompareByType(const void * ctr1, const void * ctr2)
{

/* Ranked as: PAB, LDAP, Mailing List, unknown...
	0 means they are equal
	-1 means ctr1 < ctr2
	1 means ctr 1 > ctr 2 */

	int status = 0; // assume equal by default
	AB_ContainerType ctr1Type = AB_UnknownContainer;
	AB_ContainerType ctr2Type = AB_UnknownContainer;

	if (ctr1)
		((AB_ContainerInfo *) ctr1)->GetType();
	
	if (ctr2)
		((AB_ContainerInfo *) ctr2)->GetType();

	if (ctr1Type == ctr2Type) // simplest case
		status = 0;
	else
		switch (ctr1Type)
		{
			case AB_PABContainer:
				status = -1;
				break;
			case AB_LDAPContainer:
				if (ctr2Type == AB_PABContainer) // is it less ? 
					status = 1;
				else
					status = -1;
				break;
			case AB_MListContainer:
				if (ctr2Type == AB_UnknownContainer) 
					status = -1;
				else
					status = 1;
				break;
			case AB_UnknownContainer:
			default:
				status = 1;  // ctr1 has to be greater
				break;
		}

	return status;
}

/*****************************************************************************************************************

  Definitions for AB_DirectoryContainerInfo class. Responsibilities include container cache and maintaining/freeing 
	description character string. 

  ****************************************************************************************************************/

AB_DirectoryContainerInfo::AB_DirectoryContainerInfo(MWContext * context, DIR_Server * server) : AB_ContainerInfo(context)
{
	m_server = server;
	m_description = NULL;
	LoadDescription();
	LoadDepth();
}

AB_DirectoryContainerInfo::~AB_DirectoryContainerInfo()
{
	// make sure we are not in the cache!
	XP_ASSERT(m_description == NULL);
	XP_ASSERT(m_server == NULL);
	if (m_IsCached) // only look in cache if it is a cached object
		XP_ASSERT(CacheLookup(m_server) == NULL);

}

int AB_DirectoryContainerInfo::UpdateDIRServer()
{
	// what attributes do we need to update?? 
	LoadDescription();
	LoadDepth();
	// the description could have changed so notify listeners!
	NotifyContainerAttribChange(this, AB_NotifyPropertyChanged, NULL);
	XP_List * servers = DIR_GetDirServers();
	DIR_SaveServerPreferences(servers);      // make sure the change is committed 
	return AB_SUCCESS;
}

int AB_DirectoryContainerInfo::Close()
{
	CacheRemove();

	if (m_server)
		m_server = NULL;

	if (m_description)
	{
		XP_FREE(m_description);
		m_description = NULL;
	}
	return AB_ContainerInfo::Close();
}
	
/* static */ int AB_DirectoryContainerInfo::Create(MWContext * context, DIR_Server * server, AB_ContainerInfo ** newCtr)
{
	int status = AB_SUCCESS;
	// first look up the dir server in the cache..
	AB_DirectoryContainerInfo * ctr = CacheLookup(server);
	if (ctr) // is it in the cache?
		ctr->Acquire(); 
	else
	{
		// here's where we need to decide what type of container we are now creating...
		// we're going to cheat and look into the DIR_Server for its type.
		if (server)
		{
			AB_Env * env = AB_Env_New();
			env->sEnv_DoTrace = 0;
			if (env)
			{
				ab_Env * ev = ab_Env::AsThis(env);
				if (server->dirType == LDAPDirectory)
					ctr = new (*ev) AB_LDAPContainerInfo(context, server);
				else
					if (server->dirType == PABDirectory)
						ctr = new (*ev) AB_PABContainerInfo(context, server);
				env = ev->AsSelf();
				if (AB_Env_Bad(env) || !ctr)  // error during creation,
				{
					if (ctr)  // object is now useless...
					{
						status = AB_FAILURE;
						ctr->GoAway();
						ctr = NULL;
					}
					else
						status = AB_OUT_OF_MEMORY;
				}
				
				AB_Env_Release(env);
			}		
			
			if (!ctr)
				status =  AB_OUT_OF_MEMORY;
			else
				ctr->CacheAdd(); // add the new container to the cache
		}
	}

	*newCtr = ctr;
	return status;
}


/* Use this call to look up directory containers where you have a DIR_Server */
/* static */ AB_DirectoryContainerInfo * AB_DirectoryContainerInfo::CacheLookup (DIR_Server * server)
{
	if (m_ctrCache)
	{
		XP_List * walkCache = m_ctrCache;
		AB_DirectoryContainerInfo * walkCtr;

		do
		{
			walkCtr = (AB_DirectoryContainerInfo *) XP_ListNextObject(walkCache);
			if (walkCtr && (DIR_AreServersSame(walkCtr->m_server,server)))
				return walkCtr;
		} while (walkCtr);
	}
	return NULL;
}

XP_List * AB_DirectoryContainerInfo::m_ctrCache = NULL;

void AB_DirectoryContainerInfo::CacheAdd()
{
	if (!m_ctrCache)
		m_ctrCache = XP_ListNew();
	XP_ListAddObject(m_ctrCache, this);
}

void AB_DirectoryContainerInfo::CacheRemove ()
{
	if (m_IsCached) // only remove from cache if it is a cached object
	{
		XP_ListRemoveObject (m_ctrCache, this);

		// delete list when empty
		if (XP_ListCount (m_ctrCache) == 0)
		{
			XP_ListDestroy (m_ctrCache);
			m_ctrCache = NULL;
		}
	}
}

void AB_DirectoryContainerInfo::LoadDepth()
{
	m_depth = 0; // all directory containers are at the root level!
}

void AB_DirectoryContainerInfo::LoadDescription()
{
	if (m_description)
		XP_FREE(m_description); 
	if (m_server && m_server->description)
	{
		if (XP_STRLEN(m_server->description) > 0)
			m_description = XP_STRDUP(m_server->description);
	}
	else
		m_description = NULL;
}


int AB_DirectoryContainerInfo::m_offlineCallbackCount = 0;

static int PR_CALLBACK OfflineContainerCallback(const char * /* prefName */, void *)
{
	XP_List * walkCache = AB_DirectoryContainerInfo::GetContainerCacheList();
	if (walkCache)
	{
		AB_DirectoryContainerInfo * walkCtr;

		do
		{
			walkCtr = (AB_DirectoryContainerInfo *) XP_ListNextObject(walkCache);
			if (walkCtr)
				walkCtr->SelectDatabase();
		} while (walkCtr);
	}

	return PREF_NOERROR;
}

/* static */ void AB_DirectoryContainerInfo::RegisterOfflineCallback()
{
	if (m_offlineCallbackCount++ == 0)
		PREF_RegisterCallback("network.online", OfflineContainerCallback, NULL);
}

/* static */ void AB_DirectoryContainerInfo::UnregisterOfflineCallback()
{
	if (--m_offlineCallbackCount == 0)
		PREF_UnregisterCallback("network.online", OfflineContainerCallback, NULL);
}


int AB_DirectoryContainerInfo::NameCompletionSearch(AB_Pane * requester, const char * ncValue)
{
	int status = AB_SUCCESS;
	AB_Env_ForgetErrors(m_db->db_env);
	if (m_db->db_table && ncValue)
	{
		ab_column_uid colID = AB_Table_GetSortColumn(m_db->db_table, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env); // come back and pick up errors later

		ab_column_uid colIDs[2];
		colIDs[0] = colID;
		colIDs[1] = 0;

		AB_Table * results = AB_Table_AcquireSearchTable(m_db->db_table, m_db->db_env, ncValue, colIDs);
		if (results)
		{
			// for each search result match, have the requester process it...
			for (ab_row_pos pos = 1; pos <= AB_Table_CountRows(results, m_db->db_env) && AB_Env_Good(m_db->db_env); pos++)
			{
				ab_row_uid rowUID = AB_Table_GetRowAt(results, m_db->db_env, pos);
				if (rowUID) // notify requester that we found a match
					requester->ProcessSearchResult(this, (ABID) rowUID); // notify requester that we found a match
			}

			AB_Table_Release(results, m_db->db_env);
			results = NULL;
		}
	}
	
	AB_Env_ForgetErrors(m_db->db_env);
	requester->SearchComplete(this);  // our work here is done
	return status;
}

/***********************************************************************************************************************

  Definitions for the Non-Directory Container Info Class. Responsibilities include container caching, description string 
	and maintaining parent container / entry ID pair. 

 ***********************************************************************************************************************/

AB_NonDirectoryContainerInfo::AB_NonDirectoryContainerInfo(MWContext * context, AB_ContainerInfo * parentCtr, ABID entryID) : AB_ContainerInfo(context)
{
	m_description = NULL;
	m_parentCtr = parentCtr;
	m_entryID = entryID;
	
	m_description = NULL;
	LoadDescription();
	m_depth = 0;
	LoadDepth();
}

AB_NonDirectoryContainerInfo::~AB_NonDirectoryContainerInfo()
{
	XP_ASSERT(m_description == NULL);
	XP_ASSERT(CacheLookup(m_parentCtr, m_entryID) == NULL);
}

int AB_NonDirectoryContainerInfo::Close()
{
	if (IsOpen())
	{
		CacheRemove();
		if (m_description)
		{
			XP_FREE(m_description);
			m_description = NULL;
		}

		AB_ContainerInfo::Close();

		if (m_parentCtr)
		{
			m_parentCtr->Release();
			m_parentCtr = NULL;
		}
	}
	return AB_SUCCESS;
}

/* static */ int AB_NonDirectoryContainerInfo::Create(MWContext * context, AB_ContainerInfo * parentCtr, ABID entryID, AB_ContainerInfo ** newCtr)
{
	int status = AB_SUCCESS;

	// first look to see if we have already created this container...
	AB_NonDirectoryContainerInfo * ctr = AB_NonDirectoryContainerInfo::CacheLookup(parentCtr, entryID);
	if (ctr) // is it in the cache?
		ctr->Acquire();
	else
	{
		// what type of container are we creating? Right now, the only non-directory container we have is a mailing list
		// container...we might have more types later on...insert them here
		if (parentCtr)
		{
			AB_Env * env = AB_Env_New();
			env->sEnv_DoTrace = 0;
			if (env)
			{
				ab_Env * ev = ab_Env::AsThis(env);
				ctr = new (* ev) AB_MListContainerInfo (context, parentCtr, entryID);

				env = ev->AsSelf();
				if (AB_Env_Bad(env) || !ctr)  // error during creation,
				{
					if (ctr)  // object is now useless...
					{
						status = AB_FAILURE;
						ctr->GoAway();
					}
					else
						status = AB_OUT_OF_MEMORY;
				}
				AB_Env_Release(env);
			}
			if (!ctr)
			{
				*newCtr = NULL;
				return AB_OUT_OF_MEMORY;
			}
			ctr->CacheAdd();
		}
	}

	*newCtr = ctr;
	return status;
}

/* static */ AB_NonDirectoryContainerInfo * AB_NonDirectoryContainerInfo::CacheLookup(AB_ContainerInfo * parentCtr, ABID entryID /* ABID in parent */)
{
	if (m_ctrCache)
	{
		XP_List * walkCache = m_ctrCache;
		AB_NonDirectoryContainerInfo * walkCtr;
		do
		{
			walkCtr = (AB_NonDirectoryContainerInfo *) XP_ListNextObject(walkCache);
			if (walkCtr && (walkCtr->m_parentCtr == parentCtr) && walkCtr->m_entryID == entryID)
				return walkCtr;
		} while (walkCtr);
	}

	return NULL;
}

XP_List * AB_NonDirectoryContainerInfo::m_ctrCache = NULL;

void AB_NonDirectoryContainerInfo::CacheAdd()
{
	if (!m_ctrCache)
		m_ctrCache = XP_ListNew();
	XP_ListAddObject(m_ctrCache, this);
}

void AB_NonDirectoryContainerInfo::CacheRemove ()
{
	XP_ListRemoveObject (m_ctrCache, this);

	// delete list when empty
	if (XP_ListCount (m_ctrCache) == 0)
	{
		XP_ListDestroy (m_ctrCache);
		m_ctrCache = NULL;
	}
}

void AB_NonDirectoryContainerInfo::LoadDepth()
{
	// our depth is one greater than our parents depth!
	m_depth = 0;
	AB_ContainerAttribValue * value = NULL;
	if (m_parentCtr)
	{
		m_parentCtr->GetAttribute(attribDepth, &value);
		if (value)
		{
			m_depth = value->u.number + 1;
			AB_FreeContainerAttribValue(value);
		}
	}
}

void AB_NonDirectoryContainerInfo::LoadDescription()
{
	// our description is actually obtained from a field in the parent container entryID
	if (IsOpen())
	{
		AB_AttributeValue * value;
		if (m_parentCtr)
			if (m_parentCtr->GetEntryAttribute(NULL, m_entryID, /* AB_attribFullName */ AB_attribDisplayName, &value) == AB_SUCCESS)
			{
				if (value->u.string)
					m_description = XP_STRDUP(value->u.string);
				else
					m_description = NULL;
				AB_FreeEntryAttributeValue(value);
			}
	}
}

DIR_Server * AB_NonDirectoryContainerInfo::GetDIRServer()
{
	if (m_parentCtr)
		return m_parentCtr->GetDIRServer();
	else
		return NULL;
}

// we want to remove the ctr from its parent...
int AB_NonDirectoryContainerInfo::DeleteSelfFromParent(AB_ContainerListener * instigator)
{
	// this causes an ugly FE crash...Not a release for the brave stopper so I'm going to
	// leave it out for now....

	if (m_parentCtr)
		return m_parentCtr->DeleteEntries(&m_entryID,1, instigator); 
	else
		return AB_FAILURE;  // not implemented yet...
}


/*****************************************************************************

  Definitions for the LDAP Container Info Class.

 *****************************************************************************/
uint32 AB_LDAPEntry::m_sortAttributes = 0;
AB_AttribID AB_LDAPEntry::m_sortOrder[3];

/* AB_LDAPEntry Constructor
 */
AB_LDAPEntry::AB_LDAPEntry()
{
	SetSortOrder();
}

/* GetPrimarySortAttribute
 */
AB_AttribID AB_LDAPEntry::GetPrimarySortAttribute()
{
	return m_sortOrder[0];
}

/* SetSortOrder
 */
void AB_LDAPEntry::SetSortOrder(AB_AttribID *list, uint32 num)
{
	if (list == NULL)
	{
		m_sortOrder[0] = AB_attribDisplayName;
		m_sortAttributes = 1;
	}
	else
	{
		if (num > 3)
			num = 3;

		m_sortAttributes = num;
		for (uint32 i = 0; i < num; i++)
			m_sortOrder[i] = list[i];
	}
}

/* Compare
 *
 * AB_LDAPEntry overloaded operator helper
 *
 * NOTE: this function assumes that the attributes for both entries will
 *       be in the same order and positions within the values array.  This
 *       a safe assumption given the current implementation of
 *       ConvertResultElement.
 */
int AB_LDAPEntry::Compare(AB_LDAPEntry &entry)
{
	int rc = 0;
	uint32 i, j;

	/* First, compare the sort attributes.  The attribute at index 0
	 * is always AB_attribEntryType == AB_Person, we can skip it.
	 */
	for (i = 0; i < m_sortAttributes; i++)
	{
		for (j = 1; j < m_numAttributes; j++)
			if (m_values[j].attrib == m_sortOrder[i])
				break;

		if (j < m_numAttributes)
		{
			if (m_values[j].u.string && entry.m_values[j].u.string)
				rc = XP_STRCASECMP(m_values[j].u.string, entry.m_values[j].u.string);
			else if (m_values[j].u.string)
				rc = 1;
			else if (entry.m_values[j].u.string)
				rc = -1;

			if (rc != 0)
				return rc;
		}
	}

	/* Second, if we're doing an extended compare (necessary for equality
	 * comparisons), compare the remaining attributes.  This is a bit redundant
	 * but it is perfectly valid since we know the values in the sort
	 * attributes are identical.
	 */
	for (i = 1; i < m_numAttributes; i++)
	{
		if (m_values[i].u.string && entry.m_values[i].u.string)
			rc = XP_STRCASECMP(m_values[i].u.string, entry.m_values[i].u.string);
		else if (m_values[i].u.string)
			rc = 1;
		else if (entry.m_values[i].u.string)
			rc = -1;

		if (rc != 0)
			return rc;
	}

	return 0;
}


/* Constructor - AB_LDAPContainerInfo
 *
 * This is a protected constructor, it should only be called by
 * AB_ContainerInfo::Create which calls it if the container does not already
 * exist.
 */

AB_LDAPContainerInfo::AB_LDAPContainerInfo(MWContext *context,
                                           DIR_Server *server, XP_Bool useDB)
                     :AB_DirectoryContainerInfo(context, server)
{
	m_interruptingSearch = FALSE;
	m_performingVLV = FALSE;
	m_replicating = FALSE;

	m_updateTotalEntries = TRUE;
	m_totalEntries = 0;
	m_firstIndex = AB_LDAP_UNINITIALIZED_INDEX;
	m_lastIndex = AB_LDAP_UNINITIALIZED_INDEX;
	m_searchType = ABLST_None;

	m_entryMatch.m_isValid = FALSE;

	m_dbNormal = m_db;
	if (server->replInfo)
	{
		m_dbReplica = (AB_Database *)XP_CALLOC(1, sizeof(AB_Database));

		/* If we are currently offline, switch to the replica database.
		 */
		if (NET_IsOffline())
			m_db = m_dbReplica;
	}
	if (useDB)
		InitializeDatabase(m_db);
}

/* Destructor - AB_LDAPContainerInfo
 */
AB_LDAPContainerInfo::~AB_LDAPContainerInfo()
{
	// assert on any object that should have been closed or deleted
	// by now. (i.e. make sure close was called b4 you get here)
}

int AB_LDAPContainerInfo::Close()
{
	uint32 i;

	/* Free the VLV match entry.
	 */
	if (m_entryMatch.m_isValid)
	{
		m_entryMatch.m_isValid = FALSE;
		AB_FreeEntryAttributeValues(m_entryMatch.m_values,
		                            m_entryMatch.m_numAttributes);
	}

	/* Free the VLV cache.
	 */
	for (i = m_entryList.GetSize(); i > 0; i--)
	{
		AB_LDAPEntry *entry = (AB_LDAPEntry *)m_entryList[i-1];
		if (entry->m_isValid)
		{
			entry->m_isValid = FALSE;
			AB_FreeEntryAttributeValues(entry->m_values,
		   	                            entry->m_numAttributes);
		}
		XP_FREE(entry);
	}
	m_entryList.RemoveAll();

	/* Free the name completion cache.
	 */
	for (i = m_ncEntryList.GetSize(); i > 0; i--)
	{
		AB_LDAPEntry *entry = (AB_LDAPEntry *)m_ncEntryList[i-1];
		AB_FreeEntryAttributeValues(entry->m_values, entry->m_numAttributes);
		XP_FREE(entry);
	}
	m_ncEntryList.RemoveAll();

	/* Close the entry databases.
	 */
	m_db = NULL;
	if (m_dbNormal)
	{
		if (m_dbNormal->db_env)
			CloseDatabase(m_dbNormal);
		XP_FREE(m_dbNormal);
		m_dbNormal = NULL;
	}
	if (m_dbReplica)
	{
		if (m_dbReplica->db_env)
			CloseDatabase(m_dbReplica);
		XP_FREE(m_dbReplica);
		m_dbReplica = NULL;
	}

	return AB_DirectoryContainerInfo::Close();
}

void AB_LDAPContainerInfo::InitializeDatabase(AB_Database *db)
{
	// Select the appropriate file name for the database we want to open.
	char *fileName, *fullFileName;
	if (db == m_dbNormal)
		fileName = m_server->fileName;
	else
		fileName = m_server->replInfo->fileName;
	DIR_GetServerFileName(&fullFileName, fileName);

	// okay, we need to get the environment, open a store based on the filename, and then open a table for the address book
	db->db_fluxStack = 0;
	db->db_listTable = NULL; // LDAP directories can't have lists....

	// (1) get the environment
	db->db_env = AB_Env_New();
	db->db_env->sEnv_DoTrace = 0;

	// (2) open a store given the filename in the DIR_Server. What if we don't have a filename? 
	db->db_store = AB_Env_NewStore(db->db_env, fullFileName, AB_Store_kGoodFootprintSpace);
	if (db->db_store)
	{
		AB_Store_OpenStoreContent(db->db_store, db->db_env);
		// (3) get an ab_table for the store
		db->db_table = AB_Store_GetTopStoreTable(db->db_store, db->db_env);

		if (AB_Env_Good(db->db_env) && db->db_table)
		{
			((ab_Table *) db->db_table)->AddView(ab_Env::AsThis(db->db_env), this);

			// (4) open a row which has all the columns in the address book
			db->db_row = AB_Table_MakeDefaultRow(db->db_table, db->db_env);	
		}
	}

	if (fullFileName)
		XP_FREE(fullFileName);
}

/* SelectDatabase
 *
 * Returns TRUE if the replica has been selected.
 */
XP_Bool AB_LDAPContainerInfo::SelectDatabase()
{
	XP_Bool bSwitching = FALSE;

	/* Select the database that is appropriate for the current network state.
	 */
	if (NET_IsOffline() && m_dbReplica != NULL && !m_replicating)
	{
		if (m_db != m_dbReplica)
		{
			bSwitching = TRUE;
			m_db = m_dbReplica;
			m_performingVLV = FALSE;
		}
	}
	else
	{
		if (m_db != m_dbNormal)
		{
			bSwitching = TRUE;
			m_db = m_dbNormal;
		}
	}

	/* If we have to switch databases:
	 *   - make sure any ongoing search is cancelled.
	 *   - notify all the panes that their views are obsolete.
	 */
	if (bSwitching)
	{
		AbortSearch();
		NotifyContainerAttribChange(this, AB_NotifyAll, NULL);

		/* If the selected database is not open, open it.
		 */
		if (m_db->db_env == NULL)
			InitializeDatabase(m_db);
	}

	return m_db == m_dbReplica;
}


ABID AB_LDAPContainerInfo::GetABIDForIndex(const MSG_ViewIndex index)
// what is going on here...well we are assigning our own ABIDs for this ctr because we are not storing any entries in the database.
// Best case scenarion, we would just map view indices directly to ABIDs (i.e. they would be the same). But view indices are zero based
// and 0 is a reserved ABID value for AB_ABIDUNKNOWN. So our ABID designation for an entry will always be 1 + the view index for the entry.
{
	if (m_performingVLV)
	{
		if (index != MSG_VIEWINDEXNONE)
			return (ABID) (index + 1);
		else
			return AB_ABIDUNKNOWN;
	}
	else  // otherwise it is in the database so you its implementation
		return AB_ContainerInfo::GetABIDForIndex(index);
}
 
MSG_ViewIndex AB_LDAPContainerInfo::GetIndexForABID(ABID entryID)
{
	if (m_performingVLV)
	{
		MSG_ViewIndex index = MSG_VIEWINDEXNONE;
		if (entryID != AB_ABIDUNKNOWN)
			index = (MSG_ViewIndex) (entryID - 1);
		return index;
	}
	else
		return AB_ContainerInfo::GetIndexForABID(entryID);
}

char * AB_LDAPContainerInfo::GetVCard(MSG_Pane * srcPane, ABID entryID)
{
	if (m_performingVLV)
	{
		return NULL; // we haven't written our own VCard code yet.....
	}
	else // it is in the database...the database knows how to convert the entry to a vcard....
		return AB_ContainerInfo::GetVCard(srcPane, entryID);
}


/* PreloadSearch
 *
 * This function serves two purposes, it:
 *  1) queries the server's LDAP capabilities (only the first time it is
 *     called, per container info);
 *  2) performs a search to retrieve the first block of VLV entries from the
 *     server (iff the server supports VLV)
 */
int AB_LDAPContainerInfo::PreloadSearch(MSG_Pane *pane)
{
	/* Select the database that is appropriate for the current network state.
	 * If the replica is selected, don't do anything.
	 */
	if (!SelectDatabase() && ServerSupportsVLV())
		return PerformSearch(pane, ABLST_Typedown, NULL, 0, FALSE);

	return AB_SUCCESS;
}

/* SearchDirectory
 *
 * Perform a basic or extended search.  This function performs the following
 * different types of searches:
 *  1) v2 Basic - A basic search on an LDAP server that does not support VLV.
 *                Returns a subset of entries that match a single search
 *                attribute.  May return all the matches if the set is small.
 *  2) v3 Basic - A basic search on an LDAP server that does support VLV.
 *                Initiates management a VLV for the all of the entries that
 *                match a single search attribute.
 *  3) Extended - Returns a subset of entries that match one or more pre-
 *                specified search attributes.  Never initiates management of
 *                a VLV.
 */
int AB_LDAPContainerInfo::SearchDirectory(AB_Pane *pane, char *searchString)
{
	/* Select the database that is appropriate for the current network state.
	 * If the replica is selected, use it to do the search.
	 */
	if (SelectDatabase())
		return ParentClass::TypedownSearch(pane, searchString, 0);

	/* The search string is a NULL pointer or nul string if we are to do an
	 * extended search.
	 */
	AB_LDAPSearchType searchType;
	if (searchString && (XP_STRLEN(searchString) > 0))
		searchType = (ServerSupportsVLV()) ? ABLST_Typedown : ABLST_Basic;
	else
		searchType = ABLST_Extended;

	return PerformSearch(pane, searchType, searchString, 0, TRUE);
}

/* TypedownSearch
 */
int AB_LDAPContainerInfo::TypedownSearch(AB_Pane *pane,
                                         const char *typeDownValue,
                                         MSG_ViewIndex startIndex)
{
	/* Select the database that is appropriate for the current network state.
	 * If the replica is selected, use it to do the search.
	 */
	if (SelectDatabase())
		return ParentClass::TypedownSearch(pane, typeDownValue, startIndex);

	AB_LDAPSearchType searchType;
	searchType = (ServerSupportsVLV()) ? ABLST_Typedown : ABLST_Basic;

	/* Put ourselves in batch mode!!
	 */
	if (m_db->db_store && m_db->db_env)
	{
		AB_Store_StartBatchMode(m_db->db_store, m_db->db_env, 150 /* JUST A TEMPORARY MADE UP VALUE!!! */);
		AB_Env_ForgetErrors(m_db->db_env); // if we failed we have no errors to really report....
	}

	return PerformSearch(pane, searchType, typeDownValue, startIndex, TRUE);
}

/* NameCompletionSearch
 */
int AB_LDAPContainerInfo::NameCompletionSearch(AB_Pane *pane, const char *ncValue)
{
	/* Select the database that is appropriate for the current network state.
	 * If the replica is selected, use it to do the search.
	 */
	if (SelectDatabase())
		return ParentClass::NameCompletionSearch(pane, ncValue);

	/* A NULL search string indicates that we should abort the current name
	 * completion search.
	 */
	if (ncValue == NULL && m_searchPane == pane)
		return AbortSearch();

	/* Otherwise, start a new name completion search.
	 */
	else
		return PerformSearch(pane, ABLST_NameCompletion, ncValue, 0, TRUE);
}


/* AbortSearch
 */
int AB_LDAPContainerInfo::AbortSearch()
{
	if (!m_searchPane)
		return AB_SUCCESS;

	if (m_entryMatch.m_isValid)
	{
		m_entryMatch.m_isValid = FALSE;
		AB_FreeEntryAttributeValues(m_entryMatch.m_values, m_entryMatch.m_numAttributes);
	}

	m_interruptingSearch = TRUE;
	MSG_SearchError err = MSG_InterruptSearchViaPane(m_searchPane);
	m_interruptingSearch = FALSE;

	if (err != SearchError_Busy)
	{
		if (m_db && m_db->db_store && m_db->db_env) // if we were in batch mode and we terminated a search, terminate the batch mode..
		{
			AB_Store_EndBatchMode(m_db->db_store, m_db->db_env);
			AB_Env_ForgetErrors(m_db->db_env);
		}

		/* Tell the old pane that it's not searching anymore.
		 */
		NotifyContainerAttribChange(this, AB_NotifyStopSearching, m_searchPane);
		m_searchPane = NULL;
		return AB_SUCCESS;
	}
	else
		return AB_FAILURE;
}

/* GetSearchAttributeFromEntry
 */
char *AB_LDAPContainerInfo::GetSearchAttributeFromEntry(AB_LDAPEntry *entry)
{
	AB_AttribID attrib = AB_LDAPEntry::GetPrimarySortAttribute();

	for (uint16 i = 0; i < entry->m_numAttributes; i++)
	{
		if (entry->m_values[i].attrib == attrib)
		{
			char *value = entry->m_values[i].u.string;

			if (value && XP_STRLEN(value) > 0)
				return XP_STRDUP(value);
			else
				return NULL;
		}
	}

	return NULL;
}

/* PerformSearch
 *
 * General purpose search function.
 */
int AB_LDAPContainerInfo::PerformSearch(MSG_Pane *pane,
                                        AB_LDAPSearchType type,
                                        const char *attrib,
                                        MSG_ViewIndex index,
                                        XP_Bool removeEntries)
{
	/* The target index is ignored for "new" searches.
	 */
	m_targetIndex = AB_LDAP_UNINITIALIZED_INDEX;

	/* First thing we do is see if this is one of the following "new" searches:
	 *  - A basic or extended search
	 *  - A different type of search than the previous search
	 *  - Any Attribute search
	 *  - An index search that is a page size or more away from the cache
	 *
	 * A "new" search is one that is discontiguous from the previous search,
	 * either by nature or by location.
	 */
	XP_Bool bNew = FALSE;
	if (   type == ABLST_RootDSE || type != m_searchType || attrib
	    || type == ABLST_Basic || type == ABLST_Extended || type == ABLST_NameCompletion)
	{
		bNew = TRUE;
	}
	else
	{
		AB_LDAPEntry *entry;


		/* Check for an index search one page size or more outside of cache
		 */
		if (index + m_pageSize - 1 < m_firstIndex || m_lastIndex + m_pageSize - 1 < index)
		{
			bNew = TRUE;
			m_targetIndex = index;
		}

		/* Check for an index near but before the beginning of the cache.
		 */
		else if (index < m_firstIndex)
		{
			entry = (AB_LDAPEntry *)m_entryList[0];
			if (entry->m_isValid)
			{
				if (AbortSearch() == AB_FAILURE) return AB_FAILURE;
				attrib = GetSearchAttributeFromEntry(entry);
				m_entryMatch.m_isValid = TRUE;
				m_entryMatch.m_values = entry->m_values;
				m_entryMatch.m_numAttributes = entry->m_numAttributes;
				entry->m_isValid = FALSE;
				m_targetIndex = m_firstIndex;
			}
			else
			{
				bNew = TRUE;
				m_targetIndex = index;
			}
		}

		/* Check for an index near but after the end of the cache.
		 */
		else if (index > m_lastIndex)
		{
			entry = (AB_LDAPEntry *)m_entryList[m_lastIndex - m_firstIndex];
			if (entry->m_isValid)
			{
				if (AbortSearch() == AB_FAILURE) return AB_FAILURE;
				attrib = GetSearchAttributeFromEntry(entry);
				m_entryMatch.m_isValid = TRUE;
				m_entryMatch.m_values = entry->m_values;
				m_entryMatch.m_numAttributes = entry->m_numAttributes;
				entry->m_isValid = FALSE;
				m_targetIndex = m_lastIndex;
			}
			else
			{
				bNew = TRUE;
				m_targetIndex = index;
			}
		}

		/* The request is for an index we already have; this should never
		 * happen, but if it does we just return.
		 */
		else
			return AB_SUCCESS;
	}

	/* Abort any search that may already be going on.  We don't do the
	 * abort here for non-"new" searches because we must do it above.
	 */
	if (bNew)
	{
		if (AbortSearch() == AB_FAILURE)
			return AB_FAILURE;
	}

	m_searchType = type;
	m_searchPane = (AB_Pane *)pane;

	/* Clear out the cache/DB
	 */
	if (removeEntries)
		RemoveAllEntries();
	
	if (type != ABLST_Extended)
	{
		MSG_SearchFree(pane);
		MSG_SearchAlloc(pane);
		MSG_AddLdapScope(pane, m_server);
	}

	MSG_SearchValue value;
	if (type == ABLST_Typedown)
	{
		m_performingVLV = TRUE;
		if (bNew)
			m_updateTotalEntries = TRUE;
	
		LDAPVirtualList *pLdapVLV = (LDAPVirtualList *)XP_ALLOC(sizeof(LDAPVirtualList));
		if (pLdapVLV)
		{
			/* Cap the search page size at 33. Most servers will return no
			 * more than 100 entries per search.
			 */
			m_searchPageSize = MIN(m_entryList.GetSize() / 3, 33);

			pLdapVLV->ldvlist_before_count = m_searchPageSize;
			pLdapVLV->ldvlist_after_count = m_searchPageSize * 2 - 1;
			pLdapVLV->ldvlist_size = (m_totalEntries ? m_totalEntries : 2);
			pLdapVLV->ldvlist_extradata = (void *)GetPairMatchingSortAttribute();

			if (attrib)
			{
				pLdapVLV->ldvlist_attrvalue = (char *)(bNew ? XP_STRDUP(attrib) : attrib);
			}
			else
			{
				/* Must add one to the target index because LDAPVirtualList
				 * indices are 1-based and search basis index is 0-based.
				 */
				pLdapVLV->ldvlist_index = m_targetIndex + 1;
				pLdapVLV->ldvlist_attrvalue = NULL;
			}

			/* If the we won't be able to retreive a page size worth of entries
			 * before the target entry, adjust the before and after cound values
			 * to maximize the hits.
			 */
			if (m_targetIndex < m_searchPageSize)
			{
				pLdapVLV->ldvlist_before_count = m_targetIndex;
				pLdapVLV->ldvlist_after_count = m_searchPageSize * 3 - m_targetIndex - 1;
			}

			/* TBD: For some reason this optimization really messes things up.
			 * TBD: I think it may be due to a server bug.
			 *
			 * else if (   m_totalEntries > 0
			 *          && m_targetIndex + pLdapVLV->ldvlist_after_count > m_totalEntries)
			 * {
			 *     pLdapVLV->ldvlist_after_count = m_totalEntries - m_targetIndex;
			 *     pLdapVLV->ldvlist_before_count = m_searchPageSize * 3 - pLdapVLV->ldvlist_after_count - 1;
			 * }
			 */

			/* Estimate the first and last index; the actual indices may
			 * differ depending upon how many entries the server returns.
			 */
			if (m_targetIndex == AB_LDAP_UNINITIALIZED_INDEX)
			{
				m_firstIndex = AB_LDAP_UNINITIALIZED_INDEX;
			}
			else
			{
				m_firstIndex = m_targetIndex - pLdapVLV->ldvlist_before_count;
				m_lastIndex = m_firstIndex + m_searchPageSize * 3 - 1;
			}
		}
		value.u.string = "";
		MSG_SetSearchParam(pane, searchLdapVLV, pLdapVLV);
	}
	else
	{
		/* Any search that get here is a non-VLV search and should set
		 * m_performSearch to FALSE, except name completion searches which
		 * should just leave the flag alone (since they can co-exist with
		 * a VLV search).
		 */
		if (type != ABLST_NameCompletion)
			m_performingVLV = FALSE;

		if (type != ABLST_Extended)
		{
			XP_ASSERT(attrib);
			value.u.string = (char *)(bNew ? XP_STRDUP(attrib) : attrib);
		}

		MSG_SetSearchParam(pane, type == ABLST_RootDSE ? searchRootDSE : searchNormal, NULL);
	}

	if (type != ABLST_Extended)
	{
		value.attribute = attribCommonName;
		MSG_AddSearchTerm(pane, attribCommonName, opLdapDwim, &value, TRUE, NULL);
	}

	if (MSG_Search(pane) == SearchError_Success)
	{
	    NotifyContainerAttribChange(this, AB_NotifyStartSearching, (AB_Pane *)pane); 
		return AB_SUCCESS;
	}
	else
		return AB_FAILURE;
}


/* RemoveAllEntries
 *
 * Removes all entries from the cache or the database depending upon what
 * type of search we are going to perform.
 */
void AB_LDAPContainerInfo::RemoveAllEntries()
{
	if (m_searchType == ABLST_NameCompletion)
	{
		for (uint32 i = m_ncEntryList.GetSize(); i > 0; i--)
		{
			AB_LDAPEntry *entry = (AB_LDAPEntry *)m_ncEntryList[i-1];
			AB_FreeEntryAttributeValues(entry->m_values, entry->m_numAttributes);
			XP_FREE(entry);
		}
		m_ncEntryList.RemoveAll();
	}
	else if (m_searchType == ABLST_Typedown)
	{
		for (uint32 i = m_entryList.GetSize(); i > 0; i--)
		{
			AB_LDAPEntry *entry = (AB_LDAPEntry *)m_entryList[i-1];
			if (entry->m_isValid)
				AB_FreeEntryAttributeValues(entry->m_values,
			   	                            entry->m_numAttributes);

			entry->m_isValid = FALSE;
		}
	}

	/* The case for removing entries from the DB is rather convoluted.  In
	 * general we want to free them if we are NOT doing a typedown or name
	 * completion search.  But, we also need to free them if we are doing
	 * a typedown and we don't know if the server supports VLV, since that
	 * will actually result in a basic search if it doesn't support VLV.
	 */
	if (   m_db == m_dbNormal
	    && (   (m_searchType != ABLST_Typedown && m_searchType != ABLST_NameCompletion)
	        || (m_searchType == ABLST_Typedown && !DIR_TestFlag(m_server, DIR_LDAP_ROOTDSE_PARSED))))
	{
		// if we are performing a new search and there are things left in the 
		if (GetNumEntries())  // database is not empty?	
		{
			CloseDatabase(m_dbNormal);
			AB_Env * env = AB_Env_New();
			env->sEnv_DoTrace = 0;
			// to be safe, we should kill the file...
			char * fileName = NULL;
			DIR_GetServerFileName(&fileName, m_server->fileName);
			AB_Env_DestroyStoreWithFileName(env, fileName);
			if (fileName)
				XP_FREE(fileName);
			// error detection??
			InitializeDatabase(m_dbNormal);  // re-opens the database, table, row, store, etc..
		}

		NotifyContainerEntryChange(this, AB_NotifyAll, 0, 0, NULL);  // notify all listeners that we have changed! 
	}
}

int AB_LDAPContainerInfo::GetEntryAttributes(MSG_Pane * srcPane, ABID entryID, AB_AttribID * attrib, AB_AttributeValue ** values, uint16 * numItems)
{
	MSG_ViewIndex index = GetIndexForABID(entryID);
	return GetEntryAttributesForIndex(srcPane, index, attrib, values, numItems);
}


int AB_LDAPContainerInfo::GetEntryAttributesForIndex(MSG_Pane *pane,
                                                     MSG_ViewIndex index,
                                                     AB_AttribID *attribs,
                                                     AB_AttributeValue **values,
                                                     uint16 *numItems)
{
	/* Select the database that is appropriate for the current network state.
	 * If the replica is selected, retreive the result entry from it.
 	 */
	if (SelectDatabase())
		return ParentClass::GetEntryAttributesForIndex(pane, index, attribs, values, numItems);

	/* Look in the name completion cache if this is a picker pane.
	 * Special case for offline:  if a name completion search was performed
 	 * before we went offline, we honor those results.  But, if we perform a
 	 * name completion search after we go off line, we use the replica.
  	 */
 	if (pane->GetPaneType() == AB_PICKERPANE && m_ncEntryList.GetSize() > 0)
	{
		if (index >= (MSG_ViewIndex)(m_ncEntryList.GetSize()))
			return AB_FAILURE;

		return CopyEntryValues(pane, index, (AB_LDAPEntry *)m_ncEntryList[index], attribs, values, numItems);
	}

	/* Look in the cache if we are doing a VLV search and the current pane is
	 * not a picker (i.e. name completion) pane.
	 */
	else if (m_performingVLV)
	{
		/* Check for the case where we have started a search but we don't
		 * know which indices will be returned.
		 */
		if (m_firstIndex == AB_LDAP_UNINITIALIZED_INDEX)
			return AB_ENTRY_BEING_FETCHED;
 
		/* Check for an invalid index.
		 */
		else if (index == MSG_VIEWINDEXNONE || index >= m_totalEntries)
			return AB_FAILURE;

		AB_LDAPEntry *entry = NULL;
		if (m_firstIndex <= index && index <= m_lastIndex)
			entry = (AB_LDAPEntry *)m_entryList[index - m_firstIndex];

		/* Return the entry if we already have it.
	 	 */
		if (entry && entry->m_isValid)
			return CopyEntryValues(pane, index, entry, attribs, values, numItems);

		/* We don't have the entry yet, but we have already started a search
		 * for it.
		 */
		else if (entry)
			return AB_ENTRY_BEING_FETCHED;

		/* We don't have the entry and we aren't in the process of retreiving it
		 * yet; start a search to get it.
		 */
		else if (PerformSearch(pane, ABLST_Typedown, NULL, index) == AB_SUCCESS)
			return AB_ENTRY_BEING_FETCHED;

		/* We don't have the entry and the search we tried to start failed.
		 */
		else
			return AB_FAILURE;
	}

	/* A normal search result request, look in the database.
	 */
	else
	{
		return ParentClass::GetEntryAttributesForIndex(pane, index, attribs, values, numItems);
	}
}

/* GetNumEntries
 */
uint32 AB_LDAPContainerInfo::GetNumEntries()
{
	/* Select the database that is appropriate for the current network state.
	 */
	SelectDatabase();

	if (m_performingVLV)
		return m_totalEntries;
	else
		return AB_DirectoryContainerInfo::GetNumEntries();
}


/* GetPairMatchingSortAttribute
 */
const char *AB_LDAPContainerInfo::GetPairMatchingSortAttribute()
{
	/* Use the standard filter if there is no VLV search pair list for the server.
	 */
	if (!m_server->searchPairList || !m_server->searchPairList[0])
		return NULL;

	/* Use the standard filter if the sort attribute is an unsupported sort
	 * attribute for LDAP; this should probably never happen.
	 */
	const char *attribName = GetSortAttributeNameFromID(m_sortedBy);
	if (!attribName)
		return NULL;

	/* Try to find a search pair that matches the sort attribute.
	 */
	char *pair = m_server->searchPairList;
	char *end = XP_STRCHR(pair, ';');
	while (pair && *pair)
	{
		if (PairMatchesAttribute(pair, end, attribName, m_sortAscending))
		{
			int len = (end ? end - pair : XP_STRLEN(pair));
			char *match = (char *)XP_ALLOC(len + 1);
			if (match)
				return XP_STRNCPY_SAFE(match, pair, len + 1);
		}

		if (end)
			pair = end+1;
		else
			pair = NULL;

		end = XP_STRCHR(pair, ';');
	}

	/* As a special case, we always allow sorting on the full name or the
	 * surname even if there was no matching search pair.
	 */
	if (m_sortedBy == AB_attribDisplayName)
	{
		/* TBD: add cases for lastname, firstname display/sorting.
		 */

		return PR_smprintf("(%s=*):%s%s", attribName, m_sortAscending ? "" : "-", attribName);
	}

	return NULL;
}

/* GetSortAttributeNameFromID
 *
 * Returns a string with the LDAP attribute name corresponding to the
 * address book ID passed in.  The string returned from this function
 * does not need to be freed.  Returns NULL if the attribute is not
 * a supported sortable attribute.
 */
const char *AB_LDAPContainerInfo::GetSortAttributeNameFromID(AB_AttribID attrib)
{
	/* Get the LDAP attribute name that corresponds to the AB attribute ID
	 * passed in.
	 */
	switch (attrib) {
	case AB_attribDisplayName:
	case AB_attribFullName:
		return DIR_GetFirstAttributeString(m_server, cn);
	case AB_attribEmailAddress:
		return DIR_GetFirstAttributeString(m_server, mail);
	case AB_attribCompanyName:
		return DIR_GetFirstAttributeString(m_server, o);
	case AB_attribWorkPhone:
		return DIR_GetFirstAttributeString(m_server, telephonenumber);
	case AB_attribLocality:
		return DIR_GetFirstAttributeString(m_server, l);
	case AB_attribNickName:
	default:
		/* We don't even request this value in LDAP searches.
		 */
		return NULL;
	}

	/* If the attribute does not correspond to one of the above, then it
	 * isn't sortable.
	 */
	return NULL;
}

/* IsEntryAttributeSortable
 *
 * Returns true if the given attribute can be used to sort the results.  For an
 * LDAP server that supports VLV, in general, that means the server must have a
 * VLV set up for that attribute.
 *
 * Note: for the return code to be valid for a VLV searches, the client must be
 *       performing a VLV search at the time this function is called.
 */
XP_Bool AB_LDAPContainerInfo::IsEntryAttributeSortable(AB_AttribID attrib)
{
	if (m_performingVLV)
	{
		/* We always assume cn is sortable, even if it is not.  This allows us
		 * to display a VLV on small servers that support VLV but don't have
		 * any VLV entries set up.
		 */
		if (attrib == AB_attribDisplayName)
			return TRUE;

		/* If there is no VLV search list or the attribute is not sortable,
		 * return FALSE.
		 */
		const char *attribName = GetSortAttributeNameFromID(attrib);
		if (!m_server->searchPairList || !m_server->searchPairList[0] || !attribName)
			return FALSE;

		/* Try to find ascending and descending search pairs that match the sort
		 * attribute.
		 */
		XP_Bool ascending = FALSE, descending = FALSE;
		char *pair = m_server->searchPairList;
		char *end = XP_STRCHR(pair, ';');
		while (pair && *pair && (!ascending || !descending))
		{
			if (!ascending && PairMatchesAttribute(pair, end, attribName, TRUE))
				ascending = TRUE;
			else if (!descending && PairMatchesAttribute(pair, end, attribName, FALSE))
				descending = TRUE;
	
			if (end)
				pair = end+1;
			else
				pair = NULL;

			end = XP_STRCHR(pair, ';');
		}

		return ascending && descending;
	}
	else
		return ParentClass::IsEntryAttributeSortable(attrib);
}

/* MatchPairToAttribute
 */
XP_Bool AB_LDAPContainerInfo::PairMatchesAttribute(const char *pair, const char *end, const char *attrib, XP_Bool ascending)
{
	char *p_s = XP_STRCHR(pair, ':');
	if (!p_s || p_s == pair || (++p_s >= end && end))
		return FALSE;
	
	if ((*p_s != '-') != ascending)
		return FALSE;
	else if (*p_s == '-')
		++p_s;

	char *p_e;
	for (p_e = p_s; *p_e && isalnum((int)*p_e); p_e++) ;
	if (p_e > end)
		return FALSE;

	return XP_STRNCASECMP(p_s, attrib, p_e - p_s) == 0;
}


/* The ValidLDAPAttribtesTable defines the order in which the attributes for
 * each entry are stored in memory.  As an optimization certain internal
 * functions may be coded to assume that particular attributes are at
 * particular indices.  If the order of attributes in this table is changed
 * these functions must be updated.  Keep in mind that this table is 1-based
 * with respect to the m_values element indices; the 0 index attribute is
 * always AB_attribEntryType with a numeric value of AB_Person.
 *
 * In general, for effeciency, this table should be sorted in order of the
 * default AB_LDAPEntry sort order.
 *
 * The functions that are implicitly dependant upon the order of attributes
 * in this table are:
 *   AB_LDAPEntry::Compare           - does not consider index 0
 *   AB_LDAPContainerInfo::GetLdapDN - retrieves distinguished name
 */
MSG_SearchAttribute ValidLDAPAttributesTable[] =
{
	attribCommonName,
    attribGivenName,
    attribSurname,
	attrib822Address,
	attribOrganization,
	attribLocality,
	attribPhoneNumber,
	attribDistinguishedName
};

const uint16 AB_NumValidLDAPAttributes = 8;

/* ConvertLDAPToABAttribID
 *
 * We really should eventually phase this routine out and have
 * MSG_SearchAttributes and AB_AttribIDs be the same thing!!!
 */
AB_AttribID AB_LDAPContainerInfo::ConvertLDAPToABAttribID (MSG_SearchAttribute attrib)
{
	switch(attrib)
	{
	case attribGivenName:
		return AB_attribGivenName;
	case attribSurname:
		return AB_attribFamilyName;
	case attrib822Address:
		return AB_attribEmailAddress;
	case attribOrganization:
		return AB_attribCompanyName;
	case attribLocality:
		return AB_attribLocality;
	case attribPhoneNumber:
		return AB_attribWorkPhone;
	case attribDistinguishedName:
		return AB_attribDistName;
	case attribCommonName:
		return AB_attribDisplayName;    /* was fullname */
	default:
		XP_ASSERT(0); // we were given an attribute we don't know how to convert
		return AB_attribGivenName;  // why not....
	}
}

/* ConvertResultElement
 *
 * We need to be able to take a result element and a list of LDAP attributes
 * and produce a list of address book attribute values which could be added as
 * entries to the container. Caller must free the array of attributes by
 * calling AB_FreeAttributeValues.
 */
int AB_LDAPContainerInfo::ConvertResultElement(MSG_ResultElement * elem, MSG_SearchAttribute * attribs, AB_AttributeValue ** retValues, uint16 * numItems)
{
	MSG_SearchValue * result;
	if (!numItems || *numItems == 0)
		return AB_FAILURE;

	AB_AttributeValue * values = (AB_AttributeValue *) XP_ALLOC(sizeof(AB_AttributeValue) * (*numItems+1));  // + 1 to account for the type (person)
	uint16 valuesIndex = 0;   
	
	if (values)
	{
		// LDAP Entries are alwyas person entries! So we are going to manually enter their type into the values list
		values[0].attrib = AB_attribEntryType;
		values[0].u.entryType = AB_Person;

		uint16 i = 0;
		valuesIndex++;  // already added one type
		for (i = 0; i < *numItems; i++,valuesIndex++)
		{
			values[valuesIndex].attrib = ConvertLDAPToABAttribID(attribs[i]);
			values[valuesIndex].u.string = NULL;
			if (MSG_GetResultAttribute(elem, attribs[i], &result) == SearchError_Success)
			{
				if (MSG_IsStringAttribute(attribs[i]))
				{
					if (result->u.string && XP_STRLEN(result->u.string) > 0)
						values[valuesIndex].u.string = XP_STRDUP(result->u.string);
					else
						values[valuesIndex].u.string = NULL;
				}
				else  // add your customized non string attributes here to check for them
					XP_ASSERT(0);  // right now, we have none. All LDAP attribs should be strings.
				MSG_DestroySearchValue(result);
			}
		}

		// Now we need to perform the common name check. If first or last names are empty, use the common name as the first name
		// POTENTIAL HACK ALERT: FOR RIGHT NOW, I am just going to use the common name if I have no first name...
		for (i = 0; i < *numItems; i++)
			if (values[i].attrib == AB_attribGivenName && values[i].u.string && XP_STRLEN(values[i].u.string) == 0)
			{
				if (MSG_GetResultAttribute(elem, attribCommonName, &result) == SearchError_Success)
					if (result->u.string)
						values[i].u.string = XP_STRDUP(result->u.string);
					else
						values[i].u.string = NULL;
				break;
			}

		*numItems = valuesIndex;
		*retValues = values;
		return AB_SUCCESS;
	}
	
	return AB_OUT_OF_MEMORY;
}

int AB_LDAPContainerInfo::CopyEntryValues(MSG_Pane * srcPane, MSG_ViewIndex index, AB_LDAPEntry *entry, AB_AttribID *attribs, AB_AttributeValue **values, uint16 *numItems)
{
	AB_AttributeValue *newValues = (AB_AttributeValue *)XP_ALLOC(sizeof(AB_AttributeValue) * (*numItems));

 	if (newValues)
	{
		int newValueMarker = 0;
		XP_Bool found = FALSE;

		// for each attribute we want...
		for (uint16 j = 0; j < *numItems; j++)
		{
			int loopStatus = AB_FAILURE;
			// by asking if it is a dbase attrib, we are also asking, is it an attribute that would be in the LDAP
			// entry attribute array? Attributes that would not be in this list are attributes that need to be computed
			// like full address or vcard....			
			if (IsDatabaseAttribute(attribs[j]))
			{
				found = FALSE;
				// ...scan through list of known attributes in attempt to find attrib[j]
				for (uint16 index = 0; index < entry->m_numAttributes; index++)
				{
					if (attribs[j] == entry->m_values[index].attrib)
					{
						loopStatus = AssignEntryValues(&(entry->m_values[index]), &newValues[newValueMarker]);
						found = TRUE;
						break;
					}
				}
					
				if (!found)
					loopStatus = AssignEmptyEntryValue(attribs[j], &newValues[newValueMarker]);
			}  // if database attribute
			else
				loopStatus = GetNonDBEntryAttributeForIndex(srcPane, index, attribs[j], &newValues[newValueMarker]);

			if (loopStatus == AB_SUCCESS) // if any of them succeeded,
				newValueMarker++;

		}

		*numItems = newValueMarker;
		*values = newValues;
		return AB_SUCCESS;
	}

	return AB_FAILURE;
}

/* ServerSupportsVLV
 *
 * Returns TRUE if the server supports the virtual list view control or if we
 * don't know if it does.  In the later case, we can always fall-back to a
 * limited implementation of whatever type of search we are going to perform.
 */
XP_Bool AB_LDAPContainerInfo::ServerSupportsVLV()
{
	return    !DIR_TestFlag(m_server, DIR_LDAP_ROOTDSE_PARSED)
	       || DIR_TestFlag(m_server, DIR_LDAP_VIRTUALLISTVIEW);
}

/* UpdatePageSize
 *
 * - Hash table is an array of AB_LDAPEntry pointers
 * - m_lastHashEntry points to the last hash entry retrieved from the server
 * - Hash table size is three times the largest page size.
 */
void AB_LDAPContainerInfo::UpdatePageSize(uint32 pageSize)
{
	AB_DirectoryContainerInfo::UpdatePageSize(pageSize);

	if (ServerSupportsVLV())
	{
		uint32 oldListSize = (uint32)m_entryList.GetSize();
		uint32 newListSize = m_pageSize * 3;

		/* If the size of the entry list we need to accomodate the new page size is
		 * greater than the old entry list size, we need to resize the entry list.
		 */
		if (newListSize > oldListSize)
		{
			m_entryList.SetSize(newListSize);

			AB_LDAPEntry *entry;
			for (;oldListSize < newListSize; oldListSize++)
			{
				entry = (AB_LDAPEntry *)XP_ALLOC(sizeof(AB_LDAPEntry));
				if (entry)
				{
					entry->m_isValid = FALSE;
					m_entryList.SetAt(oldListSize, entry);
				}
				else
				{
					/* Ack! We ran out of memory.  Do the best we can with what
					 * we have.
					 */
					m_entryList.SetSize(oldListSize - 1);
				}
			}
		}
	}
}


/* UseExtendedSelection
 */
XP_Bool AB_LDAPContainerInfo::UseExtendedSelection(MSG_Pane *pane)
{
	return m_performingVLV && pane->GetPaneType() != AB_PICKERPANE;
}

/* CopySelectionEntry
 */
void *AB_LDAPContainerInfo::CopySelectionEntry(void *entry)
{
	return XP_STRDUP((char *)entry);
}

/* DeleteSelectionEntry
 */
void AB_LDAPContainerInfo::DeleteSelectionEntry(void *entry)
{
	XP_FREEIF(entry);
}

/* GetSelectionEntryForIndex
 *
 * Returns the distinguished name for the entry at the given index.  Returns
 * NULL if the index is not for an entry in cache or if we are not currently
 * performing a VLV search.
 */
void *AB_LDAPContainerInfo::GetSelectionEntryForIndex(MSG_ViewIndex index)
{
	if (m_performingVLV)
	{
		AB_LDAPEntry *entry = NULL;
		if (m_firstIndex <= index && index <= m_lastIndex)
			entry = (AB_LDAPEntry *)m_entryList[index - m_firstIndex];

		/* The eighth value is always the distinguished name.
		 */
		if (entry && entry->m_isValid)
			return entry->m_values[8].u.string;
	}

	return NULL;
}

/* CompareSelections
 */
int AB_LDAPContainerInfo::CompareSelections(const void *s1, const void *s2)
{
	return XP_STRCASECMP(*((const char **)s1), *((const char **)s2));
}


/* LDAPSearchResults
 *
 * Called by the front end for each result entry returned from a search.
 */
int AB_LDAPContainerInfo::LDAPSearchResults(MSG_Pane * pane, MSG_ViewIndex index, int32 num)
{
	/* VLV and name completion entry results get added when the search is
	 * complete.
	 */
	if (m_searchType == ABLST_Typedown && ServerSupportsVLV())
		return AB_SUCCESS;

	/* We have a name completion search result, to the NC cache.
	 */
	else if (m_searchType == ABLST_NameCompletion)
	{
		MSG_ResultElement *elem;

		for (MSG_ViewIndex i = index; i < num + index; i++) 
		{
			if (MSG_GetResultElement (pane, index, &elem) == SearchError_Success) 
			{
				AB_LDAPEntry *entry = (AB_LDAPEntry *)XP_ALLOC(sizeof(AB_LDAPEntry));
				if (entry)
				{
					entry->m_numAttributes = AB_NumValidLDAPAttributes;

					if (ConvertResultElement(elem, ValidLDAPAttributesTable, &entry->m_values,
					                         &entry->m_numAttributes) == AB_SUCCESS) 
					{
						int position = m_ncEntryList.Add(entry); // record position to act as ABID for call back
						if (pane->GetPaneType() == AB_PICKERPANE && position >= 0)
							((AB_PickerPane *) pane)->ProcessSearchResult(this, (ABID) position); // call back into NC picker with result
					}
					else
						XP_FREE(entry);
				}
			}
		}
	}

	/* We've got a non-VLV, non-namecompletion result, store it in the DB.
	 */
	else
	{
		uint16 numAttribs = AB_NumValidLDAPAttributes;
		AB_AttributeValue *newValues;  // ptr to array of attribs we want to add to represent the new entry
		MSG_ResultElement *elem;

		for (MSG_ViewIndex i = index; i < num + index; i++) 
		{
			if (MSG_GetResultElement (pane, index, &elem) == SearchError_Success) 
			{
				if (ConvertResultElement(elem, ValidLDAPAttributesTable, &newValues, &numAttribs) == AB_SUCCESS) 
				{
					ABID entryID;
					AddEntry(newValues, numAttribs, &entryID);
					AB_FreeEntryAttributeValues(newValues, numAttribs);
				}
			}
		}
	}

	return AB_SUCCESS;
}

/* FinishSearch
 */
int AB_LDAPContainerInfo::FinishSearch(AB_Pane *pane)
{
	XP_Bool bTotalContentChanged = TRUE;

	if (m_interruptingSearch)
		return AB_SUCCESS;

	/* For LDAP VLV searches we add the entries here and update a variety of
	 * member variables.
	 */
	m_searchPane = NULL;
	if (MSG_GetSearchType(pane) == searchLdapVLV)
	{
		LDAPVirtualList *pLdapVLV;
		pLdapVLV = (LDAPVirtualList *)MSG_GetSearchParam(pane);

		if (pLdapVLV)
		{
			uint32 num = MSG_GetNumResults(pane);
			if (num > 0)
			{
				XP_Bool bFoundMatch = FALSE;
				MSG_ResultElement *elem;
				AB_LDAPEntry *entry;

				if (m_entryMatch.m_isValid)
					m_firstIndex = m_targetIndex;

				for (MSG_ViewIndex i = 0; i < num; i++) 
				{
					entry = (AB_LDAPEntry *)m_entryList[i];
					entry->m_numAttributes = AB_NumValidLDAPAttributes;
					if (MSG_GetResultElement (pane, i, &elem) == SearchError_Success) 
					{
						if (ConvertResultElement(elem, ValidLDAPAttributesTable, &entry->m_values,
							                     &entry->m_numAttributes) == AB_SUCCESS) 
							entry->m_isValid = TRUE;
					}

					if (!bFoundMatch && m_entryMatch.m_isValid)
					{
						if (m_entryMatch != *entry)
							m_firstIndex--;
						else
							bFoundMatch = TRUE;
					}
				}

				/* Update the content count, if necessary.  Do this before
				 * sending any notifications, since that can a new search
				 * which in turn can cause the LDAPVirtualList object to be
				 * destroyed.
				 */
				if (m_updateTotalEntries)
					m_totalEntries = pLdapVLV->ldvlist_size;
				else
					bTotalContentChanged = FALSE;

				/* We need to recompute the first index for any "new" search and
				 * for the special case where our match entry was deleted from
				 * server between the time we retrieved it and now.
				 */
				if (!bFoundMatch)
				{
					/* Must subtract one VLV index index because LDAPVirtualList indices
					 * are 1-based and m_firstIndex is 0-based.
					 */
					if (pLdapVLV->ldvlist_index > m_searchPageSize)
						m_firstIndex = pLdapVLV->ldvlist_index - m_searchPageSize - 1;
					else
						m_firstIndex = 0;
				}
				m_lastIndex = m_firstIndex + num - 1;

				/* When we're doing a new search, we need to notify the FE of
				 * the new top index.
				 */
				if (!bFoundMatch)
					NotifyContainerEntryChange(this, AB_NotifyNewTopIndex, pLdapVLV->ldvlist_index - 1, GetABIDForIndex(pLdapVLV->ldvlist_index - 1), pane);

				/* Notify the FE of the new entries.
				 */
				m_performingVLV = TRUE;
				NotifyContainerEntryRangeChange(this, AB_NotifyPropertyChanged, m_firstIndex, m_lastIndex - m_firstIndex + 1, pane);
			}
			else
			{
				m_performingVLV = FALSE;
				m_searchType = ABLST_None;
				bTotalContentChanged = TRUE;
				m_totalEntries = 0;
				m_firstIndex = 0;
				m_lastIndex = 0;
			}
		}
		else
		{
			m_performingVLV = FALSE;
		}
	}
	else
	{
		m_performingVLV = FALSE;
	}

	/* Free the match entry if it was used for this search.  We do it here
	 * instead of in the VLV case (where it is used) because certain errors
	 * in the search adapter may change the search type and we may no longer
	 * be in a VLV search.
	 */
	if (m_entryMatch.m_isValid)
	{
		m_entryMatch.m_isValid = FALSE;
		AB_FreeEntryAttributeValues(m_entryMatch.m_values, m_entryMatch.m_numAttributes);
	}

	if (bTotalContentChanged)
		NotifyContainerEntryRangeChange(this, AB_NotifyLDAPTotalContentChanged, 0, GetNumEntries(), pane);

	if (m_db && m_db->db_store && m_db->db_env) // if we were in batch mode and we finished a search, end the batch mode...
	{
		AB_Store_EndBatchMode(m_db->db_store, m_db->db_env);
		AB_Env_ForgetErrors(m_db->db_env);
	}


	NotifyContainerAttribChange(this, AB_NotifyStopSearching, pane);
	return AB_SUCCESS;
}


/*
 * Replication Functions
 */

/* StartReplication
 */
AB_LDAPContainerInfo *AB_LDAPContainerInfo::BeginReplication(MWContext *context, DIR_Server *server)
{
	AB_ContainerInfo *ctr;
	if (Create(context, server, &ctr) != AB_SUCCESS)
		return NULL;

	/* It is an error to call this function with a non-LDAP server or when
	 * we're already replicating.
	 */
	AB_LDAPContainerInfo *lctr = ctr->GetLDAPContainerInfo();
	if (!lctr || lctr->m_replicating)
	{
		ctr->Release();
		return NULL;
	}

	/* Allocate the replication database.
	 */
	if (!lctr->m_dbReplica)
	{
		lctr->m_dbReplica = (AB_Database *)XP_CALLOC(1, sizeof(AB_Database));
		if (!lctr->m_dbReplica)
		{
			lctr->Release();
			return NULL;
		}
	}

	/* Allocate the replica attribute map structure.
	 */
	lctr->m_attribMap = lctr->CreateReplicaAttributeMap();
	if (!lctr->m_attribMap)
	{
		lctr->Release();
		return NULL;
	}

	/* Open the replication database.
	 */
	if (!lctr->m_dbReplica->db_env) 
		lctr->InitializeDatabase(lctr->m_dbReplica);

	lctr->m_replicating = TRUE;
	return lctr;
}

/* EndReplication
 */
void AB_LDAPContainerInfo::EndReplication()
{
	if (!m_replicating)
		return;
	
	if (m_attribMap)
		FreeReplicaAttributeMap(m_attribMap);

	m_replicating = FALSE;
	Release();
}

/* AddEntry
 */
XP_Bool AB_LDAPContainerInfo::AddReplicaEntry(char **valueList)
{
	XP_Bool bSuccess = FALSE;

	if (m_replicating)
	{
		// clear the row out...
		AB_Row_ClearAllCells(m_dbReplica->db_row, m_dbReplica->db_env);

		// now write the attributes for our new value into the row....
		AB_Row_WriteCell(m_dbReplica->db_row, m_dbReplica->db_env, "t", AB_Attrib_AsStdColUid(AB_Attrib_kIsPerson));
		for (uint16 i = 0; i < m_attribMap->numAttribs; i++)
			AB_Row_WriteCell(m_dbReplica->db_row, m_dbReplica->db_env, valueList[i], AB_Attrib_AsStdColUid(m_attribMap->attribIDs[i]));

		// once all the cells have been written, write the row to the table
		ab_row_uid RowUid = AB_Row_NewTableRowAt(m_dbReplica->db_row, m_dbReplica->db_env, 0);
		AB_Env_ForgetErrors(m_dbReplica->db_env);

		// did it succeed?
		if (RowUid > 0)
		{
			bSuccess = TRUE;

			if (m_db == m_dbReplica)
				NotifyContainerEntryChange(this, AB_NotifyInserted, GetIndexForABID((ABID) RowUid), (ABID) RowUid, NULL);
		}

		AB_Env_ForgetErrors(m_dbReplica->db_env);
	}

	return bSuccess;
}

/* DeleteEntry
 */
void AB_LDAPContainerInfo::DeleteReplicaEntry(char *targetDn)
{
	if (m_replicating)
	{
		ab_row_uid rowUID = AB_Table_FindFirstRowWithPrefix(m_dbReplica->db_table, m_dbReplica->db_env,
		                                                    targetDn, AB_Attrib_kDistName);

	    if (AB_Env_Good(m_dbReplica->db_env) && rowUID)
		{
			AB_Table_CutRow(m_dbReplica->db_table, m_dbReplica->db_env, rowUID);
			AB_Env_ForgetErrors(m_dbReplica->db_env);
		}
	}
}

/* RemoveReplicaEntries
 */
void AB_LDAPContainerInfo::RemoveReplicaEntries()
{
	if (m_replicating && AB_Table_CountRows(m_dbReplica->db_table, m_dbReplica->db_env))
	{
		CloseDatabase(m_dbReplica);

		char *fullFileName;
		DIR_GetServerFileName(&fullFileName, m_server->replInfo->fileName);
		if (fullFileName)
		{
			/* To be safe, we should kill the file...
			 */
			AB_Env *env = AB_Env_New();
			AB_Env_DestroyStoreWithFileName(env, fullFileName);

			XP_FREE(fullFileName);
		}

		InitializeDatabase(m_dbReplica);
	}
}


/* These arrays provide a way to efficiently set up the replica attribute
 * structure, which itself provides an effecient mapping between ldap attribute
 * names and address book column IDs.
 *
 * Oh how nice it would be if attribute IDs were compatible amongst all the
 * different objects...
 */
static DIR_AttributeId _dirList[8] = 
    { cn,                    givenname,
      sn,                    mail,
      telephonenumber,       o,
      l,                     street
    };
static char *_strList[5] =
    { "title",               "postaladdress",
      "postalcode",          "facsimiletelephonenumber"
    };
static AB_Column_eAttribute _abList[13] =
    { AB_Attrib_kFullName,   AB_Attrib_kGivenName,
      AB_Attrib_kFamilyName, AB_Attrib_kEmail,
      AB_Attrib_kWorkPhone,  AB_Attrib_kCompanyName,
      AB_Attrib_kLocality,   AB_Attrib_kStreetAddress,

      /* The following IDs have no equivalent      * 
       * preference but get replicated.  The LDAP  * 
       * equivalent attribute names come from      *
       * _strList.                                 */
      AB_Attrib_kTitle,      AB_Attrib_kPostalAddress,
      AB_Attrib_kZip,        AB_Attrib_kFax
    };

/* CreateAttributeMap
 */
AB_ReplicaAttributeMap *AB_LDAPContainerInfo::CreateReplicaAttributeMap()
{
	int i, j, cAttribs, cPrefAttribs;
	AB_ReplicaAttributeMap *attribMap;

	/* Allocate space for the attribute list struct and the two lists within
	 * it.  The attribNames list is NULL terminated so that it may be passed
	 * as is to ldap_search.  The maximum number of attributes is the number of
	 * IDs in _abList.
	 */
	cAttribs = sizeof(_abList) / sizeof(AB_Column_eAttribute);
	attribMap = (AB_ReplicaAttributeMap *)XP_CALLOC(1, sizeof(AB_ReplicaAttributeMap));
	if (attribMap)
	{
		attribMap->numAttribs = cAttribs;
		attribMap->attribIDs = (AB_Column_eAttribute *)XP_ALLOC(cAttribs * sizeof(AB_Column_eAttribute));
		attribMap->attribNames = (char **)XP_ALLOC((cAttribs + 1) * sizeof(char *));
	}
	if (!attribMap || !attribMap->attribIDs || !attribMap->attribNames)
		return NULL;

	/* Initialize the attribute list mapping structure with the default values,
	 * and any preference adjustments.
	 */
	cPrefAttribs = sizeof(_dirList) / sizeof(DIR_AttributeId);
	for (cAttribs = 0; cAttribs < cPrefAttribs; cAttribs++)
	{
		attribMap->attribNames[cAttribs] = (char *)DIR_GetFirstAttributeString(m_server, _dirList[cAttribs]);
		attribMap->attribIDs[cAttribs] = _abList[cAttribs];
	}
	for (i = 0; cAttribs < attribMap->numAttribs; i++, cAttribs++)
	{
		attribMap->attribNames[cAttribs] = _strList[i];
		attribMap->attribIDs[cAttribs] = _abList[cAttribs];
	}

	/* Remove any attributes in the exclude list from our replication list.
	 */
	if (m_server->replInfo)
	{
		for (i = 0; i < m_server->replInfo->excludedAttributesCount; i++)
		{
			for (j = 0; j < cAttribs; )
			{
				/* If the current attribute is to be excluded, move the last entry
				 * to the current position and decrement the attribute count.
				 */
				if (!XP_STRCASECMP(attribMap->attribNames[j], m_server->replInfo->excludedAttributes[i]))
				{
					cAttribs--;
					attribMap->attribNames[j] = attribMap->attribNames[cAttribs];
					attribMap->attribIDs[j] = attribMap->attribIDs[cAttribs];
				}
				else
					j++;
			}
		}
	}

	/* Make sure the attribute names list is NULL terminated.
	 */
	attribMap->attribNames[cAttribs] = NULL;

	return attribMap;
}

/* FreeReplicaAttributeMap
 */
void AB_LDAPContainerInfo::FreeReplicaAttributeMap(AB_ReplicaAttributeMap *attribMap)
{
	if (attribMap)
	{
		XP_FREEIF(attribMap->attribIDs);
		XP_FREEIF(attribMap->attribNames);
		XP_FREE(attribMap);
	}
}

/* AttributeMatchesId
 */
XP_Bool AB_LDAPContainerInfo::ReplicaAttributeMatchesId(AB_ReplicaAttributeMap *attribMap, int attribIndex, DIR_AttributeId id)
{
	AB_Column_eAttribute columnId = attribMap->attribIDs[attribIndex];

	switch (columnId) {
    case AB_Attrib_kFullName:
		return id == cn;
	case AB_Attrib_kGivenName:
		return id == givenname;
    case AB_Attrib_kFamilyName:
		return id == sn;
	case AB_Attrib_kEmail:
		return id == mail;
    case AB_Attrib_kWorkPhone:
		return id == telephonenumber;
	case AB_Attrib_kCompanyName:
		return id == o;
    case AB_Attrib_kLocality:
		return id == l;
	case AB_Attrib_kStreetAddress:
		return id == street;

    case AB_Attrib_kTitle:
	case AB_Attrib_kPostalAddress:
    case AB_Attrib_kZip:
	case AB_Attrib_kFax:
	default:
		break;
	}

	return FALSE;
}

AB_LDAPResultsContainerInfo * AB_LDAPContainerInfo::AcquireLDAPResultsContainer(MWContext * context /* context to use for the search */)
{
	AB_LDAPResultsContainerInfo * ctr = NULL;
	if (m_db->db_env)
	{
		ab_Env * ev = ab_Env::AsThis(m_db->db_env);
		ctr = new (*ev) AB_LDAPResultsContainerInfo(context, this);
		m_db->db_env = ev->AsSelf();
		if (AB_Env_Bad(m_db->db_env) || !ctr)  // error during creation,
		{
			if (ctr)  // object is now useless...
			{
				ctr->GoAway();
				ctr = NULL;
			}
		}
		
		AB_Env_ForgetErrors(m_db->db_env);
	}

	return ctr;
}

char * AB_LDAPContainerInfo::BuildLDAPUrl(const char *prefix, const char * distName)
{
	// I pulled this pretty much as is from the old address book code...
	// Generate the URL, using the portnumber only if it isn't the standard one.
	// The 'prefix' is different depending on whether the URL is supposed to open
	// the entry or add it to the Address Book.

	char *url = NULL;

	if (m_server)
	{
		if (m_server->isSecure) 
		{
			if (m_server->port == LDAPS_PORT)
				url = PR_smprintf ("%ss://%s/%s", prefix, m_server->serverName, distName);
			else
				url = PR_smprintf ("%ss://%s:%d/%s", prefix, m_server->serverName, m_server->port, distName);	
		}
		else 
		{
			if (m_server->port == LDAP_PORT)
				url = PR_smprintf ("%s://%s/%s", prefix, m_server->serverName, distName);
			else
				url = PR_smprintf ("%s://%s:%d/%s", prefix, m_server->serverName, m_server->port, distName);
		}
	}

	return url;
}


/***********************************************************************************************************************************

  Definitions for LDAP Result Container Info 

 **********************************************************************************************************************************/

AB_LDAPResultsContainerInfo::AB_LDAPResultsContainerInfo(MWContext * context, AB_LDAPContainerInfo * LDAPCtr)
                            :AB_LDAPContainerInfo(context,LDAPCtr->GetDIRServer(), FALSE)
{
	m_IsCached = FALSE;  // we are not going to be found in the cache!!
	InitializeDatabase(m_db);
}

AB_LDAPResultsContainerInfo::~AB_LDAPResultsContainerInfo()
{
	XP_ASSERT(m_entryList.GetSize() == 0);
}

int AB_LDAPResultsContainerInfo::Close()
{
	RemoveAllUsers();
	return AB_LDAPContainerInfo::Close(); // propgate the close back up
}

void AB_LDAPResultsContainerInfo::RemoveAllUsers()
{
	// free each entry in the results list...
	for (int32 i = m_entryList.GetSize(); i > 0; i--)
	{
		AB_LDAPEntry *entry = (AB_LDAPEntry *) m_entryList[i-1];
		AB_FreeEntryAttributeValues(entry->m_values, entry->m_numAttributes);
		XP_FREE(entry);
	}

	m_entryList.RemoveAll();
}

uint32 AB_LDAPResultsContainerInfo::GetNumEntries()
{
	return m_entryList.GetSize();
}

AB_LDAPEntry * AB_LDAPResultsContainerInfo::GetEntryForIndex(const MSG_ViewIndex index)
{
	if (m_entryList.IsValidIndex(index))
		return m_entryList.GetAt(index);
	else
		return NULL;  // no entry for index
}
 
XP_Bool AB_LDAPResultsContainerInfo::IsInContainer(ABID entryID)
{
	return IsIndexInContainer( GetIndexForABID(entryID) );
}

XP_Bool AB_LDAPResultsContainerInfo::IsIndexInContainer(MSG_ViewIndex index)
{
	return m_entryList.IsValidIndex(index);
}

// entry operations......
int AB_LDAPResultsContainerInfo::CopyEntriesTo(AB_ContainerInfo * destContainer, ABID * idArray, int32 numItems, XP_Bool /*deleteAterCopy*/)
{
	// for each entry, get its LDAP entry & turn it into an array of attribuutes....call AddUser on the dest field
	if (destContainer && destContainer->AcceptsNewEntries())
	{
		for (int32 i = 0; i < numItems; i++)
		{
			MSG_ViewIndex index = GetIndexForABID(idArray[i]);
			AB_LDAPEntry * entry = GetEntryForIndex(index);
			if (entry) // if we have an entry for that index, add it to the destination container...
			{
				ABID entryID = AB_ABIDUNKNOWN;
				destContainer->AddEntry(entry->m_values, entry->m_numAttributes, &entryID); 

				if (CanDeleteEntries())
					DeleteEntries(&idArray[i], 1, NULL);
			}
		}
	}

	return AB_SUCCESS;
}


int AB_LDAPResultsContainerInfo::MoveEntriesTo(AB_ContainerInfo * destContainer, ABID * idArray, int32 numItems)
{
	// can we ever remove elements from here? Not really....
	return CopyEntriesTo(destContainer, idArray, numItems, TRUE);

}

// Getting / Setting entry attributes 

int AB_LDAPResultsContainerInfo::GetEntryAttributes(MSG_Pane * srcPane, ABID entryID, AB_AttribID * attribs, AB_AttributeValue ** values, uint16 * numItems)
{
	MSG_ViewIndex index = GetIndexForABID(entryID);
	return GetEntryAttributesForIndex(srcPane, index, attribs, values, numItems);
}

int AB_LDAPResultsContainerInfo::GetEntryAttributesForIndex(MSG_Pane * srcPane, MSG_ViewIndex index, AB_AttribID * attribs, AB_AttributeValue ** values, uint16 * numItems)
{
	AB_LDAPEntry * entry = GetEntryForIndex(index);
	if (entry)
	{
		AB_AttributeValue *newValues = (AB_AttributeValue *)XP_ALLOC(sizeof(AB_AttributeValue) * (*numItems));
		if (newValues)
		{
			int newValueMarker = 0;
			XP_Bool found = FALSE;

			// for each attribute we want...
			for (uint16 j = 0; j < *numItems; j++)
			{
				int loopStatus = AB_FAILURE;
				if (IsDatabaseAttribute(attribs[j])) // if it is, then it should be in our attribute list, otherwise it is something that needs built like vcard...
 				{
					found = FALSE;
					// ...scan through list of known attributes in attempt to find attrib[j]
					for (uint16 index = 0; index < entry->m_numAttributes; index++)
					{
						if (attribs[j] == entry->m_values[index].attrib)
						{
							loopStatus = AssignEntryValues(&(entry->m_values[index]), &newValues[newValueMarker]);
							found = TRUE;
							break;
						}
					}
					
					if (!found)
						loopStatus = AssignEmptyEntryValue(attribs[j], &newValues[newValueMarker]);
				} // if a database attribute
				else
					loopStatus = GetNonDBEntryAttributeForIndex(srcPane, index, attribs[j], &newValues[newValueMarker]);

				if (loopStatus == AB_SUCCESS) // if someone succeeded....
					newValueMarker++;
			}

			*numItems = newValueMarker;
			*values = newValues;
			return AB_SUCCESS;
		}
		else
			return AB_OUT_OF_MEMORY;
	}
	
	return AB_FAILURE;
 }

// Methods for processung a search
int AB_LDAPResultsContainerInfo::LDAPSearchResults(MSG_Pane * pane, MSG_ViewIndex index, int32 num)
{
	MSG_ResultElement *elem;
	for (MSG_ViewIndex i = index; i < num + index; i++) 
	{
		if (MSG_GetResultElement (pane, index, &elem) == SearchError_Success) 
		{
			AB_LDAPEntry *entry = (AB_LDAPEntry *)XP_ALLOC(sizeof(AB_LDAPEntry));
			if (entry)
			{
				entry->m_numAttributes = AB_NumValidLDAPAttributes;
				if (ConvertResultElement(elem, ValidLDAPAttributesTable, &entry->m_values, &entry->m_numAttributes) == AB_SUCCESS) 
				{
					uint32 position = m_entryList.Add(entry); // record position to act as ABID for call back
					if (pane->GetPaneType() == AB_PICKERPANE && position != MSG_VIEWINDEXNONE)
						((AB_PickerPane *) pane)->ProcessSearchResult(this, GetABIDForIndex(position)); // call back into NC picker with result
				}
				else
					XP_FREE(entry);
			}
		}
	}

	return AB_SUCCESS;
}

int AB_LDAPResultsContainerInfo::FinishSearch(AB_Pane *pane)
{

	if (m_interruptingSearch)
		return AB_SUCCESS;

	NotifyContainerAttribChange(this, AB_NotifyStopSearching, pane);
	if (pane)
		pane->SearchComplete(this); // call back to search (used by name completion)
	m_searchPane = NULL;
	return AB_SUCCESS;
}

int AB_LDAPResultsContainerInfo::NameCompletionSearch(AB_Pane *pane, const char *ncValue)
{
	/* A NULL search string indicates that we should abort the current name
	 * completion search.
	 */
	if (ncValue == NULL)
		return AbortSearch();

	/* Otherwise, start a new name completion search.
	 */
	else
		return PerformSearch(pane, ABLST_NameCompletion, ncValue, 0);
}

ABID AB_LDAPResultsContainerInfo::GetABIDForIndex(const MSG_ViewIndex index)
// what is going on here...well we are assigning our own ABIDs for this ctr because we are not storing any entries in the database.
// Best case scenarion, we would just map view indices directly to ABIDs (i.e. they would be the same). But view indices are zero based
// and 0 is a reserved ABID value for AB_ABIDUNKNOWN. So our ABID designation for an entry will always be 1 + the view index for the entry.
{
	if (index != MSG_VIEWINDEXNONE)
		return (ABID) (index + 1);
	else
		return AB_ABIDUNKNOWN;
}
 
MSG_ViewIndex AB_LDAPResultsContainerInfo::GetIndexForABID(ABID entryID)
{
	MSG_ViewIndex index = MSG_VIEWINDEXNONE;
	if (entryID != AB_ABIDUNKNOWN)
		index = (MSG_ViewIndex) (entryID - 1);
	return index;
}

void AB_LDAPResultsContainerInfo::InitializeDatabase(AB_Database * db)  // each subclass should support code for how it wants to initialize the db.
{
	// okay, we need to get the environment, open a store based on the DIR_Server filename, and then open a table for the address book
	db->db_fluxStack = 0;
	db->db_env = NULL;
	db->db_table = NULL;
	db->db_listTable = NULL;
	db->db_row = NULL;

	// (1) get the environment
	db->db_env = AB_Env_New();
	db->db_env->sEnv_DoTrace = 0;
}

/***********************************************************************************************************************************
 Definitions for PAB Container Info 
 **********************************************************************************************************************************/

AB_PABContainerInfo::AB_PABContainerInfo(MWContext * context, DIR_Server * server) : AB_DirectoryContainerInfo (context, server)
{
	InitializeDatabase(m_db);
}

int AB_PABContainerInfo::Close()
{
	// if we need to do anything for a PAB, insert it here...
	return AB_DirectoryContainerInfo::Close(); 
}


AB_PABContainerInfo::~AB_PABContainerInfo()
{
}

int AB_PABContainerInfo::DeleteEntries(ABID * ids, int32 numIndices, AB_ContainerListener * /*instigator*/)
{
	if (numIndices > 0)
	{
		for (int i = 0; i < numIndices; i++)
		{
			if (m_db->db_table)
			{
				AB_Table_CutRow(m_db->db_table, m_db->db_env, (ab_row_uid) ids[i]);
				AB_Env_ForgetErrors(m_db->db_env);
			}
		}
	}

	return AB_SUCCESS;
}

/* Getting / Setting Entry Attributes with the database implemented */
void AB_PABContainerInfo::InitializeDatabase(AB_Database *db)
{
	// okay, we need to get the environment, open a store based on the DIR_Server filename, and then open a table for the address book
	db->db_fluxStack = 0;
	db->db_env = NULL;
	db->db_table = NULL;
	db->db_listTable = NULL;
	db->db_row = NULL;

	// (1) get the environment
	db->db_env = AB_Env_New();
	db->db_env->sEnv_DoTrace = 0;

	// (2) open a store given the filename in the DIR_Server. What if we don't have a filename? 

	// hack for development only!!!! make sure we don't over write developer's PAB. So always use another
	// file name if our server is the personal address book.
	char * fileName = m_server->fileName;
	if (!XP_STRCASECMP("abook.nab", m_server->fileName))
		fileName = AB_kGromitDbFileName;

	char * myFileName = NULL;
	DIR_GetServerFileName(&myFileName, fileName);
	db->db_store = AB_Env_NewStore(db->db_env, myFileName /* AB_kGromitDbFileName */, AB_Store_kGoodFootprintSpace);
	if (myFileName)
		XP_FREE(myFileName);
	if (db->db_store)
	{
		AB_Store_OpenStoreContent(db->db_store, db->db_env);
		// (3) get an ab_table for the store
		db->db_table = AB_Store_GetTopStoreTable(db->db_store,db->db_env);

		if (AB_Env_Good(db->db_env))
		{
			((ab_Table *) db->db_table)->AddView(ab_Env::AsThis(db->db_env), this);

			// (4) open a row which has all the columns in the address book
			db->db_row = AB_Table_MakeDefaultRow(db->db_table, db->db_env);
			
			AB_Env_ForgetErrors(db->db_env);

			// get table for all entries which are lists...
			db->db_listTable = AB_Table_AcquireListsTable(db->db_table, db->db_env);
		}
	}
}

void AB_PABContainerInfo::SeeBeginModelFlux(ab_Env* /* ev */, ab_Model* /* m */)
{
	if (m_db->db_fluxStack == 0)
	{
		// what do we need to do for this notification?
	}
	
	m_db->db_fluxStack++;
}

void AB_PABContainerInfo::SeeEndModelFlux(ab_Env* /* ev */, ab_Model* /* m */)
{
	if (m_db->db_fluxStack)
		m_db->db_fluxStack--;
	if (m_db->db_fluxStack == 0)
	{
		// what do we need to do for this notification?
		NotifyContainerEntryChange(this, AB_NotifyAll, 0, 0, NULL);
	}
}

void AB_PABContainerInfo::SeeChangedModel(ab_Env* /* ev */, ab_Model* /* m */, const ab_Change* c)
{
	ABID entryID = AB_ABIDUNKNOWN;
	MSG_ViewIndex index = MSG_VIEWINDEXNONE;
	if (c)
	{
		entryID = (ABID) c->mChange_Row; // extract the row UID effected
		index = (MSG_ViewIndex) c->mChange_RowPos;
	}

	if (m_db->db_fluxStack == 0)  // only perform actions if we aren't in a nested model flux...
	{
		if (c->mChange_Mask & ab_Change_kKillRow) /* for delete, remove by index value...*/
			NotifyContainerEntryChange(this, AB_NotifyDeleted, index, entryID, NULL);
		else 
		if (c->mChange_Mask & ab_Change_kAddRow || c->mChange_Mask & ab_Change_kNewRow)
			NotifyContainerEntryChange(this, AB_NotifyInserted, index, entryID, NULL);
		else if (c->mChange_Mask & ab_Change_kPutRow)
		{
			if (index == 0) // database is currently not giving us a position...this needs to change!!!
				index = GetIndexForABID(entryID);
			NotifyContainerEntryChange(this, AB_NotifyPropertyChanged, index, entryID, NULL);
		}
		else
			// let's do worst case and notify all
			NotifyContainerEntryChange(this, AB_NotifyAll, 0, 0, NULL);
	}
}
    
void AB_PABContainerInfo::SeeClosingModel(ab_Env* /* ev */, ab_Model* /* m */, const ab_Change* /* c */)  // database is going away
{
	// stop what we're doing & release all of our database objects, setting them to NULL.
	
	if (GetState() == AB_ObjectOpen)
	{
		// database is closing!!! We should close.....
		Close();  // this should notify listeners that we are going away

	}
}

XP_Bool AB_PABContainerInfo::IsInContainer(ABID entryID)
{
	if (m_db->db_table && m_db->db_env && entryID != AB_ABIDUNKNOWN)
	{
		AB_Env_ForgetErrors(m_db->db_env);
		ab_row_pos pos = AB_Table_RowPos(m_db->db_table, m_db->db_env, (ab_row_uid) entryID);  // if pos > 0, then it is in the table
		if (AB_Env_Good(m_db->db_env) && pos > 0)  
			return TRUE;
		
		// so we had an error or it wasn't in the list...in either case, clear env, return false

		AB_Env_ForgetErrors(m_db->db_env);
	}
	return FALSE; 
}

/**********************************************************************************************************

  Mailing List Container Info class definitions

************************************************************************************************************/

AB_MListContainerInfo::AB_MListContainerInfo(MWContext * context, AB_ContainerInfo * parentContainer, ABID entryID) : AB_NonDirectoryContainerInfo(context, parentContainer, entryID)
{
	// will need to get the actual mailing list table from the database 
	Initialize();
}


AB_MListContainerInfo::~AB_MListContainerInfo()
{
	
}

int AB_MListContainerInfo::Close()
{
	return AB_NonDirectoryContainerInfo::Close();
}

void AB_MListContainerInfo::CloseDatabase(AB_Database *db)		// releases all of our database objects, saves the content, etc. 
{
	if (db->db_row)
	{
		AB_Row_Release(db->db_row, db->db_env);
		db->db_row = NULL;
	}

	// (1) release the table
	if (db->db_table)
	{
		if ( ( (ab_Table *) db->db_table)->IsOpenObject() )
			((ab_Table *) db->db_table)->CutView(ab_Env::AsThis(db->db_env), this);
		
		AB_Table_Close(db->db_table, db->db_env);
		AB_Table_Release(db->db_table, db->db_env);
		// check errors...

		db->db_table = NULL;
	}

	// we don't close the store...only the parent does...

	// (2) release the store
	if (db->db_store)
	{
		AB_Store_SaveStoreContent(db->db_store, db->db_env); /* commit */
		AB_Store_Release(db->db_store, db->db_env);
		AB_Env_ForgetErrors(db->db_env);
		db->db_store = NULL;
	}

	if (db->db_env)
	{
		AB_Env_Release(db->db_env);
		db->db_env = NULL;
	}
}

void AB_MListContainerInfo::Initialize()
{
	// Need to ref count the parent container so that it cannot go away until we are done with it!!
	if (m_parentCtr)
		m_parentCtr->Acquire();

	InitializeDatabase(m_db);
}

void AB_MListContainerInfo::InitializeDatabase(AB_Database *db)
{
	// okay, we need to get the environment, get a table from the parent
	db->db_fluxStack = 0;
	db->db_env = NULL;
	db->db_table = NULL;
	db->db_row = NULL;
	db->db_listTable = NULL;

	// (1) get the environment
	db->db_env = AB_Env_New();
	db->db_env->sEnv_DoTrace = 0;

	// (2) open a store given the filename in the DIR_Server. What if we don't have a filename? 
	db->db_store = m_parentCtr->GetStore(); // do we need to ref count this object??

	if (db->db_store)
	{
		AB_Store_Acquire(db->db_store, db->db_env);  // we need to acquire the store
		AB_Env_ForgetErrors(db->db_env);

		// (3) get an ab_table for the store
		db->db_table = AB_Table_AcquireRowChildrenTable(m_parentCtr->GetTable(), db->db_env, m_entryID);

		if (AB_Env_Good(db->db_env))
		{
			((ab_Table *) db->db_table)->AddView(ab_Env::AsThis(db->db_env), this);
			// (4) open a row which has all the columns in the address book
			db->db_row = AB_Table_MakeDefaultRow(db->db_table, db->db_env);	

			AB_Env_ForgetErrors(db->db_env);

			// get table for all entries which are lists...
//			db->db_listTable = AB_Table_AcquireListsTable(db->db_table, db->db_env);
		}
	}
}

int AB_MListContainerInfo::DeleteEntries(ABID * ids, int32 numIndices, AB_ContainerListener * /* instigator */)
{
	if (numIndices > 0)
	{
		for (int i = 0; i < numIndices; i++)
		{
			if (m_db->db_table)
			{
				AB_Table_CutRow(m_db->db_table, m_db->db_env, (ab_row_uid) ids[i]);
				AB_Env_ForgetErrors(m_db->db_env);
			}
		}
	}

	return AB_SUCCESS;
}

/* database notification handlers */
void AB_MListContainerInfo::SeeChangedModel(ab_Env* /* ev */, ab_Model* /* m */, const ab_Change* c)
{
	ABID entryID = AB_ABIDUNKNOWN;
	MSG_ViewIndex index = MSG_VIEWINDEXNONE;
	if (c)
	{
		entryID = (ABID) c->mChange_Row; // extract the row UID effected
		index = (MSG_ViewIndex) c->mChange_RowPos;
	}


	if (m_db->db_fluxStack == 0)  // only perform actions if we aren't in a nested model flux...
	{
		if (c->mChange_Mask & ab_Change_kKillRow)
			NotifyContainerEntryChange(this, AB_NotifyDeleted, index, entryID, NULL);
		else 
		if (c->mChange_Mask & ab_Change_kAddRow || c->mChange_Mask & ab_Change_kNewRow)
			NotifyContainerEntryChange(this, AB_NotifyInserted, index, entryID, NULL);
		else if (c->mChange_Mask & ab_Change_kPutRow)
		{
			if (index == 0) // database is currently not giving us a position...this needs to change!!!
				index = GetIndexForABID(entryID);
			NotifyContainerEntryChange(this, AB_NotifyPropertyChanged, index, entryID, NULL);
		}
		else if (c->mChange_Mask & ab_Change_kClosing)
			Close();  // we are closing so clean up the ctr and notify all listeners that we are closing
		else
			// let's do worst case and notify all
			NotifyContainerEntryChange(this, AB_NotifyAll, 0, 0, NULL);
	}
}
