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
218 changes: 167 additions & 51 deletions src/main/java/net/datafaker/service/FakeValuesService.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class FakeValuesService {

private static final JsonTransformer<Object> JSON_TRANSFORMER = JsonTransformer.builder().build();

private final Map<String, RgxGen> expression2generex = new CopyOnWriteMap<>(WeakHashMap::new);
private static final Map<String, RgxGen> expression2generex = new CopyOnWriteMap<>(WeakHashMap::new);
private final CopyOnWriteMap<SingletonLocale, Map<String, String>> key2Expression = new CopyOnWriteMap<>(IdentityHashMap::new);
private static final Map<String, String[]> ARGS_2_SPLITTED_ARGS = new CopyOnWriteMap<>(WeakHashMap::new);

Expand All @@ -81,7 +81,17 @@ public class FakeValuesService {

private static final Map<String, String[]> EXPRESSION_2_SPLITTED = new CopyOnWriteMap<>(WeakHashMap::new);

private final Map<RegExpContext, ValueResolver> REGEXP2SUPPLIER_MAP = new CopyOnWriteMap<>(HashMap::new);
/**
* L1: static recipe cache — context-free resolvers shared across all Fakers with same locale.
* Growth is bounded in practice by unique YAML expressions × locales. User-supplied dynamic
* expressions via {@code faker.expression()} carry the same theoretical unbounded-growth exposure
* as the other static string caches in this class (NAME_2_YAML, EXPRESSION_2_SPLITTED, etc.).
*/
private static final Map<CacheKey, ValueResolver> RECIPE_MAP = new CopyOnWriteMap<>(HashMap::new);

/** L2: per-instance materialized cache — resolvers pre-bound to this Faker's providers for fast repeated calls. */
private final Map<String, ValueResolver> instanceMap = new CopyOnWriteMap<>(HashMap::new);

public void updateFakeValuesInterfaceMap(List<SingletonLocale> locales) {
for (final SingletonLocale l : locales) {
fakeValuesInterfaceMap.computeIfAbsent(l, this::getCachedFakeValue);
Expand Down Expand Up @@ -165,23 +175,22 @@ public String fetchString(String key, FakerContext context) {
return (String) fetch(key, context);
}

private class SafeFetchResolver implements ValueResolver {
private static class SafeFetchResolver implements ValueResolver {
private final String simpleDirective;
private final FakerContext context;

private SafeFetchResolver(String simpleDirective, FakerContext context) {
private SafeFetchResolver(String simpleDirective) {
this.simpleDirective = simpleDirective;
this.context = context;
}

@Override
public Object resolve() {
return safeFetch(simpleDirective, context, null);
public Object resolve(ProviderRegistration root, FakerContext context) {
if (root == null) return null;
return root.fakeValuesService().safeFetch(simpleDirective, context, null);
}
Comment thread
mferretti marked this conversation as resolved.

@Override
public String toString() {
return "%s[simpleDirective=%s, context=%s]".formatted(getClass().getSimpleName(), simpleDirective, context);
return "%s[simpleDirective=%s]".formatted(getClass().getSimpleName(), simpleDirective);
}
}

Expand Down Expand Up @@ -593,20 +602,35 @@ protected String resolveExpression(String expression, Object current, ProviderRe
}
continue;
}
final RegExpContext regExpContext = new RegExpContext(expr, root, context);
final ValueResolver val = REGEXP2SUPPLIER_MAP.get(regExpContext);
final Object resolved;
if (val != null) {
resolved = val.resolve();
// L2: per-instance hit — fast, provider already bound
final ValueResolver fast = instanceMap.get(expr);
if (fast != null) {
resolved = fast.resolve(root, context);
} else {
int j = 0;
final int length = expr.length();
while (j < length && !Character.isWhitespace(expr.charAt(j))) j++;
String directive = expr.substring(0, j);
while (j < length && Character.isWhitespace(expr.charAt(j))) j++;
final String arguments = j == length ? "" : expr.substring(j);
final String[] args = splitArguments(arguments);
resolved = resExp(directive, args, current, root, context, regExpContext);
final CacheKey cacheKey = new CacheKey(expr, context.getSingletonLocale());
// L1: static recipe hit — materialize once, store in L2
final ValueResolver recipe = RECIPE_MAP.get(cacheKey);
if (recipe != null) {
final ValueResolver materialized = root != null ? recipe.materialize(root) : recipe;
if (root != null) instanceMap.put(expr, materialized);
resolved = materialized.resolve(root, context);
} else {
// Both miss: full discovery
int j = 0;
final int length = expr.length();
while (j < length && !Character.isWhitespace(expr.charAt(j))) j++;
String directive = expr.substring(0, j);
while (j < length && Character.isWhitespace(expr.charAt(j))) j++;
final String arguments = j == length ? "" : expr.substring(j);
final String[] args = splitArguments(arguments);
resolved = resolveExpression(directive, args, current, root, context, cacheKey);
// resolveExpression stored recipe in RECIPE_MAP if cacheable; materialize for L2
final ValueResolver stored = RECIPE_MAP.get(cacheKey);
if (stored != null && root != null) {
instanceMap.put(expr, stored.materialize(root));
}
}
}
if (resolved == null) {
throw new RuntimeException("Unable to resolve #{" + expr + "} directive for FakerContext " + context + ".");
Expand Down Expand Up @@ -681,12 +705,12 @@ private String[] splitExpressions(String expression, int length) {
});
}

private Object resExp(String directive, String[] args, Object current, ProviderRegistration root, FakerContext context, RegExpContext regExpContext) {
private Object resolveExpression(String directive, String[] args, Object current, ProviderRegistration root, FakerContext context, CacheKey cacheKey) {
Object res = resolveExpression(directive, args, current, root, context);
LOG.fine(() -> "resExp(%s [%s]) current: %s, root: %s, context: %s, regExpContext: %s -> res: %s".formatted(directive, Arrays.toString(args), current, root, context, regExpContext, res));
LOG.fine(() -> "resolveExpression(%s [%s]) current: %s, root: %s, context: %s, cacheKey: %s -> res: %s".formatted(directive, Arrays.toString(args), current, root, context, cacheKey, res));
if (res instanceof CharSequence) {
if (((CharSequence) res).isEmpty()) {
REGEXP2SUPPLIER_MAP.put(regExpContext, EMPTY_STRING);
RECIPE_MAP.put(cacheKey, EMPTY_STRING);
}
return res;
}
Expand All @@ -696,11 +720,13 @@ private Object resExp(String directive, String[] args, Object current, ProviderR
Object valueResolver = it.next();
Object value;
if (valueResolver instanceof ValueResolver resolver) {
value = resolver.resolve();
value = resolver.resolve(root, context);
if (value == null) {
it.remove();
} else {
REGEXP2SUPPLIER_MAP.put(regExpContext, resolver);
if (resolver.cacheable()) {
RECIPE_MAP.put(cacheKey, resolver);
}
return value;
}
}
Expand Down Expand Up @@ -733,19 +759,19 @@ private Object resolveExpression(String directive, String[] args, Object current
if (current instanceof AbstractProvider) {
final Method method = BaseFaker.getMethod((AbstractProvider<?>) current, directive);
if (method != null) {
res.add(new MethodResolver(method, current, args));
res.add(new ProviderMethodResolver(current.getClass().getSimpleName(), method, args));
return res;
}
Comment thread
mferretti marked this conversation as resolved.
}
res.add(resolveFromMethodOn(current, directive, args));
res.add(resolveFromMethodOn(current, directive, args, root));
}
if (dotIndex > 0) {
if (dotIndex > 0 && root != null) {
String providerClassName = directive.substring(0, dotIndex);
String methodName = directive.substring(dotIndex + 1);
AbstractProvider<?> ap = root.getProvider(providerClassName);
Method method = ap == null ? null : ObjectMethods.getMethodByName(ap, methodName);
if (method != null) {
res.add(new MethodResolver(method, ap, args));
res.add(new ProviderMethodResolver(providerClassName, method, args));
return res;
}
}
Expand All @@ -758,12 +784,12 @@ private Object resolveExpression(String directive, String[] args, Object current
// car.wheel will be looked up in the YAML file.
// It's only "simple" if there aren't args
if (args.length == 0) {
res.add(new SafeFetchResolver(simpleDirective, context));
res.add(new SafeFetchResolver(simpleDirective));
}

// resolve method references on faker object like #{regexify '[a-z]'}
if (dotIndex == -1 && root != null && (current == null || root.getClass() != current.getClass())) {
res.add(resolveFromMethodOn(root, directive, args));
res.add(resolveFromMethodOn(root, directive, args, root));
}

// Resolve Faker Object method references like #{ClassName.method_name}
Expand All @@ -778,7 +804,7 @@ private Object resolveExpression(String directive, String[] args, Object current
// class.method_name (lowercase)
if (dotIndex >= 0) {
final String key = javaNameToYamlName(simpleDirective);
res.add(new SafeFetchResolver(key, context));
res.add(new SafeFetchResolver(key));
}

return res;
Expand Down Expand Up @@ -860,12 +886,23 @@ private String javaNameToYamlName(String expression) {
* {@link Name} then this method would return {@link Name#firstName()}. Returns null if the directive is nested
* (i.e. has a '.') or the method doesn't exist on the <em>obj</em> object.
*/
private ValueResolver resolveFromMethodOn(Object obj, String directive, String[] args) {
private ValueResolver resolveFromMethodOn(Object obj, String directive, String[] args, ProviderRegistration root) {
if (obj == null) {
return null;
}
final MethodAndCoercedArgs accessor = retrieveMethodAccessor(obj, directive, args);
return accessor == null ? NULL_VALUE : new MethodAndCoercedArgsResolver(accessor, obj);
if (accessor == null) return NULL_VALUE;
if (obj instanceof ProviderRegistration) {
return new RootCoercedResolver(accessor);
}
if (obj instanceof AbstractProvider && root != null) {
String providerName = obj.getClass().getSimpleName();
Object registered = root.getProvider(providerName);
if (registered != null && registered.getClass() == obj.getClass()) {
return new NamedProviderCoercedResolver(providerName, accessor);
}
}
return new InstanceCoercedResolver(accessor, obj);
}

/**
Expand Down Expand Up @@ -896,7 +933,7 @@ private ValueResolver resolveFakerObjectAndMethod(ProviderRegistration faker, St
return NULL_VALUE;
}

return new MethodAndCoercedArgsResolver(accessor, objectWithMethodToInvoke);
return new ChainedCoercedResolver(fakerAccessor, accessor);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException("Failed to resolve faker object and method for %s (dotIndex=%s, args=%s)"
.formatted(key, dotIndex, Arrays.toString(args)), e);
Expand Down Expand Up @@ -1148,56 +1185,135 @@ public String toString() {
}
}

private record RegExpContext(String exp, ProviderRegistration root, FakerContext context) {
private record CacheKey(String exp, SingletonLocale locale) {
}

private interface ValueResolver {
Object resolve();
Object resolve(ProviderRegistration root, FakerContext context);
default boolean cacheable() { return true; }
/** Produce a fast per-instance resolver with provider pre-bound. Default: self (already fast or context-dependent). */
default ValueResolver materialize(ProviderRegistration root) { return this; }
}

private record ConstantResolver(String value) implements ValueResolver {
@Override
public Object resolve() {
public Object resolve(ProviderRegistration root, FakerContext context) {
return value;
}
}

private static final ConstantResolver EMPTY_STRING = new ConstantResolver("");
private static final ConstantResolver NULL_VALUE = new ConstantResolver(null);

private record MethodResolver(Method method, Object current, Object[] args) implements ValueResolver {
/** L2: fast resolver — method pre-bound to a specific provider instance. Never stored in L1. */
private record InstanceMethodResolver(Object provider, Method method, Object[] args) implements ValueResolver {
@Override
public Object resolve() {
public Object resolve(ProviderRegistration root, FakerContext context) {
try {
return method.invoke(current);
return method.invoke(provider);
} catch (Exception e) {
throw new RuntimeException("Failed to call method %s.%s() on %s (args: %s)".formatted(
method.getDeclaringClass().getName(), method.getName(), current, Arrays.toString(args)), e);
method.getDeclaringClass().getName(), method.getName(), provider, Arrays.toString(args)), e);
}
}

@Override
public boolean cacheable() { return false; }
}

/** L1 recipe: resolves a no-arg method on a named provider looked up from root at call time. */
private record ProviderMethodResolver(String providerName, Method method, Object[] args) implements ValueResolver {
@Override
public Object resolve(ProviderRegistration root, FakerContext context) {
if (root == null) return null;
return new InstanceMethodResolver(root.getProvider(providerName), method, args).resolve(root, context);
}

@Override
public ValueResolver materialize(ProviderRegistration root) {
if (root == null) return this;
return new InstanceMethodResolver(root.getProvider(providerName), method, args);
}

@Override
public String toString() {
return "%s[method=%s.%s(), current=%s, args=%s]".formatted(getClass().getSimpleName(),
method.getDeclaringClass().getSimpleName(), method.getName(), current, Arrays.toString(args));
return "%s[provider=%s, method=%s.%s(), args=%s]".formatted(getClass().getSimpleName(),
providerName, method.getDeclaringClass().getSimpleName(), method.getName(), Arrays.toString(args));
}
}

private record MethodAndCoercedArgsResolver(MethodAndCoercedArgs accessor, Object obj) implements ValueResolver {
/** L1 recipe: resolves a coerced-args method directly on the root ProviderRegistration. */
private record RootCoercedResolver(MethodAndCoercedArgs accessor) implements ValueResolver {
@Override
public Object resolve(ProviderRegistration root, FakerContext context) {
if (root == null) return null;
return invokeCoerced(accessor, root);
}

@Override
public Object resolve() {
return invokeAndToString(accessor, obj);
public ValueResolver materialize(ProviderRegistration root) {
if (root == null) return this;
return new InstanceCoercedResolver(accessor, root);
}
}

private static Object invokeAndToString(MethodAndCoercedArgs accessor, Object objectWithMethodToInvoke) {
/** L1 recipe: resolves a coerced-args method on a named provider looked up from root at call time. */
private record NamedProviderCoercedResolver(String providerName, MethodAndCoercedArgs accessor) implements ValueResolver {
@Override
public Object resolve(ProviderRegistration root, FakerContext context) {
if (root == null) return null;
return invokeCoerced(accessor, root.getProvider(providerName));
}

@Override
public ValueResolver materialize(ProviderRegistration root) {
if (root == null) return this;
return new InstanceCoercedResolver(accessor, root.getProvider(providerName));
}
}

/** L1 recipe: two-step chain — invokes fakerAccessor on root to get provider, then accessor on it. */
private record ChainedCoercedResolver(MethodAndCoercedArgs fakerAccessor, MethodAndCoercedArgs accessor) implements ValueResolver {
@Override
public Object resolve(ProviderRegistration root, FakerContext context) {
if (root == null) return null;
try {
return invokeCoerced(accessor, fakerAccessor.invoke(root));
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException("Failed to invoke chained resolver on %s".formatted(root), unwrap(e));
}
}

@Override
public ValueResolver materialize(ProviderRegistration root) {
if (root == null) return this;
try {
return accessor.invoke(objectWithMethodToInvoke);
return new InstanceCoercedResolver(accessor, fakerAccessor.invoke(root));
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException("Failed to invoke %s on %s".formatted(accessor, objectWithMethodToInvoke), unwrap(e));
throw new RuntimeException("Failed to materialize chained resolver on %s".formatted(root), unwrap(e));
}
}
}

/** L2: fast resolver — coerced method pre-bound to a specific instance. Also used for non-registered providers (never in L1). */
private record InstanceCoercedResolver(MethodAndCoercedArgs accessor, Object instance) implements ValueResolver {
@Override
public Object resolve(ProviderRegistration root, FakerContext context) {
return invokeCoerced(accessor, instance);
}

@Override
public boolean cacheable() { return false; }
}

private static Object invokeCoerced(MethodAndCoercedArgs accessor, Object target) {
try {
return accessor.invoke(target);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException("Failed to invoke %s on %s".formatted(accessor, target), unwrap(e));
}
}

private static Throwable unwrap(Throwable e) {
return e instanceof InvocationTargetException reflection ? unwrap(reflection.getTargetException()) : e;
}
Expand Down
Loading
Loading