package common.filters;

import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.InputSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.net.MalformedURLException;
import java.util.*;

import common.components.ServletContextComponent;
import common.components.SessionContextComponent;
import common.components.RequestContextComponent;

public class ComponentFilter extends DefaultHandler implements Filter {

    private static Log log = LogFactory.getLog(ComponentFilter.class);

    public static final String DEFAULT_COMPONENTS_FILE_NAME = "/WEB-INF/components.xml";

    private File componentsFile;

    private HttpServletRequest request;
    private HttpSession session;
    private ServletContext servlet;

    private List servletComponents;
    private List sessionComponents;
    private List requestComponents;

    private long componentsFileLastModified;

    public void init(FilterConfig config) throws ServletException {

        this.servlet = config.getServletContext();

        String componentsFilePath = servlet.getRealPath(DEFAULT_COMPONENTS_FILE_NAME);

        if (componentsFilePath != null) {
            componentsFile = new File(componentsFilePath);
        }

        servletComponents = new ArrayList();
        sessionComponents = new ArrayList();
        requestComponents = new ArrayList();

        loadConfiguration();
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        this.request = (HttpServletRequest) request;
        this.session = this.request.getSession(true);

        if ((componentsFile != null) && (componentsFileLastModified != componentsFile.lastModified())) {
            loadConfiguration();
            instantiateSessionComponents();
        } else {
            if (session.isNew()) {
                instantiateSessionComponents();
            }
        }

        instantiateRequestComponents();

        filterChain.doFilter(request, response);
    }

    private void instantiateRequestComponents() {

        int componentsCount = requestComponents.size();

        Component component;
        String name;
        String clazz;
        Map attributes;

        for (int i=0; i < componentsCount; i++) {

            component = (Component) requestComponents.get(i);

            name = component.getName();
            clazz = component.getClazz();
            attributes = component.getAttributes();

            try {

                Class componentClass = null;

                log.debug("Loading request context component class \"" + clazz + "\".");
                try {
                    componentClass = Class.forName(clazz);
                } catch (ClassNotFoundException e) {
                    componentClass = Class.forName(clazz, true, Thread.currentThread().getContextClassLoader());
                }

                Object componentInstance = componentClass.newInstance();

                if (componentInstance instanceof RequestContextComponent) {
                    ((RequestContextComponent) componentInstance).setRequestContext(request);
                    ((RequestContextComponent) componentInstance).setAttributes(attributes);
                }
                log.debug("Adding component instance of class \"" + clazz + "\" to request context with name \"" + name + "\".");
                request.setAttribute(name, componentInstance);

            } catch (ClassNotFoundException e) {
                log.warn("Could not load request context component class \"" + clazz + "\".", e);
            } catch (IllegalAccessException e) {
                log.warn("Could not instantiate request context component class \"" + clazz + "\".", e);
            } catch (InstantiationException e) {
                log.warn("Could not instantiate request context component class \"" + clazz + "\".", e);
            }
        }

    }

    private void instantiateSessionComponents() {

        int componentsCount = sessionComponents.size();

        Component component;
        String name;
        String clazz;
        Map attributes;

        for (int i=0; i < componentsCount; i++) {

            component = (Component) sessionComponents.get(i);

            name = component.getName();
            clazz = component.getClazz();
            attributes = component.getAttributes();

            try {

                Class componentClass = null;

                log.debug("Loading session context component class \"" + clazz + "\".");
                try {
                    componentClass = Class.forName(clazz);
                } catch (ClassNotFoundException e) {
                    componentClass = Class.forName(clazz, true, Thread.currentThread().getContextClassLoader());
                }

                Object componentInstance = componentClass.newInstance();

                if (componentInstance instanceof SessionContextComponent) {
                    ((SessionContextComponent) componentInstance).setSessionContext(session);
                    ((SessionContextComponent) componentInstance).setAttributes(attributes);
                }
                log.debug("Adding component instance of class \"" + clazz + "\" to session context with name \"" + name + "\".");
                session.setAttribute(name, componentInstance);

            } catch (ClassNotFoundException e) {
                log.warn("Could not load session context component class \"" + clazz + "\".", e);
            } catch (IllegalAccessException e) {
                log.warn("Could not instantiate session context component class \"" + clazz + "\".", e);
            } catch (InstantiationException e) {
                log.warn("Could not instantiate session context component class \"" + clazz + "\".", e);
            }
        }

    }

    public void destroy() {
    }


    private synchronized void loadConfiguration() {

        Component component;

        int componentsCount = 0;

        componentsCount = servletComponents.size();

        for (int i=0; i < componentsCount; i++) {
            component = (Component) servletComponents.get(i);
            servlet.removeAttribute(component.getName());
        }

        componentsCount = sessionComponents.size();

        for (int i=0; i < componentsCount; i++) {
            component = (Component) sessionComponents.get(i);
            session.removeAttribute(component.getName());
        }

        componentsCount = requestComponents.size();

        for (int i=0; i < componentsCount; i++) {
            component = (Component) requestComponents.get(i);
            request.removeAttribute(component.getName());
        }

        servletComponents.clear();
        sessionComponents.clear();
        requestComponents.clear();

        InputStream is = null;

        try {

            XMLReader xml = XMLReaderFactory.createXMLReader();
            xml.setContentHandler(this);

            is = null;

            if (componentsFile == null) {
                is = new BufferedInputStream(servlet.getResourceAsStream(DEFAULT_COMPONENTS_FILE_NAME));
            } else if ((componentsFile.exists()) && (componentsFile.canRead())) {
                is = new BufferedInputStream(componentsFile.toURL().openStream());
            }

            if (componentsFile != null) {
                componentsFileLastModified = componentsFile.lastModified();
            }

            xml.parse(new InputSource(is));

        } catch (SAXException e) {
            log.error("Could not parse file \"" + DEFAULT_COMPONENTS_FILE_NAME + "\".", e);
        } catch (MalformedURLException e) {
            log.error("Could not open file \"" + DEFAULT_COMPONENTS_FILE_NAME + "\".", e);
        } catch (IOException e) {
            log.error("Could not read file \"" + DEFAULT_COMPONENTS_FILE_NAME + "\".", e);
        } finally {
            if (is != null) try { is.close(); } catch (IOException e) { }
        }

    }

    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {

        if (!"component".equals(localName)) {
            return;
        }

        String name = null;
        String clazz = null;
        String context = null;
        String attributeName = null;
        String attributeValue = null;
        Map componentAttributes = Collections.EMPTY_MAP;

        int attributeCount = attributes.getLength();

        for (int i=0; i < attributeCount; i++) {
            attributeName = attributes.getLocalName(i);
            attributeValue = attributes.getValue(i);

            if ("name".equals(attributeName)) {
                name = attributeValue.trim();
            } else if ("class".equals(attributeName)) {
                clazz = attributeValue.trim();
            } else if ("context".equals(attributeName)) {
                context = attributeValue.trim();
            } else {
                if (componentAttributes == Collections.EMPTY_MAP) {
                    componentAttributes = new HashMap();
                }
                componentAttributes.put(attributeName, attributeValue);
            }
        }

        if ((name == null) || (name.length() == 0) || (clazz == null) || (clazz.length() == 0)) {
            return;
        }

        if ((!"servlet".equals(context)) && (!"session".equals(context)) && (!"request".equals(context))) {
            context = "request";
        }

        try {

            Class componentClass = null;

            log.debug("Loading component class : \"" + clazz + "\".");
            try {
                componentClass = Class.forName(clazz);
            } catch (ClassNotFoundException e) {
                componentClass = Class.forName(clazz, true, Thread.currentThread().getContextClassLoader());
            }

            Object componentInstance = componentClass.newInstance();

            if ("servlet".equals(context)) {
                log.debug("Adding component class : \"" + clazz + "\" to servlet components collection.");
                servletComponents.add(new Component(name, clazz, componentAttributes));
                if (componentInstance instanceof ServletContextComponent) {
                    ((ServletContextComponent) componentInstance).setServletContext(servlet);
                    ((ServletContextComponent) componentInstance).setAttributes(componentAttributes);
                }
                log.debug("Adding component instance of class \"" + clazz + "\" to servlet context with name \"" + name + "\".");
                servlet.setAttribute(name, componentInstance);
            } else if ("session".equals(context)) {
                log.debug("Adding component class \"" + clazz + "\" to session components collection.");
                sessionComponents.add(new Component(name, clazz, componentAttributes));
            } else if ("request".equals(context)) {
                log.debug("Adding component class \"" + clazz + "\" to request components collection.");
                requestComponents.add(new Component(name, clazz, componentAttributes));
            }

        } catch (ClassNotFoundException e) {
            log.warn("Could not load component class \"" + clazz + "\".", e);
        } catch (IllegalAccessException e) {
            log.warn("Could not instantiate component class \"" + clazz + "\".", e);
        } catch (InstantiationException e) {
            log.warn("Could not instantiate component class \"" + clazz + "\".", e);
        }

    }

    private class Component {

        private String name;
        private String clazz;
        private Map attributes;

        public Component(String name, String clazz, Map attributes) {
            this.name = name;
            this.clazz = clazz;
            this.attributes = attributes;
        }

        public String getName() {
            return name;
        }

        public String getClazz() {
            return clazz;
        }

        public Map getAttributes() {
            return attributes;
        }

    }

}