/*
 * Copyright (C) 2018-2020, 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
            fix cache miss error in fetchDescendants
	2.5.0   Support MS SQLServer
    2.6.0   Move hasAncestor() and hasDescendant() to StoryLineIndexUtils
	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.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;

import java.sql.*;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonWriter;
import jakarta.json.JsonWriterFactory;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;

/**
This is the 'closure table' for the storyline (thread) hierarchy.<BR>
Given a tree structure<BR> 
{@code A->B->C, A->D}<BR>
there is an entry for each link, chased all the way back up:<BR>
(A,A,0) (A,B,1) (B,B,0) (A,C,2) (C,C,0) (B,C,1),<BR>
(A,D,1) (D,D,0)<BR>
It's easy to get the id of every ancestor or descendant of a given node.<BR>
The self-referencing nodes (A,A) make it easy to insert new nodes (single SQL rather than several bits).<BR>
Multiple parents are supported (cf a BoM structure).<BR>

For appropriate SQL see, e.g.,<BR>
<a href="https://www.slideshare.net/billkarwin/models-for-hierarchical-data">https://www.slideshare.net/billkarwin/models-for-hierarchical-data</a>
(slide 41 et seq)<BR>
and also<BR>
<a href="https://gist.github.com/ekillaby/2377806">https://gist.github.com/ekillaby/2377806</a>
and also<BR>
<a href="http://karwin.blogspot.co.uk/2010/03/rendering-trees-with-closure-tables.html">http://karwin.blogspot.co.uk/2010/03/rendering-trees-with-closure-tables.html</a>

*
* @throws	GNDBException	All visible methods can throw this if there is an underlying DB Exception.
*							The causal SQLException can be retrieved by <code>getCause()</code>
* 
*	@author	Andy Gegg
*	@version	3.1.0
*	@since	1.0
*/

final class StoryLineIndexLister
{
	private static final Logger LOGGER = LogManager.getLogger();

//	/**
//	PurchaseItems do not have ancestors
//	*	check if this PurchaseItem has any ancestors
//	*
//	*	@param	child	check for ancestors of this PurchaseItem
//	*
//	*	@return	true if this PurchaseItem has any ancestors
//	*/
//	boolean hasAncestor(IPurchaseItem child) throws GNDBException
//	{
//		return StoryLineIndexUtils.hasAncestor(NotebookEntryType.PURCHASEITEM, child.getKey());
//	}

//	/**
//	*	get the ancestors of this PurchaseItem
//	PurchaseItems do not have ancestors
//	*
//	*	@param	child	get ancestors of this PurchaseItem
//	*
//	*	@return	the links for the ancestors of this PurchaseItem
//	*/
//	StoryLineTree<? extends INotebookEntry> getAncestors(IPurchaseItem child) throws GNDBException
//	{
//		return fetchAncestors(NotebookEntryType.PURCHASEITEM, child.getKey());
//	}

	/**
	*	check if this PurchaseItem has any descendants
	*
	*	@param	parent	check for descendants of this PurchaseItem
	*
	*	@return	true if this PurchaseItem has any descendants
	*/
	boolean hasDescendant(IPurchaseItem parent) throws GNDBException
	{
		return StoryLineIndexUtils.hasDescendant(NotebookEntryType.PURCHASEITEM, parent.getKey());
	}

	/**
	*	get the descendants of this PurchaseItem
	*
	*	@param	parent	get descendants of this PurchaseItem
	*
	*	@return	the links for the descendants of this PurchaseItem
	*/
	StoryLineTree<? extends INotebookEntry> getDescendants(IPurchaseItem parent) throws GNDBException
	{
		return fetchDescendants(NotebookEntryType.PURCHASEITEM, parent.getKey());
	}

	/**
	*	check if this Groundwork has any ancestors
	*
	*	@param	child	check for ancestors of this Groundwork
	*
	*	@return	true if this Groundwork has any ancestors
	*/
	boolean hasAncestor(IGroundwork child) throws GNDBException
	{
		return StoryLineIndexUtils.hasAncestor(NotebookEntryType.GROUNDWORK, child.getKey());
	}

	/**
	*	get the ancestors of this Groundwork
	*
	*	@param	child	get ancestors of this Groundwork
	*
	*	@return	the links for the ancestors of this Groundwork
	*/
	StoryLineTree<? extends INotebookEntry> getAncestors(IGroundwork child) throws GNDBException
	{
		return fetchAncestors(NotebookEntryType.GROUNDWORK, child.getKey());
	}

	/**
	*	check if this Groundwork has any descendants
	*
	*	@param	parent	check for descendants of this Groundwork
	*
	*	@return	true if this Groundwork has any descendants
	*/
	boolean hasDescendant(IGroundwork parent) throws GNDBException
	{
		return StoryLineIndexUtils.hasDescendant(NotebookEntryType.GROUNDWORK, parent.getKey());
	}

	/**
	*	get the descendants of this Groundwork
	*
	*	@param	parent	get descendants of this Groundwork
	*
	*	@return	the links for the descendants of this Groundwork
	*/
	StoryLineTree<? extends INotebookEntry> getDescendants(IGroundwork parent) throws GNDBException
	{
		return fetchDescendants(NotebookEntryType.GROUNDWORK, parent.getKey());
	}

	/**
	*	check if this AfflictionEvent has any ancestors
	*
	*	@param	child	check for ancestors of this AfflictionEvent
	*
	*	@return	true if this AfflictionEvent has any ancestors
	*/
	boolean hasAncestor(IAfflictionEvent child) throws GNDBException
	{
		return StoryLineIndexUtils.hasAncestor(NotebookEntryType.AFFLICTIONEVENT, child.getKey());
	}

	/**
	*	get the ancestors of this AfflictionEvent
	*
	*	@param	child	get ancestors of this AfflictionEvent
	*
	*	@return	the links for the ancestors of this AfflictionEvent
	*/
	StoryLineTree<? extends INotebookEntry> getAncestors(IAfflictionEvent child) throws GNDBException
	{
		return fetchAncestors(NotebookEntryType.AFFLICTIONEVENT, child.getKey());
	}

	/**
	*	check if this AfflictionEvent has any descendants
	*
	*	@param	parent	check for descendants of this AfflictionEvent
	*
	*	@return	true if this AfflictionEvent has any descendants
	*/
	boolean hasDescendant(IAfflictionEvent parent) throws GNDBException
	{
		return StoryLineIndexUtils.hasDescendant(NotebookEntryType.AFFLICTIONEVENT, parent.getKey());
	}

	/**
	*	get the descendants of this AfflictionEvent
	*
	*	@param	parent	get descendants of this AfflictionEvent
	*
	*	@return	the links for the descendants of this AfflictionEvent
	*/
	StoryLineTree<? extends INotebookEntry> getDescendants(IAfflictionEvent parent) throws GNDBException
	{
		return fetchDescendants(NotebookEntryType.AFFLICTIONEVENT, parent.getKey());
	}

	/**
	*	check if this Husbandry has any ancestors
	*
	*	@param	child	check for ancestors of this Husbandry
	*
	*	@return	true if this Husbandry has any ancestors
	*/
	boolean hasAncestor(IHusbandry child) throws GNDBException
	{
		return StoryLineIndexUtils.hasAncestor(NotebookEntryType.HUSBANDRY, child.getKey());
	}

	/**
	*	get the ancestors of this Husbandry
	*
	*	@param	child	get ancestors of this Husbandry
	*
	*	@return	the links for the ancestors of this Husbandry
	*/
	StoryLineTree<? extends INotebookEntry> getAncestors(IHusbandry child) throws GNDBException
	{
		return fetchAncestors(NotebookEntryType.HUSBANDRY, child.getKey());
	}

	/**
	*	check if this Husbandry has any descendants
	*
	*	@param	parent	check for descendants of this Husbandry
	*
	*	@return	true if this Husbandry has any descendants
	*/
	boolean hasDescendant(IHusbandry parent) throws GNDBException
	{
		return StoryLineIndexUtils.hasDescendant(NotebookEntryType.HUSBANDRY, parent.getKey());
	}

	/**
	*	get the descendants of this Husbandry
	*
	*	@param	parent	get descendants of this Husbandry
	*
	*	@return	the links for the descendants of this Husbandry
	*/
	StoryLineTree<? extends INotebookEntry> getDescendants(IHusbandry parent) throws GNDBException
	{
		return fetchDescendants(NotebookEntryType.HUSBANDRY, parent.getKey());
	}

	/**
	*	check if this SaleItem has any ancestors
	*
	*	@param	child	check for ancestors of this SaleItem
	*
	*	@return	true if this SaleItem has any ancestors
	*/
	boolean hasAncestor(ISaleItem child) throws GNDBException
	{
		return StoryLineIndexUtils.hasAncestor(NotebookEntryType.SALEITEM, child.getKey());
	}

	/**
	*	get the ancestors of this SaleItem
	*
	*	@param	child	get ancestors of this SaleItem
	*
	*	@return	the links for the ancestors of this SaleItem
	*/
	StoryLineTree<? extends INotebookEntry> getAncestors(ISaleItem child) throws GNDBException
	{
		return fetchAncestors(NotebookEntryType.SALEITEM, child.getKey());
	}

//	/**
//	SaleItems do not have descendants
//	*	check if this SaleItem has any descendants
//	*
//	*	@param	parent	check for descendants of this SaleItem
//	*
//	*	@return	true if this SaleItem has any descendants
//	*/
//	boolean hasDescendant(ISaleItem parent) throws GNDBException
//	{
//		return StoryLineIndexUtils.hasDescendant(NotebookEntryType.SALEITEM, parent.getKey());
//	}

//	/**
//	*	Get the descendants of this SaleItem.
//	SaleItems do not have descendants
//    *   The SQL is more complicated than just getting a list of descendants (select * ... where ancestorid=? and ancestortype=? )
//    *   as we need to preserve the tree structure for display later.
//	*
//	*	@param	parent	get descendants of this SaleItem
//	*
//	*	@return	the links for the descendants of this SaleItem
//	*/
//	StoryLineTree<? extends INotebookEntry> getDescendants(ISaleItem parent) throws GNDBException
//	{
//		return fetchDescendants(NotebookEntryType.SALEITEM, parent.getKey());
//	}

	private StoryLineTree<? extends INotebookEntry> fetchDescendants(NotebookEntryType ancestorType, int ancestorId) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchDescendants({}, {})", ancestorType, ancestorId);

		List<StoryLineIndex> vals = new ArrayList<>();

		StringBuilder query = new StringBuilder("select c1.depth as depth, ");
		query.append("c2.ancestorId as ancestorId, c2.ancestorType as ancestorType, ").
				append("c2.descendantId as descendantId, c2.descendantType as descendantType, ").
				append("c1.created as created ").
				append("from storylineindex as c1 ").
				append("join storylineindex as c2 on c1.descendantId = c2.descendantId and c1.descendantType = c2.descendantType ").
				append("where c1.ancestorId = ").append(ancestorId).append(" and ").
				append("c1.ancestorType = '").append(ancestorType.type()).append("' and ").
				append("c1.depth > 0 and c2.depth=1 ").
				append("order by depth, c1.descendantType, c1.descendantId");
LOGGER.debug("fetchDescendants(): query: {}", query);
		try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement();)
		{
			ResultSet rs = stmt.executeQuery(query.toString());
			vals = processResults(rs);
			stmt.close();
		}catch (SQLException ex) {
			LOGGER.error("fetchDescendants(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
		
        // bug 190435-1, cache miss
		Map<NotebookEntryType, Set<Integer>> keys = new HashMap<>();
		
		for (StoryLineIndex sli : vals)
		{// there must be a streams way of doing this...
			keys.computeIfAbsent(sli.getAncestorType(), k -> new HashSet<>()).add(sli.ancestorId());
			keys.computeIfAbsent(sli.getDescendantType(), k -> new HashSet<>()).add(sli.descendantId());
		}
		populateCache(keys);

		return translateDescendantTree(vals);
	}

	private StoryLineTree<? extends INotebookEntry> fetchAncestors(NotebookEntryType descendantType, int descendantId) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fetchAncestors({}, {})", descendantType, descendantId);

		List<StoryLineIndex> vals = new ArrayList<>();

		StringBuilder query = new StringBuilder("select c1.depth as depth, c1.ancestorId as ancestorId, c1.ancestorType as ancestorType, ");
		query.append(descendantId).append(" as descendantId, '").append(descendantType.type()).append("' as descendantType, ").
				append("c1.created as created ").
				append("from storylineindex as c1 ").
				append("where c1.descendantId = ").append(descendantId).append(" and ").
				append("c1.descendantType = '").append(descendantType.type()).append("'").
				append("order by depth desc");
LOGGER.debug("fetchAncestors(): query: {}", query);
		try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement();)
		{
			ResultSet rs = stmt.executeQuery(query.toString());
			vals = processResults(rs);
			stmt.close();
		}catch (SQLException ex) {
			LOGGER.error("fetchAncestors(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
		Map<NotebookEntryType, Set<Integer>> keys = new HashMap<>();
		
		for (StoryLineIndex sli : vals)
		{// there must be a streams way of doing this...
			keys.computeIfAbsent(sli.getAncestorType(), k -> new HashSet<>()).add(sli.ancestorId());
			keys.computeIfAbsent(sli.getDescendantType(), k -> new HashSet<>()).add(sli.descendantId());
		}
		populateCache(keys);

		return translateAncestorTree(vals);
	}	//	fetchAncestors()

	private List<StoryLineIndex> processResults(ResultSet rs) throws SQLException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("processResults()");

		List<StoryLineIndex> vals = new ArrayList<>();

		StoryLineIndex item = null;

		while (rs.next())
		{
			int ancestorId = rs.getInt("ancestorId");
			String ancestorType = rs.getString("ancestorType");
			int descendantId = rs.getInt("descendantId");
			String descendantType = rs.getString("descendantType");
			int depth = rs.getInt("depth");
			LocalDateTime created = rs.getTimestamp("created").toLocalDateTime();
LOGGER.debug("ancestorId: {}, ancestorType: {}, descendantId: {}, descendantType: {}, depth: {}, created: {}", ancestorId, ancestorType, descendantId, descendantType, depth, created);

			item = new StoryLineIndex(0, ancestorId, ancestorType, descendantId, descendantType, depth, created);
			vals.add(item);

		}
		LOGGER.traceExit(log4jEntryMsg);
		return vals;
	}	//	processResults()

	private void populateCache(Map<NotebookEntryType, Set<Integer>> keys) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("populateCache()");

		for(NotebookEntryType key : keys.keySet())
		{
			int[] ids = keys.get(key).stream().mapToInt(Integer::intValue).toArray();
			LOGGER.debug("populateCache(): key:{}, ids:{}", key.type(), ids);
			switch (key)
			{
				case PURCHASEITEM -> new PurchaseItemLister().id(ids).fetch();
				case GROUNDWORK -> new GroundworkLister().id(ids).fetch();
				case AFFLICTIONEVENT -> new AfflictionEventLister().id(ids).fetch();
				case HUSBANDRY -> new HusbandryLister().id(ids).fetch();
				case SALEITEM -> new SaleItemLister().id(ids).fetch();
				default -> { }
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	populateCache()

	/**
	*	Ancestor tree is assumed to be A->B->C, single item at each level
	*/
	private StoryLineTree<? extends INotebookEntry> translateAncestorTree(List<StoryLineIndex> vals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("translateAncestorTree()");

		if (vals == null || vals.isEmpty())
		{
			LOGGER.debug("translateAncestorTree(): empty list");
			return StoryLineTree.emptyTree();
		}
		StoryLineTree<INotebookEntry> tree = null;
		//	the level values as retrieved show how many generations back this ancestor is, so need to invert them
		int maxLevel = 0;
		//depth values are 'inverted' (remotest ancestor is most remote so has greatest depth value)
		for (StoryLineIndex item : vals)
		{
			if (item.depth() > maxLevel) maxLevel = item.depth();
		}
		StoryLineTree<INotebookEntry> curr = null;
		for (StoryLineIndex item : vals)
		{
			LOGGER.debug("translateAncestorTree(): item: {}", item);
			INotebookEntry article = translateIndex(item.getAncestorType(), item.ancestorId());
			LOGGER.debug("translateAncestorTree(): article: {}", article);
			if (tree == null)
			{
				tree = new StoryLineTree<>(article,0);
				curr = tree;
			}
			else
			{
				curr = curr.addLeaf(article);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
		return tree;
	}	//	translateAncestorTree()

	/**
	*	Descendant tree is assumed to be multiply branched with multiple items at each level
	*
	*	For a tree O 
	*	           |-> A
	*	           |   |-> A1
	*	           |   |    |-> A12
	*	           |   |-> A2
	*	           |-> B
	*	           |   |-> B1
	*	           |-> C
	*	               |-> C1
	*	i.e. O has immediate daughters A, B, C; A has immediate daughters A1 and A2; A1 has daughter A12; B has daughter B1; etc
	*	The incoming list is (O,A,level=1) (O,B,1) (O,C,1) (A,A1,2) (A,A2,2) (B,B1,2) (C,C1,2) (A1,A12,3)
	*	The sort order is by depth first then by type then by id.  The order should be by date of child, but that's not available
	*
	*/
	private StoryLineTree<? extends INotebookEntry> translateDescendantTree(List<StoryLineIndex> vals)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("translateDescendantTree()");

		if (vals == null || vals.isEmpty())
		{
			LOGGER.debug("translateAncestorTree(): empty list");
			return StoryLineTree.emptyTree();
		}
		INotebookEntry article = translateIndex(vals.get(0).getAncestorType(), vals.get(0).ancestorId());
		StoryLineTree<INotebookEntry> tree = new StoryLineTree<>(article);
		walkDescendantList(tree, vals.get(0).getAncestorType(), vals.get(0).ancestorId(), vals, 1);
		LOGGER.traceExit(log4jEntryMsg);
		return tree;
	}	//	translateDescendantTree()

	private INotebookEntry translateIndex(NotebookEntryType type, int id)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("translateIndex(): type: {}, id: {}", type, id);

		INotebookEntry article = null;
		switch (type)
		{
			case PURCHASEITEM -> article = MySQLCache.cachePurchaseItem.get(id);
			case GROUNDWORK -> article = MySQLCache.cacheGroundwork.get(id);
			case AFFLICTIONEVENT -> article = MySQLCache.cacheAfflictionEvent.get(id);
			case HUSBANDRY -> article = MySQLCache.cacheHusbandry.get(id);
			case SALEITEM -> article = MySQLCache.cacheSaleItem.get(id);
			default -> {	}
		}
		return LOGGER.traceExit(log4jEntryMsg, article);
	}	//	translateIndex()

	private void walkDescendantList(StoryLineTree<INotebookEntry> tree, NotebookEntryType ancestorType, int ancestorId, List<StoryLineIndex> vals, int level)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("walkDescendantList(): ancestorType: {}, ancestorId: {}, level: {}", ancestorType, ancestorId, level);

		for (StoryLineIndex item : vals)
		{
			if (item.depth() == level && item.getAncestorType() == ancestorType && item.ancestorId() == ancestorId)
			{
				StoryLineTree<INotebookEntry> currNode = tree.addLeaf(translateIndex(item.getDescendantType(), item.descendantId()));
				walkDescendantList(currNode, item.getDescendantType(), item.descendantId(), vals, level+1);
			}
		}
		LOGGER.traceExit(log4jEntryMsg);
	}	//	walkDescendantList()

	void toJson(JsonBuilderFactory builderFactory, JsonWriterFactory writerFactory, File dumpDirectory) throws GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("toJson()");

		JsonArrayBuilder jsonHc = builderFactory.createArrayBuilder();
		try (Connection conn = DBConnection.getConnection(); Statement stmt = conn.createStatement();)
		{
			ResultSet rs = stmt.executeQuery("select * from storylineindex");
			while (rs.next())
			{
				int id = rs.getInt("storylineindexid");
				LOGGER.debug("id: {}", id);
				int ancestorId = rs.getInt("ancestorId");
				LOGGER.debug("ancestorId: {}", ancestorId);
				String ancestorType = rs.getString("ancestorType");
				LOGGER.debug("ancestorType: {}", ancestorType);
				int descendantId = rs.getInt("descendantId");
				LOGGER.debug("descendantId: {}", descendantId);
				String descendantType = rs.getString("descendantType");
				LOGGER.debug("descendantType: {}", descendantType);
				int depth = rs.getInt("depth");
				LOGGER.debug("depth: {}", depth);
				LocalDateTime created = rs.getTimestamp("created").toLocalDateTime();
				LOGGER.debug("created: {}", created);
				
				JsonObjectBuilder jsonBuilder = builderFactory.createObjectBuilder();
                jsonBuilder.add("JsonMode", "DUMP");
                jsonBuilder.add("JsonNBClass", "StoryLineIndex");
                
				jsonBuilder.add("id", id);
				jsonBuilder.add("ancestorId", ancestorId);
				jsonBuilder.add("ancestorType", ancestorType);
				jsonBuilder.add("descendantId", descendantId);
				jsonBuilder.add("descendantType", descendantType);
				jsonBuilder.add("depth", depth);
				jsonBuilder.add("created", created.toString());
				
				jsonHc.add(jsonBuilder);
			}
			stmt.close();
		}catch (SQLException ex) {
			LOGGER.error("toJson(): SQLException: errorCode: {}, SQLstate: {}, message: {}", ex.getErrorCode(), ex.getSQLState(), ex.getMessage());
			throw new GNDBException(ex, ex.getErrorCode(), ex.getSQLState());
		}
		        
        JsonObjectBuilder job = builderFactory.createObjectBuilder();
        job.add("JsonMode", "DUMP");
        job.add("JsonNBClass", "StoryLineIndex");
        job.add("values", jsonHc);
        
		try (JsonWriter writer = writerFactory.createWriter(new FileWriter(new File(dumpDirectory, "StoryLineIndex.json"), false)))
		{
			writer.writeObject(job.build());
		} catch (IOException ex) {
			LOGGER.error("toJson(): IOException", ex);
		}
	}	// toJson

}
