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.plugins.linkdirectory.link;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import javax.jcr.RepositoryException;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.logger.AbstractLogEnabled;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.cocoon.Constants;
036import org.apache.cocoon.components.ContextHelper;
037import org.apache.cocoon.environment.Context;
038import org.apache.commons.lang3.StringUtils;
039
040import org.ametys.cms.data.Binary;
041import org.ametys.core.observation.Event;
042import org.ametys.core.observation.ObservationManager;
043import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
044import org.ametys.core.right.RightManager;
045import org.ametys.core.ui.Callable;
046import org.ametys.core.upload.Upload;
047import org.ametys.core.upload.UploadManager;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.core.util.JSONUtils;
051import org.ametys.plugins.explorer.ObservationConstants;
052import org.ametys.plugins.explorer.resources.Resource;
053import org.ametys.plugins.linkdirectory.DirectoryEvents;
054import org.ametys.plugins.linkdirectory.DirectoryHelper;
055import org.ametys.plugins.linkdirectory.Link;
056import org.ametys.plugins.linkdirectory.Link.LinkStatus;
057import org.ametys.plugins.linkdirectory.Link.LinkType;
058import org.ametys.plugins.linkdirectory.LinkDirectoryColorsComponent;
059import org.ametys.plugins.linkdirectory.dynamic.DynamicInformationProvider;
060import org.ametys.plugins.linkdirectory.dynamic.DynamicInformationProviderExtensionPoint;
061import org.ametys.plugins.linkdirectory.repository.DefaultLink;
062import org.ametys.plugins.linkdirectory.repository.DefaultLinkFactory;
063import org.ametys.plugins.repository.AmetysObject;
064import org.ametys.plugins.repository.AmetysObjectIterable;
065import org.ametys.plugins.repository.AmetysObjectResolver;
066import org.ametys.plugins.repository.AmetysRepositoryException;
067import org.ametys.plugins.repository.ModifiableAmetysObject;
068import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
069import org.ametys.plugins.repository.TraversableAmetysObject;
070import org.ametys.plugins.repository.UnknownAmetysObjectException;
071import org.ametys.plugins.repository.jcr.NameHelper;
072import org.ametys.runtime.i18n.I18nizableText;
073import org.ametys.web.repository.page.Page;
074import org.ametys.web.repository.site.Site;
075import org.ametys.web.repository.site.SiteManager;
076
077/**
078 * DAO for manipulating {@link Link}
079 */
080public class LinkDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
081{
082    /** Avalon Role */
083    public static final String ROLE = LinkDAO.class.getName();
084    
085    private AmetysObjectResolver _resolver;
086    
087    private ObservationManager _observationManager;
088    private SiteManager _siteManager;
089    private CurrentUserProvider _currentUserProvider;
090    private UploadManager _uploadManager;
091    private RightManager _rightManager;
092    private ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageEP;
093
094    private JSONUtils _jsonUtils;
095    private DirectoryHelper _directoryHelper;
096    
097    private org.apache.avalon.framework.context.Context _context;
098    private Context _cocoonContext;
099
100    private DynamicInformationProviderExtensionPoint _dynamicInfoExtensionPoint;
101    
102    private LinkDirectoryColorsComponent _colorComponent;
103    
104    @Override
105    public void service(ServiceManager manager) throws ServiceException
106    {
107        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
108        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
109        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
110        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
111        _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE);
112        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
113        _directoryHelper = (DirectoryHelper) manager.lookup(DirectoryHelper.ROLE);
114        _dynamicInfoExtensionPoint = (DynamicInformationProviderExtensionPoint) manager.lookup(DynamicInformationProviderExtensionPoint.ROLE);
115        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
116        _profileAssignmentStorageEP = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
117        _colorComponent = (LinkDirectoryColorsComponent) manager.lookup(LinkDirectoryColorsComponent.ROLE);
118    }
119    
120    @Override
121    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
122    {
123        _context = context;
124        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
125    }
126    
127    /**
128     * Create a new link
129     * @param parameters a map of the following parameters for the link : siteName, language, url, title, content, url-alternative, picture, picture#type, picture-alternative, themes, grant-any-user, fousers, fogroups
130     * @return The new link id
131     */
132    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
133    public Map<String, Object> createLink(Map<String, Object> parameters)
134    {
135        Map<String, Object> result = new HashMap<>();
136        
137        String siteName = (String) parameters.get("siteName");
138        String language = (String) parameters.get("lang");
139        Site site = _siteManager.getSite(siteName);
140
141        String url = StringUtils.defaultString((String) parameters.get("url"));
142        String internalUrl = StringUtils.defaultString((String) parameters.get("internal-url"));
143        
144        // Check that the word doesn't already exist.
145        if (_urlExists(url, internalUrl, siteName, language))
146        {
147            result.put("already-exists", true);
148            return result;
149        }
150
151        ModifiableTraversableAmetysObject rootNode = _directoryHelper.getLinksNode(site, language);
152        
153        String name = url;
154        if (StringUtils.isBlank(name))
155        {
156            name = internalUrl;
157        }
158        DefaultLink link = _createLink(name, rootNode);
159        _setValues(link, parameters);
160        
161        rootNode.saveChanges();
162        
163        // Notify listeners
164        Map<String, Object> eventParams = new HashMap<>();
165        eventParams.put(ObservationConstants.ARGS_ID, link.getId());
166        eventParams.put(ObservationConstants.ARGS_PARENT_ID, rootNode.getId());
167        eventParams.put(ObservationConstants.ARGS_NAME, link.getName());
168        eventParams.put(ObservationConstants.ARGS_PATH, link.getPath());
169
170        _observationManager.notify(new Event(DirectoryEvents.LINK_CREATED, _currentUserProvider.getUser(), eventParams));
171        
172        // Set public access
173        _setAccess(link, null);
174        
175        return convertLink2JsonObject(link);
176    }
177    
178    /**
179     * Create a new user link
180     * @param parameters a map of the following parameters for the link : siteName, language, url, title, content, url-alternative, picture, picture#type, picture-alternative
181     * @return The new link id
182     */
183    public Map<String, Object> createUserLink(Map<String, Object> parameters)
184    {
185        Map<String, Object> result = new HashMap<>();
186        
187        UserIdentity currentUser = _currentUserProvider.getUser();
188        if (currentUser == null)
189        {
190            result.put("unauthenticated-user", true);
191            return result;
192        }
193        
194        String siteName = (String) parameters.get("siteName");
195        String language = (String) parameters.get("lang");
196        Site site = _siteManager.getSite(siteName);
197
198        String url = StringUtils.defaultString((String) parameters.get("url"));
199        String internalUrl = StringUtils.defaultString((String) parameters.get("internal-url"));
200
201        // Check that the word doesn't already exist for the given user.
202        if (_urlExistsForUser(url, internalUrl, siteName, language, currentUser))
203        {
204            result.put("already-exists", true);
205            return result;
206        }
207        
208        ModifiableTraversableAmetysObject rootNodeForUser = _directoryHelper.getLinksForUserNode(site, language, currentUser);
209        
210        String name = url;
211        if (StringUtils.isBlank(name))
212        {
213            name = internalUrl;
214        }
215        DefaultLink link = _createLink(name, rootNodeForUser);
216        _setValues(link, parameters);
217        
218        rootNodeForUser.saveChanges();
219
220        // Notify listeners
221        Map<String, Object> eventParams = new HashMap<>();
222        eventParams.put(ObservationConstants.ARGS_ID, link.getId());
223        eventParams.put(ObservationConstants.ARGS_PARENT_ID, rootNodeForUser.getId());
224        eventParams.put(ObservationConstants.ARGS_NAME, link.getName());
225        eventParams.put(ObservationConstants.ARGS_PATH, link.getPath());
226
227        _observationManager.notify(new Event(DirectoryEvents.LINK_CREATED, _currentUserProvider.getUser(), eventParams));
228        
229        // Set public access
230        _setAccess(link, currentUser);
231        
232        return convertLink2JsonObject(link);
233    }
234    
235    /**
236     * Indicate if a link is a user link
237     * @param link the link
238     * @return true if the link is a user link
239     */
240    public boolean isUserLink(DefaultLink link)
241    {
242        AmetysObject parent = link.getParent();
243        AmetysObject linksNode = _directoryHelper.getLinksNode(link.getSite(), link.getLanguage());
244        return !parent.equals(linksNode);
245    }
246
247    
248    /**
249     * Updates a link
250     * @param parameters a map of the following parameters for the link : siteName, language, id, url, title, content, url-alternative, picture, picture#type, picture-alternative, themes, grant-any-user, fousers, fogroups
251     * @return the update link
252     */
253    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
254    public Map<String, Object> updateLink(Map<String, Object> parameters)
255    {
256        Map<String, Object> result = new HashMap<>();
257        
258        String siteName = (String) parameters.get("siteName");
259        String language = (String) parameters.get("language");
260        String id = StringUtils.defaultString((String) parameters.get("id"));
261        String url = StringUtils.defaultString((String) parameters.get("url"));
262        String internalUrl = StringUtils.defaultString((String) parameters.get("internal-url"));
263        
264        try
265        {
266            DefaultLink link = _resolver.resolveById(id);
267            
268            // If the url was changed, check that the new url doesn't already exist.
269            if (!link.getUrl().equals(url) && _urlExists(url, internalUrl, siteName, language))
270            {
271                result.put("already-exists", true);
272                return result;
273            }
274            
275            _setValues(link, parameters);
276            link.saveChanges();
277            
278            // Notify listeners
279            Map<String, Object> eventParams = new HashMap<>();
280            eventParams.put(ObservationConstants.ARGS_ID, link.getId());
281            eventParams.put(ObservationConstants.ARGS_NAME, link.getName());
282            eventParams.put(ObservationConstants.ARGS_PATH, link.getPath());
283    
284            _observationManager.notify(new Event(DirectoryEvents.LINK_MODIFIED, _currentUserProvider.getUser(), eventParams));
285            
286            return convertLink2JsonObject(link);
287        }
288        catch (UnknownAmetysObjectException e)
289        {
290            result.put("unknown-link", true);
291            return result;
292        }
293        catch (AmetysRepositoryException e)
294        {
295            throw new IllegalStateException(e);
296        }
297    }
298    
299    @SuppressWarnings("unchecked")
300    private void _setValues(Link link, Map<String, Object> values)
301    {
302        // Set values (url, type, title, etc.)
303        _setLinkValues(link, values);
304        
305        // Set themes
306        List<String> themes = Collections.EMPTY_LIST;
307        Object themesFromValues = values.get("themes");
308        if (themesFromValues instanceof List)
309        {
310            themes = (List<String>) themesFromValues;
311        }
312        else if (themesFromValues instanceof String)
313        {
314            themes = _jsonUtils.convertJsonToList((String) themesFromValues).stream()
315                         .map(Object::toString)
316                         .collect(Collectors.toList());
317        }
318
319        _setThemes(link, themes);
320    }
321    
322    private void _setLinkValues(Link link, Map<String, Object> values)
323    {
324        String url = StringUtils.defaultString((String) values.get("url"));
325        String dynInfoProviderId = StringUtils.defaultString((String) values.get("dynamic-info-provider"));
326        String internalUrl = StringUtils.defaultString((String) values.get("internal-url"));
327        String urlType = StringUtils.defaultString((String) values.get("url-type"));
328        String title = StringUtils.defaultString((String) values.get("title"));
329        String content = StringUtils.defaultString((String) values.get("content"));
330        String alternative = StringUtils.defaultString((String) values.get("url-alternative"));
331        String color = StringUtils.defaultString((String) values.get("color"));
332        String pageId = StringUtils.defaultString((String) values.get("page"));
333        String status = StringUtils.defaultString((String) values.get("status"));
334        
335        String pictureAsStr = StringUtils.defaultString((String) values.get("picture"));
336        String pictureAlternative = StringUtils.defaultString((String) values.get("picture-alternative"));
337
338        // Check the dynamic provider still exists
339        if (!_dynamicInfoExtensionPoint.hasExtension(dynInfoProviderId))
340        {
341            dynInfoProviderId = "";
342        }
343
344        link.setUrl(LinkType.valueOf(urlType), url);
345        link.setDynamicInformationProvider(dynInfoProviderId);
346        link.setInternalUrl(internalUrl);
347        link.setTitle(title);
348        link.setContent(content);
349        link.setAlternative(alternative);
350        link.setPictureAlternative(pictureAlternative);
351        link.setColor(color);
352        link.setPage(pageId);
353        link.setStatus(StringUtils.isNotBlank(status) ? LinkStatus.valueOf(status) : LinkStatus.NORMAL);
354        
355        _setPicture(link, pictureAsStr);
356    }
357    
358    private void _setThemes(Link link, List<String> themes)
359    {
360        link.setThemes(themes.toArray(new String[themes.size()]));
361    }
362    
363    private void _setPicture(Link link, String valueAsStr)
364    {
365        if (StringUtils.isNotEmpty(valueAsStr))
366        {
367            Map<String, Object> picture = _jsonUtils.convertJsonToMap(valueAsStr);
368            
369            if (!picture.isEmpty())
370            {
371                String pictureType = (String) picture.get("type");
372                String value = (String) picture.get("id");
373                
374                if (pictureType.equals("explorer") && !"untouched".equals(value))
375                {
376                    link.setResourcePicture(value);
377                }
378                else if (pictureType.equals("glyph"))
379                {
380                    link.setPictureGlyph(value);
381                }
382                else if (!"untouched".equals(value))
383                {
384                    UserIdentity user = _currentUserProvider.getUser();
385                    Upload upload = _uploadManager.getUpload(user, value);
386                    
387                    String filename = upload.getFilename();
388                    String mimeType = upload.getMimeType() != null ? upload.getMimeType() : _cocoonContext.getMimeType(filename);
389                    String finalMimeType = mimeType != null ? mimeType : "application/unknown";
390                    
391                    link.setExternalPicture(finalMimeType, filename, upload.getInputStream());
392                }
393            }
394            else
395            {
396                // Remove picture
397                link.setNoPicture(); 
398            }
399            
400        }
401        else
402        {
403            // Remove picture
404            link.setNoPicture();
405        }
406    }
407    
408    /**
409     * Delete one or multiples links
410     * @param ids a list of links' ids
411     * @return true if all the links were deleted, false if at least one link could not be delete.
412     */
413    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
414    public List<String> deleteLinks(List<String> ids)
415    {
416        List<String> result = new ArrayList<>();
417        
418        for (String id : ids)
419        {
420            try
421            {
422                DefaultLink link = _resolver.resolveById(id);
423                
424                String siteName = link.getSiteName();
425                String language = link.getLanguage();
426                String linkId = link.getId();
427                String name = link.getName();
428                String path = link.getPath();
429                
430                ModifiableAmetysObject parent = link.getParent();
431                link.remove(); 
432    
433                parent.saveChanges();
434    
435                // Notify listeners
436                Map<String, Object> eventParams = new HashMap<>();
437                eventParams.put(ObservationConstants.ARGS_ID, linkId);
438                eventParams.put(ObservationConstants.ARGS_NAME, name);
439                eventParams.put(ObservationConstants.ARGS_PATH, path);
440                eventParams.put("siteName", siteName);
441                eventParams.put("language", language);
442                _observationManager.notify(new Event(DirectoryEvents.LINK_DELETED, _currentUserProvider.getUser(), eventParams));
443    
444                result.add(linkId);
445            }
446            catch (UnknownAmetysObjectException e)
447            {
448                // ignore the id and continue the deletion.
449                getLogger().error("Unable to delete the link of id '" + id + ", because it does not exist.", e);
450            }
451        }
452        
453        return result;
454    }
455    
456    /**
457     * Move a link in the list
458     * @param id the link id
459     * @param role the move action
460     * @throws RepositoryException if a repository error occurs.
461     * @return the moved link in JSON
462     */
463    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
464    public Map<String, Object> moveLink(String id, String role) throws RepositoryException
465    {
466        DefaultLink link = _resolver.resolveById(id);
467        
468        switch (role)
469        {
470            case "move-first":
471                _moveFirst(link);
472                break;
473            case "move-up":
474                _moveUp(link);
475                break;
476            case "move-down":
477                _moveDown(link);
478                break;
479            case "move-last":
480                _moveLast(link);
481                break;
482            default:
483                break;
484        }
485        
486        return convertLink2JsonObject(link);
487    }
488    
489    
490    
491    /**
492     * Get the JSON object representing a link
493     * @param id the id of link
494     * @return the link as JSON object
495     */
496    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
497    public Map<String, Object> getLink (String id)
498    {
499        DefaultLink link = _resolver.resolveById(id);
500        return convertLink2JsonObject(link);
501    }
502    
503    /**
504     * Determines if the restriction IP parameter is not empty
505     * @param siteName the site name
506     * @return true if the restriction IP parameter is not empty
507     */
508    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
509    public boolean isInternalURLAllowed (String siteName)
510    {
511        Site site = _siteManager.getSite(siteName);
512        String allowedIpParameter = site.getValue("allowed-ip");
513        return StringUtils.isNotBlank(allowedIpParameter);
514    }
515    
516    /**
517     * Returns the list of providers of dynamic information as json object
518     * @return the providers of dynamic information 
519     */
520    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
521    public List<Map<String, Object>> getDynamicInformationProviders()
522    {
523        List<Map<String, Object>> result = new ArrayList<>();
524        
525        for (String id : _dynamicInfoExtensionPoint.getExtensionsIds())
526        {
527            DynamicInformationProvider provider = _dynamicInfoExtensionPoint.getExtension(id);
528            Map<String, Object> info = new HashMap<>();
529            info.put("id", provider.getId());
530            info.put("label", provider.getLabel());
531            result.add(info);
532        }
533        
534        return result;
535    }
536    
537    /**
538     * Convert a link to JSON object
539     * @param link the link
540     * @return the link as JSON object
541     */
542    public Map<String, Object> convertLink2JsonObject (DefaultLink link)
543    {
544        Map<String, Object> infos = new HashMap<>();
545        
546        infos.put("id", link.getId());
547        infos.put("lang", link.getLanguage());
548        infos.put("url", link.getUrl());
549        infos.put("dynamicInfoProvider", link.getDynamicInformationProvider());
550        infos.put("internalUrl", link.getInternalUrl());
551        infos.put("urlType", link.getUrlType().toString());
552        
553        if (link.getUrlType() == LinkType.PAGE)
554        {
555            String pageId = link.getUrl();
556            try
557            {
558                Page page = _resolver.resolveById(pageId);
559                infos.put("pageTitle", page.getTitle());
560            }
561            catch (UnknownAmetysObjectException e)
562            {
563                infos.put("unknownPage", true);
564            }
565        }
566        
567        infos.put("title", link.getTitle());
568        infos.put("alternative", link.getAlternative());
569        
570        infos.put("content", StringUtils.defaultString(link.getContent()));
571        infos.put("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
572        
573        Map<String, Object> pictureInfos = new HashMap<>();
574        String pictureType = link.getPictureType();
575        
576        TraversableAmetysObject parent = link.getParent();
577        infos.put("position", parent.getChildPosition(link));
578        infos.put("count", parent.getChildren().getSize());
579        
580        infos.put("color", StringUtils.defaultString(link.getColor()));
581        infos.put("page", StringUtils.defaultString(link.getPage()));
582        if (link.getStatus() != null)
583        {
584            infos.put("status", link.getStatus().name());
585        }
586        
587        if (pictureType.equals("resource"))
588        {
589            String resourceId = link.getResourcePictureId();
590            pictureInfos.put("id", resourceId);
591            try
592            {
593                Resource resource = _resolver.resolveById(resourceId);
594                
595                pictureInfos.put("filename", resource.getName());
596                pictureInfos.put("size", resource.getLength());
597                pictureInfos.put("type", "explorer");
598                pictureInfos.put("lastModified", resource.getLastModified());
599                
600                String contextPath = ContextHelper.getRequest(_context).getContextPath();
601                String viewUrl = contextPath + "/plugins/explorer/resource?id=" + resource.getId();
602                String downloadUrl = viewUrl + "&download=true";
603                pictureInfos.put("viewUrl", viewUrl);
604                pictureInfos.put("downloadUrl", downloadUrl);
605            }
606            catch (UnknownAmetysObjectException e)
607            {
608                getLogger().error("The resource of id'" + resourceId + "' does not exist anymore. The picture for link of id '" + link.getId() + "' will be ignored.", e);
609                infos.put("pictureNotFound", true);
610            }
611        }
612        else if (pictureType.equals("external"))
613        {
614            Binary picMeta = link.getExternalPicture();
615            
616            pictureInfos.put("path", DefaultLink.PROPERTY_PICTURE);
617            pictureInfos.put("filename", picMeta.getFilename());
618            pictureInfos.put("size", picMeta.getLength());
619            pictureInfos.put("lastModified", picMeta.getLastModificationDate());
620            pictureInfos.put("type", "metadata");
621            
622            String contextPath = ContextHelper.getRequest(_context).getContextPath();
623            String viewUrl = contextPath + "/plugins/cms/binary/" + DefaultLink.PROPERTY_PICTURE + "?objectId=" + link.getId();
624            String downloadUrl = viewUrl + "&download=true";
625            pictureInfos.put("viewUrl", viewUrl);
626            pictureInfos.put("downloadUrl", downloadUrl);
627            
628        }
629        else if (pictureType.equals("glyph"))
630        {
631            pictureInfos.put("id", link.getPictureGlyph());
632            pictureInfos.put("type", "glyph");
633        }
634        infos.put("picture", pictureInfos);
635        
636        infos.put("isRestricted", !_rightManager.hasAnonymousReadAccess(link)); 
637        
638        // Themes
639        List<Map<String, Object>> themesList = new ArrayList<>();
640        for (String themeId : link.getThemes())
641        {
642            try
643            {
644                I18nizableText themeTitle = _directoryHelper.getThemeTitle(themeId, link.getSiteName(), link.getLanguage());
645                Map<String, Object> themeData = new HashMap<>();
646                themeData.put("id", themeId);
647                themeData.put("label", themeTitle);
648                themesList.add(themeData);
649            }
650            catch (UnknownAmetysObjectException e)
651            {
652                // Theme does not exist anymore
653            }
654        }
655        
656        infos.put("themes", themesList);
657        
658        return infos;
659    }
660    
661    /**
662     * Get links infos
663     * @param linkIds the link id
664     * @return the link infos
665     */
666    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
667    public List<Map<String, Object>> getLinks(List<String> linkIds)
668    {
669        List<Map<String, Object>> result = new ArrayList<>();
670        
671        for (String linkId: linkIds)
672        {
673            try
674            {
675                result.add(getLink(linkId));
676            }
677            catch (UnknownAmetysObjectException e)
678            {
679                // does not exists
680            }
681        }
682        return result; 
683    }
684    
685    /**
686     * Test if a link with the specified url or internal url exists in the directory.
687     * @param url the url to test.
688     * @param internalUrl the internal url to test
689     * @param siteName the site name.
690     * @param language the language.
691     * @return true if the link exists.
692     * @throws AmetysRepositoryException if a repository error occurs.
693     */
694    protected boolean _urlExists(String url, String internalUrl, String siteName, String language) throws AmetysRepositoryException
695    {
696        boolean externalLinkExists = false;
697        if (StringUtils.isNotBlank(url))
698        {
699            String externalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, url);
700            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
701            {
702                externalLinkExists = externalLinks.iterator().hasNext();
703            }
704        }
705        
706        boolean internalLinkExists = false;
707        if (StringUtils.isNotBlank(internalUrl))
708        {
709            String internalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, internalUrl);
710            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
711            {
712                internalLinkExists = internalLinks.iterator().hasNext();
713            }
714        }
715        
716        return externalLinkExists || internalLinkExists;
717    }
718    
719    /**
720     * Test if a link with the specified url or internal url exists in the directory for the given user.
721     * @param url the url to test.
722     * @param internalUrl the internal url to test
723     * @param siteName the site name.
724     * @param language the language.
725     * @param user The user identity
726     * @return true if the link exists for the given user.
727     * @throws AmetysRepositoryException if a repository error occurs.
728     */
729    protected boolean _urlExistsForUser(String url, String internalUrl, String siteName, String language, UserIdentity user) throws AmetysRepositoryException
730    {
731        boolean externalLinkExists = false;
732        if (StringUtils.isNotBlank(url))
733        {
734            String externalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, url, user);
735            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
736            {
737                externalLinkExists = externalLinks.iterator().hasNext();
738            }
739        }
740        
741        boolean internalLinkExists = false;
742        if (StringUtils.isNotBlank(internalUrl))
743        {
744            String internalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, internalUrl, user);
745            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
746            {
747                internalLinkExists = internalLinks.iterator().hasNext();
748            }
749        }
750        
751        return externalLinkExists || internalLinkExists;
752    }
753    
754    /**
755     * Create the link object.
756     * @param name the desired link name.
757     * @param rootNode the links root node.
758     * @return the created Link.
759     */
760    protected DefaultLink _createLink(String name, ModifiableTraversableAmetysObject rootNode)
761    {
762        String originalName = NameHelper.filterName(name);
763        
764        // Find unique name
765        String uniqueName = originalName;
766        int index = 2;
767        while (rootNode.hasChild(uniqueName))
768        {
769            uniqueName = originalName + "-" + (index++);
770        }
771        
772        return rootNode.createChild(uniqueName, DefaultLinkFactory.LINK_NODE_TYPE);
773    }
774    
775    /**
776     * Move link the first position
777     * @param link the link to move
778     * @throws RepositoryException if an error occurs while exploring the repository
779     */
780    private void _moveFirst(DefaultLink link) throws RepositoryException
781    {
782        try (AmetysObjectIterable<AmetysObject>  children = ((TraversableAmetysObject) link.getParent()).getChildren();)
783        {
784            // Resolve the link in the same session or the linkRoot.saveChanges() call below won't see the order changes.
785            link.orderBefore(((TraversableAmetysObject) link.getParent()).getChildren().iterator().next());
786            ((ModifiableAmetysObject) link.getParent()).saveChanges();
787        }
788    }
789
790    /**
791     * Move link after its following
792     * @param link the link to move down
793     * @throws RepositoryException if an error occurs while exploring the repository
794     */
795    private void _moveDown(DefaultLink link) throws RepositoryException
796    {
797        TraversableAmetysObject parentNode = link.getParent();
798        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
799        Iterator<AmetysObject> it = siblings.iterator();
800        
801        boolean iterate = true;
802        while (it.hasNext() && iterate)
803        {
804            DefaultLink sibling = (DefaultLink) it.next();
805            iterate = !sibling.getName().equals(link.getName());
806        }
807        
808        if (it.hasNext())
809        {
810            // Move the link after his next sibling: move the next sibling before the link to move.
811            DefaultLink nextLink = (DefaultLink) it.next();
812            nextLink.orderBefore(link);
813    
814            link.saveChanges();
815        }
816    }
817    
818    /**
819     * Move link before its preceding
820     * @param link the link to move up
821     * @throws RepositoryException if an error occurs while exploring the repository
822     */
823    private void _moveUp(DefaultLink link) throws RepositoryException
824    {
825        TraversableAmetysObject parentNode = link.getParent();
826        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
827        Iterator<AmetysObject> it = siblings.iterator();
828        DefaultLink previousLink = null;
829
830        while (it.hasNext())
831        {
832            DefaultLink sibling = (DefaultLink) it.next();
833            if (sibling.getName().equals(link.getName()))
834            {
835                break;
836            }
837            
838            previousLink = sibling;
839        }
840            
841        if (previousLink != null)
842        {
843            // Move the link after his next sibling: move the next sibling before the link to move.
844            link.orderBefore(previousLink);
845            link.saveChanges();
846        }
847    }
848    
849    /**
850     * Move link to the last position.
851     * @param link the link to move up
852     * @throws RepositoryException if an error occurs while exploring the repository
853     */
854    private void _moveLast(DefaultLink link) throws RepositoryException
855    {
856        link.moveTo(link.getParent(), false);
857        ((ModifiableAmetysObject) link.getParent()).saveChanges();
858    }
859    
860    /**
861     * Set access to the link
862     * @param link the link
863     * @param user the user to set access. Can be null, in this case, we set anonymous right
864     */
865    private void _setAccess (Link link, UserIdentity user)
866    {
867        if (user != null)
868        {
869            _profileAssignmentStorageEP.allowProfileToUser(user, RightManager.READER_PROFILE_ID, link);
870        }
871        else
872        {
873            _profileAssignmentStorageEP.allowProfileToAnonymous(RightManager.READER_PROFILE_ID, link);
874        }
875        
876        Map<String, Object> eventParams = new HashMap<>();
877        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, link);
878        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
879        
880        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
881    }
882    
883    /**
884     * Retrieves the color of the link.
885     * If there is no color configured on the link, the default site color
886     * @param link the link
887     * @return the color of the link
888     */
889    public String getLinkColor(DefaultLink link)
890    {
891        Map<String, Map<String, String>> colors = _colorComponent.getColors(link.getSiteName());
892        if (colors.containsKey(link.getColor()))
893        {
894            return colors.get(link.getColor()).get("main");
895        }
896        else
897        {
898            return colors.get(_colorComponent.getDefaultKey(link.getSiteName())).get("main");
899        }
900    }
901}