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