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