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