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