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}