/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package org.springframework.geode.data.json;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionService;
import org.apache.geode.cache.Scope;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.JSONFormatterException;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.PdxInstanceFactory;
import org.apache.geode.pdx.PdxSerializationException;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.gemfire.LocalRegionFactoryBean;
import org.springframework.data.gemfire.config.annotation.EnablePdx;
import org.springframework.data.gemfire.config.annotation.PeerCacheApplication;
import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.geode.pdx.PdxInstanceBuilder;
import org.springframework.lang.NonNull;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import example.app.crm.model.Customer;
import example.app.pos.model.LineItem;
import example.app.pos.model.Product;
import example.app.pos.model.PurchaseOrder;

/**
 * Integration Test for {@link JsonCacheDataImporterExporter}.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see com.fasterxml.jackson.databind.ObjectMapper
 * @see org.apache.geode.cache.Cache
 * @see org.apache.geode.cache.Region
 * @see org.apache.geode.cache.RegionService
 * @see org.apache.geode.pdx.JSONFormatter
 * @see org.apache.geode.pdx.PdxInstance
 * @see org.apache.geode.pdx.PdxInstanceFactory
 * @see org.springframework.context.ConfigurableApplicationContext
 * @see org.springframework.context.annotation.AnnotationConfigApplicationContext
 * @see org.springframework.context.annotation.Bean
 * @see org.springframework.core.io.ClassPathResource
 * @see org.springframework.core.io.Resource
 * @see org.springframework.data.gemfire.LocalRegionFactoryBean
 * @see org.springframework.data.gemfire.config.annotation.EnablePdx
 * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication
 * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport
 * @see org.springframework.geode.pdx.PdxInstanceBuilder
 * @since 1.3.0
 */
@SuppressWarnings("unused")
public class JsonCacheDataImporterExporterIntegrationTests extends IntegrationTestsSupport {

	private static final String EXPORT_ENABLED_PROPERTY = "spring.boot.data.gemfire.cache.data.export.enabled";

	private static volatile Supplier<Resource> resourceSupplier;

	private static volatile Supplier<StringWriter> writerSupplier;

	private ConfigurableApplicationContext applicationContext;

	@Before
	public void initializeSuppliers() {
		resourceSupplier = () -> null;
		writerSupplier = StringWriter::new;
	}

	@After
	public void closeApplicationContext() {

		Optional.ofNullable(this.applicationContext)
			.ifPresent(ConfigurableApplicationContext::close);

		this.applicationContext = null;
	}

	private ConfigurableApplicationContext newApplicationContext(Class<?>... componentClasses) {

		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();

		applicationContext.register(componentClasses);
		applicationContext.registerShutdownHook();
		applicationContext.refresh();

		this.applicationContext = applicationContext;

		return applicationContext;
	}

	private Class<?>[] withResource(String path) {

		if (StringUtils.hasText(path)) {
			resourceSupplier = () -> new ClassPathResource(path);
		}

		return new Class[] { TestGeodeConfiguration.class };
	}

	private <K, V> Region<K, V> assertExampleRegion(Region<K, V> example) {

		assertThat(example).isNotNull();
		assertThat(example.getName()).isEqualTo("Example");
		assertThat(example.getAttributes()).isNotNull();
		assertThat(example.getAttributes().getDataPolicy()).isEqualTo(DataPolicy.NORMAL);
		assertThat(example.getAttributes().getScope()).isEqualTo(Scope.LOCAL);

		return example;
	}

	@SuppressWarnings("unchecked")
	private <K, V> Region<K, V> getExampleRegion(ConfigurableApplicationContext applicationContext) {
		return assertExampleRegion(applicationContext.getBean("Example", Region.class));
	}

	@Test
	public void exampleRegionContainsJonDoe() {

		Region<?, ?> example = getExampleRegion(newApplicationContext(withResource("data-example-jondoe.json")));

		assertThat(example).hasSize(1);

		Object value = example.values().stream()
			.findFirst()
			.orElse(null);

		assertThat(value).isInstanceOf(PdxInstance.class);

		PdxInstance pdxInstance = (PdxInstance) value;

		assertThat(pdxInstance.getField("id")).isEqualTo((byte) 1);
		assertThat(pdxInstance.getField("name")).isEqualTo("Jon Doe");
		assertThat(pdxInstance.getField("@type")).isEqualTo(Customer.class.getName());

		Customer jonDoe = (Customer) pdxInstance.getObject();

		assertThat(jonDoe).isNotNull();
		assertThat(jonDoe.getId()).isEqualTo(1);
		assertThat(jonDoe.getName()).isEqualTo("Jon Doe");
	}

	@Test
	public void exampleRegionContainsDoeFamily() {

		Region<?, ?> example =
			getExampleRegion(newApplicationContext(withResource("data-example-doefamily.json")));

		assertThat(example).hasSize(9);

		Set<Customer> customers = example.values().stream()
			.filter(PdxInstance.class::isInstance)
			.map(PdxInstance.class::cast)
			.map(PdxInstance::getObject)
			.filter(Customer.class::isInstance)
			.map(Customer.class::cast)
			.collect(Collectors.toSet());

		assertThat(customers).hasSize(example.size());

		assertThat(customers).containsExactlyInAnyOrder(
			Customer.newCustomer(1L, "Jon Doe"),
			Customer.newCustomer(2L, "Jane Doe"),
			Customer.newCustomer(3L, "Cookie Doe"),
			Customer.newCustomer(4L, "Fro Doe"),
			Customer.newCustomer(5L, "Ginger Doe"),
			Customer.newCustomer(6L, "Hoe Doe"),
			Customer.newCustomer(7L, "Joe Doe"),
			Customer.newCustomer(8L, "Pie Doe"),
			Customer.newCustomer(9L, "Sour Doe")
		);
	}

	private void assertPurchaseOrder(Object purchaseOrder, int expectedNumberOfLineItems) {

		assertThat(purchaseOrder).isInstanceOf(PdxInstance.class);
		assertThat(((PdxInstance) purchaseOrder).hasField("@type"));
		assertThat(((PdxInstance) purchaseOrder).hasField("id"));
		assertThat(((PdxInstance) purchaseOrder).hasField("lineItems"));
		assertThat(((PdxInstance) purchaseOrder).getField("@type")).isEqualTo(PurchaseOrder.class.getName());

		Object lineItems = ((PdxInstance) purchaseOrder).getField("lineItems");

		assertThat(lineItems).isInstanceOf(Collection.class);
		assertThat((Collection<?>) lineItems).hasSize(expectedNumberOfLineItems);

		((Collection<?>) lineItems).forEach(this::assertLineItem);
	}

	private void assertLineItem(Object lineItem) {

		assertThat(lineItem).isInstanceOf(PdxInstance.class);
		assertThat(((PdxInstance) lineItem).hasField("@type")).isTrue();
		assertThat(((PdxInstance) lineItem).hasField("product")).isTrue();
		assertThat(((PdxInstance) lineItem).hasField("quantity")).isTrue();
		assertThat(((PdxInstance) lineItem).getField("@type")).isEqualTo(LineItem.class.getName());
		assertProduct(((PdxInstance) lineItem).getField("product"));
	}

	private void assertProduct(Object product) {

		assertThat(product).isInstanceOf(PdxInstance.class);
		assertThat(((PdxInstance) product).hasField("@type")).isTrue();
		assertThat(((PdxInstance) product).hasField("name")).isTrue();
		assertThat(((PdxInstance) product).hasField("price")).isTrue();
		assertThat(((PdxInstance) product).hasField("category")).isTrue();
		assertThat(((PdxInstance) product).getField("@type")).isEqualTo(Product.class.getName());
	}

	private void assertPurchaseOrder(PurchaseOrder purchaseOrder, Long expectedId, List<LineItem> expectedLineItems,
			BigDecimal expectedTotal) {

		assertThat(purchaseOrder).isNotNull();
		assertThat(purchaseOrder).hasSize(expectedLineItems.size());
		assertThat(purchaseOrder.getId()).isEqualTo(expectedId);
		assertThat(purchaseOrder.getTotal()).isEqualTo(expectedTotal);

		expectedLineItems.forEach(lineItem ->
			assertLineItem(purchaseOrder.findBy(lineItem.getProduct().getName()).orElse(null), lineItem));
	}

	private void assertLineItem(LineItem actual, LineItem expected) {

		assertThat(actual).isNotNull();
		assertThat(actual.getQuantity()).isEqualTo(expected.getQuantity());
		assertProduct(actual.getProduct(), expected.getProduct());
	}

	private void assertProduct(Product actual, Product expected) {

		assertThat(actual).isNotNull();
		assertThat(actual.getCategory()).isEqualTo(expected.getCategory());
		assertThat(actual.getName()).isEqualTo(expected.getName());
		assertThat(actual.getPrice()).isEqualTo(expected.getPrice());
	}

	private void log(String message, Object... args) {
		System.err.printf(message, args);
		System.err.flush();
	}

	private ObjectMapper newObjectMapper() {

		return new ObjectMapper()
			.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
			.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
	}

	private PdxInstance serializeToPdx(RegionService regionService, Object value) {

		return PdxInstanceBuilder.create(regionService)
			.from(value)
			.create();
	}

	private PdxInstance serializeToPdx(RegionService regionService, Customer customer) {

		PdxInstanceFactory pdxFactory = regionService.createPdxInstanceFactory(customer.getClass().getName());

		//pdxFactory.writeString("@type", customer.getClass().getName());
		pdxFactory.writeLong("id", customer.getId());
		pdxFactory.writeString("name", customer.getName());
		pdxFactory.markIdentityField("id");

		return pdxFactory.create();
	}

	@Test
	public void exampleRegionContainsComplexPurchaseOrderType() {

		Region<?, ?> example =
			getExampleRegion(newApplicationContext(withResource("data-example-purchaseorder.json")));

		assertThat(example).hasSize(1);

		Object value = example.values().stream()
			.findFirst()
			.orElse(null);

		assertPurchaseOrder(value, 3);

		List<LineItem> expectedLineItems = Arrays.asList(
			LineItem.newLineItem(Product.newProduct("Apple iPad").havingPrice(BigDecimal.valueOf(1499.0d)).in(Product.Category.SHOPPING)).withQuantity(2), // 2998.00
			LineItem.newLineItem(Product.newProduct("Apple iPhone").havingPrice(BigDecimal.valueOf(1249.0d)).in(Product.Category.SHOPPING)).withQuantity(3), // 3747.00
			LineItem.newLineItem(Product.newProduct("Apple iPod").havingPrice(BigDecimal.valueOf(599.0d)).in(Product.Category.SHOPPING)).withQuantity(1) // 599.00
		);

		PurchaseOrder purchaseOrder = (PurchaseOrder) ((PdxInstance) value).getObject();

		assertPurchaseOrder(purchaseOrder, 1L, expectedLineItems, BigDecimal.valueOf(7344.0d));
	}

	@Test
	public void exportFromExampleRegionToJson() {

		try {
			System.setProperty(EXPORT_ENABLED_PROPERTY, Boolean.TRUE.toString());

			StringWriter writer = new StringWriter();

			writerSupplier = () -> writer;

			Region<Long, Customer> example =
				getExampleRegion(newApplicationContext(withResource("data-example.json")));

			assertExampleRegion(example);
			assertThat(example).isEmpty();

			Customer playDoe = Customer.newCustomer(42L, "Play Doe");

			example.put(playDoe.getId(), playDoe);

			assertThat(example).hasSize(1);
			assertThat(example.get(42L)).isEqualTo(playDoe);

			closeApplicationContext();

			String actualJson = StringUtils.trimAllWhitespace(writer.toString());

			String expectedJson = String.format("[{\"@type\":\"%s\",\"id\":42,\"name\":\"PlayDoe\"}]",
				playDoe.getClass().getName());

			assertThat(actualJson).isEqualTo(expectedJson);
		}
		finally {
			System.clearProperty(EXPORT_ENABLED_PROPERTY);
		}
	}

	@Test
	public void exportFromExampleRegionImportsIntoExampleRegion() throws IOException {

		try {
			// EXPORT
			System.setProperty(EXPORT_ENABLED_PROPERTY, Boolean.TRUE.toString());

			StringWriter writer = new StringWriter();

			writerSupplier = () -> writer;

			Region<Long, PurchaseOrder> example =
				getExampleRegion(newApplicationContext(withResource("data-example.json")));

			assertExampleRegion(example);
			assertThat(example).isEmpty();

			Product golfBalls = Product.newProduct("Titliest ProV1x Golf Balls")
				.havingPrice(BigDecimal.valueOf(34.99d))
				.in(Product.Category.SPECIALTY);

			LineItem lineItem = LineItem.newLineItem(golfBalls)
				.withQuantity(1);

			PurchaseOrder purchaseOrder = new PurchaseOrder()
				.identifiedAs(72L)
				.add(lineItem);

			assertThat(example.put(purchaseOrder.getId(), purchaseOrder)).isNull();
			assertThat(example).hasSize(1);
			assertPurchaseOrder(example.get(purchaseOrder.getId()), purchaseOrder.getId(),
				Collections.singletonList(lineItem), golfBalls.getPrice());

			//log("JSON from JSONFormatter '%s'%n",
			//	JSONFormatter.toJSON(serializeToPdx(example.getRegionService(), purchaseOrder)));

			closeApplicationContext();

			String json = writer.toString();

			assertThat(json).isNotEmpty();

			//log("JSON '%s'%n", json);
			//log("PurchaseOrder from JSON [%s]%n",
			//	newObjectMapper().readValue(json.substring(1, json.length() - 1), PurchaseOrder.class));

			// IMPORT
			System.clearProperty(EXPORT_ENABLED_PROPERTY);

			Resource mockResource = mock(Resource.class, withSettings().lenient());

			doReturn(true).when(mockResource).exists();
			doReturn("MOCK").when(mockResource).getDescription();
			doReturn(new ByteArrayInputStream(json.getBytes())).when(mockResource).getInputStream();

			resourceSupplier = () -> mockResource;

			example = getExampleRegion(newApplicationContext(withResource(null)));

			assertExampleRegion(example);
			assertThat(example).hasSize(1);

			Object value = example.values().stream().findFirst().orElse(null);

			assertThat(value).isInstanceOf(PdxInstance.class);

			assertPurchaseOrder(ObjectUtils.asType(value, PurchaseOrder.class),
				purchaseOrder.getId(), Collections.singletonList(lineItem), golfBalls.getPrice());
		}
		finally {
			System.clearProperty(EXPORT_ENABLED_PROPERTY);
		}
	}

	@Test
	public void exportImportWithRegionContainingObjectsAndPdxInstances() throws IOException {

		try {
			// EXPORT
			System.setProperty(EXPORT_ENABLED_PROPERTY, Boolean.TRUE.toString());

			StringWriter writer = new StringWriter();

			writerSupplier = () -> writer;

			Region<Object, Object> example =
				getExampleRegion(newApplicationContext(withResource("data-example.json")));

			assertExampleRegion(example);
			assertThat(example).isEmpty();

			Customer jonDoe = Customer.newCustomer(1L, "Jon Doe");
			Customer janeDoe = Customer.newCustomer(2L, "JaneDoe");

			PdxInstance janeDoePdx = serializeToPdx(example.getRegionService(), janeDoe);

			assertThat(example.put(jonDoe.getId(), jonDoe)).isNull();
			assertThat(example.put(janeDoe.getId(), janeDoePdx)).isNull();
			assertThat(example).hasSize(2);
			assertThat(example.get(jonDoe.getId())).isEqualTo(jonDoe);
			assertThat(example.get(janeDoe.getId())).isInstanceOf(PdxInstance.class);

			closeApplicationContext();

			String json = writer.toString();

			assertThat(json).isNotEmpty();

			// IMPORT
			System.clearProperty(EXPORT_ENABLED_PROPERTY);

			Resource mockResource = mock(Resource.class, withSettings().lenient());

			doReturn(true).when(mockResource).exists();
			doReturn("MOCK").when(mockResource).getDescription();
			doReturn(new ByteArrayInputStream(json.getBytes())).when(mockResource).getInputStream();

			resourceSupplier = () -> mockResource;

			example = getExampleRegion(newApplicationContext(withResource(null)));

			assertExampleRegion(example);
			assertThat(example).hasSize(2);

			for (Customer doe : Arrays.asList(jonDoe, janeDoe)) {

				Object value = example.get(doe.getId().byteValue());

				assertThat(value).isInstanceOf(PdxInstance.class);
				assertThat(((PdxInstance) value).getObject()).isEqualTo(doe);
			}
		}
		finally {
			System.clearProperty(EXPORT_ENABLED_PROPERTY);
		}
	}

	// APACHE GEODE BUG 1!!!
	@Test(expected = JSONFormatterException.class)
	public void geodeJsonFormatterFromJsonCannotParseArrays() throws IOException {

		try {

			byte[] json = FileCopyUtils.copyToByteArray(
				new ClassPathResource("data-example-doefamily.json").getInputStream());

			assertThat(json).isNotNull();
			assertThat(json).isNotEmpty();

			JSONFormatter.fromJSON(json);
		}
		catch (JSONFormatterException expected) {

			// Caused because the JSONFormatter.fromJSON(..) method's JsonParser is not configured correctly!

			assertThat(expected).hasMessageStartingWith("Could not parse JSON document");
			assertThat(expected).hasCauseInstanceOf(IllegalStateException.class);
			assertThat(expected.getCause()).hasMessageStartingWith("Array start called when state is NONE");
			assertThat(expected.getCause()).hasNoCause();

			throw expected;
		}
	}

	// APACHE GEODE BUG 2!!!
	@Test
	public void geodeJsonFormatterToJsonDoesNotGenerateAtTypeJsonObjectPropertyFromPdxInstanceGetClassName() {

		Cache peerCache = newApplicationContext(TestGeodeConfiguration.class).getBean(Cache.class);

		assertThat(peerCache).isNotNull();

		Customer doeDoe = Customer.newCustomer(13L, "Doe Doe");

		PdxInstance pdx = serializeToPdx(peerCache, doeDoe);

		assertThat(pdx).isNotNull();
		assertThat(pdx.getClassName()).isEqualTo(doeDoe.getClass().getName());

		String json = JSONFormatter.toJSON(pdx);

		assertThat(json).isNotEmpty();
		assertThat(json).contains("\"name\":\"Doe Doe\"");

		PdxInstance pdxFromJson = JSONFormatter.fromJSON(json);

		assertThat(pdxFromJson).isNotNull();
		assertThat(pdxFromJson.getClassName()).isEqualTo(JSONFormatter.JSON_CLASSNAME);
		assertThat(pdxFromJson.hasField("@type")).isFalse();

		Object value = pdxFromJson.getObject();

		assertThat(value).isInstanceOf(PdxInstance.class);
		assertThat(value).isNotEqualTo(doeDoe);

		// Causes ClassCastException!
		// Bug caused by the JSONFormatter.toJSON(:PdxInstance) method not properly setting the '@type' JSON object
		// property from the PdxInstance.getClassName() when the class name is a valid Java class!
		//Customer jonDoeAgain = (Customer) value;
	}

	// APACHE GEODE BUG 3!!!
	@Test(expected = PdxSerializationException.class)
	public void geodePdxInstanceObjectMapperCannotDeserializeJava8Types() {

		try {

			Cache peerCache = newApplicationContext(TestGeodeConfiguration.class).getBean(Cache.class);

			ObjectMapper objectMapper = newObjectMapper();

			TimedType value = TimedType.create().with(LocalDate.now());

			ObjectNode objectNode = objectMapper.valueToTree(value);

			objectNode.put("@type", value.getClass().getName());

			String json = objectNode.toString();

			PdxInstance pdx = JSONFormatter.fromJSON(json);

			// BOOM!
			pdx.getObject();
		}
		catch (PdxSerializationException expected) {

			// Caused because the PdxInstance ObjectMapper is not properly configured (to findAndRegisterModules()
			// or Jackson Module Extensions on the classpath)!

			assertThat(expected).hasMessageStartingWith("Could not deserialize as java class '%s'",
				TimedType.class.getName());
			assertThat(expected.getCause()).isInstanceOf(InvalidDefinitionException.class);
			assertThat(expected.getCause()).hasMessageStartingWith("Cannot construct instance of `java.time.LocalDate`");
			assertThat(expected.getCause()).hasNoCause();

			throw expected;
		}
	}

	// APACHE GEODE BUG 4!!!
	@Test(expected = PdxSerializationException.class)
	public void geodePdxInstanceObjectMapperCannotDeserializeTypedJsonObjects()
			throws JsonProcessingException {

		try {

			Cache peerCache = newApplicationContext(TestGeodeConfiguration.class).getBean(Cache.class);

			PurchaseOrder purchaseOrder = new PurchaseOrder()
				.add(LineItem.newLineItem(Product.newProduct("Test Product")
					.havingPrice(BigDecimal.valueOf(39.99))
					.in(Product.Category.UNSOUGHT))
					.withQuantity(2))
				.identifiedAs(1L);

			ObjectMapper objectMapper = newObjectMapper()
				.activateDefaultTypingAsProperty(null, ObjectMapper.DefaultTyping.EVERYTHING, "@type");

			String json = objectMapper.writeValueAsString(purchaseOrder);

			assertThat(json).isNotEmpty();
			assertThat(json).describedAs("Actual JSON [%s]", json)
				.contains(String.format("\"@type\":\"%s\"", purchaseOrder.getClass().getName()));

			PdxInstance pdx = JSONFormatter.fromJSON(json);

			// BOOM!
			pdx.getObject();
		}
		catch (PdxSerializationException expected) {

			// Caused because the PdxInstance.getObject() method's ObjectMapper is not properly configured!

			assertThat(expected).hasMessageStartingWith("Could not deserialize as java class '%s'",
				PurchaseOrder.class.getName());
			assertThat(expected.getCause()).isInstanceOf(MismatchedInputException.class);
			assertThat(expected.getCause())
				.hasMessageStartingWith("Cannot deserialize instance of `java.lang.Long` out of START_ARRAY token");
			assertThat(expected.getCause()).hasNoCause();

			throw expected;
		}
	}

	@PeerCacheApplication
	@EnablePdx(readSerialized = true)
	static class TestGeodeConfiguration {

		@Bean("Example")
		LocalRegionFactoryBean<Object, Object> exampleRegion(Cache peerCache) {

			LocalRegionFactoryBean<Object, Object> exampleRegion = new LocalRegionFactoryBean<>();

			exampleRegion.setCache(peerCache);

			return exampleRegion;
		}

		@Bean
		JsonCacheDataImporterExporter exampleRegionDataImporter() {

			return new JsonCacheDataImporterExporter() {

				@Override @SuppressWarnings("rawtypes")
				protected Optional<Resource> getResource(@NonNull Region region, String resourcePrefix) {
					return Optional.ofNullable(resourceSupplier.get());
				}

				@NonNull @Override
				Writer newWriter(@NonNull Resource resource) {
					return writerSupplier.get();
				}
			};
		}
	}

	public static class TimedType {

		public static TimedType create() {
			return new TimedType();
		}

		private LocalDate time;

		public LocalDate getTime() {
			return this.time;
		}

		public TimedType with(LocalDate time) {
			this.time = time;
			return this;
		}
	}
}