/**
 * This software program, Simple Data Access Layer (SDAL), is copyrighted by Object
 * Computing inc of St. Louis MO USA. It is provided under the open-source model
 * and is free of license fees. You are free to modify this code for your own use
 * but you may not claim copyright.
 *
 * Since SDAL is open source and free of licensing fees, you are free to use,
 * modify, and distribute the source code, as long as you include this copyright
 * statement.
 *
 * In particular, you can use SDAL to build proprietary software and are under no
 * obligation to redistribute any of your source code that is built using SDAL.
 * Note, however, that you may not do anything to the SDAL code, such as
 * copyrighting it yourself or claiming authorship of the SDAL code, that will
 * prevent SDAL from being distributed freely using an open source development
 * model.
 *
 * Warranty
 * LICENSED PRODUCT, SDAL, IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING THE
 * WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE,
 * NONINFRINGEMENT, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
 *
 * Support
 * LICENSED PRODUCT, SDAL, IS PROVIDED WITH NO SUPPORT AND WITHOUT ANY OBLIGATION ON THE
 * PART OF OCI OR ANY OF ITS SUBSIDIARIES OR AFFILIATES TO ASSIST IN ITS USE,
 * CORRECTION, MODIFICATION OR ENHANCEMENT.
 *
 * Support may be available from OCI to users who have agreed to a support
 * contract.
 *
 * Liability
 * OCI OR ANY OF ITS SUBSIDIARIES OR AFFILIATES SHALL HAVE NO LIABILITY WITH
 * RESPECT TO THE INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY
 * LICENSED PRODUCT OR ANY PART THEREOF.
 *
 * IN NO EVENT WILL OCI OR ANY OF ITS SUBSIDIARIES OR AFFILIATES BE LIABLE FOR ANY
 * LOST REVENUE OR PROFITS OR OTHER SPECIAL, INDIRECT AND CONSEQUENTIAL DAMAGES,
 * EVEN IF OCI HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * Copyright OCI. St. Louis MO USA, 2004
 *
 */
package com.ociweb.service;

import com.ociweb.service.ServiceLocator;
import com.ociweb.service.ServiceLocatorException;
import com.ociweb.service.*;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

import net.sf.hibernate.Session;
import net.sf.hibernate.HibernateException;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;

class ServiceLocatorImpl implements ServiceLocator {
    private static final Map COMMANDS = new Hashtable();
    private static final Command FIND_WITH_NAMED_QUERY_COMMAND = new FindWithNamedQueryCommand();
    private List validatedClasses = new Vector();

    public ServiceLocatorImpl() {
        COMMANDS.put("add", new AddCommand());
        COMMANDS.put("update", new UpdateCommand());
        COMMANDS.put("remove", new RemoveCommand());
        COMMANDS.put("findByPrimaryKey", new FindByPrimaryKeyCommand());
        COMMANDS.put("findAll", new FindAllCommand());
    }

    public Object getDomainObjectManager(Class managerClass) throws ServiceLocatorException {
        validate(managerClass);
        return Proxy.newProxyInstance(managerClass.getClassLoader(), new Class[]{managerClass},
                                      new ManagerDelegate());
    }

    private void validate(Class managerClass) throws ServiceLocatorException {
        if (!validatedClasses.contains(managerClass)) {
            validateIsInterface(managerClass);
            validateHasCRUDlikeAPI(managerClass);
            validatedClasses.add(managerClass);
        }
    }

    private void validateIsInterface(Class managerClass) throws ServiceLocatorException {
        if (!managerClass.isInterface()) {
            throw exceptionFactory(managerClass, " is not an Interface");
        }
    }

    private void validateHasCRUDlikeAPI(Class managerClass) throws ServiceLocatorException {
        Method[] methods = managerClass.getMethods();
        List mgrMethods = new ArrayList(methods.length);
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            mgrMethods.add(method.getName());
        }
        if (!mgrMethods.containsAll(COMMANDS.keySet())) {
            throw exceptionFactory(managerClass,
                                   " must contain all of the following methods: 'add', 'update', 'remove', " +
                                   "'findByPrimaryKey', 'findAll'");
        }
    }

    private ServiceLocatorException exceptionFactory(Class managerClass, String message) {
        return new ServiceLocatorException("The supplied Class object (" + managerClass.getName() + ") " + message);
    }

    private static class ManagerDelegate implements InvocationHandler {
        private Logger log = LogManager.getLogger(getClass());

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Command command = resolveCommand(method);
            if (command == null) {
                throw new UnsupportedOperationException();
            }
            try {
                return command.execute(method, args, getSession());
            } catch (Exception e) {
                invalidateSession();
                throw e;
            }
        }

        private Command resolveCommand(Method method) {
            Command result = (Command) COMMANDS.get(method.getName());
            if (result == null && method.getName().startsWith("find")) {
                //If it is not one of the default commands but it begins with 'find', assume it is a finder for
                //named queries
                result = FIND_WITH_NAMED_QUERY_COMMAND;
            }
            return result;
        }

        private Session getSession() throws SessionException {
            Session session = ThreadSessionHolder.get();

            if (!session.isConnected()) {
                try {
                    session.reconnect();
                } catch (HibernateException he) {
                    throw new SessionException("Could not reconnect the session", he);
                }
            }
            return session;
        }

        private void invalidateSession() {
            try {
                ThreadSessionHolder.get().close();
            } catch (HibernateException e) {
                log.error("Unable to close the session");
            }
            ThreadSessionHolder.set(null);
        }
    }
}
