/*
 * Copyright (C) 2018-2021, 2023 Andrew Gegg
 *
 *	This file is part of the Garden Notebook application
 *
 * The Garden Notebook application is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl.html>.
 */

/*
	Change log
	2.2.0   Support hsqldb dialect
    2.3.0   Retrieve generated keys properly!
            Date handling change - seems to be a MySQL 8 thing.
    2.4.0   Support MS SQLServer
    2.6.0   Prevent setting SaleItem as direct ancestor
            Prevent setting PurchaseItem as a descendant (PIs cannot be descendants)
            allow leaf items to be disconnected from their ancestors
	3.0.0	Support Locations
			Support Quantity and Severity fields
			Use DBCommentHandler
	3.0.1	Add restoreJsonDump to replace doJsonInsert
	3.1.0	Use jakarta implementation of JSON
 */

package uk.co.gardennotebook.mysql;

import uk.co.gardennotebook.spi.*;

import uk.co.gardennotebook.util.StoryLineTree;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Date;
import java.sql.Timestamp;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import org.apache.logging.log4j.Level;

import jakarta.json.JsonObject;

/**
*
*{@inheritDoc}
*
*	@author	Andy Gegg
*	@version	3.1.0
*	@since	1.0
*/
final class AfflictionEventBuilder implements IAfflictionEventBuilder
{
	private static final Logger LOGGER = LogManager.getLogger();

	private IAfflictionEvent oldInstance = null;

	private final boolean newInstance;

	private INotebookEntry newAncestor = null;
	private boolean changedAncestor = false;

	private int id;
	private int afflictionId;
	private boolean changedAfflictionId = false;

	/*
	*	The problem occurred on plants of this species.
@apiNote
If present, plantVarietyId may or may not be given.  This 'denormalises' the model but enables easy searches for e.g. 'all tomatoes'
	*/
	private Integer plantSpeciesId;
	private boolean changedPlantSpeciesId = false;

	/*
	*	The problem occurred on plants of this variety.
@apiNote
If present, plantSpeciesId must be given.  This 'denormalises' the model but enables easy searches for e.g. 'all tomatoes'
	*/
	private Integer plantVarietyId;
	private boolean changedPlantVarietyId = false;

	/*
	 *	Where the outbreak took place.
	 */
	private Integer locationId;
	private boolean changedLocationId = false;

	/*
	*	When an outbreak is spotted or there is some development
	*/
	private LocalDate date;
	private boolean changedDate = false;

	/*
	 *	Quantifies the activity, e.g. how many seeds were sown.
	 */
	private String quantity;
	private boolean changedQuantity = false;

	/*
	 *	How severe was the outbreak?
	 */
	private String severity;
	private boolean changedSeverity = false;

	private LocalDateTime lastUpdated;
	private LocalDateTime created;
	private boolean somethingChanged = false;

	private boolean changedComments = false;
	private DBCommentHandler commentHandler;	//	compiler will not allow declaration as final

	/**
	*	constructor to use for a new entry
	*/
	AfflictionEventBuilder()
	{
		this(null);
	}

	/**
	*	constructor to use to edit or delete an existing entry
	*
	*	@param	oldVal	the existing item to modify or delete; if null a new entry will be created
	*/
	AfflictionEventBuilder(final IAfflictionEvent oldVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("constructor(): oldVal={}", oldVal);
		if (oldVal == null || oldVal.getKey() == null || !(oldVal.getKey() instanceof Integer))
		{
			newInstance = true;
			oldInstance = null;
			this.id = -1;
			//	need to set the default value for dates
			this.date = LocalDate.now();
			commentHandler = new DBCommentHandler(NotebookEntryType.AFFLICTIONEVENT);
			return;
		}

		newInstance = false;
		oldInstance = oldVal;

		AfflictionEvent baseObj;
		if (oldVal instanceof AfflictionEvent)
		{
			baseObj = (AfflictionEvent)oldVal;
			this.id = baseObj.getId();
			this.afflictionId = baseObj.getAfflictionId();
			this.plantSpeciesId = baseObj.getPlantSpeciesId();
			this.plantVarietyId = baseObj.getPlantVarietyId();
			this.locationId = baseObj.getLocationId();
			this.date = baseObj.getDate();
			this.quantity = baseObj.getQuantity().orElse(null);
			this.severity = baseObj.getSeverity().orElse(null);
			this.lastUpdated = baseObj.getLastUpdated();
			this.created = baseObj.getCreated();
		}
		else
		{
			Object ky = oldVal.getKey();
			if (ky == null) return;
			if (ky instanceof Integer)
				this.id = (Integer)ky;
			ky = oldVal.getAffliction();
			if (ky == null)
			{
				this.afflictionId = 0;
			}
			else
			{
				this.afflictionId = ((IAffliction)ky).getKey();
			}
			ky = oldVal.getPlantSpecies().orElse(null);
			if (ky == null)
			{
				this.plantSpeciesId = null;
			}
			else
			{
				this.plantSpeciesId = ((IPlantSpecies)ky).getKey();
			}
			ky = oldVal.getPlantVariety().orElse(null);
			if (ky == null)
			{
				this.plantVarietyId = null;
			}
			else
			{
				this.plantVarietyId = ((IPlantVariety)ky).getKey();
			}

			ky = oldVal.getLocation().orElse(null);
			if (ky == null)
			{
				this.locationId = null;
			}
			else
			{
				this.locationId = ((ILocation)ky).getKey();
			}

			this.date = oldVal.getDate();

			this.quantity = oldVal.getQuantity().orElse(null);
			this.severity = oldVal.getSeverity().orElse(null);

			this.lastUpdated = oldVal.getLastUpdated();
			this.created = oldVal.getCreated();
		}
		commentHandler = new DBCommentHandler(NotebookEntryType.AFFLICTIONEVENT, this.id);
		LOGGER.traceExit(log4jEntryMsg);
	}	//	constructor()

	/**
	*	give the (new) value of afflictionId
	*
	*	@param	newVal	the new value
	*	@return	this Builder
	*/
	IAfflictionEventBuilder afflictionId(final int newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("afflictionId(): oldVal={}, newVal={}", this.afflictionId, newVal);
		if (this.afflictionId == newVal) return this;
		this.afflictionId = newVal;
		changedAfflictionId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}
	@Override
	public IAfflictionEventBuilder affliction(final IAffliction newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("affliction(): oldVal={}, newVal={}", this.afflictionId, newVal);
		if (newVal == null) return this;
		if (this.afflictionId == newVal.getKey()) return this;
		this.afflictionId = newVal.getKey();
		changedAfflictionId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	/**
	*	give the (new) value of plantSpeciesId
	*
	*	@param	newVal	the new value
	*	@return	this Builder
	*/
	IAfflictionEventBuilder plantSpeciesId(final int newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("plantSpeciesId(): oldVal={}, newVal={}", this.plantSpeciesId, newVal);
		if (this.plantSpeciesId == newVal) return this;
		this.plantSpeciesId = newVal;
		changedPlantSpeciesId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}
	@Override
	public IAfflictionEventBuilder plantSpecies(final IPlantSpecies newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("plantSpecies(): oldVal={}, newVal={}", this.plantSpeciesId, newVal);
		if ((newVal == null) && (this.plantSpeciesId == null)) return this;
		if ((newVal != null) && (this.plantSpeciesId != null) && (this.plantSpeciesId.equals(newVal.getKey()))) return this;
		if (newVal == null)
		{
			this.plantSpeciesId = null;
			if (this.plantVarietyId != null)
			{
				this.plantVarietyId = null;
				changedPlantVarietyId = true;
			}
		}
		else
		{	//	non-null value
			this.plantSpeciesId = newVal.getKey();
			if (this.plantVarietyId != null)
			{
				this.plantVarietyId = null;
				changedPlantVarietyId = true;
			}
		}
		changedPlantSpeciesId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	/**
	*	give the (new) value of plantVarietyId
	*
	*	@param	newVal	the new value
	*	@return	this Builder
	*/
	IAfflictionEventBuilder plantVarietyId(final int newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("plantVarietyId(): oldVal={}, newVal={}", this.plantVarietyId, newVal);
		if (this.plantVarietyId == newVal) return this;
		this.plantVarietyId = newVal;
		changedPlantVarietyId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}
	@Override
	public IAfflictionEventBuilder plantVariety(final IPlantVariety newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("plantVariety(): oldVal={}, newVal={}", this.plantVarietyId, newVal);
		if ((newVal == null) && (this.plantVarietyId == null)) return this;
		if ((newVal != null) && (this.plantVarietyId != null) && (this.plantVarietyId.equals(newVal.getKey()))) return this;
		if (newVal == null)
		{
			this.plantVarietyId = null;
		}
		else
		{	//	non-null value
			this.plantVarietyId = newVal.getKey();
			Integer ps_Id = newVal.getPlantSpecies().getKey();	// cannot be null
			if (!ps_Id.equals(this.plantSpeciesId))	// equals returns false if arg is null
			{
				this.plantSpeciesId = ps_Id;
				changedPlantSpeciesId = true;
			}
		}
		changedPlantVarietyId = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	/**
	 *	give the (new) value of locationId
	 *
	 *	@param	newVal	the new value
	 *	@return	this Builder
	 */
	IAfflictionEventBuilder locationId(final int newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("location(): oldVal={}, newVal={}", this.locationId, newVal);
		if (this.locationId == newVal) return this;
		this.locationId = newVal;
		changedLocationId = true;
		somethingChanged = true;
		LOGGER.traceExit();
		return this;
	}

	@Override
	public IAfflictionEventBuilder location(final ILocation newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("location(): oldVal={}, newVal={}", this.locationId, newVal);
		if ((newVal == null) && (this.locationId == null)) return this;
		if ((newVal != null) && (this.locationId != null) && (this.locationId.equals(newVal.getKey()))) return this;
		if (newVal == null)
		{
			this.locationId = null;
		}
		else
		{	//	non-null value
			this.locationId = newVal.getKey();
		}
		changedLocationId = true;
		somethingChanged = true;
		LOGGER.traceExit();
		return this;
	}

	@Override
	public IAfflictionEventBuilder date(final LocalDate newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("date(): oldVal={}, newVal={}", this.date, newVal);

		if (newVal == null) return this;
		if (newVal.equals(this.date)) return this;
		this.date = newVal;
		changedDate = true;
		somethingChanged = true;
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder quantity(String newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("quantity(): oldVal={}, newVal={}", this.quantity, newVal);

		if (newVal == null && this.quantity == null) return this;
		if (newVal != null && newVal.equals(this.quantity)) return this;
		this.quantity = newVal;
		changedQuantity = true;
		somethingChanged = true;
		LOGGER.traceExit();
		return this;
	}

	@Override
	public IAfflictionEventBuilder severity(String newVal)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("severity(): oldVal={}, newVal={}", this.severity, newVal);

		if (newVal == null && this.severity == null) return this;
		if (newVal != null && newVal.equals(this.severity)) return this;
		this.severity = newVal;
		changedSeverity = true;
		somethingChanged = true;
		LOGGER.traceExit();
		return this;
	}

	@Override
	public IAfflictionEventBuilder addComment(final String... newVals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment[array]()");

		commentHandler.addComment(newVals);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit("addComment");
		return this;
	}

	@Override
	public IAfflictionEventBuilder addComment(final List<String> newVals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("addComment<list>()");

		commentHandler.addComment(newVals);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit("addComment");
		return this;
	}

	/**
	*	remove a comment from this item
	*
	*	@param	newVals	the comment to remove.  If the comment does not exist, this is a null-op
	*	@return	 this Builder
	*/
	IAfflictionEventBuilder deleteComment(int... newVals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteComment()");

		commentHandler.deleteComment(newVals);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder deleteComment(final IComment... newVals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteComment()");

		commentHandler.deleteComment(newVals);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder deleteComment(final List<IComment> vals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteComment()");

		commentHandler.deleteComment(vals);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder changeComment(final IComment base, final String comment)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeComment()");

		commentHandler.changeComment(base, comment);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder changeComment(final IComment base, final LocalDate date)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeComment()");

		commentHandler.changeComment(base, date);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEventBuilder changeComment(final IComment base, final LocalDate date, final String comment)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("changeComment()");

		commentHandler.changeComment(base, date, comment);
		changedComments = commentHandler.isChangedComments();
		LOGGER.traceExit(log4jEntryMsg);
		return this;
	}

	@Override
	public IAfflictionEvent save() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("save()");

		if (!somethingChanged && !newInstance && !changedComments && !changedAncestor)
		{
			LOGGER.traceExit("nothing changed");
			return MySQLCache.cacheAfflictionEvent.get(this.id);
		}

		if (newInstance)
		{
			doInsert();
		}
		else if (somethingChanged)
		{
			doUpdate();
		}

		if (changedComments)
		{
			commentHandler.setParentId(this.id);
			commentHandler.save();
		}

		if (changedAncestor)
		{
			setAncestor(newAncestor);
		}

// mark cache as dirty
		if (!newInstance &&(somethingChanged || changedComments))
		{
			MySQLCache.cacheAfflictionEvent.remove(this.id);
		}
// populate the cache
		new AfflictionEventLister().id(this.id).fetch();
		IAfflictionEvent newValue = MySQLCache.cacheAfflictionEvent.get(this.id);
		if (oldInstance != null)
		{
			oldInstance.flagReplaced(newValue);
		}

	//	tell any parent beans the child list has mutated
	//	only additions and deletions matter, other changes will be reflected through the child bean
		if (newInstance)
		{
			MySQLCache.cacheAffliction.get(afflictionId).flagChildAdded(newValue);
			if (changedPlantSpeciesId && (plantSpeciesId != null) )
			{
				MySQLCache.cachePlantSpecies.get(plantSpeciesId).flagChildAdded(newValue);
			}
			if (changedPlantVarietyId && (plantVarietyId != null) )
			{
				MySQLCache.cachePlantVariety.get(plantVarietyId).flagChildAdded(newValue);
			}
			if (changedLocationId && (locationId != null) )
			{
				MySQLCache.cacheLocation.get(locationId).flagChildAdded(newValue);
			}
		}
		else
		{	//	updated
			if (changedAfflictionId)
			{
				if (oldInstance != null)
				{
					MySQLCache.cacheAffliction.get(oldInstance.getAffliction().getKey()).flagChildDeleted(oldInstance);
				}
				MySQLCache.cacheAffliction.get(newValue.getAffliction().getKey()).flagChildAdded(newValue);
			}
			if (changedPlantSpeciesId)
			{
				if (oldInstance != null)
				{
					oldInstance.getPlantSpecies().ifPresent(item -> MySQLCache.cachePlantSpecies.get(item.getKey()).flagChildDeleted(oldInstance) );
				}
				newValue.getPlantSpecies().ifPresent(item -> MySQLCache.cachePlantSpecies.get(item.getKey()).flagChildAdded(newValue) );
			}
			if (changedPlantVarietyId)
			{
				if (oldInstance != null)
				{
					oldInstance.getPlantVariety().ifPresent(item -> MySQLCache.cachePlantVariety.get(item.getKey()).flagChildDeleted(oldInstance) );
				}
				newValue.getPlantVariety().ifPresent(item -> MySQLCache.cachePlantVariety.get(item.getKey()).flagChildAdded(newValue) );
			}
			if (changedLocationId)
			{
				if (oldInstance != null)
				{
					oldInstance.getLocation().ifPresent(item -> MySQLCache.cacheLocation.get(item.getKey()).flagChildDeleted(oldInstance) );
				}
				newValue.getLocation().ifPresent(item -> MySQLCache.cacheLocation.get(item.getKey()).flagChildAdded(newValue) );
			}
		}

		//	stop multiple saves!
		oldInstance = null;

		somethingChanged = false;
		changedComments = false;
		changedAncestor = false;
		changedAfflictionId = false;
		changedPlantSpeciesId = false;
		changedPlantVarietyId = false;
		changedLocationId = false;
		changedDate = false;
		changedQuantity = false;
		changedSeverity = false;

		LOGGER.traceExit(log4jEntryMsg);
		return newValue;
	}	//	save()

	@Override
	public boolean needSave()
	{
		return somethingChanged || changedComments || changedAncestor;
	}	// needSave()

	@Override
	public boolean canSave()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("canSave(): newInstance={}", newInstance);

		if (!needSave())
		{//	save() will be a null-op but that's OK
			return true;
		}
		if (this.afflictionId <= 0)
		{
			LOGGER.debug("afflictionId not set");
			return false;
		}
		if (this.date == null ||
			this.date == LocalDate.MAX ||
			this.date == LocalDate.MIN)
		{
			LOGGER.debug("date not set");
			return false;
		}
		return true;
	}	// canSave()

	@Override
	public boolean canDelete() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("canDelete(): newInstance={}", newInstance);

		if (newInstance) return LOGGER.traceExit(log4jEntryMsg, false);

		boolean  readValue = false;
		try (	Connection conn = DBConnection.getConnection();
				Statement stmt = conn.createStatement();	)
		{
            String query;
            ResultSet rs;
			switch (DBConnection.DB_IN_USE)
			{
				case MariaDB, MySQL -> query = "select exists (select 1 from storylineindex where (ancestorId = " + this.id +
													" and ancestorType = 'AE') or (descendantId = " + this.id +
													" and descendantType = 'AE')) as fred";
				case hsqldb -> query = "select exists (select 1 from storylineindex where (ancestorId = " + this.id +
													" and ancestorType = 'AE') or (descendantId = " + this.id +
													" and descendantType = 'AE')) as fred from (values(99))";
				case MSSQLServer -> query = "select CASE WHEN EXISTS (select 1 from storylineindex where (ancestorId = " + this.id +
													" and ancestorType = 'AE') OR (descendantId = " + this.id +
													" and descendantType = 'AE')) THEN 1 ELSE 0 END as fred";
				default -> {
					LOGGER.error("canDelete(): no known rdbms");
					throw new GNDBException(new IllegalStateException("AfflictionBuilder: canDelete(): no known RDBMS"));
				}
			}
LOGGER.debug("canDelete(): query: {}", query);
			rs = stmt.executeQuery(query);
            rs.next();
            readValue = rs.getBoolean("fred");
LOGGER.debug("canDelete(): readValue: {}", readValue);
			if (readValue)
			{
				return LOGGER.traceExit(log4jEntryMsg, false);
			}
			stmt.close();
		}catch (SQLException ex) {
			LOGGER.error("canDelete(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
		return LOGGER.traceExit(log4jEntryMsg, !readValue);
	}	// canDelete()

	@Override
	public void delete() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("delete(): newInstance={}", newInstance);

		if (newInstance) return;
		if (!canDelete()) return;

		int res = 0;
		String query = "delete from afflictionevent where afflictionEventId = " + this.id;
LOGGER.debug("delete(): query: {}", query);
		try (	Connection conn = DBConnection.getConnection();
				Statement stmt = conn.createStatement();	)
		{
			res = stmt.executeUpdate(query);
LOGGER.debug("delete(): result: {}", res);
			// tidy up dependencies with nullable keys
			if (res > 0) {
				query = "delete from comment where ownerId = " + this.id + " and ownerType = 'AE'";
				int res2 = stmt.executeUpdate(query);
LOGGER.debug("delete() comments: result: {}", res2);
			}
			stmt.close();
		}catch (SQLException ex) {
			LOGGER.error("delete(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
		if (res > 0)
		{
			oldInstance.flagDeleted();
			MySQLCache.cacheAfflictionEvent.remove(this.id);
	//	tell any parent beans the child list has mutated
	//	only additions and deletions matter, other changes will be reflected through the child bean
			MySQLCache.cacheAffliction.get(oldInstance.getAffliction().getKey()).flagChildDeleted(oldInstance);
			oldInstance.getPlantSpecies().ifPresent(item -> MySQLCache.cachePlantSpecies.get(item.getKey()).flagChildDeleted(oldInstance) );
			oldInstance.getPlantVariety().ifPresent(item -> MySQLCache.cachePlantVariety.get(item.getKey()).flagChildDeleted(oldInstance) );
		}
		oldInstance = null;
LOGGER.traceExit(log4jEntryMsg);
	}	// delete()

	private void doUpdate() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("doUpdate(): newInstance={}, somethingChanged={}", newInstance, somethingChanged);

		if (newInstance) return;
		if (!somethingChanged) return;
		StringBuilder query = new StringBuilder("update afflictionevent set ");
		if (changedAfflictionId)
		{
			query.append("afflictionId = ?, ");
		}

		if (changedPlantSpeciesId)
		{
			query.append("plantSpeciesId = ?, ");
		}

		if (changedPlantVarietyId)
		{
			query.append("plantVarietyId = ?, ");
		}

		if (changedLocationId)
		{
			query.append("locationId = ?, ");
		}

		if (changedDate)
		{
			query.append("date = ?, ");
		}

		if (changedQuantity)
		{
			query.append("quantity = ?, ");
		}

		if (changedSeverity)
		{
			query.append("severity = ?, ");
		}

		query.delete(query.length()-2, query.length());
		query.append(" where afflictionEventId = ").append(this.id);
LOGGER.debug("doUpdate(): query={} ", query.toString());
		try (	Connection conn = DBConnection.getConnection();
				PreparedStatement stmt = conn.prepareStatement(query.toString());	)
		{
			int paramIx = 1;
			if (changedAfflictionId)
			{
LOGGER.debug("doUpdate(): param {}={}", paramIx, this.afflictionId);
				stmt.setInt(paramIx++, this.afflictionId);
			}

			if (changedPlantSpeciesId)
			{
				if (this.plantSpeciesId == null)
				{
LOGGER.debug("doUpdate(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
LOGGER.debug("doUpdate(): param {}={}", paramIx, this.plantSpeciesId);
					stmt.setInt(paramIx++, this.plantSpeciesId);
				}
			}

			if (changedPlantVarietyId)
			{
				if (this.plantVarietyId == null)
				{
LOGGER.debug("doUpdate(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
LOGGER.debug("doUpdate(): param {}={}", paramIx, this.plantVarietyId);
					stmt.setInt(paramIx++, this.plantVarietyId);
				}
			}

			if (changedLocationId)
			{
				if (this.locationId == null)
				{
					LOGGER.debug("doUpdate(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
					LOGGER.debug("doUpdate(): param {}={}", paramIx, this.locationId);
					stmt.setInt(paramIx++, this.locationId);
				}
			}

			if (changedDate)
			{
LOGGER.debug("doUpdate(): param {}={}", paramIx, Date.valueOf(this.date));
				stmt.setDate(paramIx++, Date.valueOf(this.date), java.util.Calendar.getInstance()); //  2.3.0
			}

			if (changedQuantity)
			{
				if (this.quantity == null)
				{
					LOGGER.debug("doUpdate(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("doUpdate(): param {}={}", paramIx, this.quantity);
					stmt.setString(paramIx++, this.quantity);
				}
			}

			if (changedSeverity)
			{
				if (this.severity == null)
				{
					LOGGER.debug("doUpdate(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("doUpdate(): param {}={}", paramIx, this.severity);
					stmt.setString(paramIx++, this.severity);
				}
			}

			stmt.executeUpdate();

		}catch (SQLException ex) {
			LOGGER.error("doUpdate(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
LOGGER.traceExit(log4jEntryMsg);
	}	// doUpdate

	private void doInsert() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("doInsert(): newInstance={}", newInstance);

		if (!newInstance) return;
		if (!canSave())
		{
			throw LOGGER.throwing(Level.ERROR, new IllegalStateException("doInsert(): save criteria not met"));
		}
		if (!this.changedAfflictionId)
		{
			throw LOGGER.throwing(Level.ERROR, new IllegalStateException("AfflictionEventBuilder: doInsert(): afflictionId unspecified"));
		}

		StringBuilder query = new StringBuilder("insert into afflictionevent (");
		query.append("afflictionId, ");
		query.append("date, ");
		if (changedPlantSpeciesId)
		{
			query.append("plantSpeciesId, ");
		}

		if (changedPlantVarietyId)
		{
			query.append("plantVarietyId, ");
		}

		if (changedLocationId)
		{
			query.append("locationId, ");
		}

		if (changedQuantity)
		{
			query.append("quantity, ");
		}

		if (changedSeverity)
		{
			query.append("severity, ");
		}

		query.replace(query.length()-2, query.length(), ") values (");
		query.append("?, ");
		query.append("?, ");
		if (changedPlantSpeciesId)
		{
			query.append("?, ");
		}

		if (changedPlantVarietyId)
		{
			query.append("?, ");
		}

		if (changedLocationId)
		{
			query.append("?, ");
		}

		if (changedQuantity)
		{
			query.append("?, ");
		}

		if (changedSeverity)
		{
			query.append("?, ");
		}

		query.replace(query.length()-2, query.length(), ")");
LOGGER.debug("doInsert(): query={}", query.toString());

		try (	Connection conn = DBConnection.getConnection();
				PreparedStatement stmt = conn.prepareStatement(query.toString(), Statement.RETURN_GENERATED_KEYS); )
		{
			int paramIx = 1;
LOGGER.debug("doInsert(): param {}={}", paramIx, this.afflictionId);
			stmt.setInt(paramIx++, this.afflictionId);
LOGGER.debug("doInsert(): param {}={}", paramIx, Date.valueOf(this.date));
			stmt.setDate(paramIx++, Date.valueOf(this.date), java.util.Calendar.getInstance()); //  2.3.0
			if (changedPlantSpeciesId) {
				if (this.plantSpeciesId == null)
				{
LOGGER.debug("doInsert(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
LOGGER.debug("doInsert(): param {}={}", paramIx, this.plantSpeciesId);
					stmt.setInt(paramIx++, this.plantSpeciesId);
				}
			}

			if (changedPlantVarietyId) {
				if (this.plantVarietyId == null)
				{
LOGGER.debug("doInsert(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
LOGGER.debug("doInsert(): param {}={}", paramIx, this.plantVarietyId);
					stmt.setInt(paramIx++, this.plantVarietyId);
				}
			}

			if (changedLocationId) {
				if (this.locationId == null)
				{
					LOGGER.debug("doInsert(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
					LOGGER.debug("doInsert(): param {}={}", paramIx, this.locationId);
					stmt.setInt(paramIx++, this.locationId);
				}
			}

			if (changedQuantity) {
				if (this.quantity == null)
				{
					LOGGER.debug("doInsert(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("doInsert(): param {}={}", paramIx, this.quantity);
					stmt.setString(paramIx++, this.quantity);
				}
			}

			if (changedSeverity) {
				if (this.severity == null)
				{
					LOGGER.debug("doInsert(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("doInsert(): param {}={}", paramIx, this.severity);
					stmt.setString(paramIx++, this.severity);
				}
			}

			stmt.executeUpdate();

			ResultSet rs = stmt.getGeneratedKeys();
			rs.next();
			int newId = rs.getInt(1);
LOGGER.debug("doInsert(): newId: {}", newId);
			this.id = newId;
		}catch (SQLException ex) {
			LOGGER.error("doInsert(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}

LOGGER.traceExit(log4jEntryMsg);
	}	// doInsert

	/**
	*	Used to restore a table from a JSON dump.
	*	The AfflictionEventBuilder(oldVal) constructor MUST be used with a AfflictionEvent object created using its JSON constructor.
	*	All fields (including timestamps) are written to the database, no checks are made.
	*
	*	@param	newVal	A AfflictionEvent object created from a JSON object written as a DUMP.
*
*	@throws	GNDBException	If the underlying MySQL database throws SQLException it is translated to this.
*				The causal SQLException can be retrieved by <code>getCause()</code>
	 *
	 * @deprecated Deprecated since 3.0.1, use the {@link #}restoreJsonDump(List<JsonObject>)} method.
	 * The replacement is much faster, particularly with MySQL.
	*/
	@Deprecated(forRemoval = true)
	void doJsonInsert(AfflictionEvent newVal) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("doJsonInsert(): newVal={}", newVal);

		int currId = newVal.getId();
		StringBuilder query = new StringBuilder("insert into afflictionevent (");
		if (newVal.getId() > 0)
		{//this forces the value of the id field.  The >0 test is a bodge.
			query.append("afflictionEventId, ");
		}
		query.append("afflictionId, ");
		query.append("plantSpeciesId, ");
		query.append("plantVarietyId, ");
		query.append("locationId, ");
		query.append("date, ");
		query.append("quantity, ");
		query.append("severity, ");
		query.append("lastUpdated, ");
		query.append("created, ");
//		query.replace(query.length()-2, query.length(), ") values (");
		query.replace(query.length()-2, query.length(), ")");
        if (newVal.getId() > 0 && DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.hsqldb)
        {
            query.append(" overriding system value ");
        }
        query.append(" values (");
		if (newVal.getId() > 0)
		{//this forces the value of the id field.  The >0 test is a bodge.
			query.append(newVal.getId()).append(", ");
		}
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?, ");
		query.append("?) ");
//		query.replace(query.length()-2, query.length(), ")");
LOGGER.debug("doJsonInsert(): query={}", query.toString());

		try (	Connection conn = DBConnection.getConnection();
				PreparedStatement stmt = conn.prepareStatement(query.toString(), Statement.RETURN_GENERATED_KEYS); )
		{
			int paramIx = 1;
LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getAfflictionId());
			stmt.setInt(paramIx++, newVal.getAfflictionId());
			if (newVal.getPlantSpeciesId() == null)
			{
LOGGER.debug("doJsonInsert(): param {} null", paramIx);
				stmt.setNull(paramIx++, java.sql.Types.INTEGER);
			}
			else
			{
LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getPlantSpeciesId());
				stmt.setInt(paramIx++, newVal.getPlantSpeciesId());
			}

			if (newVal.getPlantVarietyId() == null)
			{
LOGGER.debug("doJsonInsert(): param {} null", paramIx);
				stmt.setNull(paramIx++, java.sql.Types.INTEGER);
			}
			else
			{
LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getPlantVarietyId());
				stmt.setInt(paramIx++, newVal.getPlantVarietyId());
			}

			if (newVal.getLocationId() == null)
			{
				LOGGER.debug("doJsonInsert(): param {} null", paramIx);
				stmt.setNull(paramIx++, java.sql.Types.INTEGER);
			}
			else
			{
				LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getLocationId());
				stmt.setInt(paramIx++, newVal.getLocationId());
			}

			LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getDate());
			stmt.setDate(paramIx++, Date.valueOf(newVal.getDate()), java.util.Calendar.getInstance()); //  2.3.0

			if (newVal.getQuantity().isEmpty())
			{
				LOGGER.debug("doJsonInsert(): param {} null", paramIx);
				stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
			}
			else
			{
				LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getQuantity().get());
				stmt.setString(paramIx++, newVal.getQuantity().get());
			}

			if (newVal.getSeverity().isEmpty())
			{
				LOGGER.debug("doJsonInsert(): param {} null", paramIx);
				stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
			}
			else
			{
				LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getSeverity().get());
				stmt.setString(paramIx++, newVal.getSeverity().get());
			}

			LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getLastUpdated());
			stmt.setTimestamp(paramIx++, Timestamp.valueOf(newVal.getLastUpdated()));
LOGGER.debug("doJsonInsert(): param {}={}", paramIx, newVal.getCreated());
			stmt.setTimestamp(paramIx++, Timestamp.valueOf(newVal.getCreated()));

            if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer )
            {
                conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent ON");
            }
            
            stmt.executeUpdate();

			if (currId <= 0)
			{
                ResultSet rs = stmt.getGeneratedKeys();
                rs.next();
				currId = rs.getInt(1);
LOGGER.debug("doJsonInsert(): currId: {}", currId);
			}
            
            if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer )
            {
                conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent OFF");
            }

            if (!newVal.getComments().isEmpty())
            {
                CommentBuilder cb = new CommentBuilder(NotebookEntryType.AFFLICTIONEVENT, currId);
                cb.doJsonInsert(newVal.getComments(), conn);
            }
		}catch (SQLException ex) {
			LOGGER.error("doJsonInsert(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}

LOGGER.traceExit(log4jEntryMsg);
	}	// doJsonInsert

	/**
	 * Process the whole JSON array from a DUMP
	 *
	 *  @param newVal    a list of JSON objects representing AfflictionEvent entries as output by a JSON DUMP
	 *  @throws	GNDBException	If the underlying MySQL database throws SQLException it is translated to this.
	 *				The causal SQLException can be retrieved by <code>getCause()</code>
	 *
	 * @since 3.0.1
	 */
	void restoreJsonDump(List<JsonObject> newVal) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("restoreJsonDump(list JSON)");

		if (newVal.isEmpty())
			return;

		String query = "insert into afflictionevent (";
		query += "afflictionEventId, ";
		query += "afflictionId, ";
		query += "plantSpeciesId, plantVarietyId, ";
		query += "locationId, ";
		query += "date, ";
		query += "quantity, ";
		query += "severity, ";
		query += "lastUpdated, created) ";

		if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.hsqldb)
		{
			query += " overriding system value ";
		}
		query += " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
		LOGGER.debug("doJsonInsert(): query={}", query);

		try (	Connection conn = DBConnection.getConnection();
				 PreparedStatement stmt = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); )
		{
			conn.setAutoCommit(false);
			int txCount = 0;
			if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer )
			{
				conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent ON");
			}

			for (JsonObject jo : newVal)
			{
				if (!"DUMP".equals(jo.getString("JsonMode", "DUMP")))
				{
					LOGGER.error("AfflictionEvent DUMP object is not DUMP");
					throw new IllegalArgumentException("AfflictionEvent DUMP object is not DUMP");
				}
				if (!"AfflictionEvent".equals(jo.getString("JsonNBClass", "AfflictionEvent")))
				{
					LOGGER.error("AfflictionEvent DUMP object is not AfflictionEvent");
					throw new IllegalArgumentException("AfflictionEvent DUMP object is not AfflictionEvent");
				}
				AfflictionEvent ps = new AfflictionEvent(jo);
				if (ps.getId() <= 0)
				{//this forces the value of the id field.  The >0 test is a bodge.
					LOGGER.error("AfflictionEvent DUMP object does not have an id");
					throw new IllegalArgumentException("AfflictionEvent DUMP object does not have an id");
				}

				int paramIx = 1;
				LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getId());
				stmt.setInt(paramIx++, ps.getId());

				LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getAfflictionId());
				stmt.setInt(paramIx++, ps.getAfflictionId());

				if (ps.getPlantSpeciesId() == null)
				{
					LOGGER.debug("restoreJsonDump(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
					LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getPlantSpeciesId());
					stmt.setInt(paramIx++, ps.getPlantSpeciesId());
				}

				if (ps.getPlantVarietyId() == null)
				{
					LOGGER.debug("restoreJsonDump(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
					LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getPlantVarietyId());
					stmt.setInt(paramIx++, ps.getPlantVarietyId());
				}

				if (ps.getLocationId() == null)
				{
					LOGGER.debug("restoreJsonDump(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.INTEGER);
				}
				else
				{
					LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getLocationId());
					stmt.setInt(paramIx++, ps.getLocationId());
				}

				LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getDate());
				stmt.setDate(paramIx++, Date.valueOf(ps.getDate()), java.util.Calendar.getInstance()); //  2.3.0

				if (ps.getQuantity().isEmpty())
				{
					LOGGER.debug("restoreJsonDump(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getQuantity().get());
					stmt.setString(paramIx++, ps.getQuantity().get());
				}

				if (ps.getSeverity().isEmpty())
				{
					LOGGER.debug("restoreJsonDump(): param {} null", paramIx);
					stmt.setNull(paramIx++, java.sql.Types.VARCHAR);
				}
				else
				{
					LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getSeverity().get());
					stmt.setString(paramIx++, ps.getSeverity().get());
				}

				LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getLastUpdated());
				stmt.setTimestamp(paramIx++, Timestamp.valueOf(ps.getLastUpdated()));
				LOGGER.debug("restoreJsonDump(): param {}={}", paramIx, ps.getCreated());
				stmt.setTimestamp(paramIx++, Timestamp.valueOf(ps.getCreated()));

				stmt.executeUpdate();


				if (!ps.getComments().isEmpty())
				{
					if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer)
					{
						conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent OFF");
					}

					CommentBuilder cb = new CommentBuilder(NotebookEntryType.AFFLICTIONEVENT, ps.getId());
					cb.doJsonInsert(ps.getComments(), conn);
					if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer )
					{
						conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent ON");
					}
				}

				if (++txCount > 100)	//	increased from 50 to improve MySQL performance
				{
					conn.commit();
					txCount = 0;
				}
			}

			conn.commit();
			if (DBConnection.DB_IN_USE == DBConnection.RDBMS_ENUM.MSSQLServer )
			{
				conn.createStatement().execute("SET IDENTITY_INSERT afflictionevent OFF");
			}

		}catch (SQLException ex) {
			LOGGER.error("doJsonInsert(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}

		LOGGER.traceExit(log4jEntryMsg);
	}	// restoreJsonDump

	@Override
	public boolean hasAncestor() throws GNDBException
	{
		if (newInstance)
		{
			return false;
		}
		return new StoryLineIndexLister().hasAncestor(oldInstance);
	}	//	hasAncestor()

	@Override
	public StoryLineTree<? extends INotebookEntry> getAncestors() throws GNDBException
	{
		if (newInstance)
		{
			return StoryLineTree.emptyTree();
		}
		return new StoryLineIndexLister().getAncestors(oldInstance);
	}	//	getAncestors()

	@Override
	public IAfflictionEventBuilder ancestor(IPurchaseItem ancestor) throws GNDBException
	{
		if (hasAncestor() || hasDescendant())
		{
			return this;
		}
		this.newAncestor = ancestor;
		changedAncestor = true;
		return this;
	}	//	ancestor(PurchaseItem)

	@Override
	public IAfflictionEventBuilder ancestor(IGroundwork ancestor) throws GNDBException
	{
		if (hasAncestor() || hasDescendant())
		{
			return this;
		}
		this.newAncestor = ancestor;
		changedAncestor = true;
		return this;
	}	//	ancestor(Groundwork)

	@Override
	public IAfflictionEventBuilder ancestor(IAfflictionEvent ancestor) throws GNDBException
	{
		if (hasAncestor() || hasDescendant())
		{
			return this;
		}
		this.newAncestor = ancestor;
		changedAncestor = true;
		return this;
	}	//	ancestor(AfflictionEvent)

	@Override
	public IAfflictionEventBuilder ancestor(IHusbandry ancestor) throws GNDBException
	{
		if (hasAncestor() || hasDescendant())
		{
			return this;
		}
		this.newAncestor = ancestor;
		changedAncestor = true;
		return this;
	}	//	ancestor(Husbandry)


	private boolean setAncestor(INotebookEntry mother) throws GNDBException
	{
		if (hasAncestor() || hasDescendant())
		{
			return false;
		}
		return new StoryLineIndexBuilder().addDescendant(mother.getType(), mother.getKey(), NotebookEntryType.AFFLICTIONEVENT, this.id);
	}	//	setAncestor()

	@Override
	public void dropLeaf() throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("dropLeaf(): newInstance={}", newInstance);

		if (newInstance)
            return;
        
        if (hasDescendant())
            return;
        if (!hasAncestor())
            return;

		new StoryLineIndexBuilder().dropLeaf(NotebookEntryType.AFFLICTIONEVENT, this.id);
    }
    
	@Override
	public boolean hasDescendant() throws GNDBException
	{
		if (newInstance)
		{
			return false;
		}
		return new StoryLineIndexLister().hasDescendant(oldInstance);
	}	//	hasDescendant()

	@Override
	public StoryLineTree<? extends INotebookEntry> getDescendants() throws GNDBException
	{
		if (newInstance)
		{
			return StoryLineTree.emptyTree();
		}
		return new StoryLineIndexLister().getDescendants(oldInstance);
	}	//	getDescendants()

	@Override
	public boolean addDescendant(IGroundwork descendant) throws GNDBException
	{
		LOGGER.debug("addDescendant(): descendant: {}", descendant);
		if (newInstance)
		{
			return false;
		}
		if (this.id <= 0)
		{
			return false;
		}
		GroundworkBuilder bld = new GroundworkBuilder(descendant);
		if (bld.hasAncestor() || bld.hasDescendant())
		{
			return false;
		}
		bld = null;
		return new StoryLineIndexBuilder().addDescendant(NotebookEntryType.AFFLICTIONEVENT, this.id, descendant.getType(), descendant.getKey());
	}	//	addDescendant(Groundwork)

	@Override
	public boolean addDescendant(IAfflictionEvent descendant) throws GNDBException
	{
		LOGGER.debug("addDescendant(): descendant: {}", descendant);
		if (newInstance)
		{
			return false;
		}
		if (this.id <= 0)
		{
			return false;
		}
		AfflictionEventBuilder bld = new AfflictionEventBuilder(descendant);
		if (bld.hasAncestor() || bld.hasDescendant())
		{
			return false;
		}
		bld = null;
		return new StoryLineIndexBuilder().addDescendant(NotebookEntryType.AFFLICTIONEVENT, this.id, descendant.getType(), descendant.getKey());
	}	//	addDescendant(AfflictionEvent)

	@Override
	public boolean addDescendant(IHusbandry descendant) throws GNDBException
	{
		LOGGER.debug("addDescendant(): descendant: {}", descendant);
		if (newInstance)
		{
			return false;
		}
		if (this.id <= 0)
		{
			return false;
		}
		HusbandryBuilder bld = new HusbandryBuilder(descendant);
		if (bld.hasAncestor() || bld.hasDescendant())
		{
			return false;
		}
		bld = null;
		return new StoryLineIndexBuilder().addDescendant(NotebookEntryType.AFFLICTIONEVENT, this.id, descendant.getType(), descendant.getKey());
	}	//	addDescendant(Husbandry)

	@Override
	public boolean addDescendant(ISaleItem descendant) throws GNDBException
	{
		LOGGER.debug("addDescendant(): descendant: {}", descendant);
		if (newInstance)
		{
			return false;
		}
		if (this.id <= 0)
		{
			return false;
		}
		SaleItemBuilder bld = new SaleItemBuilder(descendant);
		if (bld.hasAncestor() || bld.hasDescendant())
		{
			return false;
		}
		bld = null;
		return new StoryLineIndexBuilder().addDescendant(NotebookEntryType.AFFLICTIONEVENT, this.id, descendant.getType(), descendant.getKey());
	}	//	addDescendant(SaleItem)

}

