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