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