001/*
002 *  Copyright 2016 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.core.right;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.commons.lang3.Strings;
032import org.apache.ibatis.session.SqlSession;
033
034import org.ametys.core.ObservationConstants;
035import org.ametys.core.cache.AbstractCacheManager;
036import org.ametys.core.cache.Cache;
037import org.ametys.core.datasource.AbstractMyBatisDAO;
038import org.ametys.core.observation.Event;
039import org.ametys.core.observation.ObservationManager;
040import org.ametys.core.ui.Callable;
041import org.ametys.core.user.CurrentUserProvider;
042import org.ametys.runtime.i18n.I18nizableText;
043
044/**
045 * Manages registration of profiles
046 */
047public class RightProfilesDAO extends AbstractMyBatisDAO implements Initializable
048{
049    /** The component role. */
050    public static final String ROLE = RightProfilesDAO.class.getName();
051
052    /** profile cache id */
053    private static final String PROFILES_CACHE = ROLE + "$profiles";
054
055    /** profile rights cache id */
056    private static final String PROFILE_RIGHTS_CACHE = ROLE + "$profileRigths";
057    
058    private ServiceManager _smanager;
059
060    private ObservationManager _observationManager;
061
062    private CurrentUserProvider _currentUserProvider;
063
064    private AbstractCacheManager _cacheManager;
065
066    
067    @Override
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        _smanager = smanager;
071        super.service(smanager);
072        _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE);
073    }
074
075
076    public void initialize() throws Exception
077    {
078        _cacheManager.createMemoryCache(PROFILES_CACHE,
079                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_PROFILES_CACHE_LABEL"),
080                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_PROFILES_CACHE_DESCRIPTION"),
081                true,
082                null);
083        _cacheManager.createMemoryCache(PROFILE_RIGHTS_CACHE,
084                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_PROFILE_RIGHTS_CACHE_LABEL"),
085                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_PROFILE_RIGHTS_CACHE_DESCRIPTION"),
086                true,
087                null);
088    }
089    
090    /**
091     * Clear the profiles cache
092     */
093    public synchronized void clearCache()
094    {
095        _getProfilesCache().invalidateAll();
096        _getProfileRightsCache().invalidateAll();
097    }
098    
099    private synchronized Cache<String, Profile> _getProfilesCache()
100    {
101        Cache<String, Profile> cache = this._cacheManager.get(PROFILES_CACHE);
102
103        if (!cache.isInitialized())
104        {
105            try (SqlSession session = getSession())
106            {
107                Map<String, Profile> profilesMap = new HashMap<>();
108                List<Profile> profiles = session.selectList("Profiles.getProfiles");
109                profilesMap = profiles.stream().collect(Collectors.toMap(Profile::getId, p -> p));
110                cache.putAll(profilesMap);
111            }
112        }
113        
114        return cache;
115    }
116
117    private synchronized Cache<String, List<String>> _getProfileRightsCache()
118    {
119
120        Cache<String, List<String>> cache = this._cacheManager.get(PROFILE_RIGHTS_CACHE);
121
122        //As the cache can be cleared from admin tool, we add an entry to check is filled or not.
123        if (!cache.isInitialized())
124        {
125            try (SqlSession session = getSession())
126            {
127
128                Map<String, List<String>> profileRightsMap = new HashMap<>();
129                List<Map<String, String>> rightAssociations = session.selectList("Profiles.getProfileRights");
130                
131                profileRightsMap = rightAssociations.stream()
132                        .collect(Collectors.groupingBy(
133                            rightAssociation -> rightAssociation.get("profileId"),
134                            Collectors.mapping(
135                                rightAssociation -> rightAssociation.get("rightId"),
136                                Collectors.toList())));
137                cache.putAll(profileRightsMap);
138            }
139        }
140        
141        return cache;
142    }
143    
144    /**
145     * Get all existing profiles
146     * @return The list for profiles
147     */
148    public List<Profile> getProfiles()
149    {
150        Collection<Profile> profiles = _getProfilesCache().asMap().values();
151        return new ArrayList<>(profiles);
152    }
153    
154    /**
155     * Get the profiles on a given context
156     * @param context The context. Can be null. If null, the profiles with no context are returned.
157     * @return The list for profiles for this context
158     */
159    public List<Profile> getProfiles(String context)
160    {
161        return _getProfilesCache().asMap().values().stream()
162                                      .filter(p -> Strings.CS.equals(p.getContext(), context))
163                                      .collect(Collectors.toList());
164    }
165    
166    /**
167     * Get the list of profile and there associated rights
168     * @param context the profile context or null to retrieve only shared profiles
169     * @param includeReader true to include the reader profile
170     * @return the profiles
171     */
172    @Callable(rights = Callable.NO_CHECK_REQUIRED)
173    public List<Map<String, Object>> getProfilesAsJson(String context, boolean includeReader)
174    {
175        List<Map<String, Object>> profilesAsJSON = new ArrayList<>();
176        
177        List<Profile> profiles = new ArrayList<>();
178        profiles.addAll(getProfiles(null));
179        
180        if (StringUtils.isNotBlank(context))
181        {
182            profiles.addAll(getProfiles(context));
183        }
184        
185        if (!includeReader)
186        {
187            Profile readerProfile = getProfile(RightManager.READER_PROFILE_ID);
188            profiles.remove(readerProfile);
189        }
190        
191        for (Profile profile : profiles)
192        {
193            Map<String, Object> profile2json = profile.toJSON();
194            profile2json.put("rights", getRights(profile));
195            profilesAsJSON.add(profile2json);
196        }
197        
198        return profilesAsJSON;
199    }
200    
201    /**
202     * Get the reader profile
203     * @return the reader profile
204     */
205    @Callable(rights = Callable.NO_CHECK_REQUIRED)
206    public Map<String, Object> getReaderProfileAsJson()
207    {
208        Profile readerProfile = getProfile(RightManager.READER_PROFILE_ID);
209        return readerProfile.toJSON();
210    }
211    
212    /**
213     * Get the profile with given identifier
214     * @param id The id of profile to retrieve
215     * @return The profile
216     */
217    public Profile getProfile(String id)
218    {
219        return _getProfilesCache().get(id);
220    }
221    
222    /**
223     * Get all profiles containing the right with given id
224     * @param rightId The id of right
225     * @return The id of profiles with this right
226     */
227    public Set<String> getProfilesWithRight (String rightId)
228    {
229        return _getProfileRightsCache().asMap().entrySet().stream()
230                .filter(e -> e.getValue().contains(rightId))
231                .map(e -> e.getKey())
232                .collect(Collectors.toSet());
233    }
234    
235    /**
236     * Creates a new profile with null context. The identifier of the profile will be automatically generated from label.
237     * @param label The label of profile
238     * @return The create profile
239     */
240    public Profile addProfile (String label)
241    {
242        return addProfile(label, null);
243    }
244    
245    /**
246     * Creates a new profile. The identifier of the profile will be automatically generated from label.
247     * @param label The label of profile
248     * @param context The context. Can be null
249     * @return The create profile
250     */
251    public Profile addProfile (String label, String context)
252    {
253        String id = _generateUniqueId(label);
254        Profile profile = new Profile(id, label, context);
255        addProfile(profile);
256        return profile;
257    }
258    
259    private String _generateUniqueId(String label)
260    {
261        // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes
262        String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", "");
263        int i = 2;
264        String suffixedValue = value;
265        while (getProfile(suffixedValue) != null)
266        {
267            suffixedValue = value + i;
268            i++;
269        }
270        
271        return suffixedValue;
272    }
273    
274    /**
275     * Creates a new profile
276     * @param id The unique identifier of profile
277     * @param label The label of profile
278     * @param context The context. Can be null
279     * @return The create profile
280     */
281    public Profile addProfile (String id, String label, String context)
282    {
283        Profile profile = new Profile(id, label, context);
284        addProfile(profile);
285        return profile;
286    }
287    
288    /**
289     * Add a new profile
290     * @param profile The profile to add
291     * @param silent Set to true to not notify observer of this update
292     */
293    public void addProfile (Profile profile, boolean silent)
294    {
295        try (SqlSession session = getSession(true))
296        {
297            session.insert("Profiles.addProfile", profile);
298            clearCache();
299            if (!silent)
300            {
301                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_ADDED);
302            }
303        }
304    }
305    
306    /**
307     * Add a new profile
308     * @param profile The profile to add
309     */
310    public void addProfile (Profile profile)
311    {
312        addProfile(profile, false);
313    }
314    
315    /**
316     * Rename a profile
317     * @param profile The profile to rename
318     * @param newLabel The updated label
319     */
320    public void renameProfile (Profile profile, String newLabel)
321    {
322        renameProfile(profile, newLabel, false);
323    }
324    
325    /**
326     * Rename a profile
327     * @param profile The profile to rename
328     * @param newLabel The updated label
329     * @param silent Set to true to not notify observer of this update
330     */
331    public void renameProfile (Profile profile, String newLabel, boolean silent)
332    {
333        try (SqlSession session = getSession(true))
334        {
335            Map<String, Object> params = new HashMap<>();
336            params.put("id", profile.getId());
337            params.put("label", newLabel);
338            session.update("Profiles.renameProfile", params);
339            
340            clearCache();
341            
342            if (!silent)
343            {
344                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_UPDATED);
345            }
346        }
347    }
348
349    
350    /**
351     * Get the rights of a profile
352     * @param profileId The profile id
353     * @return The rights
354     */
355    public List<String> getRights (String profileId)
356    {
357        if (!StringUtils.isEmpty(profileId))
358        {
359            List<String> list = _getProfileRightsCache().get(profileId);
360            if (list != null)
361            {
362                return list;
363            }
364        }
365        return Collections.EMPTY_LIST;
366    }
367
368    /**
369     * Get the rights of a profile
370     * @param profile The profile
371     * @return The rights
372     */
373    public List<String> getRights (Profile profile)
374    {
375        if (profile == null)
376        {
377            return Collections.EMPTY_LIST;
378        }
379        else
380        {
381            return getRights(profile.getId());
382        }
383    }
384    
385    /**
386     * Add a right to a profile
387     * @param profile The profile
388     * @param rightId The id of right to add
389     */
390    public void addRight (Profile profile, String rightId)
391    {
392        try (SqlSession session = getSession(true))
393        {
394            _addRight (session, profile, rightId);
395        }
396        
397        clearCache();
398    }
399    
400    /**
401     * Add a right to a profile
402     * @param profile The profile
403     * @param rightIds The id of rights to add
404     */
405    public void addRights (Profile profile, List<String> rightIds)
406    {
407        try (SqlSession session = getSession())
408        {
409            for (String rightId : rightIds)
410            {
411                _addRight (session, profile, rightId);
412            }
413            
414            session.commit();
415        }
416        
417        clearCache();
418    }
419    
420    /**
421     * Update the rights of a profile
422     * @param profile The profile
423     * @param rights The rights of the profile
424     */
425    public void updateRights (Profile profile, List<String> rights)
426    {
427        updateRights(profile, rights, false);
428    }
429    
430    /**
431     * Update the rights of a profile
432     * @param profile The profile
433     * @param rights The rights of the profile
434     * @param silent Set to true to not notify observer of this update
435     */
436    public void updateRights (Profile profile, List<String> rights, boolean silent)
437    {
438        try (SqlSession session = getSession())
439        {
440            session.delete("Profiles.deleteProfileRights", profile.getId());
441            
442            if (rights != null)
443            {
444                for (String rightId : rights)
445                {
446                    _addRight (session, profile, rightId);
447                }
448            }
449            
450            session.commit();
451            
452            clearCache();
453            
454            if (!silent)
455            {
456                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_UPDATED);
457            }
458        }
459    }
460    
461    private void _addRight (SqlSession session, Profile profile, String rightId)
462    {
463        Map<String, Object> params = new HashMap<>();
464        params.put("profileId", profile.getId());
465        params.put("rightId", rightId);
466        
467        session.insert("Profiles.addRight", params);
468    }
469    
470    /**
471     * Add a right to a profile
472     * @param profile The profile
473     */
474    public void removeRights (Profile profile)
475    {
476        removeRights(profile, false);
477    }
478    
479    /**
480     * Add a right to a profile
481     * @param profile The profile
482     * @param silent Set to true to not notify observer of this update
483     */
484    public void removeRights (Profile profile, boolean silent)
485    {
486        try (SqlSession session = getSession(true))
487        {
488            session.delete("Profiles.deleteProfileRights", profile.getId());
489            
490            clearCache();
491            
492            if (!silent)
493            {
494                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_UPDATED);
495            }
496        }
497    }
498    
499    /**
500     * Remove a right from a profile
501     * @param profile The profile
502     * @param rightId The id of right to add
503     */
504    public void removeRight (Profile profile, String rightId)
505    {
506        removeRight(profile, rightId, false);
507    }
508
509    /**
510     * Remove a right from a profile
511     * @param profile The profile
512     * @param rightId The id of right to add
513     * @param silent Set to true to not notify observer of this update
514     */
515    public void removeRight (Profile profile, String rightId, boolean silent)
516    {
517        try (SqlSession session = getSession(true))
518        {
519            Map<String, Object> params = new HashMap<>();
520            params.put("profileId", profile.getId());
521            params.put("rightId", rightId);
522            session.delete("Profiles.deleteProfileRight", params);
523            
524            clearCache();
525            
526            if (!silent)
527            {
528                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_UPDATED);
529            }
530        }
531        
532        clearCache();
533    }
534    
535    /**
536     * Delete a profile
537     * @param profile The profile to delete
538     */
539    public void deleteProfile (Profile profile)
540    {
541        deleteProfile(profile, false);
542    }
543    
544    /**
545     * Delete a profile
546     * @param profile The profile to delete
547     * @param silent Set to true to not notify observer of this update
548     */
549    public void deleteProfile (Profile profile, boolean silent)
550    {
551        try (SqlSession session = getSession())
552        {
553            session.delete("Profiles.deleteProfile", profile.getId());
554            session.delete("Profiles.deleteProfileRights", profile.getId());
555            
556            session.commit();
557            
558            clearCache();
559            
560            if (!silent)
561            {
562                _notifyEvent(profile, ObservationConstants.EVENT_PROFILE_DELETED);
563            }
564        }
565    }
566    
567    private void _notifyEvent (Profile profile, String eventId)
568    {
569        try
570        {
571            if (_observationManager == null)
572            {
573                _observationManager = (ObservationManager) _smanager.lookup(ObservationManager.ROLE);
574            }
575            if (_currentUserProvider == null)
576            {
577                _currentUserProvider = (CurrentUserProvider) _smanager.lookup(CurrentUserProvider.ROLE);
578            }
579            
580            Map<String, Object> eventParams = new HashMap<>();
581            eventParams.put(ObservationConstants.ARGS_PROFILE, profile);
582            _observationManager.notify(new Event(eventId, _currentUserProvider.getUser(), eventParams));
583        }
584        catch (ServiceException e)
585        {
586            getLogger().error("Fail to notify observers for event '" + eventId + "'", e);
587        }
588    }
589}