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