# UnitOfWorkConcern.java

    /*  Copyright 2008 Edward Yakop.
     *  Copyright 2009 Niclas Hedhman.
     *
     * 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.api.unitofwork.concern;

    import java.lang.reflect.Method;
    import org.qi4j.api.common.AppliesTo;
    import org.qi4j.api.concern.GenericConcern;
    import org.qi4j.api.injection.scope.Invocation;
    import org.qi4j.api.injection.scope.Structure;
    import org.qi4j.api.injection.scope.Uses;
    import org.qi4j.api.structure.Module;
    import org.qi4j.api.unitofwork.ConcurrentEntityModificationException;
    import org.qi4j.api.unitofwork.UnitOfWork;
    import org.qi4j.api.usecase.Usecase;
    import org.qi4j.api.usecase.UsecaseBuilder;

    /**
     * {@code UnitOfWorkConcern} manages the unit of work complete, discard and retry policy.
     *
     * @see UnitOfWorkPropagation
     * @see UnitOfWorkDiscardOn
     */
    @AppliesTo( UnitOfWorkPropagation.class )
    public class UnitOfWorkConcern
        extends GenericConcern
    {
        private static final Class<?>[] DEFAULT_DISCARD_CLASSES = new Class[]{ Throwable.class };

        @Structure
        Module module;

        @Invocation
        UnitOfWorkPropagation propagation;

        /**
         * Handles method with {@code UnitOfWorkPropagation} annotation.
         *
         * @param proxy  The object.
         * @param method The invoked method.
         * @param args   The method arguments.
         *
         * @return The returned value of method invocation.
         *
         * @throws Throwable Thrown if the method invocation throw exception.
         */
        @Override
        public Object invoke( Object proxy, Method method, Object[] args )
            throws Throwable
        {
            UnitOfWorkPropagation.Propagation propagationPolicy = propagation.value();
            if( propagationPolicy == UnitOfWorkPropagation.Propagation.REQUIRED )
            {
                if( module.isUnitOfWorkActive() )
                {
                    return next.invoke( proxy, method, args );
                }
                else
                {
                    Usecase usecase = usecase();
                    return invokeWithCommit( proxy, method, args, module.newUnitOfWork( usecase ) );
                }
            }
            else if( propagationPolicy == UnitOfWorkPropagation.Propagation.MANDATORY )
            {
                if( !module.isUnitOfWorkActive() )
                {
                    throw new IllegalStateException( "UnitOfWork was required but there is no available unit of work." );
                }
            }
            else if( propagationPolicy == UnitOfWorkPropagation.Propagation.REQUIRES_NEW )
            {
                Usecase usecase = usecase();
                return invokeWithCommit( proxy, method, args, module.newUnitOfWork( usecase ) );
            }
            return next.invoke( proxy, method, args );
        }

        private Usecase usecase()
        {
            String usecaseName = propagation.usecase();
            Usecase usecase;
            if( usecaseName == null )
            {
                usecase = Usecase.DEFAULT;
            }
            else
            {
                usecase = UsecaseBuilder.newUsecase( usecaseName );
            }
            return usecase;
        }

        protected Object invokeWithCommit( Object proxy, Method method, Object[] args, UnitOfWork currentUnitOfWork )
            throws Throwable
        {
            try
            {
                UnitOfWorkRetry retryAnnot = method.getAnnotation( UnitOfWorkRetry.class );
                int maxTries = 0;
                long delayFactor = 0;
                long initialDelay = 0;
                if( retryAnnot != null )
                {
                    maxTries = retryAnnot.retries();
                    initialDelay = retryAnnot.initialDelay();
                    delayFactor = retryAnnot.delayFactory();
                }
                int retry = 0;
                while( true )
                {
                    Object result = next.invoke( proxy, method, args );
                    try
                    {
                        currentUnitOfWork.complete();
                        return result;
                    }
                    catch( ConcurrentEntityModificationException e )
                    {
                        if( retry >= maxTries )
                        {
                            throw e;
                        }
                        module.currentUnitOfWork().discard();
                        Thread.sleep( initialDelay + retry * delayFactor );
                        retry++;
                        currentUnitOfWork = module.newUnitOfWork( usecase() );
                    }
                }
            }
            catch( Throwable throwable )
            {
                // Discard only if this concern create a unit of work
                discardIfRequired( method, currentUnitOfWork, throwable );
                throw throwable;
            }
        }

        /**
         * Discard unit of work if the discard policy match.
         *
         * @param aMethod     The invoked method. This argument must not be {@code null}.
         * @param aUnitOfWork The current unit of work. This argument must not be {@code null}.
         * @param aThrowable  The exception thrown. This argument must not be {@code null}.
         */
        protected void discardIfRequired( Method aMethod, UnitOfWork aUnitOfWork, Throwable aThrowable )
        {
            UnitOfWorkDiscardOn discardPolicy = aMethod.getAnnotation( UnitOfWorkDiscardOn.class );
            Class<?>[] discardClasses;
            if( discardPolicy != null )
            {
                discardClasses = discardPolicy.value();
            }
            else
            {
                discardClasses = DEFAULT_DISCARD_CLASSES;
            }

            Class<? extends Throwable> aThrowableClass = aThrowable.getClass();
            for( Class<?> discardClass : discardClasses )
            {
                if( discardClass.isAssignableFrom( aThrowableClass ) )
                {
                    aUnitOfWork.discard();
                }
            }
        }
    }