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