Fine grained repository permissions (#2562)
1. Introduce @PrreAuthorize check based on hasPermission - allowing custom processing (compared with non-modifiable hasAuthority/Role processing) 2. Dedicated permissions could be implemented on management api level. Check is made by plugged in PermissionEvaluator 3. Thus common XXX_REPOSITORY permissions could differ for extending services 4. Change create/update entity builder pattern - not via EntityFactory but via clean static lombok based builders (with fine fluent api). 5. Implement abstract repository management jpa class that handles the boilerplate code from extending classes in single place consistently -> AbsreactJpaRepositoryManagement 6. Register management api-s as **Sevice**-s instead of **Bean**-s in order to make easier maintainable and get away from heavy argument forwading 7. Simplify custom hawkbit repository registration + adding proxy to handle exception mapping at lower level - thus not depending on Aspects for converting exceptions 8. Implemented general purpose 'copy' utility (ObjectCopyUtil) that using getter/setter patterns is able to copy (e.g. Create/Update) objects to other objects (e.g. JPA entity objects)
This commit is contained in:
@@ -11,10 +11,15 @@ package org.eclipse.hawkbit.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* {@link GenericSpServerException} is thrown when a given entity in's actual and cannot be stored within the current session. Reason could be
|
||||
* that it has been changed within another session.
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class GenericSpServerException extends AbstractServerRtException {
|
||||
|
||||
@Serial
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.utils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
public class ObjectCopyUtil {
|
||||
|
||||
private static final Map<FromTo, CopyFunction> FROM_TO_SETTER = new HashMap<>();
|
||||
|
||||
// java:S4276 - it is intended to be generic - not specialized
|
||||
@SuppressWarnings("java:S4276")
|
||||
public static boolean copy(final Object from, final Object to, boolean setNullValues, final UnaryOperator<Object> propertyProcessor) {
|
||||
final FromTo fromTo = new FromTo(from.getClass(), to.getClass());
|
||||
CopyFunction fromToFunction;
|
||||
synchronized (FROM_TO_SETTER) {
|
||||
fromToFunction = FROM_TO_SETTER.get(fromTo);
|
||||
if (fromToFunction == null) {
|
||||
fromToFunction = fromToFunction(from.getClass(), to.getClass());
|
||||
FROM_TO_SETTER.put(fromTo, fromToFunction);
|
||||
}
|
||||
}
|
||||
return fromToFunction.apply(from, to, setNullValues, propertyProcessor);
|
||||
}
|
||||
|
||||
// java:S4276 - it is intended to be generic - not specialized
|
||||
// java:S3776 - complexity is due to reflection and dynamic method invocation
|
||||
// java:S1141 - better readable that way
|
||||
// java:S3011 - low-level, reflection utility. intentionally changes the accessibility
|
||||
@SuppressWarnings({ "java:S4276", "java:S3776", "java:S1141", "java:S3011" })
|
||||
private static CopyFunction fromToFunction(final Class<?> fromClass, final Class<?> toClass) {
|
||||
final List<CopyFunction> propertySetters = new ArrayList<>();
|
||||
|
||||
for (final Method fromMethod : getMethods(fromClass)) {
|
||||
if (fromMethod.getParameterCount() == 0 && fromMethod.getReturnType() != void.class) {
|
||||
final String methodName = fromMethod.getName();
|
||||
if (methodName.equals("getClass")) {
|
||||
continue; // skip
|
||||
}
|
||||
final boolean isGet = isGet(methodName);
|
||||
if (isGet || isIs(fromMethod, methodName)) {
|
||||
final String fieldName = Character.toLowerCase(methodName.charAt(isGet ? 3 : 2)) + methodName.substring(isGet ? 4 : 3);
|
||||
fromMethod.setAccessible(true); // if needed
|
||||
final UnaryOperator<Object> toGetter = toGetter(toClass, fromMethod.getName(), fieldName);
|
||||
final String setterName = "set" + methodName.substring(isGet ? 3 : 2);
|
||||
final BiConsumer<Object, Object> toSetter = toSetter(toClass, setterName, fieldName, fromMethod.getReturnType());
|
||||
if (toSetter == null && toGetter == null) {
|
||||
// we allow toSetter to be null, but in that case the toGetter must not be null and the
|
||||
// from value shall always match the to value (without setting it)
|
||||
throw new IllegalStateException("Setter counterpart for " + fromMethod + " is not found in " + toClass.getName());
|
||||
}
|
||||
propertySetters.add((from, to, setNullValues, propertyProcessor) -> {
|
||||
final Object value;
|
||||
try {
|
||||
value = fromMethod.invoke(from);
|
||||
} catch (final IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to get source value", e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to get source value",
|
||||
e.getTargetException() == null ? e : e.getTargetException());
|
||||
}
|
||||
if (value == null && !setNullValues) { // if !setNullValues null means no change
|
||||
return false;
|
||||
}
|
||||
if (toGetter != null) {
|
||||
final Object currentValue = toGetter.apply(to);
|
||||
if (Objects.equals(value, currentValue)) {
|
||||
return false; // no change
|
||||
}
|
||||
}
|
||||
if (toSetter == null) {
|
||||
throw new IllegalStateException(
|
||||
"Setter counterpart for " + fromMethod + " is not found in " + toClass.getName() +
|
||||
" and the 'from' value is not equal to the 'to' value");
|
||||
}
|
||||
toSetter.accept(to, propertyProcessor.apply(value));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (from, to, setNullValues, entityManager) -> {
|
||||
boolean updated = false;
|
||||
for (final CopyFunction fieldSetter : propertySetters) {
|
||||
updated = fieldSetter.apply(from, to, setNullValues, entityManager) || updated;
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
}
|
||||
|
||||
// java:S3011 - low-level, reflection utility. intentionally changes the accessibility
|
||||
@SuppressWarnings("java:S3011")
|
||||
private static BiConsumer<Object, Object> toSetter(
|
||||
final Class<?> toClass, final String setterName, final String fieldName, final Class<?> type) {
|
||||
try {
|
||||
final Method toSetterMethod = getMethod(toClass, setterName, type);
|
||||
return (to, value) -> {
|
||||
try {
|
||||
toSetterMethod.invoke(to, value);
|
||||
} catch (final InvocationTargetException e) {
|
||||
throw new IllegalStateException(
|
||||
"Error invoking " + toSetterMethod,
|
||||
e.getTargetException() == null ? e : e.getTargetException());
|
||||
} catch (final IllegalAccessException | IllegalArgumentException e) {
|
||||
throw new IllegalStateException("Error invoking " + toSetterMethod, e);
|
||||
}
|
||||
};
|
||||
} catch (final NoSuchMethodException nsme) {
|
||||
final Field field = getField(toClass, fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
} else {
|
||||
return (to, value) -> {
|
||||
try {
|
||||
field.set(to, value);
|
||||
} catch (final IllegalAccessException | IllegalArgumentException e) {
|
||||
throw new IllegalStateException("Error setting field " + field, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UnaryOperator<Object> toGetter(final Class<?> toClass, final String getterName, final String fieldName) {
|
||||
try {
|
||||
final Method toGetterMethod = getMethod(toClass, getterName);
|
||||
return to -> {
|
||||
try {
|
||||
return toGetterMethod.invoke(to);
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException("Error invoking " + toGetterMethod, e);
|
||||
}
|
||||
};
|
||||
} catch (final NoSuchMethodException e) {
|
||||
// no method to get current value of the target field, try with field access
|
||||
final Field field = getField(toClass, fieldName); // is or get
|
||||
return field == null ? null : to -> {
|
||||
try {
|
||||
return field.get(to);
|
||||
} catch (final Exception e2) {
|
||||
throw new IllegalStateException("Error getting field " + field, e2);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIs(final Method fromMethod, final String methodName) {
|
||||
return methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2)) &&
|
||||
fromMethod.getReturnType() == boolean.class;
|
||||
}
|
||||
|
||||
private static boolean isGet(final String methodName) {
|
||||
return methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3));
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3011") // low-level, reflection utility. intentionally changes the accessibility
|
||||
private static Field getField(final Class<?> clazz, final String fieldName) {
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
if (fieldName.equals(field.getName())) {
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
final Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass == null) {
|
||||
return null;
|
||||
} else {
|
||||
return getField(superClass, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes)
|
||||
throws NoSuchMethodException {
|
||||
try {
|
||||
return getMethod(clazz, clazz, methodName, parameterTypes);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
if (parameterTypes.length == 1 && parameterTypes[0] == Boolean.class) {
|
||||
try {
|
||||
return getMethod(clazz, methodName, boolean.class);
|
||||
} catch (final NoSuchMethodException e2) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3011") // low-level, reflection utility. intentionally changes the accessibility
|
||||
private static Method getMethod(final Class<?> target, final Class<?> clazz, final String methodName, final Class<?>... parameterTypes)
|
||||
throws NoSuchMethodException {
|
||||
try {
|
||||
final Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
} catch (final NoSuchMethodException e) {
|
||||
final Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass == null) {
|
||||
throw new NoSuchMethodException("Method " + methodName + " not found in " + target.getSimpleName());
|
||||
} else {
|
||||
return getMethod(target, superClass, methodName, parameterTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Method> getMethods(final Class<?> clazz) {
|
||||
final List<Method> methods = new ArrayList<>();
|
||||
for (final Method method : clazz.getDeclaredMethods()) {
|
||||
if (!method.isSynthetic() && !method.isBridge()) {
|
||||
methods.add(method);
|
||||
}
|
||||
}
|
||||
final Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null) {
|
||||
methods.addAll(getMethods(superClass));
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
// key for copy of class to class function
|
||||
private record FromTo(Class<?> from, Class<?> to) {}
|
||||
|
||||
// functional interface to apply the copy operation
|
||||
private interface CopyFunction {
|
||||
|
||||
boolean apply(Object from, Object to, boolean setNullValues, UnaryOperator<Object> propertyProcessor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user