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.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.data.holder.ModelAwareDataHolder; 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.plugins.repository.jcr.NameHelper; 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 return (A) _getChildPage(_program, name) 242 .map(cp -> _factory.getODFPageHandler().addRedirectIfNeeded(cp, name)) 243 .map(cp -> _factory.getODFPageHandler().exploreQueuePath(cp, queuePath)) 244 .orElseThrow(() -> new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId())); 245 } 246 247 private Optional<Page> _getChildPage(TraversableProgramPart parent, String name) 248 { 249 return _findChild(parent, name) 250 .map(child -> _toChildPage(child)); 251 } 252 253 private Optional<ProgramItem> _findChild(TraversableProgramPart parent, String name) 254 { 255 return _traverseChildren(parent) 256 .filter(child -> _filterByName(child, name)) 257 .findFirst(); 258 } 259 260 private boolean _filterByName(ProgramItem programItem, String name) 261 { 262 // If last part is equals to the program item code, the page matches 263 if (programItem.getCode().equals(name.substring(name.lastIndexOf("-") + 1))) 264 { 265 return true; 266 } 267 268 if (programItem instanceof SubProgram subProgram) 269 { 270 // For legacy purpose we use the subProgramName when the subProgramCode is null. 271 String subProgramPageName = NameHelper.filterName(subProgram.getTitle()) + "-" + programItem.getName(); 272 return name.equals(subProgramPageName); 273 } 274 275 return false; 276 } 277 278 279 private Page _toChildPage(ProgramItem child) 280 { 281 if (child instanceof SubProgram subProgram) 282 { 283 return _toChildProgramPage(subProgram); 284 } 285 else if (child instanceof Course course) 286 { 287 return _toChildCoursePage(course); 288 } 289 290 return null; 291 } 292 293 private ProgramPage _toChildProgramPage(SubProgram child) 294 { 295 return new ProgramPage(_factory, _root, child, _path != null ? _path + '/' + getName() : getName(), getParentProgram(), this); 296 } 297 298 private CoursePage _toChildCoursePage(Course course) 299 { 300 return new CoursePage(_factory.getCoursePageFactory(), _root, course, getParentProgram(), _path != null ? _path + '/' + getName() : getName(), this); 301 } 302 303 @Override 304 public boolean hasChild(String name) throws AmetysRepositoryException 305 { 306 return _findChild(_program, name).isPresent(); 307 } 308 309 @Override 310 public String getId() throws AmetysRepositoryException 311 { 312 // E.g: program://_root?rootId=xxxx&programId=xxxx (for a program) 313 // E.g: program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx (for a subprogram) 314 StringBuilder sb = new StringBuilder("program://"); 315 sb.append(StringUtils.isNotEmpty(_path) ? _path : "_root"); 316 sb.append("?rootId=").append(_root.getId()); 317 sb.append("&programId=").append(_program.getId()); 318 319 if (_parent != null) 320 { 321 sb.append("&parentId=").append(_parent.getId()); 322 } 323 324 return sb.toString(); 325 } 326 327 @Override 328 public String getName() throws AmetysRepositoryException 329 { 330 // E.g: licence-lea-anglais-allemand-H7AIIUYW 331 return _factory.getODFPageHandler().getPageName(_program); 332 } 333 334 @SuppressWarnings("unchecked") 335 @Override 336 public Page getParent() throws AmetysRepositoryException 337 { 338 if (_parentPage == null) 339 { 340 if (StringUtils.isBlank(_factory.getODFPageHandler().getLevel1Metadata(_root))) 341 { 342 _parentPage = _root; 343 } 344 else 345 { 346 String relParentPath = _computeLevelsPath() + (_path != null ? "/" + _path : ""); 347 _parentPage = _root.getChild(relParentPath); 348 } 349 } 350 351 return _parentPage; 352 } 353 354 @Override 355 public String getParentPath() throws AmetysRepositoryException 356 { 357 if (StringUtils.isBlank(_factory.getODFPageHandler().getLevel1Metadata(_root))) 358 { 359 return _root.getPath(); 360 } 361 else 362 { 363 return _root.getPath() + '/' + _computeLevelsPath() + (_path != null ? "/" + _path : ""); 364 } 365 } 366 367 public ModelLessDataHolder getDataHolder() 368 { 369 RepositoryData repositoryData = new MemoryRepositoryData(getName()); 370 return new DefaultModelLessDataHolder(_factory.getPageDataTypeEP(), repositoryData); 371 } 372 373 private Stream<ProgramItem> _traverseChildren(TraversableProgramPart parent) 374 { 375 Predicate<ProgramItem> isSubProgram = SubProgram.class::isInstance; 376 Predicate<ProgramItem> isCourse = Course.class::isInstance; 377 378 ProgramPartTraverser traverser = new ProgramPartTraverser(parent.getProgramPartChildren()); 379 return traverser.stream().filter(isSubProgram.or(isCourse)); 380 } 381 382 /** 383 * Program part traverser. Iterate recursively on child program base. 384 */ 385 static class ProgramPartTraverser extends AbstractTreeIterator<ProgramItem> 386 { 387 public ProgramPartTraverser(Collection<? extends ProgramItem> programPartChildren) 388 { 389 super(programPartChildren); 390 } 391 392 @Override 393 protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent) 394 { 395 if (parent instanceof CourseList courseList) 396 { 397 return new ProgramPartTraverser(courseList.getCourses()); 398 } 399 400 if (parent instanceof Container container) 401 { 402 return new ProgramPartTraverser(container.getProgramPartChildren()); 403 } 404 405 return null; 406 } 407 } 408 409 /** 410 * Breadth first search iterator for tree structure 411 * Each node can provide an iterator that will be put in the end of the queue. 412 * @param <T> A tree item 413 */ 414 abstract static class AbstractTreeIterator<T> implements Iterator<T> 415 { 416 protected final Queue<Iterator<T>> _nodeIterators = new LinkedList<>(); 417 private Boolean _hasNext; 418 419 AbstractTreeIterator(Iterator<T> iterator) 420 { 421 if (iterator != null && iterator.hasNext()) 422 { 423 _nodeIterators.add(iterator); 424 } 425 } 426 427 AbstractTreeIterator(Collection<? extends T> children) 428 { 429 this(handleConstructorChildren(children)); 430 } 431 432 private static <T> Iterator<T> handleConstructorChildren(Collection<? extends T> children) 433 { 434 Collection<T> tChildren = Collections.unmodifiableCollection(children); 435 return tChildren.iterator(); 436 } 437 438 public boolean hasNext() 439 { 440 if (_hasNext != null) 441 { 442 return _hasNext; 443 } 444 445 Iterator<T> it = _getOrUpdateHead(); 446 if (_hasNext == null) 447 { 448 _hasNext = it != null ? it.hasNext() : false; 449 } 450 451 return _hasNext; 452 } 453 454 public T next() 455 { 456 if (BooleanUtils.isFalse(_hasNext)) 457 { 458 throw new NoSuchElementException(); 459 } 460 461 Iterator<T> it = null; 462 if (_hasNext == null) 463 { 464 it = _getOrUpdateHead(); 465 } 466 else 467 { 468 it = _nodeIterators.peek(); 469 } 470 471 T next = Optional.ofNullable(it) 472 .map(Iterator::next) 473 .orElseThrow(NoSuchElementException::new); 474 475 Iterator<T> childIterator = provideChildIterator(next); 476 if (childIterator != null && childIterator.hasNext()) 477 { 478 _nodeIterators.add(childIterator); 479 } 480 481 // reset cached has next 482 _hasNext = null; 483 484 return next; 485 } 486 487 protected abstract Iterator<T> provideChildIterator(T next); 488 489 public Stream<T> stream() 490 { 491 Iterable<T> iterable = () -> this; 492 return StreamSupport.stream(iterable.spliterator(), false); 493 } 494 495 private Iterator<T> _getOrUpdateHead() 496 { 497 return Optional.ofNullable(_nodeIterators.peek()) 498 .filter(it -> 499 { 500 if (it.hasNext()) 501 { 502 _hasNext = true; 503 return true; 504 } 505 506 return false; 507 }) 508 .orElseGet(() -> _updateHead()); 509 } 510 511 private Iterator<T> _updateHead() 512 { 513 _nodeIterators.poll(); // remove actual head 514 return _nodeIterators.peek(); 515 } 516 } 517 518 public ModelAwareDataHolder getTemplateParametersHolder() throws AmetysRepositoryException 519 { 520 return null; 521 } 522}