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 List<DefaultLink> links = _directoryHelper.getLinks(themeIds, user, siteName, language); 257 258 XMLUtils.startElement(contentHandler, "links"); 259 260 // SAX common links 261 _directoryHelper.saxLinks(siteName, contentHandler, links, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true, false); 262 263 // SAX the user own links 264 if (user != null && displayUserLinks) 265 { 266 List<DefaultLink> userLinks = _directoryHelper.getUserLinks(siteName, language, user).stream().collect(Collectors.toList()); 267 _directoryHelper.saxLinks(siteName, contentHandler, userLinks, _directoryHelper.getContextVars(request), siteName + "/" + language, user, true, true, true); 268 } 269 270 XMLUtils.endElement(contentHandler, "links"); 271 } 272 catch (Exception e) 273 { 274 throw new ProcessingException("An error occurred while retrieving or saxing the links", e); 275 } 276 } 277 278 /** 279 * Retrieve the configured themes names defined in the skin file link-themes.xml for the current template and current language 280 * @param skinId The skin 281 * @param template the current page's template 282 * @param lang the current language 283 * @return the list of configured themes ids or null 284 */ 285 private List<String> _getConfiguredThemes(String skinId, String template, String lang) 286 { 287 if (CollectionUtils.isNotEmpty(_themesCache.get(skinId))) 288 { 289 for (ThemeInputData themeInputData : _themesCache.get(skinId)) 290 { 291 List<String> templates = themeInputData.getTemplates(); 292 if (templates.contains(template) || templates.contains(__WILDCARD)) 293 { 294 List<String> matchingThemes = new ArrayList<>(); 295 296 List<Map<String, String>> themes = themeInputData.getThemes(); 297 for (Map<String, String> theme : themes) 298 { 299 if (theme.get("lang").equals(lang)) 300 { 301 matchingThemes.add(theme.get("id")); 302 } 303 } 304 return matchingThemes; 305 } 306 } 307 } 308 309 // The current template is not configured for a link directory input data 310 return null; 311 } 312 313 /** 314 * Retrieve the "configurable" and "applicable" attributes of a template of the configuration file 315 * @param skinId The skin 316 * @param template the template's name 317 * @return a Map with configurable and applicable configuration 318 */ 319 private Map<String, Boolean> _getTemplateProperties(String skinId, String template) 320 { 321 boolean isApplicable = false; 322 boolean isConfigurable = false; 323 boolean displayUserLinks = false; 324 325 if (CollectionUtils.isNotEmpty(_themesCache.get(skinId))) 326 { 327 for (ThemeInputData themeInputData : _themesCache.get(skinId)) 328 { 329 if (themeInputData.getTemplates().contains(template)) 330 { 331 isApplicable = true; 332 isConfigurable = themeInputData.isConfigurable(); 333 displayUserLinks = themeInputData.displayUserLinks(); 334 break; 335 } 336 } 337 } 338 339 Map<String, Boolean> result = new HashMap<> (); 340 result.put("applicable", isApplicable); 341 result.put("configurable", isConfigurable); 342 result.put("displayUserLinks", displayUserLinks); 343 344 return result; 345 } 346 347 /** 348 * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them 349 * @param skinId The skin 350 * @throws Exception if an exception occurs 351 */ 352 private void _updateConfigurationValues(String skinId) throws Exception 353 { 354 Source source = null; 355 try 356 { 357 source = _sourceResolver.resolveURI(__CONF_FILE_PATH); 358 if (source.exists()) 359 { 360 long lastModified = source.getLastModified(); 361 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 362 { 363 return; 364 } 365 366 _cacheConfigurationValues(source, skinId); 367 } 368 else 369 { 370 if (getLogger().isInfoEnabled()) 371 { 372 getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory)."); 373 } 374 375 _lastConfUpdate.put(skinId, (long) 0); 376 _themesCache.put(skinId, null); 377 } 378 } 379 finally 380 { 381 if (_sourceResolver != null && source != null) 382 { 383 _sourceResolver.release(source); 384 } 385 } 386 } 387 388 /** 389 * Read the configuration values and store them 390 * @param source the file's source 391 * @param skinId The skin 392 */ 393 private synchronized void _cacheConfigurationValues (Source source, String skinId) 394 { 395 long lastModified = source.getLastModified(); 396 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 397 { 398 // While waiting for synchronized, someone else may have updated the cache 399 return; 400 } 401 402 List<ThemeInputData> themesCache = new ArrayList<>(); 403 404 getLogger().info("Caching configuration"); 405 406 try (InputStream is = source.getInputStream()) 407 { 408 409 Configuration configuration = new DefaultConfigurationBuilder().build(is); 410 411 Configuration[] themesConfigurations = configuration.getChildren("themes"); 412 413 for (Configuration themesConfiguration : themesConfigurations) 414 { 415 List<Map<String, String>> themes = new ArrayList<> (); 416 417 Configuration[] themeConfigurations = themesConfiguration.getChildren(); 418 for (Configuration themeConfiguration : themeConfigurations) 419 { 420 Map<String, String> theme = new HashMap<> (); 421 theme.put("id", themeConfiguration.getAttribute("id", null)); 422 theme.put("lang", themeConfiguration.getAttribute("lang", null)); 423 themes.add(theme); 424 } 425 426 String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", "*"), ','); 427 428 ThemeInputData themeInputData = new ThemeInputData(Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false)); 429 themesCache.add(themeInputData); 430 } 431 432 _configurationError.remove(skinId); 433 _themesCache.put(skinId, themesCache); 434 _lastConfUpdate.put(skinId, (long) 0); 435 } 436 catch (Exception e) 437 { 438 getLogger().warn("An error occured while getting the configuration's file values", e); 439 _configurationError.put(skinId, e.getMessage()); 440 } 441 } 442 443 /** 444 * Get the current template 445 * @param request the request 446 * @return the current template 447 */ 448 private String _getTemplate(Request request) 449 { 450 return (String) request.getAttribute("template"); 451 } 452 453 454 /** 455 * Get the current skin 456 * @param request the request 457 * @return the current skin 458 */ 459 private String _getSkin(Request request) 460 { 461 return (String) request.getAttribute("skin"); 462 } 463 464 private class ThemeInputData 465 { 466 private List<String> _templates; 467 private List<Map<String, String>> _themes; 468 private boolean _configurable; 469 private boolean _displayUserLinks; 470 471 ThemeInputData (List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks) 472 { 473 _templates = templates; 474 _themes = themes; 475 _configurable = configurable; 476 _displayUserLinks = displayUserLinks; 477 } 478 479 boolean isConfigurable () 480 { 481 return _configurable; 482 } 483 484 boolean displayUserLinks() 485 { 486 return _displayUserLinks; 487 } 488 489 List<String> getTemplates () 490 { 491 return _templates; 492 } 493 494 List<Map<String, String>> getThemes () 495 { 496 return _themes; 497 } 498 499 } 500}