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