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                String siteName = link.getSiteName();
449                String language = link.getLanguage();
450                String linkId = link.getId();
451                String name = link.getName();
452                String path = link.getPath();
453                
454                ModifiableAmetysObject parent = link.getParent();
455                link.remove(); 
456    
457                parent.saveChanges();
458    
459                // Notify listeners
460                Map<String, Object> eventParams = new HashMap<>();
461                eventParams.put(ObservationConstants.ARGS_ID, linkId);
462                eventParams.put(ObservationConstants.ARGS_NAME, name);
463                eventParams.put(ObservationConstants.ARGS_PATH, path);
464                eventParams.put("siteName", siteName);
465                eventParams.put("language", language);
466                _observationManager.notify(new Event(DirectoryEvents.LINK_DELETED, _currentUserProvider.getUser(), eventParams));
467    
468                result.add(linkId);
469            }
470            catch (UnknownAmetysObjectException e)
471            {
472                // ignore the id and continue the deletion.
473                getLogger().error("Unable to delete the link of id '" + id + ", because it does not exist.", e);
474            }
475        }
476        
477        return result;
478    }
479    
480    /**
481     * Move a link in the list
482     * @param id the link id
483     * @param role the move action
484     * @throws RepositoryException if a repository error occurs.
485     * @return the moved link in JSON
486     */
487    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
488    public Map<String, Object> moveLink(String id, String role) throws RepositoryException
489    {
490        DefaultLink link = _resolver.resolveById(id);
491        
492        switch (role)
493        {
494            case "move-first":
495                _moveFirst(link);
496                break;
497            case "move-up":
498                _moveUp(link);
499                break;
500            case "move-down":
501                _moveDown(link);
502                break;
503            case "move-last":
504                _moveLast(link);
505                break;
506            default:
507                break;
508        }
509        
510        return convertLink2JsonObject(link);
511    }
512    
513    
514    
515    /**
516     * Get the JSON object representing a link
517     * @param id the id of link
518     * @return the link as JSON object
519     */
520    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
521    public Map<String, Object> getLink (String id)
522    {
523        DefaultLink link = _resolver.resolveById(id);
524        return convertLink2JsonObject(link);
525    }
526    
527    /**
528     * Determines if the restriction IP parameter is not empty
529     * @param siteName the site name
530     * @return true if the restriction IP parameter is not empty
531     */
532    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
533    public boolean isInternalURLAllowed (String siteName)
534    {
535        Site site = _siteManager.getSite(siteName);
536        String allowedIpParameter = site.getValue("allowed-ip");
537        return StringUtils.isNotBlank(allowedIpParameter);
538    }
539    
540    /**
541     * Returns the list of providers of dynamic information as json object
542     * @return the providers of dynamic information 
543     */
544    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
545    public List<Map<String, Object>> getDynamicInformationProviders()
546    {
547        List<Map<String, Object>> result = new ArrayList<>();
548        
549        for (String id : _dynamicInfoExtensionPoint.getExtensionsIds())
550        {
551            DynamicInformationProvider provider = _dynamicInfoExtensionPoint.getExtension(id);
552            Map<String, Object> info = new HashMap<>();
553            info.put("id", provider.getId());
554            info.put("label", provider.getLabel());
555            result.add(info);
556        }
557        
558        return result;
559    }
560    
561    /**
562     * Convert a link to JSON object
563     * @param link the link
564     * @return the link as JSON object
565     */
566    public Map<String, Object> convertLink2JsonObject (DefaultLink link)
567    {
568        Map<String, Object> infos = new HashMap<>();
569        
570        infos.put("id", link.getId());
571        infos.put("lang", link.getLanguage());
572        infos.put("url", link.getUrl());
573        infos.put("dynamicInfoProvider", link.getDynamicInformationProvider());
574        infos.put("internalUrl", link.getInternalUrl());
575        infos.put("urlType", link.getUrlType().toString());
576        
577        if (link.getUrlType() == LinkType.PAGE)
578        {
579            String pageId = link.getUrl();
580            try
581            {
582                Page page = _resolver.resolveById(pageId);
583                infos.put("pageTitle", page.getTitle());
584            }
585            catch (UnknownAmetysObjectException e)
586            {
587                infos.put("unknownPage", true);
588            }
589        }
590        
591        infos.put("title", link.getTitle());
592        infos.put("alternative", link.getAlternative());
593        
594        infos.put("content", StringUtils.defaultString(link.getContent()));
595        infos.put("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
596        
597        Map<String, Object> pictureInfos = new HashMap<>();
598        String pictureType = link.getPictureType();
599        
600        TraversableAmetysObject parent = link.getParent();
601        infos.put("position", parent.getChildPosition(link));
602        infos.put("count", parent.getChildren().getSize());
603        
604        infos.put("color", StringUtils.defaultString(link.getColor()));
605        infos.put("page", StringUtils.defaultString(link.getPage()));
606        if (link.getStatus() != null)
607        {
608            infos.put("status", link.getStatus().name());
609        }
610        
611        if (pictureType.equals("resource"))
612        {
613            String resourceId = link.getResourcePictureId();
614            pictureInfos.put("id", resourceId);
615            try
616            {
617                Resource resource = _resolver.resolveById(resourceId);
618                
619                pictureInfos.put("filename", resource.getName());
620                pictureInfos.put("size", resource.getLength());
621                pictureInfos.put("type", "explorer");
622                pictureInfos.put("lastModified", resource.getLastModified());
623                
624                String viewUrl = ResolveURIComponent.resolve("explorer", resourceId, false);
625                String downloadUrl = ResolveURIComponent.resolve("explorer", resourceId, true);
626                pictureInfos.put("viewUrl", viewUrl);
627                pictureInfos.put("downloadUrl", downloadUrl);
628            }
629            catch (UnknownAmetysObjectException e)
630            {
631                getLogger().error("The resource of id'" + resourceId + "' does not exist anymore. The picture for link of id '" + link.getId() + "' will be ignored.", e);
632                infos.put("pictureNotFound", true);
633            }
634        }
635        else if (pictureType.equals("external"))
636        {
637            Binary picMeta = link.getExternalPicture();
638            
639            pictureInfos.put("path", DefaultLink.PROPERTY_PICTURE);
640            pictureInfos.put("filename", picMeta.getFilename());
641            pictureInfos.put("size", picMeta.getLength());
642            pictureInfos.put("lastModified", picMeta.getLastModificationDate());
643            pictureInfos.put("type", "link-data");
644            
645            String viewUrl = ResolveURIComponent.resolve("link-data", DefaultLink.PROPERTY_PICTURE + "?objectId=" + link.getId(), false);
646            String downloadUrl = ResolveURIComponent.resolve("link-data", DefaultLink.PROPERTY_PICTURE + "?objectId=" + link.getId(), true);
647            
648            pictureInfos.put("viewUrl", viewUrl);
649            pictureInfos.put("downloadUrl", downloadUrl);
650            
651        }
652        else if (pictureType.equals("glyph"))
653        {
654            pictureInfos.put("id", link.getPictureGlyph());
655            pictureInfos.put("type", "glyph");
656        }
657        infos.put("picture", pictureInfos);
658        
659        infos.put("isRestricted", !_rightManager.hasAnonymousReadAccess(link)); 
660        
661        // Themes
662        List<Map<String, Object>> themesList = new ArrayList<>();
663        for (String themeId : link.getThemes())
664        {
665            try
666            {
667                I18nizableText themeTitle = _directoryHelper.getThemeTitle(themeId, link.getSiteName(), link.getLanguage());
668                Map<String, Object> themeData = new HashMap<>();
669                themeData.put("id", themeId);
670                themeData.put("label", themeTitle);
671                themesList.add(themeData);
672            }
673            catch (UnknownAmetysObjectException e)
674            {
675                // Theme does not exist anymore
676            }
677        }
678        
679        infos.put("themes", themesList);
680        
681        return infos;
682    }
683    
684    /**
685     * Get links infos
686     * @param linkIds the link id
687     * @return the link infos
688     */
689    @Callable (rights = "LinkDirectory_Rights_Links_Handle")
690    public List<Map<String, Object>> getLinks(List<String> linkIds)
691    {
692        List<Map<String, Object>> result = new ArrayList<>();
693        
694        for (String linkId: linkIds)
695        {
696            try
697            {
698                result.add(getLink(linkId));
699            }
700            catch (UnknownAmetysObjectException e)
701            {
702                // does not exists
703            }
704        }
705        return result; 
706    }
707    
708    /**
709     * Test if a link with the specified url or internal url exists in the directory.
710     * @param url the url to test.
711     * @param internalUrl the internal url to test
712     * @param siteName the site name.
713     * @param language the language.
714     * @return true if the link exists.
715     * @throws AmetysRepositoryException if a repository error occurs.
716     */
717    protected boolean _urlExists(String url, String internalUrl, String siteName, String language) throws AmetysRepositoryException
718    {
719        boolean externalLinkExists = false;
720        if (StringUtils.isNotBlank(url))
721        {
722            String externalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, url);
723            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
724            {
725                externalLinkExists = externalLinks.iterator().hasNext();
726            }
727        }
728        
729        boolean internalLinkExists = false;
730        if (StringUtils.isNotBlank(internalUrl))
731        {
732            String internalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, internalUrl);
733            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
734            {
735                internalLinkExists = internalLinks.iterator().hasNext();
736            }
737        }
738        
739        return externalLinkExists || internalLinkExists;
740    }
741    
742    /**
743     * Test if a link with the specified url or internal url exists in the directory for the given user.
744     * @param url the url to test.
745     * @param internalUrl the internal url to test
746     * @param siteName the site name.
747     * @param language the language.
748     * @param user The user identity
749     * @return true if the link exists for the given user.
750     * @throws AmetysRepositoryException if a repository error occurs.
751     */
752    protected boolean _urlExistsForUser(String url, String internalUrl, String siteName, String language, UserIdentity user) throws AmetysRepositoryException
753    {
754        boolean externalLinkExists = false;
755        if (StringUtils.isNotBlank(url))
756        {
757            String externalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, url, user);
758            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
759            {
760                externalLinkExists = externalLinks.iterator().hasNext();
761            }
762        }
763        
764        boolean internalLinkExists = false;
765        if (StringUtils.isNotBlank(internalUrl))
766        {
767            String internalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, internalUrl, user);
768            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
769            {
770                internalLinkExists = internalLinks.iterator().hasNext();
771            }
772        }
773        
774        return externalLinkExists || internalLinkExists;
775    }
776    
777    /**
778     * Create the link object.
779     * @param name the desired link name.
780     * @param rootNode the links root node.
781     * @return the created Link.
782     */
783    protected DefaultLink _createLink(String name, ModifiableTraversableAmetysObject rootNode)
784    {
785        String originalName = NameHelper.filterName(name);
786        
787        // Find unique name
788        String uniqueName = originalName;
789        int index = 2;
790        while (rootNode.hasChild(uniqueName))
791        {
792            uniqueName = originalName + "-" + (index++);
793        }
794        
795        return rootNode.createChild(uniqueName, DefaultLinkFactory.LINK_NODE_TYPE);
796    }
797    
798    /**
799     * Move link the first position
800     * @param link the link to move
801     * @throws RepositoryException if an error occurs while exploring the repository
802     */
803    private void _moveFirst(DefaultLink link) throws RepositoryException
804    {
805        try (AmetysObjectIterable<AmetysObject>  children = ((TraversableAmetysObject) link.getParent()).getChildren();)
806        {
807            // Resolve the link in the same session or the linkRoot.saveChanges() call below won't see the order changes.
808            link.orderBefore(((TraversableAmetysObject) link.getParent()).getChildren().iterator().next());
809            ((ModifiableAmetysObject) link.getParent()).saveChanges();
810        }
811    }
812
813    /**
814     * Move link after its following
815     * @param link the link to move down
816     * @throws RepositoryException if an error occurs while exploring the repository
817     */
818    private void _moveDown(DefaultLink link) throws RepositoryException
819    {
820        TraversableAmetysObject parentNode = link.getParent();
821        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
822        Iterator<AmetysObject> it = siblings.iterator();
823        
824        boolean iterate = true;
825        while (it.hasNext() && iterate)
826        {
827            DefaultLink sibling = (DefaultLink) it.next();
828            iterate = !sibling.getName().equals(link.getName());
829        }
830        
831        if (it.hasNext())
832        {
833            // Move the link after his next sibling: move the next sibling before the link to move.
834            DefaultLink nextLink = (DefaultLink) it.next();
835            nextLink.orderBefore(link);
836    
837            link.saveChanges();
838        }
839    }
840    
841    /**
842     * Move link before its preceding
843     * @param link the link to move up
844     * @throws RepositoryException if an error occurs while exploring the repository
845     */
846    private void _moveUp(DefaultLink link) throws RepositoryException
847    {
848        TraversableAmetysObject parentNode = link.getParent();
849        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
850        Iterator<AmetysObject> it = siblings.iterator();
851        DefaultLink previousLink = null;
852
853        while (it.hasNext())
854        {
855            DefaultLink sibling = (DefaultLink) it.next();
856            if (sibling.getName().equals(link.getName()))
857            {
858                break;
859            }
860            
861            previousLink = sibling;
862        }
863            
864        if (previousLink != null)
865        {
866            // Move the link after his next sibling: move the next sibling before the link to move.
867            link.orderBefore(previousLink);
868            link.saveChanges();
869        }
870    }
871    
872    /**
873     * Move link to the last position.
874     * @param link the link to move up
875     * @throws RepositoryException if an error occurs while exploring the repository
876     */
877    private void _moveLast(DefaultLink link) throws RepositoryException
878    {
879        link.moveTo(link.getParent(), false);
880        ((ModifiableAmetysObject) link.getParent()).saveChanges();
881    }
882    
883    /**
884     * Set access to the link
885     * @param link the link
886     * @param user the user to set access. Can be null, in this case, we set anonymous right
887     */
888    private void _setAccess (Link link, UserIdentity user)
889    {
890        if (user != null)
891        {
892            _profileAssignmentStorageEP.allowProfileToUser(user, RightManager.READER_PROFILE_ID, link);
893        }
894        else
895        {
896            _profileAssignmentStorageEP.allowProfileToAnonymous(RightManager.READER_PROFILE_ID, link);
897        }
898        
899        Map<String, Object> eventParams = new HashMap<>();
900        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, link);
901        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
902        
903        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
904    }
905    
906    /**
907     * Retrieves the color of the link.
908     * If there is no color configured on the link, the default site color
909     * @param link the link
910     * @return the color of the link
911     */
912    public String getLinkColor(DefaultLink link)
913    {
914        Map<String, Map<String, String>> colors = _colorComponent.getColors(link.getSiteName());
915        if (colors.containsKey(link.getColor()))
916        {
917            return colors.get(link.getColor()).get("main");
918        }
919        else
920        {
921            return colors.get(_colorComponent.getDefaultKey(link.getSiteName())).get("main");
922        }
923    }
924}