001/* 002 * Copyright 2012 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.odfweb.repository; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Comparator; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.Set; 027import java.util.TreeMap; 028 029import org.apache.avalon.framework.activity.Initializable; 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.repository.ContentTypeExpression; 037import org.ametys.cms.repository.LanguageExpression; 038import org.ametys.core.cache.AbstractCacheManager; 039import org.ametys.core.cache.Cache; 040import org.ametys.odf.ProgramItem; 041import org.ametys.odf.program.Program; 042import org.ametys.odf.program.ProgramFactory; 043import org.ametys.odf.tree.OdfClassificationHandler.LevelValue; 044import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 045import org.ametys.plugins.repository.AmetysObject; 046import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 047import org.ametys.plugins.repository.AmetysObjectIterable; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.CollectionIterable; 050import org.ametys.plugins.repository.UnknownAmetysObjectException; 051import org.ametys.plugins.repository.provider.WorkspaceSelector; 052import org.ametys.plugins.repository.query.QueryHelper; 053import org.ametys.plugins.repository.query.expression.AndExpression; 054import org.ametys.plugins.repository.query.expression.Expression.Operator; 055import org.ametys.plugins.repository.query.expression.StringExpression; 056import org.ametys.runtime.i18n.I18nizableText; 057import org.ametys.runtime.plugin.component.AbstractLogEnabled; 058import org.ametys.web.repository.page.Page; 059 060import com.google.common.collect.Iterables; 061 062/** 063 * Maintains a per-request cache, dispatching ODF virtual pages accross degrees and domains. 064 */ 065public class ODFPageCache extends AbstractLogEnabled implements Serviceable, Initializable, Component 066{ 067 /** Avalon role. */ 068 public static final String ROLE = ODFPageCache.class.getName(); 069 070 // Tree key when no first level metadata has been selected 071 private static final String __NO_FIRST_DATA_KEY = "_no-first-data"; 072 073 // Tree key when no second level metadata has been selected 074 private static final String __NO_SECOND_DATA_KEY = "_no-second-data"; 075 076 private static final String __TREE_CACHE = ODFPageCache.class.getName() + "$tree"; 077 private static final String __PROGRAM_CACHE = ODFPageCache.class.getName() + "$program"; 078 private static final String __INDEXABLE_CACHE = ODFPageCache.class.getName() + "$indexable"; 079 080 private OdfPageHandler _odfPageHandler; 081 private WorkspaceSelector _workspaceSelector; 082 private AmetysObjectResolver _resolver; 083 084 private AmetysObjectFactoryExtensionPoint _ametysObjectFactoryEP; 085 086 private AbstractCacheManager _cacheManager; 087 088 @Override 089 public void service(ServiceManager manager) throws ServiceException 090 { 091 _odfPageHandler = (OdfPageHandler) manager.lookup(OdfPageHandler.ROLE); 092 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 093 _ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 094 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 095 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 096 } 097 098 public void initialize() throws Exception 099 { 100 _cacheManager.createRequestCache(__TREE_CACHE, 101 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_TREE_LABEL"), 102 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_TREE_DESCRIPTION"), 103 false); 104 _cacheManager.createRequestCache(__PROGRAM_CACHE, 105 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_PROGRAM_LABEL"), 106 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_PAGE_PROGRAM_DESCRIPTION"), 107 false); 108 _cacheManager.createRequestCache(__INDEXABLE_CACHE, 109 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_INDEXABLE_LABEL"), 110 new I18nizableText("plugin.odf-web", "PLUGINS_ODF_WEB_CACHE_ODF_ROOT_INDEXABLE_DESCRIPTION"), 111 false); 112 } 113 114 Map<String, Map<String, Collection<Program>>> getProgramCache(Page rootPage, boolean computeIfNotPresent) 115 { 116 String workspace = _workspaceSelector.getWorkspace(); 117 String pageId = rootPage.getId(); 118 119 Cache<ODFPageCacheKey, Map<String, Map<String, Collection<Program>>>> odfPageCache = _getODFPageCache(); 120 Map<String, Map<String, Collection<Program>>> level1Tree = odfPageCache.get(ODFPageCacheKey.of(workspace, pageId)); 121 122 if (level1Tree == null) 123 { 124 if (!computeIfNotPresent) 125 { 126 return null; 127 } 128 129 Map<String, LevelValue> level1Values = _odfPageHandler.getLevel1Values(rootPage); 130 Set<String> level1Codes = level1Values.keySet(); 131 132 LevelComparator level1Comparator = new LevelComparator(level1Values); 133 134 Map<String, LevelValue> level2Values = _odfPageHandler.getLevel2Values(rootPage); 135 Set<String> level2Codes = level2Values.keySet(); 136 LevelComparator level2Comparator = new LevelComparator(level2Values); 137 138 level1Tree = new TreeMap<>(level1Comparator); 139 odfPageCache.put(ODFPageCacheKey.of(workspace, pageId), level1Tree); 140 141 AmetysObjectIterable<Program> programs = _odfPageHandler.getProgramsWithRestrictions(rootPage, null, null, null, null); 142 143 String level1MetaPath = _odfPageHandler.getLevel1Metadata(rootPage); 144 String level2MetaPath = _odfPageHandler.getLevel2Metadata(rootPage); 145 146 for (Program program : programs) 147 { 148 if (StringUtils.isBlank(level1MetaPath)) 149 { 150 Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(__NO_FIRST_DATA_KEY, x -> new TreeMap<>(level2Comparator)); 151 Collection<Program> programCache = level2Tree.computeIfAbsent(__NO_SECOND_DATA_KEY, x -> new ArrayList<>()); 152 153 programCache.add(program); 154 } 155 else if (StringUtils.isBlank(level2MetaPath)) 156 { 157 String programL1Value = _odfPageHandler.getProgramLevelValue(program, level1MetaPath); 158 if (level1Codes.contains(programL1Value)) 159 { 160 Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(programL1Value, x -> new TreeMap<>(level2Comparator)); 161 Collection<Program> programCache = level2Tree.computeIfAbsent(__NO_SECOND_DATA_KEY, x -> new ArrayList<>()); 162 163 programCache.add(program); 164 } 165 } 166 else 167 { 168 String programL1Value = _odfPageHandler.getProgramLevelValue(program, level1MetaPath); 169 String programL2Value = _odfPageHandler.getProgramLevelValue(program, level2MetaPath); 170 171 if (level1Codes.contains(programL1Value) && level2Codes.contains(programL2Value)) 172 { 173 Map<String, Collection<Program>> level2Tree = level1Tree.computeIfAbsent(programL1Value, x -> new TreeMap<>(level2Comparator)); 174 Collection<Program> programCache = level2Tree.computeIfAbsent(programL2Value, x -> new ArrayList<>()); 175 176 programCache.add(program); 177 } 178 } 179 } 180 } 181 182 return level1Tree; 183 } 184 185 boolean areChildrenIndexable(Page rootPage) 186 { 187 return _getODFRootIndexablePageCache().get(rootPage.getId(), __ -> rootPage.getValue(AbstractOdfPage.INDEXABLE_CHILDREN, true)); 188 } 189 190 private static class LevelComparator implements Comparator<String> 191 { 192 private final Map<String, LevelValue> _levelValues; 193 194 public LevelComparator(Map<String, LevelValue> initialMap) 195 { 196 _levelValues = initialMap; 197 } 198 199 public int compare(String s1, String s2) 200 { 201 if (_levelValues.containsKey(s1)) 202 { 203 // Compare levels on orders then labels 204 return _levelValues.get(s1).compareTo(_levelValues.get(s2)); 205 } 206 else 207 { 208 // _no-first-data or _no-second-data 209 return 0; 210 } 211 } 212 } 213 214 /** 215 * Get programs to given levels 216 * @param rootPage The ODF root page 217 * @param level1 The value of first level or <code>null</code> if there is no first level 218 * @param level2 The value of second level or <code>null</code> if there is no second level 219 * @param computeIfNotPresent When false, no result will be returned if the root page in not already in the cache 220 * @return The matching programs 221 */ 222 Optional<AmetysObjectIterable<Program>> getPrograms(Page rootPage, String level1, String level2, boolean computeIfNotPresent) 223 { 224 return Optional.ofNullable(getProgramCache(rootPage, computeIfNotPresent)) 225 .map(firstLevelCache -> firstLevelCache.get(Optional.ofNullable(level1).orElse(__NO_FIRST_DATA_KEY))) 226 .map(secondLevelCache -> secondLevelCache.get(Optional.ofNullable(level2).orElse(__NO_SECOND_DATA_KEY))) 227 .map(CollectionIterable<Program>::new); 228 } 229 230 /** 231 * Get the child page from its relative path. 232 * @param rootPage The ODF root page 233 * @param parentPage The parent page 234 * @param level1 The value of first level or <code>null</code> if there is no first level 235 * @param level2 The value of second level or <code>null</code> if there is no second level 236 * @param path the path of the child page 237 * @return the child page 238 * @throws UnknownAmetysObjectException if no child page was found at the given path 239 */ 240 Page getChildProgramPage(Page rootPage, Page parentPage, String level1, String level2, String path) throws UnknownAmetysObjectException 241 { 242 List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2)); 243 String queuePath = Iterables.get(headQueuePath, 1, null); 244 245 String pageName = headQueuePath.get(0); 246 247 ProgramPageFactory programPageFactory = (ProgramPageFactory) _ametysObjectFactoryEP.getExtension(ProgramPageFactory.class.getName()); 248 249 return getProgramFromPageName(rootPage, level1, level2, pageName) 250 .map(program -> { 251 return programPageFactory.createProgramPage(rootPage, program, null, null, parentPage); 252 }) 253 .map(cp -> _odfPageHandler.addRedirectIfNeeded(cp, pageName)) 254 .map(cp -> _odfPageHandler.exploreQueuePath(cp, queuePath)) 255 .orElseThrow(() -> new UnknownAmetysObjectException("There's no program for page's name " + pageName)); 256 } 257 258 /** 259 * Get cached program corresponding to the page name 260 * @param rootPage The ODF root page 261 * @param level1 The value of first level or <code>null</code> if there is no first level 262 * @param level2 The value of second level or <code>null</code> if there is no second level 263 * @param pageName the page's name 264 * @return The program if found 265 */ 266 Optional<Program> getProgramFromPageName(Page rootPage, String level1, String level2, String pageName) 267 { 268 // Page's name is like [title]-[code] 269 String programName = null; 270 String programCode = null; 271 272 // FIXME Search for program's name for legacy purpose. To be removed in a later version. 273 // In legacy, page's name can be like the program name (program-[title]) 274 int j = pageName.lastIndexOf("program-"); 275 if (j != -1) 276 { 277 String name = pageName.substring(j); 278 // The page name contains "program-", so it could be a legacy page name. 279 // ... but it also could be a program whose title contains the text "program" 280 // ... so check if the program name exist to assure it a legacy page name 281 if (_doesProgramNameExist(rootPage, name)) 282 { 283 programName = name; 284 } 285 } 286 287 if (programName == null) 288 { 289 j = pageName.lastIndexOf("-"); 290 programCode = j == -1 ? pageName : pageName.substring(j + 1); 291 } 292 293 return Optional.ofNullable(getProgram(rootPage, level1, level2, programCode, programName)); 294 } 295 296 private boolean _doesProgramNameExist(Page rootPage, String programName) 297 { 298 String catalog = _odfPageHandler.getCatalog(rootPage); 299 String lang = rootPage.getSitemapName(); 300 301 AndExpression expression = new AndExpression(); 302 expression.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE)); 303 expression.add(new LanguageExpression(Operator.EQ, lang)); 304 if (catalog != null) 305 { 306 expression.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog)); 307 } 308 309 String xPathQuery = QueryHelper.getXPathQuery(programName, "ametys:content", expression); 310 AmetysObjectIterable<AmetysObject> programs = _resolver.query(xPathQuery); 311 return programs.getSize() > 0; 312 } 313 314 /** 315 * Get cached program 316 * @param rootPage The ODF root page 317 * @param level1 The value of first level or <code>null</code> if there is no first level 318 * @param level2 The value of second level or <code>null</code> if there is no second level 319 * @param programCode The code of program. Can be null if programName is not null. 320 * @param programName The name of program. Can be null if programCode is not null. 321 * @return program The program 322 */ 323 Program getProgram(Page rootPage, String level1, String level2, String programCode, String programName) 324 { 325 String workspace = _workspaceSelector.getWorkspace(); 326 String pageId = rootPage.getId(); 327 328 // Map<level1, Map<level2, Map<program name, Program>>> 329 Cache<ODFPageProgramCacheKey, Program> odfPageCache = _getODFPageProgramCache(); 330 331 String level1Key = Objects.toString(level1, __NO_FIRST_DATA_KEY); 332 String level2Key = Objects.toString(level2, __NO_SECOND_DATA_KEY); 333 334 // For legacy purpose we use the programName when the programCode is null. 335 String programKey = Objects.toString(programCode, programName); 336 337 return odfPageCache.get( 338 ODFPageProgramCacheKey.of(workspace, pageId, level1Key, level2Key, programKey), 339 key -> this._getProgramWithRestrictions(rootPage, level1, level2, programCode, programName) 340 ); 341 } 342 343 private Program _getProgramWithRestrictions(Page rootPage, String level1, String level2, String programCode, String programName) 344 { 345 return _odfPageHandler.getProgramsWithRestrictions(rootPage, level1, level2, programCode, programName).stream() 346 .findFirst().orElse(null); 347 } 348 349 /** 350 * Clear page cache 351 * @param rootPage The ODF root page 352 */ 353 public void clearCache (Page rootPage) 354 { 355 _getODFPageCache().invalidate(ODFPageCacheKey.of(null, rootPage.getId())); 356 _getODFPageProgramCache().invalidate(ODFPageProgramCacheKey.of(null, rootPage.getId(), null, null, null)); 357 _getODFRootIndexablePageCache().invalidate(rootPage.getId()); 358 } 359 360 private Cache<ODFPageCacheKey, Map<String, Map<String, Collection<Program>>>> _getODFPageCache() 361 { 362 return _cacheManager.get(__TREE_CACHE); 363 } 364 365 private static class ODFPageCacheKey extends AbstractCacheKey 366 { 367 public ODFPageCacheKey(String workspace, String pageId) 368 { 369 super(workspace, pageId); 370 } 371 372 public static ODFPageCacheKey of(String workspace, String pageId) 373 { 374 return new ODFPageCacheKey(workspace, pageId); 375 } 376 } 377 378 private Cache<ODFPageProgramCacheKey, Program> _getODFPageProgramCache() 379 { 380 return _cacheManager.get(__PROGRAM_CACHE); 381 } 382 383 private static class ODFPageProgramCacheKey extends AbstractCacheKey 384 { 385 public ODFPageProgramCacheKey(String workspace, String pageId, String level1Value, String level2Value, String programName) 386 { 387 super(workspace, pageId, level1Value, level2Value, programName); 388 } 389 390 public static ODFPageProgramCacheKey of(String workspace, String pageId, String level1Value, String level2Value, String programName) 391 { 392 return new ODFPageProgramCacheKey(workspace, pageId, level1Value, level2Value, programName); 393 } 394 } 395 396 private Cache<String, Boolean> _getODFRootIndexablePageCache() 397 { 398 return _cacheManager.get(__INDEXABLE_CACHE); 399 } 400}