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