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.Date;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Set;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
029import org.apache.avalon.framework.logger.AbstractLogEnabled;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceResolver;
035import org.xml.sax.SAXException;
036
037import org.ametys.core.cache.AbstractCacheManager;
038import org.ametys.runtime.i18n.I18nizableText;
039import org.ametys.web.repository.site.Site;
040import org.ametys.web.repository.site.SiteType;
041import org.ametys.web.repository.site.SiteTypesExtensionPoint;
042import org.ametys.web.service.ServiceExtensionPoint;
043
044/**
045 * This implementation of the services handler is based on services declared in the whole application
046 */
047public class DefaultServicesAssignmentHandler extends AbstractLogEnabled  implements ServicesAssignmentHandler, Serviceable, Initializable
048{
049    private static final String __CACHE_ID = DefaultServicesAssignmentHandler.class.getName() + "$cache";
050    
051    /** The services manager */
052    protected ServiceExtensionPoint _serviceEP;
053    /** The site type manager */
054    protected SiteTypesExtensionPoint _siteTypeExtensionPoint;
055    /** The source resolver */
056    protected SourceResolver _srcResolver;
057    /** The cache manager */
058    protected AbstractCacheManager _cacheManager;
059    
060    @Override
061    public void service(ServiceManager manager) throws ServiceException
062    {
063        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
064        _siteTypeExtensionPoint = (SiteTypesExtensionPoint) manager.lookup(SiteTypesExtensionPoint.ROLE);
065        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
066        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
067    }
068    
069    @Override
070    public void initialize() throws Exception
071    {
072        _cacheManager.createMemoryCache(__CACHE_ID,
073                new I18nizableText("plugin.web", "PLUGINS_WEB_DEFAULT_SERVICES_ASSIGNMENT_HANDLER_CACHE_LABEL"),
074                new I18nizableText("plugin.web", "PLUGINS_WEB_DEFAULT_SERVICES_ASSIGNMENT_HANDLER_CACHE_DESCRIPTION"),
075                true,
076                null);
077    }
078    
079    @Override
080    public Set<String> getAvailableServices(SitemapElement page, String zoneName)
081    {
082        Site site = page.getSite();
083        SiteType siteType = _siteTypeExtensionPoint.getExtension(site.getType());
084        
085        Set<String> services = _getServicesForZone(siteType.getName(), site.getSkinId(), page.getTemplate(), zoneName);
086        if (services != null)
087        {
088            services = new HashSet<>(services);
089            services.retainAll(_getPublicServices());
090            return services;
091        }
092        
093        services = _getServicesForTemplate(siteType.getName(), site.getSkinId(), page.getTemplate());
094        if (services != null)
095        {
096            services = new HashSet<>(services);
097            services.retainAll(_getPublicServices());
098            return services;
099        }
100        
101        services = _getServicesForSkin(siteType.getName(), site.getSkinId());
102        if (services != null)
103        {
104            services = new HashSet<>(services);
105            services.retainAll(_getPublicServices());
106            return services;
107        }
108        
109        services = _getServicesForSiteType(siteType.getName());
110        if (services != null)
111        {
112            services = new HashSet<>(services);
113            services.retainAll(_getPublicServices());
114            return services;
115        }
116        
117        return _getPublicServices();
118    }
119    
120    public Set<String> limitAvailableServiceViews(Set<String> allViews, SitemapElement page, String zoneName, String serviceId)
121    {
122        Site site = page.getSite();
123        SiteType siteType = _siteTypeExtensionPoint.getExtension(site.getType());
124        
125        ViewCacheValue services = _getServiceViewsForZone(siteType.getName(), site.getSkinId(), page.getTemplate(), zoneName, serviceId);
126        if (services != null)
127        {
128            return _viewCacheValueToViews(allViews, services);
129        }
130        
131        services = _getServiceViewsForTemplate(siteType.getName(), site.getSkinId(), page.getTemplate(), serviceId);
132        if (services != null)
133        {
134            return _viewCacheValueToViews(allViews, services);
135        }
136        
137        services = _getServiceViewsForSkin(siteType.getName(), site.getSkinId(), serviceId);
138        if (services != null)
139        {
140            return _viewCacheValueToViews(allViews, services);
141        }
142        
143        services = _getServiceViewsForSiteType(siteType.getName(), serviceId);
144        if (services != null)
145        {
146            return _viewCacheValueToViews(allViews, services);
147        }
148        
149        return allViews;
150    }
151    
152    private Set<String> _getServicesForZone(String siteType, String skinName, String templateName, String zoneName)
153    {
154        String key = "services/" + siteType + "/" + skinName + "/" + templateName + "/" + zoneName;
155        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/services-" + siteType + ".xml";
156
157        Source configFile = null;
158        try
159        {
160            configFile = _srcResolver.resolveURI(file);
161            if (!configFile.exists())
162            {
163                return null;
164            }
165
166            ServiceCache data = (ServiceCache) getCache().get(key);
167            if (data == null || !data.isValid(configFile.getLastModified()))
168            {
169                data = _parseZoneServices(configFile, zoneName);
170                getCache().put(key, data);
171            }
172            return data;
173
174        }
175        catch (IOException e)
176        {
177            getLogger().error("Unable to read the services configuration file", e);
178            return null;
179        }
180        finally
181        {
182            _srcResolver.release(configFile);
183        }
184    }
185    
186    private ViewCacheValue _getServiceViewsForZone(String siteType, String skinName, String templateName, String zoneName, String serviceId)
187    {
188        String key = "views/" + siteType + "/" + skinName + "/" + templateName + "/" + zoneName;
189        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/services-views-" + siteType + ".xml";
190
191        Source configFile = null;
192        try
193        {
194            configFile = _srcResolver.resolveURI(file);
195            if (!configFile.exists())
196            {
197                return null;
198            }
199
200            ViewCache data = (ViewCache) getCache().get(key);
201            if (data == null || !data.isValid(configFile.getLastModified()))
202            {
203                data = _parseZoneServicesViews(configFile, zoneName);
204                getCache().put(key, data);
205            }
206            return data != null ? data.get(serviceId) : null;
207
208        }
209        catch (IOException e)
210        {
211            getLogger().error("Unable to read the services views configuration file", e);
212            return null;
213        }
214        finally
215        {
216            _srcResolver.release(configFile);
217        }
218    }
219    
220    private Set<String> _getServicesForTemplate (String siteType, String skinName, String templateName)
221    {
222        String key = "services/" + siteType + "/" + skinName + "/" + templateName;
223        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/services-" + siteType + ".xml";
224
225        Source configFile = null;
226        try
227        {
228            configFile = _srcResolver.resolveURI(file);
229            if (!configFile.exists())
230            {
231                return null;
232            }
233
234            ServiceCache data = (ServiceCache) getCache().get(key);
235            if (data == null || !data.isValid(configFile.getLastModified()))
236            {
237                data = _parseTemplateServices(configFile);
238                getCache().put(key, data);
239            }
240            return data;
241
242        }
243        catch (IOException e)
244        {
245            getLogger().error("Unable to read the services configuration file", e);
246            return null;
247        }
248        finally
249        {
250            _srcResolver.release(configFile);
251        }
252    }
253    
254    private ViewCacheValue _getServiceViewsForTemplate (String siteType, String skinName, String templateName, String serviceId)
255    {
256        String key = "views/" + siteType + "/" + skinName + "/" + templateName;
257        String file = "skin:" + skinName + "://templates/" + templateName + "/conf/services-views-" + siteType + ".xml";
258
259        Source configFile = null;
260        try
261        {
262            configFile = _srcResolver.resolveURI(file);
263            if (!configFile.exists())
264            {
265                return null;
266            }
267
268            ViewCache data = (ViewCache) getCache().get(key);
269            if (data == null || !data.isValid(configFile.getLastModified()))
270            {
271                data = _parseTemplateServicesViews(configFile);
272                getCache().put(key, data);
273            }
274            return data != null ? data.get(serviceId) : null;
275
276        }
277        catch (IOException e)
278        {
279            getLogger().error("Unable to read the services configuration file", e);
280            return null;
281        }
282        finally
283        {
284            _srcResolver.release(configFile);
285        }
286    }
287    
288    private Set<String> _getServicesForSkin (String siteType, String skinName)
289    {
290        String key = "services/" + siteType + "/" + skinName;
291        String file = "skin:" + skinName + "://conf/services-" + siteType + ".xml";
292
293        Source configFile = null;
294        try
295        {
296            configFile = _srcResolver.resolveURI(file);
297            if (!configFile.exists())
298            {
299                return null;
300            }
301
302            ServiceCache data = (ServiceCache) getCache().get(key);
303            if (data == null || !data.isValid(configFile.getLastModified()))
304            {
305                data = _parseServices(configFile);
306                getCache().put(key, data);
307            }
308            return data;
309
310        }
311        catch (IOException e)
312        {
313            getLogger().error("Unable to read the services configuration file", e);
314            return null;
315        }
316        finally
317        {
318            _srcResolver.release(configFile);
319        }
320    }
321    
322    private ViewCacheValue _getServiceViewsForSkin (String siteType, String skinName, String serviceId)
323    {
324        String key = "views/" + siteType + "/" + skinName;
325        String file = "skin:" + skinName + "://conf/services-views-" + siteType + ".xml";
326
327        Source configFile = null;
328        try
329        {
330            configFile = _srcResolver.resolveURI(file);
331            if (!configFile.exists())
332            {
333                return null;
334            }
335
336            ViewCache data = (ViewCache) getCache().get(key);
337            if (data == null || !data.isValid(configFile.getLastModified()))
338            {
339                data = _parseServicesViews(configFile);
340                getCache().put(key, data);
341            }
342            return data != null ? data.get(serviceId) : null;
343
344        }
345        catch (IOException e)
346        {
347            getLogger().error("Unable to read the services configuration file", e);
348            return null;
349        }
350        finally
351        {
352            _srcResolver.release(configFile);
353        }
354    }
355    
356    private Set<String> _getServicesForSiteType (String siteType)
357    {
358        String key = "services/" + siteType;
359        String file = "context://WEB-INF/param/services-" + siteType + ".xml";
360        
361        Source configFile = null;
362        try
363        {
364            configFile = _srcResolver.resolveURI(file);
365            if (!configFile.exists())
366            {
367                return null;
368            }
369
370            ServiceCache data = (ServiceCache) getCache().get(key);
371            if (data == null || !data.isValid(configFile.getLastModified()))
372            {
373                data = _parseServices(configFile);
374                getCache().put(key, data);
375            }
376            return data;
377
378        }
379        catch (IOException e)
380        {
381            getLogger().error("Unable to read the services configuration file", e);
382            return null;
383        }
384        finally
385        {
386            _srcResolver.release(configFile);
387        }
388    }
389    
390    private ViewCacheValue _getServiceViewsForSiteType (String siteType, String serviceId)
391    {
392        String key = "views/" + siteType;
393        String file = "context://WEB-INF/param/services-views-" + siteType + ".xml";
394        
395        Source configFile = null;
396        try
397        {
398            configFile = _srcResolver.resolveURI(file);
399            if (!configFile.exists())
400            {
401                return null;
402            }
403
404            ViewCache data = (ViewCache) getCache().get(key);
405            if (data == null || !data.isValid(configFile.getLastModified()))
406            {
407                data = _parseServicesViews(configFile);
408                getCache().put(key, data);
409            }
410            return data != null ? data.get(serviceId) : null;
411
412        }
413        catch (IOException e)
414        {
415            getLogger().error("Unable to read the services configuration file", e);
416            return null;
417        }
418        finally
419        {
420            _srcResolver.release(configFile);
421        }
422    }
423    
424    /**
425     * Get the public services.
426     * @return the public services.
427     */
428    protected ServiceCache _getPublicServices()
429    {
430        ServiceCache publicServices = new ServiceCache(new Date().getTime());
431        
432        for (String id : _serviceEP.getExtensionsIds())
433        {
434            if (!_serviceEP.getExtension(id).isPrivate())
435            {
436                publicServices.add(id);
437            }
438        }
439        
440        return publicServices;
441    }
442    
443    /**
444     * Parse a template configuration file.
445     * @param configFile the template configuration file.
446     * @return the set of available services for the template.
447     */
448    protected ServiceCache _parseTemplateServices(Source configFile)
449    {
450        ServiceCache services = null;
451        
452        if (!configFile.exists())
453        {
454            return null;
455        }
456        
457        try (InputStream is = configFile.getInputStream())
458        {
459            Configuration configuration = new DefaultConfigurationBuilder().build(is);
460            
461            Configuration tplConf = configuration.getChild("template", false);
462            if (tplConf != null)
463            {
464                services = _parseServices(tplConf, configFile.getLastModified());
465            }
466        }
467        catch (IOException | ConfigurationException | SAXException e)
468        {
469            getLogger().error("Unable parse the services configuration file", e);
470            return null;
471        }
472        
473        return services;
474    }
475    
476    /**
477     * Parse a template configuration file.
478     * @param configFile the template configuration file.
479     * @return the set of available services views for the template.
480     */
481    protected ViewCache _parseTemplateServicesViews(Source configFile)
482    {
483        ViewCache services = null;
484        
485        if (!configFile.exists())
486        {
487            return null;
488        }
489        
490        try (InputStream is = configFile.getInputStream())
491        {
492            Configuration configuration = new DefaultConfigurationBuilder().build(is);
493            
494            Configuration tplConf = configuration.getChild("template", false);
495            if (tplConf != null)
496            {
497                services = _parseServicesViews(tplConf, configFile.getLastModified());
498            }
499        }
500        catch (IOException | ConfigurationException | SAXException e)
501        {
502            getLogger().error("Unable to read the services views configuration file", e);
503            return null;
504        }
505        
506        return services;
507    }
508    
509    /**
510     * Parse a template configuration file and get the set of available services for a given zone.
511     * @param configFile the template configuration file.
512     * @param zoneName the zone name.
513     * @return the set of available services for the given zone.
514     */
515    protected ServiceCache _parseZoneServices(Source configFile, String zoneName)
516    {
517        ServiceCache services = null;
518        
519        if (!configFile.exists())
520        {
521            return null;
522        }
523
524        try (InputStream is = configFile.getInputStream())
525        {
526            Configuration configuration = new DefaultConfigurationBuilder().build(is);
527            
528            Configuration zoneConfs = configuration.getChild("zones", false);
529            if (zoneConfs != null)
530            {
531                for (Configuration zoneConf : zoneConfs.getChildren("zone"))
532                {
533                    if (zoneConf.getAttribute("id").equals(zoneName))
534                    {
535                        services = _parseServices(zoneConf, configFile.getLastModified());
536                    }
537                }
538            }
539        }
540        catch (IOException | ConfigurationException | SAXException e)
541        {
542            getLogger().error("Unable parse the services configuration file", e);
543            return null;
544        }
545        
546        return services;
547    }
548
549    /**
550     * Parse a template configuration file and get the set of available services for a given zone.
551     * @param configFile the template configuration file.
552     * @param zoneName the zone name.
553     * @return the set of available services views for the given zone.
554     */
555    protected ViewCache _parseZoneServicesViews(Source configFile, String zoneName)
556    {
557        ViewCache services = null;
558        
559        if (!configFile.exists())
560        {
561            return null;
562        }
563
564        try (InputStream is = configFile.getInputStream())
565        {
566            Configuration configuration = new DefaultConfigurationBuilder().build(is);
567            
568            Configuration zoneConfs = configuration.getChild("zones", false);
569            if (zoneConfs != null)
570            {
571                for (Configuration zoneConf : zoneConfs.getChildren("zone"))
572                {
573                    if (zoneConf.getAttribute("id").equals(zoneName))
574                    {
575                        services = _parseServicesViews(zoneConf, configFile.getLastModified());
576                    }
577                }
578            }
579        }
580        catch (IOException | ConfigurationException | SAXException e)
581        {
582            getLogger().error("Unable parse the services views configuration file", e);
583            return null;
584        }
585        
586        return services;
587    }
588    
589    /**
590     * Parses the valid services for the site type
591     * @param configFile the configuration file.
592     * @return the services id in a Set
593     */
594    protected ServiceCache _parseServices (Source configFile)
595    {
596        if (!configFile.exists())
597        {
598            return null;
599        }
600        
601        ServiceCache services = null;
602        
603        try (InputStream is = configFile.getInputStream())
604        {
605            Configuration configuration = new DefaultConfigurationBuilder().build(is);
606            
607            services = _parseServices(configuration, configFile.getLastModified());
608        }
609        catch (IOException | ConfigurationException | SAXException e)
610        {
611            getLogger().error("Unable parse the services configuration file", e);
612            return null;
613        }
614        
615        return services;
616    }
617
618    /**
619     * Parses the valid services views for the site type
620     * @param configFile the configuration file.
621     * @return the services views id in a Set
622     */
623    protected ViewCache _parseServicesViews(Source configFile)
624    {
625        if (!configFile.exists())
626        {
627            return null;
628        }
629        
630        ViewCache services = null;
631        
632        try (InputStream is = configFile.getInputStream())
633        {
634            Configuration configuration = new DefaultConfigurationBuilder().build(is);
635            
636            services = _parseServicesViews(configuration, configFile.getLastModified());
637        }
638        catch (IOException | ConfigurationException | SAXException e)
639        {
640            getLogger().error("Unable parse the services views configuration file", e);
641            return null;
642        }
643        
644        return services;
645    }
646    
647    /**
648     * Parses the valid services in a configuration.
649     * @param configuration the configuration.
650     * @param lastModificationDade last date of modification
651     * @return the services id in a Set
652     * @throws ConfigurationException if configuration is invalid
653     */
654    protected ServiceCache _parseServices(Configuration configuration, long lastModificationDade) throws ConfigurationException
655    {
656        ServiceCache services;
657        
658        String mode = configuration.getAttribute("mode", "include");
659        if ("exclude".equals(mode))
660        {
661            services = _getPublicServices();
662            for (Configuration serviceConf : configuration.getChildren("service"))
663            {
664                String id = serviceConf.getAttribute("id");
665                services.remove(id);
666            }
667        }
668        else
669        {
670            services = new ServiceCache(lastModificationDade);
671            for (Configuration serviceConf : configuration.getChildren("service"))
672            {
673                String id = serviceConf.getAttribute("id");
674                services.add(id);
675            }
676        }
677        
678        return services;
679    }
680
681    /**
682     * Parses the valid services in a configuration.
683     * @param configuration the configuration.
684     * @param lastModificationDade last date of modification
685     * @return the services id in a Set
686     * @throws ConfigurationException if configuration is invalid
687     */
688    protected ViewCache _parseServicesViews(Configuration configuration, long lastModificationDade) throws ConfigurationException
689    {
690        ViewCache servicesViews = new ViewCache(lastModificationDade);
691        
692        for (Configuration serviceConf : configuration.getChildren("service"))
693        {
694            Set<String> viewNames = new HashSet<>();
695            
696            String id = serviceConf.getAttribute("id");
697            Configuration viewsConf = serviceConf.getChild("views");
698            String mode = viewsConf.getAttribute("mode", "include");
699            
700            for (Configuration viewConf : viewsConf.getChildren("view"))
701            {
702                String viewName = viewConf.getAttribute("name") + ".xsl";
703                viewNames.add(viewName);
704            }
705            
706            servicesViews.put(id, new ViewCacheValue("exclude".equals(mode), viewNames));
707        }
708        
709        return servicesViews;
710    }
711    
712    /**
713     * The cache is a HashSet of String + a date
714     */
715    protected static class ServiceCache extends HashSet<String>
716    {
717        private long _sourceLastModified;
718        
719        /**
720         * Build the cache
721         * @param sourceLastModified The last modification date
722         */
723        public ServiceCache(long sourceLastModified)
724        {
725            super();
726        }
727        
728        /**
729         * Determine if the cache is valid
730         * @param newSourceLastModified The new last modification date
731         * @return true if the cache is still valid
732         */
733        public boolean isValid(long newSourceLastModified)
734        {
735            return newSourceLastModified <= _sourceLastModified;
736        }
737    }
738
739    private Set<String> _viewCacheValueToViews(Set<String> allViews, ViewCacheValue viewCacheValue)
740    {
741        Set<String> views = new HashSet<>(allViews);
742        
743        if (viewCacheValue.exclude)
744        {
745            views.removeAll(viewCacheValue.views);
746        }
747        else
748        {
749            views.retainAll(viewCacheValue.views);
750        }
751        
752        return views;
753    }
754    
755    private record ViewCacheValue(Boolean exclude, Set<String> views) { }
756    
757    /**
758     * The cache is a HashMap of String + a date
759     */
760    protected static class ViewCache extends HashMap<String, ViewCacheValue>
761    {
762        private long _sourceLastModified;
763        
764        /**
765         * Build the cache
766         * @param sourceLastModified The last modification date
767         */
768        public ViewCache(long sourceLastModified)
769        {
770            super();
771        }
772        
773        /**
774         * Determine if the cache is valid
775         * @param newSourceLastModified The new last modification date
776         * @return true if the cache is still valid
777         */
778        public boolean isValid(long newSourceLastModified)
779        {
780            return newSourceLastModified <= _sourceLastModified;
781        }
782    }
783
784    private org.ametys.core.cache.Cache<String, Object> getCache()
785    {
786        return _cacheManager.get(__CACHE_ID);
787    }
788}