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);
}
}