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