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