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