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