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