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