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