This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.testing.osgi-mock-2.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-osgi-mock.git
commit 3f5a1729ed9dcc5b10affea05320b3f0f9d91612 Author: Stefan Seifert <[email protected]> AuthorDate: Mon Nov 23 23:45:50 2015 +0000 SLING-5324 osgi-mock: Support OSGi R6 field-based reference bindings git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/mocks/osgi-mock@1715990 13f79535-47bb-0310-9956-ffa450edef68 --- .../sling/testing/mock/osgi/OsgiMetadataUtil.java | 108 +++++++++++++ .../sling/testing/mock/osgi/OsgiServiceUtil.java | 173 +++++++++++++++++++-- ...ockBundleContextDynamicReferncesOsgiR6Test.java | 169 ++++++++++++++++++++ .../testing/mock/osgi/OsgiServiceUtilTest.java | 56 +++++++ ...sling.testing.mock.osgi.OsgiServiceUtilTest.xml | 8 + 5 files changed, 500 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java index 7c3db48..85db326 100644 --- a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java +++ b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiMetadataUtil.java @@ -386,8 +386,11 @@ final class OsgiMetadataUtil { private final String interfaceType; private final ReferenceCardinality cardinality; private final ReferencePolicy policy; + private final ReferencePolicyOption policyOption; private final String bind; private final String unbind; + private final String field; + private final FieldCollectionType fieldCollectionType; private Reference(Class<?> clazz, Node node) { this.clazz = clazz; @@ -395,8 +398,11 @@ final class OsgiMetadataUtil { this.interfaceType = getAttributeValue(node, "interface"); this.cardinality = toCardinality(getAttributeValue(node, "cardinality")); this.policy = toPolicy(getAttributeValue(node, "policy")); + this.policyOption = toPolicyOption(getAttributeValue(node, "policy-option")); this.bind = getAttributeValue(node, "bind"); this.unbind = getAttributeValue(node, "unbind"); + this.field = getAttributeValue(node, "field"); + this.fieldCollectionType = toFieldCollectionType(getAttributeValue(node, "field-collection-type")); } public Class<?> getServiceClass() { @@ -411,14 +417,31 @@ final class OsgiMetadataUtil { return this.interfaceType; } + public Class getInterfaceTypeAsClass() { + try { + return Class.forName(getInterfaceType()); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Service reference type not found: " + getInterfaceType()); + } + } + public ReferenceCardinality getCardinality() { return this.cardinality; } + + public boolean isCardinalityMultiple() { + return this.cardinality == ReferenceCardinality.OPTIONAL_MULTIPLE + || this.cardinality == ReferenceCardinality.MANDATORY_MULTIPLE; + } public ReferencePolicy getPolicy() { return policy; } + public ReferencePolicyOption getPolicyOption() { + return policyOption; + } + public String getBind() { return this.bind; } @@ -427,6 +450,14 @@ final class OsgiMetadataUtil { return this.unbind; } + public String getField() { + return this.field; + } + + public FieldCollectionType getFieldCollectionType() { + return this.fieldCollectionType; + } + private static ReferenceCardinality toCardinality(String value) { for (ReferenceCardinality item : ReferenceCardinality.values()) { if (StringUtils.equals(item.getCardinalityString(), value)) { @@ -445,6 +476,24 @@ final class OsgiMetadataUtil { return ReferencePolicy.STATIC; } + private static ReferencePolicyOption toPolicyOption(String value) { + for (ReferencePolicyOption item : ReferencePolicyOption.values()) { + if (StringUtils.equalsIgnoreCase(item.name(), value)) { + return item; + } + } + return ReferencePolicyOption.RELUCTANT; + } + + private static FieldCollectionType toFieldCollectionType(String value) { + for (FieldCollectionType item : FieldCollectionType.values()) { + if (StringUtils.equalsIgnoreCase(item.name(), value)) { + return item; + } + } + return null; + } + } @@ -513,4 +562,63 @@ final class OsgiMetadataUtil { DYNAMIC; } + + /** + * Options for {@link Reference#policyOption()} property. + */ + enum ReferencePolicyOption { + + /** + * The reluctant policy option is the default policy option. + * When a new target service for a reference becomes available, + * references having the reluctant policy option for the static + * policy or the dynamic policy with a unary cardinality will + * ignore the new target service. References having the dynamic + * policy with a multiple cardinality will bind the new + * target service + */ + RELUCTANT, + + /** + * When a new target service for a reference becomes available, + * references having the greedy policy option will bind the new + * target service. + */ + GREEDY; + } + + /** + * Options for {@link Reference#policyOption()} property. + */ + enum FieldCollectionType { + + /** + * The bound service object. This is the default field collection type. + */ + SERVICE, + + /** + * A Service Reference for the bound service. + */ + REFERENCE, + + /** + * A Component Service Objects for the bound service. + */ + SERVICEOBJECTS, + + /** + * An unmodifiable Map containing the service properties of the bound service. + * This Map must implement Comparable. + */ + PROPERTIES, + + /** + * An unmodifiable Map.Entry whose key is an unmodifiable Map containing the + * service properties of the bound service, as above, and whose value is the + * bound service object. This Map.Entry must implement Comparable. + */ + TUPLE; + } + } diff --git a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtil.java b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtil.java index 84ccbf8..1f8eaa0 100644 --- a/src/main/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtil.java +++ b/src/main/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtil.java @@ -18,10 +18,12 @@ */ package org.apache.sling.testing.mock.osgi; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,6 +31,7 @@ import java.util.SortedSet; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.FieldCollectionType; import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.OsgiMetadata; import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.Reference; import org.apache.sling.testing.mock.osgi.OsgiMetadataUtil.ReferenceCardinality; @@ -262,6 +265,49 @@ final class OsgiServiceUtil { } } + private static Field getField(Class clazz, String fieldName, Class<?> type) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (StringUtils.equals(field.getName(), fieldName) && field.getType().equals(type)) { + return field; + } + } + // not found? check super classes + Class<?> superClass = clazz.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return getField(superClass, fieldName, type); + } + return null; + } + + private static Field getFieldWithAssignableType(Class clazz, String fieldName, Class<?> type) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (StringUtils.equals(field.getName(), fieldName) && field.getType().isAssignableFrom(type)) { + return field; + } + } + // not found? check super classes + Class<?> superClass = clazz.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return getFieldWithAssignableType(superClass, fieldName, type); + } + return null; + } + + private static void setField(Object target, Field field, Object value) { + try { + field.setAccessible(true); + field.set(target, value); + } catch (IllegalAccessException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } catch (IllegalArgumentException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } + } + /** * Simulate OSGi service dependency injection. Injects direct references and * multiple references. @@ -294,12 +340,7 @@ final class OsgiServiceUtil { Class<?> targetClass = target.getClass(); // get reference type - Class<?> type; - try { - type = Class.forName(reference.getInterfaceType()); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("Unable to instantiate reference type: " + reference.getInterfaceType(), ex); - } + Class<?> type = reference.getInterfaceTypeAsClass(); // get matching service references List<ServiceInfo> matchingServices = getMatchingServices(type, bundleContext); @@ -330,6 +371,13 @@ final class OsgiServiceUtil { // try to invoke bind method String methodName = bind ? reference.getBind() : reference.getUnbind(); + String fieldName = reference.getField(); + + if (StringUtils.isEmpty(methodName) && StringUtils.isEmpty(fieldName)) { + throw new RuntimeException("No bind/unbind method name or file name defined " + + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); + } + if (StringUtils.isNotEmpty(methodName)) { // 1. ServiceReference @@ -340,12 +388,7 @@ final class OsgiServiceUtil { } // 2. assignable from service instance - Class<?> interfaceType; - try { - interfaceType = Class.forName(reference.getInterfaceType()); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Service reference type not found: " + reference.getInterfaceType()); - } + Class<?> interfaceType = reference.getInterfaceTypeAsClass(); method = getMethodWithAssignableTypes(targetClass, methodName, new Class<?>[] { interfaceType }); if (method != null) { invokeMethod(target, method, new Object[] { serviceInfo.getServiceInstance() }); @@ -358,10 +401,112 @@ final class OsgiServiceUtil { invokeMethod(target, method, new Object[] { serviceInfo.getServiceInstance(), serviceInfo.getServiceConfig() }); return; } + + throw new RuntimeException((bind ? "Bind" : "Unbind") + " method with name " + methodName + " not found " + + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); + } + + // in OSGi declarative services 1.3 there are no bind/unbind methods - modify the field directly + else if (StringUtils.isNotEmpty(fieldName)) { + + // check for field with list/collection reference + if (reference.isCardinalityMultiple()) { + switch (reference.getFieldCollectionType()) { + case SERVICE: + case REFERENCE: + Object item = serviceInfo.getServiceInstance(); + if (reference.getFieldCollectionType() == FieldCollectionType.REFERENCE) { + item = serviceInfo.getServiceReference(); + } + // 1. collection + Field field = getFieldWithAssignableType(targetClass, fieldName, Collection.class); + if (field != null) { + if (bind) { + addToCollection(target, field, item); + } + else { + removeFromCollection(target, field, item); + } + return; + } + + // 2. list + field = getField(targetClass, fieldName, List.class); + if (field != null) { + if (bind) { + addToCollection(target, field, item); + } + else { + removeFromCollection(target, field, item); + } + return; + } + break; + default: + throw new RuntimeException("Field collection type '" + reference.getFieldCollectionType() + "' not supported " + + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); + } + } + + // check for single field reference + else { + // 1. assignable from service instance + Class<?> interfaceType = reference.getInterfaceTypeAsClass(); + Field field = getFieldWithAssignableType(targetClass, fieldName, interfaceType); + if (field != null) { + setField(target, field, bind ? serviceInfo.getServiceInstance() : null); + return; + } + + // 2. ServiceReference + field = getField(targetClass, fieldName, ServiceReference.class); + if (field != null) { + setField(target, field, bind ? serviceInfo.getServiceReference() : null); + return; + } + } } - throw new RuntimeException((bind ? "Bind" : "Unbind") + " method with name " + methodName + " not found " - + "for reference '" + reference.getName() + "' for class " + targetClass.getName()); + } + + @SuppressWarnings("unchecked") + private static void addToCollection(Object target, Field field, Object item) { + try { + field.setAccessible(true); + Collection<Object> collection = (Collection<Object>)field.get(target); + if (collection == null) { + collection = new ArrayList<>(); + } + collection.add(item); + field.set(target, collection); + + } catch (IllegalAccessException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } catch (IllegalArgumentException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } + } + + @SuppressWarnings("unchecked") + private static void removeFromCollection(Object target, Field field, Object item) { + try { + field.setAccessible(true); + Collection<Object> collection = (Collection<Object>)field.get(target); + if (collection == null) { + return; + } + collection.remove(item); + field.set(target, collection); + + } catch (IllegalAccessException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } catch (IllegalArgumentException ex) { + throw new RuntimeException("Unable to set field '" + field.getName() + "' for class " + + target.getClass().getName(), ex); + } } /** diff --git a/src/test/java/org/apache/sling/testing/mock/osgi/MockBundleContextDynamicReferncesOsgiR6Test.java b/src/test/java/org/apache/sling/testing/mock/osgi/MockBundleContextDynamicReferncesOsgiR6Test.java new file mode 100644 index 0000000..0af80e9 --- /dev/null +++ b/src/test/java/org/apache/sling/testing/mock/osgi/MockBundleContextDynamicReferncesOsgiR6Test.java @@ -0,0 +1,169 @@ +/* + * 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.testing.mock.osgi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.Service3OsgiR6; +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.ServiceInterface1; +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.ServiceInterface1Optional; +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.ServiceInterface2; +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.ServiceInterface3; +import org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.ServiceSuperInterface3; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +import com.google.common.collect.ImmutableSet; + +@RunWith(MockitoJUnitRunner.class) +public class MockBundleContextDynamicReferncesOsgiR6Test { + + private BundleContext bundleContext; + private Service3OsgiR6 service; + private ServiceRegistration reg1a; + private ServiceRegistration reg2a; + + @Mock + private ServiceInterface1 dependency1a; + @Mock + private ServiceInterface1 dependency1b; + @Mock + private ServiceInterface1Optional dependency1aOptional; + @Mock + private ServiceInterface1Optional dependency1bOptional; + @Mock + private ServiceInterface2 dependency2a; + @Mock + private ServiceInterface2 dependency2b; + @Mock + private ServiceSuperInterface3 dependency3a; + @Mock + private ServiceSuperInterface3 dependency3b; + + @Before + public void setUp() { + bundleContext = MockOsgi.newBundleContext(); + + // setup service instance with only minimum mandatory references + reg1a = bundleContext.registerService(ServiceInterface1.class.getName(), dependency1a, null); + reg2a = bundleContext.registerService(ServiceInterface2.class.getName(), dependency2a, null); + + service = new Service3OsgiR6(); + MockOsgi.injectServices(service, bundleContext); + MockOsgi.activate(service, bundleContext); + bundleContext.registerService(Service3OsgiR6.class.getName(), service, null); + + assertDependency1(dependency1a); + assertDependency1Optional(null); + assertDependencies2(dependency2a); + assertDependencies3(); + } + + @Test + public void testAddRemoveOptionalUnaryService() { + ServiceRegistration reg1aOptional = bundleContext.registerService(ServiceInterface1Optional.class.getName(), dependency1aOptional, null); + assertDependency1Optional(dependency1aOptional); + + reg1aOptional.unregister(); + assertDependency1Optional(null); + } + + public void testAddOptionalUnaryService_TooMany() { + bundleContext.registerService(ServiceInterface1Optional.class.getName(), dependency1aOptional, null); + assertDependency1Optional(dependency1aOptional); + + // in real OSGi this should fail - but this is not covered by the current implementation. so test the real implementation here. + bundleContext.registerService(ServiceInterface1Optional.class.getName(), dependency1bOptional, null); + assertDependency1Optional(dependency1bOptional); + } + + @Test(expected = ReferenceViolationException.class) + public void testAddMandatoryUnaryService_TooMany() { + bundleContext.registerService(ServiceInterface1.class.getName(), dependency1b, null); + } + + @Test(expected = ReferenceViolationException.class) + public void testRemoveMandatoryUnaryService_TooMany() { + reg1a.unregister(); + } + + @Test + public void testAddRemoveOptionalMultipleService() { + ServiceRegistration reg3a = bundleContext.registerService(ServiceInterface3.class.getName(), dependency3a, null); + assertDependencies3(dependency3a); + + ServiceRegistration reg3b = bundleContext.registerService(ServiceInterface3.class.getName(), dependency3b, null); + assertDependencies3(dependency3a, dependency3b); + + reg3a.unregister(); + assertDependencies3(dependency3b); + + reg3b.unregister(); + assertDependencies3(); + } + + @Test + public void testAddRemoveMandatoryMultipleService() { + ServiceRegistration reg2b = bundleContext.registerService(ServiceInterface2.class.getName(), dependency2b, null); + assertDependencies2(dependency2a, dependency2b); + + reg2b.unregister(); + assertDependencies2(dependency2a); + + // in real OSGi this should fail - but this is not covered by the current implementation. so test the real implementation here. + reg2a.unregister(); + assertDependencies2(); + } + + private void assertDependency1(ServiceInterface1 instance) { + if (instance == null) { + assertNull(service.getReference1()); + } + else { + assertSame(instance, service.getReference1()); + } + } + + private void assertDependency1Optional(ServiceInterface1Optional instance) { + if (instance == null) { + assertNull(service.getReference1Optional()); + } + else { + assertSame(instance, service.getReference1Optional()); + } + } + + private void assertDependencies2(ServiceInterface2... instances) { + assertEquals(ImmutableSet.<ServiceInterface2>copyOf(instances), + ImmutableSet.<ServiceInterface2>copyOf(service.getReferences2())); + } + + private void assertDependencies3(ServiceSuperInterface3... instances) { + assertEquals(ImmutableSet.<ServiceSuperInterface3>copyOf(instances), + ImmutableSet.<ServiceSuperInterface3>copyOf(service.getReferences3())); + } + +} diff --git a/src/test/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtilTest.java b/src/test/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtilTest.java index 170957f..9135bcf 100644 --- a/src/test/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtilTest.java +++ b/src/test/java/org/apache/sling/testing/mock/osgi/OsgiServiceUtilTest.java @@ -335,6 +335,62 @@ public class OsgiServiceUtilTest { } + public static class Service3OsgiR6 { + + private ServiceInterface1 reference1; + private ServiceInterface1Optional reference1Optional; + private List<ServiceReference> references2 = new ArrayList<ServiceReference>(); + private List<ServiceSuperInterface3> references3 = new ArrayList<ServiceSuperInterface3>(); + + private ComponentContext componentContext; + private Map<String, Object> config; + + @Activate + private void activate(ComponentContext ctx) { + this.componentContext = ctx; + this.config = MapUtil.toMap(ctx.getProperties()); + } + + @Deactivate + private void deactivate(ComponentContext ctx) { + this.componentContext = null; + } + + @Modified + private void modified(Map<String,Object> newConfig) { + this.config = newConfig; + } + + public ServiceInterface1 getReference1() { + return this.reference1; + } + + public ServiceInterface1Optional getReference1Optional() { + return this.reference1Optional; + } + + public List<ServiceInterface2> getReferences2() { + List<ServiceInterface2> services = new ArrayList<ServiceInterface2>(); + for (ServiceReference<?> serviceReference : references2) { + services.add((ServiceInterface2)componentContext.getBundleContext().getService(serviceReference)); + } + return services; + } + + public List<ServiceSuperInterface3> getReferences3() { + return this.references3; + } + + public ComponentContext getComponentContext() { + return this.componentContext; + } + + public Map<String, Object> getConfig() { + return config; + } + + } + @Component @Reference(referenceInterface = ServiceInterface1.class, name = "customName", bind = "customBind", unbind = "customUnbind") public static class Service4 { diff --git a/src/test/resources/OSGI-INF/org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.xml b/src/test/resources/OSGI-INF/org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.xml index a9d3623..94602cc 100644 --- a/src/test/resources/OSGI-INF/org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.xml +++ b/src/test/resources/OSGI-INF/org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest.xml @@ -43,6 +43,14 @@ <reference name="reference2" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface2" cardinality="1..n" policy="dynamic" bind="bindReference2" unbind="unbindReference2"/> <reference name="reference3" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface3" cardinality="0..n" policy="dynamic" bind="bindReference3" unbind="unbindReference3"/> </scr:component> + <scr:component name="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service3OsgiR6" activate="activate" deactivate="deactivate" modified="modified"> + <implementation class="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service3OsgiR6"/> + <property name="service.pid" value="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service3OsgiR6"/> + <reference name="reference1" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface1" cardinality="1..1" policy="dynamic" field="reference1"/> + <reference name="reference1Optional" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface1Optional" cardinality="0..1" policy="dynamic" field="reference1Optional"/> + <reference name="reference2" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface2" cardinality="1..n" policy="dynamic" field="references2" field-collection-type="reference"/> + <reference name="reference3" interface="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$ServiceInterface3" cardinality="0..n" policy="dynamic" field="references3" field-collection-type="service"/> + </scr:component> <scr:component name="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service4_other_name"> <implementation class="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service4"/> <property name="service.pid" value="org.apache.sling.testing.mock.osgi.OsgiServiceUtilTest$Service4"/> -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
