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