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