001/*
002 *  Copyright 2015 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.site;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026
027import javax.jcr.RepositoryException;
028import javax.jcr.Session;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.core.group.GroupDirectoryContextHelper;
037import org.ametys.core.observation.Event;
038import org.ametys.core.observation.ObservationManager;
039import org.ametys.core.ui.Callable;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.core.user.population.PopulationContextHelper;
042import org.ametys.core.util.I18nUtils;
043import org.ametys.plugins.repository.AmetysObject;
044import org.ametys.plugins.repository.AmetysObjectIterable;
045import org.ametys.plugins.repository.AmetysObjectResolver;
046import org.ametys.plugins.repository.AmetysRepositoryException;
047import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
048import org.ametys.plugins.repository.UnknownAmetysObjectException;
049import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.model.ElementDefinition;
052import org.ametys.runtime.model.ModelHelper;
053import org.ametys.runtime.model.disableconditions.DefaultDisableConditionsEvaluator;
054import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator;
055import org.ametys.runtime.model.type.DataContext;
056import org.ametys.runtime.model.type.ElementType;
057import org.ametys.runtime.parameter.ValidationResult;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059import org.ametys.web.ObservationConstants;
060import org.ametys.web.cache.FOCommHelper;
061import org.ametys.web.cache.pageelement.PageElementCache;
062import org.ametys.web.repository.sitemap.Sitemap;
063import org.ametys.web.site.SiteConfigurationManager;
064
065/**
066 * DAO for manipulating sites
067 *
068 */
069public class SiteDAO extends AbstractLogEnabled implements Serviceable, Component
070{
071    /** Avalon Role */
072    public static final String ROLE = SiteDAO.class.getName();
073    
074    /** Id of the default site type */
075    public static final String DEFAULT_SITE_TYPE_ID = "org.ametys.web.sitetype.Default";
076    
077    private static final List<String> __FORBIDDEN_SITE_NAMES = Arrays.asList("preview", "live", "archives", "generate");
078    
079    private SiteManager _siteManager;
080    private AmetysObjectResolver _resolver;
081    private ObservationManager _observationManager;
082    private CurrentUserProvider _currentUserProvider;
083    private PageElementCache _inputDataCache;
084    private PageElementCache _zoneItemCache;
085    private SiteConfigurationManager _siteConfigurationManager;
086    private SiteTypesExtensionPoint _siteTypesEP;
087    private I18nUtils _i18nUtils;
088    private PopulationContextHelper _populationContextHelper;
089    private GroupDirectoryContextHelper _groupDirectoryContextHelper;
090    private DisableConditionsEvaluator _disableConditionsEvaluator;
091    private FOCommHelper _foCommHelper;
092    
093    @Override
094    public void service(ServiceManager smanager) throws ServiceException
095    {
096        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
097        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
098        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
099        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
100        _inputDataCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/inputData");
101        _zoneItemCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/zoneItem");
102        _siteConfigurationManager = (SiteConfigurationManager) smanager.lookup(SiteConfigurationManager.ROLE);
103        _siteTypesEP = (SiteTypesExtensionPoint) smanager.lookup(SiteTypesExtensionPoint.ROLE);
104        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
105        _populationContextHelper = (PopulationContextHelper) smanager.lookup(PopulationContextHelper.ROLE);
106        _groupDirectoryContextHelper = (GroupDirectoryContextHelper) smanager.lookup(GroupDirectoryContextHelper.ROLE);
107        _disableConditionsEvaluator = (DisableConditionsEvaluator) smanager.lookup(DefaultDisableConditionsEvaluator.ROLE);
108        _foCommHelper = (FOCommHelper) smanager.lookup(FOCommHelper.ROLE);
109    }
110    
111    /**
112     * Get the root id
113     * @return the root id
114     */
115    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
116    public String getRootId ()
117    {
118        return _siteManager.getRoot().getId();
119    }
120    
121    /**
122     * Get the properties of given sites
123     * @param names the site names
124     * @return the properties of the sites in a result map
125     */
126    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
127    public Map<String, Object> getSitesInfos(List<String> names)
128    {
129        Map<String, Object> result = new HashMap<>();
130        
131        List<Map<String, Object>> sites = new ArrayList<>();
132        List<String> sitesNotFound = new ArrayList<>();
133        
134        for (String name : names)
135        {
136            try
137            {
138                Site site = _siteManager.getSite(name);
139                sites.add(getSiteInfos(site));
140            }
141            catch (UnknownAmetysObjectException e)
142            {
143                sitesNotFound.add(name);
144            }
145        }
146        
147        result.put("sites", sites);
148        result.put("sitesNotFound", sitesNotFound);
149        
150        return result;
151    }
152    
153    /**
154     * Get the site's properties
155     * @param name the site name
156     * @return the properties
157     */
158    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
159    public Map<String, Object> getSiteInfos(String name)
160    {
161        Site site = _siteManager.getSite(name);
162        return getSiteInfos(site);
163    }
164    
165    /**
166     * Get the site's properties
167     * @param site the site
168     * @return the properties
169     */
170    public Map<String, Object> getSiteInfos(Site site)
171    {
172        Map<String, Object> infos = new HashMap<>();
173        
174        infos.put("id", site.getId());
175        infos.put("title", site.getTitle());
176        infos.put("description", site.getDescription());
177        infos.put("name", site.getName());
178        infos.put("path", site.getSitePath());
179        infos.put("url", site.getUrl());
180        
181        SiteType siteType = _siteTypesEP.getExtension(site.getType());
182        infos.put("type", _i18nUtils.translate(siteType.getLabel()));
183        
184        return infos;
185    }
186    
187    /**
188     * Creates a new site
189     * @param parentId The id of parent site. Can be null to create a root site.
190     * @param name The site's name
191     * @param type The site's type
192     * @param renameIfExists Set to true to automatically rename the site if already exists
193     * @return The result map with id of created site
194     */
195    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
196    public Map<String, Object> createSite(String parentId, String name, String type, boolean renameIfExists)
197    {
198        Map<String, Object> result = new HashMap<>();
199        
200        if (__FORBIDDEN_SITE_NAMES.contains(name))
201        {
202            // Name is invalid
203            result.put("name", name);
204            result.put("invalid-name", true);
205        }
206        else if (_siteManager.hasSite(name) && !renameIfExists)
207        {
208            // A site with same name already exists
209            result.put("name", name);
210            result.put("already-exists", true);
211        }
212        else
213        {
214            String siteParentId = null;
215            if (StringUtils.isNotEmpty(parentId))
216            {
217                AmetysObject parent = _resolver.resolveById(parentId);
218                if (parent instanceof Site)
219                {
220                    siteParentId = parent.getId();
221                }
222            }
223            
224            String siteName = name;
225            int index = 2;
226            while (_siteManager.hasSite(siteName))
227            {
228                siteName = name + "-" + (index++);
229            }
230            
231            // Create site
232            Site site = _siteManager.createSite(siteName, siteParentId);
233            site.setType(type);
234            site.saveChanges();
235            
236            result.put("id", site.getId());
237            result.put("name", site.getName());
238            
239            if (siteParentId != null)
240            {
241                result.put("parentId", siteParentId);
242            }
243            
244            // Notify observers
245            Map<String, Object> eventParams = new HashMap<>();
246            eventParams.put(ObservationConstants.ARGS_SITE, site);
247            _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_ADDED, _currentUserProvider.getUser(), eventParams));
248        }
249        
250        return result;
251    }
252    
253    /**
254     * Create a site by copy of another.
255     * @param parentId The id of parent site. Can be null to create a root site.
256     * @param name the name of site to create
257     * @param id the id of site to copy
258     * @return The result map with id of created site
259     * @throws Exception if an error ocurred while populating new site
260     */
261    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
262    public Map<String, Object> copySite (String parentId, String name, String id) throws Exception
263    {
264        Map<String, Object> result = new HashMap<>();
265        
266        if (__FORBIDDEN_SITE_NAMES.contains(name))
267        {
268            // Name is invalid
269            result.put("name", name);
270            result.put("invalid-name", true);
271        }
272        else if (_siteManager.hasSite(name))
273        {
274            // A site with same name already exists
275            result.put("name", name);
276            result.put("already-exists", true);
277        }
278        else
279        {
280            Site site = _resolver.resolveById(id);
281            
282            // Create site by copy
283            Site cSite = _siteManager.copySite(site, parentId, name);
284            cSite.saveChanges();
285            
286            // Notify observers
287            Map<String, Object> eventParams = new HashMap<>();
288            eventParams.put(ObservationConstants.ARGS_SITE, cSite);
289            _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_ADDED, _currentUserProvider.getUser(), eventParams));
290            
291            result.put("id", cSite.getId());
292            result.put("name", cSite.getName());
293        }
294        
295        return result;
296    }
297    
298    /**
299     * Delete a site
300     * @param siteId The id of site to delete
301     * @throws RepositoryException if an error occurred during deletion
302     */
303    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
304    public void deleteSite(String siteId) throws RepositoryException
305    {
306        Site site = _resolver.resolveById(siteId);
307        String siteName = site.getName();
308        String jcrPath = site.getNode().getPath().substring(1);
309        Session session = site.getNode().getSession();
310        
311        Collection<String> siteNames = _getChildrenSiteNames(site);
312        
313        site.remove();
314        session.save();
315        _siteManager.clearCache();
316        
317        _siteConfigurationManager.removeSiteConfiguration(site);
318        
319        // Notify observers of site deletion
320        Map<String, Object> eventParams = new HashMap<>();
321        eventParams.put(ObservationConstants.ARGS_SITE_ID, siteId);
322        eventParams.put(ObservationConstants.ARGS_SITE_NAME, siteName);
323        eventParams.put(ObservationConstants.ARGS_SITE_PATH, jcrPath);
324        eventParams.put(ObservationConstants.ARGS_SITE_CHILDREN, siteNames.toArray(new String[siteNames.size()]));
325        _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_DELETED, _currentUserProvider.getUser(), eventParams));
326        
327        // Remove the links between this site and the populations and the group directories
328        String context = "/sites/" + siteName;
329        _populationContextHelper.link(context, Collections.EMPTY_LIST);
330        _groupDirectoryContextHelper.link(context, Collections.EMPTY_LIST);
331    }
332    
333    
334    /**
335     * Move sites
336     * @param targetId The target
337     * @param ids the ids of sites to move
338     * @return The result with the ids of moved sites
339     * @throws AmetysRepositoryException if an error occurs
340     * @throws RepositoryException if an error occurs
341     */
342    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
343    public Map<String, Object> moveSite (String targetId, List<String> ids) throws AmetysRepositoryException, RepositoryException
344    {
345        Map<String, Object> result = new HashMap<>();
346        List<String> movedSites = new ArrayList<>();
347        
348        ModifiableTraversableAmetysObject root = _siteManager.getRoot();
349        ModifiableTraversableAmetysObject target = _resolver.resolveById(targetId);
350        
351        for (String id : ids)
352        {
353            Site site = _resolver.resolveById(id);
354            if (!site.getParent().equals(target))
355            {
356                String oldPath = site.getNode().getPath().substring(1);
357                site.moveTo(target, true);
358                
359                // Path is modified
360                String newPath = site.getNode().getPath().substring(1);
361                
362                if (root.needsSave())
363                {
364                    root.saveChanges();
365                }
366                
367                // Notify observers
368                Map<String, Object> eventParams = new HashMap<>();
369                eventParams.put(ObservationConstants.ARGS_SITE, site);
370                eventParams.put(ObservationConstants.ARGS_SITE_PATH, newPath);
371                eventParams.put(ObservationConstants.ARGS_SITE_OLD_PATH, oldPath);
372                eventParams.put(ObservationConstants.ARGS_SITE_PARENT, target);
373                _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_MOVED, _currentUserProvider.getUser(), eventParams));
374             
375                movedSites.add(site.getId());
376            }
377        }
378        
379        result.put("ids", movedSites);
380        result.put("target", targetId);
381        
382        return result;
383    }
384    
385    
386    /**
387     * Clear site's cache
388     * @param siteName The site name
389     * @throws Exception  if an error occurred during cache deletion
390     */
391    @Callable(rights = "Web_Rights_Admin_Sites", context = "/admin")
392    public void clearCache (String siteName) throws Exception
393    {
394        assert StringUtils.isEmpty(siteName);
395        
396        Site site = _siteManager.getSite(siteName);
397        assert site != null;
398        
399        clearCache(site);
400    }
401    
402    /**
403     * Clear cache of all sites.
404     * @return The list of sites which failed
405     */
406    @Callable(rights = "Web_Rights_Admin_Sites", context = "/admin")
407    public Map<String, Object> clearAllCaches ()
408    {
409        int count = 0;
410        List<String> errors = new ArrayList<>();
411        
412        AmetysObjectIterable<Site> sites = _siteManager.getSites();
413        for (Site site : sites)
414        {
415            count++;
416            try
417            {
418                clearCache(site);
419            }
420            catch (Exception e)
421            {
422                getLogger().error("Unable to clear cache of site " + site.getName(), e);
423                errors.add(site.getName());
424            }
425        }
426        
427        return Map.of(
428            "errors", errors,
429            "count", count,
430            "front", _foCommHelper.getFrontURLS().length
431        );
432    }
433    
434    /**
435     * Clear cache of a site
436     * @param site the site
437     * @throws Exception if an error occurred
438     */
439    public void clearCache (Site site) throws Exception
440    {
441        String siteName = site.getName();
442        
443        if (getLogger().isInfoEnabled())
444        {
445            getLogger().info("Clearing cache for site " + siteName);
446        }
447        
448        _foCommHelper.invalidateFOCacheImmediately(siteName);
449        _inputDataCache.clear(null, siteName);
450        _zoneItemCache.clear(null, siteName);
451    }
452    
453    /**
454     * Configure site
455     * @param siteName The site name.
456     * @param values the configuration's values
457     * @return The result map. Contains the possible errors
458     * @throws Exception if an error occurred
459     */
460    @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin")
461    public Map<String, Object> configureSite (String siteName, Map<String, Object> values) throws Exception
462    {
463        Map<String, Object> result = new HashMap<>();
464                
465        Site site = _siteManager.getSite(siteName);
466        
467        // Site updating event
468        Map<String, Object> eventParams = new HashMap<>();
469        eventParams.put(ObservationConstants.ARGS_SITE, site);
470        _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATING, _currentUserProvider.getUser(), eventParams));
471        
472        Map<String, List<I18nizableText>> errors = _setParameterValues(site, values);
473        
474        if (!errors.isEmpty())
475        {
476            List<Map<String, Object>> allErrors = new ArrayList<>();
477            
478            for (Map.Entry<String, List<I18nizableText>> entry : errors.entrySet())
479            {
480                Map<String, Object> error = new HashMap<>();
481                
482                error.put("name", entry.getKey());
483                error.put("errorMessages", entry.getValue());
484                
485                allErrors.add(error);
486            }
487            
488            result.put("errors", allErrors);
489            return result;
490        }
491        
492        if (values.containsKey("lang"))
493        {
494            @SuppressWarnings("unchecked")
495            List<String> codes = (List<String>) values.get("lang");
496            setLanguages(site, codes);
497        }
498        
499        site.getNode().getSession().save();
500        
501        // Reload this site's configuration.
502        _siteConfigurationManager.reloadSiteConfiguration(site);
503        
504        // Site updated event
505        _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams));
506        
507        clearCache(site);
508        
509        return result;
510    }
511    
512    /**
513     * Set the languages of a site
514     * @param site The site to edit
515     * @param codes The list of new codes. Such as "fr", "en".
516     */
517    public void setLanguages(Site site, List<String> codes)
518    {
519        Map<String, Object> eventParams = new HashMap<>();
520        eventParams.put(ObservationConstants.ARGS_SITE, site);
521
522        for (Sitemap sitemap : site.getSitemaps())
523        {
524            String sitemapName = sitemap.getName();
525            
526            if (!codes.contains(sitemapName))
527            {
528                sitemap.remove();
529                
530                eventParams.put(ObservationConstants.ARGS_SITEMAP_NAME, sitemapName);
531                _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_DELETED, _currentUserProvider.getUser(), eventParams));
532            }
533        }
534        
535        for (String code : codes)
536        {
537            if (!site.hasSitemap(code))
538            {
539                Sitemap sitemap = site.addSitemap(code);
540                
541                eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap);
542                _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_ADDED, _currentUserProvider.getUser(), eventParams));
543            }
544        }
545    }
546    
547    /**
548     * Set the site parameters
549     * @param site the site
550     * @param values the parameters' values
551     * @return the parameters' errors
552     */
553    protected Map<String, List<I18nizableText>> _setParameterValues(Site site, Map<String, Object> values)
554    {
555        String siteTypeId = site.getType();
556        SiteType siteType = _siteTypesEP.getExtension(siteTypeId);
557        
558        Map<String, Object> typedValues = new HashMap<>();
559        for (ElementDefinition definition: siteType.getModelItems())
560        {
561            // TODO WORKSPACES-566: the filter to ignore site's illustration should be remove when this parameter is managed like the other ones
562            if (!Site.ILLUSTRATION_PARAMETER.equals(definition.getName()))
563            {
564                Object typedValue = _getTypedValue(values, definition);
565                typedValues.put(definition.getName(), typedValue); // Unable to use streams because typedVaue can be null
566            }
567        }
568        
569        Map<String, List<I18nizableText>> allErrors = new HashMap<>();
570        for (String parameterName : typedValues.keySet())
571        {
572            Object value = typedValues.get(parameterName);
573            if (!(value instanceof UntouchedValue) && !"lang".equals(parameterName))
574            {
575                List<I18nizableText> errors = _setParameterValue(site, typedValues, siteType.getModelItem(parameterName));
576                if (!errors.isEmpty())
577                {
578                    allErrors.put(parameterName, errors);
579                }
580            }
581        }
582        return allErrors;
583    }
584    
585    private Object _getTypedValue(Map<String, Object> jsonValues, ElementDefinition definition)
586    {
587        Object jsonValue = jsonValues.get(definition.getName());
588        ElementType parameterType = definition.getType();
589        return parameterType.fromJSONForClient(jsonValue, DataContext.newInstance().withDataPath(definition.getName()));
590    }
591
592    private List<I18nizableText> _setParameterValue(Site site, Map<String, Object> values, ElementDefinition definition)
593    {
594        boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values);
595        boolean isDisabled = _disableConditionsEvaluator.evaluateDisableConditions(definition, definition.getName(), Optional.empty(), values, site, new HashMap<>());
596        
597        List<I18nizableText> errors = new ArrayList<>();
598        if (isGroupSwitchOn  && !isDisabled)
599        {
600            Object value = values.get(definition.getName());
601            
602            ValidationResult validationResult = ModelHelper.validateValue(definition, value);
603            if (validationResult.hasErrors())
604            {
605                errors.addAll(validationResult.getErrors());
606            }
607            else
608            {
609                site.setValue(definition.getName(), value);
610            }
611        }
612        
613        return errors;
614    }
615    
616    /**
617     * Get all children site's names of a site
618     * @param site The site
619     * @return the children site's names.
620     */
621    private Collection<String> _getChildrenSiteNames (Site site)
622    {
623        ArrayList<String> result = new ArrayList<>();
624        
625        result.add(site.getName());
626        
627        AmetysObjectIterable<Site> sites = site.getChildrenSites();
628        for (Site child : sites)
629        {
630            result.addAll(_getChildrenSiteNames(child));
631        }
632        
633        return result;
634    }
635}