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