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        Optional<Page> childPage = _getChildPage(_program, name);
242        if (childPage.isPresent())
243        {
244            if (StringUtils.isEmpty(queuePath))
245            {
246                if (name.lastIndexOf("subprogram-") != -1)
247                {
248                    // Support of legacy pattern with the program's name (instead of program's code)
249                    return (A) new RedirectPage(childPage.get());
250                }
251                else
252                {
253                    return (A) childPage.get();
254                }
255            }
256            else
257            {
258                if (name.lastIndexOf("subprogram-") != -1)
259                {
260                    // Support of legacy pattern with the program's name (instead of program's code)
261                    return (A) new RedirectPage((Page) childPage.get().getChild(queuePath));
262                }
263                else
264                {
265                    return (A) childPage.get().getChild(queuePath);
266                }
267            }
268        }
269        else
270        {
271            throw new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId());
272        }
273    }
274    
275    private Optional<Page> _getChildPage(TraversableProgramPart parent, String name)
276    {
277        return _findChild(parent, name)
278                .map(child -> _toChildPage(child));
279    }
280    
281    private Optional<ProgramItem> _findChild(TraversableProgramPart parent, String name)
282    {
283        return _traverseChildren(parent)
284            .filter(child -> _filterByName(child, name))
285            .findFirst();
286    }
287    
288    private boolean _filterByName(ProgramItem programItem, String name)
289    {
290        if (programItem instanceof SubProgram)
291        {
292            String subProgramPageName = _factory.getODFPageHandler().getPageName(programItem);
293            if (name.equals(subProgramPageName))
294            {
295                return true;
296            }
297            else
298            {
299                // For legacy purpose
300                subProgramPageName = NameHelper.filterName(((SubProgram) programItem).getTitle()) + "-" + programItem.getName();
301                return name.equals(subProgramPageName);
302            }
303        }
304        else if (programItem instanceof Course)
305        {
306            String coursePageName = _factory.getODFPageHandler().getPageName(programItem);
307            return name.equals(coursePageName);
308        }
309        
310        return false;
311    }
312    
313    
314    private Page _toChildPage(ProgramItem child)
315    {
316        if (child instanceof SubProgram)
317        {
318            return _toChildProgramPage((SubProgram) child);
319        }
320        else if (child instanceof Course)
321        {
322            return _toChildCoursePage((Course) child);
323        }
324        
325        return null;
326    }
327    
328    private ProgramPage _toChildProgramPage(SubProgram child)
329    {
330        return new ProgramPage(_factory, _root, child, _path != null ? _path + '/' + getName() : getName(), getParentProgram(), this);
331    }
332    
333    private CoursePage _toChildCoursePage(Course course)
334    {
335        return new CoursePage(_factory.getCoursePageFactory(), _root, course, getParentProgram(), _path != null ? _path + '/' + getName() : getName(), this);
336    }
337    
338    @Override
339    public boolean hasChild(String name) throws AmetysRepositoryException
340    {
341        return _findChild(_program, name).isPresent();
342    }
343    
344    @Override
345    public String getId() throws AmetysRepositoryException
346    {
347        // E.g: program://_root?rootId=xxxx&programId=xxxx (for a program)
348        // E.g: program://path/to/subprogram?rootId=xxxx&programId=xxxx&parentId=xxxx (for a subprogram)
349        StringBuilder sb = new StringBuilder("program://");
350        sb.append(StringUtils.isNotEmpty(_path) ? _path : "_root");
351        sb.append("?rootId=").append(_root.getId());
352        sb.append("&programId=").append(_program.getId());
353        
354        if (_parent != null)
355        {
356            sb.append("&parentId=").append(_parent.getId());
357        }
358        
359        return sb.toString();
360    }
361
362    @Override
363    public String getName() throws AmetysRepositoryException
364    {
365        // E.g: licence-lea-anglais-allemand-H7AIIUYW
366        return _factory.getODFPageHandler().getPageName(_program);
367    }
368
369    @SuppressWarnings("unchecked")
370    @Override
371    public Page getParent() throws AmetysRepositoryException
372    {
373        if (_parentPage == null)
374        {
375            if (StringUtils.isBlank(_factory.getODFPageHandler().getLevel1Metadata(_root)))
376            {
377                _parentPage = _root;
378            }
379            else
380            {
381                String relParentPath = _computeLevelsPath() + (_path != null ? "/" + _path : "");
382                _parentPage = _root.getChild(relParentPath);
383            }
384        }
385        
386        return _parentPage;
387    }
388
389    @Override
390    public String getParentPath() throws AmetysRepositoryException
391    {
392        if (StringUtils.isBlank(_factory.getODFPageHandler().getLevel1Metadata(_root)))
393        {
394            return _root.getPath();
395        }
396        else
397        {
398            return _root.getPath() + '/' + _computeLevelsPath() + (_path != null ? "/" + _path : "");
399        }
400    }
401
402    public ModelLessDataHolder getDataHolder()
403    {
404        RepositoryData repositoryData = new MemoryRepositoryData(getName());
405        return new DefaultModelLessDataHolder(_factory.getPageDataTypeEP(), repositoryData);
406    }
407
408    private Stream<ProgramItem> _traverseChildren(TraversableProgramPart parent)
409    {
410        Predicate<ProgramItem> isSubProgram = SubProgram.class::isInstance;
411        Predicate<ProgramItem> isCourse = Course.class::isInstance;
412        
413        ProgramPartTraverser traverser = new ProgramPartTraverser(parent.getProgramPartChildren());
414        return traverser.stream().filter(isSubProgram.or(isCourse));
415    }
416    
417    /**
418     * Program part traverser. Iterate recursively on child program base.
419     */
420    static class ProgramPartTraverser extends AbstractTreeIterator<ProgramItem>
421    {
422        public ProgramPartTraverser(Collection<? extends ProgramItem> programPartChildren)
423        {
424            super(programPartChildren);
425        }
426
427        @Override
428        protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent)
429        {
430            if (parent instanceof CourseList)
431            {
432                CourseList courseList = (CourseList) parent;
433                return new ProgramPartTraverser(courseList.getCourses());
434            }
435            
436            if (parent instanceof Container)
437            {
438                Container container = (Container) parent;
439                return new ProgramPartTraverser(container.getProgramPartChildren());
440            }
441            
442            return null;
443        }
444    }
445    
446    /**
447     * Breadth first search iterator for tree structure
448     * Each node can provide an iterator that will be put in the end of the queue. 
449     * @param <T> A tree item
450     */
451    abstract static class AbstractTreeIterator<T> implements Iterator<T>
452    {
453        protected final Queue<Iterator<T>> _nodeIterators = new LinkedList<>();
454        private Boolean _hasNext;
455        
456        AbstractTreeIterator(Iterator<T> iterator)
457        {
458            if (iterator != null && iterator.hasNext())
459            {
460                _nodeIterators.add(iterator);
461            }
462        }
463        
464        AbstractTreeIterator(Collection<? extends T> children)
465        {
466            this(handleConstructorChildren(children));
467        }
468        
469        private static <T> Iterator<T> handleConstructorChildren(Collection<? extends T> children)
470        {
471            Collection<T> tChildren = Collections.unmodifiableCollection(children);
472            return tChildren.iterator();
473        }
474        
475        public boolean hasNext()
476        {
477            if (_hasNext != null)
478            {
479                return _hasNext;
480            }
481            
482            Iterator<T> it = _getOrUpdateHead();
483            if (_hasNext == null)
484            {
485                _hasNext = it != null ? it.hasNext() : false;
486            }
487            
488            return _hasNext;
489        }
490        
491        public T next()
492        {
493            if (BooleanUtils.isFalse(_hasNext))
494            {
495                throw new NoSuchElementException();
496            }
497            
498            Iterator<T> it = null;
499            if (_hasNext == null)
500            {
501                it = _getOrUpdateHead();
502            }
503            else
504            {
505                it = _nodeIterators.peek();
506            }
507            
508            T next = Optional.ofNullable(it)
509                .map(Iterator::next)
510                .orElseThrow(NoSuchElementException::new);
511            
512            Iterator<T> childIterator = provideChildIterator(next);
513            if (childIterator != null && childIterator.hasNext())
514            {
515                _nodeIterators.add(childIterator);
516            }
517            
518            // reset cached has next
519            _hasNext = null;
520            
521            return next;
522        }
523        
524        protected abstract Iterator<T> provideChildIterator(T next);
525        
526        public Stream<T> stream()
527        {
528            Iterable<T> iterable = () -> this;
529            return StreamSupport.stream(iterable.spliterator(), false);
530        }
531        
532        private Iterator<T> _getOrUpdateHead()
533        {
534            return Optional.ofNullable(_nodeIterators.peek())
535                .filter(it ->
536                {
537                    if (it.hasNext())
538                    {
539                        _hasNext = true;
540                        return true;
541                    }
542                    
543                    return false;
544                })
545                .orElseGet(() -> _updateHead());
546        }
547        
548        private Iterator<T> _updateHead()
549        {
550            _nodeIterators.poll(); // remove actual head
551            return _nodeIterators.peek();
552        }
553    }
554    
555    public ModelAwareDataHolder getTemplateParametersHolder() throws AmetysRepositoryException
556    {
557        return null;
558    }
559}