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        if (!_factory.isIndexing())
465        {
466            // computing educational paths is actually very expensive, and only useful during rendering
467            // we are very conservative here and only disable that computing for specific indexing cases
468            // (we could have chosen to only enable it when rendering, but we don't want to forget specific cases)
469            setCurrentEducationalPaths(program);
470        }
471        
472        return program;
473    }
474    
475}