/* * 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; } } }