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