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