io.evitadb.api.proxy.impl.AbstractEntityProxyState 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.
The newest version!
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ V /| | || (_| | |_| | |_) |
* \___| \_/ |_|\__\__,_|____/|____/
*
* Copyright (c) 2023-2024
*
* 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.CollectionNotFoundException;
import io.evitadb.api.exception.EntityClassInvalidException;
import io.evitadb.api.proxy.ReferencedEntityBuilderProvider;
import io.evitadb.api.proxy.SealedEntityProxy;
import io.evitadb.api.proxy.SealedEntityProxy.EntityBuilderWithCallback;
import io.evitadb.api.proxy.SealedEntityProxy.ProxyType;
import io.evitadb.api.proxy.impl.ProxycianFactory.ProxyEntityCacheKey;
import io.evitadb.api.requestResponse.data.EntityClassifier;
import io.evitadb.api.requestResponse.data.EntityContract;
import io.evitadb.api.requestResponse.data.EntityEditor.EntityBuilder;
import io.evitadb.api.requestResponse.data.ReferenceContract;
import io.evitadb.api.requestResponse.data.mutation.EntityMutation;
import io.evitadb.api.requestResponse.data.structure.EntityReference;
import io.evitadb.api.requestResponse.data.structure.InitialEntityBuilder;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.exception.GenericEvitaInternalError;
import io.evitadb.utils.Assert;
import io.evitadb.utils.ReflectionLookup;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import one.edee.oss.proxycian.recipe.ProxyRecipe;
import one.edee.oss.proxycian.trait.localDataStore.LocalDataStore;
import one.edee.oss.proxycian.trait.localDataStore.LocalDataStoreProvider;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
/**
* Abstract parent class for all entity proxy states. It contains all the common fields and methods.
*
* @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2023
*/
@EqualsAndHashCode(of = {"entity", "proxyClass"})
abstract class AbstractEntityProxyState implements
Serializable,
LocalDataStoreProvider,
EntityClassifier,
ReferencedEntityBuilderProvider
{
@Serial private static final long serialVersionUID = -6935480192166155348L;
/**
* The sealed entity that is being proxied.
*/
@Nonnull protected final EntityContract entity;
/**
* The class of the proxy was built upon.
*/
@Nonnull protected final Class> proxyClass;
/**
* The map of recipes provided from outside that are used to build the proxy.
*/
@Nonnull protected final Map recipes;
/**
* The index of referenced entity schemas by their type at the time the proxy was built.
*/
@Getter
@Nonnull protected final Map referencedEntitySchemas;
/**
* The merged map all recipes - the ones provided from outside and the ones created with default configuration on
* the fly during the proxy building.
*/
@Nonnull protected transient Map collectedRecipes;
/**
* The reflection lookup instance used to access the reflection data in a memoized fashion.
*/
@Nonnull protected transient ReflectionLookup reflectionLookup;
/**
* Cache for the already generated proxies so that the same method call returns the same instance.
*/
@Nonnull protected transient Map generatedProxyObjects;
/**
* The local data store that is used to store the data that are not part of the sealed entity.
*/
private Map localDataStore;
protected AbstractEntityProxyState(
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull Class> proxyClass,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull ReflectionLookup reflectionLookup
) {
this.entity = entity;
this.referencedEntitySchemas = referencedEntitySchemas;
this.proxyClass = proxyClass;
this.recipes = recipes;
this.collectedRecipes = collectedRecipes;
this.reflectionLookup = reflectionLookup;
this.generatedProxyObjects = new ConcurrentHashMap<>();
}
protected AbstractEntityProxyState(
@Nonnull EntityContract entity,
@Nonnull Map referencedEntitySchemas,
@Nonnull Class> proxyClass,
@Nonnull Map recipes,
@Nonnull Map collectedRecipes,
@Nonnull ReflectionLookup reflectionLookup,
@Nonnull Map externalProxyObjectsCache
) {
this.entity = entity;
this.referencedEntitySchemas = referencedEntitySchemas;
this.proxyClass = proxyClass;
this.recipes = recipes;
this.collectedRecipes = collectedRecipes;
this.reflectionLookup = reflectionLookup;
this.generatedProxyObjects = externalProxyObjectsCache;
}
/**
* Returns the sealed entity that is being proxied.
*/
@Nonnull
public EntityContract getEntity() {
return entity;
}
/**
* Returns the primary key of the sealed entity that is being proxied.
*/
@Nullable
public Integer getPrimaryKey() {
return entity.getPrimaryKey();
}
/**
* Returns the class of the proxy was built upon.
*/
@Nonnull
public Class> getProxyClass() {
return proxyClass;
}
/**
* Returns the map of recipes provided from outside that are used to build the proxy.
*/
@Nonnull
public ReflectionLookup getReflectionLookup() {
return reflectionLookup;
}
/**
* Returns entity schema from the {@link #entity}.
*/
@Nonnull
public EntitySchemaContract getEntitySchema() {
return entity.getSchema();
}
/**
* Returns entity schema for the passed entity type.
*
* @param entityType name of the entity type
* @return entity schema for the passed entity type or empty result
*/
@Nonnull
public Optional getEntitySchema(@Nonnull String entityType) {
return ofNullable(referencedEntitySchemas.get(entityType));
}
/**
* Returns entity schema for the passed entity type.
*
* @param entityType name of the entity type
* @return entity schema for the passed entity type or empty result
*/
@Nonnull
public EntitySchemaContract getEntitySchemaOrThrow(@Nonnull String entityType) throws CollectionNotFoundException {
return ofNullable(referencedEntitySchemas.get(entityType))
.orElseThrow(() -> new CollectionNotFoundException(entityType));
}
/**
* Returns existing or creates new memory Map data store for data written by {@link LocalDataStore}.
*/
@Override
public Map getOrCreateLocalDataStore() {
if (localDataStore == null) {
localDataStore = new ConcurrentHashMap<>();
}
return localDataStore;
}
/**
* Returns existing memory Map data store for data written by {@link LocalDataStore}. Might return null, if there
* are no data written yet.
*/
@Override
public Map getLocalDataStoreIfPresent() {
return localDataStore;
}
/**
* Creates new or returns existing proxy for an external entity that is accessible via reference of current entity.
* Possible referenced entities are {@link ProxyType#PARENT} or {@link ProxyType#REFERENCED_ENTITY}.
*
* This method should be used if the referenced entity is not known (doesn't exists) but its entity primary key is
* known upfront.
*
* @param entitySchema schema of the entity to be created
* @param expectedType contract that the proxy should implement
* @param proxyType type of the proxy (either {@link ProxyType#PARENT} or {@link ProxyType#REFERENCED_ENTITY})
* @param primaryKey primary key of the referenced entity
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
* @throws EntityClassInvalidException if the proxy contract is not valid
*/
@Nonnull
public T getOrCreateReferencedEntityProxy(
@Nonnull EntitySchemaContract entitySchema,
@Nonnull Class expectedType,
@Nonnull ProxyType proxyType,
int primaryKey
) throws EntityClassInvalidException {
final Supplier instanceSupplier = () -> new ProxyWithUpsertCallback(
ProxycianFactory.createEntityProxy(
expectedType, recipes, collectedRecipes,
new InitialEntityBuilder(entitySchema, primaryKey),
referencedEntitySchemas,
getReflectionLookup()
)
);
return generatedProxyObjects.computeIfAbsent(
new ProxyInstanceCacheKey(entitySchema.getName(), primaryKey, proxyType),
key -> instanceSupplier.get()
)
.proxy(
expectedType,
instanceSupplier
);
}
/**
* Creates new or returns existing proxy for an external entity that is accessible via reference of current entity.
* Possible referenced entities are {@link ProxyType#PARENT} or {@link ProxyType#REFERENCED_ENTITY}.
*
* This method should be used if the referenced entity is known.
*
* @param expectedType contract that the proxy should implement
* @param entity sealed entity to create proxy for
* @param proxyType type of the proxy (either {@link ProxyType#PARENT} or {@link ProxyType#REFERENCED_ENTITY})
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
*/
@Nonnull
public T getOrCreateReferencedEntityProxy(
@Nonnull Class expectedType,
@Nonnull EntityContract entity,
@Nonnull ProxyType proxyType
) {
final Supplier instanceSupplier = () -> new ProxyWithUpsertCallback(
createReferencedEntityProxy(expectedType, entity)
);
return generatedProxyObjects.computeIfAbsent(
new ProxyInstanceCacheKey(entity.getType(), entity.getPrimaryKey(), proxyType),
key -> instanceSupplier.get()
)
.proxy(expectedType, instanceSupplier);
}
/**
* Creates proxy instance of entity builder that implements `expectedType` contract and creates new entity builder.
*
* This method should be used if the referenced entity is not known (doesn't exists), and its primary key is also
* not known (the referenced entity needs to be persisted first).
*
* @param entitySchema schema of the entity to be created
* @param expectedType contract that the proxy should implement
* @param callback callback that will be called when the entity is upserted
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
* @throws EntityClassInvalidException if the proxy contract is not valid
*/
@Nonnull
public T createReferencedEntityProxyWithCallback(
@Nonnull EntitySchemaContract entitySchema,
@Nonnull Class expectedType,
@Nonnull ProxyType proxyType,
@Nonnull Consumer callback
) throws EntityClassInvalidException {
final Supplier instanceSupplier = () -> new ProxyWithUpsertCallback(
ProxycianFactory.createEntityProxy(
expectedType, recipes, collectedRecipes,
new InitialEntityBuilder(entitySchema),
referencedEntitySchemas,
getReflectionLookup()
),
callback
);
return generatedProxyObjects.computeIfAbsent(
new ProxyInstanceCacheKey(entitySchema.getName(), Integer.MIN_VALUE, proxyType),
key -> instanceSupplier.get()
)
.proxy(expectedType, instanceSupplier);
}
/**
* Creates new proxy for an entity that is accessible via reference of current entity.
*
* @param expectedType contract that the proxy should implement
* @param entity sealed entity to create proxy for
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
*/
@Nonnull
public T createReferencedEntityProxy(
@Nonnull Class expectedType,
@Nonnull EntityContract entity
) {
return ProxycianFactory.createEntityProxy(
expectedType, recipes, collectedRecipes, entity, referencedEntitySchemas, getReflectionLookup()
);
}
/**
* Creates new proxy for a reference.
*
* This method should be used if the reference is known.
*
* @param expectedType contract that the proxy should implement
* @param entity sealed entity to create proxy for
* @param reference reference instance to create proxy for
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
*/
@Nonnull
public T createNewReferenceProxy(
@Nonnull Class> mainType,
@Nonnull Class expectedType,
@Nonnull EntityContract entity,
@Nonnull ReferenceContract reference
) {
return ProxycianFactory.createEntityReferenceProxy(
mainType, expectedType, recipes, collectedRecipes,
entity, this::getPrimaryKey,
referencedEntitySchemas, reference, getReflectionLookup(),
this.generatedProxyObjects
);
}
/**
* Creates new proxy for a reference.
*
* This method should be used if the referenced entity is not known (doesn't exists), and its primary key is also
* not known (the referenced entity needs to be persisted first).
*
* @param expectedType contract that the proxy should implement
* @param reference reference instance to create proxy for
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
*/
@Nonnull
public T getOrCreateEntityReferenceProxy(
@Nonnull Class expectedType,
@Nonnull ReferenceContract reference
) {
final Supplier instanceSupplier = () -> new ProxyWithUpsertCallback(
ProxycianFactory.createEntityReferenceProxy(
this.getProxyClass(), expectedType, recipes, collectedRecipes,
entity, this::getPrimaryKey,
referencedEntitySchemas, reference, getReflectionLookup(),
this.generatedProxyObjects
)
);
return generatedProxyObjects.computeIfAbsent(
new ProxyInstanceCacheKey(reference.getReferenceName(), reference.getReferencedPrimaryKey(), ProxyType.REFERENCE),
key -> instanceSupplier.get()
)
.proxy(
expectedType,
instanceSupplier
);
}
/**
* Creates new proxy for an entity wrapped by this proxy using passed entity but sharing outer contract and recipes.
*
* @param entity entity to be wrapped
* @param type of contract that the proxy should implement
* @return proxy instance of sealed entity
*/
@Nonnull
public T cloneProxy(@Nonnull EntityContract entity) {
//noinspection DataFlowIssue,unchecked
return (T) ProxycianFactory.createEntityProxy(
getProxyClass(), recipes, collectedRecipes, entity, referencedEntitySchemas, getReflectionLookup(),
stateClone -> stateClone.registerReferencedInstancesObjects(this.generatedProxyObjects)
);
}
/**
* Returns the registered proxy, matching the registration context in creation methods method.
*
* @param referencedEntityType the {@link EntitySchemaContract#getName()} of the referenced entity type
* @param referencedPrimaryKey the {@link EntityContract#getPrimaryKey()} of the referenced entity
* @param expectedType the expected class type of the proxy
* @param proxyType set of logical types to be searched for the proxy instance
*/
@Nonnull
public Optional getReferencedEntityObjectIfPresent(
@Nonnull String referencedEntityType,
int referencedPrimaryKey,
@Nonnull Class expectedType,
@Nonnull ProxyType proxyType
) {
return ofNullable(
generatedProxyObjects.get(
new ProxyInstanceCacheKey(referencedEntityType, referencedPrimaryKey, proxyType)
)
).flatMap(it -> it.proxyIfPossible(expectedType));
}
/**
* Method registers created proxy object that was created by this proxy instance and relates to referenced objects
* accessed via it. We need to provide exactly the same instances of those objects when the same method is called
* or the logically same object is retrieved via different method with compatible type.
*
* @param referencedEntityType the {@link EntitySchemaContract#getName()} of the referenced entity type
* @param referencedPrimaryKey the {@link EntityContract#getPrimaryKey()} of the referenced entity
* @param proxy the proxy object
* @param logicalType logical type of the proxy object
*/
public void registerReferencedEntityObject(
@Nonnull String referencedEntityType,
int referencedPrimaryKey,
@Nonnull Object proxy,
@Nonnull ProxyType logicalType
) {
generatedProxyObjects.put(
new ProxyInstanceCacheKey(referencedEntityType, referencedPrimaryKey, logicalType),
new ProxyWithUpsertCallback(proxy)
);
}
/**
* Method unregisters created proxy object that was created by this proxy instance and relates to referenced objects
* accessed via it.
*
* @param referencedEntityType the {@link EntitySchemaContract#getName()} of the referenced entity type
* @param referencedPrimaryKey the {@link EntityContract#getPrimaryKey()} of the referenced entity
* @param logicalType logical type of the proxy object
*/
public void unregisterReferencedEntityObject(
@Nonnull String referencedEntityType,
int referencedPrimaryKey,
@Nonnull ProxyType logicalType
) {
generatedProxyObjects.remove(
new ProxyInstanceCacheKey(referencedEntityType, referencedPrimaryKey, logicalType)
);
}
@Override
@Nonnull
public Stream getReferencedEntityBuildersWithCallback() {
return this.generatedProxyObjects.entrySet().stream()
.filter(
it -> it.getKey().proxyType() == ProxyType.PARENT ||
it.getKey().proxyType() == ProxyType.REFERENCED_ENTITY
)
.flatMap(
it -> Stream.concat(
// we need first store the referenced entities of referenced entity (depth wise)
it.getValue().getSealedEntityProxies()
.flatMap(SealedEntityProxy::getReferencedEntityBuildersWithCallback),
// and then the referenced entity itself
it.getValue().getSealedEntityProxies()
.map(SealedEntityProxy::getEntityBuilderWithCallback)
.filter(Optional::isPresent)
.map(Optional::get)
.map(
mutation -> {
final EntityBuilder theBuilder = mutation.builder();
final Consumer mutationCallback = mutation.upsertCallback();
final Consumer externalCallback = it.getValue().callback();
return new EntityBuilderWithCallback(
theBuilder,
mutationCallback == null ?
externalCallback :
entityReference1 -> {
mutation.updateEntityReference(entityReference1);
externalCallback.accept(entityReference1);
}
);
}
)
)
);
}
/**
* Method propagates references to the referenced instances to the {@link #generatedProxyObjects} cache of this state.
*
* @param generatedProxyObjects from previous state
*/
void registerReferencedInstancesObjects(@Nonnull Map generatedProxyObjects) {
this.generatedProxyObjects.putAll(generatedProxyObjects);
}
/**
* Method is called during Java (de)serialization process. It is used to initialize the transient fields.
*/
@Serial
private void readObject(@Nonnull ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
this.reflectionLookup = ReflectionLookup.NO_CACHE_INSTANCE;
this.collectedRecipes = new ConcurrentHashMap<>(recipes);
this.generatedProxyObjects = new ConcurrentHashMap<>();
}
/**
* Cache key for generated proxies.
*
* @param identifier entity of reference name
* @param primaryKey primary key of the object being wrapped
* @param proxyType the type of the proxy (entity or reference)
*/
protected record ProxyInstanceCacheKey(
@Nonnull String identifier,
int primaryKey,
@Nonnull ProxyType proxyType
) {
}
/**
* Record contains reference to the proxy instance and the callback lambda, that needs to be invoked when the proxy
* mutations are persisted via. {@link io.evitadb.api.EvitaSessionContract#upsertEntity(EntityMutation)}. If there
* is no need to invoke any callback, the {@link #DO_NOTHING_CALLBACK} is used.
*/
protected static class ProxyWithUpsertCallback {
private static final Consumer DO_NOTHING_CALLBACK = entityReference -> {
};
@Nonnull private final List