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.syndication;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
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.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.components.source.impl.SitemapSource;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.generation.ServiceableGenerator;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.collections.CollectionUtils;
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.lang3.ArrayUtils;
039import org.apache.excalibur.source.SourceResolver;
040import org.xml.sax.SAXException;
041
042import org.ametys.core.group.Group;
043import org.ametys.core.group.GroupIdentity;
044import org.ametys.core.group.GroupManager;
045import org.ametys.core.right.RightManager;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.userpref.UserPreferencesException;
049import org.ametys.core.userpref.UserPreferencesManager;
050import org.ametys.core.util.IgnoreRootHandler;
051import org.ametys.core.util.JSONUtils;
052import org.ametys.core.util.URIUtils;
053import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
054import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
055import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
056import org.ametys.web.renderingcontext.RenderingContextHandler;
057import org.ametys.web.repository.page.Page;
058import org.ametys.web.repository.page.ZoneItem;
059import org.ametys.web.userpref.FOUserPreferencesConstants;
060
061/**
062 * Generator for feeds service
063 */
064public class FeedsGenerator extends ServiceableGenerator
065{
066    /** User pref key for urls */
067    public static final String USER_PREF_RSS_URL_KEY = "user-pref-rss-urls";
068    
069    /** User pref key for positions */
070    public static final String USER_PREF_RSS_ID_KEY = "user-pref-rss-positions";
071    
072    private static HashMap<String, Integer> _lifeTimes = new HashMap<>();
073    static 
074    {
075        _lifeTimes.put("1", 1440); // 24 hours
076        _lifeTimes.put("2", 180); // 3 hours
077        _lifeTimes.put("3", 30); // 30 minutes
078    }
079    
080    /** Conf access component */
081    protected RssFeedUserPrefsComponent _confAccess;
082    
083    /** The right manager */
084    protected RightManager _rightManager;
085    
086    /** The group manager */
087    protected GroupManager _groupManager;
088    
089    /** The feed cache */
090    protected FeedCache _feedCache;
091    
092    /** The source resolver */
093    protected SourceResolver _resolver;
094    
095    /** The user preferences manager. */
096    protected UserPreferencesManager _userPrefManager;
097    
098    /** The rendering context handler. */
099    protected RenderingContextHandler _renderingContext;
100
101    /** The current user provider */
102    protected CurrentUserProvider _currentUserProvider;
103
104    private JSONUtils _jsonUtils;
105    
106    @Override
107    public void service(ServiceManager serviceManager) throws ServiceException
108    {
109        super.service(serviceManager);
110        _resolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
111        _feedCache = (FeedCache) serviceManager.lookup(FeedCache.ROLE);
112        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
113        _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE);
114        _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE + ".FO");
115        _renderingContext = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
116        _confAccess = (RssFeedUserPrefsComponent) serviceManager.lookup(RssFeedUserPrefsComponent.ROLE);
117        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
118        _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE);
119    }
120    
121    public void generate() throws IOException, SAXException, ProcessingException
122    {
123        Request request = ObjectModelHelper.getRequest(objectModel);
124        
125        ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
126        UserIdentity user = _currentUserProvider.getUser();
127        
128        Page page = zoneItem.getZone().getPage();
129        String siteName = page.getSiteName();
130        String lang = page.getSitemapName();
131        
132        String storageContext = siteName + "/" + lang + "/" + zoneItem.getId();
133        Map<String, String> contextVars = _getContextVars(siteName, lang);
134        
135        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
136
137        List<String> urlList = new ArrayList<>();
138        List<String> idList = new ArrayList<>();
139        try
140        {
141            // Check user prefence only if the page has limited access
142            if (!_rightManager.hasAnonymousReadAccess(page))
143            {
144                urlList = _getListUrl(user, storageContext, contextVars);
145                idList = _getListId(user, storageContext, contextVars);
146            }
147        }
148        catch (UserPreferencesException e1)
149        {
150            getLogger().error("Can't have the user preference from user : '" + user + "'");
151        }
152        
153        contentHandler.startDocument();
154        _saxAllFeed(request, zoneItem, serviceParameters, urlList, idList);
155        contentHandler.endDocument();
156    }
157    
158    // Sax all the feed depend on the service parameters
159    private void _saxAllFeed(Request request, ZoneItem zoneItem, ModelAwareDataHolder serviceParameters, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
160    {
161        long defaultLength = serviceParameters.getValue("length", false, -1L);
162        String defaultLifeTime = serviceParameters.getValue("cache");
163
164        AttributesImpl atts = new AttributesImpl();
165
166        ModelAwareRepeater rssFeeds = serviceParameters.getRepeater("feeds");
167        List<ModelAwareRepeaterEntry> listFeeds = rssFeeds.getEntries().stream()
168                                                                       .filter(entry -> _checkUserAccessRSS(entry))
169                                                                       .collect(Collectors.toList());
170
171        int nbMaxFeedFullSaxed = Math.toIntExact(serviceParameters.getValue("nb-feed-max", false, 0L));
172        int nbMaxUser = Math.toIntExact(serviceParameters.getValue("nb-feed-user", false, 0L));
173        
174        atts.addCDATAAttribute("nbFeedService", String.valueOf(listFeeds.size()));
175        atts.addCDATAAttribute("zoneItemId", String.valueOf(zoneItem.getId()));
176        atts.addCDATAAttribute("nbMaxUser", String.valueOf(nbMaxUser));
177        atts.addCDATAAttribute("nbMax", String.valueOf(nbMaxFeedFullSaxed));
178        
179        Page page = (Page) request.getAttribute(Page.class.getName());
180        atts.addCDATAAttribute("showForm", String.valueOf(_confAccess.showPreferenceForm(page, nbMaxUser, listFeeds.size(), nbMaxFeedFullSaxed)));
181        
182        XMLUtils.startElement(contentHandler, "rssFeeds", atts);
183        
184        _saxFeeds(nbMaxUser, listFeeds, urlList, nbMaxFeedFullSaxed, defaultLength, defaultLifeTime, serviceParameters, userSelectedFeedsId);
185        _saxFeedsConfig(nbMaxUser, listFeeds, urlList, userSelectedFeedsId);
186        
187        XMLUtils.endElement(contentHandler, "rssFeeds");
188    }
189    
190    private void _saxFeedsConfig(long nbMaxUser, List<ModelAwareRepeaterEntry> listFeeds, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
191    {
192        for (ModelAwareRepeaterEntry rssFeed : listFeeds)
193        {
194            _saxInfoFeed(rssFeed.getValue("url"), rssFeed.getValue("title"), rssFeed.getValue("id"), false, userSelectedFeedsId.contains(rssFeed.getValue("id")));
195        }
196        
197        for (int i = 1; i <= nbMaxUser; i++)
198        {
199            boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
200            if (i <= urlList.size())
201            {
202                String url  = urlList.get(i - 1);
203                _saxInfoFeed(url, "", "feed-url" + i, true, isSelected);
204            }
205            else
206            {
207                _saxInfoFeed("", "", "feed-url" + i, true, isSelected);
208            }
209        }
210    }
211    
212    private void _saxFeeds(int nbMaxUser, List<ModelAwareRepeaterEntry> listFeeds, List<String> urlList, long nbMaxFeedFullSaxed, long defaultLength, String defaultLifeTime, ModelAwareDataHolder serviceParameters,  List<String> userSelectedFeedsId)
213    {
214        int nbFeedFullSaxed = 0;
215        for (ModelAwareRepeaterEntry rssFeed : listFeeds)
216        {
217            boolean isSelected = userSelectedFeedsId.contains(rssFeed.getValue("id"));
218            if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
219                    && (userSelectedFeedsId.isEmpty() || isSelected))
220            {
221                nbFeedFullSaxed++;
222                _saxFullInfoFeed(rssFeed, isSelected, defaultLength, defaultLifeTime); // Sax all informations
223            }
224        }
225        
226        for (int i = 1; i <= nbMaxUser; i++)
227        {
228            if (i <= urlList.size())
229            {
230                String url = urlList.get(i - 1);
231                boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
232                if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
233                        && (userSelectedFeedsId.isEmpty() || isSelected))
234                {
235                    nbFeedFullSaxed++;
236                    _saxFullInfoFeedCustom(serviceParameters, url, defaultLength, isSelected); // Sax all informations
237                }
238            }
239        }
240    }
241    
242    // Sax few info from the feed for the user preference form
243    private void _saxInfoFeed(String url, String name, String feedId, boolean isCustom, boolean isSelected) throws SAXException
244    {
245        AttributesImpl atts = new AttributesImpl();
246
247        atts.addCDATAAttribute("feedUrl", url);
248        atts.addCDATAAttribute("feedName", name);
249        atts.addCDATAAttribute("feedId", feedId);
250        atts.addCDATAAttribute("isCustom", String.valueOf(isCustom));
251        atts.addCDATAAttribute("isSelected", String.valueOf(isSelected));
252
253        XMLUtils.createElement(contentHandler, "feed-conf", atts);
254    }
255    
256    // Sax all info from the feed for the service (Feed from service parameters)
257    private void _saxFullInfoFeed(ModelAwareRepeaterEntry rssFeed, boolean isSelected, long defaultLength, String defaultLifeTime)
258    {
259        long nbItems = rssFeed.getValue("length", false, Long.valueOf(defaultLength));
260        String lifeTime = rssFeed.getValue("cache");
261        if (StringUtils.isBlank(lifeTime))
262        {
263            lifeTime = defaultLifeTime;
264        }
265        
266        String url = rssFeed.getValue("url");
267        String name = rssFeed.getValue("title");
268        
269        _saxFeed(nbItems, url, name, lifeTime, false, isSelected);
270    }
271    
272    // Sax all info from the feed for the service (Feed from user preference)
273    private void _saxFullInfoFeedCustom(ModelAwareDataHolder serviceParameters, String url, long defaultLength, boolean isSelected)
274    {
275        String lifeTime = serviceParameters.getValue("cache");
276        _saxFeed(defaultLength, url, "", lifeTime, true, isSelected);
277    }
278    
279    // Call the pipeline to sax all information from a feed
280    private void _saxFeed(long length, String url, String name, String lifeTime, Boolean isCustom, Boolean isSelected)
281    {
282        SitemapSource sitemapSource = null;
283        try
284        {
285            HashMap<String, Object> parameter = new HashMap<>();
286            parameter.put("length", length);
287            parameter.put("url", url);
288            parameter.put("name", name);
289            parameter.put("cache", _lifeTimes.get(lifeTime));
290            parameter.put("isCustom", isCustom);
291            parameter.put("isSelected", isSelected);
292            
293            sitemapSource = (SitemapSource) _resolver.resolveURI("cocoon:/feed", null, parameter);
294            sitemapSource.toSAX(new IgnoreRootHandler(contentHandler));
295        }
296        catch (Exception e)
297        {
298            getLogger().error("There is an error in the flux : '" + url + "'");
299        }
300        finally
301        {
302            _resolver.release(sitemapSource);
303        }
304    }
305    
306    // Get the list of url from the user preferences
307    private List<String> _getListUrl(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
308    {
309        if (user != null)
310        {
311            String urls = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_URL_KEY);
312            
313            List<String> urlAsList = new ArrayList<>();
314            if (StringUtils.isNotEmpty(urls))
315            {
316                String[] urlsAsArray = StringUtils.split(urls, ",");
317                for (String url : urlsAsArray)
318                {
319                    urlAsList.add(URIUtils.decode(url));
320                }
321            }
322              
323            return urlAsList;
324        }
325        
326        return new ArrayList<>();
327        
328    }
329    
330    // Get the list of chosen feed (unique id) from the user preferences
331    private List<String> _getListId(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
332    {
333        if (user != null)
334        {
335            String ids = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_ID_KEY);
336            if (StringUtils.isNotEmpty(ids))
337            {
338                String[] positionsAsArray = ids.split(",");
339                return Arrays.asList(positionsAsArray);
340            }
341        }
342        
343        return new ArrayList<>();
344    }
345    
346    // Check if the user can access to the feed
347    private boolean _checkUserAccessRSS(ModelAwareRepeaterEntry rssFeed)
348    {
349        UserIdentity user = _currentUserProvider.getUser();
350        
351        Boolean limited = rssFeed.getValue("limited");
352        if (limited)
353        {
354            // Return false if no user is logged
355            return user != null;
356        }
357        else 
358        {
359            String[] foGroupsStr = rssFeed.getValue("fo-group", false, ArrayUtils.EMPTY_STRING_ARRAY);
360            Set<GroupIdentity> foGroups = Arrays.asList(foGroupsStr).stream()
361                    .filter(identityAsStr -> identityAsStr.length() > 0)
362                    .map(identityAsStr -> _jsonUtils.convertJsonToMap(identityAsStr))
363                    .map(identityAsMap -> new GroupIdentity((String) identityAsMap.get("groupId"), (String) identityAsMap.get("groupDirectory")))
364                    .collect(Collectors.toSet());
365            
366            UserIdentity[] foUsersArray = rssFeed.getValue("fo-user", false, new UserIdentity[0]);
367            Set<UserIdentity> foUsers = Arrays.asList(foUsersArray)
368                                              .stream()
369                                              .collect(Collectors.toSet());
370            
371            if (CollectionUtils.isEmpty(foUsers) && CollectionUtils.isEmpty(foGroups))
372            {
373                return true;
374            }
375            
376            if (user == null)
377            {
378                return false;
379            }
380            
381            if (foUsers.contains(user))
382            {
383                return true;
384            }
385            
386            for (GroupIdentity foGroup : foGroups)
387            {
388                if (foGroup != null)
389                {
390                    Group group = _groupManager.getGroup(foGroup);
391                    if (group != null && group.getUsers().contains(user))
392                    {
393                        return true;
394                    }
395                }
396            }
397            
398            return false;
399        }
400    }
401    
402    private Map<String, String> _getContextVars(String siteName, String language)
403    {
404        Map<String, String> contextVars = new HashMap<>();
405        
406        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
407        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language);
408        
409        return contextVars;
410    }
411}
412
413