/* * 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.grails.web.errors; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.lang.reflect.Constructor; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.control.MultipleCompilationErrorsException; import org.codehaus.groovy.control.messages.SyntaxErrorMessage; import org.grails.core.artefact.ControllerArtefactHandler; import grails.core.GrailsApplication; import grails.util.GrailsStringUtils; import org.grails.core.artefact.ServiceArtefactHandler; import org.grails.core.artefact.TagLibArtefactHandler; import org.grails.core.io.support.GrailsFactoriesLoader; import org.grails.core.exceptions.GrailsException; import org.grails.exceptions.reporting.SourceCodeAware; import org.grails.gsp.ResourceAwareTemplateEngine; import org.grails.buffer.FastStringPrintWriter; import org.grails.web.util.GrailsApplicationAttributes; import org.grails.web.servlet.mvc.GrailsWebRequest; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.context.support.WebApplicationContextUtils; /** * Wraps a Grails RuntimeException and attempts to extract more relevent diagnostic messages * from the stack trace. * * @author Graeme Rocher * @since 0.1 */ public class GrailsWrappedRuntimeException extends GrailsException { private static final Class<? extends GrailsApplicationAttributes> grailsApplicationAttributesClass = GrailsFactoriesLoader.loadFactoryClasses(GrailsApplicationAttributes.class, GrailsWebRequest.class.getClassLoader()).get(0); private static final Constructor<? extends GrailsApplicationAttributes> grailsApplicationAttributesConstructor = ClassUtils.getConstructorIfAvailable(grailsApplicationAttributesClass, ServletContext.class); private static final long serialVersionUID = 7284065617154554366L; private static final Pattern PARSE_DETAILS_STEP1 = Pattern.compile("\\((\\w+)\\.groovy:(\\d+)\\)"); private static final Pattern PARSE_DETAILS_STEP2 = Pattern.compile("at\\s{1}(\\w+)\\$_closure\\d+\\.doCall\\(\\1:(\\d+)\\)"); private static final Pattern PARSE_GSP_DETAILS_STEP1 = Pattern.compile("(\\S+?)_\\S+?_gsp.run\\((\\S+?\\.gsp):(\\d+)\\)"); public static final String URL_PREFIX = "/WEB-INF/grails-app/"; private static final Log LOG = LogFactory.getLog(GrailsWrappedRuntimeException.class); private String className = UNKNOWN; private int lineNumber = - 1; private String stackTrace; private String[] codeSnippet = new String[0]; private String gspFile; private Throwable cause; private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); private String[] stackTraceLines; private static final String UNKNOWN = "Unknown"; private String fileName; /** * @param servletContext The ServletContext instance * @param t The exception that was thrown */ public GrailsWrappedRuntimeException(ServletContext servletContext, Throwable t) { super(t.getMessage(), t); this.cause = t; Throwable cause = t; FastStringPrintWriter pw = FastStringPrintWriter.newInstance(); cause.printStackTrace(pw); stackTrace = pw.toString(); while (cause.getCause() != cause) { if (cause.getCause() == null) { break; } cause = cause.getCause(); } stackTraceLines = stackTrace.split("\\n"); if (cause instanceof MultipleCompilationErrorsException) { MultipleCompilationErrorsException mcee = (MultipleCompilationErrorsException)cause; Object message = mcee.getErrorCollector().getErrors().iterator().next(); if (message instanceof SyntaxErrorMessage) { SyntaxErrorMessage sem = (SyntaxErrorMessage)message; lineNumber = sem.getCause().getLine(); className = sem.getCause().getSourceLocator(); sem.write(pw); } } else { Matcher m1 = PARSE_DETAILS_STEP1.matcher(stackTrace); Matcher m2 = PARSE_DETAILS_STEP2.matcher(stackTrace); Matcher gsp = PARSE_GSP_DETAILS_STEP1.matcher(stackTrace); try { if (gsp.find()) { className = gsp.group(2); lineNumber = Integer.parseInt(gsp.group(3)); gspFile = URL_PREFIX + "views/" + gsp.group(1) + '/' + className; } else { if (m1.find()) { do { className = m1.group(1); lineNumber = Integer.parseInt(m1.group(2)); } while (m1.find()); } else { while (m2.find()) { className = m2.group(1); lineNumber = Integer.parseInt(m2.group(2)); } } } } catch (NumberFormatException nfex) { // ignore } } LineNumberReader reader = null; try { checkIfSourceCodeAware(t); checkIfSourceCodeAware(cause); if (getLineNumber() > -1) { String fileLocation; String url = null; if (fileName != null) { fileLocation = fileName; } else { String urlPrefix = ""; if (gspFile == null) { fileName = className.replace('.', '/') + ".groovy"; GrailsApplication application = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext).getBean(GrailsApplication.APPLICATION_ID, GrailsApplication.class); // @todo Refactor this to get the urlPrefix from the ArtefactHandler if (application.isArtefactOfType(ControllerArtefactHandler.TYPE, className)) { urlPrefix += "/controllers/"; } else if (application.isArtefactOfType(TagLibArtefactHandler.TYPE, className)) { urlPrefix += "/taglib/"; } else if (application.isArtefactOfType(ServiceArtefactHandler.TYPE, className)) { urlPrefix += "/services/"; } url = URL_PREFIX + urlPrefix + fileName; } else { url = gspFile; GrailsApplicationAttributes attrs = null; try { attrs = grailsApplicationAttributesConstructor.newInstance(servletContext); } catch (Exception e) { ReflectionUtils.rethrowRuntimeException(e); } ResourceAwareTemplateEngine engine = attrs.getPagesTemplateEngine(); lineNumber = engine.mapStackLineNumber(url, lineNumber); } fileLocation = "grails-app" + urlPrefix + fileName; } InputStream in = null; if (!GrailsStringUtils.isBlank(url)) { in = servletContext.getResourceAsStream(url); LOG.debug("Attempting to display code snippet found in url " + url); } if (in == null) { Resource r = null; try { r = resolver.getResource(fileLocation); in = r.getInputStream(); } catch (Throwable e) { r = resolver.getResource("file:" + fileLocation); if (r.exists()) { try { in = r.getInputStream(); } catch (IOException e1) { // ignore } } } } if (in != null) { reader = new LineNumberReader(new InputStreamReader(in, "UTF-8")); String currentLine = reader.readLine(); StringBuilder buf = new StringBuilder(); while (currentLine != null) { int currentLineNumber = reader.getLineNumber(); if ((lineNumber > 0 && currentLineNumber == lineNumber - 1) || (currentLineNumber == lineNumber)) { buf.append(currentLineNumber) .append(": ") .append(currentLine) .append("\n"); } else if (currentLineNumber == lineNumber + 1) { buf.append(currentLineNumber) .append(": ") .append(currentLine); break; } currentLine = reader.readLine(); } codeSnippet = buf.toString().split("\n"); } } } catch (IOException e) { LOG.warn("[GrailsWrappedRuntimeException] I/O error reading line diagnostics: " + e.getMessage(), e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { // ignore } } } } private void checkIfSourceCodeAware(Throwable t) { if (!(t instanceof SourceCodeAware)) { return; } final SourceCodeAware codeAware = (SourceCodeAware) t; if (codeAware.getFileName() != null) { fileName = codeAware.getFileName(); if (className == null || UNKNOWN.equals(className)) { className = codeAware.getFileName(); } } if (codeAware.getLineNumber() > -1) { lineNumber = codeAware.getLineNumber(); } } /** * @return Returns the line. */ public String[] getCodeSnippet() { return codeSnippet; } /** * @return Returns the className. */ public String getClassName() { return className; } /** * @return Returns the lineNumber. */ public int getLineNumber() { return lineNumber; } /** * @return Returns the stackTrace. */ public String getStackTraceText() { return stackTrace; } /** * @return Returns the stackTrace lines */ public String[] getStackTraceLines() { return stackTraceLines; } /* (non-Javadoc) * @see groovy.lang.GroovyRuntimeException#getMessage() */ @Override public String getMessage() { return cause.getMessage(); } }