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