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.io.UnsupportedEncodingException;
020import java.net.URLDecoder;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.ProcessingException;
032import org.apache.cocoon.components.source.impl.SitemapSource;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.generation.ServiceableGenerator;
036import org.apache.cocoon.xml.AttributesImpl;
037import org.apache.cocoon.xml.XMLUtils;
038import org.apache.commons.collections.CollectionUtils;
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.lang3.ArrayUtils;
041import org.apache.excalibur.source.SourceResolver;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.contenttype.MetadataManager;
045import org.ametys.core.group.Group;
046import org.ametys.core.group.GroupIdentity;
047import org.ametys.core.group.GroupManager;
048import org.ametys.core.right.RightManager;
049import org.ametys.core.user.CurrentUserProvider;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.userpref.UserPreferencesException;
052import org.ametys.core.userpref.UserPreferencesManager;
053import org.ametys.core.util.IgnoreRootHandler;
054import org.ametys.core.util.JSONUtils;
055import org.ametys.plugins.repository.metadata.CompositeMetadata;
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        CompositeMetadata serviceParameters = zoneItem.getServiceParameters();
136
137        List<String> urlList = new ArrayList<>();
138        List<String> idList = new ArrayList<>();
139        try
140        {
141            urlList = _getListUrl(user, storageContext, contextVars);
142            idList = _getListId(user, storageContext, contextVars);
143        }
144        catch (UserPreferencesException e1)
145        {
146            getLogger().error("Can't have the user preference from user : '" + user + "'");
147        }
148        
149        contentHandler.startDocument();
150        _saxAllFeed(request, zoneItem, serviceParameters, urlList, idList);
151        contentHandler.endDocument();
152    }
153    
154    // Sax all the feed depend on the service parameters
155    private void _saxAllFeed(Request request, ZoneItem zoneItem, CompositeMetadata serviceParameters, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
156    {
157        String defaultLengthValueAsString = serviceParameters.getString("length");
158        if (StringUtils.isEmpty(defaultLengthValueAsString))
159        {
160            defaultLengthValueAsString = "-1";
161        }
162        String defaultLifeTime = serviceParameters.getString("cache");
163
164        AttributesImpl atts = new AttributesImpl();
165
166        CompositeMetadata rssFeeds = serviceParameters.getCompositeMetadata("feeds");
167        List<CompositeMetadata> listFeeds = _filterAndPreloadCacheMetadata(rssFeeds);
168
169        String nbMaxFeedFullSaxedAsString = serviceParameters.getString("nb-feed-max");
170        if (StringUtils.isEmpty(nbMaxFeedFullSaxedAsString))
171        {
172            nbMaxFeedFullSaxedAsString = "0";
173        }
174        int nbMaxFeedFullSaxed = Integer.parseInt(nbMaxFeedFullSaxedAsString);
175        
176        String nbMaxUserAsString = serviceParameters.getString("nb-feed-user");
177        if (StringUtils.isEmpty(nbMaxUserAsString))
178        {
179            nbMaxUserAsString = "0";
180        }
181        int nbMaxUser = Integer.parseInt(nbMaxUserAsString);
182        
183        atts.addCDATAAttribute("nbFeedService", String.valueOf(listFeeds.size()));
184        atts.addCDATAAttribute("zoneItemId", String.valueOf(zoneItem.getId()));
185        atts.addCDATAAttribute("nbMaxUser", String.valueOf(nbMaxUser));
186        atts.addCDATAAttribute("nbMax", String.valueOf(nbMaxFeedFullSaxed));
187        
188        Page page = (Page) request.getAttribute(Page.class.getName());
189        atts.addCDATAAttribute("showForm", String.valueOf(_confAccess.showPreferenceForm(page, nbMaxUser, listFeeds.size(), nbMaxFeedFullSaxed)));
190        
191        XMLUtils.startElement(contentHandler, "rssFeeds", atts);
192        
193        _saxFeeds(nbMaxUser, listFeeds, urlList, nbMaxFeedFullSaxed, defaultLengthValueAsString, defaultLifeTime, serviceParameters, userSelectedFeedsId);
194        _saxFeedsConfig(nbMaxUser, listFeeds, urlList, userSelectedFeedsId);
195        
196        XMLUtils.endElement(contentHandler, "rssFeeds");
197    }
198    
199    private void _saxFeedsConfig(long nbMaxUser, List<CompositeMetadata> listFeeds, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
200    {
201        for (CompositeMetadata rssFeed : listFeeds)
202        {
203            _saxInfoFeed(rssFeed.getString("url"), rssFeed.getString("title"), rssFeed.getString("id"), false, userSelectedFeedsId.contains(rssFeed.getString("id")));
204        }
205        
206        for (int i = 1; i <= nbMaxUser; i++)
207        {
208            boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
209            if (i <= urlList.size())
210            {
211                String url  = urlList.get(i - 1);
212                _saxInfoFeed(url, "", "feed-url" + i, true, isSelected);
213            }
214            else
215            {
216                _saxInfoFeed("", "", "feed-url" + i, true, isSelected);
217            }
218        }
219    }
220    
221    private void _saxFeeds(int nbMaxUser, List<CompositeMetadata> listFeeds, List<String> urlList, long nbMaxFeedFullSaxed, String defaultLengthValueAsString, String defaultLifeTime, CompositeMetadata serviceParameters,  List<String> userSelectedFeedsId)
222    {
223        int nbFeedFullSaxed = 0;
224        for (CompositeMetadata rssFeed : listFeeds)
225        {
226            boolean isSelected = userSelectedFeedsId.contains(rssFeed.getString("id"));
227            if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
228                    && (userSelectedFeedsId.isEmpty() || isSelected))
229            {
230                nbFeedFullSaxed++;
231                _saxFullInfoFeed(rssFeed, isSelected, defaultLengthValueAsString, defaultLifeTime); // Sax all informations
232            }
233        }
234        
235        for (int i = 1; i <= nbMaxUser; i++)
236        {
237            if (i <= urlList.size())
238            {
239                String url = urlList.get(i - 1);
240                boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
241                if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
242                        && (userSelectedFeedsId.isEmpty() || isSelected))
243                {
244                    nbFeedFullSaxed++;
245                    _saxFullInfoFeedCustom(serviceParameters, url, defaultLengthValueAsString, isSelected); // Sax all informations
246                }
247            }
248        }
249    }
250    
251    // Sax few info from the feed for the user preference form
252    private void _saxInfoFeed(String url, String name, String feedId, boolean isCustom, boolean isSelected) throws SAXException
253    {
254        AttributesImpl atts = new AttributesImpl();
255
256        atts.addCDATAAttribute("feedUrl", url);
257        atts.addCDATAAttribute("feedName", name);
258        atts.addCDATAAttribute("feedId", feedId);
259        atts.addCDATAAttribute("isCustom", String.valueOf(isCustom));
260        atts.addCDATAAttribute("isSelected", String.valueOf(isSelected));
261
262        XMLUtils.createElement(contentHandler, "feed-conf", atts);
263    }
264    
265    // Sax all info from the feed for the service (Feed from service parameters)
266    private void _saxFullInfoFeed(CompositeMetadata rssFeed, boolean isSelected, String defaultLengthValueAsString, String defaultLifeTime)
267    {
268        String nbItemsAsString = rssFeed.getString("length");
269        if (StringUtils.isEmpty(nbItemsAsString))
270        {
271            nbItemsAsString = defaultLengthValueAsString;
272        }
273        long nbItems = Long.parseLong(nbItemsAsString);
274        String lifeTime = rssFeed.getString("cache", defaultLifeTime);
275        if (StringUtils.isEmpty(lifeTime))
276        {
277            lifeTime = defaultLifeTime;
278        }
279        
280        String url = rssFeed.getString("url");
281        String name = rssFeed.getString("title");
282        
283        _saxFeed(nbItems, url, name, lifeTime, false, isSelected);
284    }
285    
286    // Sax all info from the feed for the service (Feed from user preference)
287    private void _saxFullInfoFeedCustom(CompositeMetadata serviceParameters, String url, String defaultLengthValueAsString, boolean isSelected)
288    {
289        String lifeTime = serviceParameters.getString("cache");
290        _saxFeed(Long.parseLong(defaultLengthValueAsString), url, "", lifeTime, true, isSelected);
291    }
292    
293    // Call the pipeline to sax all information from a feed
294    private void _saxFeed(long length, String url, String name, String lifeTime, Boolean isCustom, Boolean isSelected)
295    {
296        SitemapSource sitemapSource = null;
297        try
298        {
299            HashMap<String, Object> parameter = new HashMap<>();
300            parameter.put("length", length);
301            parameter.put("url", url);
302            parameter.put("name", name);
303            parameter.put("cache", _lifeTimes.get(lifeTime));
304            parameter.put("isCustom", isCustom);
305            parameter.put("isSelected", isSelected);
306            
307            sitemapSource = (SitemapSource) _resolver.resolveURI("cocoon:/feed", null, parameter);
308            sitemapSource.toSAX(new IgnoreRootHandler(contentHandler));
309        }
310        catch (Exception e)
311        {
312            getLogger().error("There is an error in the flux : '" + url + "'");
313        }
314        finally
315        {
316            _resolver.release(sitemapSource);
317        }
318    }
319    
320    // Get the list of url from the user preferences
321    private List<String> _getListUrl(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException, UnsupportedEncodingException
322    {
323        if (user != null)
324        {
325            String urls = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_URL_KEY);
326            
327            List<String> urlAsList = new ArrayList<>();
328            if (StringUtils.isNotEmpty(urls))
329            {
330                String[] urlsAsArray = StringUtils.split(urls, ",");
331                for (String url : urlsAsArray)
332                {
333                    urlAsList.add(URLDecoder.decode(url, "utf-8"));
334                }
335            }
336              
337            return urlAsList;
338        }
339        
340        return new ArrayList<>();
341        
342    }
343    
344    // Get the list of chosen feed (unique id) from the user preferences
345    private List<String> _getListId(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
346    {
347        if (user != null)
348        {
349            String ids = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_ID_KEY);
350            if (StringUtils.isNotEmpty(ids))
351            {
352                String[] positionsAsArray = ids.split(",");
353                return Arrays.asList(positionsAsArray);
354            }
355        }
356        
357        return new ArrayList<>();
358    }
359    
360    // prefilter before sax the feeds
361    private List<CompositeMetadata> _filterAndPreloadCacheMetadata(CompositeMetadata rssFeeds)
362    {
363        List<CompositeMetadata> listFeeds = new ArrayList<>();
364        
365        String[] metadataNames = rssFeeds.getMetadataNames();
366        Arrays.sort(metadataNames, MetadataManager.REPEATER_ENTRY_COMPARATOR);
367        
368        for (String name : metadataNames)
369        {
370            CompositeMetadata rssFeed = rssFeeds.getCompositeMetadata(name);
371
372            Boolean access = _checkUserAccessRSS(rssFeed);
373            if (access)
374            {
375                listFeeds.add(rssFeed);
376            }
377        }
378        
379        return listFeeds;
380    }
381
382    // Check if the user can access to the feed
383    private boolean _checkUserAccessRSS(CompositeMetadata rssFeed)
384    {
385        UserIdentity user = _currentUserProvider.getUser();
386        
387        Boolean limited = rssFeed.getBoolean("limited");
388        if (limited)
389        {
390            // Return false if no user is logged
391            return user != null;
392        }
393        else 
394        {
395            String[] foGroupsStr = rssFeed.getStringArray("fo-group", ArrayUtils.EMPTY_STRING_ARRAY);
396            Set<GroupIdentity> foGroups = Arrays.asList(foGroupsStr).stream()
397                    .filter(identityAsStr -> identityAsStr.length() > 0)
398                    .map(identityAsStr -> _jsonUtils.convertJsonToMap(identityAsStr))
399                    .map(identityAsMap -> new GroupIdentity((String) identityAsMap.get("groupId"), (String) identityAsMap.get("groupDirectory")))
400                    .collect(Collectors.toSet());
401            
402            String[] foUsersStr = rssFeed.getStringArray("fo-user", ArrayUtils.EMPTY_STRING_ARRAY);
403            Set<UserIdentity> foUsers = Arrays.asList(foUsersStr).stream()
404                    .filter(identityAsStr -> identityAsStr.length() > 0)
405                    .map(identityAsStr -> _jsonUtils.convertJsonToMap(identityAsStr))
406                    .map(identityAsMap -> new UserIdentity((String) identityAsMap.get("login"), (String) identityAsMap.get("populationId")))
407                    .collect(Collectors.toSet());
408            
409            if (CollectionUtils.isEmpty(foUsers) && CollectionUtils.isEmpty(foGroups))
410            {
411                return true;
412            }
413            
414            if (user == null)
415            {
416                return false;
417            }
418            
419            if (foUsers.contains(user))
420            {
421                return true;
422            }
423            
424            for (GroupIdentity foGroup : foGroups)
425            {
426                if (foGroup != null)
427                {
428                    Group group = _groupManager.getGroup(foGroup);
429                    if (group != null && group.getUsers().contains(user))
430                    {
431                        return true;
432                    }
433                }
434            }
435            
436            return false;
437        }
438    }
439    
440    private Map<String, String> _getContextVars(String siteName, String language)
441    {
442        Map<String, String> contextVars = new HashMap<>();
443        
444        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
445        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language);
446        
447        return contextVars;
448    }
449}
450
451