io.evitadb.api.proxy.impl.ProxycianFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of evita_api Show documentation
Show all versions of evita_api Show documentation
Module contains external API of the evitaDB.
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2023
*
* Licensed under the Business Source License, Version 1.1 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/FgForrest/evitaDB/blob/master/LICENSE
*
* 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 io.evitadb.api.proxy.impl;
import io.evitadb.api.exception.EntityClassInvalidException;
import io.evitadb.api.proxy.ProxyFactory;
import io.evitadb.api.proxy.ProxyReferenceFactory;
import io.evitadb.api.proxy.SealedEntityProxy;
import io.evitadb.api.proxy.SealedEntityReferenceProxy;
import io.evitadb.api.proxy.impl.AbstractEntityProxyState.ProxyInstanceCacheKey;
import io.evitadb.api.proxy.impl.AbstractEntityProxyState.ProxyWithUpsertCallback;
import io.evitadb.api.proxy.impl.entity.EntityContractAdvice;
import io.evitadb.api.proxy.impl.entity.GetAssociatedDataMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetAttributeMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetEntityTypeMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetLocalesMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetParentEntityMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetPriceMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetPrimaryKeyMethodClassifier;
import io.evitadb.api.proxy.impl.entity.GetReferenceMethodClassifier;
import io.evitadb.api.proxy.impl.entityBuilder.EntityBuilderAdvice;
import io.evitadb.api.proxy.impl.reference.EntityReferenceContractAdvice;
import io.evitadb.api.proxy.impl.reference.GetReferenceAttributeMethodClassifier;
import io.evitadb.api.proxy.impl.reference.GetReferencedEntityMethodClassifier;
import io.evitadb.api.proxy.impl.reference.GetReferencedEntityPrimaryKeyMethodClassifier;
import io.evitadb.api.proxy.impl.reference.GetReferencedGroupEntityPrimaryKeyMethodClassifier;
import io.evitadb.api.proxy.impl.referenceBuilder.EntityReferenceBuilderAdvice;
import io.evitadb.api.requestResponse.data.EntityContract;
import io.evitadb.api.requestResponse.data.ReferenceContract;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.api.requestResponse.schema.ReferenceSchemaContract;
import io.evitadb.function.ExceptionRethrowingBiFunction;
import io.evitadb.function.ExceptionRethrowingFunction;
import io.evitadb.function.ExceptionRethrowingIntBiFunction;
import io.evitadb.function.ExceptionRethrowingIntTriFunction;
import io.evitadb.utils.ArrayUtils;
import io.evitadb.utils.CollectionUtils;
import io.evitadb.utils.ReflectionLookup;
import lombok.RequiredArgsConstructor;
import one.edee.oss.proxycian.bytebuddy.ByteBuddyProxyGenerator;
import one.edee.oss.proxycian.recipe.Advice;
import one.edee.oss.proxycian.recipe.ProxyRecipe;
import one.edee.oss.proxycian.trait.delegate.DelegateCallsAdvice;
import one.edee.oss.proxycian.trait.localDataStore.LocalDataStoreAdvice;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static io.evitadb.utils.ClassUtils.isAbstract;
import static io.evitadb.utils.ClassUtils.isFinal;
/**
* Implementation of the {@link ProxyFactory} interface based on Proxycian (ByteBuddy) library.
*
* @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2023
*/
@RequiredArgsConstructor
public class ProxycianFactory implements ProxyFactory {
/**
* Function that creates a default {@link ProxyRecipe} for an entity.
*/
final static Function DEFAULT_ENTITY_RECIPE = cacheKey -> new ProxyRecipe(
new Class>[]{cacheKey.type()},
new Advice[]{
new DelegateCallsAdvice<>(SealedEntityProxy.class, Function.identity(), true),
LocalDataStoreAdvice.INSTANCE,
EntityContractAdvice.INSTANCE,
EntityBuilderAdvice.INSTANCE
}
);
/**
* Function that creates a default {@link ProxyRecipe} for an entity reference.
*/
final static Function DEFAULT_ENTITY_REFERENCE_RECIPE = cacheKey -> new ProxyRecipe(
new Class>[]{cacheKey.subType()},
new Advice[]{
new DelegateCallsAdvice<>(SealedEntityReferenceProxy.class, Function.identity(), true),
LocalDataStoreAdvice.INSTANCE,
EntityReferenceContractAdvice.INSTANCE,
EntityReferenceBuilderAdvice.INSTANCE
}
);
/**
* Cache for the identified best matching constructors to speed up proxy creation.
*/
private final static ConcurrentHashMap> ENTITY_CONSTRUCTOR_CACHE = CollectionUtils.createConcurrentHashMap(256);
private final static ConcurrentHashMap> REFERENCE_CONSTRUCTOR_CACHE = CollectionUtils.createConcurrentHashMap(256);
/**
* The map of recipes provided from outside that are used to build the proxy.
*/
private final Map recipes = new ConcurrentHashMap<>(64);
/**
* The merged map all recipes - the ones provided from outside and the ones created with default configuration on
* the fly during the proxy building.
*/
private final Map collectedRecipes = new ConcurrentHashMap<>(64);
/**
* The reflection lookup instance used to access the reflection data in a memoized fashion.
*/
private final ReflectionLookup reflectionLookup;
/**
* Creates a new proxy instance for passed {@link EntityContract} instance.
*/
static T createEntityProxy(
@Nonnull Class expectedType,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReflectionLookup reflectionLookup
) {
return createProxy(
expectedType, recipes, collectedRecipes, entity, referencedEntitySchemas, reflectionLookup,
theCacheKey -> collectedRecipes.computeIfAbsent(theCacheKey, DEFAULT_ENTITY_RECIPE),
null
);
}
/**
* Creates a new proxy instance for passed {@link EntityContract} instance.
*/
static T createEntityProxy(
@Nonnull Class expectedType,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReflectionLookup reflectionLookup,
@Nullable Consumer stateInitializer
) {
return createProxy(
expectedType, recipes, collectedRecipes, entity, referencedEntitySchemas, reflectionLookup,
theCacheKey -> collectedRecipes.computeIfAbsent(theCacheKey, DEFAULT_ENTITY_RECIPE),
stateInitializer
);
}
/**
* Creates a new proxy instance for passed {@link EntityContract} and {@link ReferenceContract} instance.
*/
static T createEntityReferenceProxy(
@Nonnull Class> mainType,
@Nonnull Class expectedType,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull EntityContract entity,
@Nonnull Supplier entityPrimaryKeySupplier,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReferenceContract reference,
@Nonnull ReflectionLookup reflectionLookup,
@Nullable Map instanceCache
) {
return createReferenceProxy(
mainType, expectedType, recipes, collectedRecipes,
entity, entityPrimaryKeySupplier,
referencedEntitySchemas, reference, reflectionLookup,
theCacheKey -> collectedRecipes.computeIfAbsent(theCacheKey, DEFAULT_ENTITY_REFERENCE_RECIPE),
instanceCache
);
}
/**
* Creates a new proxy instance for passed {@link EntityContract} instance.
*/
private static T createProxy(
@Nonnull Class expectedType,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReflectionLookup reflectionLookup,
@Nonnull Function recipeLocator,
@Nullable Consumer stateInitializer
) {
try {
final String entityName = entity.getSchema().getName();
final ProxyEntityCacheKey cacheKey = new ProxyEntityCacheKey(expectedType, entityName);
if (expectedType.isRecord()) {
final BestMatchingEntityConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas, reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup),
new DirectProxyReferenceFactory(recipes, collectedRecipes, reflectionLookup)
);
return bestMatchingConstructor.constructor().newInstance(
bestMatchingConstructor.constructorArguments(entity)
);
} else {
if (expectedType.isInterface()) {
final SealedEntityProxyState proxyState = new SealedEntityProxyState(
entity, referencedEntitySchemas, expectedType, recipes, collectedRecipes, reflectionLookup
);
if (stateInitializer != null) {
stateInitializer.accept(proxyState);
}
final ProxyRecipe recipe = recipeLocator.apply(cacheKey);
return ByteBuddyProxyGenerator.instantiate(
recipe,
proxyState,
recipe.getInterfaces()[0].getClassLoader()
);
} else if (!isFinal(expectedType)) {
final BestMatchingEntityConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas, reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup),
new DirectProxyReferenceFactory(recipes, collectedRecipes, reflectionLookup)
);
final SealedEntityProxyState proxyState = new SealedEntityProxyState(
entity, referencedEntitySchemas, expectedType, recipes, collectedRecipes, reflectionLookup
);
if (stateInitializer != null) {
stateInitializer.accept(proxyState);
}
final ProxyRecipe recipe = recipeLocator.apply(cacheKey);
return ByteBuddyProxyGenerator.instantiate(
recipe,
proxyState,
bestMatchingConstructor.constructor().getParameterTypes(),
bestMatchingConstructor.constructorArguments(entity),
recipe.getInterfaces()[0].getClassLoader()
);
} else {
final BestMatchingEntityConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas, reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup),
new DirectProxyReferenceFactory(recipes, collectedRecipes, reflectionLookup)
);
return bestMatchingConstructor.constructor().newInstance(
bestMatchingConstructor.constructorArguments(entity)
);
}
}
} catch (Exception e) {
throw new EntityClassInvalidException(expectedType, e);
}
}
/**
* Creates a new proxy instance for passed {@link EntityContract} and {@link ReferenceContract} instance.
*/
private static T createReferenceProxy(
@Nonnull Class> mainType,
@Nonnull Class expectedType,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull EntityContract entity,
@Nonnull Supplier entityPrimaryKeySupplier,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReferenceContract reference,
@Nonnull ReflectionLookup reflectionLookup,
@Nonnull Function recipeLocator,
@Nullable Map instanceCache
) {
try {
final String entityName = entity.getSchema().getName();
final ProxyEntityCacheKey cacheKey = new ProxyEntityCacheKey(mainType, entityName, expectedType, reference.getReferenceName());
if (expectedType.isRecord()) {
final BestMatchingReferenceConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas,
reference.getReferenceSchemaOrThrow(), reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup)
);
return bestMatchingConstructor.constructor().newInstance(
bestMatchingConstructor.constructorArguments(entity, reference)
);
} else {
if (expectedType.isInterface()) {
final ProxyRecipe recipe = recipeLocator.apply(cacheKey);
return ByteBuddyProxyGenerator.instantiate(
recipe,
new SealedEntityReferenceProxyState(
entity, entityPrimaryKeySupplier,
referencedEntitySchemas, reference,
mainType, expectedType, recipes,
collectedRecipes, reflectionLookup,
instanceCache
),
recipe.getInterfaces()[0].getClassLoader()
);
} else if (isAbstract(expectedType)) {
final BestMatchingReferenceConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas,
reference.getReferenceSchemaOrThrow(), reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup)
);
final ProxyRecipe recipe = recipeLocator.apply(cacheKey);
return ByteBuddyProxyGenerator.instantiate(
recipe,
new SealedEntityReferenceProxyState(
entity, entityPrimaryKeySupplier,
referencedEntitySchemas, reference,
mainType, expectedType,
recipes, collectedRecipes, reflectionLookup,
instanceCache
),
bestMatchingConstructor.constructor().getParameterTypes(),
bestMatchingConstructor.constructorArguments(entity, reference),
recipe.getInterfaces()[0].getClassLoader()
);
} else {
final BestMatchingReferenceConstructorWithExtractionLambda bestMatchingConstructor = findBestMatchingConstructor(
cacheKey, entity.getSchema(), referencedEntitySchemas,
reference.getReferenceSchemaOrThrow(), reflectionLookup,
new DirectProxyFactory(recipes, collectedRecipes, reflectionLookup)
);
return bestMatchingConstructor.constructor().newInstance(
bestMatchingConstructor.constructorArguments(entity, reference)
);
}
}
} catch (Exception e) {
throw new EntityClassInvalidException(expectedType, e);
}
}
/**
* Method tries to identify the best matching constructor for passed {@link EntitySchemaContract} and {@link Class}
* type. It tries to find a constructor with most of the arguments matching the schema fields.
*/
private static BestMatchingEntityConstructorWithExtractionLambda findBestMatchingConstructor(
@Nonnull ProxyEntityCacheKey cacheKey,
@Nonnull EntitySchemaContract schema,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReflectionLookup reflectionLookup,
@Nonnull ProxyFactory proxyFactory,
@Nonnull ProxyReferenceFactory proxyReferenceFactory
) {
final Class> expectedType = cacheKey.type();
if (ENTITY_CONSTRUCTOR_CACHE.containsKey(cacheKey)) {
//noinspection unchecked
return (BestMatchingEntityConstructorWithExtractionLambda) ENTITY_CONSTRUCTOR_CACHE.get(cacheKey);
} else {
int bestConstructorScore = Integer.MIN_VALUE;
BestMatchingEntityConstructorWithExtractionLambda bestConstructor = null;
for (Constructor> declaredConstructor : expectedType.getDeclaredConstructors()) {
int score = 0;
final Parameter[] parameters = declaredConstructor.getParameters();
//noinspection unchecked
final ExceptionRethrowingFunction[] argumentExtractors =
new ExceptionRethrowingFunction[parameters.length];
for (int i = 0; i < parameters.length; i++) {
final ExceptionRethrowingFunction pkFct =
GetPrimaryKeyMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup
);
if (pkFct != null) {
argumentExtractors[i] = pkFct;
score++;
continue;
}
final ExceptionRethrowingFunction localeFct =
GetLocalesMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i]
);
if (localeFct != null) {
argumentExtractors[i] = localeFct;
score++;
continue;
}
final ExceptionRethrowingFunction entityTypeFct =
GetEntityTypeMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup
);
if (entityTypeFct != null) {
argumentExtractors[i] = entityTypeFct;
score++;
continue;
}
final ExceptionRethrowingFunction attributeFct =
GetAttributeMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup, schema
);
if (attributeFct != null) {
argumentExtractors[i] = attributeFct;
score++;
continue;
}
final ExceptionRethrowingFunction priceFct =
GetPriceMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup, schema
);
if (priceFct != null) {
argumentExtractors[i] = priceFct;
score++;
continue;
}
final ExceptionRethrowingFunction parentFct =
GetParentEntityMethodClassifier.getExtractorIfPossible(
referencedEntitySchemas, expectedType, parameters[i], reflectionLookup, proxyFactory
);
if (parentFct != null) {
argumentExtractors[i] = parentFct;
score++;
continue;
}
final ExceptionRethrowingFunction referenceFct =
GetReferenceMethodClassifier.getExtractorIfPossible(
schema, referencedEntitySchemas, expectedType, parameters[i], reflectionLookup,
(itemType, entity) -> proxyFactory.createEntityProxy(itemType, entity, referencedEntitySchemas),
proxyReferenceFactory
);
if (referenceFct != null) {
argumentExtractors[i] = referenceFct;
score++;
continue;
}
final ExceptionRethrowingFunction associatedDataFct =
GetAssociatedDataMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup, schema
);
if (associatedDataFct != null) {
argumentExtractors[i] = associatedDataFct;
score++;
continue;
}
argumentExtractors[i] = entity -> null;
}
if (score > bestConstructorScore) {
bestConstructorScore = score;
//noinspection unchecked
bestConstructor = new BestMatchingEntityConstructorWithExtractionLambda<>(
(Constructor) declaredConstructor,
(argumentIndex, entity) -> argumentExtractors[argumentIndex].apply(entity)
);
}
}
if (bestConstructor == null) {
throw new EntityClassInvalidException(
expectedType,
"Cannot find any constructor with matching arguments in class: `" + expectedType.getName() + "`"
);
} else {
ENTITY_CONSTRUCTOR_CACHE.putIfAbsent(cacheKey, bestConstructor);
return bestConstructor;
}
}
}
/**
* Method tries to identify the best matching constructor for passed {@link EntitySchemaContract} and {@link Class}
* type. It tries to find a constructor with most of the arguments matching the schema fields.
*/
private static BestMatchingReferenceConstructorWithExtractionLambda findBestMatchingConstructor(
@Nonnull ProxyEntityCacheKey cacheKey,
@Nonnull EntitySchemaContract schema,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReferenceSchemaContract referenceSchema,
@Nonnull ReflectionLookup reflectionLookup,
@Nonnull ProxyFactory proxyFactory
) {
final Class> expectedType = cacheKey.subType();
if (REFERENCE_CONSTRUCTOR_CACHE.containsKey(cacheKey)) {
//noinspection unchecked
return (BestMatchingReferenceConstructorWithExtractionLambda) REFERENCE_CONSTRUCTOR_CACHE.get(cacheKey);
} else {
int bestConstructorScore = Integer.MIN_VALUE;
BestMatchingReferenceConstructorWithExtractionLambda bestConstructor = null;
for (Constructor> declaredConstructor : expectedType.getDeclaredConstructors()) {
int score = 0;
final Parameter[] parameters = declaredConstructor.getParameters();
//noinspection unchecked
final ExceptionRethrowingBiFunction[] argumentExtractors =
new ExceptionRethrowingBiFunction[parameters.length];
for (int i = 0; i < parameters.length; i++) {
final ExceptionRethrowingBiFunction refEntityPkFct =
GetReferencedEntityPrimaryKeyMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup
);
if (refEntityPkFct != null) {
argumentExtractors[i] = refEntityPkFct;
score++;
continue;
}
final ExceptionRethrowingBiFunction refEntityGroupPkFct =
GetReferencedGroupEntityPrimaryKeyMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup
);
if (refEntityGroupPkFct != null) {
argumentExtractors[i] = refEntityGroupPkFct;
score++;
continue;
}
final ExceptionRethrowingBiFunction attributeFct =
GetReferenceAttributeMethodClassifier.getExtractorIfPossible(
expectedType, parameters[i], reflectionLookup, schema, referenceSchema
);
if (attributeFct != null) {
argumentExtractors[i] = attributeFct;
score++;
continue;
}
final ExceptionRethrowingBiFunction referenceFct =
GetReferencedEntityMethodClassifier.getExtractorIfPossible(
referencedEntitySchemas, expectedType, parameters[i],
reflectionLookup, referenceSchema, proxyFactory
);
if (referenceFct != null) {
argumentExtractors[i] = referenceFct;
score++;
continue;
}
argumentExtractors[i] = (entity, reference) -> null;
}
if (score > bestConstructorScore) {
bestConstructorScore = score;
//noinspection unchecked
bestConstructor = new BestMatchingReferenceConstructorWithExtractionLambda<>(
(Constructor) declaredConstructor,
(argumentIndex, EntityContract, reference) -> argumentExtractors[argumentIndex]
.apply(EntityContract, reference)
);
}
}
if (bestConstructor == null) {
throw new EntityClassInvalidException(
expectedType,
"Cannot find any constructor with matching arguments in class: `" + expectedType.getName() + "`"
);
} else {
REFERENCE_CONSTRUCTOR_CACHE.putIfAbsent(cacheKey, bestConstructor);
return bestConstructor;
}
}
}
/**
* Method allows to provide explicit recipe for passed entity and output contract type.
*
* @param type the proxy class for which the recipe should be used (combines with entityName)
* @param entityName the name of the entity for which the recipe should be used (combines with type)
* @param recipe the Proxycian recipe to be used
*/
public void registerEntityRecipe(
@Nonnull Class type,
@Nonnull String entityName,
@Nonnull ProxyRecipe recipe
) {
final ProxyRecipe theRecipe = new ProxyRecipe(
recipe.getInterfaces(),
ArrayUtils.mergeArrays(
new Advice[]{
new DelegateCallsAdvice<>(SealedEntityProxy.class, Function.identity(), true),
LocalDataStoreAdvice.INSTANCE,
EntityContractAdvice.INSTANCE,
EntityBuilderAdvice.INSTANCE
},
recipe.getAdvices()
),
recipe.getInstantiationCallback()
);
final ProxyEntityCacheKey key = new ProxyEntityCacheKey(type, entityName);
recipes.put(key, theRecipe);
collectedRecipes.put(key, theRecipe);
}
/**
* Method allows to provide explicit recipe for passed entity reference and output contract referenceType.
*
* @param mainType the proxy class of the main entity type inside which the reference proxy is created
* @param referenceType the proxy class for which the recipe should be used (combines with entityName and referenceName)
* @param entityName the name of the entity for which the recipe should be used (combines with referenceType and referenceName)
* @param referenceName the name of the entity reference schema for which the recipe should be used (combines with entityName and referenceType)
* @param recipe the Proxycian recipe to be used
*/
public void registerEntityReferenceRecipe(
@Nonnull Class> mainType,
@Nonnull Class referenceType,
@Nonnull String entityName,
@Nonnull String referenceName,
@Nonnull ProxyRecipe recipe
) {
final ProxyRecipe theRecipe = new ProxyRecipe(
recipe.getInterfaces(),
ArrayUtils.mergeArrays(
new Advice[]{
new DelegateCallsAdvice<>(SealedEntityReferenceProxy.class, Function.identity(), true),
LocalDataStoreAdvice.INSTANCE,
EntityReferenceContractAdvice.INSTANCE,
EntityReferenceBuilderAdvice.INSTANCE,
},
recipe.getAdvices()
),
recipe.getInstantiationCallback()
);
final ProxyEntityCacheKey key = new ProxyEntityCacheKey(mainType, entityName, referenceType, referenceName);
recipes.put(key, theRecipe);
collectedRecipes.put(key, theRecipe);
}
@Nonnull
@Override
public T createEntityProxy(
@Nonnull Class expectedType,
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas
) throws EntityClassInvalidException {
return createEntityProxy(expectedType, recipes, collectedRecipes, entity, referencedEntitySchemas, reflectionLookup);
}
/**
* DTO for storing constructor and constructor argument value extraction lambda.
*
* @param constructor proxy class constructor
* @param extractionLambda lambda for extracting constructor argument value from sealed entity for specific
* index of the argument in the constructor
*/
private record BestMatchingEntityConstructorWithExtractionLambda(
@Nonnull Constructor constructor,
@Nonnull ExceptionRethrowingIntBiFunction extractionLambda
) {
/**
* Extracts constructor arguments from sealed entity for particular constructor method.
*/
@Nonnull
public Object[] constructorArguments(@Nonnull EntityContract entity) throws Exception {
final Class>[] parameterTypes = constructor.getParameterTypes();
final Object[] parameterArguments = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameterArguments[i] = extractionLambda.apply(i, entity);
}
return parameterArguments;
}
}
/**
* DTO for storing constructor and constructor argument value extraction lambda.
*
* @param constructor proxy class constructor
* @param extractionLambda lambda for extracting constructor argument value from sealed entity for specific
* index of the argument in the constructor
*/
private record BestMatchingReferenceConstructorWithExtractionLambda(
@Nonnull Constructor constructor,
@Nonnull ExceptionRethrowingIntTriFunction extractionLambda
) {
/**
* Extracts constructor arguments from sealed entity for particular constructor method.
*/
@Nonnull
public Object[] constructorArguments(@Nonnull EntityContract EntityContract, @Nonnull ReferenceContract reference) throws Exception {
final Class>[] parameterTypes = constructor.getParameterTypes();
final Object[] parameterArguments = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameterArguments[i] = extractionLambda.apply(i, EntityContract, reference);
}
return parameterArguments;
}
}
/**
* Cache key for particular type/entity/reference combination.
*
* @param type the proxy class
* @param entityName the name of the entity {@link EntitySchemaContract#getName()}
* @param subType the proxy class of the reference sub-type
* @param referenceName the name of the entity reference schema {@link ReferenceSchemaContract#getName()}
*/
public record ProxyEntityCacheKey(
@Nonnull Class> type,
@Nonnull String entityName,
@Nullable Class> subType,
@Nullable String referenceName
) implements Serializable {
public ProxyEntityCacheKey(@Nonnull Class> type, @Nonnull String entityName) {
this(type, entityName, null, null);
}
}
/**
* Direct implementation of the {@link ProxyFactory} interface that uses the provided maps of recipes and avoids
* going through {@link AbstractEntityProxyState}.
*/
private record DirectProxyFactory(
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull ReflectionLookup reflectionLookup
) implements ProxyFactory {
@Nonnull
@Override
public T createEntityProxy(
@Nonnull Class expectedType,
@Nonnull EntityContract entityContract,
@Nonnull Map referencedEntitySchemas
) throws EntityClassInvalidException {
return ProxycianFactory.createEntityProxy(expectedType, recipes, collectedRecipes, entityContract, referencedEntitySchemas, reflectionLookup);
}
}
/**
* Direct implementation of the {@link ProxyReferenceFactory} interface that uses the provided maps of recipes and avoids
* going through {@link AbstractEntityProxyState}.
*/
private record DirectProxyReferenceFactory(
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull ReflectionLookup reflectionLookup
) implements ProxyReferenceFactory {
@Nonnull
@Override
public T createEntityReferenceProxy(
@Nonnull Class> mainType,
@Nonnull Class expectedType,
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull ReferenceContract reference
) throws EntityClassInvalidException {
return ProxycianFactory.createEntityReferenceProxy(
mainType, expectedType, recipes, collectedRecipes,
entity, () -> null,
referencedEntitySchemas, reference, reflectionLookup,
null
);
}
}
}