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