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