# EntityInstance.java

    /*
     * Copyright (c) 2008-2009, Rickard Öberg. All Rights Reserved.
     * Copyright (c) 2009-2013, Niclas Hedhman. All Rights Reserved.
     * Copyright (c) 2014, Paul Merlin. All Rights Reserved.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     *
     */
    package org.qi4j.runtime.entity;

    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.HashSet;
    import java.util.Set;
    import org.qi4j.api.association.Association;
    import org.qi4j.api.association.AssociationDescriptor;
    import org.qi4j.api.association.AssociationStateDescriptor;
    import org.qi4j.api.association.ManyAssociation;
    import org.qi4j.api.association.NamedAssociation;
    import org.qi4j.api.composite.CompositeDescriptor;
    import org.qi4j.api.composite.CompositeInstance;
    import org.qi4j.api.constraint.ConstraintViolationException;
    import org.qi4j.api.entity.EntityComposite;
    import org.qi4j.api.entity.EntityReference;
    import org.qi4j.api.entity.Identity;
    import org.qi4j.api.entity.LifecycleException;
    import org.qi4j.api.unitofwork.NoSuchEntityException;
    import org.qi4j.api.unitofwork.UnitOfWork;
    import org.qi4j.api.unitofwork.UnitOfWorkException;
    import org.qi4j.runtime.composite.CompositeMethodInstance;
    import org.qi4j.runtime.composite.MixinsInstance;
    import org.qi4j.runtime.structure.ModuleUnitOfWork;
    import org.qi4j.spi.entity.EntityState;
    import org.qi4j.spi.entity.EntityStatus;
    import org.qi4j.spi.module.ModuleSpi;

    /**
     * Entity instance
     */
    public final class EntityInstance
        implements CompositeInstance, MixinsInstance
    {
        public static EntityInstance entityInstanceOf( EntityComposite composite )
        {
            return (EntityInstance) Proxy.getInvocationHandler( composite );
        }

        private final EntityComposite proxy;
        private final ModuleUnitOfWork uow;
        private final ModuleSpi moduleInstance;
        private final EntityModel entityModel;
        private final EntityReference identity;
        private final EntityState entityState;

        private Object[] mixins;
        private EntityStateInstance state;

        public EntityInstance( ModuleUnitOfWork uow,
                               ModuleSpi moduleInstance,
                               EntityModel entityModel,
                               EntityState entityState
        )
        {
            this.uow = uow;
            this.moduleInstance = moduleInstance;
            this.entityModel = entityModel;
            this.identity = entityState.identity();
            this.entityState = entityState;

            proxy = (EntityComposite) entityModel.newProxy( this );
        }

        @Override
        public Object invoke( Object proxy, Method method, Object[] args )
            throws Throwable
        {
            return entityModel.invoke( this, this.proxy, method, args, moduleInstance );
        }

        public EntityReference identity()
        {
            return identity;
        }

        @Override
        @SuppressWarnings( "unchecked" )
        public <T> T proxy()
        {
            return (T) proxy;
        }

        @Override
        public CompositeDescriptor descriptor()
        {
            return entityModel;
        }

        @Override
        public <T> T newProxy( Class<T> mixinType )
            throws IllegalArgumentException
        {
            return entityModel.newProxy( this, mixinType );
        }

        @Override
        public Object invokeComposite( Method method, Object[] args )
            throws Throwable
        {
            return entityModel.invoke( this, proxy, method, args, moduleInstance );
        }

        @Override
        public <T> T metaInfo( Class<T> infoType )
        {
            return entityModel.metaInfo( infoType );
        }

        public EntityModel entityModel()
        {
            return entityModel;
        }

        @Override
        public Iterable<Class<?>> types()
        {
            return entityModel.types();
        }

        @Override
        public ModuleSpi module()
        {
            return moduleInstance;
        }

        public UnitOfWork unitOfWork()
        {
            return uow;
        }

        public EntityState entityState()
        {
            return entityState;
        }

        @Override
        public EntityStateInstance state()
        {
            if( state == null )
            {
                initState();
            }

            return state;
        }

        public EntityStatus status()
        {
            return entityState.status();
        }

        @Override
        public Object invoke( Object composite, Object[] params, CompositeMethodInstance methodInstance )
            throws Throwable
        {
            if( mixins == null )
            {
                initState();
            }

            Object mixin = methodInstance.getMixinFrom( mixins );

            if( mixin == null )
            {
                mixin = entityModel.newMixin( mixins, state, this, methodInstance.method() );
            }

            return methodInstance.invoke( proxy, params, mixin );
        }

        @Override
        public Object invokeObject( Object proxy, Object[] args, Method method )
            throws Throwable
        {
            return method.invoke( this, args );
        }

        private void initState()
        {
            if( !uow.isOpen() )
            {
                throw new UnitOfWorkException( "Unit of work has been closed" );
            }

            if( status() == EntityStatus.REMOVED )
            {
                throw new NoSuchEntityException( identity, entityModel.types(), unitOfWork().usecase() );
            }

            mixins = entityModel.newMixinHolder();
            state = new EntityStateInstance( entityModel.state(), uow, entityState );
        }

        @Override
        public int hashCode()
        {
            return identity.hashCode();
        }

        @Override
        public boolean equals( Object o )
        {
            try
            {
                Identity other = ( (Identity) o );
                return other != null && other.identity().get().equals( identity.identity() );
            }
            catch( ClassCastException e )
            {
                return false;
            }
        }

        @Override
        public String toString()
        {
            return identity.toString();
        }

        public void remove( UnitOfWork unitOfWork )
            throws LifecycleException
        {
            invokeRemove();

            removeAggregatedEntities( unitOfWork );

            entityState.remove();
            mixins = null;
        }

        public void invokeCreate()
        {
            lifecyleInvoke( true );
        }

        private void invokeRemove()
        {
            lifecyleInvoke( false );
        }

        private void lifecyleInvoke( boolean create )
        {
            if( mixins == null )
            {
                initState();
            }

            entityModel.invokeLifecycle( create, mixins, this, state );
        }

        private void removeAggregatedEntities( UnitOfWork unitOfWork )
        {
            // Calculate aggregated Entities
            AssociationStateDescriptor stateDescriptor = entityModel.state();
            Set<Object> aggregatedEntities = new HashSet<>();
            Iterable<? extends AssociationDescriptor> associations = stateDescriptor.associations();
            for( AssociationDescriptor association : associations )
            {
                if( association.isAggregated() )
                {
                    Association<?> assoc = state.associationFor( association.accessor() );
                    Object aggregatedEntity = assoc.get();
                    if( aggregatedEntity != null )
                    {
                        aggregatedEntities.add( aggregatedEntity );
                    }
                }
            }
            Iterable<? extends AssociationDescriptor> manyAssociations = stateDescriptor.manyAssociations();
            for( AssociationDescriptor association : manyAssociations )
            {
                if( association.isAggregated() )
                {
                    ManyAssociation<?> manyAssoc = state.manyAssociationFor( association.accessor() );
                    for( Object entity : manyAssoc )
                    {
                        aggregatedEntities.add( entity );
                    }
                }
            }
            Iterable<? extends AssociationDescriptor> namedAssociations = stateDescriptor.namedAssociations();
            for( AssociationDescriptor association : namedAssociations )
            {
                if( association.isAggregated() )
                {
                    NamedAssociation<?> namedAssoc = state.namedAssociationFor( association.accessor() );
                    for( String name : namedAssoc )
                    {
                        aggregatedEntities.add( namedAssoc.get( name ) );
                    }
                }
            }

            // Remove aggregated Entities
            for( Object aggregatedEntity : aggregatedEntities )
            {
                unitOfWork.remove( aggregatedEntity );
            }
        }

        public void checkConstraints()
        {
            try
            {
                state.checkConstraints();
            }
            catch( ConstraintViolationException e )
            {
                throw new ConstraintViolationException( identity.identity(), entityModel.types(), e.mixinTypeName(), e.methodName(), e
                    .constraintViolations() );
            }
        }
    }