All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.evitadb.api.proxy.impl.referenceBuilder.SetReferenceGroupMethodClassifier Maven / Gradle / Ivy

There is a newer version: 2024.10.0
Show newest version
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ 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.referenceBuilder;

import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.exception.EntityClassInvalidException;
import io.evitadb.api.proxy.SealedEntityProxy;
import io.evitadb.api.proxy.SealedEntityProxy.ProxyType;
import io.evitadb.api.proxy.impl.SealedEntityReferenceProxyState;
import io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.EntityRecognizedIn;
import io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.RecognizedContext;
import io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.ResolvedParameter;
import io.evitadb.api.requestResponse.data.EntityClassifier;
import io.evitadb.api.requestResponse.data.EntityContract;
import io.evitadb.api.requestResponse.data.EntityReferenceContract;
import io.evitadb.api.requestResponse.data.ReferenceContract.GroupEntityReference;
import io.evitadb.api.requestResponse.data.ReferenceEditor.ReferenceBuilder;
import io.evitadb.api.requestResponse.data.SealedEntity;
import io.evitadb.api.requestResponse.data.annotation.CreateWhenMissing;
import io.evitadb.api.requestResponse.data.annotation.ReferencedEntityGroup;
import io.evitadb.api.requestResponse.data.annotation.RemoveWhenExists;
import io.evitadb.api.requestResponse.schema.ReferenceSchemaContract;
import io.evitadb.dataType.EvitaDataTypes;
import io.evitadb.utils.Assert;
import io.evitadb.utils.NumberUtils;
import io.evitadb.utils.ReflectionLookup;
import one.edee.oss.proxycian.CurriedMethodContextInvocationHandler;
import one.edee.oss.proxycian.DirectMethodClassification;
import one.edee.oss.proxycian.utils.GenericsUtils;
import one.edee.oss.proxycian.utils.GenericsUtils.GenericBundle;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

import static io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.isEntityRecognizedIn;
import static io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.recognizeCallContext;
import static io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.resolveFirstParameter;
import static io.evitadb.api.proxy.impl.entityBuilder.SetReferenceMethodClassifier.resolvedTypeIs;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;

/**
 * Identifies methods that are used to set entity referenced group into an entity and provides their implementation.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2023
 */
public class SetReferenceGroupMethodClassifier extends DirectMethodClassification {
	/**
	 * We may reuse singleton instance since advice is stateless.
	 */
	public static final SetReferenceGroupMethodClassifier INSTANCE = new SetReferenceGroupMethodClassifier();

	/**
	 * Method returns the referenced entity group type and verifies that it is managed by evitaDB.
	 *
	 * @param referenceSchema the reference schema
	 * @return the referenced entity group type
	 */
	@Nullable
	private static String getReferencedGroupType(@Nonnull ReferenceSchemaContract referenceSchema, boolean requireManagedOnly) {
		final String referencedEntityType = referenceSchema.getReferencedGroupType();
		Assert.isTrue(
			!requireManagedOnly || referenceSchema.isReferencedGroupTypeManaged(),
			"Referenced entity group type `" + referencedEntityType + "` is not managed " +
				"by evitaDB and cannot be created by method call!"
		);
		return referencedEntityType;
	}

	/**
	 * Return a method implementation that removes the single reference if exists.
	 *
	 * @param referenceSchema the reference schema to use
	 * @return the method implementation
	 */
	@Nullable
	private static CurriedMethodContextInvocationHandler removeReferencedEntity(
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nonnull ReferenceSchemaContract referenceSchema,
		@Nonnull Class returnType,
		boolean entityRecognizedInReturnType
	) {
		if (returnType.equals(proxyState.getProxyClass())) {
			return (proxy, theMethod, args, theState, invokeSuper) -> {
				theState.getReferenceBuilder().removeGroup();
				return proxy;
			};
		} else if (Number.class.isAssignableFrom(returnType)) {
			return (proxy, theMethod, args, theState, invokeSuper) -> {
				final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
				final Optional groupRef = referenceBuilder.getGroup();
				if (groupRef.isPresent()) {
					referenceBuilder.removeGroup();
					return true;
				} else {
					return false;
				}
			};
		} else if (Boolean.class.isAssignableFrom(returnType)) {
			return (proxy, theMethod, args, theState, invokeSuper) -> {
				final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
				final Optional groupRef = referenceBuilder.getGroup();
				if (groupRef.isPresent()) {
					referenceBuilder.removeGroup();
					return groupRef.get().getPrimaryKey();
				} else {
					return null;
				}
			};
		} else if (returnType.equals(void.class)) {
			return (proxy, theMethod, args, theState, invokeSuper) -> {
				theState.getReferenceBuilder().removeGroup();
				return null;
			};
		} else if (entityRecognizedInReturnType) {
			return removeReferencedEntityAndReturnItsProxy(referenceSchema, returnType);
		} else {
			return null;
		}
	}

	/**
	 * Method implementation that removes the single reference if exists and returns the removed reference proxy.
	 *
	 * @param referenceSchema the reference schema to use
	 * @param returnType      the return type
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler removeReferencedEntityAndReturnItsProxy(
		@Nonnull ReferenceSchemaContract referenceSchema,
		@Nonnull Class returnType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
			final String referenceName = referenceSchema.getName();
			final Optional groupEntity = referenceBuilder.getGroupEntity();
			if (groupEntity.isPresent()) {
				// do nothing
				if (referenceBuilder.getGroup().isPresent()) {
					throw ContextMissingException.referencedEntityContextMissing(theState.getType(), referenceName);
				} else {
					return null;
				}
			} else {
				referenceBuilder.removeGroup();
				final SealedEntity referencedEntity = groupEntity.get();
				return theState.getOrCreateReferencedEntityProxy(
					returnType, referencedEntity, ProxyType.REFERENCED_ENTITY
				);
			}
		};
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * (without knowing its primary key since it hasn't been assigned yet) and returns the reference to the created
	 * proxy allowing to set reference properties on it.
	 *
	 * @param referenceSchema the reference schema to use
	 * @param expectedType    the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	@Nonnull
	private static CurriedMethodContextInvocationHandler getOrCreateWithReferencedEntityGroupResult(
		@Nonnull ReferenceSchemaContract referenceSchema,
		@Nonnull Class expectedType
	) {
		final String referencedEntityType = getReferencedGroupType(referenceSchema, true);
		final String referenceName = referenceSchema.getName();
		return (entityClassifier, theMethod, args, theState, invokeSuper) -> {
			final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
			final Optional group = referenceBuilder.getGroup();
			if (group.isEmpty()) {
				return theState.createReferencedEntityProxyWithCallback(
					theState.getEntitySchemaOrThrow(referencedEntityType), expectedType, ProxyType.REFERENCED_ENTITY,
					entityReference -> referenceBuilder.setGroup(entityReference.primaryKey())
				);
			} else {
				final Optional referencedInstance = theState.getReferencedEntityObjectIfPresent(
					referencedEntityType, group.get().getPrimaryKey(),
					expectedType, ProxyType.REFERENCED_ENTITY
				);
				if (referencedInstance.isPresent()) {
					return referencedInstance.get();
				} else {
					final Optional groupEntity = referenceBuilder.getGroupEntity();
					Assert.isTrue(
						groupEntity.isPresent(),
						() -> ContextMissingException.referencedEntityContextMissing(theState.getType(), referenceName)
					);
					return groupEntity
						.map(
							it -> theState.getOrCreateReferencedEntityProxy(
								expectedType, it, ProxyType.REFERENCED_ENTITY
							)
						)
						.orElse(null);
				}
			}
		};
	}

	/**
	 * Returns method implementation that creates or updates reference by passing directly the entities fetched from
	 * other sources. This implementation doesn't allow to set attributes on the reference.
	 *
	 * @param proxyState      the proxy state
	 * @param returnType      the return type
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityByEntity(
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nonnull Class returnType,
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		if (returnType.equals(proxyState.getProxyClass())) {
			return setReferencedEntityGroupWithBuilderResult(referenceSchema);
		} else {
			return setReferencedEntityGroupWithVoidResult(referenceSchema);
		}
	}

	/**
	 * Returns method implementation that sets the referenced entity by extracting it from {@link SealedEntityProxy}
	 * and return no result.
	 *
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupWithVoidResult(
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		final String expectedEntityType = getReferencedGroupType(referenceSchema, true);
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroup(expectedEntityType, args, theState);
			return null;
		};
	}

	/**
	 * Returns method implementation that sets the referenced entity by extracting it from {@link SealedEntityProxy}
	 * and return the reference to the proxy to allow chaining (builder pattern).
	 *
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupWithBuilderResult(
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		final String expectedEntityType = getReferencedGroupType(referenceSchema, true);
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroup(expectedEntityType, args, theState);
			return proxy;
		};
	}

	/**
	 * Sets the referenced entity group by extracting it from the provided SealedEntityProxy object.
	 *
	 * @param expectedEntityType the expected type of the referenced entity group
	 * @param args               the arguments passed to the method
	 * @param theState           the SealedEntityReferenceProxyState object
	 */
	private static void setReferencedEntityGroup(
		@Nonnull String expectedEntityType,
		@Nonnull Object[] args,
		@Nonnull SealedEntityReferenceProxyState theState
	) {
		final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
		final SealedEntityProxy referencedEntity = (SealedEntityProxy) args[0];
		final EntityContract sealedEntity = referencedEntity.entity();
		Assert.isTrue(
			expectedEntityType.equals(sealedEntity.getType()),
			"Entity type `" + sealedEntity.getType() + "` in passed argument " +
				"doesn't match the referencedGroupEntity entity type: `" + expectedEntityType + "`!"
		);
		referenceBuilder.setGroup(sealedEntity.getPrimaryKey());
	}

	/**
	 * Returns method implementation that creates or updates reference by consumer lambda, that
	 * could immediately modify referenced entity. This method creates the reference without possibility to set
	 * attributes on the reference and works directly with the referenced entity. The referenced entity has no primary
	 * key, which is assigned later when the entity is persisted.
	 *
	 * @param method          the method
	 * @param proxyState      the proxy state
	 * @param referenceSchema the reference schema
	 * @param returnType      the return type
	 * @param expectedType    the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nullable
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupByEntityConsumer(
		@Nonnull Method method,
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nonnull ReferenceSchemaContract referenceSchema,
		@Nonnull Class returnType,
		@Nonnull Class expectedType
	) {
		final String referencedGroupType = getReferencedGroupType(referenceSchema, true);
		if (method.isAnnotationPresent(CreateWhenMissing.class) ||
			Arrays.stream(method.getParameterAnnotations()[0]).anyMatch(CreateWhenMissing.class::isInstance)) {
			if (returnType.equals(proxyState.getProxyClass())) {
				return createReferencedEntityGroupWithEntityBuilderResult(
					referencedGroupType,
					expectedType
				);
			} else if (void.class.equals(returnType)) {
				return createReferencedEntityGroupWithVoidResult(
					referencedGroupType,
					expectedType
				);
			} else {
				return null;
			}
		} else {
			if (returnType.equals(proxyState.getProxyClass())) {
				return updateReferencedEntityWithEntityGroupBuilderResult(
					referenceSchema.getName(),
					expectedType
				);
			} else if (void.class.equals(returnType)) {
				return updateReferencedEntityGroupWithVoidResult(
					referenceSchema.getName(), expectedType
				);
			} else {
				return null;
			}
		}
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * (without knowing its primary key since it hasn't been assigned yet) and returns the reference to the entity proxy
	 * to allow chaining (builder pattern).
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler createReferencedEntityGroupWithEntityBuilderResult(
		@Nonnull String referencedEntityType,
		@Nonnull Class expectedType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			createReferencedEntityGroup(referencedEntityType, expectedType, args, theState);
			return proxy;
		};
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * (without knowing its primary key since it hasn't been assigned yet) and returns no result.
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler createReferencedEntityGroupWithVoidResult(
		@Nonnull String referencedEntityType,
		@Nonnull Class expectedType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			createReferencedEntityGroup(referencedEntityType, expectedType, args, theState);
			return null;
		};
	}

	/**
	 * Creates a new entity group referenced proxy object of type {@code SealedEntityReferenceProxyState}.
	 *
	 * @param referencedEntityType the type of the referenced entity
	 * @param expectedType         the expected type of the referenced entity proxy
	 * @param args                 the arguments passed to the method
	 * @param theState             the SealedEntityReferenceProxyState object
	 */
	private static void createReferencedEntityGroup(
		@Nonnull String referencedEntityType,
		@Nonnull Class expectedType,
		@Nonnull Object[] args,
		@Nonnull SealedEntityReferenceProxyState theState
	) {
		final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
		final Object referencedEntityInstance = theState.createReferencedEntityProxyWithCallback(
			theState.getEntitySchemaOrThrow(referencedEntityType),
			expectedType,
			ProxyType.REFERENCED_ENTITY,
			entityReference -> referenceBuilder.setGroup(entityReference.getPrimaryKey())
		);
		//noinspection unchecked
		final Consumer consumer = (Consumer) args[0];
		consumer.accept(referencedEntityInstance);
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * (without knowing its primary key since it hasn't been assigned yet) and returns the reference to the entity proxy
	 * to allow chaining (builder pattern).
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler updateReferencedEntityWithEntityGroupBuilderResult(
		@Nonnull String referenceName,
		@Nonnull Class expectedType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			updateReferencedEntity(referenceName, expectedType, args, theState);
			return proxy;
		};
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * (without knowing its primary key since it hasn't been assigned yet) and returns no result.
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler updateReferencedEntityGroupWithVoidResult(
		@Nonnull String referenceName,
		@Nonnull Class expectedType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			updateReferencedEntity(referenceName, expectedType, args, theState);
			return null;
		};
	}

	/**
	 * Update the referenced entity with the given reference name, expected type, arguments, and proxy state.
	 *
	 * @param referenceName the name of the reference
	 * @param expectedType  the expected type of the referenced entity
	 * @param args          the arguments to update the referenced entity
	 * @param theState      the proxy state
	 */
	private static void updateReferencedEntity(
		@Nonnull String referenceName,
		@Nonnull Class expectedType,
		@Nonnull Object[] args,
		@Nonnull SealedEntityReferenceProxyState theState
	) {
		final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
		final Optional group = referenceBuilder.getGroup();
		if (group.isEmpty()) {
			throw ContextMissingException.referenceContextMissing(referenceName);
		} else {
			final Object referencedEntityInstance = theState.getOrCreateReferencedEntityProxy(
				expectedType,
				referenceBuilder.getGroupEntity()
					.orElseThrow(() -> ContextMissingException.referencedEntityContextMissing(theState.getType(), referenceName)),
				ProxyType.REFERENCED_ENTITY
			);
			//noinspection unchecked
			final Consumer consumer = (Consumer) args[0];
			consumer.accept(referencedEntityInstance);
		}
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * and returns the reference to the created proxy allowing to set reference properties on it.
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setOrRemoveReferencedEntityByEntityReturnType(
		@Nonnull Class expectedType
	) {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
			final int referencedGroupId = EvitaDataTypes.toTargetType((Serializable) args[0], int.class);
			final Object referencedEntityInstance = theState.getOrCreateReferencedEntityProxy(
				theState.getEntitySchema(),
				expectedType,
				ProxyType.REFERENCED_ENTITY,
				referencedGroupId
			);
			referenceBuilder.setGroup(referencedGroupId);
			return referencedEntityInstance;
		};
	}

	/**
	 * Returns method implementation that creates, updates or removes reference by passing referenced entity ids.
	 * This implementation doesn't allow to set attributes on the reference.
	 *
	 * @param proxyState the proxy state
	 * @param method     the method
	 * @param returnType the return type
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setOrRemoveReferencedGroupById(
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nonnull Method method,
		@Nonnull Class returnType
	) {
		if (method.isAnnotationPresent(RemoveWhenExists.class)) {
			throw new EntityClassInvalidException(
				proxyState.getProxyClass(),
				"Method `" + method.getName() + "` of entity `" + proxyState.getType() + "` " +
					"that accepts integer argument cannot be annotated with `@RemoveWhenExists`!"
			);
		} else {
			if (returnType.equals(proxyState.getProxyClass())) {
				return setReferencedEntityGroupIdWithBuilderResult();
			} else if (returnType.equals(void.class)) {
				return setReferencedEntityGroupIdWithVoidResult();
			} else if (method.isAnnotationPresent(CreateWhenMissing.class)) {
				return getOrCreateByGroupIdWithReferencedEntityResult(returnType);
			} else {
				return null;
			}
		}
	}

	/**
	 * Returns method implementation that sets the referenced group entity id and return no result.
	 *
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupIdWithVoidResult() {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroupId(args, theState);
			return null;
		};
	}

	/**
	 * Returns method implementation that sets the referenced entity group id and return the reference to the proxy to
	 * allow chaining (builder pattern).
	 *
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupIdWithBuilderResult() {
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroupId(args, theState);
			return proxy;
		};
	}

	/**
	 * Sets the referenced entity group id.
	 *
	 * @param args     the arguments passed to the method
	 * @param theState the state of the proxy
	 */
	private static void setReferencedEntityGroupId(
		@Nonnull Object[] args,
		@Nonnull SealedEntityReferenceProxyState theState
	) {
		final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
		final Serializable referencedPrimaryKey = (Serializable) args[0];
		referenceBuilder.setGroup(
			EvitaDataTypes.toTargetType(referencedPrimaryKey, int.class)
		);
	}

	/**
	 * Return a method implementation that creates new proxy object representing a reference to and external entity
	 * group and returns the created proxy allowing to set properties on it.
	 *
	 * @param expectedType the expected type of the referenced entity proxy
	 * @return the method implementation
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	@Nonnull
	private static CurriedMethodContextInvocationHandler getOrCreateByGroupIdWithReferencedEntityResult(
		@Nonnull Class expectedType
	) {
		return (entityClassifier, theMethod, args, theState, invokeSuper) -> {
			final int referencedId = EvitaDataTypes.toTargetType((Serializable) args[0], int.class);
			final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
			referenceBuilder.setGroup(referencedId);
			return theState.getOrCreateReferencedEntityProxy(
				expectedType,
				theState.getEntity(),
				ProxyType.REFERENCED_ENTITY
			);
		};
	}

	/**
	 * Returns method implementation that creates or updates reference group by passing {@link EntityClassifier}
	 * instances to the method parameter.
	 *
	 * @param proxyState      the proxy state
	 * @param returnType      the return type
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferenceByEntityClassifier(
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nonnull Class returnType,
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		if (returnType.equals(proxyState.getProxyClass())) {
			return setReferencedEntityGroupClassifierWithBuilderResult(referenceSchema);
		} else {
			return setReferencedEntityGroupClassifierWithVoidResult(referenceSchema);
		}
	}

	/**
	 * Returns method implementation that sets the referenced entity by extracting it from {@link EntityClassifier}
	 * and return the reference to the proxy to allow chaining (builder pattern).
	 *
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupClassifierWithBuilderResult(
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		final String expectedEntityType = getReferencedGroupType(referenceSchema, false);
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroupClassifier(expectedEntityType, args, theState);
			return proxy;
		};
	}

	/**
	 * Returns method implementation that sets the referenced entity by extracting it from {@link EntityClassifier}
	 * and return no result.
	 *
	 * @param referenceSchema the reference schema
	 * @return the method implementation
	 */
	@Nonnull
	private static CurriedMethodContextInvocationHandler setReferencedEntityGroupClassifierWithVoidResult(
		@Nonnull ReferenceSchemaContract referenceSchema
	) {
		final String expectedEntityType = getReferencedGroupType(referenceSchema, false);
		return (proxy, theMethod, args, theState, invokeSuper) -> {
			setReferencedEntityGroupClassifier(expectedEntityType, args, theState);
			return null;
		};
	}

	/**
	 * Sets the referenced entity group classifier by extracting it from an {@link EntityClassifier}.
	 *
	 * @param expectedEntityType the expected entity type of the referenced entity group
	 * @param args               the arguments passed to the method
	 * @param theState           the state of the sealed entity reference proxy
	 */
	private static void setReferencedEntityGroupClassifier(
		@Nonnull String expectedEntityType,
		@Nonnull Object[] args,
		@Nonnull SealedEntityReferenceProxyState theState
	) {
		final ReferenceBuilder referenceBuilder = theState.getReferenceBuilder();
		final EntityClassifier referencedClassifier = (EntityClassifier) args[0];
		Assert.isTrue(
			expectedEntityType.equals(referencedClassifier.getType()),
			"Entity type `" + referencedClassifier.getType() + "` in passed argument " +
				"doesn't match the referenced entity group type: `" + expectedEntityType + "`!"
		);
		referenceBuilder.setGroup(referencedClassifier.getPrimaryKey());
	}

	/**
	 * Resolves the parameter type for the given method and proxy state.
	 *
	 * @param method         the method for which to resolve the parameter type
	 * @param proxyState     the state of the sealed entity reference proxy
	 * @param firstParameter the first parameter of the method (nullable)
	 * @return an Optional containing the resolved parameter type, or empty if the parameter type cannot be resolved
	 */
	@Nonnull
	private static Optional resolveParameterType(
		@Nonnull Method method,
		@Nonnull SealedEntityReferenceProxyState proxyState,
		@Nullable ResolvedParameter firstParameter
	) {
		Optional referencedType = ofNullable(firstParameter);
		if (referencedType.map(it -> EntityReferenceContract.class.isAssignableFrom(it.resolvedType())).orElse(false)) {
			referencedType = of(new ResolvedParameter(EntityReferenceContract.class, EntityReferenceContract.class));
		} else if (referencedType.map(it -> Consumer.class.isAssignableFrom(it.resolvedType())).orElse(false)) {
			final List genericType = GenericsUtils.getGenericType(proxyState.getProxyClass(), method.getGenericParameterTypes()[0]);
			referencedType = of(
				new ResolvedParameter(
					referencedType.get().resolvedType(),
					genericType.get(0).getResolvedType()
				)
			);
		}
		return referencedType;
	}

	public SetReferenceGroupMethodClassifier() {
		super(
			"setReferencedGroup",
			(method, proxyState) -> {
				final int parameterCount = method.getParameterCount();
				// now we need to identify reference schema that is being requested
				final ReflectionLookup reflectionLookup = proxyState.getReflectionLookup();
				final ReferenceSchemaContract referenceSchema = proxyState.getReferenceSchema();

				final ReferencedEntityGroup referencedEntityGroup = reflectionLookup.getAnnotationInstanceForProperty(method, ReferencedEntityGroup.class);
				if (referencedEntityGroup == null) {
					return null;
				}

				final ResolvedParameter firstParameter = resolveFirstParameter(method, proxyState.getProxyClass());
				final Optional referencedType = resolveParameterType(method, proxyState, firstParameter);

				@SuppressWarnings("rawtypes") final Class returnType = method.getReturnType();
				final Optional entityRecognizedIn = recognizeCallContext(
					reflectionLookup, referenceSchema,
					of(returnType)
						.filter(it -> !void.class.equals(it) && !returnType.equals(proxyState.getProxyClass()))
						.orElse(null),
					firstParameter,
					referencedType.map(ResolvedParameter::resolvedType).orElse(null)
				);

				final boolean noDirectlyReferencedEntityRecognized = entityRecognizedIn.isEmpty();
				final Class expectedType = referencedType.map(ResolvedParameter::resolvedType).orElse(null);

				if (parameterCount == 0) {
					if (method.isAnnotationPresent(RemoveWhenExists.class)) {
						//noinspection unchecked
						return removeReferencedEntity(
							proxyState, referenceSchema,
							entityRecognizedIn.map(it -> it.entityContract().resolvedType()).orElse(returnType),
							isEntityRecognizedIn(entityRecognizedIn, EntityRecognizedIn.RETURN_TYPE)
						);
					} else if (isEntityRecognizedIn(entityRecognizedIn, EntityRecognizedIn.RETURN_TYPE)) {
						//noinspection unchecked
						return getOrCreateWithReferencedEntityGroupResult(
							referenceSchema,
							entityRecognizedIn.map(it -> it.entityContract().resolvedType()).orElse(returnType)
						);
					} else {
						return null;
					}
				} else if (parameterCount == 1) {
					final boolean parameterIsNumber = NumberUtils.isIntConvertibleNumber(method.getParameterTypes()[0]);
					if (parameterIsNumber && noDirectlyReferencedEntityRecognized) {
						return setOrRemoveReferencedGroupById(proxyState, method, returnType);
					} else if (resolvedTypeIs(referencedType, EntityReferenceContract.class) && noDirectlyReferencedEntityRecognized) {
						return setReferenceByEntityClassifier(proxyState, returnType, referenceSchema);
					} else if (isEntityRecognizedIn(entityRecognizedIn, EntityRecognizedIn.PARAMETER)) {
						return setReferencedEntityByEntity(proxyState, returnType, referenceSchema);
					} else if (isEntityRecognizedIn(entityRecognizedIn, EntityRecognizedIn.CONSUMER)) {
						return setReferencedEntityGroupByEntityConsumer(method, proxyState, referenceSchema, returnType, expectedType);
					} else if (isEntityRecognizedIn(entityRecognizedIn, EntityRecognizedIn.RETURN_TYPE) && parameterIsNumber) {
						return setOrRemoveReferencedEntityByEntityReturnType(entityRecognizedIn.get().entityContract().resolvedType());
					}
				}

				return null;
			}
		);
	}

}