SGF-787 - Add 'requiredPermissions' to @GemfireFunction annotation.

This commit is contained in:
John Blum
2018-09-07 19:27:43 -07:00
parent 1980528789
commit 9f9aa6975a
7 changed files with 733 additions and 56 deletions

View File

@@ -24,11 +24,13 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.security.ResourcePermission;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
@@ -50,6 +52,8 @@ import org.springframework.util.StringUtils;
*/
public abstract class GemfireFunctionUtils {
private static final String DEFAULT_FUNCTION_ID = null;
private static Log log = LogFactory.getLog(GemfireFunctionUtils.class);
/**
@@ -61,19 +65,39 @@ public abstract class GemfireFunctionUtils {
* @see java.lang.reflect.Method
*/
public static boolean isGemfireFunction(Method method) {
return (method != null && method.isAnnotationPresent(GemfireFunction.class));
return method != null && method.isAnnotationPresent(GemfireFunction.class);
}
private static boolean isMatchingGemfireFunction(Method method, String functionId) {
/**
* Determines whether the given {@link Method} is a POJO, {@link GemfireFunction} annotated {@link Method}
* having an ID matching the given {@code functionId}.
*
* @param method {@link Method} to evaluate.
* @param functionId {@link String} containing the {@link Function#getId() ID} to match.
* @return a boolean indicating whether the given {@link Method} is a POJO, {@link GemfireFunction} annotated
* {@link Method} with the given {@code functionId}.
* @see #getGemfireFunctionId(Method)
* @see java.lang.reflect.Method
*/
public static boolean isMatchingGemfireFunction(Method method, String functionId) {
return getGemfireFunctionId(method).filter(methodFunctionId ->
ObjectUtils.nullSafeEquals(methodFunctionId, functionId)).isPresent();
}
private static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element,
static Annotation getAnnotation(AnnotatedElement element, Class<? extends Annotation> annotationType) {
return AnnotationUtils.getAnnotation(element, annotationType);
}
static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element, Annotation annotation) {
return AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes(element, annotation));
}
static AnnotationAttributes getAnnotationAttributes(AnnotatedElement element,
Class<? extends Annotation> annotationType) {
return AnnotationAttributes.fromMap(
AnnotationUtils.getAnnotationAttributes(element, element.getAnnotation(annotationType)));
return element.isAnnotationPresent(annotationType)
? getAnnotationAttributes(element, getAnnotation(element, annotationType))
: null;
}
/**
@@ -83,24 +107,24 @@ public abstract class GemfireFunctionUtils {
* If the {@link Method} is not {@literal null} and annotated with {@link GemfireFunction} then this method
* tries to determine the {@link Function#getId()} from the {@link GemfireFunction#id()} attribute.
*
* If the {@link GemfireFunction#id()} attribute was not set, then the {@link Method#getName()}
* If the {@link GemfireFunction#id()} attribute was not explicitly set, then the {@link Method#getName()}
* is returned as the {@link Function#getId() Function ID}.
*
* @param method {@link GemfireFunction} annotated POJO {@link Method} containing the implementation
* for a Pivotal GemFire/Apache Geode {@link Function}.
* @return the Pivotal GemFire/Apache Geode Function ID of the given {@link Method}, or an empty {@link Optional}
* if the {@link Method} is not a Pivotal GemFire/Apache Geode {@link Function}.
* of the GemFire/Geode {@link Function}.
* @return the GemFire/Geode {@link Function#getId() Function ID} of the given {@link Method},
* or an empty {@link Optional} if the {@link Method} is not a GemFire/Geode {@link Function}.
* @see org.springframework.data.gemfire.function.annotation.GemfireFunction
* @see java.lang.reflect.Method
* @see #isGemfireFunction(Method)
*/
private static Optional<String> getGemfireFunctionId(Method method) {
@SuppressWarnings("all")
public static Optional<String> getGemfireFunctionId(Method method) {
return Optional.ofNullable(method)
.filter(GemfireFunctionUtils::isGemfireFunction)
.map(it ->
Optional.of(it.getAnnotation(GemfireFunction.class))
.map(annotation -> getAnnotationAttributes(it, annotation.getClass()))
Optional.ofNullable(getAnnotationAttributes(it, GemfireFunction.class))
.filter(annotationAttributes -> annotationAttributes.containsKey("id"))
.map(annotationAttributes -> annotationAttributes.getString("id"))
.filter(StringUtils::hasText)
@@ -114,7 +138,8 @@ public abstract class GemfireFunctionUtils {
.map(constructor -> BeanUtils.instantiateClass(constructor, constructorArguments))
.orElseThrow(() -> newIllegalArgumentException(
"No suitable constructor was found for type [%s] having parameters [%s]", type.getName(),
stream(nullSafeArray(constructorArguments, Object.class)).map(ObjectUtils::nullSafeClassName)
stream(nullSafeArray(constructorArguments, Object.class))
.map(ObjectUtils::nullSafeClassName)
.collect(Collectors.toList())));
}
@@ -160,6 +185,7 @@ public abstract class GemfireFunctionUtils {
* @see #registerFunctionForPojoMethod(Object, Method, AnnotationAttributes, boolean)
* @see #isMatchingGemfireFunction(Method, String)
*/
@SuppressWarnings("unused")
public static void registerFunctionForPojoMethod(Object target, String functionId) {
Assert.notNull(target, "Target object is required");
@@ -183,6 +209,7 @@ public abstract class GemfireFunctionUtils {
* @see #registerFunctionForPojoMethod(Object, Method, AnnotationAttributes, boolean)
* @see #isGemfireFunction(Method)
*/
@SuppressWarnings("unused")
public static void registerFunctionForPojoMethod(Object target, Method method, boolean overwrite) {
Assert.notNull(target, "Target object is required");
@@ -206,48 +233,153 @@ public abstract class GemfireFunctionUtils {
public static void registerFunctionForPojoMethod(Object target, Method method,
AnnotationAttributes gemfireFunctionAttributes, boolean overwrite) {
String id = gemfireFunctionAttributes.containsKey("id")
? gemfireFunctionAttributes.getString("id") : "";
PojoFunctionWrapper function =
new PojoFunctionWrapper(target, method, resolveFunctionId(gemfireFunctionAttributes));
PojoFunctionWrapper function = new PojoFunctionWrapper(target, method, id);
configureBatchSize(target, method, gemfireFunctionAttributes, function);
configureHighAvailability(gemfireFunctionAttributes, function);
configureHasResult(gemfireFunctionAttributes, function);
configureOptimizeForWrite(gemfireFunctionAttributes, function);
configureRequiredPermissions(gemfireFunctionAttributes, function);
doFunctionRegistration(function, overwrite);
}
static String resolveFunctionId(AnnotationAttributes gemfireFunctionAttributes) {
return gemfireFunctionAttributes.containsKey("id")
? gemfireFunctionAttributes.getString("id")
: DEFAULT_FUNCTION_ID;
}
static void configureBatchSize(Object target, Method method, AnnotationAttributes gemfireFunctionAttributes,
PojoFunctionWrapper function) {
if (gemfireFunctionAttributes.containsKey("batchSize")) {
int batchSize = gemfireFunctionAttributes.getNumber("batchSize");
Assert.isTrue(batchSize >= 0,
String.format("batchSize [%1$d] specified on [%2$s.%3$s] must be a non-negative value",
batchSize, target.getClass().getName(), method.getName()));
String.format("%1$s.batchSize [%2$d] specified on [%3$s.%4$s] must be a non-negative value",
GemfireFunction.class.getSimpleName(), batchSize, target.getClass().getName(), method.getName()));
function.setBatchSize(batchSize);
}
}
static void configureHighAvailability(AnnotationAttributes gemfireFunctionAttributes,
PojoFunctionWrapper function) {
if (gemfireFunctionAttributes.containsKey("HA")) {
function.setHA(gemfireFunctionAttributes.getBoolean("HA"));
}
}
static void configureHasResult(AnnotationAttributes gemfireFunctionAttributes, PojoFunctionWrapper function) {
if (gemfireFunctionAttributes.containsKey("hasResult")) {
if (Boolean.TRUE.equals(gemfireFunctionAttributes.getBoolean("hasResult"))) {
function.setHasResult(true);
}
}
}
static void configureOptimizeForWrite(AnnotationAttributes gemfireFunctionAttributes,
PojoFunctionWrapper function) {
if (gemfireFunctionAttributes.containsKey("optimizeForWrite")) {
function.setOptimizeForWrite(gemfireFunctionAttributes.getBoolean("optimizeForWrite"));
}
}
static void configureRequiredPermissions(AnnotationAttributes gemfireFunctionAttributes,
PojoFunctionWrapper function) {
String[] requiredPermissionStrings = nullSafeArray(gemfireFunctionAttributes.containsKey("requiredPermissions")
? gemfireFunctionAttributes.getStringArray("requiredPermissions")
: null, String.class);
Stream<String> requiredPermissionsStream = Arrays.stream(requiredPermissionStrings);
Optional.of(requiredPermissionsStream
.filter(StringUtils::hasText)
.map(GemfireFunctionUtils::parseResourcePermission)
.collect(Collectors.toList()))
.filter(requiredPermissions -> !requiredPermissions.isEmpty())
.ifPresent(function::setRequiredPermissions);
}
static ResourcePermission parseResourcePermission(String resourcePermissionString) {
Assert.hasText(resourcePermissionString,
String.format("ResourcePermission [%s] is required", resourcePermissionString));
ResourcePermission.Resource resource = ResourcePermission.Resource.DATA;
ResourcePermission.Operation operation = ResourcePermission.Operation.WRITE;
String key = null;
String target = null;
String[] resourcePermissionStringComponents = resourcePermissionString.split(":");
if (resourcePermissionStringComponents.length > 0) {
resource = parseEnum(resourcePermissionStringComponents[0], ResourcePermission.Resource.class,
ResourcePermission.Resource.values());
}
if (resourcePermissionStringComponents.length > 1) {
operation = parseEnum(resourcePermissionStringComponents[1], ResourcePermission.Operation.class,
ResourcePermission.Operation.values());
}
if (resourcePermissionStringComponents.length > 2) {
target = nullSafeTrim(resourcePermissionStringComponents[2]);
}
if (resourcePermissionStringComponents.length > 3) {
key = nullSafeTrim(resourcePermissionStringComponents[3]);
}
return new ResourcePermission(resource, operation, target, key);
}
private static String nullSafeToUpperCase(String value) {
return StringUtils.hasText(value) ? value.trim().toUpperCase() : "null";
}
private static String nullSafeTrim(String value) {
return value != null ? value.trim() : null;
}
private static <T extends Enum<T>> T parseEnum(String name, Class<T> enumType, Enum[] enumeratedValues) {
name = nullSafeToUpperCase(name);
try {
return Enum.valueOf(enumType, name);
}
catch (IllegalArgumentException cause) {
throw newIllegalArgumentException(cause, "[%1$s] is not a valid [%2$s] type; must be 1 of %3$s",
name, enumType.getName(), Arrays.toString(enumeratedValues));
}
}
static void doFunctionRegistration(PojoFunctionWrapper function, boolean overwrite) {
if (FunctionService.isRegistered(function.getId())) {
if (overwrite) {
if (log.isDebugEnabled()) {
log.debug(String.format("Unregistering Function [%s]", function.getId()));
log.debug(String.format("Overwrite enabled; Unregistering Function [%s]", function.getId()));
}
FunctionService.unregisterFunction(function.getId());
}
}
if (!FunctionService.isRegistered(function.getId())) {
if (FunctionService.isRegistered(function.getId())) {
if (log.isDebugEnabled()) {
log.debug(String.format("Function [%s] is already registered", function.getId()));
}
}
else {
FunctionService.registerFunction(function);
@@ -255,11 +387,6 @@ public abstract class GemfireFunctionUtils {
log.debug(String.format("Registered Function [%s]", function.getId()));
}
}
else {
if (log.isDebugEnabled()) {
log.debug(String.format("Function [%s] is already registered", function.getId()));
}
}
}
/**

View File

@@ -12,7 +12,11 @@
*/
package org.springframework.data.gemfire.function;
import static org.springframework.data.gemfire.util.CollectionUtils.asSet;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -20,6 +24,8 @@ import org.apache.geode.cache.Region;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.cache.execute.ResultSender;
import org.apache.geode.management.internal.security.ResourcePermissions;
import org.apache.geode.security.ResourcePermission;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -52,6 +58,8 @@ public class PojoFunctionWrapper implements Function {
private volatile int batchSize;
private Collection<ResourcePermission> requiredPermissions = asSet(ResourcePermissions.DATA_WRITE);
private final FunctionArgumentResolver functionArgumentResolver;
private final Method method;
@@ -60,21 +68,41 @@ public class PojoFunctionWrapper implements Function {
private final String id;
public PojoFunctionWrapper(Object target, Method method) {
this(target, method, null);
}
public PojoFunctionWrapper(Object target, Method method, String id) {
this.target = target;
this.method = method;
this.id = StringUtils.hasText(id) ? id : method.getName();
this.functionArgumentResolver = new FunctionContextInjectingArgumentResolver(method);
this.id = resolveId(method, id);
this.functionArgumentResolver = newFunctionArgumentResolver(method);
this.HA = false;
this.hasResult = !method.getReturnType().equals(void.class);
this.hasResult = resolveHasResult(method);
this.optimizeForWrite = false;
}
protected FunctionArgumentResolver newFunctionArgumentResolver(Method method) {
return new FunctionContextInjectingArgumentResolver(method);
}
protected boolean resolveHasResult(Method method) {
return !method.getReturnType().equals(void.class);
}
protected String resolveId(Method method, String id) {
return StringUtils.hasText(id) ? id : method.getName();
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public int getBatchSize() {
return this.batchSize;
}
public void setHA(boolean HA) {
this.HA = HA;
}
@@ -107,6 +135,15 @@ public class PojoFunctionWrapper implements Function {
return this.optimizeForWrite;
}
public void setRequiredPermissions(Collection<ResourcePermission> requiredPermissions) {
this.requiredPermissions = requiredPermissions;
}
@Override
public Collection<ResourcePermission> getRequiredPermissions(String regionName) {
return Collections.unmodifiableCollection(this.requiredPermissions);
}
@Override
@SuppressWarnings("unchecked")
public void execute(FunctionContext functionContext) {

View File

@@ -17,37 +17,33 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.ResultSender;
/**
*
* Used to declare a concrete method as a Pivotal GemFire function implementation
* Used to declare a concrete method as a Pivotal GemFire {@link Function} implementation.
*
* @author David Turanski
*
* @author John Blum
* @see org.apache.geode.cache.execute.Function
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface GemfireFunction {
String DEFAULT_RESOURCE_PERMISSION = "DATA:WRITE";
/**
* The name of the registered function. If not provided the simple method name will be used
* @return the function id
* The {@link String name} of the registered {@link Function}.
*
* If not provided the simple {@link String method name} will be used.
*
* @return the {@link Function} {@link Function#getId() id}.
* @see org.apache.geode.cache.execute.Function#getId()
*/
String id() default "";
/**
* Attribute to determine whether the Pivotal GemFire Function is HA (Highly Available).
*
* @return a boolean value indicating whether the defined Pivotal GemFire Function is HA.
*/
boolean HA() default false;
/**
* Attribute to determine whether the Pivotal GemFire Function is optimized for write operations.
*
* @return a boolean value indicating if the Pivotal GemFire Function is configured for optimized write operations.
*/
boolean optimizeForWrite() default false;
/**
* Controls the maximum number of results sent at one time.
*
@@ -56,11 +52,31 @@ public @interface GemfireFunction {
int batchSize() default 0;
/**
* Normally follows the method return type, i.e., false if void, true otherwise. This allows overriding
* a void method which uses the resultSender directly.
* Attribute used to configure whether the {@link Function} is HA (Highly Available).
*
* @return a boolean value indicating if the Pivotal GemFire Function is expected to return a result.
* @return a boolean value configuring whether the defined {@link Function} is HA.
* @see org.apache.geode.cache.execute.Function#isHA()
*/
boolean HA() default false;
/**
* Normally follows the method return type, i.e. {@literal false} if {@code void}, {@literal true} otherwise.
*
* This allows overriding a {@code void} method which uses the {@link ResultSender} directly.
*
* @return a boolean value indicating if the {@link Function} is expected to return a result.
* @see org.apache.geode.cache.execute.Function#hasResult()
*/
boolean hasResult() default false;
/**
* Attribute to configure whether the {@link Function} is optimized for write operations.
*
* @return a boolean value indicating if the {@link Function} is configured for optimized write operations.
* @see org.apache.geode.cache.execute.Function#optimizeForWrite()
*/
boolean optimizeForWrite() default false;
String[] requiredPermissions() default { DEFAULT_RESOURCE_PERMISSION };
}

View File

@@ -15,9 +15,11 @@ package org.springframework.data.gemfire.function.config;
import static java.util.Arrays.stream;
import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.apache.geode.cache.execute.Function;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationAttributes;
@@ -28,17 +30,21 @@ import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Spring {@link BeanPostProcessor} that discovers bean components wired as Function implementations,
* i.e. beans containing methods annotated with {@link GemfireFunction}.
* Spring {@link BeanPostProcessor} that discovers bean components configured as {@link Function} implementations,
* i.e. beans containing {@link Method methods} annotated with {@link GemfireFunction}.
*
* @author David Turanski
* @author John Blum
* @see java.lang.annotation.Annotation
* @see java.lang.reflect.Method
* @see org.apache.geode.cache.execute.Function
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.data.gemfire.function.annotation.GemfireFunction
*/
public class GemfireFunctionBeanPostProcessor implements BeanPostProcessor {
/* (non-Javadoc)
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
*/
@Override
@@ -61,11 +67,17 @@ public class GemfireFunctionBeanPostProcessor implements BeanPostProcessor {
String.format("The bean [%s] method [%s] annotated with [%s] must be public",
bean.getClass().getName(), method.getName(), GemfireFunction.class.getName()));
AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(
AnnotationUtils.getAnnotationAttributes(gemfireFunctionAnnotation,false,true));
AnnotationAttributes gemfireFunctionAttributes = resolveAnnotationAttributes(gemfireFunctionAnnotation);
GemfireFunctionUtils.registerFunctionForPojoMethod(bean, method, annotationAttributes, false);
GemfireFunctionUtils.registerFunctionForPojoMethod(bean, method,
gemfireFunctionAttributes, false);
}
});
}
private AnnotationAttributes resolveAnnotationAttributes(Annotation annotation) {
return AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes(annotation,
false, true));
}
}

View File

@@ -30,7 +30,10 @@ public class GemfireFunctionBeanPostProcessorRegistrar implements ImportBeanDefi
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(GemfireFunctionBeanPostProcessor.class);
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(GemfireFunctionBeanPostProcessor.class);
BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry);
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.springframework.data.gemfire.function;
import static org.assertj.core.api.Assertions.assertThat;
import java.lang.annotation.Annotation;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.security.ResourcePermission;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.function.annotation.GemfireFunction;
import org.springframework.data.gemfire.function.config.EnableGemfireFunctions;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests testing the registration of Apache Geode/Pivotal GemFire {@link Function Functions}
* with required permissions specified using the {@link GemfireFunction} {@link Annotation}.
*
* @author John Blum
* @see org.junit.Test
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.data.gemfire.function.annotation.GemfireFunction
* @see org.springframework.data.gemfire.function.config.EnableGemfireFunctions
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @since 2.1.0
*/
@RunWith(SpringRunner.class)
@ContextConfiguration
@SuppressWarnings({ "unchecked", "unused" })
public class FunctionWithRequiredPermissionsRegistrationIntegrationTests {
@Test
public void functionsRegisteredWithRequiredPermissionsSuccessfully() {
Function function = FunctionService.getFunction("testFunctionWithRequiredPermissions");
assertThat(function).isNotNull();
assertThat(function.getRequiredPermissions("test")).containsExactly(
new ResourcePermission(ResourcePermission.Resource.CLUSTER, ResourcePermission.Operation.MANAGE),
new ResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, "Example")
);
}
@Configuration
@EnableGemfireFunctions
static class TestConfiguration {
@Bean
TestFunctions testFunctions() {
return new TestFunctions();
}
}
static class TestFunctions {
@GemfireFunction(requiredPermissions = { "CLUSTER:MANAGE", "DATA:READ:Example" })
public void testFunctionWithRequiredPermissions() { }
}
}

View File

@@ -0,0 +1,400 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.springframework.data.gemfire.function;
import static org.assertj.core.api.Assertions.assertThat;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.apache.geode.security.ResourcePermission;
import org.junit.Test;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.data.gemfire.function.annotation.GemfireFunction;
/**
* Unit tests for {@link GemfireFunctionUtils}.
*
* @author John Blum
* @see org.junit.Test
* @see org.springframework.data.gemfire.function.GemfireFunctionUtils
* @since 2.1.0
*/
@SuppressWarnings("unused")
public class GemfireFunctionUtilsUnitTests {
private final TestFunctions testFunctions = new TestFunctions();
private ResourcePermission newResourcePermission(ResourcePermission.Resource resource,
ResourcePermission.Operation operation, ResourcePermission.Target target, String key) {
return new ResourcePermission(resource, operation, target.name(), key);
}
private ResourcePermission newResourcePermission(ResourcePermission.Resource resource,
ResourcePermission.Operation operation, String target, String key) {
return new ResourcePermission(resource, operation, target, key);
}
@Test
public void gemfireFunctionAnnotatedMethodIsGemFireFunction() throws Exception {
assertThat(GemfireFunctionUtils.isGemfireFunction(TestFunctions.class.getDeclaredMethod("testFunction")))
.isTrue();
}
@Test
public void nonGemfireFunctionAnnotatedMethodIsNotGemFireFunction() throws Exception {
assertThat(GemfireFunctionUtils.isGemfireFunction(TestFunctions.class.getDeclaredMethod("nonFunction")))
.isFalse();
}
@Test
public void matchingGemFireFunctionReturnsTrue() throws Exception {
assertThat(GemfireFunctionUtils.isMatchingGemfireFunction(
TestFunctions.class.getDeclaredMethod("identifiedFunction"), "MyFunction")).isTrue();
}
@Test
public void nonMatchingGemFireFunctionReturnsFalse() throws Exception {
assertThat(GemfireFunctionUtils.isMatchingGemfireFunction(
TestFunctions.class.getDeclaredMethod("identifiedFunction"), "YourFunction"))
.isFalse();
assertThat(GemfireFunctionUtils.isMatchingGemfireFunction(
TestFunctions.class.getDeclaredMethod("nonFunction"), "nonFunction"))
.isFalse();
assertThat(GemfireFunctionUtils.isMatchingGemfireFunction(
TestFunctions.class.getDeclaredMethod("requiredPermissionsFunction"), "MyFunction"))
.isFalse();
assertThat(GemfireFunctionUtils.isMatchingGemfireFunction(
TestFunctions.class.getDeclaredMethod("testFunction"), "MockFunction"))
.isFalse();
}
@Test
public void resolvesFunctionIdFromGemfireFunctionIdAttribute() {
AnnotationAttributes gemfireFunctionAttributes = new AnnotationAttributes();
gemfireFunctionAttributes.put("id", "2");
assertThat(GemfireFunctionUtils.resolveFunctionId(gemfireFunctionAttributes)).isEqualTo("2");
}
@Test
public void unresolvableFunctionIdReturnsNull() {
assertThat(GemfireFunctionUtils.resolveFunctionId(new AnnotationAttributes())).isNull();
}
@Test
public void configureWithBatchSize() throws Exception {
Method functionWithBatchSize = TestFunctions.class.getDeclaredMethod("functionWithBatchSize");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(functionWithBatchSize, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithBatchSize);
GemfireFunctionUtils.configureBatchSize(this.testFunctions, functionWithBatchSize,
gemfireFunctionAttributes, function);
assertThat(function.getBatchSize()).isEqualTo(10);
}
@Test(expected = IllegalArgumentException.class)
public void configureWithInvalidBatchSizeThrowsIllegalArgumentException() throws Exception {
Method functionWithInvalidBatchSize =
TestFunctions.class.getDeclaredMethod("functionWithInvalidBatchSize");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(functionWithInvalidBatchSize, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithInvalidBatchSize);
try {
GemfireFunctionUtils.configureBatchSize(this.testFunctions, functionWithInvalidBatchSize,
gemfireFunctionAttributes, function);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("GemfireFunction.batchSize [-5] specified on [%1$s.%2$s] must be a non-negative value",
testFunctions.getClass().getName(), functionWithInvalidBatchSize.getName());
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test
public void configureWithNoBatchSize() throws Exception {
Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction);
GemfireFunctionUtils.configureBatchSize(this.testFunctions, testFunction, gemfireFunctionAttributes, function);
assertThat(function.getBatchSize()).isEqualTo(0);
}
@Test
public void configureWithHighAvailability() throws Exception {
Method functionWithHighAvailability = TestFunctions.class.getDeclaredMethod("functionWithHighAvailability");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(functionWithHighAvailability, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithHighAvailability);
GemfireFunctionUtils.configureHighAvailability(gemfireFunctionAttributes, function);
assertThat(function.isHA()).isTrue();
}
@Test
public void configureWithNoHighAvailability() throws Exception {
Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction);
GemfireFunctionUtils.configureHighAvailability(gemfireFunctionAttributes, function);
assertThat(function.isHA()).isFalse();
}
@Test
public void configureWithResult() throws Exception {
Method functionWithResult = TestFunctions.class.getDeclaredMethod("functionWithResult");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(functionWithResult, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionWithResult);
GemfireFunctionUtils.configureHasResult(gemfireFunctionAttributes, function);
assertThat(function.hasResult()).isTrue();
}
@Test
public void configureWithNoResult() throws Exception {
Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction);
GemfireFunctionUtils.configureHasResult(gemfireFunctionAttributes, function);
assertThat(function.hasResult()).isFalse();
}
@Test
public void configureWithOptimizeForWrite() throws Exception {
Method functionOptimizedForWrite = TestFunctions.class.getDeclaredMethod("functionOptimizedForWrite");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(functionOptimizedForWrite, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, functionOptimizedForWrite);
GemfireFunctionUtils.configureOptimizeForWrite(gemfireFunctionAttributes, function);
assertThat(function.optimizeForWrite()).isTrue();
}
@Test
public void configureWithNoOptimizeForWrite() throws Exception {
Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction);
GemfireFunctionUtils.configureOptimizeForWrite(gemfireFunctionAttributes, function);
assertThat(function.optimizeForWrite()).isFalse();
}
@Test
public void cofigureWithDefaultRequiredPermissions() throws Exception {
Method testFunction = TestFunctions.class.getDeclaredMethod("testFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(testFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, testFunction);
GemfireFunctionUtils.configureRequiredPermissions(gemfireFunctionAttributes, function);
assertThat(function.getRequiredPermissions("test")).containsExactly(
newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.WRITE, (String) null, null)
);
}
@Test
public void configureWithRequiredPermissions() throws Exception {
Method requiredPermissionsFunction = TestFunctions.class.getDeclaredMethod("requiredPermissionsFunction");
AnnotationAttributes gemfireFunctionAttributes =
GemfireFunctionUtils.getAnnotationAttributes(requiredPermissionsFunction, GemfireFunction.class);
PojoFunctionWrapper function = new PojoFunctionWrapper(this.testFunctions, requiredPermissionsFunction);
GemfireFunctionUtils.configureRequiredPermissions(gemfireFunctionAttributes, function);
assertThat(function.getRequiredPermissions("Example")).containsExactly(
newResourcePermission(ResourcePermission.Resource.CLUSTER, ResourcePermission.Operation.MANAGE, (String) null, null),
newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.MANAGE, "System", null),
newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ, ResourcePermission.Target.QUERY, "ALL"),
newResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.WRITE, "Example", "123")
);
}
public void testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(
String resourcePermissionString) {
try {
GemfireFunctionUtils.parseResourcePermission(resourcePermissionString);
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("ResourcePermission [%s] is required", resourcePermissionString);
assertThat(expected).hasNoCause();
throw expected;
}
}
@Test(expected = IllegalArgumentException.class)
public void parseResourcePermissionWithNullStringThrowsIllegalArgumentException() {
testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(null);
}
@Test(expected = IllegalArgumentException.class)
public void parseResourcePermissionWithEmptyStringThrowsIllegalArgumentException() {
testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException("");
}
@Test(expected = IllegalArgumentException.class)
public void parseResourcePermissionWithBlankStringThrowsIllegalArgumentException() {
testParseResourcePermissionWithInvalidArgumentThrowsIllegalArgumentException(" ");
}
@Test(expected = IllegalArgumentException.class)
public void parseResourcePermissionWithInvalidResourceThrowsIllegalArgumentException() {
try {
GemfireFunctionUtils.parseResourcePermission("SOCKET:OPEN");
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("[SOCKET] is not a valid [%1$s] type; must be 1 of %2$s",
ResourcePermission.Resource.class.getName(), Arrays.toString(ResourcePermission.Resource.values()));
assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class);
throw expected;
}
}
@Test(expected = IllegalArgumentException.class)
public void parseResourcePermissionWithInvalidOperationThrowsIllegalArgumentException() {
try {
GemfireFunctionUtils.parseResourcePermission("DATA:COPY");
}
catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("[COPY] is not a valid [%1$s] type; must be 1 of %2$s",
ResourcePermission.Operation.class.getName(), Arrays.toString(ResourcePermission.Operation.values()));
assertThat(expected).hasCauseInstanceOf(IllegalArgumentException.class);
throw expected;
}
}
@Test
public void parseResourcePermissionIsSuccessful() {
ResourcePermission resourcePermission =
GemfireFunctionUtils.parseResourcePermission("CLUSTER:MANAGE");
assertThat(resourcePermission).isNotNull();
assertThat(resourcePermission.getResource()).isEqualTo(ResourcePermission.Resource.CLUSTER);
assertThat(resourcePermission.getOperation()).isEqualTo(ResourcePermission.Operation.MANAGE);
}
static class TestFunctions {
@GemfireFunction(batchSize = 10)
void functionWithBatchSize() { }
@GemfireFunction(batchSize = -5)
void functionWithInvalidBatchSize() { }
@GemfireFunction(HA = true)
void functionWithHighAvailability() { }
@GemfireFunction(hasResult = true)
void functionWithResult() { }
@GemfireFunction(optimizeForWrite = true)
void functionOptimizedForWrite() { }
@GemfireFunction(id = "MyFunction")
void identifiedFunction() { }
void nonFunction() { }
@GemfireFunction(requiredPermissions = {
"CLUSTER:MANAGE",
"DATA:MANAGE:System",
"DATA:READ:QUERY:ALL",
"DATA:WRITE:Example:123"
})
void requiredPermissionsFunction() { }
@GemfireFunction
void testFunction() { }
}
}