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}