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