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