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 RequestConfiguration requestConfig;
124
125    protected volatile boolean isDirty;
126
127    public WebEngine(File root) {
128        this(new EmptyRegistry(), root);
129    }
130
131    public WebEngine(ResourceRegistry registry, File root) {
132        this.registry = registry;
133        this.root = root;
134        webLoader = new WebLoader(this);
135        apps = new HashMap<String, WebEngineModule>();
136        scripting = new Scripting(webLoader);
137        annoMgr = new AnnotationManager();
138
139        skinPathPrefix = Framework.getProperty(SKIN_PATH_PREFIX_KEY);
140        if (skinPathPrefix == null) {
141            // TODO: should put this in web.xml and not use jboss.home.dir to
142            // test if on jboss
143            skinPathPrefix = System.getProperty("jboss.home.dir") != null ? "/nuxeo/site/skin" : "/skin";
144        }
145
146        env = new HashMap<String, Object>();
147        env.put("installDir", root);
148        env.put("engine", "Nuxeo Web Engine");
149        // TODO this should be put in the MANIFEST
150        env.put("version", "1.0.0.rc");
151
152        rendering = new FreemarkerEngine();
153        rendering.setResourceLocator(this);
154        rendering.setSharedVariable("env", getEnvironment());
155
156        requestConfig = new RequestConfiguration();
157
158    }
159
160    public RequestConfiguration getRequestConfiguration() {
161        return requestConfig;
162    }
163
164    /**
165     * JSP taglib support
166     */
167    public void loadJspTaglib(GenericServlet servlet) {
168        if (rendering instanceof FreemarkerEngine) {
169            FreemarkerEngine fm = (FreemarkerEngine) rendering;
170            ServletContextHashModel servletContextModel = new ServletContextHashModel(servlet, fm.getObjectWrapper());
171            fm.setSharedVariable("Application", servletContextModel);
172            fm.setSharedVariable("__FreeMarkerServlet.Application__", servletContextModel);
173            fm.setSharedVariable("Application", servletContextModel);
174            fm.setSharedVariable("__FreeMarkerServlet.Application__", servletContextModel);
175            fm.setSharedVariable("JspTaglibs", new TaglibFactory(servlet.getServletContext()));
176        }
177    }
178
179    public void initJspRequestSupport(GenericServlet servlet, HttpServletRequest request, HttpServletResponse response) {
180        if (rendering instanceof FreemarkerEngine) {
181            FreemarkerEngine fm = (FreemarkerEngine) rendering;
182            HttpRequestHashModel requestModel = new HttpRequestHashModel(request, response, fm.getObjectWrapper());
183            fm.setSharedVariable("__FreeMarkerServlet.Request__", requestModel);
184            fm.setSharedVariable("Request", requestModel);
185            fm.setSharedVariable("RequestParameters", new HttpRequestParametersHashModel(request));
186
187            // HttpSessionHashModel sessionModel = null;
188            // HttpSession session = request.getSession(false);
189            // if(session != null) {
190            // sessionModel = (HttpSessionHashModel)
191            // session.getAttribute(ATTR_SESSION_MODEL);
192            // if (sessionModel == null || sessionModel.isZombie()) {
193            // sessionModel = new HttpSessionHashModel(session, wrapper);
194            // session.setAttribute(ATTR_SESSION_MODEL, sessionModel);
195            // if(!sessionModel.isZombie()) {
196            // initializeSession(request, response);
197            // }
198            // }
199            // }
200            // else {
201            // sessionModel = new HttpSessionHashModel(servlet, request,
202            // response, fm.getObjectWrapper());
203            // }
204            // sessionModel = new HttpSessionHashModel(request, response,
205            // fm.getObjectWrapper());
206            // fm.setSharedVariable("Session", sessionModel);
207        }
208    }
209
210    public WebLoader getWebLoader() {
211        return webLoader;
212    }
213
214    public void setSkinPathPrefix(String skinPathPrefix) {
215        this.skinPathPrefix = skinPathPrefix;
216    }
217
218    public String getSkinPathPrefix() {
219        return skinPathPrefix;
220    }
221
222    @Deprecated
223    public ResourceRegistry getRegistry() {
224        return registry;
225    }
226
227    public Class<?> loadClass(String className) throws ClassNotFoundException {
228        return webLoader.loadClass(className);
229    }
230
231    public String getMimeType(String ext) {
232        return (String) mimeTypes.get(ext);
233    }
234
235    public AnnotationManager getAnnotationManager() {
236        return annoMgr;
237    }
238
239    public void registerRenderingExtension(String id, Object obj) {
240        rendering.setSharedVariable(id, obj);
241    }
242
243    public void unregisterRenderingExtension(String id) {
244        rendering.setSharedVariable(id, null);
245    }
246
247    public Map<String, Object> getEnvironment() {
248        return env;
249    }
250
251    public Scripting getScripting() {
252        return scripting;
253    }
254
255    public synchronized WebEngineModule[] getApplications() {
256        return apps.values().toArray(new WebEngineModule[apps.size()]);
257    }
258
259    public synchronized void addApplication(WebEngineModule app) {
260        flushCache();
261        apps.put(app.getId(), app);
262    }
263
264    public ModuleManager getModuleManager() {
265        if (moduleMgr == null) { // avoid synchronizing if not needed
266            synchronized (this) {
267                /**
268                 * the duplicate if is used avoid synchronizing when no needed. note that the this.moduleMgr member must
269                 * be set at the end of the synchronized block after the module manager is completely initialized
270                 */
271                if (moduleMgr == null) {
272                    ModuleManager moduleMgr = new ModuleManager(this);
273                    File deployRoot = getDeploymentDirectory();
274                    if (deployRoot.isDirectory()) {
275                        // load modules present in deploy directory
276                        for (String name : deployRoot.list()) {
277                            String path = name + "/module.xml";
278                            File file = new File(deployRoot, path);
279                            if (file.isFile()) {
280                                webLoader.addClassPathElement(file.getParentFile());
281                                moduleMgr.loadModule(file);
282                            }
283                        }
284                    }
285                    for (WebEngineModule app : getApplications()) {
286                        ModuleConfiguration mc = app.getConfiguration();
287                        moduleMgr.loadModule(mc);
288                    }
289                    // set member at the end to be sure moduleMgr is completely
290                    // initialized
291                    this.moduleMgr = moduleMgr;
292                }
293            }
294        }
295        return moduleMgr;
296    }
297
298    public Module getModule(String name) {
299        ModuleConfiguration md = getModuleManager().getModule(name);
300        if (md != null) {
301            return md.get();
302        }
303        return null;
304    }
305
306    public File getRootDirectory() {
307        return root;
308    }
309
310    public File getDeploymentDirectory() {
311        return new File(root, "deploy");
312    }
313
314    public File getModulesDirectory() {
315        return new File(root, "modules");
316    }
317
318    public RenderingEngine getRendering() {
319        return rendering;
320    }
321
322    /**
323     * Manage jax-rs root resource bindings
324     *
325     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
326     */
327    @Deprecated
328    public void addResourceBinding(ResourceBinding binding) {
329        try {
330            binding.resolve(this);
331            registry.addBinding(binding);
332        } catch (ClassNotFoundException e) {
333            throw WebException.wrap("Failed o register binding: " + binding, e);
334        }
335    }
336
337    /**
338     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
339     */
340    @Deprecated
341    public void removeResourceBinding(ResourceBinding binding) {
342        registry.removeBinding(binding);
343    }
344
345    /**
346     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
347     */
348    @Deprecated
349    public ResourceBinding[] getBindings() {
350        return registry.getBindings();
351    }
352
353    public synchronized void setDirty(boolean dirty) {
354        isDirty = dirty;
355    }
356
357    public boolean tryReload() {
358        if (isDirty) {
359            synchronized (this) {
360                if (isDirty) {
361                    reload();
362                    return true;
363                }
364            }
365        }
366        return false;
367    }
368
369    public synchronized boolean isDirty() {
370        return isDirty;
371    }
372
373    public synchronized void flushCache() {
374        isDirty = false;
375        if (moduleMgr != null) {
376            webLoader.flushCache();
377            moduleMgr = null;
378        }
379    }
380
381    /**
382     * Reloads configuration.
383     */
384    public synchronized void reload() {
385        log.info("Reloading WebEngine");
386        isDirty = false;
387        webLoader.flushCache();
388        apps = new HashMap<String, WebEngineModule>();
389        if (moduleMgr != null) { // avoid synchronizing if not needed
390            for (ModuleConfiguration mc : moduleMgr.getModules()) {
391                if (mc.isLoaded()) {
392                    // remove module level caches
393                    mc.get().flushCache();
394                }
395            }
396            moduleMgr = null;
397        }
398    }
399
400    public synchronized void reloadModules() {
401        if (moduleMgr != null) {
402            moduleMgr.reloadModules();
403        }
404    }
405
406    public void start() {
407    }
408
409    public void stop() {
410        registry.clear();
411    }
412
413    protected ModuleConfiguration getModuleFromPath(String rootPath, String path) {
414        path = path.substring(rootPath.length() + 1);
415        int p = path.indexOf('/');
416        String moduleName = path;
417        if (p > -1) {
418            moduleName = path.substring(0, p);
419        }
420        return moduleMgr.getModule(moduleName);
421    }
422
423    /* ResourceLocator API */
424
425    @Override
426    public URL getResourceURL(String key) {
427        try {
428            return new URL(key);
429        } catch (MalformedURLException e) {
430            return null;
431        }
432    }
433
434    @Override
435    public File getResourceFile(String key) {
436        WebContext ctx = getActiveContext();
437        if (key.startsWith("@")) {
438            Resource rs = ctx.getTargetObject();
439            if (rs != null) {
440                return rs.getView(key.substring(1)).script().getFile();
441            }
442        } else {
443            ScriptFile file = ctx.getFile(key);
444            if (file != null) {
445                return file.getFile();
446            }
447        }
448        return null;
449    }
450
451}