diff --git a/spring-geode/src/main/java/org/springframework/geode/data/AbstractCacheDataImporterExporter.java b/spring-geode/src/main/java/org/springframework/geode/data/AbstractCacheDataImporterExporter.java index c0087a0d..8d7ecd3e 100644 --- a/spring-geode/src/main/java/org/springframework/geode/data/AbstractCacheDataImporterExporter.java +++ b/spring-geode/src/main/java/org/springframework/geode/data/AbstractCacheDataImporterExporter.java @@ -116,7 +116,7 @@ public abstract class AbstractCacheDataImporterExporter * @see org.springframework.context.ApplicationContext * @see java.util.Optional */ - protected Optional getApplicationContext() { + public Optional getApplicationContext() { return Optional.ofNullable(this.applicationContext); } diff --git a/spring-geode/src/main/java/org/springframework/geode/data/json/JsonCacheDataImporterExporter.java b/spring-geode/src/main/java/org/springframework/geode/data/json/JsonCacheDataImporterExporter.java index 0c39feb0..940b2535 100644 --- a/spring-geode/src/main/java/org/springframework/geode/data/json/JsonCacheDataImporterExporter.java +++ b/spring-geode/src/main/java/org/springframework/geode/data/json/JsonCacheDataImporterExporter.java @@ -17,24 +17,34 @@ package org.springframework.geode.data.json; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; +import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.Writer; import java.util.Optional; import org.apache.geode.cache.Region; -import org.apache.geode.pdx.JSONFormatter; import org.apache.geode.pdx.PdxInstance; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.geode.data.AbstractCacheDataImporterExporter; import org.springframework.geode.data.CacheDataExporter; import org.springframework.geode.data.CacheDataImporter; +import org.springframework.geode.data.json.converter.JsonToPdxConverter; +import org.springframework.geode.data.json.converter.ObjectToJsonConverter; +import org.springframework.geode.data.json.support.JSONFormatterJsonToPdxConverter; +import org.springframework.geode.data.json.support.JSONFormatterPdxToJsonConverter; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * The {@link JsonCacheDataImporterExporter} class is a {@link CacheDataImporter} and {@link CacheDataExporter} @@ -42,10 +52,15 @@ import org.springframework.util.Assert; * * @author John Blum * @see org.apache.geode.cache.Region + * @see org.apache.geode.pdx.PdxInstance + * @see org.springframework.core.io.ClassPathResource * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader * @see org.springframework.geode.data.AbstractCacheDataImporterExporter * @see org.springframework.geode.data.CacheDataExporter * @see org.springframework.geode.data.CacheDataImporter + * @see org.springframework.geode.data.json.converter.JsonToPdxConverter + * @see org.springframework.geode.data.json.converter.ObjectToJsonConverter * @see org.springframework.stereotype.Component * @since 1.3.0 */ @@ -53,46 +68,154 @@ import org.springframework.util.Assert; @SuppressWarnings("rawtypes") public class JsonCacheDataImporterExporter extends AbstractCacheDataImporterExporter { + private static final int CONTENT_PREVIEW_LENGTH = 50; + private static final int DEFAULT_BUFFER_SIZE = 32768; + + protected static final String CLASSPATH_RESOURCE_PREFIX = ResourceLoader.CLASSPATH_URL_PREFIX; + protected static final String FILESYSTEM_RESOURCE_PREFIX = "file://"; + protected static final String ID_FIELD_NAME = "id"; protected static final String RESOURCE_NAME_PATTERN = "data-%s.json"; + private JsonToPdxConverter toPdxConverter = newJsonToPdxConverter(); + + private ObjectToJsonConverter toJsonConverter = newObjectToJsonConverter(); + + // TODO configure via an SPI + private @NonNull JsonToPdxConverter newJsonToPdxConverter() { + return new JSONFormatterJsonToPdxConverter(); + } + + // TODO configure via an SPI + private @NonNull ObjectToJsonConverter newObjectToJsonConverter() { + return new JSONFormatterPdxToJsonConverter(); + } + + /** + * Gets a reference to the configured {@link JsonToPdxConverter}. + * + * @return a reference to the configured {@link JsonToPdxConverter}. + * @see org.springframework.geode.data.json.converter.JsonToPdxConverter + */ + protected @NonNull JsonToPdxConverter getJsonToPdxConverter() { + return this.toPdxConverter; + } + + /** + * Gets a reference to the configured {@link ObjectToJsonConverter}. + * + * @return a reference to the configured {@link ObjectToJsonConverter}. + * @see org.springframework.geode.data.json.converter.ObjectToJsonConverter + */ + protected @NonNull ObjectToJsonConverter getObjectToJsonConverter() { + return this.toJsonConverter; + } + + /** + * @inheritDoc + */ @NonNull @Override public Region doExportFrom(@NonNull Region region) { Assert.notNull(region, "Region must not be null"); - String regionName = region.getName(); + String json = toJson(region); + + getLogger().debug("Saving JSON [{}] from Region [{}]", json, region.getName()); + + getResource(region, getResourceLocation()) + .ifPresent(resource -> save(json, resource)); return region; } + /** + * Saves the given {@link String content} (e.g. {@literal JSON}) to the specified {@link Resource}. + * + * @param content {@link String} containing the content to save. + * @param resource {@link Resource} to save the {@link String content} to; must not be {@literal null}. + * @throws IllegalArgumentException if {@link Resource} is {@literal null}. + * @see org.springframework.core.io.Resource + */ + protected void save(@Nullable String content, @NonNull Resource resource) { + + Assert.notNull(resource, "Resource must not be null"); + + if (StringUtils.hasText(content)) { + try (Writer writer = newWriter(resource)) { + writer.write(content, 0, content.length()); + writer.flush(); + } + catch (IOException cause) { + + String message = String.format("Failed to save content '%s' to Resource [%s]", + formatContentForPreview(content), resource.getDescription()); + + throw new DataAccessResourceFailureException(message, cause); + } + } + } + + @NonNull Writer newWriter(@NonNull Resource resource) throws IOException { + return new BufferedWriter(new FileWriter(resource.getFile(), false)); + } + + @Nullable String formatContentForPreview(@Nullable String content) { + + if (StringUtils.hasText(content)) { + + int length = Math.min(content.length(), CONTENT_PREVIEW_LENGTH); + + content = content.substring(0, length).concat(length < content.length() ? "..." : ""); + } + + return content; + } + + /** + * @inheritDoc + */ @NonNull @Override @SuppressWarnings("unchecked") public Region doImportInto(@NonNull Region region) { Assert.notNull(region, "Region must not be null"); - // TODO: handle a JSON array of objects and PdxInstances!!! - getResource(region) + getResource(region, CLASSPATH_RESOURCE_PREFIX) .filter(Resource::exists) .map(this::getContent) - .map(this::toPdxInstance) + .map(this::toPdx) .ifPresent(pdxInstance -> region.put(getIdentifier(pdxInstance), pdxInstance)); return region; } - private @NonNull byte[] getContent(@NonNull Resource resource) { + /** + * Gets the content of the given {@link Resource} as an array of {@literal Byte#TYPE bytes}. + * + * @param resource {@link Resource} from which to load the content; must not be {@literal null}. + * @return an array of {@link Byte#TYPE bytes} containing the contents of the given {@link Resource}. + * @throws IllegalArgumentException if {@link Resource} is {@literal null}. + * @throws DataAccessResourceFailureException if an {@link IOException} occurs while reading from + * and loading the contents of the given {@link Resource}. + * @see org.springframework.core.io.Resource + */ + protected @NonNull byte[] getContent(@NonNull Resource resource) { + + Assert.notNull(resource, "Resource must not be null"); try (InputStream in = resource.getInputStream()){ ByteArrayOutputStream out = new ByteArrayOutputStream(in.available()); - byte[] buffer = new byte[32768]; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; for (int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) { out.write(buffer, 0, bytesRead); out.flush(); } + out.flush(); + out.close(); + return out.toByteArray(); } catch (IOException cause) { @@ -101,27 +224,139 @@ public class JsonCacheDataImporterExporter extends AbstractCacheDataImporterExpo } } - private Optional getResource(@NonNull Region region) { + /** + * Determines the {@link Object identifier} for the given {@link PdxInstance}. + * + * @param pdxInstance {@link PdxInstance} to evaluate; must not be {@literal null}. + * @return the {@link Object identifier} for the given {@link PdxInstance}. + * @throws IllegalArgumentException if {@link PdxInstance} is {@literal null}. + * @throws IllegalStateException if the {@link Object identifier} for the {@link PdxInstance} + * cannot be determined. + * @see org.apache.geode.pdx.PdxInstance + * @see #getIdField(PdxInstance) + */ + protected @NonNull Object getIdentifier(@NonNull PdxInstance pdxInstance) { + + Assert.notNull(pdxInstance, "PdxInstance must not be null"); + + Optional idFieldName = CollectionUtils.nullSafeList(pdxInstance.getFieldNames()).stream() + .filter(StringUtils::hasText) + .filter(pdxInstance::isIdentityField) + .findFirst(); + + return idFieldName + .map(pdxInstance::getField) + .orElseGet(() -> getIdField(pdxInstance)); + } + + /** + * Searches for a PDX {@link String field name} on the {@link PdxInstance} named {@literal id} and returns + * its value as the {@link Object identifier} for the {@link PdxInstance}. + * + * @param pdxInstance {@link PdxInstance} to evaluate; must not be {@literal null}. + * @return the value of the {@literal id} {@link String field name} of the {@link PdxInstance} if present. + * @throws IllegalStateException if the {@link PdxInstance} does not have an {@literal id} + * {@link String field name}. + * @see org.apache.geode.pdx.PdxInstance + * @see #getIdentifier(PdxInstance) + */ + protected @NonNull Object getIdField(@NonNull PdxInstance pdxInstance) { + + Assert.notNull(pdxInstance, "PdxInstance must not be null"); + + return Optional.of(pdxInstance) + .filter(it -> it.hasField(ID_FIELD_NAME)) + .map(it -> it.getField(ID_FIELD_NAME)) + .orElseThrow(() -> newIllegalStateException("PdxInstance for type [%1$s] has no %2$s", + pdxInstance.getClassName(), pdxInstance.hasField(ID_FIELD_NAME) ? "id" : "declared identity field")); + } + + /** + * Returns a computed {@literal JSON} {@link Resource} containing {@literal JSON} data that will be loaded into + * the given a {@link Region}. + * + * @param region target {@link Region} used to compute the {@literal JSON} {@link Resource} to load. + * @return an {@link Optional} {@literal JSON} {@link Resource} containing {@literal JSON} data + * to load into the given {@link Region}. + * @see org.springframework.core.io.Resource + * @see org.apache.geode.cache.Region + * @see java.util.Optional + */ + protected Optional getResource(@NonNull Region region, @Nullable String resourcePrefix) { + + Assert.notNull(region, "Region must not be null"); String regionName = region.getName().toLowerCase(); String resourceName = String.format(RESOURCE_NAME_PATTERN, regionName); + String resolvedResourcePrefix = StringUtils.hasText(resourcePrefix) + ? resourcePrefix + : CLASSPATH_RESOURCE_PREFIX; - return getApplicationContext() - .map(it -> it.getResource(String.format("classpath:%s", resourceName))); + Resource resource = getApplicationContext() + .map(it -> it.getResource(String.format("%1$s%2$s", resolvedResourcePrefix, resourceName))) + .orElseGet((() -> new ClassPathResource(resourceName))); + + return Optional.of(resource); } - protected @NonNull Object getIdentifier(@NonNull PdxInstance pdxInstance) { - - String idField = CollectionUtils.nullSafeList(pdxInstance.getFieldNames()).stream() - .filter(pdxInstance::isIdentityField) - .findFirst() - .orElseThrow(() -> newIllegalStateException("PdxInstance for type [%s] has no declared identify field", - pdxInstance.getClassName())); - - return pdxInstance.getField(idField); + /** + * Returns the {@link String file system path} specifying the location for where to export the {@link Resource}. + * + * @return the {@link String file system path} specifying the location for where to export the {@link Resource}. + */ + protected @NonNull String getResourceLocation() { + return String.format("%1$s%2$s", FILESYSTEM_RESOURCE_PREFIX, System.getProperty("user.dir")); } - protected PdxInstance toPdxInstance(byte[] json) { - return JSONFormatter.fromJSON(json); + /** + * Convert {@link Object values} contained in the {@link Region} to {@literal JSON}. + * + * @param region {@link Region} to process; must not be {@literal null}. + * @return a {@literal JSON} {@link String} containing the {@link Object values} + * from the given {@link Region}. + * @see org.apache.geode.cache.Region + * @see #toJson(Object) + */ + @SuppressWarnings("unchecked") + protected @NonNull String toJson(@NonNull Region region) { + + Assert.notNull(region, "Region must not be null"); + + StringBuilder json = new StringBuilder("["); + + boolean addComma = false; + + for (Object value : CollectionUtils.nullSafeCollection(region.values())) { + json.append(addComma ? ", " : ""); + json.append(toJson(value)); + addComma = true; + } + + json.append("]"); + + return json.toString(); + } + + /** + * Converts the given {@link Object} into a {@literal JSON} {@link String}. + * + * @param source {@link Object} to convert into {@literal JSON}. + * @return a {@link String} containing the {@literal JSON} generated from the given {@link Object}. + * @see #getObjectToJsonConverter() + */ + protected @NonNull String toJson(@NonNull Object source) { + return getObjectToJsonConverter().convert(source); + } + + /** + * Converts the array of {@literal JSON} {@link Byte#TYPE bytes} into a {@link PdxInstance}. + * + * @param json array of {@link Byte#TYPE} containing {@literal JSON} data. + * @return a {@link PdxInstance} converted from the {@literal JSON} {@link Byte#TYPE bytes}. + * @see org.apache.geode.pdx.PdxInstance + * @see #getJsonToPdxConverter() + */ + protected @NonNull PdxInstance toPdx(@NonNull byte[] json) { + return getJsonToPdxConverter().convert(json); } } diff --git a/spring-geode/src/test/java/org/springframework/geode/data/json/JsonCacheDataImporterExporterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/data/json/JsonCacheDataImporterExporterUnitTests.java new file mode 100644 index 00000000..13e006fd --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/data/json/JsonCacheDataImporterExporterUnitTests.java @@ -0,0 +1,742 @@ +/* + * 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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.Region; +import org.apache.geode.pdx.PdxInstance; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.geode.data.json.converter.JsonToPdxConverter; +import org.springframework.geode.data.json.converter.ObjectToJsonConverter; + +import example.app.crm.model.Customer; +import example.app.pos.model.LineItem; + +/** + * Unit Tests for {@link JsonCacheDataImporterExporter}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.Region + * @see org.apache.geode.pdx.PdxInstance + * @see org.springframework.core.io.Resource + * @see org.springframework.geode.data.json.JsonCacheDataImporterExporter + * @since 1.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class JsonCacheDataImporterExporterUnitTests { + + @Spy + private JsonCacheDataImporterExporter importer; + + @Test + @SuppressWarnings("unchecked") + public void doExportFromRegionLogsAndSavesJson() { + + String json = "[{ \"name\": \"Jon Doe\"}]"; + + Resource mockResource = mock(Resource.class); + + Region mockRegion = mock(Region.class); + + doReturn("TestRegion").when(mockRegion).getName(); + + doReturn(JsonCacheDataImporterExporter.FILESYSTEM_RESOURCE_PREFIX).when(importer).getResourceLocation(); + doReturn(Optional.of(mockResource)).when(this.importer).getResource(eq(mockRegion), + eq(JsonCacheDataImporterExporter.FILESYSTEM_RESOURCE_PREFIX)); + doNothing().when(this.importer).save(anyString(), any(Resource.class)); + doReturn(json).when(this.importer).toJson(any()); + + assertThat(this.importer.doExportFrom(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)).toJson(eq(mockRegion)); + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.FILESYSTEM_RESOURCE_PREFIX)); + verify(this.importer, times(1)).save(eq(json), eq(mockResource)); + } + + @Test(expected = IllegalArgumentException.class) + public void doExportFromNullRegion() { + + try { + this.importer.doExportFrom(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void saveJsonToResource() throws IOException { + + String json = "[{ \"name\": \"Jon Doe\"}, { \"name\": \"Jane Doe\"}]"; + + StringWriter writer = new StringWriter(json.length()); + + Resource mockResource = mock(Resource.class); + + doReturn(writer).when(this.importer).newWriter(eq(mockResource)); + + this.importer.save(json, mockResource); + + assertThat(writer.toString()).isEqualTo(json); + + verify(this.importer, times(1)).newWriter(eq(mockResource)); + } + + @Test(expected = DataAccessResourceFailureException.class) + public void saveThrowsIoException() throws IOException { + + Writer mockWriter = mock(Writer.class); + + String json = "[{ \"name\": \"Jon Doe\"}, { \"name\": \"Jane Doe\"}" + + ", { \"name\": \"Pie Doe\"}, { \"name\": \"Sour Doe\"}]"; + + Resource mockResource = mock(Resource.class); + + doReturn("/path/to/resource.json").when(mockResource).getDescription(); + doReturn(mockWriter).when(this.importer).newWriter(eq(mockResource)); + doThrow(new IOException("TEST")).when(mockWriter).write(anyString(), anyInt(), anyInt()); + + try { + this.importer.save(json, mockResource); + } + catch (DataAccessResourceFailureException expected) { + + assertThat(expected) + .hasMessageStartingWith("Failed to save content '%s' to Resource [/path/to/resource.json]", + this.importer.formatContentForPreview(json)); + assertThat(expected).hasCauseInstanceOf(IOException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + finally { + verify(this.importer, times(1)).newWriter(eq(mockResource)); + verify(mockWriter, times(1)).write(eq(json), eq(0), eq(json.length())); + } + } + @Test + public void saveWithNoContent() throws IOException { + + Resource mockResource = mock(Resource.class); + + try { + this.importer.save(" ", mockResource); + } + finally { + verify(this.importer, never()).newWriter(any(Resource.class)); + verifyNoInteractions(mockResource); + } + } + + @Test(expected = IllegalArgumentException.class) + public void saveWithNullResource() { + + try { + this.importer.save("{}", null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Resource must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + @SuppressWarnings("unchecked") + public void doImportIntoPutsPdxIntoRegionForJson() { + + Resource mockResource = mock(Resource.class); + + Region mockRegion = mock(Region.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + byte[] json = "{ \"name\": \"Jon Doe\"}".getBytes(); + + doReturn(Optional.of(mockResource)).when(this.importer).getResource(eq(mockRegion), + eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + doReturn(true).when(mockResource).exists(); + doReturn(json).when(this.importer).getContent(eq(mockResource)); + doReturn(mockPdxInstance).when(this.importer).toPdx(eq(json)); + doReturn(1).when(this.importer).getIdentifier(eq(mockPdxInstance)); + + assertThat(this.importer.doImportInto(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + verify(this.importer, times(1)).getContent(eq(mockResource)); + verify(this.importer, times(1)).toPdx(eq(json)); + verify(this.importer, times(1)).getIdentifier(eq(mockPdxInstance)); + verify(mockResource, times(1)).exists(); + verify(mockRegion, times(1)).put(eq(1), eq(mockPdxInstance)); + } + + @Test + @SuppressWarnings("unchecked") + public void doImportIntoWithNoResource() { + + Region mockRegion = mock(Region.class); + + doReturn(Optional.empty()).when(this.importer) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + + assertThat(this.importer.doImportInto(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)).doImportInto(eq(mockRegion)); + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + verifyNoMoreInteractions(this.importer); + verifyNoInteractions(mockRegion); + } + + @Test + @SuppressWarnings("unchecked") + public void doImportIntoWithNonExistingResource() { + + Resource mockResource = mock(Resource.class); + + Region mockRegion = mock(Region.class); + + doReturn(Optional.of(mockResource)).when(this.importer) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + doReturn(false).when(mockResource).exists(); + + assertThat(this.importer.doImportInto(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)).doImportInto(eq(mockRegion)); + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + verify(mockResource, times(1)).exists(); + verifyNoMoreInteractions(this.importer); + verifyNoMoreInteractions(mockResource); + verifyNoInteractions(mockRegion); + } + + @Test + @SuppressWarnings("unchecked") + public void doImportIntoWithResourceContainingNoContent() { + + Resource mockResource = mock(Resource.class); + + Region mockRegion = mock(Region.class); + + doReturn(Optional.of(mockResource)).when(this.importer) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + doReturn(true).when(mockResource).exists(); + doReturn(null).when(this.importer).getContent(eq(mockResource)); + + assertThat(this.importer.doImportInto(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)).doImportInto(eq(mockRegion)); + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + verify(this.importer, times(1)).getContent(eq(mockResource)); + verify(mockResource, times(1)).exists(); + verifyNoMoreInteractions(this.importer); + verifyNoMoreInteractions(mockResource); + verifyNoInteractions(mockRegion); + } + + @Test + @SuppressWarnings("unchecked") + public void doImportIntoWithNoPdxInstance() { + + Resource mockResource = mock(Resource.class); + + Region mockRegion = mock(Region.class); + + byte[] json = "[]".getBytes(); + + doReturn(Optional.of(mockResource)).when(this.importer) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + doReturn(true).when(mockResource).exists(); + doReturn(json).when(this.importer).getContent(eq(mockResource)); + doReturn(null).when(this.importer).toPdx(eq(json)); + + assertThat(this.importer.doImportInto(mockRegion)).isEqualTo(mockRegion); + + verify(this.importer, times(1)).doImportInto(eq(mockRegion)); + verify(this.importer, times(1)) + .getResource(eq(mockRegion), eq(JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX)); + verify(this.importer, times(1)).getContent(eq(mockResource)); + verify(this.importer, times(1)).toPdx(eq(json)); + verify(mockResource, times(1)).exists(); + verifyNoMoreInteractions(this.importer); + verifyNoMoreInteractions(mockResource); + verifyNoInteractions(mockRegion); + } + + @Test(expected = IllegalArgumentException.class) + public void doImportIntoNullRegion() { + + try { + this.importer.doImportInto(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void getContentFromResource() throws IOException { + + byte[] json = "{ \"name\": \"Jon Doe\" }".getBytes(); + + ByteArrayInputStream in = spy(new ByteArrayInputStream(json)); + + Resource mockResource = mock(Resource.class); + + doReturn(in).when(mockResource).getInputStream(); + + assertThat(this.importer.getContent(mockResource)).isEqualTo(json); + + verify(mockResource, times(1)).getInputStream(); + verify(in, times(1)).close(); + } + + @Test(expected = IllegalArgumentException.class) + public void getContentFromNullResource() { + + try { + this.importer.getContent(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Resource must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = DataAccessResourceFailureException.class) + public void getContentThrowsIoException() throws IOException { + + Resource mockResource = mock(Resource.class); + + doReturn("Test Resource").when(mockResource).getDescription(); + doThrow(new IOException("TEST")).when(mockResource).getInputStream(); + + try { + this.importer.getContent(mockResource); + } + catch (DataAccessResourceFailureException expected) { + + assertThat(expected).hasMessageStartingWith("Failed to read from Resource [Test Resource]"); + assertThat(expected).hasCauseInstanceOf(IOException.class); + assertThat(expected.getCause()).hasMessage("TEST"); + assertThat(expected.getCause()).hasNoCause(); + + throw expected; + } + } + + @Test + public void getIdentifierFromPdxInstance() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(Arrays.asList("age", "id", "name")).when(mockPdxInstance).getFieldNames(); + doReturn(true).when(mockPdxInstance).isIdentityField(eq("id")); + doReturn(42).when(mockPdxInstance).getField(eq("id")); + + assertThat(this.importer.getIdentifier(mockPdxInstance)).isEqualTo(42); + + verify(this.importer, never()).getIdField(any(PdxInstance.class)); + verify(mockPdxInstance, times(1)).getFieldNames(); + verify(mockPdxInstance, times(1)).isIdentityField(eq("age")); + verify(mockPdxInstance, times(1)).isIdentityField(eq("id")); + verify(mockPdxInstance, never()).isIdentityField(eq("name")); + verify(mockPdxInstance, times(1)).getField(eq("id")); + } + + @Test + public void getIdentifierFromPdxInstanceHavingNoFields() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(null).when(mockPdxInstance).getFieldNames(); + doReturn(69).when(this.importer).getIdField(eq(mockPdxInstance)); + + assertThat(this.importer.getIdentifier(mockPdxInstance)).isEqualTo(69); + + verify(this.importer, times(1)).getIdField(eq(mockPdxInstance)); + verify(mockPdxInstance, times(1)).getFieldNames(); + verify(mockPdxInstance, never()).isIdentityField(anyString()); + verify(mockPdxInstance, never()).getField(anyString()); + } + + @Test + public void getIdentifierFromPdxInstanceHavingNoIdentityFields() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(Arrays.asList("", "age", null, "name", " ")).when(mockPdxInstance).getFieldNames(); + doReturn(false).when(mockPdxInstance).isIdentityField(anyString()); + doReturn(101).when(this.importer).getIdField(eq(mockPdxInstance)); + + assertThat(this.importer.getIdentifier(mockPdxInstance)).isEqualTo(101); + + verify(this.importer, times(1)).getIdField(eq(mockPdxInstance)); + verify(mockPdxInstance, times(1)).getFieldNames(); + verify(mockPdxInstance, times(1)).isIdentityField(eq("age")); + verify(mockPdxInstance, times(1)).isIdentityField(eq("name")); + verify(mockPdxInstance, never()).isIdentityField(isNull()); + verify(mockPdxInstance, never()).isIdentityField(eq("")); + verify(mockPdxInstance, never()).isIdentityField(eq(" ")); + verify(mockPdxInstance, never()).getField(anyString()); + } + + @Test(expected = IllegalArgumentException.class) + public void getIdentifierFromNullPdxInstance() { + + try { + this.importer.getIdentifier(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("PdxInstance must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) + public void getIdentifierFromPdxInstanceWithNoIdentifier() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(Collections.singletonList("name")).when(mockPdxInstance).getFieldNames(); + doReturn(false).when(mockPdxInstance).isIdentityField(anyString()); + doThrow(new IllegalStateException("TEST")).when(this.importer).getIdField(eq(mockPdxInstance)); + + try { + this.importer.getIdentifier(mockPdxInstance); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("TEST"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)).getFieldNames(); + verify(mockPdxInstance, times(1)).isIdentityField(eq("name")); + verify(mockPdxInstance, never()).getField(anyString()); + } + } + + @Test + public void getIdFieldFromPdxInstance() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance).hasField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + doReturn(42).when(mockPdxInstance).getField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + + assertThat(this.importer.getIdField(mockPdxInstance)).isEqualTo(42); + + verify(mockPdxInstance, times(1)) + .hasField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + verify(mockPdxInstance, times(1)) + .getField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + } + + @Test(expected = IllegalArgumentException.class) + public void getIdFieldFromNullPdxInstance() { + + try { + this.importer.getIdField(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("PdxInstance must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) + public void getIdFieldFromPdxInstanceWithNoIdField() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(LineItem.class.getName()).when(mockPdxInstance).getClassName(); + doReturn(false).when(mockPdxInstance).hasField(anyString()); + + try { + this.importer.getIdField(mockPdxInstance); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("PdxInstance for type [%s] has no declared identity field", + LineItem.class.getName()); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(2)) + .hasField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + verify(mockPdxInstance, never()).getField(anyString()); + } + } + + @Test(expected = IllegalStateException.class) + public void getIdFieldFromPdxInstanceWithNoId() { + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(true).when(mockPdxInstance).hasField(anyString()); + doReturn(null).when(mockPdxInstance).getField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + doReturn(LineItem.class.getName()).when(mockPdxInstance).getClassName(); + + try { + this.importer.getIdField(mockPdxInstance); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("PdxInstance for type [%s] has no id", + LineItem.class.getName()); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(mockPdxInstance, times(1)).getClassName(); + verify(mockPdxInstance, times(1)) + .getField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + verify(mockPdxInstance, times(2)) + .hasField(eq(JsonCacheDataImporterExporter.ID_FIELD_NAME)); + } + } + + @Test + public void getResourceFromRegionAndResourcePrefix() { + + ApplicationContext mockApplicationContext = mock(ApplicationContext.class); + + Region mockRegion = mock(Region.class); + + Resource mockResource = mock(Resource.class); + + doReturn(mockResource).when(mockApplicationContext).getResource(anyString()); + doReturn("Example").when(mockRegion).getName(); + doReturn(Optional.of(mockApplicationContext)).when(this.importer).getApplicationContext(); + + assertThat(this.importer.getResource(mockRegion, "sftp://host/resources/").orElse(null)) + .isEqualTo(mockResource); + + verify(mockApplicationContext, times(1)) + .getResource(eq("sftp://host/resources/data-example.json")); + verify(mockRegion, times(1)).getName(); + } + + @Test + public void getResourceWhenApplicationContextIsNotPresent() { + + Region mockRegion = mock(Region.class); + + doReturn("TEST").when(mockRegion).getName(); + + Resource resource = this.importer.getResource(mockRegion, "file://").orElse(null); + + assertThat(resource).isInstanceOf(ClassPathResource.class); + assertThat(((ClassPathResource) resource).getPath()).isEqualTo("data-test.json"); + + verify(mockRegion, times(1)).getName(); + } + + @Test + public void getResourceWhenApplicationContextReturnsNoResource() { + + ApplicationContext mockApplicationContext = mock(ApplicationContext.class); + + Region mockRegion = mock(Region.class); + + doReturn(null).when(mockApplicationContext).getResource(anyString()); + doReturn("MocK").when(mockRegion).getName(); + doReturn(Optional.ofNullable(mockApplicationContext)).when(this.importer).getApplicationContext(); + + Resource resource = this.importer.getResource(mockRegion, null).orElse(null); + + assertThat(resource).isInstanceOf(ClassPathResource.class); + assertThat(((ClassPathResource) resource).getPath()).isEqualTo("data-mock.json"); + + verify(mockApplicationContext, times(1)) + .getResource(eq("classpath:data-mock.json")); + verify(mockRegion, times(1)).getName(); + } + + @Test(expected = IllegalArgumentException.class) + public void getResourceFromNullRegion() { + + try { + this.importer.getResource(null, JsonCacheDataImporterExporter.CLASSPATH_RESOURCE_PREFIX); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void getResourceLocationIsInWorkingDirectory() { + assertThat(this.importer.getResourceLocation()).isEqualTo(String.format("%1$s%2$s", + JsonCacheDataImporterExporter.FILESYSTEM_RESOURCE_PREFIX, System.getProperty("user.dir"))); + } + + @Test + public void toJsonFromRegion() { + + String jonDoeJson = "{ \"name\": \"Jon Doe\" }"; + String janeDoeJson = "{ \"name\": \"Jane Doe\" }"; + String pieDoeJson = "{ \"name\": \"Pie Doe\" }"; + String expectedJson = String.format("[%1$s, %2$s, %3$s]", jonDoeJson, janeDoeJson, pieDoeJson); + + Customer jonDoe = Customer.newCustomer(1L, "Jon Doe"); + Customer janeDoe = Customer.newCustomer(2L, "Jane Doe"); + Customer pieDoe = Customer.newCustomer(3L, "Pie Doe"); + + Region customers = mock(Region.class); + + doReturn(Arrays.asList(jonDoe, janeDoe, pieDoe)).when(customers).values(); + doReturn(jonDoeJson).when(this.importer).toJson(eq(jonDoe)); + doReturn(janeDoeJson).when(this.importer).toJson(eq(janeDoe)); + doReturn(pieDoeJson).when(this.importer).toJson(eq(pieDoe)); + + assertThat(this.importer.toJson(customers)).isEqualTo(expectedJson); + + verify(this.importer, times(1)).toJson(eq(jonDoe)); + verify(this.importer, times(1)).toJson(eq(janeDoe)); + verify(this.importer, times(1)).toJson(eq(pieDoe)); + verify(customers, times(1)).values(); + } + + @Test + public void toJsonFromEmptyRegion() { + + Region mockRegion = mock(Region.class); + + assertThat(this.importer.toJson(mockRegion)).isEqualTo("[]"); + + verify(mockRegion, times(1)).values(); + } + + @Test(expected = IllegalArgumentException.class) + public void toJsonFromNullRegion() { + + try { + this.importer.toJson(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Region must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void toJsonFromObjectCallsObjectToJsonConverter() { + + String json = "{ \"name\": \"Jon Doe\" }"; + + ObjectToJsonConverter mockConverter = mock(ObjectToJsonConverter.class); + + doReturn(mockConverter).when(this.importer).getObjectToJsonConverter(); + doReturn(json).when(mockConverter).convert(eq("TEST")); + + assertThat(this.importer.toJson("TEST")).isEqualTo(json); + + verify(this.importer, times(1)).getObjectToJsonConverter(); + verify(mockConverter, times(1)).convert(eq("TEST")); + } + + @Test + public void toPdxFromJsonCallsJsonToPdxConverter() { + + byte[] json = "{ \"name\": \"Jon Doe\" }".getBytes(); + + JsonToPdxConverter mockConverter = mock(JsonToPdxConverter.class); + + PdxInstance mockPdxInstance = mock(PdxInstance.class); + + doReturn(mockConverter).when(this.importer).getJsonToPdxConverter(); + doReturn(mockPdxInstance).when(mockConverter).convert(eq(json)); + + assertThat(this.importer.toPdx(json)).isEqualTo(mockPdxInstance); + + verify(this.importer, times(1)).getJsonToPdxConverter(); + verify(mockConverter, times(1)).convert(eq(json)); + } +}