From bb865a6c60acdf7f9d2addd774def6fa2d3575d2 Mon Sep 17 00:00:00 2001 From: Andreas Schaefer Date: Fri, 23 Aug 2019 11:45:08 -0700 Subject: [PATCH 1/4] SLING-8655 - Updated dependency on API, provided Externalized Path Injector (with default Provider) and an unit test --- pom.xml | 2 +- .../injectors/ExternalizedPathInjector.java | 113 ++++++++++++++++ .../ExternalizedPathInjectorTest.java | 128 ++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java create mode 100644 src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java diff --git a/pom.xml b/pom.xml index 5b8b5488..a663fd39 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 diff --git a/src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java b/src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java new file mode 100644 index 00000000..9aded00a --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java @@ -0,0 +1,113 @@ +/* + * 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.injectors; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.models.annotations.ExternalizePath; +import org.apache.sling.models.annotations.injectorspecific.ExternalizedPathProvider; +import org.apache.sling.models.spi.DisposalCallbackRegistry; +import org.apache.sling.models.spi.Injector; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Constants; +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.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@Component( + property=Constants.SERVICE_RANKING+":Integer=1000", + service={ + Injector.class + } +) +public class ExternalizedPathInjector + extends AbstractInjector + implements Injector +{ + List providerList = new ArrayList<>(); + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL) + void bindExternalizedPathProvider(ExternalizedPathProvider provider) { + providerList.add(provider); + // The providers are sorted so that the one with the highest priority is the first entry + Collections.sort( + providerList, + Comparator.comparingInt(ExternalizedPathProvider::getPriority).reversed() + ); + } + + void unbindExternalizedPathProvider(ExternalizedPathProvider provider) { + providerList.remove(provider); + } + + public ExternalizedPathInjector() { + bindExternalizedPathProvider(new DefaultExternalizedPathProvider()); + } + + @Override + public @NotNull String getName() { + return "externalize-path"; + } + + @Override + public Object getValue(@NotNull Object adaptable, String name, @NotNull Type type, @NotNull AnnotatedElement element, + @NotNull DisposalCallbackRegistry callbackRegistry) { + if (adaptable == ObjectUtils.NULL) { + return null; + } + if (element.isAnnotationPresent(ExternalizePath.class)) { + ValueMap properties = getValueMap(adaptable); + if(properties != ObjectUtils.NULL) { + String imagePath = properties.get(name, String.class); + if(imagePath != null) { + ExternalizedPathProvider provider = providerList.get(0); + return provider.externalize(adaptable, imagePath); + } + } + } + return null; + } + + /** Fallback Implementation of the Externalized Path Provider that uses the Resource Resolver's map function **/ + private class DefaultExternalizedPathProvider + implements ExternalizedPathProvider + { + @Override + public int getPriority() { + return FALLBACK_PRIORITY; + } + + @Override + public String externalize(@NotNull Object adaptable, String sourcePath) { + String answer = sourcePath; + ResourceResolver resourceResolver = getResourceResolver(adaptable); + if(sourcePath != null && resourceResolver != null) { + answer = resourceResolver.map(sourcePath); + } + return answer; + } + } +} diff --git a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java new file mode 100644 index 00000000..2f40e014 --- /dev/null +++ b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java @@ -0,0 +1,128 @@ +package org.apache.sling.models.impl.injectors; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.models.annotations.ExternalizePath; +import org.apache.sling.models.annotations.injectorspecific.ExternalizedPathProvider; +import org.apache.sling.models.spi.DisposalCallbackRegistry; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ExternalizedPathInjectorTest { + + @Test + public void testNoResolveInjection() { + String imagePath = "/content/test/image/test-image.jpg"; + + ExternalizedPathInjector injector = new ExternalizedPathInjector(); + Resource adaptable = mock(Resource.class); + ValueMap valueMap = mock(ValueMap.class); + when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); + String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); + Type type = String.class; + AnnotatedElement element = mock(AnnotatedElement.class); + when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); + DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + when(adaptable.getResourceResolver()).thenReturn(resourceResolver); + when(resourceResolver.map(imagePath)).thenReturn(imagePath); + + Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); + assertEquals("No Mapping was expected", imagePath, value); + } + + @Test + public void testResolveInjection() { + String imagePath = "/content/test/image/test-image.jpg"; + String mappedImagePath = "/image/test-image.jpg"; + + ExternalizedPathInjector injector = new ExternalizedPathInjector(); + Resource adaptable = mock(Resource.class); + ValueMap valueMap = mock(ValueMap.class); + when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); + String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); + Type type = String.class; + AnnotatedElement element = mock(AnnotatedElement.class); + when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); + DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + when(adaptable.getResourceResolver()).thenReturn(resourceResolver); + when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); + + Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); + assertEquals("Mapping was expected", mappedImagePath, value); + } + + @Test + public void testCustomProviderInjection() { + 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 mappedImagePath1 = "/image1/test-image.jpg"; + String mappedImagePath2 = "/image2/test-image.jpg"; + String mappedImagePath3 = "/image3/test-image.jpg"; + + ExternalizedPathInjector injector = new ExternalizedPathInjector(); + Resource adaptable = mock(Resource.class); + ValueMap valueMap = mock(ValueMap.class); + when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); + String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); + Type type = String.class; + AnnotatedElement element = mock(AnnotatedElement.class); + when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); + DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); + ResourceResolver resourceResolver = mock(ResourceResolver.class); + when(adaptable.getResourceResolver()).thenReturn(resourceResolver); + when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); + + TestExternalizedPathProvider provider1 = new TestExternalizedPathProvider(100, from, to1); + injector.bindExternalizedPathProvider(provider1); + TestExternalizedPathProvider provider3 = new TestExternalizedPathProvider(300, from, to3); + injector.bindExternalizedPathProvider(provider3); + TestExternalizedPathProvider provider2 = new TestExternalizedPathProvider(200, from, to2); + injector.bindExternalizedPathProvider(provider2); + + Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); + assertEquals("Wrong Provider was selected", mappedImagePath3, value); + } + + private class TestExternalizedPathProvider + implements ExternalizedPathProvider + { + private int priority; + private String from = "/"; + private String to = "/"; + + public TestExternalizedPathProvider(int priority, String from, String to) { + this.priority = priority; + this.from = from; + this.to = to; + } + @Override + public int getPriority() { return priority; } + + @Override + public String externalize(@NotNull Object adaptable, String sourcePath) { + String answer = sourcePath; + if(sourcePath.startsWith(from)) { + answer = to + sourcePath.substring(from.length()); + } + return answer; + } + } +} From d9b9a6d74e6de332b148fb91027f1faff350083c Mon Sep 17 00:00:00 2001 From: Andreas Schaefer Date: Mon, 26 Aug 2019 15:22:36 -0700 Subject: [PATCH 2/4] SLING-8655 - Removed Priorities, adjusted Naming and used Ranked Services to deal with Provider Priorities --- .../DefaultExternalizePathProvider.java | 48 +++++++ ...ctor.java => ExternalizePathInjector.java} | 75 +++++------ .../ExternalizedPathInjectorTest.java | 120 +++++++++++------- 3 files changed, 149 insertions(+), 94 deletions(-) create mode 100644 src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java rename src/main/java/org/apache/sling/models/impl/injectors/{ExternalizedPathInjector.java => ExternalizePathInjector.java} (55%) diff --git a/src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java b/src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java new file mode 100644 index 00000000..e5324804 --- /dev/null +++ b/src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java @@ -0,0 +1,48 @@ +/* + * 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.injectors; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Component; + +/** 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 adaptable, String sourcePath) { + String answer = sourcePath; + if (adaptable instanceof Resource) { + ResourceResolver resourceResolver = ((Resource) adaptable).getResourceResolver(); + if (sourcePath != null && resourceResolver != null) { + answer = resourceResolver.map(sourcePath); + } + } + return answer; + } +} diff --git a/src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java b/src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java similarity index 55% rename from src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java rename to src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java index 9aded00a..98830135 100644 --- a/src/main/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjector.java +++ b/src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java @@ -17,10 +17,11 @@ package org.apache.sling.models.impl.injectors; import org.apache.commons.lang3.ObjectUtils; -import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.commons.osgi.Order; +import org.apache.sling.commons.osgi.RankedServices; import org.apache.sling.models.annotations.ExternalizePath; -import org.apache.sling.models.annotations.injectorspecific.ExternalizedPathProvider; +import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; import org.apache.sling.models.spi.DisposalCallbackRegistry; import org.apache.sling.models.spi.Injector; import org.jetbrains.annotations.NotNull; @@ -32,39 +33,39 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.Iterator; +import java.util.Map; +/** + * Injector for a Model Property with the Annotation 'Externalize Path' + * which will change a Sling Path into its externalize form (shortening etc) + * + * The Externalize Path Provider is what will do the actual transformation and + * is pluggable. The component with the highest service ranking is selected here + * to provide the transformation. + */ @Component( property=Constants.SERVICE_RANKING+":Integer=1000", service={ Injector.class } ) -public class ExternalizedPathInjector +public class ExternalizePathInjector extends AbstractInjector implements Injector { - List providerList = new ArrayList<>(); + private RankedServices providers = new RankedServices<>(Order.DESCENDING); - @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL) - void bindExternalizedPathProvider(ExternalizedPathProvider provider) { - providerList.add(provider); - // The providers are sorted so that the one with the highest priority is the first entry - Collections.sort( - providerList, - Comparator.comparingInt(ExternalizedPathProvider::getPriority).reversed() - ); + @Reference( + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC + ) + protected void bindExternalizePathProvider(final ExternalizePathProvider provider, final Map props) { + providers.bind(provider, props); } - void unbindExternalizedPathProvider(ExternalizedPathProvider provider) { - providerList.remove(provider); - } - - public ExternalizedPathInjector() { - bindExternalizedPathProvider(new DefaultExternalizedPathProvider()); + protected void unbindExternalizePathProvider(final ExternalizePathProvider provider, final Map props) { + providers.unbind(provider, props); } @Override @@ -80,34 +81,18 @@ public Object getValue(@NotNull Object adaptable, String name, @NotNull Type typ } if (element.isAnnotationPresent(ExternalizePath.class)) { ValueMap properties = getValueMap(adaptable); - if(properties != ObjectUtils.NULL) { + if (properties != ObjectUtils.NULL) { String imagePath = properties.get(name, String.class); - if(imagePath != null) { - ExternalizedPathProvider provider = providerList.get(0); - return provider.externalize(adaptable, imagePath); + if (imagePath != null) { + Iterator i = providers.iterator(); + if (i.hasNext()) { + ExternalizePathProvider provider = i.next(); + return provider.externalize(adaptable, imagePath); + } } } } return null; } - /** Fallback Implementation of the Externalized Path Provider that uses the Resource Resolver's map function **/ - private class DefaultExternalizedPathProvider - implements ExternalizedPathProvider - { - @Override - public int getPriority() { - return FALLBACK_PRIORITY; - } - - @Override - public String externalize(@NotNull Object adaptable, String sourcePath) { - String answer = sourcePath; - ResourceResolver resourceResolver = getResourceResolver(adaptable); - if(sourcePath != null && resourceResolver != null) { - answer = resourceResolver.map(sourcePath); - } - return answer; - } - } } diff --git a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java index 2f40e014..ff73c270 100644 --- a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java +++ b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java @@ -1,40 +1,78 @@ +/* + * 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.injectors; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.models.annotations.ExternalizePath; -import org.apache.sling.models.annotations.injectorspecific.ExternalizedPathProvider; +import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; import org.apache.sling.models.spi.DisposalCallbackRegistry; import org.jetbrains.annotations.NotNull; +import org.junit.Before; import org.junit.Test; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.osgi.framework.Constants.SERVICE_ID; +import static org.osgi.framework.Constants.SERVICE_RANKING; public class ExternalizedPathInjectorTest { + private ExternalizePathInjector injector; + private Resource adaptable; + private ValueMap valueMap; + private Type type; + private AnnotatedElement element; + private DisposalCallbackRegistry callbackRegistry; + private ResourceResolver resourceResolver; + + @Before + public void setup() { + injector = new ExternalizePathInjector(); + adaptable = mock(Resource.class); + valueMap = mock(ValueMap.class); + when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); + type = String.class; + element = mock(AnnotatedElement.class); + when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); + callbackRegistry = mock(DisposalCallbackRegistry.class); + resourceResolver = mock(ResourceResolver.class); + when(adaptable.getResourceResolver()).thenReturn(resourceResolver); + ExternalizePathProvider defaultProvider = new DefaultExternalizePathProvider(); + Map props = new HashMap<>(); + props.put(SERVICE_ID, 1L); + props.put(SERVICE_RANKING, 1); + injector.bindExternalizePathProvider(defaultProvider, props); + } + @Test public void testNoResolveInjection() { String imagePath = "/content/test/image/test-image.jpg"; - - ExternalizedPathInjector injector = new ExternalizedPathInjector(); - Resource adaptable = mock(Resource.class); - ValueMap valueMap = mock(ValueMap.class); - when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); - Type type = String.class; - AnnotatedElement element = mock(AnnotatedElement.class); - when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); - DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - when(adaptable.getResourceResolver()).thenReturn(resourceResolver); when(resourceResolver.map(imagePath)).thenReturn(imagePath); Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); @@ -45,19 +83,9 @@ public void testNoResolveInjection() { public void testResolveInjection() { String imagePath = "/content/test/image/test-image.jpg"; String mappedImagePath = "/image/test-image.jpg"; - - ExternalizedPathInjector injector = new ExternalizedPathInjector(); - Resource adaptable = mock(Resource.class); - ValueMap valueMap = mock(ValueMap.class); - when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); - Type type = String.class; - AnnotatedElement element = mock(AnnotatedElement.class); - when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); - DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - when(adaptable.getResourceResolver()).thenReturn(resourceResolver); when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); @@ -72,49 +100,43 @@ public void testCustomProviderInjection() { String to2 = "/image2/"; String to3 = "/image3/"; String mappedImagePath = "/image/test-image.jpg"; - String mappedImagePath1 = "/image1/test-image.jpg"; - String mappedImagePath2 = "/image2/test-image.jpg"; String mappedImagePath3 = "/image3/test-image.jpg"; - - ExternalizedPathInjector injector = new ExternalizedPathInjector(); - Resource adaptable = mock(Resource.class); - ValueMap valueMap = mock(ValueMap.class); - when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); String name = "imagePath"; + when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); - Type type = String.class; - AnnotatedElement element = mock(AnnotatedElement.class); - when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); - DisposalCallbackRegistry callbackRegistry = mock(DisposalCallbackRegistry.class); - ResourceResolver resourceResolver = mock(ResourceResolver.class); - when(adaptable.getResourceResolver()).thenReturn(resourceResolver); when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); - TestExternalizedPathProvider provider1 = new TestExternalizedPathProvider(100, from, to1); - injector.bindExternalizedPathProvider(provider1); - TestExternalizedPathProvider provider3 = new TestExternalizedPathProvider(300, from, to3); - injector.bindExternalizedPathProvider(provider3); - TestExternalizedPathProvider provider2 = new TestExternalizedPathProvider(200, from, to2); - injector.bindExternalizedPathProvider(provider2); + 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); + injector.bindExternalizePathProvider(provider1, props); + TestExternalizePathProvider provider3 = new TestExternalizePathProvider(from, to3); + props = new HashMap<>(); + props.put(SERVICE_ID, 1235L); + props.put(SERVICE_RANKING, 400); + injector.bindExternalizePathProvider(provider3, props); + TestExternalizePathProvider provider2 = new TestExternalizePathProvider(from, to2); + props = new HashMap<>(); + props.put(SERVICE_ID, 1236L); + props.put(SERVICE_RANKING, 200); + injector.bindExternalizePathProvider(provider2, props); Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); assertEquals("Wrong Provider was selected", mappedImagePath3, value); } - private class TestExternalizedPathProvider - implements ExternalizedPathProvider + private class TestExternalizePathProvider + implements ExternalizePathProvider { - private int priority; private String from = "/"; private String to = "/"; - public TestExternalizedPathProvider(int priority, String from, String to) { - this.priority = priority; + public TestExternalizePathProvider(String from, String to) { this.from = from; this.to = to; } - @Override - public int getPriority() { return priority; } @Override public String externalize(@NotNull Object adaptable, String sourcePath) { From 06aee3b42841e67bc4652e95d1f1c0982a6eb5e0 Mon Sep 17 00:00:00 2001 From: Andreas Schaefer Date: Mon, 26 Aug 2019 15:29:36 -0700 Subject: [PATCH 3/4] SLING-8655 - Made inner test class static --- .../models/impl/injectors/ExternalizedPathInjectorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java index ff73c270..2004ec02 100644 --- a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java +++ b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java @@ -127,7 +127,7 @@ public void testCustomProviderInjection() { assertEquals("Wrong Provider was selected", mappedImagePath3, value); } - private class TestExternalizePathProvider + private static class TestExternalizePathProvider implements ExternalizePathProvider { private String from = "/"; From 31bcf930278e7901cdf7c92d38cd35f1463d133f Mon Sep 17 00:00:00 2001 From: Andreas Schaefer Date: Thu, 29 Aug 2019 16:11:48 -0700 Subject: [PATCH 4/4] SLING-8655 - Changed the Annotation to be part of the Json Serialization --- pom.xml | 5 + .../DefaultExternalizePathProvider.java | 48 -- .../injectors/ExternalizePathInjector.java | 98 ---- .../DefaultExternalizePathProvider.java | 111 +++++ .../ExternalizePathProviderManager.java | 27 ++ ...ExternalizePathProviderManagerService.java | 60 +++ .../serializer/ExternalizePathSerializer.java | 209 +++++++++ .../ExternalizedPathInjectorTest.java | 150 ------ .../ExternalizedPathSerializerTest.java | 436 ++++++++++++++++++ 9 files changed, 848 insertions(+), 296 deletions(-) delete mode 100644 src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java delete mode 100644 src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java create mode 100644 src/main/java/org/apache/sling/models/impl/serializer/DefaultExternalizePathProvider.java create mode 100644 src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManager.java create mode 100644 src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathProviderManagerService.java create mode 100644 src/main/java/org/apache/sling/models/impl/serializer/ExternalizePathSerializer.java delete mode 100644 src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java create mode 100644 src/test/java/org/apache/sling/models/impl/serializer/ExternalizedPathSerializerTest.java diff --git a/pom.xml b/pom.xml index a663fd39..ec098653 100644 --- a/pom.xml +++ b/pom.xml @@ -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/injectors/DefaultExternalizePathProvider.java b/src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java deleted file mode 100644 index e5324804..00000000 --- a/src/main/java/org/apache/sling/models/impl/injectors/DefaultExternalizePathProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.injectors; - -import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; -import org.jetbrains.annotations.NotNull; -import org.osgi.framework.Constants; -import org.osgi.service.component.annotations.Component; - -/** 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 adaptable, String sourcePath) { - String answer = sourcePath; - if (adaptable instanceof Resource) { - ResourceResolver resourceResolver = ((Resource) adaptable).getResourceResolver(); - if (sourcePath != null && resourceResolver != null) { - answer = resourceResolver.map(sourcePath); - } - } - return answer; - } -} diff --git a/src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java b/src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java deleted file mode 100644 index 98830135..00000000 --- a/src/main/java/org/apache/sling/models/impl/injectors/ExternalizePathInjector.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.injectors; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.commons.osgi.Order; -import org.apache.sling.commons.osgi.RankedServices; -import org.apache.sling.models.annotations.ExternalizePath; -import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; -import org.apache.sling.models.spi.DisposalCallbackRegistry; -import org.apache.sling.models.spi.Injector; -import org.jetbrains.annotations.NotNull; -import org.osgi.framework.Constants; -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.lang.reflect.AnnotatedElement; -import java.lang.reflect.Type; -import java.util.Iterator; -import java.util.Map; - -/** - * Injector for a Model Property with the Annotation 'Externalize Path' - * which will change a Sling Path into its externalize form (shortening etc) - * - * The Externalize Path Provider is what will do the actual transformation and - * is pluggable. The component with the highest service ranking is selected here - * to provide the transformation. - */ -@Component( - property=Constants.SERVICE_RANKING+":Integer=1000", - service={ - Injector.class - } -) -public class ExternalizePathInjector - extends AbstractInjector - implements Injector -{ - 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 @NotNull String getName() { - return "externalize-path"; - } - - @Override - public Object getValue(@NotNull Object adaptable, String name, @NotNull Type type, @NotNull AnnotatedElement element, - @NotNull DisposalCallbackRegistry callbackRegistry) { - if (adaptable == ObjectUtils.NULL) { - return null; - } - if (element.isAnnotationPresent(ExternalizePath.class)) { - ValueMap properties = getValueMap(adaptable); - if (properties != ObjectUtils.NULL) { - String imagePath = properties.get(name, String.class); - if (imagePath != null) { - Iterator i = providers.iterator(); - if (i.hasNext()) { - ExternalizePathProvider provider = i.next(); - return provider.externalize(adaptable, imagePath); - } - } - } - } - return null; - } - -} 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/injectors/ExternalizedPathInjectorTest.java b/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java deleted file mode 100644 index 2004ec02..00000000 --- a/src/test/java/org/apache/sling/models/impl/injectors/ExternalizedPathInjectorTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.injectors; - -import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.models.annotations.ExternalizePath; -import org.apache.sling.models.annotations.injectorspecific.ExternalizePathProvider; -import org.apache.sling.models.spi.DisposalCallbackRegistry; -import org.jetbrains.annotations.NotNull; -import org.junit.Before; -import org.junit.Test; - -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.osgi.framework.Constants.SERVICE_ID; -import static org.osgi.framework.Constants.SERVICE_RANKING; - -public class ExternalizedPathInjectorTest { - - private ExternalizePathInjector injector; - private Resource adaptable; - private ValueMap valueMap; - private Type type; - private AnnotatedElement element; - private DisposalCallbackRegistry callbackRegistry; - private ResourceResolver resourceResolver; - - @Before - public void setup() { - injector = new ExternalizePathInjector(); - adaptable = mock(Resource.class); - valueMap = mock(ValueMap.class); - when(adaptable.adaptTo(eq(ValueMap.class))).thenReturn(valueMap); - type = String.class; - element = mock(AnnotatedElement.class); - when(element.isAnnotationPresent(eq(ExternalizePath.class))).thenReturn(true); - callbackRegistry = mock(DisposalCallbackRegistry.class); - resourceResolver = mock(ResourceResolver.class); - when(adaptable.getResourceResolver()).thenReturn(resourceResolver); - ExternalizePathProvider defaultProvider = new DefaultExternalizePathProvider(); - Map props = new HashMap<>(); - props.put(SERVICE_ID, 1L); - props.put(SERVICE_RANKING, 1); - injector.bindExternalizePathProvider(defaultProvider, props); - } - - @Test - public void testNoResolveInjection() { - String imagePath = "/content/test/image/test-image.jpg"; - String name = "imagePath"; - - when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); - when(resourceResolver.map(imagePath)).thenReturn(imagePath); - - Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); - assertEquals("No Mapping was expected", imagePath, value); - } - - @Test - public void testResolveInjection() { - String imagePath = "/content/test/image/test-image.jpg"; - String mappedImagePath = "/image/test-image.jpg"; - String name = "imagePath"; - - when(valueMap.get(eq(name), eq(String.class))).thenReturn(imagePath); - when(resourceResolver.map(imagePath)).thenReturn(mappedImagePath); - - Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); - assertEquals("Mapping was expected", mappedImagePath, value); - } - - @Test - public void testCustomProviderInjection() { - 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(valueMap.get(eq(name), eq(String.class))).thenReturn(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); - injector.bindExternalizePathProvider(provider1, props); - TestExternalizePathProvider provider3 = new TestExternalizePathProvider(from, to3); - props = new HashMap<>(); - props.put(SERVICE_ID, 1235L); - props.put(SERVICE_RANKING, 400); - injector.bindExternalizePathProvider(provider3, props); - TestExternalizePathProvider provider2 = new TestExternalizePathProvider(from, to2); - props = new HashMap<>(); - props.put(SERVICE_ID, 1236L); - props.put(SERVICE_RANKING, 200); - injector.bindExternalizePathProvider(provider2, props); - - Object value = injector.getValue(adaptable, name, type, element, callbackRegistry); - assertEquals("Wrong Provider was selected", mappedImagePath3, value); - } - - 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 adaptable, String sourcePath) { - String answer = sourcePath; - if(sourcePath.startsWith(from)) { - answer = to + sourcePath.substring(from.length()); - } - return answer; - } - } -} 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; + } + } +}