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.Arrays;
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Objects;
023import java.util.Optional;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import org.apache.commons.lang3.StringUtils;
028
029import org.ametys.odf.ProgramItem;
030import org.ametys.odf.course.Course;
031import org.ametys.odf.courselist.CourseList;
032import org.ametys.odf.program.Program;
033import org.ametys.plugins.odfweb.repository.ProgramPage.AbstractTreeIterator;
034import org.ametys.plugins.repository.AmetysObject;
035import org.ametys.plugins.repository.AmetysObjectIterable;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.CollectionIterable;
038import org.ametys.plugins.repository.UnknownAmetysObjectException;
039import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
040import org.ametys.plugins.repository.data.holder.impl.DefaultModelLessDataHolder;
041import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
042import org.ametys.plugins.repository.data.repositorydata.impl.MemoryRepositoryData;
043import org.ametys.web.repository.page.Page;
044import org.ametys.web.repository.page.virtual.VirtualPageConfiguration;
045
046import com.google.common.collect.Iterables;
047
048/**
049 * Page representing a course.
050 */
051public class CoursePage extends AbstractProgramItemPage<CoursePageFactory>
052{
053    private Course _course;
054    private String _path;
055    private Page _parentPage;
056    private Program _parentProgram;
057
058    /**
059     * Constructor.
060     * @param factory The factory
061     * @param root the odf root page.
062     * @param course the course.
063     * @param parentProgram the parent program
064     * @param path path from the parent {@link ProgramPage}
065     * @param parentPage the parent {@link Page} or null if not yet computed.
066     * @param configuration The Course virtual page's configuration
067     */
068    public CoursePage(Page root, VirtualPageConfiguration configuration, CoursePageFactory factory, Course course, Program parentProgram, String path, Page parentPage)
069    {
070        super(root, configuration, factory.getScheme(), factory);
071        
072        _course = course;
073        _path = path;
074        _parentPage = parentPage;
075        _parentProgram = parentProgram;
076    }
077    
078    /**
079     * Returns the associated {@link Course}.
080     * @return the associated {@link Course}.
081     */
082    public Course getCourse()
083    {
084        return _course;
085    }
086    
087    @Override
088    protected ProgramItem getProgramItem()
089    {
090        return getCourse();
091    }
092
093    @Override
094    public int getDepth() throws AmetysRepositoryException
095    {
096        int levelDepth = 0;
097        if (StringUtils.isNotBlank(_factory.getODFPageHandler().getLevel1Metadata(_root)))
098        {
099            levelDepth++;
100            if (StringUtils.isNotBlank(_factory.getODFPageHandler().getLevel2Metadata(_root)))
101            {
102                levelDepth++;
103            }
104        }
105        
106        return _root.getDepth() + levelDepth + _path.split("/").length;
107    }
108
109    @Override
110    public String getTitle() throws AmetysRepositoryException
111    {
112        return _course.getTitle();
113    }
114
115    @Override
116    public String getLongTitle() throws AmetysRepositoryException
117    {
118        return _course.getTitle();
119    }
120 
121    @Override
122    public AmetysObjectIterable<? extends Page> getChildrenPages() throws AmetysRepositoryException
123    {
124        Collection<CoursePage> children = _transformChildrenPages(_traverseCourseLists()).toList();
125        return new CollectionIterable<>(children);
126    }
127
128    private CoursePage _createCoursePage(Course course)
129    {
130        return _factory.createCoursePage(_root, course, _parentProgram, _path + '/' + getName(), this);
131    }
132
133    @Override
134    public String getPathInSitemap() throws AmetysRepositoryException
135    {
136        String path = _computePath(_root.getPathInSitemap());
137        return path == null ? null : path + "/" + getName();
138    }
139    
140    private Program _getParentProgram()
141    {
142        return _parentProgram;
143    }
144    
145    @SuppressWarnings("unchecked")
146    @Override
147    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
148    {
149        if (path.isEmpty())
150        {
151            throw new AmetysRepositoryException("path must be non empty");
152        }
153        
154        List<String> headQueuePath = Arrays.asList(StringUtils.split(path, "/", 2));
155        String name = headQueuePath.get(0);
156        String queuePath = Iterables.get(headQueuePath, 1, null);
157        
158        return (A) _findChildPage(name)
159            .map(cp -> _factory.getODFPageHandler().addRedirectIfNeeded(cp, name))
160            .map(cp -> _factory.getODFPageHandler().exploreQueuePath(cp, queuePath))
161            .orElseThrow(() -> new UnknownAmetysObjectException("Unknown child page '" + path + "' for page " + getId()));
162    }
163    
164    private Optional<CoursePage> _findChildPage(String name)
165    {
166        return _transformChildrenPages(_traverseCourseLists().filter(child -> _filterByName(child, name))).findFirst();
167    }
168    
169    private boolean _filterByName(Course course, String pageName)
170    {
171        // If last part is equals to the course code, the page matches
172        return course.getCode().equals(pageName.substring(pageName.lastIndexOf("-") + 1));
173    }
174
175    @Override
176    public boolean hasChild(String name) throws AmetysRepositoryException
177    {
178        return _findChildPage(name).isPresent();
179    }
180
181    @Override
182    public String getId() throws AmetysRepositoryException
183    {
184        // E.g: course://licence-lea-anglais-allemand-H6YOLDDJ/parcours-1-f7usj1ss?rootId=xxx&courseId=xxx&programId=xxxx
185        return "course://" + _path + "?rootId=" + _root.getId() + "&courseId=" + _course.getId() + "&programId=" + _parentProgram.getId();
186    }
187    @Override
188    public String getName() throws AmetysRepositoryException
189    {
190        // E.g: langue-anglaise-1-H6YP1P98
191        return _factory.getODFPageHandler().getPageName(_course);
192    }
193
194    @SuppressWarnings("unchecked")
195    @Override
196    public Page getParent() throws AmetysRepositoryException
197    {
198        if (_parentPage == null)
199        {
200            String childPath = _computePath(null);
201            if (childPath != null)
202            {
203                _parentPage = childPath.isEmpty() ? _root : _root.getChild(childPath);
204            }
205        }
206        
207        return _parentPage;
208    }
209    
210    @Override
211    public String getParentPath() throws AmetysRepositoryException
212    {
213        return _computePath(_root.getPath());
214    }
215
216    public ModelLessDataHolder getDataHolder()
217    {
218        RepositoryData repositoryData = new MemoryRepositoryData(getName());
219        return new DefaultModelLessDataHolder(_factory.getPageDataTypeEP(), repositoryData);
220    }
221
222    private Stream<Course> _traverseCourseLists()
223    {
224        CourseListTraverser traverser = new CourseListTraverser(_course.getCourseLists());
225        return traverser.stream()
226                .filter(Course.class::isInstance)
227                .map(Course.class::cast);
228    }
229    
230    static class CourseListTraverser extends AbstractTreeIterator<ProgramItem>
231    {
232        public CourseListTraverser(Collection<? extends ProgramItem> programPartChildren)
233        {
234            super(programPartChildren);
235        }
236        
237        @Override
238        protected Iterator<ProgramItem> provideChildIterator(ProgramItem parent)
239        {
240            if (parent instanceof CourseList courseList)
241            {
242                return new CourseListTraverser(courseList.getCourses());
243            }
244            
245            return null;
246        }
247    }
248    
249    @SuppressWarnings("unchecked")
250    @Override
251    public Course getContent()
252    {
253        Course course = getCourse();
254        course.setContextPath(getPathInSitemap());
255        
256        if (!_factory.isIndexing())
257        {
258            // computing educational paths is actually very expensive, and only useful during rendering
259            // we are very conservative here and only disable that computing for specific indexing cases
260            // (we could have chosen to only enable it when rendering, but we don't want to forget specific cases)
261            setCurrentEducationalPaths(course);
262        }
263        
264        return course;
265    }
266    
267    private String _computePath(String rootPath)
268    {
269        String levelsPath = _factory.getODFPageHandler().computeLevelsPath(_root, _getParentProgram());
270        
271        // The current program has no valid attributes for the levels selected in the ODF root
272        if (levelsPath == null)
273        {
274            throw new UnknownAmetysObjectException("Page of program " + _getParentProgram().getId() + " does not have a valid level path");
275        }
276        
277        return Stream.of(rootPath, levelsPath, _path)
278            .filter(StringUtils::isNotEmpty)
279            .collect(Collectors.joining("/"));
280    }
281    
282    private Stream<CoursePage> _transformChildrenPages(Stream<Course> children)
283    {
284        return children
285            .map(this::_createCoursePage)
286            .filter(Objects::nonNull)
287            // Test if the child page is in existing virtual pages
288            .filter(page -> {
289                try
290                {
291                    page.getPathInSitemap();
292                    return true;
293                }
294                catch (UnknownAmetysObjectException e)
295                {
296                    return false;
297                }
298            });
299    }
300}