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