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