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.Set;
029import java.util.function.Predicate;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032import java.util.stream.StreamSupport;
033
034import org.apache.commons.lang3.BooleanUtils;
035import org.apache.commons.lang3.StringUtils;
036
037import org.ametys.cms.FilterNameHelper;
038import org.ametys.odf.ProgramItem;
039import org.ametys.odf.course.Course;
040import org.ametys.odf.courselist.CourseList;
041import org.ametys.odf.program.AbstractProgram;
042import org.ametys.odf.program.Container;
043import org.ametys.odf.program.Program;
044import org.ametys.odf.program.SubProgram;
045import org.ametys.odf.program.TraversableProgramPart;
046import org.ametys.plugins.explorer.resources.ResourceCollection;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysObjectIterable;
049import org.ametys.plugins.repository.AmetysObjectIterator;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.CollectionIterable;
053import org.ametys.plugins.repository.UnknownAmetysObjectException;
054import org.ametys.plugins.repository.metadata.CompositeMetadata;
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 implements Page
067{
068    private AmetysObjectResolver _resolver;
069    private OdfPageHandler _odfPageHandler;
070    private Page _root;
071    private AbstractProgram _program;
072    private String _path;
073    private Program _parent;
074    private Page _parentPage;
075    
076    /**
077     * Constructor for program page holding a {@link Program}.
078     * @param resolver the {@link AmetysObjectResolver}.
079     * @param odfPageHandler the {@link OdfPageHandler} to handle ODF pages.
080     * @param root the ODF root page.
081     * @param program the program.
082     * @param parentPage the parent {@link Page} or null if not yet computed.
083     */
084    public ProgramPage(AmetysObjectResolver resolver, OdfPageHandler odfPageHandler, Page root, Program program, Page parentPage)
085    {
086        this(resolver, odfPageHandler, root, program, null, null, parentPage);
087    }
088    
089    /**
090     * Constructor for program page holding a {@link Program} or {@link SubProgram}
091     * @param resolver the {@link AmetysObjectResolver}.
092     * @param odfPageHandler the {@link OdfPageHandler} to handle ODF pages.
093     * @param root the ODF root page.
094     * @param program the program or subprogram.
095     * @param path The path from the virtual second level page. Can be null if abstract program is a {@link Program}
096     * @param parent the parent program in case of a subprogram, null otherwise
097     * @param parentPage the parent {@link Page} or null if not yet computed.
098     */
099    public ProgramPage(AmetysObjectResolver resolver, OdfPageHandler odfPageHandler, Page root, AbstractProgram program, String path, Program parent, Page parentPage)
100    {
101        _root = root;
102        _program = program;
103        _resolver = resolver;
104        _odfPageHandler = odfPageHandler;
105        _path = path;
106        _parent = parent;
107        _parentPage = parentPage;
108    }
109    
110    /**
111     * Returns the associated {@link Program} or {@link SubProgram}.
112     * @return the associated {@link Program} or {@link SubProgram}.
113     */
114    public AbstractProgram getProgram()
115    {
116        return _program;
117    }
118    
119    @Override
120    public int getDepth() throws AmetysRepositoryException
121    {
122        return _root.getDepth() + 2 + (_path != null ? _path.split("/").length : 0);
123    }
124
125    @Override
126    public Set<String> getReferers() throws AmetysRepositoryException
127    {
128        throw new UnsupportedOperationException("getReferers not supported on virtual odf pages");
129    }
130
131    @Override
132    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
133    {
134        return null;
135    }
136
137    @Override
138    public String getTemplate() throws AmetysRepositoryException
139    {
140        return "program";
141    }
142
143    @Override
144    public String getTitle() throws AmetysRepositoryException
145    {
146        return _program.getTitle();
147    }
148
149    @Override
150    public String getLongTitle() throws AmetysRepositoryException
151    {
152        return _program.getTitle();
153    }
154 
155    @Override
156    public PageType getType() throws AmetysRepositoryException
157    {
158        return PageType.CONTAINER;
159    }
160
161    @Override
162    public String getURL() throws AmetysRepositoryException
163    {
164        throw new UnsupportedOperationException("getURL not supported on virtual odf pages");
165    }
166
167    @Override
168    public LinkType getURLType() throws AmetysRepositoryException
169    {
170        throw new UnsupportedOperationException("getURLType not supported on virtual odf pages");
171    }
172
173    @Override
174    public Zone getZone(String name) throws UnknownZoneException, AmetysRepositoryException
175    {
176        if (!"default".equals(name))
177        {
178            throw new IllegalArgumentException("Only the zone named 'default' is actually supported on virtual program pages.");
179        }
180        
181        return new ProgramZone(this);
182    }
183
184    @Override
185    public AmetysObjectIterable< ? extends Zone> getZones() throws AmetysRepositoryException
186    {
187        ArrayList<Zone> zones = new ArrayList<>();
188        zones.add(new ProgramZone(this));
189        return new CollectionIterable<>(zones);
190    }
191
192    @Override
193    public boolean hasZone(String name) throws AmetysRepositoryException
194    {
195        return "default".equals(name);
196    }
197
198    @Override
199    public AmetysObjectIterable<? extends Page> getChildrenPages() throws AmetysRepositoryException
200    {
201        Collection<Page> children = _traverseChildren(_program)
202            .distinct()
203            .map(child -> _toChildPage(child))
204            .collect(Collectors.toList());
205        
206        return new CollectionIterable<>(children);
207    }
208    
209    @Override
210    public String getPathInSitemap() throws AmetysRepositoryException
211    {
212        return Stream.of(_root.getPathInSitemap(), _computeLevelsPath(), _path, getName())
213            .filter(StringUtils::isNotEmpty)
214            .collect(Collectors.joining("/"));
215    }
216    
217    Program getParentProgram()
218    {
219        return Optional.ofNullable(_parent)
220            .orElseGet(() -> (Program) _program);
221    }
222
223    /**
224     * Compute the path from the root odf page, representing the first and second level pages.
225     * @return the path
226     */
227    String _computeLevelsPath()
228    {
229        // Get the parent program holding the level values
230        Program parentProgram = getParentProgram();
231        
232        String level1 = _odfPageHandler.getProgramLevel1Value(_root, parentProgram);
233        String level2 = _odfPageHandler.getProgramLevel2Value(_root, parentProgram);
234        
235        // The path is no more valid, re-calculate the real path
236        String secondLevelPageId = "odfLevel2://" + level1 + "/" + level2 + "?rootId=" + _root.getId();
237        Page secondLevelPage = _resolver.resolveById(secondLevelPageId);
238        
239        return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName();
240    }
241    
242    @Override
243    public Site getSite() throws AmetysRepositoryException
244    {
245        return _root.getSite();
246    }
247
248    @Override
249    public String getSiteName() throws AmetysRepositoryException
250    {
251        return _root.getSiteName();
252    }
253
254    @Override
255    public Sitemap getSitemap() throws AmetysRepositoryException
256    {
257        return _root.getSitemap();
258    }
259
260    @Override
261    public String getSitemapName() throws AmetysRepositoryException
262    {
263        return _root.getSitemapName();
264    }
265
266    @SuppressWarnings("unchecked")
267    @Override
268    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
269    {
270        if (path.isEmpty())
271        {
272            throw new AmetysRepositoryException("path must be non empty");
273        }
274        
275        List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2));
276        String name = headQueuePath.get(0);
277        String queuePath = Iterables.get(headQueuePath, 1, null); 
278        
279        return (A) _getChildPage(_program, name)
280                .map(page -> StringUtils.isEmpty(queuePath) ? page : page.getChild(queuePath))
281                .orElseThrow(() -> 
282                    new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId()));
283    }
284    
285    private Optional<Page> _getChildPage(TraversableProgramPart parent, String name)
286    {
287        return _findChild(parent, name)
288                .map(child -> _toChildPage(child));
289    }
290    
291    private Optional<ProgramItem> _findChild(TraversableProgramPart parent, String name)
292    {
293        return _traverseChildren(parent)
294            .filter(child -> _filterByName(child, name))
295            .findFirst();
296    }
297    
298    private boolean _filterByName(ProgramItem programItem, String name)
299    {
300        if (programItem instanceof SubProgram)
301        {
302            String subProgramPageName = FilterNameHelper.filterName(((SubProgram) programItem).getTitle()) + "-" + programItem.getName();
303            return name.equals(subProgramPageName);
304        }
305        else if (programItem instanceof Course)
306        {
307            Course course = (Course) programItem;
308            String coursePageName = FilterNameHelper.filterName(course.getTitle()) + "-" + course.getCode();
309            return name.equals(coursePageName);
310        }
311        
312        return false;
313    }
314    
315    private Page _toChildPage(ProgramItem child)
316    {
317        if (child instanceof SubProgram)
318        {
319            return _toChildProgramPage((SubProgram) child);
320        }
321        else if (child instanceof Course)
322        {
323            return _toChildCoursePage((Course) child);
324        }
325        
326        return null;
327    }
328    
329    private ProgramPage _toChildProgramPage(SubProgram child)
330    {
331        return new ProgramPage(_resolver, _odfPageHandler, _root, child, _path != null ? _path + '/' + getName() : getName(), getParentProgram(), this);
332    }
333    
334    private CoursePage _toChildCoursePage(Course course)
335    {
336        return new CoursePage(_resolver, _odfPageHandler, _root, course, getParentProgram(), _path != null ? _path + '/' + getName() : getName(), this);
337    }
338    
339    @SuppressWarnings("unchecked")
340    @Override
341    public AmetysObjectIterable<? extends Page> getChildren() throws AmetysRepositoryException
342    {
343        return getChildrenPages();
344    }
345
346    @Override
347    public boolean hasChild(String name) throws AmetysRepositoryException
348    {
349        return _findChild(_program, name).isPresent();
350    }
351    
352    @Override
353    public String getId() throws AmetysRepositoryException
354    {
355        // E.g: program://_root?rootId=xxxx&programId=xxxx (for a program)
356        // E.g: program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx (for a subprogram)
357        StringBuilder sb = new StringBuilder("program://");
358        sb.append(StringUtils.isNotEmpty(_path) ? _path : "_root");
359        sb.append("?rootId=").append(_root.getId());
360        sb.append("&programId=").append(_program.getId());
361        
362        if (_parent != null)
363        {
364            sb.append("&parentId=").append(_parent.getId());
365        }
366        
367        return sb.toString();
368    }
369
370    @Override
371    public String getName() throws AmetysRepositoryException
372    {
373        // E.g: licence-lea-anglais-allemand-program-fruai3182988abcde07
374        return FilterNameHelper.filterName(_program.getTitle()) + "-" + _program.getName();
375    }
376
377    @SuppressWarnings("unchecked")
378    @Override
379    public Page getParent() throws AmetysRepositoryException
380    {
381        if (_parentPage == null)
382        {
383            String relParentPath = _computeLevelsPath() + (_path != null ? "/" + _path : "");
384            _parentPage = _root.getChild(relParentPath);
385        }
386        
387        return _parentPage;
388    }
389
390    @Override
391    public String getParentPath() throws AmetysRepositoryException
392    {
393        return _root.getPath() + '/' + _computeLevelsPath() + (_path != null ? "/" + _path : "");
394    }
395
396    @Override
397    public String getPath() throws AmetysRepositoryException
398    {
399        return getParentPath() + "/" + getName();
400    }
401
402    @Override
403    public CompositeMetadata getMetadataHolder()
404    {
405        return new StaticCompositeMetadata();
406    }
407
408    @Override
409    public Set<String> getTags() throws AmetysRepositoryException
410    {
411        return Collections.emptySet();
412    }
413    
414    @Override
415    public boolean isVisible() throws AmetysRepositoryException
416    {
417        return true;
418    }
419
420    @Override
421    public AmetysObjectIterable<? extends Page> getChildrenPages(boolean includeInvisiblePages) throws AmetysRepositoryException
422    {
423        return getChildrenPages();
424    }
425
426    public Page getChildPageAt(int index) throws UnknownAmetysObjectException, AmetysRepositoryException
427    {
428        // TODO make a default method or call a helper instead of duplicate the code into each Page (FirstLevel / SecondLevel / Program etc...)
429        
430        if (index < 0)
431        {
432            throw new AmetysRepositoryException("Child page index cannot be negative");
433        }
434        
435        AmetysObjectIterator<? extends Page> childPages = getChildrenPages().iterator();
436        
437        try
438        {
439            childPages.skip(index);
440            return childPages.next();
441        }
442        catch (NoSuchElementException e)
443        {
444            throw new UnknownAmetysObjectException("There's no child page at index " + index + " for page " + this.getId());
445        }
446    }
447    
448    private Stream<ProgramItem> _traverseChildren(TraversableProgramPart parent)
449    {
450        Predicate<ProgramItem> isSubProgram = SubProgram.class::isInstance;
451        Predicate<ProgramItem> isCourse = Course.class::isInstance;
452        
453        ProgramPartTraverser traverser = new ProgramPartTraverser(parent.getProgramPartChildren());
454        return traverser.stream().filter(isSubProgram.or(isCourse));
455    }
456    
457    /**
458     * Program part traverser. Iterate recursively on child program base.
459     */
460    static class ProgramPartTraverser extends AbstractTreeIterator<ProgramItem>
461    {
462        public ProgramPartTraverser(Collection<? extends ProgramItem> programPartChildren)
463        {
464            super(programPartChildren);
465        }
466
467        @Override
468        protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent)
469        {
470            if (parent instanceof CourseList)
471            {
472                CourseList courseList = (CourseList) parent;
473                return new ProgramPartTraverser(courseList.getCourses());
474            }
475            
476            if (parent instanceof Container)
477            {
478                Container container = (Container) parent;
479                return new ProgramPartTraverser(container.getProgramPartChildren());
480            }
481            
482            return null;
483        }
484    }
485    
486    /**
487     * Breadth first search iterator for tree structure
488     * Each node can provide an iterator that will be put in the end of the queue. 
489     * @param <T> A tree item
490     */
491    abstract static class AbstractTreeIterator<T> implements Iterator<T>
492    {
493        protected final Queue<Iterator<T>> _nodeIterators = new LinkedList<>();
494        private Boolean _hasNext;
495        
496        AbstractTreeIterator(Iterator<T> iterator)
497        {
498            if (iterator != null && iterator.hasNext())
499            {
500                _nodeIterators.add(iterator);
501            }
502        }
503        
504        AbstractTreeIterator(Collection<? extends T> children)
505        {
506            this(handleConstructorChildren(children));
507        }
508        
509        private static <T> Iterator<T> handleConstructorChildren(Collection<? extends T> children)
510        {
511            Collection<T> tChildren = Collections.unmodifiableCollection(children);
512            return tChildren.iterator();
513        }
514        
515        public boolean hasNext()
516        {
517            if (_hasNext != null)
518            {
519                return _hasNext;
520            }
521            
522            Iterator<T> it = _getOrUpdateHead();
523            if (_hasNext == null)
524            {
525                _hasNext = it != null ? it.hasNext() : false;
526            }
527            
528            return _hasNext;
529        }
530        
531        public T next()
532        {
533            if (BooleanUtils.isFalse(_hasNext))
534            {
535                throw new NoSuchElementException();
536            }
537            
538            Iterator<T> it = null;
539            if (_hasNext == null)
540            {
541                it = _getOrUpdateHead();
542            }
543            else
544            {
545                it = _nodeIterators.peek();
546            }
547            
548            T next = Optional.ofNullable(it)
549                .map(Iterator::next)
550                .orElseThrow(NoSuchElementException::new);
551            
552            Iterator<T> childIterator = provideChildIterator(next);
553            if (childIterator != null && childIterator.hasNext())
554            {
555                _nodeIterators.add(childIterator);
556            }
557            
558            // reset cached has next
559            _hasNext = null;
560            
561            return next;
562        }
563        
564        protected abstract Iterator<T> provideChildIterator(T next);
565        
566        public Stream<T> stream()
567        {
568            Iterable<T> iterable = () -> this;
569            return StreamSupport.stream(iterable.spliterator(), false);
570        }
571        
572        private Iterator<T> _getOrUpdateHead()
573        {
574            return Optional.ofNullable(_nodeIterators.peek())
575                .filter(it ->
576                {
577                    if (it.hasNext())
578                    {
579                        _hasNext = true;
580                        return true;
581                    }
582                    
583                    return false;
584                })
585                .orElseGet(() -> _updateHead());
586        }
587        
588        private Iterator<T> _updateHead()
589        {
590            _nodeIterators.poll(); // remove actual head
591            return _nodeIterators.peek();
592        }
593    }
594}