001/*
002 *  Copyright 2012 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.newsletter.userpref;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.Set;
028import java.util.UUID;
029
030import org.apache.avalon.framework.component.Component;
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;
035
036import org.ametys.core.user.User;
037import org.ametys.core.user.UserIdentity;
038import org.ametys.core.user.UserManager;
039import org.ametys.core.user.directory.NotUniqueUserException;
040import org.ametys.core.user.population.PopulationContextHelper;
041import org.ametys.core.userpref.UserPreferencesException;
042import org.ametys.core.userpref.UserPreferencesStorage;
043import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint;
044import org.ametys.plugins.newsletter.daos.Subscriber;
045import org.ametys.plugins.newsletter.daos.SubscribersDAO;
046import org.ametys.plugins.newsletter.daos.SubscribersDAO.UnsubscribeOrigin;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.web.repository.site.Site;
049import org.ametys.web.repository.site.SiteManager;
050import org.ametys.web.userpref.FOUserPreferencesConstants;
051
052/**
053 * Retrieves and stores newsletter user preferences values as subscriptions.
054 */
055public class NewsletterUserPreferencesStorage extends AbstractLogEnabled implements UserPreferencesStorage, Component, Serviceable
056{
057    /** The front-office users manager. */
058    protected UserManager _foUserManager;
059    
060    /** The category provider extension point. */
061    protected CategoryProviderExtensionPoint _categoryEP;
062    
063    /** The site manager */
064    protected SiteManager _siteManager;
065    
066    /** The subscribers DAO. */
067    protected SubscribersDAO _subscribersDao;
068
069    /** The population context helper */
070    protected PopulationContextHelper _populationContextHelper;
071    
072    @Override
073    public void service(ServiceManager serviceManager) throws ServiceException
074    {
075        _foUserManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
076        _categoryEP = (CategoryProviderExtensionPoint) serviceManager.lookup(CategoryProviderExtensionPoint.ROLE);
077        _populationContextHelper = (PopulationContextHelper) serviceManager.lookup(PopulationContextHelper.ROLE);
078        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
079        _subscribersDao = (SubscribersDAO) serviceManager.lookup(SubscribersDAO.ROLE);
080    }
081    
082    public Map<UserIdentity, Map<String, String>> getAllUnTypedUserPrefs(String storageContext, Map<String, String> contextVars) throws UserPreferencesException
083    {
084        return _getUnTypedUserPrefs(null, storageContext, contextVars);
085    }
086    
087    @Override
088    public Map<String, String> getUnTypedUserPrefs(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
089    {
090        if (userIdentity == null)
091        {
092            return new HashMap<>();
093        }
094        
095        Map<UserIdentity, Map<String,String>> unTypedUserPrefs = _getUnTypedUserPrefs(userIdentity, storageContext, contextVars);
096        return unTypedUserPrefs.getOrDefault(userIdentity, new HashMap<>());
097    }
098    
099    private Map<UserIdentity, Map<String, String>> _getUnTypedUserPrefs(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
100    {
101        Map<UserIdentity, Map<String, String>> userPrefs = new HashMap<>();
102
103        try
104        {
105            String siteName = contextVars.get(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME);
106            if (siteName != null)
107            {
108                for (User u : _getUsers(siteName, user))
109                {
110                    Map<String, String> preferenceValues = new HashMap<>();
111                    
112                    List<Subscriber> subscriptions = _subscribersDao.getSubscriptions(u.getEmail(), siteName);
113                    for (Subscriber subscription : subscriptions)
114                    {
115                        preferenceValues.put(subscription.getCategoryId(), "true");
116                    }
117                    
118                    userPrefs.put(u.getIdentity(), preferenceValues);
119                }
120            }
121            return userPrefs;
122        }
123        catch (Exception e)
124        {
125            String message = user == null 
126                ? "Error getting newsletter all user's preferences in context " + storageContext
127                : "Error getting newsletter user preferences for login " + user + " and context " + storageContext;
128            getLogger().error(message, e);
129            throw new UserPreferencesException(message, e);
130        }
131    }
132    
133    private List<User> _getUsers(String siteName, UserIdentity userIdentity)
134    {
135        if (userIdentity != null)
136        {
137            User user = _foUserManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
138            return user != null ? List.of(user) : List.of();
139        }
140        
141        Set<String> userPopulationsOnSite = _populationContextHelper.getUserPopulationsOnContexts(Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName), false, false);
142        return _subscribersDao.getSubscribers()
143                    .stream()
144                    .map(s -> {
145                        try
146                        {
147                            return _foUserManager.getUserByEmail(userPopulationsOnSite, s.getEmail());
148                        }
149                        catch (NotUniqueUserException e)
150                        {
151                            getLogger().warn("Can get user preference fo user with mail '" + s.getEmail() + "' because several users is found", e);
152                            return null;
153                        }
154                    })
155                    .filter(Objects::nonNull)
156                    .toList();
157    }
158    
159    @Override
160    public void setUserPreferences(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, Map<String, String> preferences) throws UserPreferencesException
161    {
162        try
163        {
164            User user = _foUserManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
165            String siteName = contextVars.get(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME);
166            
167            if (user != null && siteName != null)
168            {
169                List<Subscriber> newSubscribers = new ArrayList<>();
170                Set<String> removeTokens = new HashSet<>();
171                
172                Map<String, String> existingCategoryIds = getExistingCategoryIds(user.getEmail(), siteName);
173                
174                for (String categoryId : preferences.keySet())
175                {
176                    String value = preferences.get(categoryId);
177                    
178                    if (value.equals("true") && _categoryEP.hasCategory(categoryId) && !existingCategoryIds.containsKey(categoryId))
179                    {
180                        Subscriber subscriber = getSubscription(siteName, user, categoryId);
181                        newSubscribers.add(subscriber);
182                    }
183                    else if (!value.equals("true") && _categoryEP.hasCategory(categoryId) && existingCategoryIds.containsKey(categoryId))
184                    {
185                        String token = existingCategoryIds.get(categoryId);
186                        removeTokens.add(token);
187                    }
188                }
189                
190                // Modify the subscriptions.
191                _subscribersDao.modifySubscriptions(newSubscribers, removeTokens, UnsubscribeOrigin.SUBSCRIBER);
192            }
193        }
194        catch (Exception e)
195        {
196            String message = "Error setting newsletter user preferences for login " + userIdentity + " and context " + storageContext;
197            getLogger().error(message, e);
198            throw new UserPreferencesException(message, e);
199        }
200    }
201    
202    @Override
203    public void removeUserPreferences(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
204    {
205        try
206        {
207            User user = _foUserManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
208            
209            if (user != null)
210            {
211                String email = user.getEmail();
212                if (email != null)
213                {
214                    String siteName = contextVars.get(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME);
215                    if (siteName != null)
216                    {
217                        // Remove the subscriptions.
218                        _subscribersDao.unsubscribe(email, storageContext, UnsubscribeOrigin.DATAPOLICY);
219                    }
220                    else
221                    {
222                        try (AmetysObjectIterable<Site> sites = _siteManager.getSites())
223                        {
224                            for (Site site : sites)
225                            {
226                                // Before unsubscribing, check that no other user use the same email on the site
227                                siteName = site.getName();
228                                Set<String> userPopulationsOnSite = _populationContextHelper.getUserPopulationsOnContexts(Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName), false, false);
229                                try
230                                {
231                                    if (_foUserManager.getUserByEmail(userPopulationsOnSite, email) == null)
232                                    {
233                                        _subscribersDao.unsubscribe(email, siteName, UnsubscribeOrigin.DATAPOLICY);
234                                    }
235                                }
236                                catch (NotUniqueUserException e)
237                                {
238                                    // Do nothing, this email is still matching existing users
239                                }
240                            }
241                        }
242                    }
243                }
244            }
245        }
246        catch (Exception e)
247        {
248            String message = "Error removing newsletter subscriptions for login " + userIdentity + " and context " + storageContext;
249            getLogger().error(message, e);
250            throw new UserPreferencesException(message, e);
251        }
252    }
253    
254    @Override
255    public String getUserPreferenceAsString(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
256    {
257        String value = null;
258        
259        Boolean booleanValue = getUserPreferenceAsBoolean(userIdentity, storageContext, contextVars, id);
260        
261        if (booleanValue != null)
262        {
263            value = booleanValue.toString();
264        }
265        
266        return value;
267    }
268    
269    @Override
270    public Long getUserPreferenceAsLong(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
271    {
272        return null;
273    }
274    
275    @Override
276    public ZonedDateTime getUserPreferenceAsDate(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
277    {
278        return null;
279    }
280    
281    @Override
282    public Boolean getUserPreferenceAsBoolean(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
283    {
284        try
285        {
286            Boolean value = null;
287            
288            User user = _foUserManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
289            String siteName = contextVars.get(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME);
290            
291            if (user != null && siteName != null)
292            {
293                Subscriber subscriber = _subscribersDao.getSubscriber(user.getEmail(), siteName, id);
294                
295                value = Boolean.valueOf(subscriber != null);
296            }
297            
298            return value;
299        }
300        catch (Exception e)
301        {
302            throw new UserPreferencesException("Error getting newsletter user preferences for login " + userIdentity + " and context " + storageContext, e);
303        }
304    }
305    
306    @Override
307    public Double getUserPreferenceAsDouble(UserIdentity userIdentity, String storageContext, Map<String, String> contextVars, String id) throws UserPreferencesException
308    {
309        return null;
310    }
311    
312    /**
313     * Create a subscriber object from the given input.
314     * @param siteName the site name.
315     * @param user the user.
316     * @param categoryId the category ID.
317     * @return the Subscriber object.
318     */
319    protected Subscriber getSubscription(String siteName, User user, String categoryId)
320    {
321        return getSubscription(siteName, user, categoryId, true);
322    }
323    
324    /**
325     * Create a subscriber object from the given input.
326     * @param siteName the site name.
327     * @param user the user.
328     * @param categoryId the category ID.
329     * @param generateDateAndToken true to generate a token and set the subscription date, false otherwise.
330     * @return the Subscriber object.
331     */
332    protected Subscriber getSubscription(String siteName, User user, String categoryId, boolean generateDateAndToken)
333    {
334        Subscriber subscriber = new Subscriber();
335        subscriber.setEmail(user.getEmail());
336        subscriber.setSiteName(siteName);
337        subscriber.setCategoryId(categoryId);
338        
339        if (generateDateAndToken)
340        {
341            subscriber.setSubscribedAt(new Date());
342            
343            // Generate unique token.
344            String token = UUID.randomUUID().toString();
345            subscriber.setToken(token);
346        }
347        
348        return subscriber;
349    }
350    
351    /**
352     * Get the existing subscriptions for a user in a given site.
353     * @param email the user e-mail address.
354     * @param siteName the site name.
355     * @return a Set of category IDs, to which user has subscribed.
356     */
357    protected Map<String, String> getExistingCategoryIds(String email, String siteName)
358    {
359        Map<String, String> existingCategoryIds = new HashMap<>();
360        
361        List<Subscriber> existingSubscriptions = _subscribersDao.getSubscriptions(email, siteName);
362        for (Subscriber subscription : existingSubscriptions)
363        {
364            existingCategoryIds.put(subscription.getCategoryId(), subscription.getToken());
365        }
366        
367        return existingCategoryIds;
368    }
369}