blob: 7a9cf93619141254562e91560bb10a45754cc8ff [file] [log] [blame]
/*
* 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.caconfig.impl;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.caconfig.ConfigurationResolveException;
import org.apache.sling.caconfig.impl.metadata.AnnotationClassParser;
import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Maps the property of a resource to a dynamic proxy object implementing
* the annotation class defining the configuration parameters.
* Nested configurations with annotation classes referencing other annotation classes are also supported.
*/
final class ConfigurationProxy {
private ConfigurationProxy() {
// static methods only
}
/**
* Get dynamic proxy for given resources's properties mapped to given annotation class.
* @param resource Resource
* @param clazz Annotation class
* @param childResolver This is used to resolve nested configuration objects relative to the current configuration resource
* @return Dynamic proxy object
*/
@SuppressWarnings("unchecked")
public @NotNull static <T> T get(@Nullable Resource resource, @NotNull Class<T> clazz, ChildResolver childResolver) {
// only annotation interface classes are supported
if (!clazz.isAnnotation()) {
throw new ConfigurationResolveException("Annotation interface class expected: " + clazz.getName());
}
// create dynamic proxy for annotation class accessing underlying resource properties
// wrap in caching invocation handler so client code can call all methods multiple times
// without having to worry about performance
return (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
new CachingInvocationHandler(new DynamicProxyInvocationHandler(resource, childResolver)));
}
/**
* Resolves nested configurations.
*/
public static interface ChildResolver {
<T> T getChild(String configName, Class<T> clazz);
<T> Collection<T> getChildren(String configName, Class<T> clazz);
}
/**
* Maps resource properties to annotation class proxy, and support nested configurations.
*/
static class DynamicProxyInvocationHandler implements InvocationHandler {
private final Resource resource;
private final ChildResolver childResolver;
private DynamicProxyInvocationHandler(Resource resource, ChildResolver childResolver) {
this.resource = resource;
this.childResolver = childResolver;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String propName = AnnotationClassParser.getPropertyName(method.getName());
// check for nested configuration classes
Class<?> targetType = method.getReturnType();
Class<?> componentType = method.getReturnType();
boolean isArray = targetType.isArray();
if (isArray) {
componentType = targetType.getComponentType();
}
if (componentType.isAnnotation()) {
if (isArray) {
Collection<?> listItems = childResolver.getChildren(propName, componentType);
return listItems.toArray((Object[])Array.newInstance(componentType, listItems.size()));
}
else {
return childResolver.getChild(propName, componentType);
}
}
// validate type
if (!isValidType(componentType)) {
throw new ConfigurationResolveException("Unsupported type " + componentType.getName()
+ " in " + method.getDeclaringClass() + "#" + method.getName());
}
// detect default value
Object defaultValue = method.getDefaultValue();
if (defaultValue == null) {
if (isArray) {
defaultValue = Array.newInstance(componentType, 0);
}
else if (targetType.isPrimitive()) {
// get default value for primitive data type (use hack via array)
defaultValue = Array.get(Array.newInstance(targetType, 1), 0);
}
}
// get value from valuemap with given type/default value
ValueMap props = ResourceUtil.getValueMap(resource);
Object value;
if (defaultValue != null) {
value = props.get(propName, defaultValue);
}
else {
value = props.get(propName, targetType);
}
return value;
}
/**
* Ensures the given type is support for reading configuration parameters.
* @param type Type
* @return true if type is supported
*/
private boolean isValidType(Class<?> type) {
return PropertyMetadata.SUPPORTED_TYPES.contains(type);
}
}
/**
* Invocation handler that caches all results for each method name, and returns
* the result from cache on next invocation.
*/
static class CachingInvocationHandler implements InvocationHandler {
private final InvocationHandler delegate;
private final Map<String, Object> results = new HashMap<>();
private static final Object NULL_OBJECT = new Object();
public CachingInvocationHandler(InvocationHandler delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String key = method.getName();
Object result = results.get(key);
if (result == null) {
result = delegate.invoke(proxy, method, args);
if (result == null) {
result = NULL_OBJECT;
}
results.put(key, result);
}
if (result == NULL_OBJECT) {
return null;
}
else {
return result;
}
}
}
}