Skip to content
Open
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
107 changes: 107 additions & 0 deletions bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/BeanD.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
package org.apache.bval.jsr.descriptor;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

Expand All @@ -31,6 +33,7 @@
import jakarta.validation.metadata.MethodType;
import jakarta.validation.metadata.PropertyDescriptor;

import org.apache.bval.jsr.groups.Group;
import org.apache.bval.jsr.groups.GroupStrategy;
import org.apache.bval.jsr.metadata.Signature;
import org.apache.bval.jsr.util.ToUnmodifiable;
Expand All @@ -52,6 +55,18 @@ public class BeanD<T> extends ElementD<Class<T>, MetadataReader.ForBean<T>> impl
private final Set<ConstructorDescriptor> constrainedConstructors;
private final Map<Set<MethodType>, Set<MethodDescriptor>> methodCache = new HashMap<>();

/** Leaf (non-composed) delegates of {@link #properties}. */
private final PropertyD<?>[] leafProperties;
/**
* Requested group strategy to the strategy with {@link Group#DEFAULT} redefined as this bean's default group
* sequence; {@link #UNCHANGED} when the redefinition is a no-op.
*/
private final GroupStrategyMap<GroupStrategy> localGroupStrategies = new GroupStrategyMap<>();
private final GroupStrategyMap<PropertyD<?>[]> propertiesByGroups = new GroupStrategyMap<>();
private final GroupStrategyMap<GroupPlan> plans = new GroupStrategyMap<>();

private static final GroupStrategy UNCHANGED = GroupStrategy.simple(Collections.emptySet());

BeanD(MetadataReader.ForBean<T> reader) {
super(reader);
this.beanClass = reader.meta.getHost();
Expand All @@ -65,6 +80,11 @@ public class BeanD<T> extends ElementD<Class<T>, MetadataReader.ForBean<T>> impl
cfpn.put(p.getPropertyName(), p);
}
constraintsForPropertyByName = Map.copyOf(cfpn);
final List<PropertyD<?>> leaves = new ArrayList<>();
for (PropertyDescriptor p : properties) {
ComposedD.forEachUnwrapped(p, PropertyD.class, leaves::add);
}
leafProperties = leaves.toArray(new PropertyD<?>[0]);
constructors = reader.getConstructors(this);
methods = reader.getMethods(this);

Expand Down Expand Up @@ -141,6 +161,93 @@ public GroupStrategy getGroupStrategy() {
return groupStrategy;
}

/**
* Get {@code groups} with {@link Group#DEFAULT} redefined as this bean's default group sequence. Returns
* {@code groups} itself (same instance) when no redefinition applies.
*
* @param groups
* @return {@link GroupStrategy}
*/
@Override
public GroupStrategy getLocalGroupStrategy(GroupStrategy groups) {
GroupStrategy result = localGroupStrategies.get(groups);
if (result == null) {
final GroupStrategy computed =
GroupStrategy.redefining(groups, Collections.singletonMap(Group.DEFAULT, groupStrategy));
result = localGroupStrategies.putIfAbsent(groups, computed == groups ? UNCHANGED : computed);
}
return result == UNCHANGED ? groups : result;
}

/**
* What validating this bean for a requested group strategy involves.
*/
public static final class GroupPlan {
private final GroupStrategy localGroupStrategy;
private final PropertyD<?>[] properties;

GroupPlan(GroupStrategy localGroupStrategy, PropertyD<?>[] properties) {
this.localGroupStrategy = localGroupStrategy;
this.properties = properties;
}

/**
* @param groups the requested strategy this plan was obtained for
* @return {@link BeanD#getLocalGroupStrategy(GroupStrategy)} of {@code groups}
*/
public GroupStrategy getLocalGroupStrategy(GroupStrategy groups) {
return localGroupStrategy == null ? groups : localGroupStrategy;
}

/**
* @return the leaf properties that need a validation frame for the local group strategy; must not be modified
*/
public PropertyD<?>[] getProperties() {
return properties;
}
}

/**
* Get the {@link GroupPlan} for validating this bean for {@code groups}, combining
* {@link #getLocalGroupStrategy(GroupStrategy)} and the properties that need a validation frame in one lookup.
*
* @param groups
* @return {@link GroupPlan}
*/
public GroupPlan getPlan(GroupStrategy groups) {
final GroupPlan cached = plans.get(groups);
if (cached != null) {
return cached;
}
final GroupStrategy local = getLocalGroupStrategy(groups);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usally use computeIfAbsent in this case, saves a call to the method

return plans.putIfAbsent(groups, new GroupPlan(local == groups ? null : local, getPropertiesFor(local)));
}

/**
* Get the leaf constrained properties that need a validation frame for {@code groups}: those with constraints
* applying to any of its groups, with constrained container element types, or that are cascaded. The result is
* cached and must not be modified.
*
* @param groups
* @return {@link PropertyD} array, possibly empty
*/
private PropertyD<?>[] getPropertiesFor(GroupStrategy groups) {
final PropertyD<?>[] cached = propertiesByGroups.get(groups);
if (cached != null) {
return cached;
}
final Set<Group> targetGroups = groups.getGroups();
final List<PropertyD<?>> relevant = new ArrayList<>(leafProperties.length);
for (PropertyD<?> p : leafProperties) {
if (p.isCascadedDeep() || !p.getConstrainedContainerElementTypes().isEmpty()
|| p.getConstraintDescriptors().stream().map(ConstraintD.class::cast)
.anyMatch(c -> matchesGroups(c, targetGroups))) {
relevant.add(p);
}
}
return propertiesByGroups.putIfAbsent(groups, relevant.toArray(new PropertyD<?>[0]));
}

public final Type getGenericType() {
return getElementClass();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public abstract class CascadableContainerD<P extends ElementD<?, ?>, E extends A
private final boolean cascaded;
private final Set<GroupConversion> groupConversions;
private final Set<ContainerElementTypeDescriptor> containerElementTypes;
private final boolean cascadedDeep;

protected CascadableContainerD(MetadataReader.ForContainer<E> reader, P parent) {
super(reader, parent);
Expand All @@ -45,6 +46,17 @@ protected CascadableContainerD(MetadataReader.ForContainer<E> reader, P parent)
.stream()
.filter(DescriptorManager::isConstrained)
.collect(ToUnmodifiable.set());
cascadedDeep = cascaded || containerElementTypes.stream().anyMatch(DescriptorManager::isCascaded);
}

/**
* Learn whether this element or any of its constrained container element types (recursively) is cascaded.
*
* @return {@code boolean}
* @see DescriptorManager#isCascaded
*/
public boolean isCascadedDeep() {
return cascadedDeep;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public static <D extends ElementDescriptor & CascadableDescriptor & ContainerDes

public static <D extends ElementDescriptor & CascadableDescriptor & ContainerDescriptor> boolean isCascaded(
D descriptor) {
if (descriptor instanceof CascadableContainerD<?, ?>) {
return ((CascadableContainerD<?, ?>) descriptor).isCascadedDeep();
}
return descriptor != null && (descriptor.isCascaded()
|| descriptor.getConstrainedContainerElementTypes().stream().anyMatch(DescriptorManager::isCascaded));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import java.util.Map;
import java.util.Set;

import jakarta.validation.groups.Default;
import jakarta.validation.metadata.ConstraintDescriptor;
import jakarta.validation.metadata.ElementDescriptor;

import org.apache.bval.jsr.groups.Group;
import org.apache.bval.jsr.groups.GroupStrategy;
import org.apache.bval.jsr.groups.GroupsComputer;
import org.apache.bval.jsr.metadata.Meta;
Expand Down Expand Up @@ -70,20 +72,48 @@ final protected BeanD<?> getBean() {
public final GroupStrategy getGroupStrategy() {
return getBean().getGroupStrategy();
}

@Override
public final GroupStrategy getLocalGroupStrategy(GroupStrategy groups) {
return getBean().getLocalGroupStrategy(groups);
}
}

private static final ConstraintD<?>[] NO_CONSTRAINTS = {};

/**
* Learn whether {@code constraint} applies to any of {@code targetGroups}.
*
* @param constraint
* @param targetGroups
* @return {@code boolean}
*/
static boolean matchesGroups(ConstraintD<?> constraint, Set<Group> targetGroups) {
final Set<Class<?>> constraintGroups = constraint.getGroups();
final boolean impliesDefault = constraintGroups.contains(Default.class);
for (final Group target : targetGroups) {
final Class<?> g = target.getGroup();
if (constraintGroups.contains(g) || impliesDefault && constraint.getDeclaringClass().equals(g)) {
return true;
}
}
return false;
}

protected final Type genericType;
final GroupsComputer groupsComputer;

private final Meta<E> meta;
private final Set<ConstraintD<?>> constraints;
private final GroupStrategyMap<ConstraintD<?>[]> constraintsByGroups;

protected ElementD(R reader) {
super();
Validate.notNull(reader, "reader");
this.meta = reader.meta;
this.genericType = reader.meta.getType();
this.constraints = reader.getConstraints();
this.constraintsByGroups = constraints.isEmpty() ? null : new GroupStrategyMap<>();
this.groupsComputer = reader.getValidatorFactory().getGroupsComputer();
}

Expand All @@ -98,6 +128,26 @@ public Set<ConstraintDescriptor<?>> getConstraintDescriptors() {
return (Set) constraints;
}

/**
* Get the constraints of this element that apply to any group of {@code groups}, in declaration order. The
* result is cached per {@link GroupStrategy} and must not be modified.
*
* @param groups
* @return {@link ConstraintD} array, possibly empty
*/
public ConstraintD<?>[] getConstraintsFor(GroupStrategy groups) {
if (constraintsByGroups == null) {
return NO_CONSTRAINTS;
}
final ConstraintD<?>[] cached = constraintsByGroups.get(groups);
if (cached != null) {
return cached;
}
final Set<Group> targetGroups = groups.getGroups();
return constraintsByGroups.putIfAbsent(groups,
constraints.stream().filter(c -> matchesGroups(c, targetGroups)).toArray(ConstraintD<?>[]::new));
}

@Override
public final ConstraintFinder findConstraints() {
return new Finder(groupsComputer, this);
Expand All @@ -119,6 +169,15 @@ public final Class<?> getDeclaringClass() {

public abstract GroupStrategy getGroupStrategy();

/**
* Get {@code groups} with {@link Group#DEFAULT} redefined as {@link #getGroupStrategy()}. Returns
* {@code groups} itself (same instance) when no redefinition applies.
*
* @param groups
* @return {@link GroupStrategy}
*/
public abstract GroupStrategy getLocalGroupStrategy(GroupStrategy groups);

@Override
public String toString() {
return String.format("%s: %s", getClass().getSimpleName(), meta.describeHost());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.bval.jsr.descriptor;

import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.bval.jsr.groups.GroupStrategy;

/**
* Thread-safe cache of values computed per {@link GroupStrategy}. Validation passes the same few strategy instances
* over and over, so lookups first scan a small array of recently seen instances by identity and only then fall back
* to an equality-keyed map.
*
* @param <V> value type
*/
final class GroupStrategyMap<V> {
private static final int MAX_IDENTITY_ENTRIES = 8;
private static final Object[] NO_ENTRIES = {};

private final ConcurrentMap<GroupStrategy, V> byEquality = new ConcurrentHashMap<>();
/** Alternating key, value pairs; replaced wholesale, so readers always see a consistent array. */
private volatile Object[] byIdentity = NO_ENTRIES;

/**
* @return the value for {@code key}, or {@code null} if none has been stored yet
*/
@SuppressWarnings("unchecked")
V get(GroupStrategy key) {
final Object[] entries = byIdentity;
for (int i = 0; i < entries.length; i += 2) {
if (entries[i] == key) {
return (V) entries[i + 1];
}
}
final V value = byEquality.get(key);
if (value != null) {
rememberIdentity(entries, key, value);
}
return value;
}

/**
* Store {@code value} for {@code key} unless a value is already present.
*
* @return the value now stored for {@code key}
*/
V putIfAbsent(GroupStrategy key, V value) {
final V previous = byEquality.putIfAbsent(key, value);
final V result = previous == null ? value : previous;
rememberIdentity(byIdentity, key, result);
return result;
}

private void rememberIdentity(Object[] entries, GroupStrategy key, V value) {
if (entries.length < 2 * MAX_IDENTITY_ENTRIES) {
// racing updates may drop an entry, which only costs a later equality lookup
final Object[] grown = Arrays.copyOf(entries, entries.length + 2);
grown[entries.length] = key;
grown[entries.length + 1] = value;
byIdentity = grown;
}
}
}
Loading
Loading