001/* 002 * Copyright 2010 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.Arrays; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.Iterator; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.NoSuchElementException; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.Queue; 028import java.util.function.Predicate; 029import java.util.stream.Collectors; 030import java.util.stream.Stream; 031import java.util.stream.StreamSupport; 032 033import org.apache.commons.lang3.BooleanUtils; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.odf.ProgramItem; 037import org.ametys.odf.course.Course; 038import org.ametys.odf.courselist.CourseList; 039import org.ametys.odf.program.AbstractProgram; 040import org.ametys.odf.program.Container; 041import org.ametys.odf.program.Program; 042import org.ametys.odf.program.SubProgram; 043import org.ametys.odf.program.TraversableProgramPart; 044import org.ametys.plugins.repository.AmetysObject; 045import org.ametys.plugins.repository.AmetysObjectIterable; 046import org.ametys.plugins.repository.AmetysRepositoryException; 047import org.ametys.plugins.repository.CollectionIterable; 048import org.ametys.plugins.repository.UnknownAmetysObjectException; 049import org.ametys.plugins.repository.jcr.NameHelper; 050import org.ametys.web.repository.page.Page; 051import org.ametys.web.repository.page.virtual.VirtualPageConfiguration; 052 053import com.google.common.collect.Iterables; 054 055/** 056 * Page representing a {@link Program} or a {@link SubProgram}. 057 */ 058@SuppressWarnings("unchecked") 059public class ProgramPage extends AbstractProgramItemPage<ProgramPageFactory> 060{ 061 private AbstractProgram _program; 062 private String _path; 063 private Page _parentPage; 064 private Program _parentProgram; 065 066 /** 067 * Constructor for program page holding a {@link Program} or {@link SubProgram} 068 * @param factory The factory 069 * @param root the ODF root page. 070 * @param program the program or subprogram. 071 * @param path The path from the virtual second level page. Can be null if abstract program is a {@link Program} 072 * @param parentProgram the parent program in case of a subprogram, null otherwise 073 * @param parentPage the parent {@link Page} or null if not yet computed. 074 * @param configuration The program virtual page's configuration 075 */ 076 public ProgramPage(Page root, VirtualPageConfiguration configuration, ProgramPageFactory factory, AbstractProgram program, String path, Program parentProgram, Page parentPage) 077 { 078 super(root, configuration, factory.getScheme(), factory); 079 080 _program = program; 081 _path = path; 082 _parentPage = parentPage; 083 _parentProgram = parentProgram; 084 } 085 086 /** 087 * Returns the associated {@link Program} or {@link SubProgram}. 088 * @return the associated {@link Program} or {@link SubProgram}. 089 */ 090 public AbstractProgram getProgram() 091 { 092 return _program; 093 } 094 095 @Override 096 protected ProgramItem getProgramItem() 097 { 098 return getProgram(); 099 } 100 101 @Override 102 public int getDepth() throws AmetysRepositoryException 103 { 104 int levelDepth = 0; 105 if (StringUtils.isNotBlank(_factory.getODFPageHandler().getLevel1Metadata(_root))) 106 { 107 levelDepth++; 108 if (StringUtils.isNotBlank(_factory.getODFPageHandler().getLevel2Metadata(_root))) 109 { 110 levelDepth++; 111 } 112 } 113 114 return _root.getDepth() + levelDepth + (_path != null ? _path.split("/").length : 0); 115 } 116 117 @Override 118 public String getTitle() throws AmetysRepositoryException 119 { 120 return _program.getTitle(); 121 } 122 123 @Override 124 public String getLongTitle() throws AmetysRepositoryException 125 { 126 return _program.getTitle(); 127 } 128 129 @Override 130 public AmetysObjectIterable<? extends Page> getChildrenPages() throws AmetysRepositoryException 131 { 132 Collection<Page> children = _transformChildrenPages(_traverseChildren(_program)).toList(); 133 return new CollectionIterable<>(children); 134 } 135 136 private Page _createChildPage(ProgramItem child) 137 { 138 if (child instanceof SubProgram subProgram) 139 { 140 return _createChildProgramPage(subProgram); 141 } 142 else if (child instanceof Course course) 143 { 144 return _createChildCoursePage(course); 145 } 146 147 return null; 148 } 149 150 private ProgramPage _createChildProgramPage(SubProgram child) 151 { 152 return _factory.createProgramPage(_root, child, _path != null ? _path + '/' + getName() : getName(), _getParentProgram(), this); 153 } 154 155 private CoursePage _createChildCoursePage(Course course) 156 { 157 return _factory.getCoursePageFactory().createCoursePage(_root, course, _getParentProgram(), _path != null ? _path + '/' + getName() : getName(), this); 158 } 159 160 @Override 161 public String getPathInSitemap() throws AmetysRepositoryException 162 { 163 String path = _computePath(_root.getPathInSitemap()); 164 return path == null ? null : path + "/" + getName(); 165 } 166 167 private Program _getParentProgram() 168 { 169 return Optional.ofNullable(_parentProgram) 170 .orElseGet(() -> (Program) _program); 171 } 172 173 @Override 174 public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException 175 { 176 if (path.isEmpty()) 177 { 178 throw new AmetysRepositoryException("path must be non empty"); 179 } 180 181 List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2)); 182 String name = headQueuePath.get(0); 183 String queuePath = Iterables.get(headQueuePath, 1, null); 184 185 return (A) _findChildPage(_program, name) 186 .map(cp -> _factory.getODFPageHandler().addRedirectIfNeeded(cp, name)) 187 .map(cp -> _factory.getODFPageHandler().exploreQueuePath(cp, queuePath)) 188 .orElseThrow(() -> new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId())); 189 } 190 191 private Optional<Page> _findChildPage(TraversableProgramPart parent, String name) 192 { 193 return _transformChildrenPages(_traverseChildren(parent).filter(child -> _filterByName(child, name))).findFirst(); 194 } 195 196 private boolean _filterByName(ProgramItem programItem, String name) 197 { 198 // If last part is equals to the program item code, the page matches 199 if (programItem.getCode().equals(name.substring(name.lastIndexOf("-") + 1))) 200 { 201 return true; 202 } 203 204 if (programItem instanceof SubProgram subProgram) 205 { 206 // For legacy purpose we use the subProgramName when the subProgramCode is null. 207 String subProgramPageName = NameHelper.filterName(subProgram.getTitle()) + "-" + programItem.getName(); 208 return name.equals(subProgramPageName); 209 } 210 211 return false; 212 } 213 214 @Override 215 public boolean hasChild(String name) throws AmetysRepositoryException 216 { 217 return _findChildPage(_program, name).isPresent(); 218 } 219 220 @Override 221 public String getId() throws AmetysRepositoryException 222 { 223 // E.g: program://_root?rootId=xxxx&programId=xxxx (for a program) 224 // E.g: program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx (for a subprogram) 225 StringBuilder sb = new StringBuilder("program://"); 226 sb.append(StringUtils.isNotEmpty(_path) ? _path : "_root"); 227 sb.append("?rootId=").append(_root.getId()); 228 sb.append("&programId=").append(_program.getId()); 229 230 if (_parentProgram != null) 231 { 232 sb.append("&parentId=").append(_parentProgram.getId()); 233 } 234 235 return sb.toString(); 236 } 237 238 @Override 239 public String getName() throws AmetysRepositoryException 240 { 241 // E.g: licence-lea-anglais-allemand-H7AIIUYW 242 return _factory.getODFPageHandler().getPageName(_program); 243 } 244 245 @Override 246 public Page getParent() throws AmetysRepositoryException 247 { 248 if (_parentPage == null) 249 { 250 String childPath = _computePath(null); 251 if (childPath != null) 252 { 253 _parentPage = childPath.isEmpty() ? _root : _root.getChild(childPath); 254 } 255 } 256 257 return _parentPage; 258 } 259 260 @Override 261 public String getParentPath() throws AmetysRepositoryException 262 { 263 return _computePath(_root.getPath()); 264 } 265 266 private Stream<ProgramItem> _traverseChildren(TraversableProgramPart parent) 267 { 268 Predicate<ProgramItem> isSubProgram = SubProgram.class::isInstance; 269 Predicate<ProgramItem> isCourse = Course.class::isInstance; 270 271 ProgramPartTraverser traverser = new ProgramPartTraverser(parent.getProgramPartChildren()); 272 return traverser.stream().filter(isSubProgram.or(isCourse)).distinct(); 273 } 274 275 /** 276 * Program part traverser. Iterate recursively on child program base. 277 */ 278 static class ProgramPartTraverser extends AbstractTreeIterator<ProgramItem> 279 { 280 public ProgramPartTraverser(Collection<? extends ProgramItem> programPartChildren) 281 { 282 super(programPartChildren); 283 } 284 285 @Override 286 protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent) 287 { 288 if (parent instanceof CourseList courseList) 289 { 290 return new ProgramPartTraverser(courseList.getCourses()); 291 } 292 293 if (parent instanceof Container container) 294 { 295 return new ProgramPartTraverser(container.getProgramPartChildren()); 296 } 297 298 return null; 299 } 300 } 301 302 /** 303 * Breadth first search iterator for tree structure 304 * Each node can provide an iterator that will be put in the end of the queue. 305 * @param <T> A tree item 306 */ 307 abstract static class AbstractTreeIterator<T> implements Iterator<T> 308 { 309 protected final Queue<Iterator<T>> _nodeIterators = new LinkedList<>(); 310 private Boolean _hasNext; 311 312 AbstractTreeIterator(Iterator<T> iterator) 313 { 314 if (iterator != null && iterator.hasNext()) 315 { 316 _nodeIterators.add(iterator); 317 } 318 } 319 320 AbstractTreeIterator(Collection<? extends T> children) 321 { 322 this(handleConstructorChildren(children)); 323 } 324 325 private static <T> Iterator<T> handleConstructorChildren(Collection<? extends T> children) 326 { 327 Collection<T> tChildren = Collections.unmodifiableCollection(children); 328 return tChildren.iterator(); 329 } 330 331 public boolean hasNext() 332 { 333 if (_hasNext != null) 334 { 335 return _hasNext; 336 } 337 338 Iterator<T> it = _getOrUpdateHead(); 339 if (_hasNext == null) 340 { 341 _hasNext = it != null ? it.hasNext() : false; 342 } 343 344 return _hasNext; 345 } 346 347 public T next() 348 { 349 if (BooleanUtils.isFalse(_hasNext)) 350 { 351 throw new NoSuchElementException(); 352 } 353 354 Iterator<T> it = null; 355 if (_hasNext == null) 356 { 357 it = _getOrUpdateHead(); 358 } 359 else 360 { 361 it = _nodeIterators.peek(); 362 } 363 364 T next = Optional.ofNullable(it) 365 .map(Iterator::next) 366 .orElseThrow(NoSuchElementException::new); 367 368 Iterator<T> childIterator = provideChildIterator(next); 369 if (childIterator != null && childIterator.hasNext()) 370 { 371 _nodeIterators.add(childIterator); 372 } 373 374 // reset cached has next 375 _hasNext = null; 376 377 return next; 378 } 379 380 protected abstract Iterator<T> provideChildIterator(T next); 381 382 public Stream<T> stream() 383 { 384 Iterable<T> iterable = () -> this; 385 return StreamSupport.stream(iterable.spliterator(), false); 386 } 387 388 private Iterator<T> _getOrUpdateHead() 389 { 390 return Optional.ofNullable(_nodeIterators.peek()) 391 .filter(it -> 392 { 393 if (it.hasNext()) 394 { 395 _hasNext = true; 396 return true; 397 } 398 399 return false; 400 }) 401 .orElseGet(() -> _updateHead()); 402 } 403 404 private Iterator<T> _updateHead() 405 { 406 _nodeIterators.poll(); // remove actual head 407 return _nodeIterators.peek(); 408 } 409 } 410 411 @Override 412 public AbstractProgram getContent() 413 { 414 AbstractProgram program = getProgram(); 415 program.setContextPath(getPathInSitemap()); 416 417 if (!_factory.isIndexing()) 418 { 419 // computing educational paths is actually very expensive, and only useful during rendering 420 // we are very conservative here and only disable that computing for specific indexing cases 421 // (we could have chosen to only enable it when rendering, but we don't want to forget specific cases) 422 setCurrentEducationalPaths(program); 423 } 424 425 return program; 426 } 427 428 private String _computePath(String rootPath) 429 { 430 String levelsPath = _factory.getODFPageHandler().computeLevelsPath(_root, _getParentProgram()); 431 432 // The current program has no valid attributes for the levels selected in the ODF root 433 if (levelsPath == null) 434 { 435 throw new UnknownAmetysObjectException("Page of program " + _getParentProgram().getId() + " does not have a valid level path"); 436 } 437 438 return Stream.of(rootPath, levelsPath, _path) 439 .filter(StringUtils::isNotEmpty) 440 .collect(Collectors.joining("/")); 441 } 442 443 private Stream<Page> _transformChildrenPages(Stream<ProgramItem> children) 444 { 445 return children 446 .map(this::_createChildPage) 447 .filter(Objects::nonNull) 448 // Test if the child page is in existing virtual pages 449 .filter(page -> { 450 try 451 { 452 page.getPathInSitemap(); 453 return true; 454 } 455 catch (UnknownAmetysObjectException e) 456 { 457 return false; 458 } 459 }); 460 } 461}