001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS <http://nuxeo.com> and others 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Jean-Marc Orliaguet, Chalmers 011 * 012 * $Id$ 013 */ 014 015package org.nuxeo.theme.html.servlets; 016 017import java.io.ByteArrayOutputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027import java.util.zip.GZIPOutputStream; 028 029import javax.servlet.http.HttpServlet; 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.nuxeo.ecm.core.io.download.BufferingServletOutputStream; 036import org.nuxeo.runtime.api.Framework; 037import org.nuxeo.theme.ApplicationType; 038import org.nuxeo.theme.Manager; 039import org.nuxeo.theme.ResourceResolver; 040import org.nuxeo.theme.html.CSSUtils; 041import org.nuxeo.theme.html.JSUtils; 042import org.nuxeo.theme.html.Utils; 043import org.nuxeo.theme.resources.ResourceType; 044import org.nuxeo.theme.themes.ThemeException; 045import org.nuxeo.theme.themes.ThemeManager; 046import org.nuxeo.theme.types.TypeFamily; 047import org.nuxeo.theme.types.TypeRegistry; 048 049public final class Resources extends HttpServlet implements Serializable { 050 051 private static final long serialVersionUID = 1L; 052 053 private static final Log log = LogFactory.getLog(Resources.class); 054 055 private static final Pattern pathPattern = Pattern.compile("/([^/]+)"); 056 057 @Override 058 protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException { 059 doPost(request, response); 060 } 061 062 @Override 063 protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws IOException { 064 doProcess(request, response); 065 } 066 067 protected void doProcess(final HttpServletRequest request, final HttpServletResponse response) throws IOException { 068 069 final String pathInfo = request.getPathInfo(); 070 if (pathInfo == null) { 071 return; 072 } 073 final Matcher m = pathPattern.matcher(pathInfo); 074 if (!m.matches()) { 075 log.error(String.format("Invalid resource path: %s", pathInfo)); 076 return; 077 } 078 079 final TypeRegistry typeRegistry = Manager.getTypeRegistry(); 080 final ThemeManager themeManager = Manager.getThemeManager(); 081 082 String contentType = null; 083 String resourceSuffix = null; 084 final List<String> resourceNames = Arrays.asList(m.group(1).split(",")); 085 for (String resourceName : resourceNames) { 086 String previousContentType = contentType; 087 if (resourceName.endsWith(".js")) { 088 contentType = "text/javascript"; 089 resourceSuffix = ".js"; 090 } else if (resourceName.endsWith(".css")) { 091 contentType = "text/css"; 092 resourceSuffix = ".css"; 093 } else if (resourceName.endsWith(".json")) { 094 contentType = "text/json"; 095 resourceSuffix = ".json"; 096 } 097 098 if (contentType == null) { 099 log.error("Resource names must end with .js, .css or .json: " + pathInfo); 100 return; 101 } 102 103 if (previousContentType != null && !contentType.equals(previousContentType)) { 104 log.error("Combined resources must be of the same type: " + pathInfo); 105 return; 106 } 107 } 108 response.addHeader("content-type", contentType); 109 110 // cache control 111 final String applicationPath = request.getParameter("path"); 112 if (applicationPath != null) { 113 ApplicationType application = (ApplicationType) Manager.getTypeRegistry().lookup(TypeFamily.APPLICATION, 114 applicationPath); 115 if (application != null) { 116 Utils.setCacheHeaders(response, application.getResourceCaching()); 117 } 118 } 119 120 StringBuilder text = new StringBuilder(); 121 String basePath = request.getParameter("basepath"); 122 123 // plug additional resources for this page 124 List<String> allResourceNames = new ArrayList<String>(); 125 allResourceNames.addAll(themeManager.getOrderedResourcesAndDeps(resourceNames)); 126 for (String resourceName : allResourceNames) { 127 if (!resourceName.endsWith(resourceSuffix)) { 128 continue; 129 } 130 final OutputStream out = new ByteArrayOutputStream(); 131 String source = themeManager.getResource(resourceName); 132 if (source == null) { 133 ResourceType resource = (ResourceType) typeRegistry.lookup(TypeFamily.RESOURCE, resourceName); 134 if (resource == null) { 135 log.error(String.format("Resource not registered %s.", resourceName)); 136 continue; 137 } 138 writeResource(resource, out); 139 source = out.toString(); 140 141 if (resourceName.endsWith(".js")) { 142 // do not shrink the script when Dev mode is enabled 143 if (resource.isShrinkable() && !Framework.isDevModeSet()) { 144 try { 145 source = JSUtils.compressSource(source); 146 } catch (ThemeException e) { 147 log.warn("failed to compress javascript source: " + resourceName); 148 } 149 } 150 } else if (resourceName.endsWith(".css")) { 151 // fix CSS url(...) declarations; 152 String cssContextPath = resource.getContextPath(); 153 if (cssContextPath != null) { 154 source = CSSUtils.expandPartialUrls(source, cssContextPath); 155 } 156 157 // expands system variables 158 source = Framework.expandVars(source); 159 160 // expand ${basePath} 161 source = source.replaceAll("\\$\\{basePath\\}", Matcher.quoteReplacement(basePath)); 162 // also expand ${org.nuxeo.ecm.contextPath} 163 source = source.replaceAll("\\$\\{org.nuxeo.ecm.contextPath\\}", Matcher.quoteReplacement(basePath)); 164 165 // do not shrink the CSS when Dev mode is enabled 166 if (resource.isShrinkable() && !Framework.isDevModeSet()) { 167 try { 168 source = CSSUtils.compressSource(source); 169 } catch (ThemeException e) { 170 log.warn("failed to compress CSS source: " + resourceName); 171 } 172 } 173 } 174 // do not cache the resource when Dev mode is enabled 175 if (!Framework.isDevModeSet()) { 176 themeManager.setResource(resourceName, source); 177 } 178 } 179 text.append(source); 180 if (out != null) { 181 out.close(); 182 } 183 } 184 185 boolean supportsGzip = Utils.supportsGzip(request); 186 OutputStream os = response.getOutputStream(); 187 BufferingServletOutputStream.stopBuffering(os); 188 if (supportsGzip) { 189 response.setHeader("Content-Encoding", "gzip"); 190 // Needed by proxy servers 191 response.setHeader("Vary", "Accept-Encoding"); 192 os = new GZIPOutputStream(os); 193 } 194 195 try { 196 os.write(text.toString().getBytes()); 197 os.close(); 198 } catch (IOException e) { 199 Throwable cause = e.getCause(); 200 if (cause != null && "Broken pipe".equals(cause.getMessage())) { 201 log.debug("Swallowing: " + e); 202 } else { 203 throw e; 204 } 205 } 206 207 log.debug(String.format("Served resource(s): %s %s", pathInfo, supportsGzip ? "with gzip compression" : "")); 208 } 209 210 private static void writeResource(final ResourceType resource, final OutputStream out) { 211 InputStream in = null; 212 try { 213 String path = resource.getPath(); 214 // checks through ServletResourceResolver 215 in = ResourceResolver.getInstance().getResourceAsStream(path); 216 if (in != null) { 217 byte[] buffer = new byte[1024]; 218 int read = in.read(buffer); 219 while (read != -1) { 220 out.write(buffer, 0, read); 221 read = in.read(buffer); 222 out.flush(); 223 } 224 out.close(); 225 } else { 226 log.error(String.format("Resource not found %s.", resource.getName())); 227 } 228 } catch (IOException e) { 229 log.error(e, e); 230 } finally { 231 if (in != null) { 232 try { 233 in.close(); 234 } catch (IOException e) { 235 log.error(e, e); 236 } 237 } 238 } 239 } 240 241}