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}