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