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                    TypeRegistry registry = createTypeRegistry();
189                    if (configuration.rootType != null) {
190                        // compatibility code for avoiding NPE
191                        rootType = registry.getType(configuration.rootType);
192                    }
193                    typeReg = registry;
194                }
195            }
196        }
197        return typeReg;
198    }
199
200    @Override
201    public Class<?> loadClass(String className) throws ClassNotFoundException {
202        return engine.loadClass(className);
203    }
204
205    @Override
206    public ResourceType getType(String typeName) {
207        ResourceType type = getTypeRegistry().getType(typeName);
208        if (type == null) {
209            throw new WebResourceNotFoundException("Type not found: " + typeName);
210        }
211        return type;
212    }
213
214    @Override
215    public ResourceType[] getTypes() {
216        return getTypeRegistry().getTypes();
217    }
218
219    @Override
220    public AdapterType[] getAdapters() {
221        return getTypeRegistry().getAdapters();
222    }
223
224    @Override
225    public AdapterType getAdapter(Resource ctx, String name) {
226        AdapterType type = getTypeRegistry().getAdapter(ctx, name);
227        if (type == null) {
228            throw new WebResourceNotFoundException("Service " + name + " not found for object: " + ctx.getPath()
229                    + " of type " + ctx.getType().getName());
230        }
231        return type;
232    }
233
234    @Override
235    public List<String> getAdapterNames(Resource ctx) {
236        return getTypeRegistry().getAdapterNames(ctx);
237    }
238
239    @Override
240    public List<AdapterType> getAdapters(Resource ctx) {
241        return getTypeRegistry().getAdapters(ctx);
242    }
243
244    @Override
245    public List<String> getEnabledAdapterNames(Resource ctx) {
246        return getTypeRegistry().getEnabledAdapterNames(ctx);
247    }
248
249    @Override
250    public List<AdapterType> getEnabledAdapters(Resource ctx) {
251        return getTypeRegistry().getEnabledAdapters(ctx);
252    }
253
254    @Override
255    public String getMediaTypeId(MediaType mt) {
256        if (configuration.mediatTypeRefs == null) {
257            return null;
258        }
259        MediaTypeRef[] refs = configuration.mediatTypeRefs;
260        for (MediaTypeRef ref : refs) {
261            String id = ref.match(mt);
262            if (id != null) {
263                return id;
264            }
265        }
266        return null;
267    }
268
269    @Override
270    public List<ResourceBinding> getResourceBindings() {
271        return configuration.resources;
272    }
273
274    @Override
275    public boolean isDerivedFrom(String moduleName) {
276        if (configuration.name.equals(moduleName)) {
277            return true;
278        }
279        if (superModule != null) {
280            return superModule.isDerivedFrom(moduleName);
281        }
282        return false;
283    }
284
285    public void loadConfiguration() {
286        linkReg = new LinkRegistry();
287        if (configuration.links != null) {
288            for (LinkDescriptor link : configuration.links) {
289                linkReg.registerLink(link);
290            }
291        }
292        configuration.links = null; // avoid storing unused data
293    }
294
295    @Override
296    public List<LinkDescriptor> getLinks(String category) {
297        return linkReg.getLinks(category);
298    }
299
300    @Override
301    public List<LinkDescriptor> getActiveLinks(Resource context, String category) {
302        return linkReg.getActiveLinks(context, category);
303    }
304
305    public LinkRegistry getLinkRegistry() {
306        return linkReg;
307    }
308
309    @Override
310    public String getTemplateFileExt() {
311        return configuration.templateFileExt;
312    }
313
314    public void flushSkinCache() {
315        log.info("Flushing skin cache for module: " + getName());
316        fileCache = new ConcurrentHashMap<String, ScriptFile>();
317    }
318
319    public void flushTypeCache() {
320        log.info("Flushing type cache for module: " + getName());
321        synchronized (typeLock) {
322            // remove type cache files if any
323            new DefaultTypeLoader(this, typeReg, configuration.directory).flushCache();
324            typeReg = null; // type registry will be recreated on first access
325        }
326    }
327
328    /**
329     * @deprecated resources are deprecated - you should use a jax-rs application to declare more resources.
330     */
331    @Deprecated
332    public void flushRootResourcesCache() {
333        if (configuration.resources != null) { // reregister resources
334            for (ResourceBinding rb : configuration.resources) {
335                try {
336                    engine.removeResourceBinding(rb);
337                    rb.reload(engine);
338                    engine.addResourceBinding(rb);
339                } catch (ClassNotFoundException e) {
340                    log.error("Failed to reload resource", e);
341                }
342            }
343        }
344    }
345
346    @Override
347    public void flushCache() {
348        reloadMessages();
349        flushSkinCache();
350        flushTypeCache();
351    }
352
353    public static File getSkinDir(File moduleDir) {
354        return new File(moduleDir, "skin");
355    }
356
357    protected void loadDirectoryStack() {
358        dirStack = new DirectoryStack();
359        try {
360            File skin = getSkinDir(configuration.directory);
361            if (!configuration.allowHostOverride) {
362                if (skin.isDirectory()) {
363                    dirStack.addDirectory(skin);
364                }
365            }
366            for (File fragmentDir : configuration.fragmentDirectories) {
367                File fragmentSkin = getSkinDir(fragmentDir);
368                if (fragmentSkin.isDirectory()) {
369                    dirStack.addDirectory(fragmentSkin);
370                }
371            }
372            if (configuration.allowHostOverride) {
373                if (skin.isDirectory()) {
374                    dirStack.addDirectory(skin);
375                }
376            }
377            if (superModule != null) {
378                DirectoryStack ds = superModule.dirStack;
379                if (ds != null) {
380                    dirStack.getDirectories().addAll(ds.getDirectories());
381                }
382            }
383        } catch (IOException e) {
384            throw new NuxeoException("Failed to load directories stack", e);
385        }
386    }
387
388    @Override
389    public ScriptFile getFile(String path) {
390        int len = path.length();
391        if (len == 0) {
392            return null;
393        }
394        char c = path.charAt(0);
395        if (c == '.') { // avoid getting files outside the web root
396            path = new Path(path).makeAbsolute().toString();
397        } else if (c != '/') {// avoid doing duplicate entries in document stack
398                              // cache
399            path = new StringBuilder(len + 1).append("/").append(path).toString();
400        }
401        try {
402            return findFile(new Path(path).makeAbsolute().toString());
403        } catch (IOException e) {
404            throw new NuxeoException(e);
405        }
406    }
407
408    /**
409     * @param path a normalized path (absolute path)
410     */
411    protected ScriptFile findFile(String path) throws IOException {
412        ScriptFile file = fileCache.get(path);
413        if (file == null) {
414            File f = dirStack.getFile(path);
415            if (f != null) {
416                file = new ScriptFile(f);
417                fileCache.put(path, file);
418            }
419        }
420        return file;
421    }
422
423    @Override
424    public ScriptFile getSkinResource(String path) throws IOException {
425        File file = dirStack.getFile(path);
426        if (file != null) {
427            return new ScriptFile(file);
428        }
429        return null;
430    }
431
432    /**
433     * TODO There are no more reasons to lazy load the type registry since module are lazy loaded. Type registry must be
434     * loaded at module creation
435     */
436    public TypeRegistry createTypeRegistry() {
437        // double s = System.currentTimeMillis();
438        TypeRegistry typeReg = null;
439        // install types from super modules
440        if (superModule != null) { // TODO add type registry listener on super
441                                   // modules to update types when needed?
442            typeReg = new TypeRegistry(superModule.getTypeRegistry(), engine, this);
443        } else {
444            typeReg = new TypeRegistry(engine, this);
445        }
446        if (configuration.directory.isDirectory()) {
447            DefaultTypeLoader loader = new DefaultTypeLoader(this, typeReg, configuration.directory);
448            loader.load();
449        }
450        // System.out.println(">>>>>>>>>>>>>"+((System.currentTimeMillis()-s)/1000));
451        return typeReg;
452    }
453
454    @Override
455    public File getRoot() {
456        return configuration.directory;
457    }
458
459    public void reloadMessages() {
460        messages = new Messages(superModule != null ? superModule.getMessages() : null, this);
461    }
462
463    @Override
464    public Messages getMessages() {
465        return messages;
466    }
467
468    @Override
469    @SuppressWarnings("unchecked")
470    public Map<String, String> getMessages(String language) {
471        log.info("Loading i18n files for module " + configuration.name);
472        File file = new File(configuration.directory,
473                new StringBuilder().append("/i18n/messages_").append(language).append(".properties").toString());
474        InputStream in = null;
475        try {
476            in = new FileInputStream(file);
477            Properties p = new Properties();
478            p.load(in);
479            return new HashMap(p); // HashMap is faster than Properties
480        } catch (IOException e) {
481            return null;
482        } finally {
483            if (in != null) {
484                try {
485                    in.close();
486                } catch (IOException ee) {
487                    log.error(ee);
488                }
489            }
490        }
491    }
492
493    @Override
494    public String toString() {
495        return getName();
496    }
497
498}