blob: 74cee97ab8a9ed76f68220776ae3517ed32e5ebc [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.geode.cache.query.internal;
import static org.apache.geode.cache.query.security.RestrictedMethodAuthorizer.UNAUTHORIZED_STRING;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.cache.EntryDestroyedException;
import org.apache.geode.cache.query.NameNotFoundException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.util.JavaWorkarounds;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.PdxSerializationException;
import org.apache.geode.pdx.internal.InternalPdxInstance;
import org.apache.geode.pdx.internal.PdxType;
import org.apache.geode.pdx.internal.TypeRegistry;
import org.apache.geode.security.NotAuthorizedException;
/**
* Utility for managing an attribute
*/
public class AttributeDescriptor {
private final String _name;
private final TypeRegistry _pdxRegistry;
/** cache for remembering the correct Member for a class and attribute */
@MakeNotStatic
static final ConcurrentMap<List, Member> _localCache = new ConcurrentHashMap<>();
public AttributeDescriptor(TypeRegistry pdxRegistry, String name) {
_name = name;
_pdxRegistry = pdxRegistry;
}
/** Validate whether this attribute <i>can</i> be evaluated for target type */
public boolean validateReadType(Class targetType) {
try {
getReadMember(targetType);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
public Object read(Object target, ExecutionContext executionContext)
throws NameNotFoundException, QueryInvocationTargetException {
if (target == null || target == QueryService.UNDEFINED) {
return QueryService.UNDEFINED;
}
if (target instanceof InternalPdxInstance) {
return readPdx((InternalPdxInstance) target, executionContext);
}
// for non pdx objects
return readReflection(target, executionContext);
}
// used when the resolution of an attribute must be on a superclass
// instead of the runtime class
Object readReflection(Object target, ExecutionContext executionContext)
throws NameNotFoundException, QueryInvocationTargetException {
Support.Assert(target != null);
Support.Assert(target != QueryService.UNDEFINED);
if (target instanceof Token) {
return QueryService.UNDEFINED;
}
Class resolutionClass = target.getClass();
Member m = getReadMember(resolutionClass);
try {
if (m instanceof Method) {
try {
Method method = (Method) m;
// Try to use previous result so authorizer gets invoked only once per query.
boolean authorizationResult;
String cacheKey = target.getClass().getCanonicalName() + "." + method.getName();
Boolean cachedResult = (Boolean) executionContext.cacheGet(cacheKey);
if (cachedResult == null) {
// First time, evaluate and cache result.
authorizationResult =
executionContext.getMethodInvocationAuthorizer().authorize(method, target);
executionContext.cachePut(cacheKey, authorizationResult);
} else {
// Use cached result.
authorizationResult = cachedResult;
}
if (!authorizationResult) {
throw new NotAuthorizedException(UNAUTHORIZED_STRING + method.getName());
}
return method.invoke(target, (Object[]) null);
} catch (EntryDestroyedException e) {
// eat the Exception
return QueryService.UNDEFINED;
} catch (IllegalAccessException e) {
throw new NameNotFoundException(
String.format(
"Method ' %s ' in class ' %s ' is not accessible to the query processor",
m.getName(), target.getClass().getName()),
e);
} catch (InvocationTargetException e) {
// if the target exception is Exception, wrap that,
// otherwise wrap the InvocationTargetException itself
Throwable t = e.getTargetException();
if ((t instanceof EntryDestroyedException)) {
// eat the exception
return QueryService.UNDEFINED;
}
if (t instanceof Exception)
throw new QueryInvocationTargetException(t);
throw new QueryInvocationTargetException(e);
}
} else {
try {
return ((Field) m).get(target);
} catch (IllegalAccessException e) {
throw new NameNotFoundException(
String.format(
"Field ' %s ' in class ' %s ' is not accessible to the query processor",
m.getName(), target.getClass().getName()),
e);
} catch (EntryDestroyedException e) {
return QueryService.UNDEFINED;
}
}
} catch (EntryDestroyedException e) {
// eat the exception
return QueryService.UNDEFINED;
}
}
@SuppressWarnings("unchecked")
Member getReadMember(Class targetClass) throws NameNotFoundException {
// mapping: public field (same name), method (getAttribute()), method (attribute())
List key = new ArrayList();
key.add(targetClass);
key.add(_name);
Member m = JavaWorkarounds.computeIfAbsent(_localCache, key, k -> {
Member member = getReadField(targetClass);
return member == null ? getReadMethod(targetClass) : member;
});
if (m == null) {
throw new NameNotFoundException(String.format(
"No public attribute named ' %s ' was found in class %s", _name, targetClass.getName()));
}
// override security for nonpublic derived classes with public members
((AccessibleObject) m).setAccessible(true);
return m;
}
Field getReadField(Class targetType) {
try {
return targetType.getField(_name);
} catch (NoSuchFieldException e) {
return null;
}
}
Method getReadMethod(Class targetType) {
Method m;
// Check for a getter method for this _name
String beanMethod = "get" + _name.substring(0, 1).toUpperCase() + _name.substring(1);
m = getReadMethod(targetType, beanMethod);
if (m != null)
return m;
return getReadMethod(targetType, _name);
}
@SuppressWarnings("unchecked")
Method getReadMethod(Class targetType, String methodName) {
try {
return targetType.getMethod(methodName, (Class[]) null);
} catch (NoSuchMethodException e) {
updateClassToMethodsMap(targetType.getCanonicalName(), _name);
return null;
}
}
/**
* reads field value from a PdxInstance
*
* @return the value of the field from PdxInstance
*/
private Object readPdx(InternalPdxInstance pdxInstance, ExecutionContext executionContext)
throws NameNotFoundException, QueryInvocationTargetException {
// if the field is present in the pdxinstance
if (pdxInstance.hasField(_name)) {
// return PdxString if field is a String otherwise invoke readField
return pdxInstance.getRawField(_name);
} else {
// field not found in the pdx instance, look for the field in any of the
// PdxTypes (versions of the pdxinstance) in the type registry
String className = pdxInstance.getClassName();
// don't look further for field or method or reflect on GemFire JSON data
if (className.equals(JSONFormatter.JSON_CLASSNAME)) {
return QueryService.UNDEFINED;
}
// check if the field was not found previously
if (!isFieldAlreadySearchedAndNotFound(className, _name)) {
PdxType pdxType = _pdxRegistry.getPdxTypeForField(_name, className);
if (pdxType == null) {
// remember the field that is not present in any version to avoid
// trips to the registry next time
updateClassToFieldsMap(className, _name);
} else {
return pdxType.getPdxField(_name).getFieldType().getDefaultValue();
}
}
// if the field is not present in any of the versions try to
// invoke implicit method call
if (!this.isMethodAlreadySearchedAndNotFound(className, _name)) {
try {
return readFieldFromDeserializedObject(pdxInstance, executionContext);
} catch (NameNotFoundException ex) {
updateClassToMethodsMap(pdxInstance.getClassName(), _name);
throw ex;
}
} else
return QueryService.UNDEFINED;
}
}
private Object readFieldFromDeserializedObject(InternalPdxInstance pdxInstance,
ExecutionContext executionContext)
throws NameNotFoundException, QueryInvocationTargetException {
Object obj;
try {
obj = pdxInstance.getCachedObject();
} catch (PdxSerializationException e) {
throw new NameNotFoundException( // the domain object is not available
String.format("Field '%s' is not accessible to the query processor because: %s",
_name, e.getMessage()));
}
return readReflection(obj, executionContext);
}
private void updateClassToFieldsMap(String className, String field) {
Map<String, Set<String>> map = DefaultQuery.getPdxClasstofieldsmap();
Set<String> fields = map.get(className);
if (fields == null) {
fields = new HashSet<>();
map.put(className, fields);
}
fields.add(field);
}
private boolean isFieldAlreadySearchedAndNotFound(String className, String field) {
Set<String> fields = DefaultQuery.getPdxClasstofieldsmap().get(className);
if (fields != null) {
return fields.contains(field);
}
return false;
}
private void updateClassToMethodsMap(String className, String field) {
Map<String, Set<String>> map = DefaultQuery.getPdxClasstoMethodsmap();
Set<String> fields = map.get(className);
if (fields == null) {
fields = new HashSet<>();
map.put(className, fields);
}
fields.add(field);
}
private boolean isMethodAlreadySearchedAndNotFound(String className, String field) {
Set<String> fields = DefaultQuery.getPdxClasstoMethodsmap().get(className);
if (fields != null) {
return fields.contains(field);
}
return false;
}
}