/*
 * Copyright (C) 2018-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.1.0   fix dump/load directory bug
    2.8.1   Put dumps into time-stamped directories as an easy backup system.
            Delete old dumps.
	3.0.0	Add new tables
			Record most recently used dump directory in prefs
	3.0.1	For AfflictionEvent, use restoreJsonDump(List)
*   3.1.0   Use jakarta implementation of JSON
 */

package uk.co.gardennotebook.mysql;

import uk.co.gardennotebook.spi.GNDBException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.FileReader;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
//import java.util.logging.Level;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonWriterFactory;
import jakarta.json.JsonReader;
import jakarta.json.JsonReaderFactory;
import jakarta.json.stream.JsonGenerator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;

/**
*	Functions to generate a JSON dump of the database and to restore the database from a JSON dump.
*
*	@author	Andy Gegg
*	@version	3.1.0
*	@since	1.0
*/

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

	private JsonHandler()
	{
	}

/**
*	Dump all the database tables in their entirety to a set of JSON files.
*
 * @implNote
*   The directory naming needs a little explanation.  Basically it's a date stamp (YYYYMMDD) for each day
*   and then subdirectories which are a timestamp (HHMMSSdd dd being fractions of a second).  Given single
*   use on the PC (necessarily!) and elapsed time requirements, a hundredth of a second is unique enough.
*   However, these are both 8 digit strings and all dates up to 2060 will match a timestamp; the file tree walker
*   used in the tidy-up visits the start directory first so it's better to know that this is the date
*   directory, rather than rely on matching by accident.  Hence the initial D and T prefixes.
*   Anyway, a purely numeric directory name looks odd!
*
*   @param prefs    the JSON node of the preferences tree
* 
*	@since	1.0
*/
	static void toJson(Preferences prefs) throws IOException, GNDBException
	{
		final File topDumpDir = new File(prefs.get("dumpDir", "JSONDumpDir"));
        LOGGER.debug("JsonHandler: toJson(): dumpDir (top level): {}", topDumpDir.getAbsolutePath());
		if (!topDumpDir.isDirectory())
		{
			if (!topDumpDir.mkdirs())
			{
				throw new IOException("Cannot create JSON dump directory (top level).");
			}
		}

        File dumpDir = new File(topDumpDir, "D"+LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE));
        LOGGER.debug("JsonHandler: toJson(): dumpDir (date stamp): {}", dumpDir.getAbsolutePath());
        if (!dumpDir.isDirectory())
		{
			if (!dumpDir.mkdirs())
			{
				throw new IOException("Cannot create JSON dump directory (date stamp).");
			}
		}

        
        dumpDir = new File(dumpDir, "T"+LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmssSS")));
        LOGGER.debug("JsonHandler: toJson(): dumpDir (time stamp): {}", dumpDir.getAbsolutePath());
		if (!dumpDir.isDirectory())
		{
			if (!dumpDir.mkdirs())
			{
				throw new IOException("Cannot create JSON dump directory (time stamp).");
			}
		}
        LOGGER.debug("JsonHandler: toJson: dump directory: {}", dumpDir.getPath());

		JsonBuilderFactory builderFactory = null;
		try
		{
			builderFactory = Json.createBuilderFactory(null);
		} catch (Exception ex)
		{
			LOGGER.info("caught exception trying to createBuilderFactory: {}", ex);
			throw new IOException("Failed to createBuilderFactory");
		}
		LOGGER.debug("JsonHandler: toJson: builderFactory: {}", builderFactory);
		Map<String, Object> config = new HashMap<>();
		config.put(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE);
		JsonWriterFactory writerFactory = Json.createWriterFactory(config);
		LOGGER.debug("JsonHandler: toJson: about to write JSON");
        
		new ProductCategoryLister().toJson(builderFactory, writerFactory, dumpDir);
		new ShoppingListLister().toJson(builderFactory, writerFactory, dumpDir);
		new PurchaseItemLister().toJson(builderFactory, writerFactory, dumpDir);
		new WildlifeSpeciesLister().toJson(builderFactory, writerFactory, dumpDir);
		new GroundworkActivityLister().toJson(builderFactory, writerFactory, dumpDir);
		new GroundworkLister().toJson(builderFactory, writerFactory, dumpDir);
		new AfflictionClassLister().toJson(builderFactory, writerFactory, dumpDir);
		new WeatherLister().toJson(builderFactory, writerFactory, dumpDir);
		new StoryLineIndexLister().toJson(builderFactory, writerFactory, dumpDir);
		new RetailerHasProductLister().toJson(builderFactory, writerFactory, dumpDir);
		new ProductBrandLister().toJson(builderFactory, writerFactory, dumpDir);
		new HusbandryClassLister().toJson(builderFactory, writerFactory, dumpDir);
		new AfflictionLister().toJson(builderFactory, writerFactory, dumpDir);
		new WildlifeLister().toJson(builderFactory, writerFactory, dumpDir);
		new ProductLister().toJson(builderFactory, writerFactory, dumpDir);
		new ReminderLister().toJson(builderFactory, writerFactory, dumpDir);
		new RetailerLister().toJson(builderFactory, writerFactory, dumpDir);
		new PurchaseLister().toJson(builderFactory, writerFactory, dumpDir);
		new PlantVarietyLister().toJson(builderFactory, writerFactory, dumpDir);
		new AfflictionEventLister().toJson(builderFactory, writerFactory, dumpDir);
		new PlantNoteLister().toJson(builderFactory, writerFactory, dumpDir);
		new HusbandryLister().toJson(builderFactory, writerFactory, dumpDir);
		new ToDoListLister().toJson(builderFactory, writerFactory, dumpDir);
		new WeatherConditionLister().toJson(builderFactory, writerFactory, dumpDir);
		new PlantSpeciesLister().toJson(builderFactory, writerFactory, dumpDir);
		new SaleLister().toJson(builderFactory, writerFactory, dumpDir);
		new SaleItemLister().toJson(builderFactory, writerFactory, dumpDir);

		new LocationLister().toJson(builderFactory, writerFactory, dumpDir);

		new JournalLister().toJson(builderFactory, writerFactory, dumpDir);

		new ReviewLister().toJson(builderFactory, writerFactory, dumpDir);
//		new ReviewReferencesLister().toJson(builderFactory, writerFactory, dumpDir);

		new CropRotationGroupLister().toJson(builderFactory, writerFactory, dumpDir);

		new CroppingPlanLister().toJson(builderFactory, writerFactory, dumpDir);

		new CroppingActualLister().toJson(builderFactory, writerFactory, dumpDir);

		prefs.put("mostrecentdumpdir", dumpDir.getAbsolutePath());

        deleteOldDumps(prefs, topDumpDir);  //  2.8.1
	}	// toJson

    /*
    *   Deletes old dump directories.
    *   Names are checked to match the expected names of time-stamped directories
    *   and files are checked to be *.json.  Any exceptions will abort silently.
    *
    *   @param  prefs   the JSON node of the preferences tree
    *   @since  2.8.1
    */
    private static void deleteOldDumps(Preferences prefs, File dumpDir)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("deleteOldDumps(): dumpDir: {}", dumpDir);
        
        final String dateRegex = "^D20\\d\\d[01]\\d[0-3]\\d$";
        final String timeRegex = "^T[0-2]\\d[0-5]\\d[0-5]\\d\\d\\d$";
        
        int retainCount = prefs.getInt("retainDumps", 0);
        if (retainCount <= 0)
            return;
        
        List<String> toDrop = Collections.emptyList();
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dumpDir.toPath(),
                                                                        d -> d.toFile().isDirectory() && d.getFileName().toString().matches(dateRegex)))
        {
            List<String> names = new ArrayList<>();
            for (Path pt : stream)
            {
                names.add(pt.getFileName().toString());
            }
            toDrop = names.stream().sorted(Comparator.reverseOrder()).skip(retainCount).collect(Collectors.toList());
            LOGGER.debug("dirs to drop: {}", toDrop);
        } catch (IOException ex) {
            LOGGER.throwing(ex);
        }
        
        if (toDrop.isEmpty())
            return;
        
        for (String parentDir : toDrop)
        {
            File drop = new File(dumpDir, parentDir);
            LOGGER.info("dir to drop: {}", drop);
            if (!drop.isDirectory())
            {
                continue;
            }
            try {
                Files.walkFileTree(drop.toPath(),
									new SimpleFileVisitor<>()
						{
							@Override
							public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
							{
								LOGGER.debug("walkFileTree: preVisitDirectory: dir: {}", dir);
								//  spot the parent date-stamp directory
								if (dir.getFileName().toString().matches(dateRegex))
									return FileVisitResult.CONTINUE;
								if (!dir.getFileName().toString().matches(timeRegex))
								{
									LOGGER.info("Deleting old JSON dumps: failed due to unexpected directory: {}", dir);
									return FileVisitResult.TERMINATE;
								}
								return FileVisitResult.CONTINUE;
							}

							@Override
							public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
									throws IOException
							{
								LOGGER.debug("walkFileTree: visitFile: file: {}", file);
								if (!file.getFileName().toString().endsWith(".json"))
								{
									LOGGER.info("Deleting old JSON dumps: failed due to unexpected file: {}", file);
									return FileVisitResult.TERMINATE;
								}
								LOGGER.debug("walkFileTree: visitFile: deleting");
								Files.delete(file);
								return FileVisitResult.CONTINUE;
							}

							@Override
							public FileVisitResult postVisitDirectory(Path dir, IOException e)
									throws IOException
							{
								LOGGER.debug("walkFileTree: postVisitDirectory: dir: {}, ex: {}", dir, e);
								if (e == null)
								{
									LOGGER.info("Deleting old JSON dump directory: {}", dir);
									Files.delete(dir);
									return FileVisitResult.CONTINUE;
								} else
								{
									// directory iteration failed
									throw e;
								}
							}
						});
                } catch (IOException ex) {
                    LOGGER.throwing(ex);
            }
        }
    }

/**
*	Restore all the database tables in their entirety from a set of JSON files.
*@apiNote
*	The sequence of restoration is important - parent tables MUST be restored before children to avoid foreign key violations.
*
*    @since	1.1.0
*/
	static void fromJson(Preferences prefs) throws IOException, GNDBException
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("fromJson()");
        
		File loadDir = new File(prefs.get("loadDir", "JSONLoadDir"));
		if (!loadDir.isDirectory())
		{
			throw new IOException("JSON load directory not found");
		}
		JsonReaderFactory readerFactory = Json.createReaderFactory(null);

		File jsonFile;

        var totalTime = System.currentTimeMillis();

		jsonFile = new File(loadDir, "PlantSpecies.json");
		LOGGER.debug("PlantSpecies.json: exists: {}", jsonFile.exists());
		if (!jsonFile.exists())
		{
			throw new FileNotFoundException("PlantSpecies.json not found in JSON load directory");
		}

		jsonFile = new File(loadDir, "CropRotationGroup.json");
		//	first clear the cache to force d/b re-read on next list
		{new CropRotationGroupLister().clear();}
		CropRotationGroupBuilder myCropRotationGroup = new CropRotationGroupBuilder();
		LOGGER.debug("myCropRotationGroup: {}", myCropRotationGroup);
        var sTime = System.currentTimeMillis();
		myCropRotationGroup.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("CropRotationGroup fromJSON: {} ms", System.currentTimeMillis() - sTime);
//if (true)return;

		jsonFile = new File(loadDir, "PlantSpecies.json");
//		LOGGER.debug("PlantSpecies.json: exists: {}", jsonFile.exists());
//		if (!jsonFile.exists())
//		{
//			throw new FileNotFoundException("PlantSpecies.json not found in JSON load directory");
//		}
		//	first clear the cache to force d/b re-read on next list
		{new PlantSpeciesLister().clear();}
		PlantSpeciesBuilder myPlantSpecies = new PlantSpeciesBuilder();
		sTime = System.currentTimeMillis();
		myPlantSpecies.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("PlantSpecies fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "PlantVariety.json");
		//	first clear the cache to force d/b re-read on next list
		{new PlantVarietyLister().clear();}
		PlantVarietyBuilder myPlantVariety = new PlantVarietyBuilder();
        sTime = System.currentTimeMillis();
        myPlantVariety.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("PlantVariety fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "PlantNote.json");
		//	first clear the cache to force d/b re-read on next list
		{new PlantNoteLister().clear();}
		PlantNoteBuilder myPlantNote = new PlantNoteBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myPlantNote.doJsonInsert(new PlantNote(jo));
		}
        LOGGER.info("PlantNote fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "GroundworkActivity.json");
		//	first clear the cache to force d/b re-read on next list
		{new GroundworkActivityLister().clear();}
		GroundworkActivityBuilder myGroundworkActivity = new GroundworkActivityBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myGroundworkActivity.doJsonInsert(new GroundworkActivity(jo));
		}
        LOGGER.info("GroundworkActivity fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Location.json");
		//	first clear the cache to force d/b re-read on next list
		{new LocationLister().clear();}
		LocationBuilder myLocation = new LocationBuilder();
		sTime = System.currentTimeMillis();
		myLocation.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("Location fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Groundwork.json");
		//	first clear the cache to force d/b re-read on next list
		{new GroundworkLister().clear();}
		GroundworkBuilder myGroundwork = new GroundworkBuilder();
        sTime = System.currentTimeMillis();
        myGroundwork.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Groundwork fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "AfflictionClass.json");
		//	first clear the cache to force d/b re-read on next list
		{new AfflictionClassLister().clear();}
		AfflictionClassBuilder myAfflictionClass = new AfflictionClassBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myAfflictionClass.doJsonInsert(new AfflictionClass(jo));
		}
        LOGGER.info("AfflictionClass fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Affliction.json");
		//	first clear the cache to force d/b re-read on next list
		{new AfflictionLister().clear();}
		AfflictionBuilder myAffliction = new AfflictionBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myAffliction.doJsonInsert(new Affliction(jo));
		}
        LOGGER.info("Affliction fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "AfflictionEvent.json");
		//	first clear the cache to force d/b re-read on next list
		{new AfflictionEventLister().clear();}
		AfflictionEventBuilder myAfflictionEvent = new AfflictionEventBuilder();
		sTime = System.currentTimeMillis();
//		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
//		{
//			myAfflictionEvent.doJsonInsert(new AfflictionEvent(jo));
//		}
		myAfflictionEvent.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("AfflictionEvent fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "HusbandryClass.json");
		//	first clear the cache to force d/b re-read on next list
		{new HusbandryClassLister().clear();}
		HusbandryClassBuilder myHusbandryClass = new HusbandryClassBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myHusbandryClass.doJsonInsert(new HusbandryClass(jo));
		}
        LOGGER.info("HusbandryClass fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Husbandry.json");
		//	first clear the cache to force d/b re-read on next list
		{new HusbandryLister().clear();}
		HusbandryBuilder myHusbandry = new HusbandryBuilder();
        sTime = System.currentTimeMillis();
        myHusbandry.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Husbandry fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "WeatherCondition.json");
		//	first clear the cache to force d/b re-read on next list
		{new WeatherConditionLister().clear();}
		WeatherConditionBuilder myWeatherCondition = new WeatherConditionBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myWeatherCondition.doJsonInsert(new WeatherCondition(jo));
		}
        LOGGER.info("WeatherCondition fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Weather.json");
		//	first clear the cache to force d/b re-read on next list
		{new WeatherLister().clear();}
		WeatherBuilder myWeather = new WeatherBuilder();
        sTime = System.currentTimeMillis();
        myWeather.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Weather fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "ProductCategory.json");
		//	first clear the cache to force d/b re-read on next list
		{new ProductCategoryLister().clear();}
		ProductCategoryBuilder myProductCategory = new ProductCategoryBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myProductCategory.doJsonInsert(new ProductCategory(jo));
		}
        LOGGER.info("ProductCategory fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Retailer.json");
		//	first clear the cache to force d/b re-read on next list
		{new RetailerLister().clear();}
		RetailerBuilder myRetailer = new RetailerBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myRetailer.doJsonInsert(new Retailer(jo));
		}
        LOGGER.info("Retailer fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "ProductBrand.json");
		//	first clear the cache to force d/b re-read on next list
		{new ProductBrandLister().clear();}
		ProductBrandBuilder myProductBrand = new ProductBrandBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myProductBrand.doJsonInsert(new ProductBrand(jo));
		}
        LOGGER.info("ProductBrand fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Product.json");
		//	first clear the cache to force d/b re-read on next list
		{new ProductLister().clear();}
		ProductBuilder myProduct = new ProductBuilder();
        sTime = System.currentTimeMillis();
        myProduct.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Product fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "RetailerHasProduct.json");
		//	first clear the cache to force d/b re-read on next list
		{new RetailerHasProductLister().clear();}
		RetailerHasProductBuilder myRetailerHasProduct = new RetailerHasProductBuilder();
        sTime = System.currentTimeMillis();
        myRetailerHasProduct.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("RetailerHasProduct fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Purchase.json");
		//	first clear the cache to force d/b re-read on next list
		{new PurchaseLister().clear();}
		PurchaseBuilder myPurchase = new PurchaseBuilder();
        sTime = System.currentTimeMillis();
        myPurchase.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Purchase fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "PurchaseItem.json");
		//	first clear the cache to force d/b re-read on next list
		{new PurchaseItemLister().clear();}
		PurchaseItemBuilder myPurchaseItem = new PurchaseItemBuilder();
        sTime = System.currentTimeMillis();
        myPurchaseItem.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("PurchaseItem fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "ShoppingList.json");
		//	first clear the cache to force d/b re-read on next list
		{new ShoppingListLister().clear();}
		ShoppingListBuilder myShoppingList = new ShoppingListBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myShoppingList.doJsonInsert(new ShoppingList(jo));
		}
        LOGGER.info("ShoppingList fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Sale.json");
		//	first clear the cache to force d/b re-read on next list
		{new SaleLister().clear();}
		SaleBuilder mySale = new SaleBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			mySale.doJsonInsert(new Sale(jo));
		}
        LOGGER.info("Sale fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "SaleItem.json");
		//	first clear the cache to force d/b re-read on next list
		{new SaleItemLister().clear();}
		SaleItemBuilder mySaleItem = new SaleItemBuilder();
        sTime = System.currentTimeMillis();
        mySaleItem.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("SaleItem fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "ToDoList.json");
		//	first clear the cache to force d/b re-read on next list
		{new ToDoListLister().clear();}
		ToDoListBuilder myToDoList = new ToDoListBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myToDoList.doJsonInsert(new ToDoList(jo));
		}
        LOGGER.info("ToDoList fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Reminder.json");
		//	first clear the cache to force d/b re-read on next list
		{new ReminderLister().clear();}
		ReminderBuilder myReminder = new ReminderBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myReminder.doJsonInsert(new Reminder(jo));
		}
        LOGGER.info("Reminder fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "WildlifeSpecies.json");
		//	first clear the cache to force d/b re-read on next list
		{new WildlifeSpeciesLister().clear();}
		WildlifeSpeciesBuilder myWildlifeSpecies = new WildlifeSpeciesBuilder();
        sTime = System.currentTimeMillis();
		for (JsonObject jo : getJsonInserts(readerFactory, jsonFile))
		{
			myWildlifeSpecies.doJsonInsert(new WildlifeSpecies(jo));
		}
        LOGGER.info("WildlifeSpecies fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Wildlife.json");
		//	first clear the cache to force d/b re-read on next list
		{new WildlifeLister().clear();}
		WildlifeBuilder myWildlife = new WildlifeBuilder();
        sTime = System.currentTimeMillis();
        myWildlife.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("Wildlife fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Journal.json");
		//	first clear the cache to force d/b re-read on next list
		{new JournalLister().clear();}
		JournalBuilder myJournal = new JournalBuilder();
		sTime = System.currentTimeMillis();
		myJournal.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("Journal fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "Review.json");
		//	first clear the cache to force d/b re-read on next list
		{new ReviewLister().clear();}
		ReviewBuilder myReview = new ReviewBuilder();
		sTime = System.currentTimeMillis();
		myReview.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("Review fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "CroppingPlan.json");
		//	first clear the cache to force d/b re-read on next list
		{new CroppingPlanLister().clear();}
		CroppingPlanBuilder myCroppingPlan = new CroppingPlanBuilder();
		sTime = System.currentTimeMillis();
		myCroppingPlan.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("CroppingPlan fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "CroppingActual.json");
		//	first clear the cache to force d/b re-read on next list
		{new CroppingActualLister().clear();}
		CroppingActualBuilder myCroppingActual = new CroppingActualBuilder();
		sTime = System.currentTimeMillis();
		myCroppingActual.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
		LOGGER.info("CroppingActual fromJSON: {} ms", System.currentTimeMillis() - sTime);

		jsonFile = new File(loadDir, "StoryLineIndex.json");
		StoryLineIndexBuilder myStoryLineIndex = new StoryLineIndexBuilder();
        sTime = System.currentTimeMillis();
        myStoryLineIndex.restoreJsonDump(getJsonInserts(readerFactory, jsonFile));
        LOGGER.info("StoryLineIndex fromJSON: {} ms", System.currentTimeMillis() - sTime);

        LOGGER.info("Total time fromJSON: {} ms", System.currentTimeMillis() - totalTime);
//skip:
		LOGGER.traceExit();
	}	// fromJson

	private static List<JsonObject> getJsonInserts(JsonReaderFactory readerFactory, File source) throws FileNotFoundException
	{
		if (!source.canRead()) return Collections.emptyList();

		JsonReader fileReader = readerFactory.createReader(new FileReader(source));
		JsonStructure jstruct = fileReader.read();
		if (jstruct.getValueType() == JsonValue.ValueType.ARRAY)
		{
			return ((JsonArray)jstruct).getValuesAs(JsonObject.class);
		}
		else if (jstruct.getValueType() == JsonValue.ValueType.OBJECT)
		{
			JsonObject jtop = (JsonObject)jstruct;
			if ("DUMP".equals(jtop.getString("JsonMode", "DUMP")))
			{
				return jtop.getJsonArray("values").getValuesAs(JsonObject.class);
			}
		}
		return Collections.emptyList();

	}	// restoreJsonDump
}

