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