Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Nitrite Database is an open source embedded NoSQL database for Java. It's a mult
- Extensible storage engines (MVStore, RocksDB)
- Full-text search and indexing
- Transaction support
- Android compatibility (API Level 24+)
- Android compatibility (API Level 26+)

## Repository Structure

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Nitrite is an embedded database ideal for desktop, mobile or small web applicati
- Transaction support
- Schema migration support
- Encryption support
- Android compatibility (API Level 24)
- Android compatibility (API Level 26)

## Kotlin Extension

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ private void populateIndex(List<Index> indexList) {
Field field = reflector.getField(type, name);
if (field != null) {
entityFields.add(field);
indexValidator.validate(field.getType(), field.getName(), nitriteMapper);
// Use InterfacePropertyHolder to get correct name and type for interface properties
String fieldName = InterfacePropertyHolder.getPropertyName(field);
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
indexValidator.validate(fieldType, fieldName, nitriteMapper);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ private void readIndices() {
Field field = reflector.getField(entityDecorator.getEntityType(), name);
if (field != null) {
entityFields.add(field);
indexValidator.validate(field.getType(), field.getName(), nitriteMapper);
// Use InterfacePropertyHolder to get correct name and type for interface properties
String fieldName = InterfacePropertyHolder.getPropertyName(field);
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
indexValidator.validate(fieldType, fieldName, nitriteMapper);
}
}

Expand All @@ -108,7 +111,9 @@ private void readIdField() {
String idFieldName = entityId.getFieldName();
if (!StringUtils.isNullOrEmpty(idFieldName)) {
Field field = reflector.getField(entityDecorator.getEntityType(), idFieldName);
indexValidator.validateId(entityId, field.getType(), idFieldName, nitriteMapper);
// Use InterfacePropertyHolder to get correct type for interface properties
Class<?> fieldType = InterfacePropertyHolder.getPropertyType(field);
indexValidator.validateId(entityId, fieldType, idFieldName, nitriteMapper);

objectIdField = new ObjectIdField();
objectIdField.setField(field);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*
* Copyright (c) 2017-2022 Nitrite 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.dizitart.no2.repository;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* Helper class to access field values, handling both regular fields and interface properties.
* For interface properties (synthetic fields from InterfacePropertyHolder), this class
* finds and accesses the actual field on the concrete implementation object.
* <p>
* Uses MethodHandles for improved security and performance (Android API 26+).
*
* @author Anindya Chatterjee
* @since 4.3.2
*/
class FieldAccessHelper {

/**
* Gets the value of a field from an object, handling both regular and interface property fields.
* Uses MethodHandles for improved security and performance (Android API 26+).
*
* @param field the field to access
* @param obj the object to get the value from
* @return the field value
* @throws IllegalAccessException if field access fails
*/
static Object get(Field field, Object obj) throws IllegalAccessException {
if (InterfacePropertyHolder.isInterfaceProperty(field)) {
// This is a synthetic field for an interface property
// Find and access the real field in the concrete object
String propertyName = InterfacePropertyHolder.getPropertyName(field);
return getPropertyValue(obj, propertyName);
} else {
return getFieldValue(field, obj);
}
}

/**
* Sets the value of a field on an object, handling both regular and interface property fields.
* Uses MethodHandles for improved security and performance (Android API 26+).
*
* @param field the field to set
* @param obj the object to set the value on
* @param value the value to set
* @throws IllegalAccessException if field access fails
*/
static void set(Field field, Object obj, Object value) throws IllegalAccessException {
if (InterfacePropertyHolder.isInterfaceProperty(field)) {
// This is a synthetic field for an interface property
// Find and set the real field in the concrete object
String propertyName = InterfacePropertyHolder.getPropertyName(field);
setPropertyValue(obj, propertyName, value);
} else {
setFieldValue(field, obj, value);
}
}

/**
* Gets the value of a field using MethodHandles for secure access.
* Uses unreflect approach compatible with Android API 26+.
*/
private static Object getFieldValue(Field field, Object obj) throws IllegalAccessException {
try {
// Use MethodHandles.lookup().unreflect which is available since Android API 26
// This is more secure than setAccessible but requires the field to be accessible
// while MethodHandles provide a more modern API, setAccessible is still required to bypass access checks.
field.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle getter = lookup.unreflectGetter(field);
return getter.invokeWithArguments(obj);
} catch (Throwable e) {
throw new IllegalAccessException("Cannot access field " + field.getName() + ": " + e.getMessage());
}
}

/**
* Sets the value of a field using MethodHandles for secure access.
* Uses unreflect approach compatible with Android API 26+.
*/
private static void setFieldValue(Field field, Object obj, Object value) throws IllegalAccessException {
try {
// Use MethodHandles.lookup().unreflect which is available since Android API 26
// This is more secure than direct setAccessible + set
// while MethodHandles provide a more modern API, setAccessible is still required to bypass access checks.
field.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle setter = lookup.unreflectSetter(field);
setter.invokeWithArguments(obj, value);
} catch (Throwable e) {
throw new IllegalAccessException("Cannot set field " + field.getName() + ": " + e.getMessage());
}
}

/**
* Gets a property value from an object, trying both field access and getter method.
*/
private static Object getPropertyValue(Object obj, String propertyName) throws IllegalAccessException {
if (propertyName == null || propertyName.isEmpty()) {
throw new IllegalAccessException("Property name cannot be null or empty");
}

// Try to find the field in the object's class
Field realField = findFieldInHierarchy(obj.getClass(), propertyName);
if (realField != null) {
return getFieldValue(realField, obj);
}

// Fall back to getter method - try both 'get' and 'is' prefixes
try {
Method getter = findGetterMethod(obj.getClass(), propertyName);
if (getter != null) {
return invokeMethod(getter, obj);
}
throw new IllegalAccessException("No getter method found for property '" + propertyName + "'");
} catch (Exception e) {
throw new IllegalAccessException("Cannot access property '" + propertyName + "' on " + obj.getClass().getName() + ": " + e.getMessage());
}
}

/**
* Sets a property value on an object, trying both field access and setter method.
*/
private static void setPropertyValue(Object obj, String propertyName, Object value) throws IllegalAccessException {
if (propertyName == null || propertyName.isEmpty()) {
throw new IllegalAccessException("Property name cannot be null or empty");
}

// Try to find the field in the object's class
Field realField = findFieldInHierarchy(obj.getClass(), propertyName);
if (realField != null) {
setFieldValue(realField, obj, value);
return;
}

// Fall back to setter method
try {
String setterName = "set" + capitalizePropertyName(propertyName);
Method setter = findSetterMethod(obj.getClass(), setterName, value);
if (setter != null) {
invokeMethod(setter, obj, value);
} else {
throw new IllegalAccessException("No setter method found for property '" + propertyName + "'");
}
} catch (Exception e) {
throw new IllegalAccessException("Cannot set property '" + propertyName + "' on " + obj.getClass().getName() + ": " + e.getMessage());
}
}

/**
* Invokes a method using MethodHandles for improved security.
* Uses unreflect approach compatible with Android API 26+.
*/
private static Object invokeMethod(Method method, Object obj, Object... args) throws IllegalAccessException {
try {
// Use MethodHandles.lookup().unreflect which is available since Android API 26
method.setAccessible(true);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.unreflect(method);
if (args.length == 0) {
return methodHandle.invokeWithArguments(obj);
} else {
return methodHandle.invokeWithArguments(obj, args[0]);
}
} catch (IllegalAccessException e) {
throw e;
} catch (Throwable e) {
throw new IllegalAccessException("Cannot invoke method " + method.getName() + ": " + e.getMessage());
}
}

/**
* Finds a getter method for a property (tries both 'get' and 'is' prefixes).
*/
private static Method findGetterMethod(Class<?> clazz, String propertyName) {
String capitalizedName = capitalizePropertyName(propertyName);
String getterName = "get" + capitalizedName;
String isGetterName = "is" + capitalizedName;

Method[] methods = clazz.getMethods();
for (Method method : methods) {
if ((method.getName().equals(getterName) || method.getName().equals(isGetterName))
&& method.getParameterTypes().length == 0) {
return method;
}
}
return null;
}

/**
* Capitalizes a property name following JavaBeans conventions.
*/
private static String capitalizePropertyName(String propertyName) {
if (propertyName == null || propertyName.isEmpty()) {
return propertyName;
}
// Follow JavaBeans convention: if first two chars are uppercase, don't change
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(0)) && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
}

/**
* Finds a field in the class hierarchy.
*/
private static Field findFieldInHierarchy(Class<?> clazz, String fieldName) {
Class<?> current = clazz;
while (current != null && current != Object.class) {
try {
return current.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
current = current.getSuperclass();
}
}
return null;
}

/**
* Finds a setter method that can accept the given value.
*/
private static Method findSetterMethod(Class<?> clazz, String setterName, Object value) {
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.getName().equals(setterName) && method.getParameterTypes().length == 1) {
Class<?> paramType = method.getParameterTypes()[0];
if (value == null || paramType.isAssignableFrom(value.getClass()) ||
(paramType.isPrimitive() && isCompatiblePrimitive(paramType, value.getClass()))) {
return method;
}
}
}
return null;
}

/**
* Checks if a value class is compatible with a primitive parameter type.
*/
private static boolean isCompatiblePrimitive(Class<?> primitiveType, Class<?> valueClass) {
// Use a more efficient lookup instead of cascading if statements
return (primitiveType == int.class && valueClass == Integer.class)
|| (primitiveType == long.class && valueClass == Long.class)
|| (primitiveType == double.class && valueClass == Double.class)
|| (primitiveType == float.class && valueClass == Float.class)
|| (primitiveType == boolean.class && valueClass == Boolean.class)
|| (primitiveType == byte.class && valueClass == Byte.class)
|| (primitiveType == short.class && valueClass == Short.class)
|| (primitiveType == char.class && valueClass == Character.class);
}
}
Loading