001/* 002 * (C) Copyright 2006-2019 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 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.webengine.model.impl; 023 024import java.io.File; 025import java.io.FileInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Properties; 032import java.util.Set; 033import java.util.concurrent.ConcurrentHashMap; 034import java.util.concurrent.ConcurrentMap; 035 036import javax.ws.rs.core.MediaType; 037 038import org.apache.commons.logging.Log; 039import org.apache.commons.logging.LogFactory; 040import org.nuxeo.common.server.WebApplication; 041import org.nuxeo.common.utils.Path; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.webengine.ResourceBinding; 044import org.nuxeo.ecm.webengine.WebEngine; 045import org.nuxeo.ecm.webengine.model.AdapterType; 046import org.nuxeo.ecm.webengine.model.LinkDescriptor; 047import org.nuxeo.ecm.webengine.model.Messages; 048import org.nuxeo.ecm.webengine.model.Module; 049import org.nuxeo.ecm.webengine.model.Resource; 050import org.nuxeo.ecm.webengine.model.ResourceType; 051import org.nuxeo.ecm.webengine.model.WebContext; 052import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException; 053import org.nuxeo.ecm.webengine.scripting.ScriptFile; 054 055import com.sun.jersey.server.impl.inject.ServerInjectableProviderContext; 056 057/** 058 * The default implementation for a web configuration. 059 * 060 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 061 */ 062public class ModuleImpl implements Module { 063 064 private static final Log log = LogFactory.getLog(ModuleImpl.class); 065 066 protected final WebEngine engine; 067 068 protected final Object typeLock = new Object(); 069 070 // volatile for double-checked locking 071 protected volatile TypeRegistry typeReg; 072 073 protected final ModuleConfiguration configuration; 074 075 protected final ServerInjectableProviderContext sic; 076 077 protected final ModuleImpl superModule; 078 079 protected LinkRegistry linkReg; 080 081 protected final String skinPathPrefix; 082 083 /** 084 * @deprecated Use {@link WebApplication} to declare modules - modules may have multiple roots 085 */ 086 @Deprecated 087 protected ResourceType rootType; 088 089 protected Messages messages; 090 091 protected DirectoryStack dirStack; 092 093 // cache used for resolved files 094 protected ConcurrentMap<String, ScriptFile> fileCache; 095 096 public ModuleImpl(WebEngine engine, ModuleImpl superModule, ModuleConfiguration config, 097 ServerInjectableProviderContext sic) { 098 this.engine = engine; 099 this.superModule = superModule; 100 this.sic = sic; 101 configuration = config; 102 skinPathPrefix = new StringBuilder().append(engine.getSkinPathPrefix()) 103 .append('/') 104 .append(config.name) 105 .toString(); 106 fileCache = new ConcurrentHashMap<>(); 107 loadConfiguration(); 108 reloadMessages(); 109 loadDirectoryStack(); 110 } 111 112 /** 113 * Whether or not this module has a GUI and should be listed in available GUI module list. For example, REST modules 114 * usually don't have a GUI. 115 * 116 * @return true if headless (no GUI is provided), false otherwise 117 */ 118 public boolean isHeadless() { 119 return configuration.isHeadless; 120 } 121 122 /** 123 * @return the natures, or null if no natures were specified 124 */ 125 public Set<String> getNatures() { 126 return configuration.natures; 127 } 128 129 public boolean hasNature(String natureId) { 130 return configuration.natures != null && configuration.natures.contains(natureId); 131 } 132 133 @Override 134 public WebEngine getEngine() { 135 return engine; 136 } 137 138 @Override 139 public String getName() { 140 return configuration.name; 141 } 142 143 @Override 144 public ModuleImpl getSuperModule() { 145 return superModule; 146 } 147 148 public ModuleConfiguration getModuleConfiguration() { 149 return configuration; 150 } 151 152 /** 153 * @deprecated Use {@link WebApplication} to declare modules 154 */ 155 @Deprecated 156 public ResourceType getRootType() { 157 // force type registry creation if needed 158 getTypeRegistry(); 159 if (rootType == null) { 160 throw new IllegalStateException("You use new web module declaration - should not call this compat. method"); 161 } 162 return rootType; 163 } 164 165 /** 166 * @deprecated Use {@link WebApplication} to declare modules 167 */ 168 @Override 169 @Deprecated 170 public Resource getRootObject(WebContext ctx) { 171 ((AbstractWebContext) ctx).setModule(this); 172 Resource obj = ctx.newObject(getRootType()); 173 obj.setRoot(true); 174 return obj; 175 } 176 177 @Override 178 public String getSkinPathPrefix() { 179 return skinPathPrefix; 180 } 181 182 public TypeRegistry getTypeRegistry() { 183 if (typeReg == null) { // create type registry if not already created 184 synchronized (typeLock) { 185 if (typeReg == null) { 186 TypeRegistry registry = createTypeRegistry(); 187 if (configuration.rootType != null) { 188 // compatibility code for avoiding NPE 189 rootType = registry.getType(configuration.rootType); 190 } 191 typeReg = registry; 192 } 193 } 194 } 195 return typeReg; 196 } 197 198 @Override 199 public Class<?> loadClass(String className) throws ClassNotFoundException { 200 return engine.loadClass(className); 201 } 202 203 @Override 204 public ResourceType getType(String typeName) { 205 ResourceType type = getTypeRegistry().getType(typeName); 206 if (type == null) { 207 throw new WebResourceNotFoundException("Type not found: " + typeName); 208 } 209 return type; 210 } 211 212 @Override 213 public ResourceType[] getTypes() { 214 return getTypeRegistry().getTypes(); 215 } 216 217 @Override 218 public AdapterType[] getAdapters() { 219 return getTypeRegistry().getAdapters(); 220 } 221 222 @Override 223 public AdapterType getAdapter(Resource ctx, String name) { 224 AdapterType type = getTypeRegistry().getAdapter(ctx, name); 225 if (type == null) { 226 throw new WebResourceNotFoundException("Service " + name + " not found for object: " + ctx.getPath() 227 + " of type " + ctx.getType().getName()); 228 } 229 return type; 230 } 231 232 @Override 233 public List<String> getAdapterNames(Resource ctx) { 234 return getTypeRegistry().getAdapterNames(ctx); 235 } 236 237 @Override 238 public List<AdapterType> getAdapters(Resource ctx) { 239 return getTypeRegistry().getAdapters(ctx); 240 } 241 242 @Override 243 public List<String> getEnabledAdapterNames(Resource ctx) { 244 return getTypeRegistry().getEnabledAdapterNames(ctx); 245 } 246 247 @Override 248 public List<AdapterType> getEnabledAdapters(Resource ctx) { 249 return getTypeRegistry().getEnabledAdapters(ctx); 250 } 251 252 @Override 253 public String getMediaTypeId(MediaType mt) { 254 if (configuration.mediatTypeRefs == null) { 255 return null; 256 } 257 MediaTypeRef[] refs = configuration.mediatTypeRefs; 258 for (MediaTypeRef ref : refs) { 259 String id = ref.match(mt); 260 if (id != null) { 261 return id; 262 } 263 } 264 return null; 265 } 266 267 @Override 268 public List<ResourceBinding> getResourceBindings() { 269 return configuration.resources; 270 } 271 272 @Override 273 public boolean isDerivedFrom(String moduleName) { 274 if (configuration.name.equals(moduleName)) { 275 return true; 276 } 277 if (superModule != null) { 278 return superModule.isDerivedFrom(moduleName); 279 } 280 return false; 281 } 282 283 public void loadConfiguration() { 284 linkReg = new LinkRegistry(); 285 if (configuration.links != null) { 286 for (LinkDescriptor link : configuration.links) { 287 linkReg.registerLink(link); 288 } 289 } 290 configuration.links = null; // avoid storing unused data 291 } 292 293 @Override 294 public List<LinkDescriptor> getLinks(String category) { 295 return linkReg.getLinks(category); 296 } 297 298 @Override 299 public List<LinkDescriptor> getActiveLinks(Resource context, String category) { 300 return linkReg.getActiveLinks(context, category); 301 } 302 303 public LinkRegistry getLinkRegistry() { 304 return linkReg; 305 } 306 307 @Override 308 public String getTemplateFileExt() { 309 return configuration.templateFileExt; 310 } 311 312 public void flushSkinCache() { 313 log.info("Flushing skin cache for module: " + getName()); 314 fileCache = new ConcurrentHashMap<>(); 315 } 316 317 public void flushTypeCache() { 318 log.info("Flushing type cache for module: " + getName()); 319 synchronized (typeLock) { 320 // remove type cache files if any 321 new DefaultTypeLoader(this, typeReg, configuration.directory).flushCache(); 322 typeReg = null; // type registry will be recreated on first access 323 } 324 } 325 326 /** 327 * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources. 328 */ 329 @Deprecated 330 public void flushRootResourcesCache() { 331 if (configuration.resources != null) { // reregister resources 332 for (ResourceBinding rb : configuration.resources) { 333 try { 334 engine.removeResourceBinding(rb); 335 rb.reload(engine); 336 engine.addResourceBinding(rb); 337 } catch (ClassNotFoundException e) { 338 log.error("Failed to reload resource", e); 339 } 340 } 341 } 342 } 343 344 @Override 345 public void flushCache() { 346 reloadMessages(); 347 flushSkinCache(); 348 flushTypeCache(); 349 } 350 351 public static File getSkinDir(File moduleDir) { 352 return new File(moduleDir, "skin"); 353 } 354 355 protected void loadDirectoryStack() { 356 dirStack = new DirectoryStack(); 357 try { 358 File skin = getSkinDir(configuration.directory); 359 if (!configuration.allowHostOverride) { 360 if (skin.isDirectory()) { 361 dirStack.addDirectory(skin); 362 } 363 } 364 for (File fragmentDir : configuration.fragmentDirectories) { 365 File fragmentSkin = getSkinDir(fragmentDir); 366 if (fragmentSkin.isDirectory()) { 367 dirStack.addDirectory(fragmentSkin); 368 } 369 } 370 if (configuration.allowHostOverride) { 371 if (skin.isDirectory()) { 372 dirStack.addDirectory(skin); 373 } 374 } 375 if (superModule != null) { 376 DirectoryStack ds = superModule.dirStack; 377 if (ds != null) { 378 dirStack.getDirectories().addAll(ds.getDirectories()); 379 } 380 } 381 } catch (IOException e) { 382 throw new NuxeoException("Failed to load directories stack", e); 383 } 384 } 385 386 @Override 387 public ScriptFile getFile(String path) { 388 int len = path.length(); 389 if (len == 0) { 390 return null; 391 } 392 char c = path.charAt(0); 393 if (c == '.') { // avoid getting files outside the web root 394 path = new Path(path).makeAbsolute().toString(); 395 } else if (c != '/') {// avoid doing duplicate entries in document stack 396 // cache 397 path = new StringBuilder(len + 1).append("/").append(path).toString(); 398 } 399 try { 400 return findFile(new Path(path).makeAbsolute().toString()); 401 } catch (IOException e) { 402 throw new NuxeoException(e); 403 } 404 } 405 406 /** 407 * @param path a normalized path (absolute path) 408 */ 409 protected ScriptFile findFile(String path) throws IOException { 410 ScriptFile file = fileCache.get(path); 411 if (file == null) { 412 File f = dirStack.getFile(path); 413 if (f != null) { 414 file = new ScriptFile(f); 415 fileCache.put(path, file); 416 } 417 } 418 return file; 419 } 420 421 @Override 422 public ScriptFile getSkinResource(String path) throws IOException { 423 File file = dirStack.getFile(path); 424 if (file != null) { 425 return new ScriptFile(file); 426 } 427 return null; 428 } 429 430 /** 431 * TODO There are no more reasons to lazy load the type registry since module are lazy loaded. Type registry must be 432 * loaded at module creation 433 */ 434 public TypeRegistry createTypeRegistry() { 435 // double s = System.currentTimeMillis(); 436 TypeRegistry typeReg = null; 437 // install types from super modules 438 if (superModule != null) { // TODO add type registry listener on super 439 // modules to update types when needed? 440 typeReg = new TypeRegistry(superModule.getTypeRegistry(), engine, this); 441 } else { 442 typeReg = new TypeRegistry(engine, this); 443 } 444 if (configuration.directory.isDirectory()) { 445 DefaultTypeLoader loader = new DefaultTypeLoader(this, typeReg, configuration.directory); 446 loader.load(); 447 } 448 // System.out.println(">>>>>>>>>>>>>"+((System.currentTimeMillis()-s)/1000)); 449 return typeReg; 450 } 451 452 @Override 453 public File getRoot() { 454 return configuration.directory; 455 } 456 457 public void reloadMessages() { 458 messages = new Messages(superModule != null ? superModule.getMessages() : null, this); 459 } 460 461 @Override 462 public Messages getMessages() { 463 return messages; 464 } 465 466 @Override 467 @SuppressWarnings({ "unchecked", "rawtypes" }) 468 public Map<String, String> getMessages(String language) { 469 log.info("Loading i18n files for module " + configuration.name); 470 File file = new File(configuration.directory, 471 new StringBuilder().append("/i18n/messages_").append(language).append(".properties").toString()); 472 InputStream in = null; 473 try { 474 in = new FileInputStream(file); 475 Properties p = new Properties(); 476 p.load(in); 477 return new HashMap(p); // HashMap is faster than Properties 478 } catch (IOException e) { 479 return null; 480 } finally { 481 if (in != null) { 482 try { 483 in.close(); 484 } catch (IOException ee) { 485 log.error(ee); 486 } 487 } 488 } 489 } 490 491 @Override 492 public String toString() { 493 return getName(); 494 } 495 496}