diff --git a/pom.xml b/pom.xml index 5b8b5488..ec098653 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,7 @@ org.apache.sling org.apache.sling.models.api - 1.3.6 + 1.3.9-SNAPSHOT provided @@ -217,5 +217,10 @@ 1.0.8 test + + com.fasterxml.jackson.core + jackson-databind + 2.3.2 + diff --git a/src/main/java/org/apache/sling/models/impl/serializer/DefaultExternalizePathProvider.java b/src/main/java/org/apache/sling/models/impl/serializer/DefaultExternalizePathProvider.java new file mode 100644 index 00000000..9a97ec38 --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/serializer/DefaultExternalizePathProvider.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.sling.models.impl.serializer; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.models.annotations.ExternalizePath; +import org.apache.sling.models.annotations.ExternalizePathProvider; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Component; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** Fallback Implementation of the Externalized Path Provider that uses the Resource Resolver's map function **/ +@Component( + property = Constants.SERVICE_RANKING + ":Integer=1", + immediate = true, + service = { + ExternalizePathProvider.class + } +) +public class DefaultExternalizePathProvider + implements ExternalizePathProvider +{ + @Override + public String externalize(@NotNull Object model, ExternalizePath annotation, String sourcePath) { + String answer = sourcePath; + ResourceResolver resourceResolver = getResourceResolver(model, annotation); + if (sourcePath != null && !sourcePath.isEmpty() && resourceResolver != null) { + answer = resourceResolver.map(sourcePath); + } + return answer; + } + + /** + * Obtains the Resource from the Model in order to Externalize + * @param model + * @param annotation + * @return + */ + private ResourceResolver getResourceResolver(Object model, ExternalizePath annotation) { + Resource answer = null; + // Get Resource from specified Resource method + String resourceMethodName = annotation.resourceMethod(); + if(!resourceMethodName.isEmpty()) { + try { + Method getResourceMethod = model.getClass().getMethod(resourceMethodName); + if(getResourceMethod.getReturnType().isAssignableFrom(Resource.class)) { + answer = (Resource) getResourceMethod.invoke(model, null); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // If not found then we cannot use Externalize Path but then we just send the original value + } + } + if(answer == null) { + // Get Resource from specified Resource Field + String resourceFieldName = annotation.resourceField(); + if (!resourceFieldName.isEmpty()) { + try { + Field resourceField = FieldUtils.getField(model.getClass(), resourceFieldName, true); + if(resourceField != null && resourceField.getType().isAssignableFrom(Resource.class)) { + answer = (Resource) resourceField.get(model); + } + } catch (IllegalAccessException e) { + // If not found then we cannot use Externalize Path but then we just send the original value + } + } + } + if(answer == null) { + // Get Resource from default location (getResource()) + try { + Method getResourceMethod = model.getClass().getMethod("getResource"); + if(getResourceMethod.getReturnType().isAssignableFrom(Resource.class)) { + answer = (Resource) getResourceMethod.invoke(model, null); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // If not found then we cannot use Externalize Path but then we just send the original value + } + } + if(answer == null) { + // Get Resource from default Resource Field (resource) + try { + Field resourceField = FieldUtils.getField(model.getClass(), "resource", true); + if(resourceField != null && resourceField.getType().isAssignableFrom(Resource.class)) { + answer = (Resource) resourceField.get(model); + } + } catch (IllegalAccessException e) { + // If not found then we cannot use Externalize Path but then we just send the original value + } + } + return answer == null ? null : answer.getResourceResolver(); + } +} diff --git a/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManager.java b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManager.java new file mode 100644 index 00000000..86e4202c --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManager.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.sling.models.impl.serializer; + +import org.apache.sling.models.annotations.ExternalizePathProvider; + +/** Service that maintains a list of All Externalize Path Providers and returns the best fitting one depending on its implementation **/ +public interface ExternalizePathProviderManager { + + /** @return The best fitting Provider that is managed here at the moment **/ + ExternalizePathProvider getExternalizedPathProvider(); + +} diff --git a/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManagerService.java b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManagerService.java new file mode 100644 index 00000000..ca3ba45d --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManagerService.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.sling.models.impl.serializer; + +import org.apache.sling.commons.osgi.Order; +import org.apache.sling.commons.osgi.RankedServices; +import org.apache.sling.models.annotations.ExternalizePathProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import java.util.Map; + +/** + * Simple Implementation of the Externalize Path Provider Manager service + * which just binds them and then selects the highest one (first one as the + * order is descending). + */ +@Component( + service={ + ExternalizePathProviderManager.class + } +) +public class ExternalizePathProviderManagerService + implements ExternalizePathProviderManager +{ + private RankedServices providers = new RankedServices<>(Order.DESCENDING); + + @Reference( + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC + ) + protected void bindExternalizePathProvider(final ExternalizePathProvider provider, final Map props) { + providers.bind(provider, props); + } + + protected void unbindExternalizePathProvider(final ExternalizePathProvider provider, final Map props) { + providers.unbind(provider, props); + } + + @Override + public ExternalizePathProvider getExternalizedPathProvider() { + return providers.getList().get(0); + } +} diff --git a/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathSerializer.java b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathSerializer.java new file mode 100644 index 00000000..76ffd997 --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathSerializer.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.sling.models.impl.serializer; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.sling.models.annotations.ExternalizePath; +import org.apache.sling.models.annotations.ExternalizePathProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Json Serializer that will take 'Externalize Path' annotation and shortens them + * with the current Externalize Path Provider. This Serializer is used as an Annotation + * on the Model: + * + * @Exporter(name = "jackson", extensions = "json") + * @JsonSerialize(using = ExternalizePathSerializer.class) + * + * ATTENTION: this class is no an OSGi class but it needs to obtain a service to the + * {@link ExternalizePathProviderManager} and so this class can only be used in an OSGi + * environment. There is also some restriction with respect to the Providers as they need + * access to other services like the Resource Resolver. + */ +public class ExternalizePathSerializer + extends JsonSerializer +{ + private Logger logger = LoggerFactory.getLogger(getClass()); + + private ExternalizePathProviderManager externalizePathProviderManager = + getService(ExternalizePathProviderManager.class, ExternalizePathProviderManager.class); + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) + throws IOException + { + jgen.writeStartObject(); + try { + if(value != null) { + Class valueClass = value.getClass(); + // List all public methods (source of the Model) + Method[] methods = value.getClass().getMethods(); + for(Method method: methods) { + // Ignore methods on Object class + if(method.getDeclaringClass() != Object.class) { + // Get Method Name, check if Method is Json Ignored and check that method is a Getter + String methodName = method.getName(); + if(method.getAnnotation(JsonIgnore.class) != null) { + logger.debug("Ignore Method because of JsonIgnore Annotation: '{}'", methodName); + } + if ((methodName.startsWith("get") || methodName.startsWith("is")) && method.getParameterTypes().length == 0) { + Object property; + try { + // Obtain Value from method and get corresponding Field Name + property = method.invoke(value, null); + String fieldName = null; + if (methodName.startsWith("get")) { + fieldName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + } else if (methodName.startsWith("is")) { + fieldName = methodName.substring(2, 3).toLowerCase() + methodName.substring(3); + } + if (property == null) { + // If Property is null then write out a NULL + jgen.writeNullField(fieldName); + } else { + // Try to get the Annotation (Method or Field) + ExternalizePath externalizePath = method.getAnnotation(ExternalizePath.class); + if(externalizePath == null) { + // If method does not have the Externalize Path Annotation then check its corresponding Field + Field propertyField = FieldUtils.getField(valueClass, fieldName, true); + if(propertyField != null) { + // Check type + Class fieldType = propertyField.getType(); + Class methodType = method.getReturnType(); + if (!fieldType.isAssignableFrom(methodType)) { + logger.warn("Matching Field: '{}' is not assignable to method: '{}', ignore Annotation", fieldName, methodName); + } else { + externalizePath = propertyField.getAnnotation(ExternalizePath.class); + } + } + } + if(externalizePath != null) { + // Enforce that this Annotation only works Strings and if so get the Externalization + // Provider and if found externalize it + if(!(property instanceof String)) { + logger.warn( + "Annotation 'Externalize Path' can only be applied to a String but was applied to: '{}'", + method.getReturnType().getName() + ); + } else { + // If this method is Externalize Path then map the value first + ExternalizePathProvider externalizePathProvider = getExternalizedPathProvider(); + if (externalizePathProvider != null) { + property = externalizePathProvider.externalize(value, externalizePath, (String) property); + } + } + } + // Write Property out + createProperty(jgen, fieldName, property, provider); + } + } catch (InvocationTargetException | RuntimeException e) { + logger.warn("Failed to Invoke Method: '{} -> ignored", e.getLocalizedMessage()); + } + } + } + } + } + } catch(JsonProcessingException | RuntimeException e) { + logger.warn("Externalize Path Serialize failed", e); + } catch (IllegalAccessException e) { + logger.warn("Externalize Path Method Access failed", e); + } finally { + jgen.writeEndObject(); + } + } + + void createProperty(final JsonGenerator jgen, final String name, final Object value, + final SerializerProvider provider) + throws IOException { + Object[] values = null; + if (value.getClass().isArray()) { + final int length = Array.getLength(value); + // write out empty array + if ( length == 0 ) { + jgen.writeArrayFieldStart(name); + jgen.writeEndArray(); + return; + } + values = new Object[Array.getLength(value)]; + for(int i=0; i T getService(Class clazz, Class type) { + Bundle currentBundle = FrameworkUtil.getBundle(clazz); + if (currentBundle == null) { + return null; + } + BundleContext bundleContext = currentBundle.getBundleContext(); + if (bundleContext == null) { + return null; + } + ServiceReference serviceReference = bundleContext.getServiceReference(type); + if (serviceReference == null) { + return null; + } + T service = bundleContext.getService(serviceReference); + if (service == null) { + return null; + } + return service; + } +} diff --git a/src/test/java/org/apache/sling/models/impl/serializer/ExternalizedPathSerializerTest.java b/src/test/java/org/apache/sling/models/impl/serializer/ExternalizedPathSerializerTest.java new file mode 100644 index 00000000..a9dcc837 --- /dev/null +++ b/src/test/java/org/apache/sling/models/impl/serializer/ExternalizedPathSerializerTest.java @@ -0,0 +1,436 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.sling.models.impl.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.models.annotations.ExternalizePath; +import org.apache.sling.models.annotations.ExternalizePathProvider; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; +import org.mockito.internal.util.reflection.Whitebox; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.osgi.framework.Constants.SERVICE_ID; +import static org.osgi.framework.Constants.SERVICE_RANKING; + +public class ExternalizedPathSerializerTest { + + private ExternalizePathSerializer externalizePathSerializer; + private ExternalizePathProviderManagerService externalizePathProviderManagerService; + private Resource resource; + private ResourceResolver resourceResolver; + private JsonGenerator jsonGenerator; + private SerializerProvider serializerProvider; + + @Before + public void setup() { + externalizePathSerializer = spy(new ExternalizePathSerializer()); + externalizePathProviderManagerService = new ExternalizePathProviderManagerService(); + ExternalizePathProvider defaultProvider = new DefaultExternalizePathProvider(); + Map props = new HashMap<>(); + props.put(SERVICE_ID, 1L); + props.put(SERVICE_RANKING, 1); + externalizePathProviderManagerService.bindExternalizePathProvider(defaultProvider, props); + + resource = mock(Resource.class); + jsonGenerator = mock(JsonGenerator.class); + serializerProvider = mock(SerializerProvider.class); + resourceResolver = mock(ResourceResolver.class); + when(resource.getResourceResolver()).thenReturn(resourceResolver); + } + + @Test + public void testNoSerialization() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String name = "imagePath"; + + NoAnnotationModel model = new NoAnnotationModel(resource, imagePath); + when(resourceResolver.map(imagePath)).thenReturn(imagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertEquals("Image Path should not have changed", imagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testSimpleSerialization() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String name = "imagePath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + MethodAnnotatedModel model = new MethodAnnotatedModel(resource, imagePath); + when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testCustomProviderInjection() throws Exception { + String imagePath = "/content/test/image/test-image.jpg"; + String from = "/content/test/image/"; + String to1 = "/image1/"; + String to2 = "/image2/"; + String to3 = "/image3/"; + String mappedImagePath = "/image/test-image.jpg"; + String mappedImagePath3 = "/image3/test-image.jpg"; + String name = "imagePath"; + + when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); + + TestExternalizePathProvider provider1 = new TestExternalizePathProvider(from, to1); + // ATTENTION: Properties Map need to be reset as they are stored into the RankedServices as is + Map props = new HashMap<>(); + props.put(SERVICE_ID, 1234L); + props.put(SERVICE_RANKING, 100); + externalizePathProviderManagerService.bindExternalizePathProvider(provider1, props); + TestExternalizePathProvider provider3 = new TestExternalizePathProvider(from, to3); + props = new HashMap<>(); + props.put(SERVICE_ID, 1235L); + props.put(SERVICE_RANKING, 400); + externalizePathProviderManagerService.bindExternalizePathProvider(provider3, props); + TestExternalizePathProvider provider2 = new TestExternalizePathProvider(from, to2); + props = new HashMap<>(); + props.put(SERVICE_ID, 1236L); + props.put(SERVICE_RANKING, 200); + externalizePathProviderManagerService.bindExternalizePathProvider(provider2, props); + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + MethodAnnotatedModel model = new MethodAnnotatedModel(resource, imagePath); + when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath3, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testAnnotationInInheritanceSerialization() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String testPath = "/content/testTest/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String mappedTestImagePath = "/image/test/test-image.jpg"; + final String name = "imagePath"; + final String testName = "testPath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + MethodAnnotatedInInheritanceModel model = new MethodAnnotatedInInheritanceModel(resource, imagePath, testPath); + when(resourceResolver.map(eq(imagePath))).thenReturn(mappedImagePath); + when(resourceResolver.map(eq(testPath))).thenReturn(mappedTestImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } else if(fieldName.equals(testName)) { + String stringValue = (String) value; + assertNotEquals("Test Path should have changed", testPath, stringValue); + assertEquals("Test Path did not change as expected", mappedTestImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testResourceMethod() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String name = "imagePath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + AnnotationResourceMethodModel model = new AnnotationResourceMethodModel(resource, imagePath); + when(resourceResolver.map(eq(imagePath))).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testResourceField() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String name = "imagePath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + AnnotationResourceFieldModel model = new AnnotationResourceFieldModel(resource, imagePath); + when(resourceResolver.map(eq(imagePath))).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testAnnotationOnField() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String name = "imagePath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + FieldAnnotatedModel model = new FieldAnnotatedModel(resource, imagePath); + when(resourceResolver.map(eq(imagePath))).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + @Test + public void testAnnotationOnSubclassField() throws Exception { + final String imagePath = "/content/test/image/test-image.jpg"; + final String mappedImagePath = "/image/test-image.jpg"; + final String name = "imagePath"; + + Whitebox.setInternalState( + externalizePathSerializer, "externalizePathProviderManager", externalizePathProviderManagerService + ); + SubclassFieldAnnotatedModel model = new SubclassFieldAnnotatedModel(resource, imagePath); + when(resourceResolver.map(eq(imagePath))).thenReturn(mappedImagePath); + doAnswer( + invocation -> { + String fieldName = (String) invocation.getArguments()[1]; + Object value = invocation.getArguments()[2]; + if(fieldName.equals(name)) { + String stringValue = (String) value; + assertNotEquals("Image Path should have changed", imagePath, stringValue); + assertEquals("Image Path did not change as expected", mappedImagePath, stringValue); + } + return null; + } + ).when(externalizePathSerializer).createProperty(any(JsonGenerator.class), anyString(), any(Object.class), any(SerializerProvider.class)); + externalizePathSerializer.serialize(model, jsonGenerator, serializerProvider); + } + + private abstract static class AbstractModel { + private Resource resource; + private String path; + + public AbstractModel(Resource resource, String path) { + this.resource = resource; + this.path = path; + } + + public Resource getResource() { return resource; } + + public String getImagePath() { + return path; + } + } + + private static class NoAnnotationModel extends AbstractModel { + public NoAnnotationModel(Resource resource, String path) { + super(resource, path); + } + + public String getImagePath() { + return super.getImagePath(); + } + } + + private static class MethodAnnotatedModel extends AbstractModel { + public MethodAnnotatedModel(Resource resource, String path) { + super(resource, path); + } + + @ExternalizePath + public String getImagePath() { + return super.getImagePath(); + } + } + + private static class MethodAnnotatedInInheritanceModel + extends MethodAnnotatedModel + { + @ExternalizePath + private String testPath; + + public MethodAnnotatedInInheritanceModel(Resource resource, String path, String testPath) { + super(resource, path); + this.testPath = testPath; + } + + public String getTestPath() { + return testPath; + } + } + + private static class AnnotationResourceMethodModel { + private Resource myResource; + private String path; + public AnnotationResourceMethodModel(Resource resource, String path) { + this.myResource = resource; + this.path = path; + } + + @ExternalizePath(resourceMethod = "getMyResource") + public String getImagePath() { + return path; + } + + public Resource getMyResource() { + return myResource; + } + } + + private static class AnnotationResourceFieldModel { + private Resource anotherResource; + private String path; + public AnnotationResourceFieldModel(Resource resource, String path) { + this.anotherResource = resource; + this.path = path; + } + + @ExternalizePath(resourceField = "anotherResource") + public String getImagePath() { + return path; + } + + public Resource getMyResource() { + return anotherResource; + } + } + + private static class FieldAnnotatedModel { + private Resource resource; + @ExternalizePath + private String imagePath; + + public FieldAnnotatedModel(Resource resource, String path) { + this.resource = resource; + this.imagePath = path; + } + + public String getImagePath() { + return imagePath; + } + } + + private static class SubclassFieldAnnotatedModel extends FieldAnnotatedModel { + public SubclassFieldAnnotatedModel(Resource resource, String path) { + super(resource, path); + } + } + + private static class TestExternalizePathProvider + implements ExternalizePathProvider + { + private String from; + private String to; + + public TestExternalizePathProvider(String from, String to) { + this.from = from; + this.to = to; + } + + @Override + public String externalize(@NotNull Object model, ExternalizePath annocation, String sourcePath) { + String answer = sourcePath; + if(sourcePath.startsWith(from)) { + answer = to + sourcePath.substring(from.length()); + } + return answer; + } + } +}