/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.wicket.proxy.jdk;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.apache.wicket.Application;
import org.apache.wicket.proxy.ILazyInitProxy;
import org.apache.wicket.proxy.IProxyFactory;
import org.apache.wicket.proxy.IProxyTargetLocator;
import org.apache.wicket.proxy.LazyInitProxyFactory;
import org.apache.wicket.proxy.LazyInitProxyFactory.IWriteReplace;
import org.apache.wicket.proxy.LazyInitProxyFactory.ProxyReplacement;

/**
 * A factory class that creates jdk dynamic proxies.
 */
public class JdkProxyFactory implements IProxyFactory
{
	/**
	 * Create a lazy init proxy for the specified type. The target object will be located using the
	 * provided locator upon first method invocation.
	 * 
	 * @param type
	 *            type that proxy will represent
	 * 
	 * @param locator
	 *            object locator that will locate the object the proxy represents
	 * 
	 * @return lazily initializable proxy
	 */
	@SuppressWarnings("unchecked")
	public <T> T createProxy(final Class<T> type, final IProxyTargetLocator locator)
	{
		JdkHandler handler = new JdkHandler(type, locator);

		try
		{
			return (T) Proxy.newProxyInstance(resolveClassLoader(),
				new Class[] { type, Serializable.class, ILazyInitProxy.class,
						IWriteReplace.class }, handler);
		}
		catch (IllegalArgumentException e)
		{
			/*
			 * STW: In some clustering environments it appears the context classloader fails to
			 * load the proxied interface (currently seen in BEA WLS 9.x clusters). If this
			 * happens, we can try and fall back to the classloader (current) that actually
			 * loaded this class.
			 */
			return (T) Proxy.newProxyInstance(LazyInitProxyFactory.class.getClassLoader(),
				new Class[] { type, Serializable.class, ILazyInitProxy.class,
						IWriteReplace.class }, handler);
		}
	}
	
	private ClassLoader resolveClassLoader()
	{
		ClassLoader classLoader = null;
		if (Application.exists())
		{
			classLoader = Application.get().getApplicationSettings()
					.getClassResolver().getClassLoader();
		}

		if (classLoader == null) {
			classLoader = Thread.currentThread().getContextClassLoader();
		}

		return classLoader;
	}

	/**
	 * Invocation handler for proxies representing interface based object. For interface backed
	 * objects dynamic jdk proxies are used.
	 * 
	 * @author Igor Vaynberg (ivaynberg)
	 * 
	 */
	private static class JdkHandler
		implements
			InvocationHandler,
			ILazyInitProxy,
			Serializable,
			IWriteReplace
	{
		private static final long serialVersionUID = 1L;

		private final IProxyTargetLocator locator;

		private final String typeName;

		private transient Object target;

		/**
		 * Constructor
		 * 
		 * @param type
		 *            class of object this handler will represent
		 * 
		 * @param locator
		 *            object locator used to locate the object this proxy represents
		 */
		public JdkHandler(final Class<?> type, final IProxyTargetLocator locator)
		{
			super();
			this.locator = locator;
			typeName = type.getName();
		}

		/**
		 * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
		 *      java.lang.reflect.Method, java.lang.Object[])
		 */
		@Override
		public Object invoke(final Object proxy, final Method method, final Object[] args)
			throws Throwable
		{
			if (LazyInitProxyFactory.isFinalizeMethod(method))
			{
				// swallow finalize call
				return null;
			}
			else if (LazyInitProxyFactory.isEqualsMethod(method))
			{
				return (equals(args[0])) ? Boolean.TRUE : Boolean.FALSE;
			}
			else if (LazyInitProxyFactory.isHashCodeMethod(method))
			{
				return hashCode();
			}
			else if (LazyInitProxyFactory.isToStringMethod(method))
			{
				return toString();
			}
			else if (method.getDeclaringClass().equals(ILazyInitProxy.class))
			{
				return getObjectLocator();
			}
			else if (LazyInitProxyFactory.isWriteReplaceMethod(method))
			{
				return writeReplace();
			}

			if (target == null)
			{

				target = locator.locateProxyTarget();
			}
			try
			{
				method.setAccessible(true);
				return method.invoke(target, args);
			}
			catch (InvocationTargetException e)
			{
				throw e.getTargetException();
			}
		}

		/**
		 * @see org.apache.wicket.proxy.ILazyInitProxy#getObjectLocator()
		 */
		@Override
		public IProxyTargetLocator getObjectLocator()
		{
			return locator;
		}

		/**
		 * @see org.apache.wicket.proxy.LazyInitProxyFactory.IWriteReplace#writeReplace()
		 */
		@Override
		public Object writeReplace() throws ObjectStreamException
		{
			return new ProxyReplacement(typeName, locator);
		}
	}
}
