-
-
Notifications
You must be signed in to change notification settings - Fork 98
Support interface entity types with EntityDecorator #1183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0e27c8f
Initial plan
Copilot 3c66bd2
Add support for interface entity types in repositories
Copilot 1d74e68
Add FieldAccessHelper for interface property field access
Copilot d7975d4
Address code review feedback in FieldAccessHelper
Copilot 333133f
Address code review feedback
Copilot 111c74b
Upgrade Android API from 24 to 30 and use MethodHandles
Copilot 7614f7d
Update Android compatibility to API Level 26
anidotnet 98b6e83
Update nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java
anidotnet a3a028f
Update nitrite/src/main/java/org/dizitart/no2/repository/Reflector.java
anidotnet 3c22c53
Clarify MethodHandles usage in FieldAccessHelper
anidotnet 6f65e43
Merge branch 'copilot/migrate-mongo-repository-to-nitrite' of github.…
anidotnet e012647
Remove unused variable in testEdgeCaseEmptyPropertyName
anidotnet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
268 changes: 268 additions & 0 deletions
268
nitrite/src/main/java/org/dizitart/no2/repository/FieldAccessHelper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
anidotnet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.