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