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