001/*
002 *  Copyright 2020 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.mobileapp;
017
018import java.time.LocalDate;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.collections4.CollectionUtils;
032
033import org.ametys.core.user.CurrentUserProvider;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.core.userpref.UserPreferencesException;
036import org.ametys.core.userpref.UserPreferencesManager;
037import org.ametys.core.util.JSONUtils;
038import org.ametys.plugins.workspaces.project.objects.Project;
039import org.ametys.runtime.plugin.component.AbstractLogEnabled;
040import org.ametys.web.repository.site.Site;
041
042/**
043 * Helper to store/retreive conf per user
044 */
045public class UserPreferencesHelper extends AbstractLogEnabled implements Serviceable, Component
046{
047    /** The avalon role. */
048    public static final String ROLE = UserPreferencesHelper.class.getName();
049
050    private static final String __USERPREF_KEY_LANG = "lang";
051    private static final String __USERPREF_KEY_SITE = "site";
052    private static final String __USERPREF_KEY_ENABLED = "enabled";
053    private static final String __USERPREF_KEY_ALL_FEEDS = "allFeeds";
054    private static final String __USERPREF_KEY_FEEDS = "feeds";
055    private static final String __USERPREF_KEY_ALL_PROJECTS = "allProjects";
056    private static final String __USERPREF_KEY_PROJECTS = "projects";
057    private static final String __USERPREF_KEY_ALL_TYPES = "allTypes";
058    private static final String __USERPREF_KEY_TYPES = "types";
059
060    /** User Preferences Manager */
061    protected UserPreferencesManager _userPreferencesManager;
062
063    /** The current user provider */
064    protected CurrentUserProvider _currentUserProvider;
065
066    /** JSON Utils */
067    protected JSONUtils _jsonUtils;
068
069
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE);
073        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
074        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
075    }
076
077    /**
078     * Get the list of impacted tokens for each feeds, for a user
079     * @param user user to check
080     * @param feedIds list of feeds to check
081     * @param site the site for the validated content
082     * @return a map with a Set of tokens for each feedId
083     */
084    @SuppressWarnings("unchecked")
085    public Map<String, Set<String>> getUserImpactedTokens(UserIdentity user, Set<String> feedIds, Site site)
086    {
087        Map<String, Set<String>> feedsAndTokens = new HashMap<>();
088        try
089        {
090            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
091            for (Map.Entry<String, String> entry : unTypedUserPrefs.entrySet())
092            {
093                String token = entry.getKey();
094                String value = entry.getValue();
095                Map<String, Object> prefs = _jsonUtils.convertJsonToMap(value);
096                
097                List<String> feeds = (List<String>) prefs.get(__USERPREF_KEY_FEEDS);
098                String siteName = (String) prefs.get(__USERPREF_KEY_SITE);
099                
100                boolean enabled = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ENABLED), true);
101                boolean allFeeds = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_FEEDS), feeds == null);
102                
103                if (enabled)
104                {
105                    boolean hasSiteQueries = site.hasValue(QueriesHelper.QUERY_CONTAINER_SITE_CONF_ID);
106                    
107                    if (siteName == null || hasSiteQueries && siteName.equals(site.getName()))
108                    {
109                        Collection<String> matchingFeeds = allFeeds || feeds == null ? feedIds : CollectionUtils.intersection(feeds, feedIds);
110                        
111                        for (String feedId : matchingFeeds)
112                        {
113                            Set tokens = feedsAndTokens.computeIfAbsent(feedId, __ -> new HashSet<>());
114                            tokens.add(token);
115                        }
116                    }
117                }
118            }
119        }
120        catch (UserPreferencesException e)
121        {
122            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
123        }
124
125        return feedsAndTokens;
126    }
127
128    /**
129     * Get the list of impacted tokens for each feeds, for a user
130     * @param user user to check
131     * @param project test if a notification should be sent for this project
132     * @param eventType test if a notification should be sent for this event type
133     * @return a map with a the set of tokens impacted for each languages
134     */
135    @SuppressWarnings("unchecked")
136    public Map<String, Set<String>> getUserImpactedTokens(UserIdentity user, Project project, String eventType)
137    {
138        Map<String, Set<String>> tokensForLanguage = new HashMap<>();
139        try
140        {
141            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
142            for (Map.Entry<String, String> entry : unTypedUserPrefs.entrySet())
143            {
144                String token = entry.getKey();
145                String value = entry.getValue();
146                Map<String, Object> prefs = _jsonUtils.convertJsonToMap(value);
147                
148                List<String> projects = (List<String>) prefs.get(__USERPREF_KEY_PROJECTS);
149                List<String> types = (List<String>) prefs.get(__USERPREF_KEY_TYPES);
150                
151                boolean enabled = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ENABLED), true);
152                boolean allProjects = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_PROJECTS), projects == null);
153                boolean allTypes = _getValueOrDefault((Boolean) prefs.get(__USERPREF_KEY_ALL_TYPES), types == null);
154                
155                if (enabled)
156                {
157                    boolean projectMatch = allProjects || projects == null || projects.contains(project.getId());
158                    boolean typeMatch = allTypes || types == null || types.contains(eventType);
159                    
160                    if (projectMatch && typeMatch)
161                    {
162                        String lang = (String) prefs.get(__USERPREF_KEY_LANG);
163                        
164                        Set<String> tokens = tokensForLanguage.computeIfAbsent(lang, __ -> new HashSet<>());
165                        tokens.add(token);
166                    }
167                }
168            }
169        }
170        catch (UserPreferencesException e)
171        {
172            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
173        }
174
175        return tokensForLanguage;
176    }
177
178    private boolean _getValueOrDefault(Boolean value, boolean defaultValue)
179    {
180        return value != null ? value : defaultValue;
181    }
182
183    /**
184     * Get all notification tokens for current user
185     * @return the list of notification tokens
186     */
187    public Set<String> getNotificationTokens()
188    {
189        UserIdentity user = _currentUserProvider.getUser();
190        return getNotificationTokens(user);
191    }
192
193    /**
194     * Get all notification tokens a user
195     * @param user the user impacted
196     * @return the list of notification tokens
197     */
198    public Set<String> getNotificationTokens(UserIdentity user)
199    {
200        try
201        {
202            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
203            return unTypedUserPrefs.keySet();
204        }
205        catch (UserPreferencesException e)
206        {
207            getLogger().error("Impossible to read a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
208        }
209
210        return Collections.EMPTY_SET;
211    }
212
213    /**
214     * Removes a notification token for the current user
215     * @param pushToken the token to remove
216     */
217    public void removeNotificationToken(String pushToken)
218    {
219        UserIdentity user = _currentUserProvider.getUser();
220        removeNotificationToken(pushToken, user);
221    }
222
223    /**
224     * Removes a notification token for a user
225     * @param pushToken the token to remove
226     * @param user the user impacted
227     */
228    public void removeNotificationToken(String pushToken, UserIdentity user)
229    {
230        try
231        {
232            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
233            unTypedUserPrefs.remove(pushToken);
234            _userPreferencesManager.setUserPreferences(user, "/mobileapp", Collections.emptyMap(), unTypedUserPrefs);
235        }
236        catch (UserPreferencesException e)
237        {
238            getLogger().error("Impossible to remove a user notification token, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
239        }
240    }
241
242    /**
243     * Remove all the notification tokens for a user
244     * @param user the user impacted
245     */
246    public void removeAllNotificationTokens(UserIdentity user)
247    {
248        try
249        {
250            _userPreferencesManager.removeAllUserPreferences(user, "/mobileapp", Collections.emptyMap());
251        }
252        catch (UserPreferencesException e)
253        {
254            getLogger().error("Impossible to remove all user notification tokens, for user '" + UserIdentity.userIdentityToString(user) + "'", e);
255        }
256    }
257
258    /**
259     * Save the notification settings for the current user
260     * @param pushToken the token to impact
261     * @param enabled if notifications are enabled
262     * @param allFeeds if all feeds are allowed to be notified
263     * @param feeds list of feeds ID for notifications
264     * @param allProjects if all projects are allowed to be notified
265     * @param projects list of project ID for notifications
266     * @param allTypes if all types are allowed to be notified
267     * @param types list of notifications types for notifications in projects
268     * @param lang lang of this device
269     */
270    public void setNotificationSettings(String pushToken, boolean enabled, boolean allFeeds, List<String> feeds, boolean allProjects, List<String> projects, boolean allTypes, List<String> types, String lang)
271    {
272        UserIdentity user = _currentUserProvider.getUser();
273        setNotificationSettings(pushToken, enabled, allFeeds, feeds, allProjects, projects, allTypes, types, lang, user);
274    }
275
276    /**
277     * Save the notification settings for a user
278     * @param pushToken token impacted by this settings
279     * @param enabled if notifications are enabled
280     * @param allFeeds if all feeds are allowed to be notified
281     * @param feeds list of feeds ID for notifications
282     * @param allProjects if all projects are allowed to be notified
283     * @param projects list of project ID for notifications
284     * @param allTypes if all types are allowed to be notified
285     * @param types list of notifications types for notifications in projects
286     * @param lang lang of this device
287     * @param user the user impacted
288     */
289    public synchronized void setNotificationSettings(String pushToken, boolean enabled, boolean allFeeds, List<String> feeds, boolean allProjects, List<String> projects, boolean allTypes, List<String> types, String lang, UserIdentity user)
290    {
291        // synchronized because it happens that the app send multiple calls nearly simultaneously
292        Map<String, Object> values = new HashMap<>();
293        values.put(__USERPREF_KEY_LANG, lang);
294        
295        values.put(__USERPREF_KEY_ENABLED, enabled);
296        
297        values.put(__USERPREF_KEY_ALL_FEEDS, allFeeds);
298        if (feeds != null)
299        {
300            values.put(__USERPREF_KEY_FEEDS, feeds);
301        }
302        
303        values.put(__USERPREF_KEY_ALL_PROJECTS, allProjects);
304        if (projects != null)
305        {
306            values.put(__USERPREF_KEY_PROJECTS, projects);
307        }
308
309        values.put(__USERPREF_KEY_ALL_TYPES, allTypes);
310        if (types != null)
311        {
312            values.put(__USERPREF_KEY_TYPES, types);
313        }
314
315        values.put("epochDay", LocalDate.now().toEpochDay());
316
317        String valuesAsString = _jsonUtils.convertObjectToJson(values);
318
319        try
320        {
321            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
322            if (unTypedUserPrefs == null)
323            {
324                unTypedUserPrefs = new HashMap<>();
325            }
326
327            unTypedUserPrefs.put(pushToken, valuesAsString);
328
329            _userPreferencesManager.setUserPreferences(user, "/mobileapp", Collections.emptyMap(), unTypedUserPrefs);
330        }
331        catch (UserPreferencesException e)
332        {
333            getLogger().error("Impossible to set user preferences for user '" + UserIdentity.userIdentityToString(user) + "'", e);
334        }
335    }
336
337    /**
338     * Get the notification settings for a token
339     * @param pushToken the token to read
340     * @param user the user impacted
341     * @return a map containing feeds, projects and types as Set&lt;String&gt;, and epochDay as the last modification date
342     */
343    public Map<String, Object> getNotificationSettings(String pushToken, UserIdentity user)
344    {
345        try
346        {
347            Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, "/mobileapp", Collections.emptyMap());
348            if (unTypedUserPrefs != null && unTypedUserPrefs.containsKey(pushToken))
349            {
350                String prefs = unTypedUserPrefs.get(pushToken);
351                return _jsonUtils.convertJsonToMap(prefs);
352            }
353        }
354        catch (UserPreferencesException e)
355        {
356            getLogger().error("Impossible to retreive user preferences for user '" + UserIdentity.userIdentityToString(user) + "'", e);
357        }
358        return null;
359    }
360
361}