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