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