001/* 002 * Copyright 2018 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.linkdirectory; 017 018import java.io.InputStream; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 029import org.apache.avalon.framework.context.Context; 030import org.apache.avalon.framework.context.ContextException; 031import org.apache.avalon.framework.context.Contextualizable; 032import org.apache.avalon.framework.logger.AbstractLogEnabled; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.ProcessingException; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.xml.AttributesImpl; 040import org.apache.cocoon.xml.XMLUtils; 041import org.apache.commons.collections.CollectionUtils; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.excalibur.source.Source; 044import org.apache.excalibur.source.SourceResolver; 045import org.xml.sax.ContentHandler; 046import org.xml.sax.SAXException; 047 048import org.ametys.core.user.CurrentUserProvider; 049import org.ametys.core.user.UserIdentity; 050import org.ametys.plugins.linkdirectory.repository.DefaultLink; 051import org.ametys.web.inputdata.InputData; 052import org.ametys.web.repository.page.Page; 053import org.ametys.web.repository.site.Site; 054 055/** 056 * Input data for the link directory user preferences in thumbnails mode 057 */ 058public class LinkDirectoryInputData extends AbstractLogEnabled implements Contextualizable, InputData, Initializable, Serviceable 059{ 060 /** The path to the configuration file */ 061 private static final String __CONF_FILE_PATH = "skin://conf/link-themes.xml"; 062 063 /** The wildcard */ 064 private static final String __WILDCARD = "*"; 065 066 /** The current user provider */ 067 protected CurrentUserProvider _currentUserProvider; 068 069 /** The Avalon context */ 070 private Context _context; 071 072 /** Excalibur source resolver */ 073 private SourceResolver _sourceResolver; 074 075 private DirectoryHelper _directoryHelper; 076 077 /** Cache for themes: <skinId, List<theme info>> */ 078 private Map<String, List<ThemeInputData>> _themesCache; 079 080 private Map<String, String> _configurationError; 081 082 /** The last time the file was loaded */ 083 private Map<String, Long> _lastConfUpdate; 084 085 @Override 086 public void contextualize(Context context) throws ContextException 087 { 088 _context = context; 089 } 090 091 @Override 092 public void initialize() throws Exception 093 { 094 _lastConfUpdate = new HashMap<>(); 095 _themesCache = new HashMap<>(); 096 _configurationError = new HashMap<>(); 097 } 098 099 @Override 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 103 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 104 _directoryHelper = (DirectoryHelper) manager.lookup(DirectoryHelper.ROLE); 105 } 106 107 @Override 108 public boolean isCacheable(Site site, Page currentPage) 109 { 110 Request request = ContextHelper.getRequest(_context); 111 112 String template = _getTemplate(request); 113 if (template == null) 114 { 115 return true; 116 } 117 118 try 119 { 120 String skinId = site.getSkinId(); 121 _updateConfigurationValues(skinId); 122 if (CollectionUtils.isEmpty(_themesCache.get(skinId)) || _configurationError.containsKey(skinId)) 123 { 124 // No configuration file or there are errors 125 return true; 126 } 127 128 String language = _directoryHelper.getLanguage(request); 129 130 Map<String, Boolean> templateProperties = _getTemplateProperties(skinId, _getTemplate(request)); 131 if (!templateProperties.get("applicable")) 132 { 133 // The current template is not configured for a link directory input data 134 return true; 135 } 136 137 if (templateProperties.get("configurable") || templateProperties.get("displayUserLinks")) 138 { 139 // The applications are configurable 140 return false; 141 } 142 143 // Find the configured theme ids for this template 144 List<String> configuredThemesNames = _getConfiguredThemes(skinId, template, language); 145 List<String> configuredThemesIds = _directoryHelper.getThemesIdsFromThemesNames(site, language, configuredThemesNames); 146 String siteName = _directoryHelper.getSiteName(request); 147 148 return !_directoryHelper.hasRestrictions(siteName, language, configuredThemesIds) && !_directoryHelper.hasInternalUrl(siteName, language, configuredThemesIds); 149 } 150 catch (Exception e) 151 { 152 getLogger().error("An error occurred while retrieving information from the skin configuration", e); 153 // Configuration file is not readable => toSAX method will not generate any xml 154 return true; 155 } 156 } 157 158 @Override 159 public void toSAX(ContentHandler contentHandler) throws ProcessingException 160 { 161 Request request = ContextHelper.getRequest(_context); 162 163 // Get the current user's login if he is in the front office 164 UserIdentity user = _currentUserProvider.getUser(); 165 166 String template = _getTemplate(request); 167 if (template == null) 168 { 169 getLogger().info("There is no current template"); 170 return; 171 } 172 173 String skinId = _getSkin(request); 174 if (skinId == null) 175 { 176 getLogger().info("There is no current skin"); 177 return; 178 } 179 180 try 181 { 182 _updateConfigurationValues(skinId); 183 if (CollectionUtils.isEmpty(_themesCache.get(skinId))) 184 { 185 return; 186 } 187 188 contentHandler.startDocument(); 189 AttributesImpl attrs = new AttributesImpl(); 190 191 // Is there an error in the configuration file ? 192 if (_configurationError.containsKey(skinId)) 193 { 194 attrs.addCDATAAttribute("error", _configurationError.get(skinId)); 195 XMLUtils.createElement(contentHandler, "linkDirectory", attrs); 196 } 197 else 198 { 199 Map<String, Boolean> templateProperties = _getTemplateProperties(skinId, template); 200 attrs.addCDATAAttribute("applicable", templateProperties.get("applicable") ? "true" : "false"); 201 attrs.addCDATAAttribute("configurable", templateProperties.get("configurable") ? "true" : "false"); 202 203 XMLUtils.startElement(contentHandler, "linkDirectory", attrs); 204 205 String language = _directoryHelper.getLanguage(request); 206 String siteName = _directoryHelper.getSiteName(request); 207 208 List<String> configuredThemesNames = _getConfiguredThemes(skinId, template, _directoryHelper.getLanguage(request)); 209 if (configuredThemesNames != null) 210 { 211 Map<String, List<String>> themesMap = _directoryHelper.getThemesMap(configuredThemesNames, siteName, language); 212 List<String> correctThemesIds = themesMap.get("themes"); 213 List<String> unknownThemesNames = themesMap.get("unknown-themes"); 214 215 _saxThemes(contentHandler, correctThemesIds, unknownThemesNames); 216 _saxLinks(contentHandler, user, request, correctThemesIds, templateProperties.get("displayUserLinks")); 217 } 218 219 XMLUtils.endElement(contentHandler, "linkDirectory"); 220 } 221 222 contentHandler.endDocument(); 223 } 224 catch (Exception e) 225 { 226 getLogger().error("An exception occurred during the processing of the link directory's input data" , e); 227 } 228 } 229 230 private void _saxThemes(ContentHandler contentHandler, List<String> themeIds, List<String> unknownThemesNames) throws SAXException 231 { 232 if (!themeIds.isEmpty()) 233 { 234 XMLUtils.startElement(contentHandler, "themes"); 235 for (String themeId : themeIds) 236 { 237 XMLUtils.createElement(contentHandler, "theme", themeId); 238 } 239 XMLUtils.endElement(contentHandler, "themes"); 240 } 241 242 if (!unknownThemesNames.isEmpty()) 243 { 244 AttributesImpl attr = new AttributesImpl(); 245 attr.addCDATAAttribute("count", Integer.toString(unknownThemesNames.size())); 246 XMLUtils.createElement(contentHandler, "unknown-themes", attr, StringUtils.join(unknownThemesNames, ", ")); 247 } 248 } 249 250 private void _saxLinks(ContentHandler contentHandler, UserIdentity user, Request request, List<String> themeIds, boolean displayUserLinks) throws ProcessingException 251 { 252 String language = _directoryHelper.getLanguage(request); 253 String siteName = _directoryHelper.getSiteName(request); 254 try 255 { 256 // SAX common links 257 List<DefaultLink> links = _directoryHelper.getLinks(themeIds, user, siteName, language); 258 259 XMLUtils.startElement(contentHandler, "links"); 260 261 262 // SAX the user own links 263 List<DefaultLink> userLinks = null; 264 if (user != null && displayUserLinks) 265 { 266 userLinks = _directoryHelper.getUserLinks(siteName, language, user).stream().collect(Collectors.toList()); 267 } 268 269 _directoryHelper.saxLinks(siteName, contentHandler, links, userLinks, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true); 270 271 XMLUtils.endElement(contentHandler, "links"); 272 } 273 catch (Exception e) 274 { 275 throw new ProcessingException("An error occurred while retrieving or saxing the links", e); 276 } 277 } 278 279 /** 280 * Retrieve the configured themes names defined in the skin file link-themes.xml for the current template and current language 281 * @param skinId The skin 282 * @param template the current page's template 283 * @param lang the current language 284 * @return the list of configured themes ids or null 285 */ 286 private List<String> _getConfiguredThemes(String skinId, String template, String lang) 287 { 288 if (CollectionUtils.isNotEmpty(_themesCache.get(skinId))) 289 { 290 for (ThemeInputData themeInputData : _themesCache.get(skinId)) 291 { 292 List<String> templates = themeInputData.getTemplates(); 293 if (templates.contains(template) || templates.contains(__WILDCARD)) 294 { 295 List<String> matchingThemes = new ArrayList<>(); 296 297 List<Map<String, String>> themes = themeInputData.getThemes(); 298 for (Map<String, String> theme : themes) 299 { 300 if (theme.get("lang").equals(lang)) 301 { 302 matchingThemes.add(theme.get("id")); 303 } 304 } 305 return matchingThemes; 306 } 307 } 308 } 309 310 // The current template is not configured for a link directory input data 311 return null; 312 } 313 314 /** 315 * Retrieve the "configurable" and "applicable" attributes of a template of the configuration file 316 * @param skinId The skin 317 * @param template the template's name 318 * @return a Map with configurable and applicable configuration 319 */ 320 private Map<String, Boolean> _getTemplateProperties(String skinId, String template) 321 { 322 boolean isApplicable = false; 323 boolean isConfigurable = false; 324 boolean displayUserLinks = false; 325 326 if (CollectionUtils.isNotEmpty(_themesCache.get(skinId))) 327 { 328 for (ThemeInputData themeInputData : _themesCache.get(skinId)) 329 { 330 if (themeInputData.getTemplates().contains(template)) 331 { 332 isApplicable = true; 333 isConfigurable = themeInputData.isConfigurable(); 334 displayUserLinks = themeInputData.displayUserLinks(); 335 break; 336 } 337 } 338 } 339 340 Map<String, Boolean> result = new HashMap<> (); 341 result.put("applicable", isApplicable); 342 result.put("configurable", isConfigurable); 343 result.put("displayUserLinks", displayUserLinks); 344 345 return result; 346 } 347 348 /** 349 * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them 350 * @param skinId The skin 351 * @throws Exception if an exception occurs 352 */ 353 private void _updateConfigurationValues(String skinId) throws Exception 354 { 355 Source source = null; 356 try 357 { 358 source = _sourceResolver.resolveURI(__CONF_FILE_PATH); 359 if (source.exists()) 360 { 361 long lastModified = source.getLastModified(); 362 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 363 { 364 return; 365 } 366 367 _cacheConfigurationValues(source, skinId); 368 } 369 else 370 { 371 if (getLogger().isInfoEnabled()) 372 { 373 getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory)."); 374 } 375 376 _lastConfUpdate.put(skinId, (long) 0); 377 _themesCache.put(skinId, null); 378 } 379 } 380 finally 381 { 382 if (_sourceResolver != null && source != null) 383 { 384 _sourceResolver.release(source); 385 } 386 } 387 } 388 389 /** 390 * Read the configuration values and store them 391 * @param source the file's source 392 * @param skinId The skin 393 */ 394 private synchronized void _cacheConfigurationValues (Source source, String skinId) 395 { 396 long lastModified = source.getLastModified(); 397 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 398 { 399 // While waiting for synchronized, someone else may have updated the cache 400 return; 401 } 402 403 List<ThemeInputData> themesCache = new ArrayList<>(); 404 405 getLogger().info("Caching configuration"); 406 407 try (InputStream is = source.getInputStream()) 408 { 409 410 Configuration configuration = new DefaultConfigurationBuilder().build(is); 411 412 Configuration[] themesConfigurations = configuration.getChildren("themes"); 413 414 for (Configuration themesConfiguration : themesConfigurations) 415 { 416 List<Map<String, String>> themes = new ArrayList<> (); 417 418 Configuration[] themeConfigurations = themesConfiguration.getChildren(); 419 for (Configuration themeConfiguration : themeConfigurations) 420 { 421 Map<String, String> theme = new HashMap<> (); 422 theme.put("id", themeConfiguration.getAttribute("id", null)); 423 theme.put("lang", themeConfiguration.getAttribute("lang", null)); 424 themes.add(theme); 425 } 426 427 String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", "*"), ','); 428 429 ThemeInputData themeInputData = new ThemeInputData(Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false)); 430 themesCache.add(themeInputData); 431 } 432 433 _configurationError.remove(skinId); 434 _themesCache.put(skinId, themesCache); 435 _lastConfUpdate.put(skinId, (long) 0); 436 } 437 catch (Exception e) 438 { 439 getLogger().warn("An error occured while getting the configuration's file values", e); 440 _configurationError.put(skinId, e.getMessage()); 441 } 442 } 443 444 /** 445 * Get the current template 446 * @param request the request 447 * @return the current template 448 */ 449 private String _getTemplate(Request request) 450 { 451 return (String) request.getAttribute("template"); 452 } 453 454 455 /** 456 * Get the current skin 457 * @param request the request 458 * @return the current skin 459 */ 460 private String _getSkin(Request request) 461 { 462 return (String) request.getAttribute("skin"); 463 } 464 465 private class ThemeInputData 466 { 467 private List<String> _templates; 468 private List<Map<String, String>> _themes; 469 private boolean _configurable; 470 private boolean _displayUserLinks; 471 472 ThemeInputData (List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks) 473 { 474 _templates = templates; 475 _themes = themes; 476 _configurable = configurable; 477 _displayUserLinks = displayUserLinks; 478 } 479 480 boolean isConfigurable () 481 { 482 return _configurable; 483 } 484 485 boolean displayUserLinks() 486 { 487 return _displayUserLinks; 488 } 489 490 List<String> getTemplates () 491 { 492 return _templates; 493 } 494 495 List<Map<String, String>> getThemes () 496 { 497 return _themes; 498 } 499 500 } 501}