/*
 * Copyright 2004-2005 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.codehaus.groovy.grails.web.errors;

import grails.util.GrailsUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.grails.exceptions.GrailsRuntimeException;
import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo;
import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder;
import org.codehaus.groovy.grails.web.servlet.mvc.exceptions.GrailsMVCException;
import org.codehaus.groovy.grails.web.util.WebUtils;
import org.codehaus.groovy.runtime.InvokerInvocationException;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *  Exception resolver that wraps any runtime exceptions with a GrailsWrappedException instance
 * 
 * @author Graeme Rocher
 * @since 22 Dec, 2005
 */
public class GrailsExceptionResolver  extends SimpleMappingExceptionResolver implements ServletContextAware {
    private ServletContext servletContext;

    private static final Log LOG = LogFactory.getLog(GrailsExceptionResolver.class);

    /* (non-Javadoc)
    * @see org.springframework.web.servlet.handler.SimpleMappingExceptionResolver#resolveException(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception)
    */
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        if ((ex instanceof InvokerInvocationException)||(ex instanceof GrailsMVCException)) {
           Throwable t = getRootCause(ex);
           if(t instanceof Exception) {
               ex = (Exception) t;
           }
        }
        ModelAndView mv = super.resolveException(request, response, handler, ex);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        // expose the servlet 2.3 specs status code request attribute as 500
        request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

        GrailsUtil.deepSanitize(ex);

        LOG.error(ex.getMessage(), ex);

        GrailsWrappedRuntimeException gwrex = new GrailsWrappedRuntimeException(servletContext,ex);
        mv.addObject("exception",gwrex);

        UrlMappingsHolder urlMappings = null;
        try {
            urlMappings = WebUtils.lookupUrlMappings(servletContext);
        } catch (Exception e) {
            // ignore, no app ctx in this case.
        }
        if(urlMappings != null) {
            UrlMappingInfo info = urlMappings.matchStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
            if(info == null) {
                info = urlMappings.matchStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, getRootCause(ex));
            }
            if(info == null) {
                info = urlMappings.matchStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 
            }
            try {
                if(info != null && info.getViewName() != null) {
                    ViewResolver viewResolver = WebUtils.lookupViewResolver(servletContext);
                    View v = WebUtils.resolveView(request, info, info.getViewName(),viewResolver);
                    if(v != null) {
                        mv.setView(v);
                    }
                }
                else if(info != null && info.getControllerName() != null) {
                    String uri;
                    if(request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) {
                        uri = (String)request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE);
                    }
                    else {
                        uri = request.getRequestURI();
                    }

                    if(!response.isCommitted()) {
                        info.configure(WebUtils.retrieveGrailsWebRequest());
                        String forwardUrl = WebUtils.forwardRequestForUrlMappingInfo(request, response, info, mv.getModel());
                        if(LOG.isDebugEnabled()) {
                            LOG.debug("Matched URI ["+uri+"] to URL mapping ["+info+"], forwarding to ["+forwardUrl+"] with response ["+response.getClass()+"]");
                        }
                        return null;
                    }
                }
            } catch (Exception e) {
                LOG.error("Unable to render errors view: " + e.getMessage(), e);
                throw new GrailsRuntimeException(e);
            }
        }

        return mv;
    }

    /**
     * Obtains the root cause of the given exception
     * @param ex The exception
     * @return The root cause
     */
    public static Throwable getRootCause(Throwable ex) {
        while(ex.getCause() != null && !ex.equals(ex.getCause())) {
            ex = ex.getCause();
        }
        return ex;
    }


    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public static int extractLineNumber(CompilationFailedException e) {
        int lineNumber = -1;
        if(e instanceof MultipleCompilationErrorsException) {
            MultipleCompilationErrorsException mcee = (MultipleCompilationErrorsException)e;
            Object message = mcee.getErrorCollector().getErrors().iterator().next();
            if(message instanceof SyntaxErrorMessage) {
                SyntaxErrorMessage sem = (SyntaxErrorMessage)message;
                lineNumber = sem.getCause().getLine();
            }
        }
        return lineNumber;
    }

    public static RuntimeException getFirstRuntimeException(Throwable e) {
        if(e instanceof RuntimeException) return (RuntimeException) e;
        Throwable ex = e;
        while(ex.getCause() != null && !ex.equals(ex.getCause())) {
            ex = ex.getCause();
            if(ex instanceof RuntimeException) return (RuntimeException) ex;
        }
        return null;
    }
}
