/* Copyright (2006-2012) Schibsted ASA * This file is part of Possom. * * Possom is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Possom is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Possom. If not, see <http://www.gnu.org/licenses/>. */ package no.sesat.search.http.filters; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.MalformedURLException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; import no.sesat.search.site.config.ResourceLoadException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import no.sesat.search.site.Site; import no.sesat.search.site.config.BytecodeLoader; import no.sesat.search.site.config.UrlResourceLoader; import org.apache.commons.lang.time.StopWatch; import org.apache.log4j.Logger; /** * Downloads JSP files from skins into possom to be compiled and used locally. * This makes it look like jsps from the other skin web applications actually are bundled into sesat. <br/><br/> * * Implementation issue: <a href="http://sesat.no/scarab/issues/id/SKER4290">Design and code with JSPs in skins</a> * * <br/><br/> * * <b>Inclusion of jsps</b> may occurr with <jsp:include page="..."/> * or some other requestDispatcher.include(..) approach. * <%@ include file=".."/%> will not work. * * * <br/><br/> * * <b>To enable JSP files</b> in a particular skin to be downloaded into possom the following configuration is required: * <ul> * <li>in the skin's web.xml add "jsp" to the resources.restricted init-param for ResourceServlet,</li> * <li>in the skin's web.xml add "jsp=jsp" to the content.paths init-param for ResourceServlet,</li> * <li>in the skin's web.xml add the servlet-mapping: * <pre> <servlet-mapping> <servlet-name>resource servlet</servlet-name> <url-pattern>*.jsp</url-pattern> </servlet-mapping> * </pre> so to avoid the skin's JspServlet and to serve the jsp files are resources back to sesat,</li> * </ul> * This have already been done in the base skin sesat-kernel/generic.sesam and can be used as an example.<br/><br/> * * <b>Tomcat, or the container used, must not use unpackWARs="false", or any non-file based deployment implementation, * as this class must be able to write files into the deployed webapps directory.</b> <br/> * Such files are written using a FileChannel obtained like * <pre>new RandomFileAccess(new File(root + "requested-jsp-name"),"rw").getChannel()</pre> * * * @version $Id$ */ public final class SiteJspLoaderFilter implements Filter { private static final Logger LOG = Logger.getLogger(SiteJspLoaderFilter.class); private FilterConfig config; private String root; public void init(final FilterConfig filterConfig) throws ServletException { config = filterConfig; root = config.getServletContext().getRealPath("/"); } public void doFilter( final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { if( request instanceof HttpServletRequest){ final String jsp = getRequestedJsp((HttpServletRequest)request); LOG.debug("jsp: " + jsp + "; resource: " + config.getServletContext().getResource(jsp)); downloadJsp((HttpServletRequest)request, jsp); } chain.doFilter(request, response); } public void destroy() { } // copied from JspServlet.serve(..) private String getRequestedJsp( final HttpServletRequest request){ String jspUri = null; String jspFile = (String) request.getAttribute(JSP_FILE); if (jspFile != null) { // JSP is specified via <jsp-file> in <servlet> declaration jspUri = jspFile; } else { /* * Check to see if the requested JSP has been the target of a * RequestDispatcher.include() */ jspUri = (String) request.getAttribute(INC_SERVLET_PATH); if (jspUri != null) { /* * Requested JSP has been target of * RequestDispatcher.include(). Its path is assembled from the * relevant javax.servlet.include.* request attributes */ String pathInfo = (String) request.getAttribute("javax.servlet.include.path_info"); if (pathInfo != null) { jspUri += pathInfo; } } else { /* * Requested JSP has not been the target of a * RequestDispatcher.include(). Reconstruct its path from the * request's getServletPath() and getPathInfo() */ jspUri = request.getServletPath(); String pathInfo = request.getPathInfo(); if (pathInfo != null) { jspUri += pathInfo; } } } return jspUri; } private void downloadJsp( final HttpServletRequest request, final String jsp) throws MalformedURLException{ final StopWatch stopWatch = new StopWatch(); stopWatch.start(); byte[] golden = new byte[0]; // search skins for the jsp and write it out to "golden" for(Site site = (Site) request.getAttribute(Site.NAME_KEY); 0 == golden.length; site = site.getParent()){ if(null == site){ if(null == config.getServletContext().getResource(jsp)){ throw new ResourceLoadException("Unable to find " + jsp + " in any skin"); } break; } final Site finalSite = site; final BytecodeLoader bcLoader = UrlResourceLoader.newBytecodeLoader( finalSite.getSiteContext(), jsp, null ); bcLoader.abut(); golden = bcLoader.getBytecode(); } // if golden now contains data save it to a local (ie local web application) file if(0 < golden.length){ try { final File file = new File(root + jsp); // create the directory structure file.getParentFile().mkdirs(); // check existing file boolean needsUpdating = true; final boolean fileExisted = file.exists(); if(!fileExisted){ file.createNewFile(); } // channel.lock() only synchronises file access between programs, but not between threads inside // the current JVM. The latter results in the OverlappingFileLockException. // At least this is my current understanding of java.nio.channels // It may be that no synchronisation or locking is required at all. A beer to whom answers :-) // So we must provide synchronisation between our own threads, // synchronisation against the file's path (using the JVM's String.intern() functionality) // should work. (I can't imagine this string be used for any other synchronisation purposes). synchronized(file.toString().intern()){ RandomAccessFile fileAccess = null; FileChannel channel = null; try{ fileAccess = new RandomAccessFile(file, "rws"); channel = fileAccess.getChannel(); channel.lock(); if(fileExisted){ final byte[] bytes = new byte[(int)channel.size()]; final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); int reads; do{ reads = channel.read(byteBuffer); }while(0 < reads); needsUpdating = !Arrays.equals(golden, bytes); } if(needsUpdating){ // download file from skin channel.write(ByteBuffer.wrap(golden), 0); file.deleteOnExit(); } }finally{ if(null != channel){ channel.close(); } if(null != fileAccess){ fileAccess.close(); } LOG.debug("resource created as " + config.getServletContext().getResource(jsp)); } } }catch (IOException ex) { LOG.error(ex.getMessage(), ex); } } stopWatch.stop(); LOG.trace("SiteJspLoaderFilter.downloadJsp(..) took " + stopWatch); } //// Imported from org.catalina.jasper.Constants /** * Request attribute for <code><jsp-file></code> element of a * servlet definition. If present on a request, this overrides the * value returned by <code>request.getServletPath()</code> to select * the JSP page to be executed. */ public static final String JSP_FILE = System.getProperty("org.apache.jasper.Constants.JSP_FILE", "org.apache.catalina.jsp_file"); /** * Servlet context and request attributes that the JSP engine * uses. */ public static final String INC_SERVLET_PATH = "javax.servlet.include.servlet_path"; }