001/*
002 *  Copyright 2010 Anyware Services
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 */
016package org.ametys.web.repository.page;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.configuration.Configuration;
026import org.apache.avalon.framework.configuration.ConfigurationException;
027import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
028import org.apache.avalon.framework.logger.AbstractLogEnabled;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.excalibur.source.Source;
033import org.apache.excalibur.source.SourceResolver;
034import org.xml.sax.SAXException;
035
036import org.ametys.cms.contenttype.ContentType;
037import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
038import org.ametys.web.repository.site.Site;
039import org.ametys.web.repository.site.SiteType;
040import org.ametys.web.repository.site.SiteTypesExtensionPoint;
041
042/**
043 * This implementation of the content types handler is based on content types declared in the whole application
044 */
045public class DefaultContentTypesAssignmentHandler extends AbstractLogEnabled implements ContentTypesAssignmentHandler, Serviceable
046{
047    /** The content types manager */
048    protected ContentTypeExtensionPoint _cTypeEP;
049    /** The site type manager */
050    protected SiteTypesExtensionPoint _siteTypeExtensionPoint;
051    /** The source resolver */
052    protected SourceResolver _srcResolver;
053    
054    private Map<String, Cache> _cache = new HashMap<>();
055    
056    @Override
057    public void service(ServiceManager manager) throws ServiceException
058    {
059        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
060        _siteTypeExtensionPoint = (SiteTypesExtensionPoint) manager.lookup(SiteTypesExtensionPoint.ROLE);
061        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
062    }
063    
064    @Override
065    public Set<String> getAvailableContentTypes(Site site)
066    {
067        return getAvailableContentTypes (site, false);
068    }
069    
070    @Override
071    public Set<String> getAvailableContentTypes(Site site, boolean includePrivate)
072    {
073        Set<String> allCTypes = _getContentTypes(includePrivate, false, false, false);
074        
075        SiteType siteType = _siteTypeExtensionPoint.getExtension(site.getType());
076        
077        Set<String> cTypes = _getContentTypesForSkin (siteType.getName(), site.getSkinId());
078        if (cTypes != null)
079        {
080            cTypes.retainAll(allCTypes);
081            return cTypes;
082        }
083        
084        cTypes = _getContentTypesForSiteType (siteType.getName());
085        if (cTypes != null)
086        {
087            cTypes.retainAll(allCTypes);
088            return cTypes;
089        }
090        
091        return allCTypes;
092    }
093    
094    @Override
095    public Set<String> getAvailableContentTypes(Page page, String zoneName, boolean includePrivate)
096    {
097        Set<String> allCTypes = _getContentTypes(includePrivate, false, false, false);
098        
099        Site site = page.getSite();
100        SiteType siteType = _siteTypeExtensionPoint.getExtension(site.getType());
101        
102        Set<String> cTypes = _getContentTypesForZone(siteType.getName(), site.getSkinId(), page.getTemplate(), zoneName);
103        if (cTypes != null)
104        {
105            cTypes.retainAll(allCTypes);
106            return cTypes;
107        }
108        
109        cTypes = _getContentTypesForTemplate(siteType.getName(), site.getSkinId(), page.getTemplate());
110        if (cTypes != null)
111        {
112            cTypes.retainAll(allCTypes);
113            return cTypes;
114        }
115        
116        cTypes = _getContentTypesForSkin (siteType.getName(), site.getSkinId());
117        if (cTypes != null)
118        {
119            cTypes.retainAll(allCTypes);
120            return cTypes;
121        }
122        
123        cTypes = _getContentTypesForSiteType (siteType.getName());
124        if (cTypes != null)
125        {
126            cTypes.retainAll(allCTypes);
127            return cTypes;
128        }
129        
130        return allCTypes;
131    }
132    
133    @Override
134    public Set<String> getAvailableContentTypes(Page page, String zoneName)
135    {
136        return getAvailableContentTypes(page, zoneName, false);
137    }
138    
139    private Set<String> _getContentTypesForZone(String siteType, String skinName, String templateName, String zoneName)
140    {
141        String key = siteType + "/" + skinName + "/" + templateName + "/" + zoneName;
142        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/content-types-" + siteType + ".xml";
143        
144        Source configFile = null;
145        try
146        {
147            configFile = _srcResolver.resolveURI(file);
148            if (!configFile.exists())
149            {
150                return null;
151            }
152        
153            Cache data = _cache.get(key); 
154            if (data == null || !data.isValid(configFile.getLastModified()))
155            {
156                data = _parseZoneContentTypes(configFile, zoneName);
157                _cache.put(key, data);
158            }
159            return data;
160        
161        }
162        catch (IOException e)
163        {
164            getLogger().error("Unable to read the content types configuration file", e);
165            return null;
166        }
167        finally
168        {
169            _srcResolver.release(configFile);
170        }
171    }
172    
173    private Set<String> _getContentTypesForTemplate (String siteType, String skinName, String templateName)
174    {
175        String key = siteType + "/" + skinName + "/" + templateName;
176        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/content-types-" + siteType + ".xml";
177        
178        Source configFile = null;
179        try
180        {
181            configFile = _srcResolver.resolveURI(file);
182            if (!configFile.exists())
183            {
184                return null;
185            }
186        
187            Cache data = _cache.get(key); 
188            if (data == null || !data.isValid(configFile.getLastModified()))
189            {
190                data = _parseTemplateContentTypes(configFile);
191                _cache.put(key, data);
192            }
193            return data;
194        
195        }
196        catch (IOException e)
197        {
198            getLogger().error("Unable to read the content types configuration file", e);
199            return null;
200        }
201        finally
202        {
203            _srcResolver.release(configFile);
204        }
205    }
206    
207    private Set<String> _getContentTypesForSkin (String siteType, String skinName)
208    {
209        String key = siteType + "/" + skinName;
210        String file = "skin:" + skinName + "://conf/content-types-" + siteType + ".xml";
211                
212        Source configFile = null;
213        try
214        {
215            configFile = _srcResolver.resolveURI(file);
216            if (!configFile.exists())
217            {
218                return null;
219            }
220
221            Cache data = _cache.get(key); 
222            if (data == null || !data.isValid(configFile.getLastModified()))
223            {
224                data = _parseContentTypes(configFile);
225                _cache.put(key, data);
226            }
227            return data;
228
229        }
230        catch (IOException e)
231        {
232            getLogger().error("Unable to read the content types configuration file", e);
233            return null;
234        }
235        finally
236        {
237            _srcResolver.release(configFile);
238        }
239    }
240    
241    private Set<String> _getContentTypesForSiteType (String siteType)
242    {
243        String key = siteType;
244        String file = "context://WEB-INF/param/content-types-" + siteType + ".xml";
245                
246        Source configFile = null;
247        try
248        {
249            configFile = _srcResolver.resolveURI(file);
250            if (!configFile.exists())
251            {
252                return null;
253            }
254
255            Cache data = _cache.get(key); 
256            if (data == null || !data.isValid(configFile.getLastModified()))
257            {
258                data = _parseContentTypes(configFile);
259                _cache.put(key, data);
260            }
261            return data;
262
263        }
264        catch (IOException e)
265        {
266            getLogger().error("Unable to read the content types configuration file", e);
267            return null;
268        }
269        finally
270        {
271            _srcResolver.release(configFile);
272        }
273    }
274    
275    /**
276     * Get the public content types.
277     * @param includePrivate <code>true</code> to include private content types
278     * @param includeReferenceTable <code>true</code> to include simple content types
279     * @param includeMixin <code>true</code> to include mixins
280     * @param includeAbstract <code>true</code> to include abstract content types
281     * @return the public content types.
282     */
283    protected Set<String> _getContentTypes(boolean includePrivate, boolean includeReferenceTable, boolean includeMixin, boolean includeAbstract)
284    {
285        Set<String> publicTypes = new HashSet<>();
286        
287        for (String id : _cTypeEP.getExtensionsIds())
288        {
289            ContentType cType = _cTypeEP.getExtension(id);
290            if ((includePrivate || !cType.isPrivate()) && (includeReferenceTable || !cType.isReferenceTable()) && (includeMixin || !cType.isMixin()) && (includeAbstract || !cType.isAbstract()))
291            {
292                publicTypes.add(id);
293            }
294        }
295        
296        return publicTypes;
297    }
298    
299    /**
300     * Parses the valid content types for a template
301     * @param configFile the template configuration file.
302     * @return the content types' id in a Set
303     */ 
304    protected Cache _parseTemplateContentTypes(Source configFile)
305    {
306        Cache cTypes = null;
307        
308        if (!configFile.exists())
309        {
310            return null;
311        }
312        
313        try (InputStream is = configFile.getInputStream())
314        {
315            Configuration configuration = new DefaultConfigurationBuilder().build(is);
316            
317            Configuration tplConf = configuration.getChild("template", false);
318            if (tplConf != null)
319            {
320                cTypes = _parseContentTypes(tplConf, configFile.getLastModified());
321            }
322        }
323        catch (IOException e)
324        {
325            getLogger().error("Unable to read the content types configuration file", e);
326            return null;
327        }
328        catch (ConfigurationException e)
329        {
330            getLogger().error("Unable to parse the content types configuration file", e);
331            return null;
332        }
333        catch (SAXException e)
334        {
335            getLogger().error("Unable parse the content types configuration file", e);
336            return null;
337        }
338        
339        return cTypes;
340    }
341    
342    
343    /**
344     * Parses the valid content types for the given zone.
345     * @param configFile the template configuration file.
346     * @param zoneName the zone name.
347     * @return the content types' id in a Set
348     * @throws IOException If an error occured while reading the file
349     */ 
350    protected Cache _parseZoneContentTypes(Source configFile, String zoneName) throws IOException
351    {
352        Cache cTypes = null;
353        
354        if (!configFile.exists())
355        {
356            return null;
357        }
358        
359        try (InputStream is = configFile.getInputStream())
360        {
361            Configuration configuration = new DefaultConfigurationBuilder().build(is);
362            
363            Configuration zoneConfs = configuration.getChild("zones", false);
364            if (zoneConfs != null)
365            {
366                for (Configuration zoneConf : zoneConfs.getChildren("zone"))
367                {
368                    if (zoneConf.getAttribute("id").equals(zoneName))
369                    {
370                        cTypes = _parseContentTypes(zoneConf, configFile.getLastModified());
371                    }
372                }
373            }
374        }
375        catch (ConfigurationException e)
376        {
377            getLogger().error("Unable to parse the content types configuration file", e);
378            return null;
379        }
380        catch (SAXException e)
381        {
382            getLogger().error("Unable parse the content types configuration file", e);
383            return null;
384        }
385        
386        return cTypes;
387    }
388    
389    /**
390     * Parses the valid content types for the site type
391     * @param configFile the configuration file.
392     * @return the content types' id in a Set
393     */ 
394    protected Cache _parseContentTypes (Source configFile)
395    {
396        Cache cTypes = null;
397        
398        if (!configFile.exists())
399        {
400            return null;
401        }
402
403        try (InputStream is = configFile.getInputStream())
404        {
405            Configuration configuration = new DefaultConfigurationBuilder().build(is);
406            
407            cTypes = _parseContentTypes(configuration, configFile.getLastModified());
408        }
409        catch (IOException e)
410        {
411            getLogger().error("Unable to read the content types configuration file", e);
412            return null;
413        }
414        catch (ConfigurationException e)
415        {
416            getLogger().error("Unable to parse the content types configuration file", e);
417            return null;
418        }
419        catch (SAXException e)
420        {
421            getLogger().error("Unable parse the content types configuration file", e);
422            return null;
423        }
424        
425        return cTypes;
426    }
427    
428    /**
429     * Parses the valid content types
430     * @param configuration the configuration.
431     * @param lastModificationDate the date the configuration was lastly modified
432     * @return the content types' id in a Set
433     * @throws ConfigurationException if configuration is invalid
434     */ 
435    protected Cache _parseContentTypes(Configuration configuration, long lastModificationDate) throws ConfigurationException
436    {
437        Cache cTypes = new Cache(lastModificationDate);
438        
439        String mode = configuration.getAttribute("mode", "include");
440        if ("exclude".equals(mode))
441        {
442            cTypes.addAll(_getContentTypes(false, false, false, false));
443            for (Configuration cTypeConf : configuration.getChildren("content-type"))
444            {
445                String id = cTypeConf.getAttribute("id");
446                cTypes.remove(id);
447            }
448        }
449        else
450        {
451            for (Configuration cTypeConf : configuration.getChildren("content-type"))
452            {
453                String id = cTypeConf.getAttribute("id");
454                cTypes.add(id);
455            }
456        }
457        
458        return cTypes;
459    }
460
461    /**
462     * The cache is a HashSet of String + a date 
463     */
464    protected static class Cache extends HashSet<String>
465    {
466        private long _sourceLastModified;
467        
468        /**
469         * Build the cache
470         * @param sourceLastModified The last modification date
471         */
472        public Cache(long sourceLastModified)
473        {
474            super();
475        }
476        
477        /**
478         * Determine if the cache is valid
479         * @param newSourceLastModified The new last modification date
480         * @return true if the cache is still valid
481         */
482        public boolean isValid(long newSourceLastModified)
483        {
484            return newSourceLastModified <= _sourceLastModified;
485        }
486    }
487}
488