001/*
002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.webengine;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.util.HashMap;
028import java.util.Map;
029import java.util.Properties;
030
031import javax.servlet.GenericServlet;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.ecm.platform.rendering.api.RenderingEngine;
039import org.nuxeo.ecm.platform.rendering.api.ResourceLocator;
040import org.nuxeo.ecm.platform.rendering.fm.FreemarkerEngine;
041import org.nuxeo.ecm.webengine.app.WebEngineModule;
042import org.nuxeo.ecm.webengine.jaxrs.context.RequestContext;
043import org.nuxeo.ecm.webengine.loader.WebLoader;
044import org.nuxeo.ecm.webengine.model.Module;
045import org.nuxeo.ecm.webengine.model.Resource;
046import org.nuxeo.ecm.webengine.model.WebContext;
047import org.nuxeo.ecm.webengine.model.impl.ModuleConfiguration;
048import org.nuxeo.ecm.webengine.model.impl.ModuleManager;
049import org.nuxeo.ecm.webengine.scripting.ScriptFile;
050import org.nuxeo.ecm.webengine.scripting.Scripting;
051import org.nuxeo.runtime.annotations.AnnotationManager;
052import org.nuxeo.runtime.api.Framework;
053
054import freemarker.ext.jsp.TaglibFactory;
055import freemarker.ext.servlet.HttpRequestHashModel;
056import freemarker.ext.servlet.HttpRequestParametersHashModel;
057import freemarker.ext.servlet.ServletContextHashModel;
058
059/**
060 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
061 */
062public class WebEngine implements ResourceLocator {
063
064    public static final String SKIN_PATH_PREFIX_KEY = "org.nuxeo.ecm.webengine.skinPathPrefix";
065
066    protected static final Map<Object, Object> mimeTypes = loadMimeTypes();
067
068    private static final Log log = LogFactory.getLog(WebEngine.class);
069
070    static Map<Object, Object> loadMimeTypes() {
071        Map<Object, Object> mimeTypes = new HashMap<Object, Object>();
072        Properties p = new Properties();
073        URL url = WebEngine.class.getClassLoader().getResource("OSGI-INF/mime.properties");
074        InputStream in = null;
075        try {
076            in = url.openStream();
077            p.load(in);
078            mimeTypes.putAll(p);
079        } catch (IOException e) {
080            throw new RuntimeException("Failed to load mime types", e);
081        } finally {
082            IOUtils.closeQuietly(in);
083        }
084        return mimeTypes;
085    }
086
087    public static WebContext getActiveContext() {
088        RequestContext ctx = RequestContext.getActiveContext();
089        if (ctx != null) {
090            return (WebContext) ctx.getRequest().getAttribute(WebContext.class.getName());
091        }
092        return null;
093    }
094
095    protected final File root;
096
097    protected HashMap<String, WebEngineModule> apps;
098
099    /**
100     * moduleMgr use double-check idiom and needs to be volatile. See
101     * http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
102     */
103    protected volatile ModuleManager moduleMgr;
104
105    protected final Scripting scripting;
106
107    protected final RenderingEngine rendering;
108
109    protected final Map<String, Object> env;
110
111    protected boolean devMode;
112
113    protected final AnnotationManager annoMgr;
114
115    protected final ResourceRegistry registry;
116
117    protected String skinPathPrefix;
118
119    protected final WebLoader webLoader;
120
121    protected RequestConfiguration requestConfig;
122
123    protected volatile boolean isDirty;
124
125    public WebEngine(File root) {
126        this(new EmptyRegistry(), root);
127    }
128
129    public WebEngine(ResourceRegistry registry, File root) {
130        this.registry = registry;
131        this.root = root;
132        webLoader = new WebLoader(this);
133        apps = new HashMap<String, WebEngineModule>();
134        scripting = new Scripting(webLoader);
135        annoMgr = new AnnotationManager();
136
137        skinPathPrefix = Framework.getProperty(SKIN_PATH_PREFIX_KEY);
138        if (skinPathPrefix == null) {
139            // TODO: should put this in web.xml and not use jboss.home.dir to
140            // test if on jboss
141            skinPathPrefix = System.getProperty("jboss.home.dir") != null ? "/nuxeo/site/skin" : "/skin";
142        }
143
144        env = new HashMap<String, Object>();
145        env.put("installDir", root);
146        env.put("engine", "Nuxeo Web Engine");
147        // TODO this should be put in the MANIFEST
148        env.put("version", "1.0.0.rc");
149
150        rendering = new FreemarkerEngine();
151        rendering.setResourceLocator(this);
152        rendering.setSharedVariable("env", getEnvironment());
153
154        requestConfig = new RequestConfiguration();
155
156    }
157
158    public RequestConfiguration getRequestConfiguration() {
159        return requestConfig;
160    }
161
162    /**
163     * JSP taglib support
164     */
165    public void loadJspTaglib(GenericServlet servlet) {
166        if (rendering instanceof FreemarkerEngine) {
167            FreemarkerEngine fm = (FreemarkerEngine) rendering;
168            ServletContextHashModel servletContextModel = new ServletContextHashModel(servlet, fm.getObjectWrapper());
169            fm.setSharedVariable("Application", servletContextModel);
170            fm.setSharedVariable("__FreeMarkerServlet.Application__", servletContextModel);
171            fm.setSharedVariable("Application", servletContextModel);
172            fm.setSharedVariable("__FreeMarkerServlet.Application__", servletContextModel);
173            fm.setSharedVariable("JspTaglibs", new TaglibFactory(servlet.getServletContext()));
174        }
175    }
176
177    public void initJspRequestSupport(GenericServlet servlet, HttpServletRequest request, HttpServletResponse response) {
178        if (rendering instanceof FreemarkerEngine) {
179            FreemarkerEngine fm = (FreemarkerEngine) rendering;
180            HttpRequestHashModel requestModel = new HttpRequestHashModel(request, response, fm.getObjectWrapper());
181            fm.setSharedVariable("__FreeMarkerServlet.Request__", requestModel);
182            fm.setSharedVariable("Request", requestModel);
183            fm.setSharedVariable("RequestParameters", new HttpRequestParametersHashModel(request));
184
185            // HttpSessionHashModel sessionModel = null;
186            // HttpSession session = request.getSession(false);
187            // if(session != null) {
188            // sessionModel = (HttpSessionHashModel)
189            // session.getAttribute(ATTR_SESSION_MODEL);
190            // if (sessionModel == null || sessionModel.isZombie()) {
191            // sessionModel = new HttpSessionHashModel(session, wrapper);
192            // session.setAttribute(ATTR_SESSION_MODEL, sessionModel);
193            // if(!sessionModel.isZombie()) {
194            // initializeSession(request, response);
195            // }
196            // }
197            // }
198            // else {
199            // sessionModel = new HttpSessionHashModel(servlet, request,
200            // response, fm.getObjectWrapper());
201            // }
202            // sessionModel = new HttpSessionHashModel(request, response,
203            // fm.getObjectWrapper());
204            // fm.setSharedVariable("Session", sessionModel);
205        }
206    }
207
208    public WebLoader getWebLoader() {
209        return webLoader;
210    }
211
212    public void setSkinPathPrefix(String skinPathPrefix) {
213        this.skinPathPrefix = skinPathPrefix;
214    }
215
216    public String getSkinPathPrefix() {
217        return skinPathPrefix;
218    }
219
220    @Deprecated
221    public ResourceRegistry getRegistry() {
222        return registry;
223    }
224
225    public Class<?> loadClass(String className) throws ClassNotFoundException {
226        return webLoader.loadClass(className);
227    }
228
229    public String getMimeType(String ext) {
230        return (String) mimeTypes.get(ext);
231    }
232
233    public AnnotationManager getAnnotationManager() {
234        return annoMgr;
235    }
236
237    public void registerRenderingExtension(String id, Object obj) {
238        rendering.setSharedVariable(id, obj);
239    }
240
241    public void unregisterRenderingExtension(String id) {
242        rendering.setSharedVariable(id, null);
243    }
244
245    public Map<String, Object> getEnvironment() {
246        return env;
247    }
248
249    public Scripting getScripting() {
250        return scripting;
251    }
252
253    public synchronized WebEngineModule[] getApplications() {
254        return apps.values().toArray(new WebEngineModule[apps.size()]);
255    }
256
257    public synchronized void addApplication(WebEngineModule app) {
258        flushCache();
259        apps.put(app.getId(), app);
260    }
261
262    public ModuleManager getModuleManager() {
263        if (moduleMgr == null) { // avoid synchronizing if not needed
264            synchronized (this) {
265                /**
266                 * the duplicate if is used avoid synchronizing when no needed. note that the this.moduleMgr member must
267                 * be set at the end of the synchronized block after the module manager is completely initialized
268                 */
269                if (moduleMgr == null) {
270                    ModuleManager moduleMgr = new ModuleManager(this);
271                    File deployRoot = getDeploymentDirectory();
272                    if (deployRoot.isDirectory()) {
273                        // load modules present in deploy directory
274                        for (String name : deployRoot.list()) {
275                            String path = name + "/module.xml";
276                            File file = new File(deployRoot, path);
277                            if (file.isFile()) {
278                                webLoader.addClassPathElement(file.getParentFile());
279                                moduleMgr.loadModule(file);
280                            }
281                        }
282                    }
283                    for (WebEngineModule app : getApplications()) {
284                        ModuleConfiguration mc = app.getConfiguration();
285                        moduleMgr.loadModule(mc);
286                    }
287                    // set member at the end to be sure moduleMgr is completely
288                    // initialized
289                    this.moduleMgr = moduleMgr;
290                }
291            }
292        }
293        return moduleMgr;
294    }
295
296    public Module getModule(String name) {
297        ModuleConfiguration md = getModuleManager().getModule(name);
298        if (md != null) {
299            return md.get();
300        }
301        return null;
302    }
303
304    public File getRootDirectory() {
305        return root;
306    }
307
308    public File getDeploymentDirectory() {
309        return new File(root, "deploy");
310    }
311
312    public File getModulesDirectory() {
313        return new File(root, "modules");
314    }
315
316    public RenderingEngine getRendering() {
317        return rendering;
318    }
319
320    /**
321     * Manage jax-rs root resource bindings
322     *
323     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
324     */
325    @Deprecated
326    public void addResourceBinding(ResourceBinding binding) {
327        try {
328            binding.resolve(this);
329            registry.addBinding(binding);
330        } catch (ClassNotFoundException e) {
331            throw WebException.wrap("Failed o register binding: " + binding, e);
332        }
333    }
334
335    /**
336     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
337     */
338    @Deprecated
339    public void removeResourceBinding(ResourceBinding binding) {
340        registry.removeBinding(binding);
341    }
342
343    /**
344     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
345     */
346    @Deprecated
347    public ResourceBinding[] getBindings() {
348        return registry.getBindings();
349    }
350
351    public synchronized void setDirty(boolean dirty) {
352        isDirty = dirty;
353    }
354
355    public boolean tryReload() {
356        if (isDirty) {
357            synchronized (this) {
358                if (isDirty) {
359                    reload();
360                    return true;
361                }
362            }
363        }
364        return false;
365    }
366
367    public synchronized boolean isDirty() {
368        return isDirty;
369    }
370
371    public synchronized void flushCache() {
372        isDirty = false;
373        if (moduleMgr != null) {
374            webLoader.flushCache();
375            moduleMgr = null;
376        }
377    }
378
379    /**
380     * Reloads configuration.
381     */
382    public synchronized void reload() {
383        log.info("Reloading WebEngine");
384        isDirty = false;
385        webLoader.flushCache();
386        apps = new HashMap<String, WebEngineModule>();
387        if (moduleMgr != null) { // avoid synchronizing if not needed
388            for (ModuleConfiguration mc : moduleMgr.getModules()) {
389                if (mc.isLoaded()) {
390                    // remove module level caches
391                    mc.get().flushCache();
392                }
393            }
394            moduleMgr = null;
395        }
396    }
397
398    public synchronized void reloadModules() {
399        if (moduleMgr != null) {
400            moduleMgr.reloadModules();
401        }
402    }
403
404    public void start() {
405    }
406
407    public void stop() {
408        registry.clear();
409    }
410
411    protected ModuleConfiguration getModuleFromPath(String rootPath, String path) {
412        path = path.substring(rootPath.length() + 1);
413        int p = path.indexOf('/');
414        String moduleName = path;
415        if (p > -1) {
416            moduleName = path.substring(0, p);
417        }
418        return moduleMgr.getModule(moduleName);
419    }
420
421    /* ResourceLocator API */
422
423    @Override
424    public URL getResourceURL(String key) {
425        try {
426            return new URL(key);
427        } catch (MalformedURLException e) {
428            return null;
429        }
430    }
431
432    @Override
433    public File getResourceFile(String key) {
434        WebContext ctx = getActiveContext();
435        if (key.startsWith("@")) {
436            Resource rs = ctx.getTargetObject();
437            if (rs != null) {
438                return rs.getView(key.substring(1)).script().getFile();
439            }
440        } else {
441            ScriptFile file = ctx.getFile(key);
442            if (file != null) {
443                return file.getFile();
444            }
445        }
446        return null;
447    }
448
449}