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