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        link.setUrl(LinkType.valueOf(urlType), url);
317        link.setDynamicInformationProvider(dynInfoProviderId);
318        link.setInternalUrl(internalUrl);
319        link.setTitle(title);
320        link.setContent(content);
321        link.setAlternative(alternative);
322        link.setPictureAlternative(pictureAlternative);
323        
324        _setPicture(link, pictureAsStr);
325    }
326    
327    private void _setThemes(Link link, List<String> themes)
328    {
329        link.setThemes(themes.toArray(new String[themes.size()]));
330    }
331    
332    private void _setPicture(Link link, String valueAsStr)
333    {
334        if (StringUtils.isNotEmpty(valueAsStr))
335        {
336            Map<String, Object> picture = _jsonUtils.convertJsonToMap(valueAsStr);
337            
338            if (!picture.isEmpty())
339            {
340                String pictureType = (String) picture.get("type");
341                String value = (String) picture.get("id");
342                
343                if (pictureType.equals("explorer") && !"untouched".equals(value))
344                {
345                    link.setResourcePicture(value);
346                }
347                else if (!"untouched".equals(value))
348                {
349                    UserIdentity user = _currentUserProvider.getUser();
350                    Upload upload = _uploadManager.getUpload(user, value);
351                    
352                    String filename = upload.getFilename();
353                    String mimeType = upload.getMimeType() != null ? upload.getMimeType() : _cocoonContext.getMimeType(filename);
354                    String finalMimeType = mimeType != null ? mimeType : "application/unknown";
355                    
356                    link.setExternalPicture(finalMimeType, filename, upload.getInputStream());
357                }
358            }
359            else
360            {
361                // Remove picture
362                link.setNoPicture(); 
363            }
364            
365        }
366        else
367        {
368            // Remove picture
369            link.setNoPicture();
370        }
371    }
372    
373    private void _setRestrictions(Link link, boolean grantAnyUser, String grantedUsersAsString, String grantedGroupsAsString)
374    {
375        Set<UserIdentity> grantedUsers = new HashSet<>();
376        List<Object> grantedUsersAsList = _jsonUtils.convertJsonToList(grantedUsersAsString);
377        for (Object obj : grantedUsersAsList)
378        {
379            @SuppressWarnings("unchecked")
380            Map<String, Object> grantedUser = (Map<String, Object>) obj;
381            String login = (String) grantedUser.get("login");
382            String populationId = (String) grantedUser.get("populationId");
383            
384            if (login != null && populationId != null)
385            {
386                grantedUsers.add(new UserIdentity(login, populationId));
387            }
388        }
389        
390        Set<GroupIdentity> grantedGroups = new HashSet<>();
391        List<Object> grantedGroupsAsList = _jsonUtils.convertJsonToList(grantedGroupsAsString);
392        for (Object obj : grantedGroupsAsList)
393        {
394            @SuppressWarnings("unchecked")
395            Map<String, Object> grantedGroup = (Map<String, Object>) obj;
396            String groupId = (String) grantedGroup.get("groupId");
397            String groupDirectory = (String) grantedGroup.get("groupDirectory");
398            
399            if (groupId != null && groupDirectory != null)
400            {
401                grantedGroups.add(new GroupIdentity(groupId, groupDirectory));
402            }
403        }
404        
405        link.setGrantAnyUser(grantAnyUser);
406        link.setGrantedUsers(grantedUsers.toArray(new UserIdentity[grantedUsers.size()]));
407        link.setGrantedGroups(grantedGroups.toArray(new GroupIdentity[grantedGroups.size()]));
408    }
409    
410    /**
411     * Delete one or multiples links
412     * @param ids a list of links' ids
413     * @return true if all the links were deleted, false if at least one link could not be delete.
414     */
415    @Callable
416    public List<String> deleteLinks(List<String> ids)
417    {
418        List<String> result = new ArrayList<>();
419        
420        for (String id : ids)
421        {
422            try
423            {
424                DefaultLink link = _resolver.resolveById(id);
425                
426                String siteName = link.getSiteName();
427                String language = link.getLanguage();
428                String linkId = link.getId();
429                String name = link.getName();
430                String path = link.getPath();
431                
432                ModifiableAmetysObject parent = link.getParent();
433                link.remove(); 
434    
435                parent.saveChanges();
436    
437                // Notify listeners
438                Map<String, Object> eventParams = new HashMap<>();
439                eventParams.put(ObservationConstants.ARGS_ID, linkId);
440                eventParams.put(ObservationConstants.ARGS_NAME, name);
441                eventParams.put(ObservationConstants.ARGS_PATH, path);
442                eventParams.put("siteName", siteName);
443                eventParams.put("language", language);
444                _observationManager.notify(new Event(DirectoryEvents.LINK_DELETED, _currentUserProvider.getUser(), eventParams));
445    
446                result.add(linkId);
447            }
448            catch (UnknownAmetysObjectException e)
449            {
450                // ignore the id and continue the deletion.
451                getLogger().error("Unable to delete the link of id '" + id + ", because it does not exist.", e);
452            }
453        }
454        
455        return result;
456    }
457    
458    /**
459     * Move a link in the list
460     * @param id the link id
461     * @param role the move action
462     * @throws RepositoryException if a repository error occurs.
463     * @return the moved link in JSON
464     */
465    @Callable
466    public Map<String, Object> moveLink(String id, String role) throws RepositoryException
467    {
468        DefaultLink link = _resolver.resolveById(id);
469        
470        switch (role)
471        {
472            case "move-first":
473                _moveFirst(link);
474                break;
475            case "move-up":
476                _moveUp(link);
477                break;
478            case "move-down":
479                _moveDown(link);
480                break;
481            case "move-last":
482                _moveLast(link);
483                break;
484            default:
485                break;
486        }
487        
488        return convertLink2JsonObject(link);
489    }
490    
491    
492    
493    /**
494     * Get the JSON object representing a link
495     * @param id the id of link
496     * @return the link as JSON object
497     */
498    @Callable
499    public Map<String, Object> getLink (String id)
500    {
501        DefaultLink link = _resolver.resolveById(id);
502        return convertLink2JsonObject(link);
503    }
504    
505    /**
506     * Determines if the restriction IP parameter is not empty
507     * @param siteName the site name
508     * @return true if the restriction IP parameter is not empty
509     */
510    @Callable
511    public boolean isInternalURLAllowed (String siteName)
512    {
513        String allowedIpParameter = _siteConfEP.getValueAsString(siteName, "allowed-ip");
514        return StringUtils.isNotBlank(allowedIpParameter);
515    }
516    
517    /**
518     * Returns the list of providers of dynamic information as json object
519     * @return the providers of dynamic information 
520     */
521    @Callable
522    public List<Map<String, Object>> getDynamicInformationProviders()
523    {
524        List<Map<String, Object>> result = new ArrayList<>();
525        
526        for (String id : _dynamicInfoExtensionPoint.getExtensionsIds())
527        {
528            DynamicInformationProvider provider = _dynamicInfoExtensionPoint.getExtension(id);
529            Map<String, Object> info = new HashMap<>();
530            info.put("id", provider.getId());
531            info.put("label", provider.getLabel());
532            result.add(info);
533        }
534        
535        return result;
536    }
537    
538    /**
539     * Convert a link to JSON object
540     * @param link the link
541     * @return the link as JSON object
542     */
543    public Map<String, Object> convertLink2JsonObject (DefaultLink link)
544    {
545        Map<String, Object> infos = new HashMap<>();
546        
547        infos.put("id", link.getId());
548        infos.put("lang", link.getLanguage());
549        infos.put("url", link.getUrl());
550        infos.put("dynamicInfoProvider", link.getDynamicInformationProvider());
551        infos.put("internalUrl", link.getInternalUrl());
552        infos.put("urlType", link.getUrlType().toString());
553        
554        if (link.getUrlType() == LinkType.PAGE)
555        {
556            String pageId = link.getUrl();
557            try
558            {
559                Page page = _resolver.resolveById(pageId);
560                infos.put("pageTitle", page.getTitle());
561            }
562            catch (UnknownAmetysObjectException e)
563            {
564                infos.put("unknownPage", true);
565            }
566        }
567        
568        infos.put("title", link.getTitle());
569        infos.put("alternative", link.getAlternative());
570        
571        infos.put("content", StringUtils.defaultString(link.getContent()));
572        infos.put("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
573        
574        Map<String, Object> pictureInfos = new HashMap<>();
575        String pictureType = link.getPictureType();
576        
577        TraversableAmetysObject parent = link.getParent();
578        infos.put("position", parent.getChildPosition(link));
579        infos.put("count", parent.getChildren().getSize());
580        
581        if (pictureType.equals("resource"))
582        {
583            String resourceId = link.getResourcePictureId();
584            pictureInfos.put("id", resourceId);
585            try
586            {
587                Resource resource = _resolver.resolveById(resourceId);
588                
589                pictureInfos.put("filename", resource.getName());
590                pictureInfos.put("size", resource.getLength());
591                pictureInfos.put("type", "explorer");
592                pictureInfos.put("lastModified", resource.getLastModified());
593                
594                String contextPath = ContextHelper.getRequest(_context).getContextPath();
595                String viewUrl = contextPath + "/plugins/explorer/resource?id=" + resource.getId();
596                String downloadUrl = viewUrl + "&download=true";
597                pictureInfos.put("viewUrl", viewUrl);
598                pictureInfos.put("downloadUrl", downloadUrl);
599            }
600            catch (UnknownAmetysObjectException e)
601            {
602                getLogger().error("The resource of id'" + resourceId + "' does not exist anymore. The picture for link of id '" + link.getId() + "' will be ignored.", e);
603                infos.put("pictureNotFound", true);
604            }
605        }
606        else if (pictureType.equals("external"))
607        {
608            BinaryMetadata picMeta = link.getExternalPicture();
609            
610            pictureInfos.put("path", DefaultLink.PROPERTY_PICTURE);
611            pictureInfos.put("filename", picMeta.getFilename());
612            pictureInfos.put("size", picMeta.getLength());
613            pictureInfos.put("lastModified", picMeta.getLastModified());
614            pictureInfos.put("type", "metadata");
615            
616            String contextPath = ContextHelper.getRequest(_context).getContextPath();
617            String viewUrl = contextPath + "/plugins/cms/binaryMetadata/" + DefaultLink.PROPERTY_PICTURE + "?objectId=" + link.getId();
618            String downloadUrl = viewUrl + "&download=true";
619            pictureInfos.put("viewUrl", viewUrl);
620            pictureInfos.put("downloadUrl", downloadUrl);
621            
622        }
623        infos.put("picture", pictureInfos);
624        
625        infos.put("grantAnyUser", String.valueOf(link.isAllowedAnyUser()));
626        
627        boolean isAccessLimited = link.isAllowedAnyUser() || ArrayUtils.isNotEmpty(link.getGrantedUsers()) || ArrayUtils.isNotEmpty(link.getGrantedGroups());
628        infos.put("isRestricted", isAccessLimited); 
629        
630        // Themes
631        List<Map<String, String>> themesList = new ArrayList<>();
632        for (String themeId : link.getThemes())
633        {
634            try
635            {
636                DefaultTheme theme = _resolver.resolveById(themeId);
637                Map<String, String> themeData = new HashMap<>();
638                themeData.put("id", themeId);
639                themeData.put("label", theme.getLabel());
640                themesList.add(themeData);
641            }
642            catch (UnknownAmetysObjectException e)
643            {
644                // Theme does not exist anymore
645            }
646        }
647        
648        infos.put("themes", themesList);
649        
650        List<Map<String, Object>> grantedUserList = new ArrayList<>();
651        for (UserIdentity grantedUser : link.getGrantedUsers())
652        {
653            User user = _userManager.getUser(grantedUser);
654            if (user != null)
655            {
656                grantedUserList.add(_userHelper.user2json(user));
657            }
658        }
659        infos.put("grantedUsers", grantedUserList);
660
661        List<Map<String, String>> grantedGroupList = new ArrayList<>();
662        for (GroupIdentity grantedGroup : link.getGrantedGroups())
663        {
664            Group group = _groupManager.getGroup(grantedGroup);
665            if (group != null)
666            {
667                Map<String, String> groupData = new HashMap<>();
668                groupData.put("groupId", grantedGroup.getId());
669                groupData.put("groupDirectory", grantedGroup.getDirectoryId());
670                groupData.put("label", group.getLabel());
671                grantedGroupList.add(groupData);
672            }
673        }
674        infos.put("grantedGroups", grantedGroupList);
675        
676        return infos;
677    }
678    
679    /**
680     * Get links infos
681     * @param linkIds the link id
682     * @return the link infos
683     */
684    @Callable
685    public List<Map<String, Object>> getLinks(List<String> linkIds)
686    {
687        List<Map<String, Object>> result = new ArrayList<>();
688        
689        for (String linkId: linkIds)
690        {
691            try
692            {
693                result.add(getLink(linkId));
694            }
695            catch (UnknownAmetysObjectException e)
696            {
697                // does not exists
698            }
699        }
700        return result; 
701    }
702    
703    /**
704     * Test if a link with the specified url or internal url exists in the directory.
705     * @param url the url to test.
706     * @param internalUrl the internal url to test
707     * @param siteName the site name.
708     * @param language the language.
709     * @return true if the link exists.
710     * @throws AmetysRepositoryException if a repository error occurs.
711     */
712    protected boolean _urlExists(String url, String internalUrl, String siteName, String language) throws AmetysRepositoryException
713    {
714        boolean externalLinkExists = false;
715        if (StringUtils.isNotBlank(url))
716        {
717            String externalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, url);
718            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
719            {
720                externalLinkExists = externalLinks.iterator().hasNext();
721            }
722        }
723        
724        boolean internalLinkExists = false;
725        if (StringUtils.isNotBlank(internalUrl))
726        {
727            String internalLinkXpathQuery = _directoryHelper.getUrlExistsQuery(siteName, language, internalUrl);
728            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
729            {
730                internalLinkExists = internalLinks.iterator().hasNext();
731            }
732        }
733        
734        return externalLinkExists || internalLinkExists;
735    }
736    
737    /**
738     * Test if a link with the specified url or internal url exists in the directory for the given user.
739     * @param url the url to test.
740     * @param internalUrl the internal url to test
741     * @param siteName the site name.
742     * @param language the language.
743     * @param user The user identity
744     * @return true if the link exists for the given user.
745     * @throws AmetysRepositoryException if a repository error occurs.
746     */
747    protected boolean _urlExistsForUser(String url, String internalUrl, String siteName, String language, UserIdentity user) throws AmetysRepositoryException
748    {
749        boolean externalLinkExists = false;
750        if (StringUtils.isNotBlank(url))
751        {
752            String externalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, url, user);
753            try (AmetysObjectIterable<DefaultLink> externalLinks = _resolver.query(externalLinkXpathQuery);)
754            {
755                externalLinkExists = externalLinks.iterator().hasNext();
756            }
757        }
758        
759        boolean internalLinkExists = false;
760        if (StringUtils.isNotBlank(internalUrl))
761        {
762            String internalLinkXpathQuery = _directoryHelper.getUrlExistsForUserQuery(siteName, language, internalUrl, user);
763            try (AmetysObjectIterable<DefaultLink> internalLinks = _resolver.query(internalLinkXpathQuery);)
764            {
765                internalLinkExists = internalLinks.iterator().hasNext();
766            }
767        }
768        
769        return externalLinkExists || internalLinkExists;
770    }
771    
772    /**
773     * Create the link object.
774     * @param name the desired link name.
775     * @param rootNode the links root node.
776     * @return the created Link.
777     */
778    protected DefaultLink _createLink(String name, ModifiableTraversableAmetysObject rootNode)
779    {
780        String originalName = FilterNameHelper.filterName(name);
781        
782        // Find unique name
783        String uniqueName = originalName;
784        int index = 2;
785        while (rootNode.hasChild(uniqueName))
786        {
787            uniqueName = originalName + "-" + (index++);
788        }
789        
790        return rootNode.createChild(uniqueName, DefaultLinkFactory.LINK_NODE_TYPE);
791    }
792    
793    /**
794     * Move link the first position
795     * @param link the link to move
796     * @throws RepositoryException if an error occurs while exploring the repository
797     */
798    private void _moveFirst(DefaultLink link) throws RepositoryException
799    {
800        try (AmetysObjectIterable<AmetysObject>  children = ((TraversableAmetysObject) link.getParent()).getChildren();)
801        {
802            // Resolve the link in the same session or the linkRoot.saveChanges() call below won't see the order changes.
803            link.orderBefore(((TraversableAmetysObject) link.getParent()).getChildren().iterator().next());
804            ((ModifiableAmetysObject) link.getParent()).saveChanges();
805        }
806    }
807
808    /**
809     * Move link after its following
810     * @param link the link to move down
811     * @throws RepositoryException if an error occurs while exploring the repository
812     */
813    private void _moveDown(DefaultLink link) throws RepositoryException
814    {
815        TraversableAmetysObject parentNode = link.getParent();
816        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
817        Iterator<AmetysObject> it = siblings.iterator();
818        
819        boolean iterate = true;
820        while (it.hasNext() && iterate)
821        {
822            DefaultLink sibling = (DefaultLink) it.next();
823            iterate = !sibling.getName().equals(link.getName());
824        }
825        
826        if (it.hasNext())
827        {
828            // Move the link after his next sibling: move the next sibling before the link to move.
829            DefaultLink nextLink = (DefaultLink) it.next();
830            nextLink.orderBefore(link);
831    
832            link.saveChanges();
833        }
834    }
835    
836    /**
837     * Move link before its preceding
838     * @param link the link to move up
839     * @throws RepositoryException if an error occurs while exploring the repository
840     */
841    private void _moveUp(DefaultLink link) throws RepositoryException
842    {
843        TraversableAmetysObject parentNode = link.getParent();
844        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
845        Iterator<AmetysObject> it = siblings.iterator();
846        DefaultLink previousLink = null;
847
848        while (it.hasNext())
849        {
850            DefaultLink sibling = (DefaultLink) it.next();
851            if (sibling.getName().equals(link.getName()))
852            {
853                break;
854            }
855            
856            previousLink = sibling;
857        }
858            
859        if (previousLink != null)
860        {
861            // Move the link after his next sibling: move the next sibling before the link to move.
862            link.orderBefore(previousLink);
863            link.saveChanges();
864        }
865    }
866    
867    /**
868     * Move link to the last position.
869     * @param link the link to move up
870     * @throws RepositoryException if an error occurs while exploring the repository
871     */
872    private void _moveLast(DefaultLink link) throws RepositoryException
873    {
874        link.moveTo(link.getParent(), false);
875        ((ModifiableAmetysObject) link.getParent()).saveChanges();
876    }
877}