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