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-directory.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 String siteName = _directoryHelper.getSiteName(request); 154 155 return !_directoryHelper.hasRestrictions(siteName, language, configuredThemesNames) && !_directoryHelper.hasInternalUrl(siteName, language, configuredThemesNames); 156 } 157 catch (Exception e) 158 { 159 getLogger().error("An error occurred while retrieving information from the skin configuration", e); 160 // Configuration file is not readable => toSAX method will not generate any xml 161 return true; 162 } 163 } 164 165 @Override 166 public void toSAX(ContentHandler contentHandler) throws ProcessingException 167 { 168 Request request = ContextHelper.getRequest(_context); 169 170 // Get the current user's login if he is in the front office 171 UserIdentity user = _currentUserProvider.getUser(); 172 173 String template = _getTemplate(request); 174 if (template == null) 175 { 176 getLogger().info("There is no current template"); 177 return; 178 } 179 180 String skinId = _getSkin(request); 181 if (skinId == null) 182 { 183 getLogger().info("There is no current skin"); 184 return; 185 } 186 187 try 188 { 189 _updateConfigurationValues(skinId); 190 if (CollectionUtils.isEmpty(_themesCache.get(skinId))) 191 { 192 return; 193 } 194 195 contentHandler.startDocument(); 196 197 // Is there an error in the configuration file ? 198 if (_configurationError.containsKey(skinId)) 199 { 200 AttributesImpl attrs = new AttributesImpl(); 201 attrs.addCDATAAttribute("error", _configurationError.get(skinId)); 202 XMLUtils.createElement(contentHandler, "linkDirectory", attrs); 203 } 204 else 205 { 206 String language = _directoryHelper.getLanguage(request); 207 String siteName = _directoryHelper.getSiteName(request); 208 List<ThemeInputData> themeInputDatas = _getThemesForSkinAndTemplate(skinId, template); 209 for (ThemeInputData themeInputData : themeInputDatas) 210 { 211 AttributesImpl attrs = new AttributesImpl(); 212 attrs.addCDATAAttribute("applicable", Boolean.TRUE.toString()); 213 attrs.addCDATAAttribute("configurable", String.valueOf(themeInputData.isConfigurable())); 214 attrs.addCDATAAttribute("displayUserLinks", String.valueOf(themeInputData.displayUserLinks())); 215 attrs.addCDATAAttribute("id", themeInputData.getId()); 216 217 XMLUtils.startElement(contentHandler, "linkDirectory", attrs); 218 219 List<String> configuredThemesNames = _getConfiguredThemes(themeInputData, language); 220 if (configuredThemesNames != null) 221 { 222 Map<String, List<String>> themesMap = _directoryHelper.getThemesMap(configuredThemesNames, siteName, language); 223 List<String> correctThemesIds = themesMap.get("themes"); 224 List<String> unknownThemesNames = themesMap.get("unknown-themes"); 225 226 _saxThemes(contentHandler, correctThemesIds, unknownThemesNames); 227 _saxLinks(contentHandler, user, request, correctThemesIds, themeInputData.displayUserLinks(), themeInputData.getId()); 228 } 229 230 XMLUtils.endElement(contentHandler, "linkDirectory"); 231 } 232 } 233 } 234 catch (Exception e) 235 { 236 getLogger().error("An exception occurred during the processing of the link directory's input data" , e); 237 } 238 } 239 240 private void _saxThemes(ContentHandler contentHandler, List<String> themeIds, List<String> unknownThemesNames) throws SAXException 241 { 242 if (!themeIds.isEmpty()) 243 { 244 XMLUtils.startElement(contentHandler, "themes"); 245 for (String themeId : themeIds) 246 { 247 XMLUtils.createElement(contentHandler, "theme", themeId); 248 } 249 XMLUtils.endElement(contentHandler, "themes"); 250 } 251 252 if (!unknownThemesNames.isEmpty()) 253 { 254 AttributesImpl attr = new AttributesImpl(); 255 attr.addCDATAAttribute("count", Integer.toString(unknownThemesNames.size())); 256 XMLUtils.createElement(contentHandler, "unknown-themes", attr, StringUtils.join(unknownThemesNames, ", ")); 257 } 258 } 259 260 private void _saxLinks(ContentHandler contentHandler, UserIdentity user, Request request, List<String> themeIds, boolean displayUserLinks, String specificContext) throws ProcessingException 261 { 262 String language = _directoryHelper.getLanguage(request); 263 String siteName = _directoryHelper.getSiteName(request); 264 try 265 { 266 // SAX common links 267 List<DefaultLink> links = _directoryHelper.getLinks(themeIds, user, siteName, language); 268 269 List<DefaultLink> userLinks = null; 270 if (user != null && displayUserLinks) 271 { 272 userLinks = _directoryHelper.getUserLinks(siteName, language, user).stream().collect(Collectors.toList()); 273 } 274 275 276 // SAX the user own links 277 XMLUtils.startElement(contentHandler, "links"); 278 279 try 280 { 281 String storageContext = siteName + "/" + language; 282 if (StringUtils.isNotEmpty(specificContext)) 283 { 284 storageContext += "/" + specificContext; 285 } 286 _directoryHelper.saxLinks(siteName, contentHandler, links, userLinks, _directoryHelper.getContextVars(request), storageContext, user); 287 } 288 catch (Exception e) 289 { 290 getLogger().error("An exception occurred while saxing the links", e); 291 } 292 293 XMLUtils.endElement(contentHandler, "links"); 294 } 295 catch (Exception e) 296 { 297 throw new ProcessingException("An error occurred while retrieving or saxing the links", e); 298 } 299 } 300 301 /** 302 * Retrieve the configured themes names defined in the skin file link-directory.xml for the current input data and the current language 303 * @param themeInputData Can be an empty {@link String} 304 * @param lang language to filter by. Themes with lang=null will always be returned. 305 * @return the list of configured themes ids, can be empty, cannot be null 306 */ 307 private List<String> _getConfiguredThemes(ThemeInputData themeInputData, String lang) 308 { 309 return themeInputData.getThemes() 310 .stream() 311 .filter(t -> t.get("lang") == null || t.get("lang").equals(lang)) 312 .map(t -> t.get("id")) 313 .collect(Collectors.toList()); 314 } 315 316 private List<ThemeInputData> _getThemesForSkinAndTemplate(String skinId, String template) 317 { 318 return _themesCache.getOrDefault(skinId, new ArrayList<>()) 319 .stream() 320 .filter(t -> _filterByTemplate(t, template)) 321 .collect(Collectors.toList()); 322 } 323 324 private boolean _filterByTemplate(ThemeInputData theme, String template) 325 { 326 List<String> templates = theme.getTemplates(); 327 return templates.contains(template) || templates.contains(__WILDCARD); 328 } 329 330 /** 331 * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them 332 * @param skinId The skin 333 * @throws Exception if an exception occurs 334 */ 335 private void _updateConfigurationValues(String skinId) throws Exception 336 { 337 Source source = null; 338 try 339 { 340 source = _sourceResolver.resolveURI(__CONF_FILE_PATH); 341 if (source.exists()) 342 { 343 long lastModified = source.getLastModified(); 344 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 345 { 346 return; 347 } 348 349 _cacheConfigurationValues(source, skinId); 350 } 351 else 352 { 353 if (getLogger().isInfoEnabled()) 354 { 355 getLogger().info("There is no configuration file at path '" + __CONF_FILE_PATH + "' (no input data for link directory)."); 356 } 357 358 _lastConfUpdate.put(skinId, (long) 0); 359 _themesCache.put(skinId, null); 360 } 361 } 362 finally 363 { 364 if (_sourceResolver != null && source != null) 365 { 366 _sourceResolver.release(source); 367 } 368 } 369 } 370 371 /** 372 * Read the configuration values and store them 373 * @param source the file's source 374 * @param skinId The skin 375 */ 376 private synchronized void _cacheConfigurationValues (Source source, String skinId) 377 { 378 long lastModified = source.getLastModified(); 379 if (_lastConfUpdate.containsKey(skinId) && _lastConfUpdate.get(skinId) != 0 && lastModified == _lastConfUpdate.get(skinId)) 380 { 381 // While waiting for synchronized, someone else may have updated the cache 382 return; 383 } 384 385 List<ThemeInputData> themesCache = new ArrayList<>(); 386 387 getLogger().info("Caching configuration"); 388 389 try (InputStream is = source.getInputStream()) 390 { 391 Configuration configuration = new DefaultConfigurationBuilder().build(is); 392 393 Configuration[] themesConfigurations = configuration.getChild("inputdata").getChildren("themes"); 394 395 for (Configuration themesConfiguration : themesConfigurations) 396 { 397 List<Map<String, String>> themes = new ArrayList<> (); 398 399 Configuration[] themeConfigurations = themesConfiguration.getChildren(); 400 for (Configuration themeConfiguration : themeConfigurations) 401 { 402 Map<String, String> theme = new HashMap<> (); 403 String id = themeConfiguration.getAttribute("id", null); 404 theme.put("id", id); 405 theme.put("lang", themeConfiguration.getAttribute("lang", null)); 406 themes.add(theme); 407 } 408 409 String[] templates = StringUtils.split(themesConfiguration.getAttribute("templates", __WILDCARD), ','); 410 411 ThemeInputData themeInputData = new ThemeInputData(themesConfiguration.getAttribute("inputDataId", StringUtils.EMPTY), Arrays.asList(templates), themes, themesConfiguration.getAttributeAsBoolean("configurable", false), themesConfiguration.getAttributeAsBoolean("displayUserLinks", false)); 412 themesCache.add(themeInputData); 413 } 414 415 _configurationError.remove(skinId); 416 _themesCache.put(skinId, themesCache); 417 _lastConfUpdate.put(skinId, (long) 0); 418 } 419 catch (Exception e) 420 { 421 getLogger().warn("An error occured while getting the configuration's file values", e); 422 _configurationError.put(skinId, e.getMessage()); 423 } 424 } 425 426 /** 427 * Get the current template 428 * @param request the request 429 * @return the current template 430 */ 431 private String _getTemplate(Request request) 432 { 433 return (String) request.getAttribute("template"); 434 } 435 436 437 /** 438 * Get the current skin 439 * @param request the request 440 * @return the current skin 441 */ 442 private String _getSkin(Request request) 443 { 444 return (String) request.getAttribute("skin"); 445 } 446 447 private class ThemeInputData 448 { 449 private String _id; 450 private List<String> _templates; 451 private List<Map<String, String>> _themes; 452 private boolean _configurable; 453 private boolean _displayUserLinks; 454 455 ThemeInputData (String id, List<String> templates, List<Map<String, String>> themes, boolean configurable, boolean displayUserLinks) 456 { 457 _id = id; 458 _templates = templates; 459 _themes = themes; 460 _configurable = configurable; 461 _displayUserLinks = displayUserLinks; 462 } 463 464 boolean isConfigurable () 465 { 466 return _configurable; 467 } 468 469 boolean displayUserLinks() 470 { 471 return _displayUserLinks; 472 } 473 474 List<String> getTemplates () 475 { 476 return _templates; 477 } 478 479 List<Map<String, String>> getThemes () 480 { 481 return _themes; 482 } 483 484 String getId() 485 { 486 return _id; 487 } 488 } 489}