/*
 * Copyright 2002-2013 the original author or authors.
 *
 * 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.springframework.aop.aspectj.autoproxy;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.Test;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.aspectj.annotation.AspectMetadata;
import org.springframework.aop.config.AopConfigUtils;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.NestedRuntimeException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.tests.Assume;
import org.springframework.tests.TestGroup;
import org.springframework.tests.sample.beans.INestedTestBean;
import org.springframework.tests.sample.beans.ITestBean;
import org.springframework.tests.sample.beans.NestedTestBean;
import org.springframework.tests.sample.beans.TestBean;
import org.springframework.util.StopWatch;

import static java.lang.String.format;
import static org.junit.Assert.*;

/**
 * Integration tests for AspectJ auto-proxying. Includes mixing with Spring AOP Advisors
 * to demonstrate that existing autoproxying contract is honoured.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Chris Beams
 * @author Sam Brannen
 */
public final class AspectJAutoProxyCreatorTests {

	private static final Log factoryLog = LogFactory.getLog(DefaultListableBeanFactory.class);

	private static void assertStopWatchTimeLimit(final StopWatch sw, final long maxTimeMillis) {
		final long totalTimeMillis = sw.getTotalTimeMillis();
		assertTrue("'" + sw.getLastTaskName() + "' took too long: expected less than<" + maxTimeMillis
				+ "> ms, actual<" + totalTimeMillis + "> ms.", totalTimeMillis < maxTimeMillis);
	}

	@Test
	public void testAspectsAreApplied() {
		ClassPathXmlApplicationContext bf = newContext("aspects.xml");
		ITestBean tb = (ITestBean) bf.getBean("adrian");
		assertEquals(68, tb.getAge());
		MethodInvokingFactoryBean factoryBean = (MethodInvokingFactoryBean) bf.getBean("&factoryBean");
		assertTrue(AopUtils.isAopProxy(factoryBean.getTargetObject()));
		assertEquals(68, ((ITestBean) factoryBean.getTargetObject()).getAge());
	}

	@Test
	public void testMultipleAspectsWithParameterApplied() {
		ClassPathXmlApplicationContext bf = newContext("aspects.xml");
		ITestBean tb = (ITestBean) bf.getBean("adrian");
		tb.setAge(10);
		assertEquals(20, tb.getAge());
	}

	@Test
	public void testAspectsAreAppliedInDefinedOrder() {
		ClassPathXmlApplicationContext bf = newContext("aspectsWithOrdering.xml");
		ITestBean tb = (ITestBean) bf.getBean("adrian");
		assertEquals(71, tb.getAge());
	}

	@Test
	public void testAspectsAndAdvisorAreApplied() {
		ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
		ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian");
		doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved);
	}

	@Test
	public void testAspectsAndAdvisorAppliedToPrototypeIsFastEnough() {
		Assume.group(TestGroup.PERFORMANCE);
		Assume.notLogging(factoryLog);
		ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
		StopWatch sw = new StopWatch();
		sw.start("Prototype Creation");
		for (int i = 0; i < 10000; i++) {
			ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian2");
			if (i < 10) {
				doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved);
			}
		}
		sw.stop();

		// What's a reasonable expectation for _any_ server or developer machine load?
		// 9 seconds?
		assertStopWatchTimeLimit(sw, 9000);
	}

	@Test
	public void testAspectsAndAdvisorNotAppliedToPrototypeIsFastEnough() {
		Assume.group(TestGroup.PERFORMANCE);
		Assume.notLogging(factoryLog);
		ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
		StopWatch sw = new StopWatch();
		sw.start("Prototype Creation");
		for (int i = 0; i < 100000; i++) {
			INestedTestBean shouldNotBeWeaved = (INestedTestBean) ac.getBean("i21");
			if (i < 10) {
				assertFalse(AopUtils.isAopProxy(shouldNotBeWeaved));
			}
		}
		sw.stop();

		// What's a reasonable expectation for _any_ server or developer machine load?
		// 3 seconds?
		assertStopWatchTimeLimit(sw, 6000);
	}

	@Test
	public void testAspectsAndAdvisorNotAppliedToManySingletonsIsFastEnough() {
		Assume.group(TestGroup.PERFORMANCE);
		Assume.notLogging(factoryLog);
		GenericApplicationContext ac = new GenericApplicationContext();
		new XmlBeanDefinitionReader(ac).loadBeanDefinitions(new ClassPathResource(qName("aspectsPlusAdvisor.xml"),
				getClass()));
		for (int i = 0; i < 10000; i++) {
			ac.registerBeanDefinition("singleton" + i, new RootBeanDefinition(NestedTestBean.class));
		}
		StopWatch sw = new StopWatch();
		sw.start("Singleton Creation");
		ac.refresh();
		sw.stop();

		// What's a reasonable expectation for _any_ server or developer machine load?
		// 8 seconds?
		assertStopWatchTimeLimit(sw, 8000);
	}

	@Test
	public void testAspectsAndAdvisorAreAppliedEvenIfComingFromParentFactory() {
		ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
		GenericApplicationContext childAc = new GenericApplicationContext(ac);
		// Create a child factory with a bean that should be woven
		RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
		bd.getPropertyValues().addPropertyValue(new PropertyValue("name", "Adrian"))
				.addPropertyValue(new PropertyValue("age", new Integer(34)));
		childAc.registerBeanDefinition("adrian2", bd);
		// Register the advisor auto proxy creator with subclass
		childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), new RootBeanDefinition(
				AnnotationAwareAspectJAutoProxyCreator.class));
		childAc.refresh();

		ITestBean beanFromChildContextThatShouldBeWeaved = (ITestBean) childAc.getBean("adrian2");
		//testAspectsAndAdvisorAreApplied(childAc, (ITestBean) ac.getBean("adrian"));
		doTestAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWeaved);
	}

	protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWeaved) {
		TestBeanAdvisor tba = (TestBeanAdvisor) ac.getBean("advisor");

		MultiplyReturnValue mrv = (MultiplyReturnValue) ac.getBean("aspect");
		assertEquals(3, mrv.getMultiple());

		tba.count = 0;
		mrv.invocations = 0;

		assertTrue("Autoproxying must apply from @AspectJ aspect", AopUtils.isAopProxy(shouldBeWeaved));
		assertEquals("Adrian", shouldBeWeaved.getName());
		assertEquals(0, mrv.invocations);
		assertEquals(34 * mrv.getMultiple(), shouldBeWeaved.getAge());
		assertEquals("Spring advisor must be invoked", 2, tba.count);
		assertEquals("Must be able to hold state in aspect", 1, mrv.invocations);
	}

	@Test
	public void testPerThisAspect() {
		ClassPathXmlApplicationContext bf = newContext("perthis.xml");

		ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
		assertTrue(AopUtils.isAopProxy(adrian1));

		assertEquals(0, adrian1.getAge());
		assertEquals(1, adrian1.getAge());

		ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
		assertNotSame(adrian1, adrian2);
		assertTrue(AopUtils.isAopProxy(adrian1));
		assertEquals(0, adrian2.getAge());
		assertEquals(1, adrian2.getAge());
		assertEquals(2, adrian2.getAge());
		assertEquals(3, adrian2.getAge());
		assertEquals(2, adrian1.getAge());
	}

	@Test
	public void testPerTargetAspect() throws SecurityException, NoSuchMethodException {
		ClassPathXmlApplicationContext bf = newContext("pertarget.xml");

		ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
		assertTrue(AopUtils.isAopProxy(adrian1));

		// Does not trigger advice or count
		int explicitlySetAge = 25;
		adrian1.setAge(explicitlySetAge);

		assertEquals("Setter does not initiate advice", explicitlySetAge, adrian1.getAge());
		// Fire aspect

		AspectMetadata am = new AspectMetadata(PerTargetAspect.class, "someBean");
		assertTrue(am.getPerClausePointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null));

		adrian1.getSpouse();

		assertEquals("Advice has now been instantiated", 0, adrian1.getAge());
		adrian1.setAge(11);
		assertEquals("Any int setter increments", 2, adrian1.getAge());
		adrian1.setName("Adrian");
		//assertEquals("Any other setter does not increment", 2, adrian1.getAge());

		ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
		assertNotSame(adrian1, adrian2);
		assertTrue(AopUtils.isAopProxy(adrian1));
		assertEquals(34, adrian2.getAge());
		adrian2.getSpouse();
		assertEquals("Aspect now fired", 0, adrian2.getAge());
		assertEquals(1, adrian2.getAge());
		assertEquals(2, adrian2.getAge());
		assertEquals(3, adrian1.getAge());
	}

	@Test
	public void testTwoAdviceAspectSingleton() {
		doTestTwoAdviceAspectWith("twoAdviceAspect.xml");
	}

	@Test
	public void testTwoAdviceAspectPrototype() {
		doTestTwoAdviceAspectWith("twoAdviceAspectPrototype.xml");
	}

	private void doTestTwoAdviceAspectWith(String location) {
		ClassPathXmlApplicationContext bf = newContext(location);

		boolean aspectSingleton = bf.isSingleton("aspect");
		ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
		testPrototype(adrian1, 0);
		ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
		assertNotSame(adrian1, adrian2);
		testPrototype(adrian2, aspectSingleton ? 2 : 0);
	}

	@Test
	public void testAdviceUsingJoinPoint() {
		ClassPathXmlApplicationContext bf = newContext("usesJoinPointAspect.xml");

		ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
		adrian1.getAge();
		AdviceUsingThisJoinPoint aspectInstance = (AdviceUsingThisJoinPoint) bf.getBean("aspect");
		//(AdviceUsingThisJoinPoint) Aspects.aspectOf(AdviceUsingThisJoinPoint.class);
		//assertEquals("method-execution(int TestBean.getAge())",aspectInstance.getLastMethodEntered());
		assertTrue(aspectInstance.getLastMethodEntered().indexOf("TestBean.getAge())") != 0);
	}

	@Test
	public void testIncludeMechanism() {
		ClassPathXmlApplicationContext bf = newContext("usesInclude.xml");

		ITestBean adrian = (ITestBean) bf.getBean("adrian");
		assertTrue(AopUtils.isAopProxy(adrian));
		assertEquals(68, adrian.getAge());
	}

	private void testPrototype(ITestBean adrian1, int start) {
		assertTrue(AopUtils.isAopProxy(adrian1));
		//TwoAdviceAspect twoAdviceAspect = (TwoAdviceAspect) bf.getBean(TwoAdviceAspect.class.getName());
		adrian1.setName("");
		assertEquals(start++, adrian1.getAge());
		int newAge = 32;
		adrian1.setAge(newAge);
		assertEquals(start++, adrian1.getAge());
		adrian1.setAge(0);
		assertEquals(start++, adrian1.getAge());
	}

	@Test
	public void testForceProxyTargetClass() {
		ClassPathXmlApplicationContext bf = newContext("aspectsWithCGLIB.xml");

		ProxyConfig pc = (ProxyConfig) bf.getBean(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME);
		assertTrue("should be proxying classes", pc.isProxyTargetClass());
		assertTrue("should expose proxy", pc.isExposeProxy());
	}

	@Test
	public void testWithAbstractFactoryBeanAreApplied() {
		ClassPathXmlApplicationContext bf = newContext("aspectsWithAbstractBean.xml");

		ITestBean adrian = (ITestBean) bf.getBean("adrian");
		assertTrue(AopUtils.isAopProxy(adrian));
		assertEquals(68, adrian.getAge());
	}

	@Test
	public void testRetryAspect() throws Exception {
		ClassPathXmlApplicationContext bf = newContext("retryAspect.xml");
		UnreliableBean bean = (UnreliableBean) bf.getBean("unreliableBean");
		RetryAspect aspect = (RetryAspect) bf.getBean("retryAspect");
		int attempts = bean.unreliable();
		assertEquals(2, attempts);
		assertEquals(2, aspect.getBeginCalls());
		assertEquals(1, aspect.getRollbackCalls());
		assertEquals(1, aspect.getCommitCalls());
	}

	/**
	 * Returns a new {@link ClassPathXmlApplicationContext} for the file ending in <var>fileSuffix</var>.
	 */
	private ClassPathXmlApplicationContext newContext(String fileSuffix) {
		return new ClassPathXmlApplicationContext(qName(fileSuffix), getClass());
	}

	/**
	 * Returns the relatively qualified name for <var>fileSuffix</var>.
	 * e.g. for a fileSuffix='foo.xml', this method will return
	 * 'AspectJAutoProxyCreatorTests-foo.xml'
	 */
	private String qName(String fileSuffix) {
		return format("%s-%s", getClass().getSimpleName(), fileSuffix);
	}

}

@Aspect("pertarget(execution(* *.getSpouse()))")
class PerTargetAspect implements Ordered {

	public int count;

	private int order = Ordered.LOWEST_PRECEDENCE;

	@Around("execution(int *.getAge())")
	public int returnCountAsAge() {
		return count++;
	}

	@Before("execution(void *.set*(int))")
	public void countSetter() {
		++count;
	}

	@Override
	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}
}

@Aspect
class AdviceUsingThisJoinPoint {

	private String lastEntry = "";

	public String getLastMethodEntered() {
		return this.lastEntry;
	}

	@Pointcut("execution(* *(..))")
	public void methodExecution() {
	}

	@Before("methodExecution()")
	public void entryTrace(JoinPoint jp) {
		this.lastEntry = jp.toString();
	}

}

@Aspect
class DummyAspect {

	@Around("execution(* setAge(int))")
	public Object test(ProceedingJoinPoint pjp) throws Throwable {
		return pjp.proceed();
	}

}

@Aspect
class DummyAspectWithParameter {

	@Around("execution(* setAge(int)) && args(age)")
	public Object test(ProceedingJoinPoint pjp, int age) throws Throwable {
		return pjp.proceed();
	}

}

class DummyFactoryBean implements FactoryBean<Object> {

	@Override
	public Object getObject() throws Exception {
		throw new UnsupportedOperationException();
	}

	@Override
	public Class<?> getObjectType() {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean isSingleton() {
		throw new UnsupportedOperationException();
	}

}

@Aspect
@Order(10)
class IncreaseReturnValue {

	@Around("execution(int *.getAge())")
	public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable {
		int result = (Integer) pjp.proceed();
		return result + 3;
	}

}

@Aspect
class MultiplyReturnValue {

	private int multiple = 2;

	public int invocations;

	public void setMultiple(int multiple) {
		this.multiple = multiple;
	}

	public int getMultiple() {
		return this.multiple;
	}

	@Around("execution(int *.getAge())")
	public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable {
		++this.invocations;
		int result = (Integer) pjp.proceed();
		return result * this.multiple;
	}

}

@Aspect
class RetryAspect {

	private int beginCalls;

	private int commitCalls;

	private int rollbackCalls;

	@Pointcut("execution(public * UnreliableBean.*(..))")
	public void execOfPublicMethod() {
	}

	/**
	 * Retry Advice
	 */
	@Around("execOfPublicMethod()")
	public Object retry(ProceedingJoinPoint jp) throws Throwable {
		boolean retry = true;
		Object o = null;
		while (retry) {
			try {
				retry = false;
				this.beginCalls++;
				try {
					o = jp.proceed();
					this.commitCalls++;
				} catch (RetryableException e) {
					this.rollbackCalls++;
					throw e;
				}
			} catch (RetryableException re) {
				retry = true;
			}
		}
		return o;
	}

	public int getBeginCalls() {
		return this.beginCalls;
	}

	public int getCommitCalls() {
		return this.commitCalls;
	}

	public int getRollbackCalls() {
		return this.rollbackCalls;
	}

}

@SuppressWarnings("serial")
class RetryableException extends NestedRuntimeException {

	public RetryableException(String msg) {
		super(msg);
	}

	public RetryableException(String msg, Throwable cause) {
		super(msg, cause);
	}
}

class UnreliableBean {

	private int calls;

	public int unreliable() {
		this.calls++;
		if (this.calls % 2 != 0) {
			throw new RetryableException("foo");
		}
		return this.calls;
	}

}

@SuppressWarnings("serial")
class TestBeanAdvisor extends StaticMethodMatcherPointcutAdvisor {

	public int count;

	public TestBeanAdvisor() {
		setAdvice(new MethodBeforeAdvice() {
			@Override
			public void before(Method method, Object[] args, Object target) throws Throwable {
				++count;
			}
		});
	}

	@Override
	public boolean matches(Method method, Class<?> targetClass) {
		return ITestBean.class.isAssignableFrom(targetClass);
	}

}