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.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Iterator; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.NoSuchElementException; 026import java.util.Optional; 027import java.util.Queue; 028import java.util.Set; 029import java.util.function.Predicate; 030import java.util.stream.Collectors; 031import java.util.stream.Stream; 032import java.util.stream.StreamSupport; 033 034import org.apache.commons.lang3.BooleanUtils; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.cms.FilterNameHelper; 038import org.ametys.odf.ProgramItem; 039import org.ametys.odf.course.Course; 040import org.ametys.odf.courselist.CourseList; 041import org.ametys.odf.program.AbstractProgram; 042import org.ametys.odf.program.Container; 043import org.ametys.odf.program.Program; 044import org.ametys.odf.program.SubProgram; 045import org.ametys.odf.program.TraversableProgramPart; 046import org.ametys.plugins.explorer.resources.ResourceCollection; 047import org.ametys.plugins.repository.AmetysObject; 048import org.ametys.plugins.repository.AmetysObjectIterable; 049import org.ametys.plugins.repository.AmetysObjectIterator; 050import org.ametys.plugins.repository.AmetysObjectResolver; 051import org.ametys.plugins.repository.AmetysRepositoryException; 052import org.ametys.plugins.repository.CollectionIterable; 053import org.ametys.plugins.repository.UnknownAmetysObjectException; 054import org.ametys.plugins.repository.metadata.CompositeMetadata; 055import org.ametys.web.repository.page.Page; 056import org.ametys.web.repository.page.UnknownZoneException; 057import org.ametys.web.repository.page.Zone; 058import org.ametys.web.repository.site.Site; 059import org.ametys.web.repository.sitemap.Sitemap; 060 061import com.google.common.collect.Iterables; 062 063/** 064 * Page representing a {@link Program} or a {@link SubProgram}. 065 */ 066public class ProgramPage implements Page 067{ 068 private AmetysObjectResolver _resolver; 069 private OdfPageHandler _odfPageHandler; 070 private Page _root; 071 private AbstractProgram _program; 072 private String _path; 073 private Program _parent; 074 private Page _parentPage; 075 076 /** 077 * Constructor for program page holding a {@link Program}. 078 * @param resolver the {@link AmetysObjectResolver}. 079 * @param odfPageHandler the {@link OdfPageHandler} to handle ODF pages. 080 * @param root the ODF root page. 081 * @param program the program. 082 * @param parentPage the parent {@link Page} or null if not yet computed. 083 */ 084 public ProgramPage(AmetysObjectResolver resolver, OdfPageHandler odfPageHandler, Page root, Program program, Page parentPage) 085 { 086 this(resolver, odfPageHandler, root, program, null, null, parentPage); 087 } 088 089 /** 090 * Constructor for program page holding a {@link Program} or {@link SubProgram} 091 * @param resolver the {@link AmetysObjectResolver}. 092 * @param odfPageHandler the {@link OdfPageHandler} to handle ODF pages. 093 * @param root the ODF root page. 094 * @param program the program or subprogram. 095 * @param path The path from the virtual second level page. Can be null if abstract program is a {@link Program} 096 * @param parent the parent program in case of a subprogram, null otherwise 097 * @param parentPage the parent {@link Page} or null if not yet computed. 098 */ 099 public ProgramPage(AmetysObjectResolver resolver, OdfPageHandler odfPageHandler, Page root, AbstractProgram program, String path, Program parent, Page parentPage) 100 { 101 _root = root; 102 _program = program; 103 _resolver = resolver; 104 _odfPageHandler = odfPageHandler; 105 _path = path; 106 _parent = parent; 107 _parentPage = parentPage; 108 } 109 110 /** 111 * Returns the associated {@link Program} or {@link SubProgram}. 112 * @return the associated {@link Program} or {@link SubProgram}. 113 */ 114 public AbstractProgram getProgram() 115 { 116 return _program; 117 } 118 119 @Override 120 public int getDepth() throws AmetysRepositoryException 121 { 122 return _root.getDepth() + 2 + (_path != null ? _path.split("/").length : 0); 123 } 124 125 @Override 126 public Set<String> getReferers() throws AmetysRepositoryException 127 { 128 throw new UnsupportedOperationException("getReferers not supported on virtual odf pages"); 129 } 130 131 @Override 132 public ResourceCollection getRootAttachments() throws AmetysRepositoryException 133 { 134 return null; 135 } 136 137 @Override 138 public String getTemplate() throws AmetysRepositoryException 139 { 140 return "program"; 141 } 142 143 @Override 144 public String getTitle() throws AmetysRepositoryException 145 { 146 return _program.getTitle(); 147 } 148 149 @Override 150 public String getLongTitle() throws AmetysRepositoryException 151 { 152 return _program.getTitle(); 153 } 154 155 @Override 156 public PageType getType() throws AmetysRepositoryException 157 { 158 return PageType.CONTAINER; 159 } 160 161 @Override 162 public String getURL() throws AmetysRepositoryException 163 { 164 throw new UnsupportedOperationException("getURL not supported on virtual odf pages"); 165 } 166 167 @Override 168 public LinkType getURLType() throws AmetysRepositoryException 169 { 170 throw new UnsupportedOperationException("getURLType not supported on virtual odf pages"); 171 } 172 173 @Override 174 public Zone getZone(String name) throws UnknownZoneException, AmetysRepositoryException 175 { 176 if (!"default".equals(name)) 177 { 178 throw new IllegalArgumentException("Only the zone named 'default' is actually supported on virtual program pages."); 179 } 180 181 return new ProgramZone(this); 182 } 183 184 @Override 185 public AmetysObjectIterable< ? extends Zone> getZones() throws AmetysRepositoryException 186 { 187 ArrayList<Zone> zones = new ArrayList<>(); 188 zones.add(new ProgramZone(this)); 189 return new CollectionIterable<>(zones); 190 } 191 192 @Override 193 public boolean hasZone(String name) throws AmetysRepositoryException 194 { 195 return "default".equals(name); 196 } 197 198 @Override 199 public AmetysObjectIterable<? extends Page> getChildrenPages() throws AmetysRepositoryException 200 { 201 Collection<Page> children = _traverseChildren(_program) 202 .distinct() 203 .map(child -> _toChildPage(child)) 204 .collect(Collectors.toList()); 205 206 return new CollectionIterable<>(children); 207 } 208 209 @Override 210 public String getPathInSitemap() throws AmetysRepositoryException 211 { 212 return Stream.of(_root.getPathInSitemap(), _computeLevelsPath(), _path, getName()) 213 .filter(StringUtils::isNotEmpty) 214 .collect(Collectors.joining("/")); 215 } 216 217 Program getParentProgram() 218 { 219 return Optional.ofNullable(_parent) 220 .orElseGet(() -> (Program) _program); 221 } 222 223 /** 224 * Compute the path from the root odf page, representing the first and second level pages. 225 * @return the path 226 */ 227 String _computeLevelsPath() 228 { 229 // Get the parent program holding the level values 230 Program parentProgram = getParentProgram(); 231 232 String level1 = _odfPageHandler.getProgramLevel1Value(_root, parentProgram); 233 String level2 = _odfPageHandler.getProgramLevel2Value(_root, parentProgram); 234 235 // The path is no more valid, re-calculate the real path 236 String secondLevelPageId = "odfLevel2://" + level1 + "/" + level2 + "?rootId=" + _root.getId(); 237 Page secondLevelPage = _resolver.resolveById(secondLevelPageId); 238 239 return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName(); 240 } 241 242 @Override 243 public Site getSite() throws AmetysRepositoryException 244 { 245 return _root.getSite(); 246 } 247 248 @Override 249 public String getSiteName() throws AmetysRepositoryException 250 { 251 return _root.getSiteName(); 252 } 253 254 @Override 255 public Sitemap getSitemap() throws AmetysRepositoryException 256 { 257 return _root.getSitemap(); 258 } 259 260 @Override 261 public String getSitemapName() throws AmetysRepositoryException 262 { 263 return _root.getSitemapName(); 264 } 265 266 @SuppressWarnings("unchecked") 267 @Override 268 public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException 269 { 270 if (path.isEmpty()) 271 { 272 throw new AmetysRepositoryException("path must be non empty"); 273 } 274 275 List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2)); 276 String name = headQueuePath.get(0); 277 String queuePath = Iterables.get(headQueuePath, 1, null); 278 279 return (A) _getChildPage(_program, name) 280 .map(page -> StringUtils.isEmpty(queuePath) ? page : page.getChild(queuePath)) 281 .orElseThrow(() -> 282 new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId())); 283 } 284 285 private Optional<Page> _getChildPage(TraversableProgramPart parent, String name) 286 { 287 return _findChild(parent, name) 288 .map(child -> _toChildPage(child)); 289 } 290 291 private Optional<ProgramItem> _findChild(TraversableProgramPart parent, String name) 292 { 293 return _traverseChildren(parent) 294 .filter(child -> _filterByName(child, name)) 295 .findFirst(); 296 } 297 298 private boolean _filterByName(ProgramItem programItem, String name) 299 { 300 if (programItem instanceof SubProgram) 301 { 302 String subProgramPageName = FilterNameHelper.filterName(((SubProgram) programItem).getTitle()) + "-" + programItem.getName(); 303 return name.equals(subProgramPageName); 304 } 305 else if (programItem instanceof Course) 306 { 307 Course course = (Course) programItem; 308 String coursePageName = FilterNameHelper.filterName(course.getTitle()) + "-" + course.getCode(); 309 return name.equals(coursePageName); 310 } 311 312 return false; 313 } 314 315 private Page _toChildPage(ProgramItem child) 316 { 317 if (child instanceof SubProgram) 318 { 319 return _toChildProgramPage((SubProgram) child); 320 } 321 else if (child instanceof Course) 322 { 323 return _toChildCoursePage((Course) child); 324 } 325 326 return null; 327 } 328 329 private ProgramPage _toChildProgramPage(SubProgram child) 330 { 331 return new ProgramPage(_resolver, _odfPageHandler, _root, child, _path != null ? _path + '/' + getName() : getName(), getParentProgram(), this); 332 } 333 334 private CoursePage _toChildCoursePage(Course course) 335 { 336 return new CoursePage(_resolver, _odfPageHandler, _root, course, getParentProgram(), _path != null ? _path + '/' + getName() : getName(), this); 337 } 338 339 @SuppressWarnings("unchecked") 340 @Override 341 public AmetysObjectIterable<? extends Page> getChildren() throws AmetysRepositoryException 342 { 343 return getChildrenPages(); 344 } 345 346 @Override 347 public boolean hasChild(String name) throws AmetysRepositoryException 348 { 349 return _findChild(_program, name).isPresent(); 350 } 351 352 @Override 353 public String getId() throws AmetysRepositoryException 354 { 355 // E.g: program://_root?rootId=xxxx&programId=xxxx (for a program) 356 // E.g: program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx (for a subprogram) 357 StringBuilder sb = new StringBuilder("program://"); 358 sb.append(StringUtils.isNotEmpty(_path) ? _path : "_root"); 359 sb.append("?rootId=").append(_root.getId()); 360 sb.append("&programId=").append(_program.getId()); 361 362 if (_parent != null) 363 { 364 sb.append("&parentId=").append(_parent.getId()); 365 } 366 367 return sb.toString(); 368 } 369 370 @Override 371 public String getName() throws AmetysRepositoryException 372 { 373 // E.g: licence-lea-anglais-allemand-program-fruai3182988abcde07 374 return FilterNameHelper.filterName(_program.getTitle()) + "-" + _program.getName(); 375 } 376 377 @SuppressWarnings("unchecked") 378 @Override 379 public Page getParent() throws AmetysRepositoryException 380 { 381 if (_parentPage == null) 382 { 383 String relParentPath = _computeLevelsPath() + (_path != null ? "/" + _path : ""); 384 _parentPage = _root.getChild(relParentPath); 385 } 386 387 return _parentPage; 388 } 389 390 @Override 391 public String getParentPath() throws AmetysRepositoryException 392 { 393 return _root.getPath() + '/' + _computeLevelsPath() + (_path != null ? "/" + _path : ""); 394 } 395 396 @Override 397 public String getPath() throws AmetysRepositoryException 398 { 399 return getParentPath() + "/" + getName(); 400 } 401 402 @Override 403 public CompositeMetadata getMetadataHolder() 404 { 405 return new StaticCompositeMetadata(); 406 } 407 408 @Override 409 public Set<String> getTags() throws AmetysRepositoryException 410 { 411 return Collections.emptySet(); 412 } 413 414 @Override 415 public boolean isVisible() throws AmetysRepositoryException 416 { 417 return true; 418 } 419 420 @Override 421 public AmetysObjectIterable<? extends Page> getChildrenPages(boolean includeInvisiblePages) throws AmetysRepositoryException 422 { 423 return getChildrenPages(); 424 } 425 426 public Page getChildPageAt(int index) throws UnknownAmetysObjectException, AmetysRepositoryException 427 { 428 // TODO make a default method or call a helper instead of duplicate the code into each Page (FirstLevel / SecondLevel / Program etc...) 429 430 if (index < 0) 431 { 432 throw new AmetysRepositoryException("Child page index cannot be negative"); 433 } 434 435 AmetysObjectIterator<? extends Page> childPages = getChildrenPages().iterator(); 436 437 try 438 { 439 childPages.skip(index); 440 return childPages.next(); 441 } 442 catch (NoSuchElementException e) 443 { 444 throw new UnknownAmetysObjectException("There's no child page at index " + index + " for page " + this.getId()); 445 } 446 } 447 448 private Stream<ProgramItem> _traverseChildren(TraversableProgramPart parent) 449 { 450 Predicate<ProgramItem> isSubProgram = SubProgram.class::isInstance; 451 Predicate<ProgramItem> isCourse = Course.class::isInstance; 452 453 ProgramPartTraverser traverser = new ProgramPartTraverser(parent.getProgramPartChildren()); 454 return traverser.stream().filter(isSubProgram.or(isCourse)); 455 } 456 457 /** 458 * Program part traverser. Iterate recursively on child program base. 459 */ 460 static class ProgramPartTraverser extends AbstractTreeIterator<ProgramItem> 461 { 462 public ProgramPartTraverser(Collection<? extends ProgramItem> programPartChildren) 463 { 464 super(programPartChildren); 465 } 466 467 @Override 468 protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent) 469 { 470 if (parent instanceof CourseList) 471 { 472 CourseList courseList = (CourseList) parent; 473 return new ProgramPartTraverser(courseList.getCourses()); 474 } 475 476 if (parent instanceof Container) 477 { 478 Container container = (Container) parent; 479 return new ProgramPartTraverser(container.getProgramPartChildren()); 480 } 481 482 return null; 483 } 484 } 485 486 /** 487 * Breadth first search iterator for tree structure 488 * Each node can provide an iterator that will be put in the end of the queue. 489 * @param <T> A tree item 490 */ 491 abstract static class AbstractTreeIterator<T> implements Iterator<T> 492 { 493 protected final Queue<Iterator<T>> _nodeIterators = new LinkedList<>(); 494 private Boolean _hasNext; 495 496 AbstractTreeIterator(Iterator<T> iterator) 497 { 498 if (iterator != null && iterator.hasNext()) 499 { 500 _nodeIterators.add(iterator); 501 } 502 } 503 504 AbstractTreeIterator(Collection<? extends T> children) 505 { 506 this(handleConstructorChildren(children)); 507 } 508 509 private static <T> Iterator<T> handleConstructorChildren(Collection<? extends T> children) 510 { 511 Collection<T> tChildren = Collections.unmodifiableCollection(children); 512 return tChildren.iterator(); 513 } 514 515 public boolean hasNext() 516 { 517 if (_hasNext != null) 518 { 519 return _hasNext; 520 } 521 522 Iterator<T> it = _getOrUpdateHead(); 523 if (_hasNext == null) 524 { 525 _hasNext = it != null ? it.hasNext() : false; 526 } 527 528 return _hasNext; 529 } 530 531 public T next() 532 { 533 if (BooleanUtils.isFalse(_hasNext)) 534 { 535 throw new NoSuchElementException(); 536 } 537 538 Iterator<T> it = null; 539 if (_hasNext == null) 540 { 541 it = _getOrUpdateHead(); 542 } 543 else 544 { 545 it = _nodeIterators.peek(); 546 } 547 548 T next = Optional.ofNullable(it) 549 .map(Iterator::next) 550 .orElseThrow(NoSuchElementException::new); 551 552 Iterator<T> childIterator = provideChildIterator(next); 553 if (childIterator != null && childIterator.hasNext()) 554 { 555 _nodeIterators.add(childIterator); 556 } 557 558 // reset cached has next 559 _hasNext = null; 560 561 return next; 562 } 563 564 protected abstract Iterator<T> provideChildIterator(T next); 565 566 public Stream<T> stream() 567 { 568 Iterable<T> iterable = () -> this; 569 return StreamSupport.stream(iterable.spliterator(), false); 570 } 571 572 private Iterator<T> _getOrUpdateHead() 573 { 574 return Optional.ofNullable(_nodeIterators.peek()) 575 .filter(it -> 576 { 577 if (it.hasNext()) 578 { 579 _hasNext = true; 580 return true; 581 } 582 583 return false; 584 }) 585 .orElseGet(() -> _updateHead()); 586 } 587 588 private Iterator<T> _updateHead() 589 { 590 _nodeIterators.poll(); // remove actual head 591 return _nodeIterators.peek(); 592 } 593 } 594}