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.List;
024import java.util.NoSuchElementException;
025import java.util.Set;
026import java.util.stream.Collectors;
027import java.util.stream.Stream;
028
029import org.apache.commons.lang3.StringUtils;
030
031import org.ametys.cms.FilterNameHelper;
032import org.ametys.odf.ProgramItem;
033import org.ametys.odf.course.Course;
034import org.ametys.odf.courselist.CourseList;
035import org.ametys.odf.program.Program;
036import org.ametys.plugins.explorer.resources.ResourceCollection;
037import org.ametys.plugins.odfweb.repository.ProgramPage.AbstractTreeIterator;
038import org.ametys.plugins.repository.AmetysObject;
039import org.ametys.plugins.repository.AmetysObjectIterable;
040import org.ametys.plugins.repository.AmetysObjectIterator;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.AmetysRepositoryException;
043import org.ametys.plugins.repository.CollectionIterable;
044import org.ametys.plugins.repository.UnknownAmetysObjectException;
045import org.ametys.plugins.repository.metadata.CompositeMetadata;
046import org.ametys.web.repository.page.Page;
047import org.ametys.web.repository.page.UnknownZoneException;
048import org.ametys.web.repository.page.Zone;
049import org.ametys.web.repository.site.Site;
050import org.ametys.web.repository.sitemap.Sitemap;
051
052import com.google.common.collect.Iterables;
053
054/**
055 * Page representing a course.
056 */
057public class CoursePage implements Page
058{
059    private AmetysObjectResolver _resolver;
060    private OdfPageHandler _odfPageHandler;
061    private Page _root;
062    private Course _course;
063    private String _path;
064    private Page _parentPage;
065    private Program _parentProgram;
066    
067    /**
068     * Constructor.
069     * @param resolver the {@link AmetysObjectResolver}.
070     * @param odfPageHandler the {@link OdfPageHandler} to handle ODF pages.
071     * @param root the odf root page.
072     * @param course the course.
073     * @param parentProgram the parent program
074     * @param path path from the parent {@link ProgramPage}
075     * @param parentPage the parent {@link Page} or null if not yet computed.
076     */
077    public CoursePage(AmetysObjectResolver resolver, OdfPageHandler odfPageHandler, Page root, Course course, Program parentProgram, String path, Page parentPage)
078    {
079        _resolver = resolver;
080        _odfPageHandler = odfPageHandler;
081        _root = root;
082        _course = course;
083        _path = path;
084        _parentPage = parentPage;
085        _parentProgram = parentProgram;
086    }
087    
088    /**
089     * Compute the path from the root odf page, representing the first and second level pages.
090     * @return the path
091     */
092    String _computeLevelsPath()
093    {
094        String level1 = _odfPageHandler.getProgramLevel1Value(_root, _parentProgram);
095        String level2 = _odfPageHandler.getProgramLevel2Value(_root, _parentProgram);
096        
097        // The path is no more valid, re-calculate the real path
098        String secondLevelPageId = "odfLevel2://" + level1 + "/" + level2 + "?rootId=" + _root.getId();
099        Page secondLevelPage = _resolver.resolveById(secondLevelPageId);
100        
101        return secondLevelPage.getParent().getName() + "/" + secondLevelPage.getName();
102    }
103    
104    /**
105     * Returns the associated {@link Course}.
106     * @return the associated {@link Course}.
107     */
108    public Course getCourse()
109    {
110        return _course;
111    }
112
113    @Override
114    public int getDepth() throws AmetysRepositoryException
115    {
116        return _root.getDepth() + 2 + _path.split("/").length;
117    }
118
119    @Override
120    public Set<String> getReferers() throws AmetysRepositoryException
121    {
122        throw new UnsupportedOperationException("getReferers not supported on virtual odf pages");
123    }
124
125    @Override
126    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
127    {
128        return null;
129    }
130
131    @Override
132    public String getTemplate() throws AmetysRepositoryException
133    {
134        return "course";
135    }
136
137    @Override
138    public String getTitle() throws AmetysRepositoryException
139    {
140        return _course.getTitle();
141    }
142
143    @Override
144    public String getLongTitle() throws AmetysRepositoryException
145    {
146        return _course.getTitle();
147    }
148 
149    @Override
150    public PageType getType() throws AmetysRepositoryException
151    {
152        return PageType.CONTAINER;
153    }
154
155    @Override
156    public String getURL() throws AmetysRepositoryException
157    {
158        throw new UnsupportedOperationException("getURL not supported on virtual odf pages");
159    }
160
161    @Override
162    public LinkType getURLType() throws AmetysRepositoryException
163    {
164        throw new UnsupportedOperationException("getURLType not supported on virtual odf pages");
165    }
166
167    @Override
168    public Zone getZone(String name) throws UnknownZoneException, AmetysRepositoryException
169    {
170        if (!"default".equals(name))
171        {
172            throw new IllegalArgumentException("Only the zone named 'default' is actually supported on virtual program pages.");
173        }
174        
175        return new CourseZone(this);
176    }
177
178    @Override
179    public AmetysObjectIterable< ? extends Zone> getZones() throws AmetysRepositoryException
180    {
181        ArrayList<Zone> zones = new ArrayList<>();
182        zones.add(new CourseZone(this));
183        return new CollectionIterable<>(zones);
184    }
185
186    @Override
187    public boolean hasZone(String name) throws AmetysRepositoryException
188    {
189        return "default".equals(name);
190    }
191
192    @Override
193    public AmetysObjectIterable<? extends Page> getChildrenPages() throws AmetysRepositoryException
194    {
195        List<Page> coursePages = _traverseCourseLists().map(this::_toCoursePage).collect(Collectors.toList());
196        return new CollectionIterable<>(coursePages);
197    }
198
199    private CoursePage _toCoursePage(Course course)
200    {
201        return new CoursePage(_resolver, _odfPageHandler, _root, course, _parentProgram, _path + '/' + getName(), this);
202    }
203
204    @Override
205    public String getPathInSitemap() throws AmetysRepositoryException
206    {
207        return _root.getPathInSitemap() + "/" + _computeLevelsPath() + "/" + _path + "/" + getName();
208    }
209
210    @Override
211    public Site getSite() throws AmetysRepositoryException
212    {
213        return _root.getSite();
214    }
215
216    @Override
217    public String getSiteName() throws AmetysRepositoryException
218    {
219        return _root.getSiteName();
220    }
221
222    @Override
223    public Sitemap getSitemap() throws AmetysRepositoryException
224    {
225        return _root.getSitemap();
226    }
227
228    @Override
229    public String getSitemapName() throws AmetysRepositoryException
230    {
231        return _root.getSitemapName();
232    }
233
234    @SuppressWarnings("unchecked")
235    @Override
236    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
237    {
238        if (path.isEmpty())
239        {
240            throw new AmetysRepositoryException("path must be non empty");
241        }
242        
243        List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2));
244        String name = headQueuePath.get(0);
245        String queuePath = Iterables.get(headQueuePath, 1, null); 
246        
247        return (A) _traverseCourseLists()
248            .filter(course -> _filterByPageName(course, name))
249            .findFirst()
250            .map(this::_toCoursePage)
251            .map(page -> StringUtils.isEmpty(queuePath) ? page : page.<Page>getChild(queuePath))
252            .orElseThrow(() -> 
253                new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId()));
254    }
255    
256    private boolean _filterByPageName(Course course, String pageName)
257    {
258        String coursePageName = FilterNameHelper.filterName(course.getTitle()) + "-" + course.getCode();
259        return StringUtils.equals(coursePageName, pageName);
260    }
261
262    @SuppressWarnings("unchecked")
263    @Override
264    public AmetysObjectIterable<? extends Page> getChildren() throws AmetysRepositoryException
265    {
266        return getChildrenPages();
267    }
268
269    @Override
270    public boolean hasChild(String name) throws AmetysRepositoryException
271    {
272        return _traverseCourseLists()
273            .filter(course -> _filterByPageName(course, name))
274            .findFirst()
275            .isPresent();
276    }
277
278    @Override
279    public String getId() throws AmetysRepositoryException
280    {
281        // E.g: course://licence-lea-anglais-allemand-program-h6yolddj/parcours-1-subprogram-f7usj1ss?rootId=xxx&courseId=xxx&programId=xxxx
282        return "course://" + _path + "?rootId=" + _root.getId() + "&courseId=" + _course.getId() + "&programId=" + _parentProgram.getId();
283    }
284    @Override
285    public String getName() throws AmetysRepositoryException
286    {
287        // E.g: langue-anglaise-1-h6yp1p98
288        return FilterNameHelper.filterName(_course.getTitle()) + "-" + _course.getCode();
289    }
290
291    @SuppressWarnings("unchecked")
292    @Override
293    public Page getParent() throws AmetysRepositoryException
294    {
295        if (_parentPage == null)
296        {
297            String relParentPath = _computeLevelsPath() + "/" + _path;
298            _parentPage = _root.getChild(relParentPath);
299        }
300        
301        return _parentPage;
302    }
303    
304    @Override
305    public String getParentPath() throws AmetysRepositoryException
306    {
307        return _root.getPath() + "/" + _computeLevelsPath() + "/" + _path;
308    }
309
310    @Override
311    public String getPath() throws AmetysRepositoryException
312    {
313        return getParentPath() + "/" + getName();
314    }
315
316    @Override
317    public CompositeMetadata getMetadataHolder()
318    {
319        return new StaticCompositeMetadata();
320    }
321
322    @Override
323    public Set<String> getTags() throws AmetysRepositoryException
324    {
325        return Collections.emptySet();
326    }
327    
328    @Override
329    public boolean isVisible() throws AmetysRepositoryException
330    {
331        return true;
332    }
333
334    @Override
335    public AmetysObjectIterable< ? extends Page> getChildrenPages(boolean includeInvisiblePages) throws AmetysRepositoryException
336    {
337        return getChildrenPages();
338    }
339
340    public Page getChildPageAt(int index) throws UnknownAmetysObjectException, AmetysRepositoryException
341    {
342        // TODO make a default method or call a helper instead of duplicate the code into each Page (FirstLevel / SecondLevel / Program etc...)
343        
344        if (index < 0)
345        {
346            throw new AmetysRepositoryException("Child page index cannot be negative");
347        }
348        
349        AmetysObjectIterator<? extends Page> childPages = getChildrenPages().iterator();
350        
351        try
352        {
353            childPages.skip(index);
354            return childPages.next();
355        }
356        catch (NoSuchElementException e)
357        {
358            throw new UnknownAmetysObjectException("There's no child page at index " + index + " for page " + this.getId());
359        }
360    }
361    
362    private Stream<Course> _traverseCourseLists()
363    {
364        CourseListTraverser traverser = new CourseListTraverser(_course.getCourseLists());
365        return traverser.stream()
366                .filter(Course.class::isInstance)
367                .map(Course.class::cast);
368    }
369    
370    static class CourseListTraverser extends AbstractTreeIterator<ProgramItem>
371    {
372        public CourseListTraverser(Collection<? extends ProgramItem> programPartChildren)
373        {
374            super(programPartChildren);
375        }
376        
377        @Override
378        protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent)
379        {
380            if (parent instanceof CourseList)
381            {
382                CourseList courseList = (CourseList) parent;
383                return new CourseListTraverser(courseList.getCourses());
384            }
385            
386            return null;
387        }
388    }
389}