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