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