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