You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by je...@apache.org on 2018/02/06 22:36:42 UTC

[geode] branch develop updated: GEODE-4389: Ensure that region template creation does not require callback classes on the locator (#1390)

This is an automated email from the ASF dual-hosted git repository.

jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new ff1b8c7  GEODE-4389: Ensure that region template creation does not require callback classes on the locator (#1390)
ff1b8c7 is described below

commit ff1b8c7bdb8b02b015b6e233cadc6ae68cc33a54
Author: Jens Deppe <jd...@pivotal.io>
AuthorDate: Tue Feb 6 14:36:39 2018 -0800

    GEODE-4389: Ensure that region template creation does not require callback classes on the locator (#1390)
---
 .../internal/cli/commands/CreateRegionCommand.java |  56 ++++++--
 .../functions/FetchRegionAttributesFunction.java   |  61 ++++++--
 .../cli/functions/RegionAttributesWrapper.java     | 108 ++++++++++++++
 .../sanctioned-geode-core-serializables.txt        |   1 +
 .../cli/commands/CreateRegionCommandDUnitTest.java | 155 +++++++++++++++++++++
 5 files changed, 356 insertions(+), 25 deletions(-)

diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommand.java
index d71e8de..012da67 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommand.java
@@ -53,6 +53,7 @@ import org.apache.geode.management.internal.cli.LogWrapper;
 import org.apache.geode.management.internal.cli.domain.ClassName;
 import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
 import org.apache.geode.management.internal.cli.functions.FetchRegionAttributesFunction;
+import org.apache.geode.management.internal.cli.functions.RegionAttributesWrapper;
 import org.apache.geode.management.internal.cli.functions.RegionCreateFunction;
 import org.apache.geode.management.internal.cli.functions.RegionFunctionArgs;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
@@ -203,8 +204,6 @@ public class CreateRegionCommand implements GfshCommand {
     RegionFunctionArgs functionArgs = new RegionFunctionArgs();
     functionArgs.setRegionPath(regionPath);
     functionArgs.setIfNotExists(ifNotExists || skipIfExists);
-    functionArgs.setKeyConstraint(keyConstraint);
-    functionArgs.setValueConstraint(valueConstraint);
     functionArgs.setStatisticsEnabled(statisticsEnabled);
     functionArgs.setEntryExpirationIdleTime(entryExpirationIdleTime, entryExpirationIdleTimeAction);
     functionArgs.setEntryExpirationTTL(entryExpirationTTL, entryExpirationTTLAction);
@@ -238,22 +237,22 @@ public class CreateRegionCommand implements GfshCommand {
                 regionPath));
       }
       functionArgs.setRegionShortcut(regionShortcut);
-    } else if (templateRegion != null) {
+    } else { // templateRegion != null
       if (!regionExists(cache, templateRegion)) {
         return ResultBuilder.createUserErrorResult(CliStrings.format(
             CliStrings.CREATE_REGION__MSG__SPECIFY_VALID_REGION_PATH_FOR_0_REGIONPATH_1_NOT_FOUND,
             CliStrings.CREATE_REGION__USEATTRIBUTESFROM, templateRegion));
       }
 
-      regionAttributes = getRegionAttributes(cache, templateRegion);
+      RegionAttributesWrapper<?, ?> wrappedAttributes = getRegionAttributes(cache, templateRegion);
 
-      if (regionAttributes == null) {
+      if (wrappedAttributes == null) {
         return ResultBuilder.createGemFireErrorResult(CliStrings.format(
             CliStrings.CREATE_REGION__MSG__COULD_NOT_RETRIEVE_REGION_ATTRS_FOR_PATH_0_VERIFY_REGION_EXISTS,
             templateRegion));
       }
 
-      if (regionAttributes.getPartitionAttributes() == null
+      if (wrappedAttributes.getRegionAttributes().getPartitionAttributes() == null
           && functionArgs.hasPartitionAttributes()) {
         return ResultBuilder.createUserErrorResult(CliStrings.format(
             CliStrings.CREATE_REGION__MSG__OPTION_0_CAN_BE_USED_ONLY_FOR_PARTITIONEDREGION,
@@ -262,13 +261,42 @@ public class CreateRegionCommand implements GfshCommand {
                 templateRegion));
       }
       functionArgs.setTemplateRegion(templateRegion);
-      functionArgs.setRegionAttributes(regionAttributes);
+
+      // These attributes will have the actual callback fields (if previously present) nulled out.
+      functionArgs.setRegionAttributes(wrappedAttributes.getRegionAttributes());
+
+      functionArgs
+          .setCacheListeners(wrappedAttributes.getCacheListenerClasses().toArray(new ClassName[0]));
+      functionArgs.setCacheWriter(wrappedAttributes.getCacheWriterClass());
+      functionArgs.setCacheLoader(wrappedAttributes.getCacheLoaderClass());
+      functionArgs.setCompressor(wrappedAttributes.getCompressorClass());
+      functionArgs.setKeyConstraint(wrappedAttributes.getKeyConstraintClass());
+      functionArgs.setValueConstraint(wrappedAttributes.getValueConstraintClass());
+    }
+
+    if (cacheListener != null) {
+      functionArgs.setCacheListeners(cacheListener);
     }
 
-    functionArgs.setCacheListeners(cacheListener);
-    functionArgs.setCacheLoader(cacheLoader);
-    functionArgs.setCacheWriter(cacheWriter);
-    functionArgs.setCompressor(compressor);
+    if (cacheLoader != null) {
+      functionArgs.setCacheLoader(cacheLoader);
+    }
+
+    if (cacheWriter != null) {
+      functionArgs.setCacheWriter(cacheWriter);
+    }
+
+    if (compressor != null) {
+      functionArgs.setCompressor(compressor);
+    }
+
+    if (keyConstraint != null) {
+      functionArgs.setKeyConstraint(keyConstraint);
+    }
+
+    if (valueConstraint != null) {
+      functionArgs.setValueConstraint(valueConstraint);
+    }
 
     DistributedSystemMXBean dsMBean = getDSMBean(cache);
     // validating colocation
@@ -388,13 +416,13 @@ public class CreateRegionCommand implements GfshCommand {
     return false;
   }
 
-  RegionAttributes getRegionAttributes(InternalCache cache, String regionPath) {
+  RegionAttributesWrapper getRegionAttributes(InternalCache cache, String regionPath) {
     if (!isClusterWideSameConfig(cache, regionPath)) {
       throw new IllegalStateException(CliStrings.format(
           CliStrings.CREATE_REGION__MSG__USE_ATTRIBUTES_FORM_REGIONS_EXISTS_BUT_DIFFERENT_SCOPE_OR_DATAPOLICY_USE_DESCRIBE_REGION_FOR_0,
           regionPath));
     }
-    RegionAttributes attributes = null;
+    RegionAttributesWrapper attributes = null;
 
     // First check whether the region exists on a this manager, if yes then no
     // need to use FetchRegionAttributesFunction to fetch RegionAttributes
@@ -425,7 +453,7 @@ public class CreateRegionCommand implements GfshCommand {
                   regionPath, th.getMessage()));
             } else { // has to be RegionAttributes
               @SuppressWarnings("unchecked") // to avoid warning :(
-              RegionAttributes regAttr = ((RegionAttributes) object);
+              RegionAttributesWrapper regAttr = ((RegionAttributesWrapper) object);
               if (attributes == null) {
                 attributes = regAttr;
                 break;
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/FetchRegionAttributesFunction.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/FetchRegionAttributesFunction.java
index 4fd7bb9..584f558 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/FetchRegionAttributesFunction.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/FetchRegionAttributesFunction.java
@@ -14,19 +14,23 @@
  */
 package org.apache.geode.management.internal.cli.functions;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.logging.log4j.Logger;
 
 import org.apache.geode.cache.AttributesFactory;
 import org.apache.geode.cache.Cache;
-import org.apache.geode.cache.Region;
-import org.apache.geode.cache.RegionAttributes;
+import org.apache.geode.cache.CacheListener;
 import org.apache.geode.cache.execute.Function;
 import org.apache.geode.cache.execute.FunctionContext;
 import org.apache.geode.internal.InternalEntity;
+import org.apache.geode.internal.cache.AbstractRegion;
 import org.apache.geode.internal.logging.LogService;
+import org.apache.geode.management.internal.cli.domain.ClassName;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
 import org.apache.geode.management.internal.security.ResourcePermissions;
 import org.apache.geode.security.ResourcePermission;
@@ -58,8 +62,8 @@ public class FetchRegionAttributesFunction implements Function, InternalEntity {
         throw new IllegalArgumentException(
             CliStrings.CREATE_REGION__MSG__SPECIFY_VALID_REGION_PATH);
       }
-      RegionAttributes<?, ?> result = getRegionAttributes(cache, regionPath);
-      context.getResultSender().lastResult(result);
+      RegionAttributesWrapper regionAttributesWrapper = getRegionAttributes(cache, regionPath);
+      context.getResultSender().lastResult(regionAttributesWrapper);
     } catch (IllegalArgumentException e) {
       if (logger.isDebugEnabled()) {
         logger.debug(e.getMessage(), e);
@@ -69,19 +73,54 @@ public class FetchRegionAttributesFunction implements Function, InternalEntity {
   }
 
   @SuppressWarnings("deprecation")
-  public static <K, V> RegionAttributes<K, V> getRegionAttributes(Cache cache, String regionPath) {
-    Region<K, V> foundRegion = cache.getRegion(regionPath);
+  public static RegionAttributesWrapper getRegionAttributes(Cache cache, String regionPath) {
+    AbstractRegion foundRegion = (AbstractRegion) cache.getRegion(regionPath);
 
     if (foundRegion == null) {
       throw new IllegalArgumentException(CliStrings.format(
           CliStrings.CREATE_REGION__MSG__SPECIFY_VALID_REGION_PATH_FOR_0_REGIONPATH_1_NOT_FOUND,
-          new Object[] {CliStrings.CREATE_REGION__USEATTRIBUTESFROM, regionPath}));
+          CliStrings.CREATE_REGION__USEATTRIBUTESFROM, regionPath));
     }
 
-    // Using AttributesFactory to get the serializable RegionAttributes
-    // Is there a better way?
-    AttributesFactory<K, V> afactory = new AttributesFactory<K, V>(foundRegion.getAttributes());
-    return afactory.create();
+    AttributesFactory afactory = new AttributesFactory(foundRegion.getAttributes());
+    RegionAttributesWrapper result = new RegionAttributesWrapper();
+
+    CacheListener[] cacheListeners = foundRegion.getCacheListeners();
+    List existingCacheListeners = Arrays.stream(cacheListeners)
+        .map((c) -> new ClassName(c.getClass().getName())).collect(Collectors.toList());
+
+    result.setCacheListenerClasses(existingCacheListeners);
+    afactory.initCacheListeners(null);
+
+    if (foundRegion.getCacheLoader() != null) {
+      result
+          .setCacheLoaderClass(new ClassName<>(foundRegion.getCacheLoader().getClass().getName()));
+      afactory.setCacheLoader(null);
+    }
+
+    if (foundRegion.getCacheWriter() != null) {
+      result
+          .setCacheWriterClass(new ClassName<>(foundRegion.getCacheWriter().getClass().getName()));
+      afactory.setCacheWriter(null);
+    }
+
+    if (foundRegion.getCompressor() != null) {
+      result.setCompressorClass(foundRegion.getCompressor().getClass().getName());
+      afactory.setCompressor(null);
+    }
+
+    if (foundRegion.getKeyConstraint() != null) {
+      result.setKeyConstraintClass(foundRegion.getKeyConstraint().getName());
+      afactory.setKeyConstraint(null);
+    }
+
+    if (foundRegion.getValueConstraint() != null) {
+      result.setValueConstraintClass(foundRegion.getValueConstraint().getName());
+      afactory.setValueConstraint(null);
+    }
+
+    result.setRegionAttributes(afactory.create());
+    return result;
   }
 
   @Override
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/RegionAttributesWrapper.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/RegionAttributesWrapper.java
new file mode 100644
index 0000000..b43b4ff
--- /dev/null
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/RegionAttributesWrapper.java
@@ -0,0 +1,108 @@
+/*
+ * 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.geode.management.internal.cli.functions;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.apache.geode.cache.CacheListener;
+import org.apache.geode.cache.CacheLoader;
+import org.apache.geode.cache.CacheWriter;
+import org.apache.geode.cache.RegionAttributes;
+import org.apache.geode.management.internal.cli.domain.ClassName;
+
+/**
+ * This class extracts serializable components from RegionAttributes and represents them simply as
+ * strings. The relevant RegionAttributes fields are nulled out so that Serialzation can occur where
+ * the relevant callbacks do not have classes defined. For example when retrieving a RegionAttribute
+ * from a server to a locator.
+ */
+public class RegionAttributesWrapper<K, V> implements Serializable {
+
+  private static final long serialVersionUID = -5517424520268271436L;
+
+  private RegionAttributes<K, V> regionAttributes;
+
+  private List<ClassName<CacheListener>> cacheListenerClasses;
+
+  private ClassName<CacheLoader> cacheLoaderClass;
+
+  private ClassName<CacheWriter> cacheWriterClass;
+
+  private String compressorClass;
+
+  private String keyConstraintClass;
+
+  private String valueConstraintClass;
+
+  public RegionAttributesWrapper() {}
+
+  public RegionAttributes<K, V> getRegionAttributes() {
+    return regionAttributes;
+  }
+
+  public void setRegionAttributes(RegionAttributes<K, V> regionAttributes) {
+    this.regionAttributes = regionAttributes;
+  }
+
+  public List<ClassName<CacheListener>> getCacheListenerClasses() {
+    return cacheListenerClasses;
+  }
+
+  public void setCacheListenerClasses(List<ClassName<CacheListener>> cacheListenerClasses) {
+    this.cacheListenerClasses = cacheListenerClasses;
+  }
+
+  public ClassName<CacheLoader> getCacheLoaderClass() {
+    return cacheLoaderClass;
+  }
+
+  public void setCacheLoaderClass(ClassName<CacheLoader> cacheLoaderClass) {
+    this.cacheLoaderClass = cacheLoaderClass;
+  }
+
+  public ClassName<CacheWriter> getCacheWriterClass() {
+    return cacheWriterClass;
+  }
+
+  public void setCacheWriterClass(ClassName<CacheWriter> cacheWriterClass) {
+    this.cacheWriterClass = cacheWriterClass;
+  }
+
+  public String getCompressorClass() {
+    return compressorClass;
+  }
+
+  public void setCompressorClass(String compressorClass) {
+    this.compressorClass = compressorClass;
+  }
+
+  public String getKeyConstraintClass() {
+    return keyConstraintClass;
+  }
+
+  public void setKeyConstraintClass(String keyConstraintClass) {
+    this.keyConstraintClass = keyConstraintClass;
+  }
+
+  public String getValueConstraintClass() {
+    return valueConstraintClass;
+  }
+
+  public void setValueConstraintClass(String valueConstraintClass) {
+    this.valueConstraintClass = valueConstraintClass;
+  }
+}
diff --git a/geode-core/src/main/resources/org/apache/geode/internal/sanctioned-geode-core-serializables.txt b/geode-core/src/main/resources/org/apache/geode/internal/sanctioned-geode-core-serializables.txt
index 313c3db..d4107e3 100644
--- a/geode-core/src/main/resources/org/apache/geode/internal/sanctioned-geode-core-serializables.txt
+++ b/geode-core/src/main/resources/org/apache/geode/internal/sanctioned-geode-core-serializables.txt
@@ -555,6 +555,7 @@ org/apache/geode/management/internal/cli/functions/NetstatFunction$NetstatFuncti
 org/apache/geode/management/internal/cli/functions/NetstatFunction$NetstatFunctionResult,true,1,compressedBytes:org/apache/geode/management/internal/cli/CliUtil$DeflaterInflaterData,headerInfo:java/lang/String,host:java/lang/String
 org/apache/geode/management/internal/cli/functions/RebalanceFunction,true,1
 org/apache/geode/management/internal/cli/functions/RegionAlterFunction,true,-4846425364943216425
+org/apache/geode/management/internal/cli/functions/RegionAttributesWrapper,true,-5517424520268271436,cacheListenerClasses:java/util/List,cacheLoaderClass:org/apache/geode/management/internal/cli/domain/ClassName,cacheWriterClass:org/apache/geode/management/internal/cli/domain/ClassName,compressorClass:java/lang/String,keyConstraintClass:java/lang/String,regionAttributes:org/apache/geode/cache/RegionAttributes,valueConstraintClass:java/lang/String
 org/apache/geode/management/internal/cli/functions/RegionCreateFunction,true,8746830191680509335
 org/apache/geode/management/internal/cli/functions/RegionDestroyFunction,true,9172773671865750685
 org/apache/geode/management/internal/cli/functions/RegionFunctionArgs,true,2204943186081037302,asyncEventQueueIds:java/util/Set,cacheListeners:java/util/Set,cacheLoader:org/apache/geode/management/internal/cli/domain/ClassName,cacheWriter:org/apache/geode/management/internal/cli/domain/ClassName,cloningEnabled:java/lang/Boolean,compressor:java/lang/String,concurrencyChecksEnabled:java/lang/Boolean,concurrencyLevel:java/lang/Integer,diskStore:java/lang/String,diskSynchronous:java/lang/Boo [...]
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandDUnitTest.java
index 66eccd8..02438c3 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/CreateRegionCommandDUnitTest.java
@@ -181,4 +181,159 @@ public class CreateRegionCommandDUnitTest {
     gfsh.executeAndAssertThat("destroy region --name=/COPY").statusIsSuccess();
     gfsh.executeAndAssertThat("destroy region --name=/TEMPLATE").statusIsSuccess();
   }
+
+  @Test
+  public void ensureOverridingCallbacksFromTemplateDoNotRequireClassesOnLocator() throws Exception {
+    final File prJarFile = new File(tmpDir.getRoot(), "myCacheListener.jar");
+    new JarBuilder().buildJar(prJarFile, getUniversalClassCode("MyCallback"),
+        getUniversalClassCode("MyNewCallback"));
+    gfsh.executeAndAssertThat("deploy --jar=" + prJarFile.getAbsolutePath()).statusIsSuccess();
+
+    String myCallback = "io.pivotal.MyCallback";
+    gfsh.executeAndAssertThat(
+        String
+            .format(
+                "create region --name=/TEMPLATE --type=PARTITION_REDUNDANT "
+                    + "--cache-listener=%1$s " + "--cache-loader=%1$s " + "--cache-writer=%1$s "
+                    + "--compressor=%1$s " + "--key-constraint=%1$s " + "--value-constraint=%1$s",
+                myCallback))
+        .statusIsSuccess();
+
+    String myNewCallback = "io.pivotal.MyNewCallback";
+    gfsh.executeAndAssertThat(String
+        .format("create region --name=/COPY --template-region=/TEMPLATE " + "--cache-listener=%1$s "
+            + "--cache-loader=%1$s " + "--cache-writer=%1$s " + "--compressor=%1$s "
+            + "--key-constraint=%1$s " + "--value-constraint=%1$s", myNewCallback))
+        .statusIsSuccess();
+
+    server1.getVM().invoke(() -> {
+      Region copy = ClusterStartupRule.getCache().getRegion("/COPY");
+
+      assertThat(Arrays.stream(copy.getAttributes().getCacheListeners())
+          .map(c -> c.getClass().getName()).collect(Collectors.toSet()))
+              .containsExactly(myNewCallback);
+
+      assertThat(copy.getAttributes().getCacheLoader().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getCacheWriter().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getCompressor().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getKeyConstraint().getName()).isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getValueConstraint().getName()).isEqualTo(myNewCallback);
+    });
+
+    gfsh.executeAndAssertThat("destroy region --name=/COPY").statusIsSuccess();
+    gfsh.executeAndAssertThat("destroy region --name=/TEMPLATE").statusIsSuccess();
+  }
+
+  @Test
+  public void ensureNewCallbacksFromTemplateDoNotRequireClassesOnLocator() throws Exception {
+    final File prJarFile = new File(tmpDir.getRoot(), "myCacheListener.jar");
+    new JarBuilder().buildJar(prJarFile, getUniversalClassCode("MyNewCallback"));
+    gfsh.executeAndAssertThat("deploy --jar=" + prJarFile.getAbsolutePath()).statusIsSuccess();
+
+    gfsh.executeAndAssertThat("create region --name=/TEMPLATE --type=PARTITION_REDUNDANT")
+        .statusIsSuccess();
+
+    String myNewCallback = "io.pivotal.MyNewCallback";
+    gfsh.executeAndAssertThat(String
+        .format("create region --name=/COPY --template-region=/TEMPLATE " + "--cache-listener=%1$s "
+            + "--cache-loader=%1$s " + "--cache-writer=%1$s " + "--compressor=%1$s "
+            + "--key-constraint=%1$s " + "--value-constraint=%1$s", myNewCallback))
+        .statusIsSuccess();
+
+    server1.getVM().invoke(() -> {
+      Region copy = ClusterStartupRule.getCache().getRegion("/COPY");
+
+      assertThat(Arrays.stream(copy.getAttributes().getCacheListeners())
+          .map(c -> c.getClass().getName()).collect(Collectors.toSet()))
+              .containsExactly(myNewCallback);
+
+      assertThat(copy.getAttributes().getCacheLoader().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getCacheWriter().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getCompressor().getClass().getName())
+          .isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getKeyConstraint().getName()).isEqualTo(myNewCallback);
+
+      assertThat(copy.getAttributes().getValueConstraint().getName()).isEqualTo(myNewCallback);
+    });
+
+    gfsh.executeAndAssertThat("destroy region --name=/COPY").statusIsSuccess();
+    gfsh.executeAndAssertThat("destroy region --name=/TEMPLATE").statusIsSuccess();
+  }
+
+  @Test
+  public void ensureOriginalCallbacksFromTemplateDoNotRequireClassesOnLocator() throws Exception {
+    final File prJarFile = new File(tmpDir.getRoot(), "myCacheListener.jar");
+    new JarBuilder().buildJar(prJarFile, getUniversalClassCode("MyCallback"));
+    gfsh.executeAndAssertThat("deploy --jar=" + prJarFile.getAbsolutePath()).statusIsSuccess();
+
+    String myCallback = "io.pivotal.MyCallback";
+    gfsh.executeAndAssertThat(
+        String
+            .format(
+                "create region --name=/TEMPLATE --type=PARTITION_REDUNDANT "
+                    + "--cache-listener=%1$s " + "--cache-loader=%1$s " + "--cache-writer=%1$s "
+                    + "--compressor=%1$s " + "--key-constraint=%1$s " + "--value-constraint=%1$s",
+                myCallback))
+        .statusIsSuccess();
+
+    gfsh.executeAndAssertThat("create region --name=/COPY --template-region=/TEMPLATE")
+        .statusIsSuccess();
+
+    server1.getVM().invoke(() -> {
+      Region copy = ClusterStartupRule.getCache().getRegion("/COPY");
+
+      assertThat(Arrays.stream(copy.getAttributes().getCacheListeners())
+          .map(c -> c.getClass().getName()).collect(Collectors.toSet()))
+              .containsExactly(myCallback);
+
+      assertThat(copy.getAttributes().getCacheLoader().getClass().getName()).isEqualTo(myCallback);
+
+      assertThat(copy.getAttributes().getCacheWriter().getClass().getName()).isEqualTo(myCallback);
+
+      assertThat(copy.getAttributes().getCompressor().getClass().getName()).isEqualTo(myCallback);
+
+      assertThat(copy.getAttributes().getKeyConstraint().getName()).isEqualTo(myCallback);
+
+      assertThat(copy.getAttributes().getValueConstraint().getName()).isEqualTo(myCallback);
+    });
+
+    gfsh.executeAndAssertThat("destroy region --name=/COPY").statusIsSuccess();
+    gfsh.executeAndAssertThat("destroy region --name=/TEMPLATE").statusIsSuccess();
+  }
+
+  private String getUniversalClassCode(String classname) {
+    String code = "package io.pivotal;" + "import org.apache.geode.cache.CacheLoader;"
+        + "import org.apache.geode.cache.CacheLoaderException;"
+        + "import org.apache.geode.cache.CacheWriter;"
+        + "import org.apache.geode.cache.CacheWriterException;"
+        + "import org.apache.geode.cache.EntryEvent;"
+        + "import org.apache.geode.cache.LoaderHelper;"
+        + "import org.apache.geode.cache.RegionEvent;"
+        + "import org.apache.geode.cache.util.CacheListenerAdapter;"
+        + "import org.apache.geode.compression.Compressor;" + "public class " + classname
+        + " extends CacheListenerAdapter "
+        + "implements CacheWriter, CacheLoader, Compressor, java.io.Serializable {"
+        + "public void beforeUpdate(EntryEvent event) throws CacheWriterException {}"
+        + "public void beforeCreate(EntryEvent event) throws CacheWriterException {}"
+        + "public void beforeDestroy(EntryEvent event) throws CacheWriterException {}"
+        + "public void beforeRegionDestroy(RegionEvent event) throws CacheWriterException {}"
+        + "public void beforeRegionClear(RegionEvent event) throws CacheWriterException {}"
+        + "public Object load(LoaderHelper helper) throws CacheLoaderException { return null; }"
+        + "public byte[] compress(byte[] input) { return new byte[0]; }"
+        + "public byte[] decompress(byte[] input) { return new byte[0]; }" + "}";
+    return code;
+  }
+
 }

-- 
To stop receiving notification emails like this one, please contact
jensdeppe@apache.org.