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.odf.program.generators;
017
018import java.io.IOException;
019import java.util.Arrays;
020import java.util.LinkedHashSet;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.ProcessingException;
028import org.apache.cocoon.components.source.impl.SitemapSource;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.generation.Generator;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.lang.StringUtils;
035import org.apache.excalibur.source.SourceResolver;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.MetadataDefinition;
040import org.ametys.cms.contenttype.MetadataDefinitionHolder;
041import org.ametys.cms.contenttype.MetadataSet;
042import org.ametys.cms.contenttype.RepeaterDefinition;
043import org.ametys.cms.repository.Content;
044import org.ametys.core.util.DateUtils;
045import org.ametys.core.util.IgnoreRootHandler;
046import org.ametys.odf.course.Course;
047import org.ametys.odf.courselist.CourseList;
048import org.ametys.odf.courselist.CourseList.ChoiceType;
049import org.ametys.odf.coursepart.CoursePart;
050import org.ametys.odf.orgunit.OrgUnit;
051import org.ametys.odf.orgunit.OrgUnitFactory;
052import org.ametys.odf.person.Person;
053import org.ametys.odf.person.PersonFactory;
054import org.ametys.odf.program.AbstractProgram;
055import org.ametys.odf.program.Container;
056import org.ametys.odf.program.Program;
057import org.ametys.odf.program.ProgramPart;
058import org.ametys.odf.program.SubProgram;
059import org.ametys.odf.program.TraversableProgramPart;
060import org.ametys.odf.translation.TranslationHelper;
061import org.ametys.plugins.repository.AmetysObject;
062import org.ametys.plugins.repository.UnknownAmetysObjectException;
063import org.ametys.plugins.repository.metadata.CompositeMetadata;
064import org.ametys.plugins.repository.metadata.UnknownMetadataException;
065
066/**
067 * {@link Generator} for rendering raw content data for a {@link Program}.
068 */
069public class ProgramContentGenerator extends ODFContentGenerator
070{
071    /** The source resolver */
072    protected SourceResolver _srcResolver;
073    
074    @Override
075    public void service(ServiceManager serviceManager) throws ServiceException
076    {
077        super.service(serviceManager);
078        _srcResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
079    }
080    
081    @Override
082    protected void _saxOtherData(Content content, Locale defaultLocale) throws SAXException, ProcessingException, IOException
083    {
084        super._saxOtherData(content, defaultLocale);
085        
086        boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false);
087        if (!isEditionMetadataSet)
088        {
089            if (content instanceof AbstractProgram)
090            {
091                AbstractProgram program = (AbstractProgram) content;
092                
093                // Contacts
094                saxPersons(program);
095                
096                // Child containers, subprograms or course lists
097                saxChildProgramPart(program, defaultLocale);
098                
099                // OrgUnits
100                saxOrgUnits(program);
101                
102                // Translations
103                saxTranslation(program);
104                
105                // Skill sets
106                saxSkillSets(program, defaultLocale);
107            }
108        }
109        
110        Request request = ObjectModelHelper.getRequest(objectModel);
111        request.setAttribute(Content.class.getName(), content);
112    }
113    
114    /**
115     * SAX the referenced {@link Person}s
116     * @param content The content to analyze
117     * @throws SAXException if an error occurs while saxing
118     */
119    protected void saxPersons(Content content) throws SAXException
120    {
121        saxLinkedContents(content, "persons", PersonFactory.PERSON_CONTENT_TYPE, "abstract");
122    }
123    
124    /**
125     * SAX the referenced {@link OrgUnit}s
126     * @param content The content to analyze
127     * @throws SAXException if an error occurs while saxing
128     */
129    protected void saxOrgUnits(Content content) throws SAXException
130    {
131        saxLinkedContents(content, "orgunits", OrgUnitFactory.ORGUNIT_CONTENT_TYPE, "link");
132    }
133    
134    /**
135     * SAX the referenced {@link ProgramPart}s
136     * @param program The program or subprogram
137     * @param defaultLocale The default locale
138     * @throws SAXException if an error occurs while saxing
139     * @throws IOException if an error occurs
140     * @throws ProcessingException if an error occurs
141     */
142    protected void saxChildProgramPart(AbstractProgram program, Locale defaultLocale) throws SAXException, ProcessingException, IOException
143    {
144        String[] children = program.getMetadataHolder().getStringArray(TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, new String[0]);
145        for (String id : children)
146        {
147            try
148            {
149                AmetysObject child = _resolver.resolveById(id);
150                
151                if (child instanceof SubProgram)
152                {
153                    saxSubProgram((SubProgram) child, program.getContextPath());
154                }
155                else if (child instanceof Container)
156                {
157                    saxContainer ((Container) child, program.getContextPath(), defaultLocale);
158                }
159                else if (child instanceof CourseList)
160                {
161                    saxCourseList((CourseList) child, program.getContextPath(), defaultLocale);
162                }
163            }
164            catch (UnknownAmetysObjectException e)
165            {
166                // The content can reference a non-existing child: ignore the exception.
167            }
168        }
169    }
170    
171    /**
172     * SAX the existing translation
173     * @param content The content
174     * @throws SAXException if an error occurs while saxing
175     */
176    protected void saxTranslation(Content content) throws SAXException
177    {
178        Map<String, String> translations = TranslationHelper.getTranslations(content);
179        if (!translations.isEmpty())
180        {
181            saxTranslations(translations);
182        }
183    }
184    
185    /**
186     * SAX the referenced content types.
187     * @param content The content to analyze
188     * @param tagName The root tagName
189     * @param linkedContentType The content type to search
190     * @param metadataSet The metadata set to parse the found contents
191     * @throws SAXException if an error occurs while saxing
192     */
193    protected void saxLinkedContents(Content content, String tagName, String linkedContentType, String metadataSet) throws SAXException
194    {
195        Set<String> contentIds = getLinkedContents(content, linkedContentType);
196        
197        XMLUtils.startElement(contentHandler, tagName);
198        for (String id : contentIds)
199        {
200            if (StringUtils.isNotEmpty(id))
201            {
202                try
203                {
204                    Content subContent = _resolver.resolveById(id);
205                    saxContent(subContent, metadataSet);
206                }
207                catch (IOException e)
208                {
209                    throw new SAXException(e);
210                }
211                catch (UnknownAmetysObjectException e)
212                {
213                    // The program can reference a non-existing person: ignore the exception.
214                }
215            }
216        }
217        XMLUtils.endElement(contentHandler, tagName);
218    }
219    
220    /**
221     * Get the linked contents of the defined content type.
222     * @param content The content to analyze
223     * @param linkedContentType The content type to search
224     * @return A {@link Set} of content ids
225     */
226    protected Set<String> getLinkedContents(Content content, String linkedContentType)
227    {
228        Set<String> contentIds = new LinkedHashSet<>();
229        
230        for (String cTypeId : content.getTypes())
231        {
232            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
233            contentIds.addAll(_getContentIds(content.getMetadataHolder(), cType, linkedContentType));
234        }
235        
236        return contentIds;
237    }
238    
239    private Set<String> _getContentIds(CompositeMetadata metadataHolder, MetadataDefinitionHolder metadataDefHolder, String contentType)
240    {
241        Set<String> contentIds = new LinkedHashSet<>();
242        
243        for (String metadataName : metadataDefHolder.getMetadataNames())
244        {
245            MetadataDefinition metadataDef = metadataDefHolder.getMetadataDefinition(metadataName);
246            
247            switch (metadataDef.getType())
248            {
249                case CONTENT:
250                    if (contentType.equals(metadataDef.getContentType()))
251                    {
252                        String[] values = metadataHolder.getStringArray(metadataName, new String[0]);
253                        contentIds.addAll(Arrays.asList(values));
254                    }
255                    break;
256                case COMPOSITE:
257                    try
258                    {
259                        CompositeMetadata subMetadataHolder = metadataHolder.getCompositeMetadata(metadataName);
260                        
261                        if (metadataDef instanceof RepeaterDefinition)
262                        {
263                            String[] entryNames = subMetadataHolder.getMetadataNames();
264                            for (String entryName : entryNames)
265                            {
266                                CompositeMetadata entryHolder = subMetadataHolder.getCompositeMetadata(entryName);
267                                contentIds.addAll(_getContentIds(entryHolder, metadataDef, contentType));
268                            }
269                        }
270                        else
271                        {
272                            contentIds.addAll(_getContentIds(subMetadataHolder, metadataDef, contentType));
273                        }
274                    }
275                    catch (UnknownMetadataException e)
276                    {
277                        // Nothing
278                    }
279                    break;
280                    
281                default:
282                    break;
283            }
284        }
285        
286        return contentIds;
287    }
288    
289    /**
290     * SAX a container
291     * @param container the container to SAX
292     * @param parentPath the parent path
293     * @param defaultLocale The default locale
294     * @throws SAXException if an error occurs
295     * @throws IOException if an error occurs
296     * @throws ProcessingException if an error occurs
297     */
298    protected void saxContainer(Container container, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException, IOException
299    {
300        AttributesImpl attrs = new AttributesImpl();
301        attrs.addCDATAAttribute("title", container.getTitle());
302        attrs.addCDATAAttribute("id", container.getId());
303        _addAttrIfNotEmpty(attrs, "code", container.getCode());
304        _addAttrIfNotEmpty(attrs, "nature", container.getNature());
305        double ects = container.getEcts();
306        if (ects > 0)
307        {
308            attrs.addCDATAAttribute("ects", String.valueOf(ects));
309        }
310        
311        XMLUtils.startElement(contentHandler, "container", attrs);
312        
313        // Children
314        String[] children = container.getMetadataHolder().getStringArray(TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, new String[0]);
315        for (String id : children)
316        {
317            try
318            {
319                AmetysObject ao = _resolver.resolveById(id);
320                if (ao instanceof SubProgram)
321                {
322                    saxSubProgram((SubProgram) ao, parentPath);
323                }
324                else if (ao instanceof Container)
325                {
326                    saxContainer ((Container) ao, parentPath, defaultLocale);
327                }
328                else if (ao instanceof CourseList)
329                {
330                    saxCourseList((CourseList) ao, parentPath, defaultLocale);
331                }
332            }
333            catch (UnknownAmetysObjectException e)
334            {
335                // The content can reference a non-existing child (in live for example): ignore the exception.
336            }
337        }
338        
339        XMLUtils.endElement(contentHandler, "container");
340    }
341    
342    /**
343     * SAX a sub program
344     * @param subProgram the sub program to SAX
345     * @param parentPath the parent path
346     * @throws SAXException if an error occurs
347     */
348    protected void saxSubProgram(SubProgram subProgram, String parentPath) throws SAXException
349    {
350        AttributesImpl attrs = new AttributesImpl();
351        attrs.addCDATAAttribute("title", subProgram.getTitle());
352        attrs.addCDATAAttribute("id", subProgram.getId());
353        _addAttrIfNotEmpty(attrs, "code", subProgram.getCode());
354        _addAttrIfNotEmpty(attrs, "ects", subProgram.getEcts());
355        
356        if (parentPath != null)
357        {
358            attrs.addCDATAAttribute("path", parentPath + "/" + subProgram.getName());
359        }
360        XMLUtils.startElement(contentHandler, "subprogram", attrs);
361        
362        // SAX the XML content of subprogram with nature "Parcours" only
363        if ("P".equals(subProgram.getMetadataHolder().getString(AbstractProgram.EDUCATION_KIND, "")))
364        {
365            try
366            {
367                saxContent(subProgram, "parcours", "xml", false, true);
368            }
369            catch (IOException e)
370            {
371                throw new SAXException(e);
372            }
373        }
374        
375        XMLUtils.endElement(contentHandler, "subprogram");
376    }
377    
378    /**
379     * SAX a course list
380     * @param courseList The course list to SAX
381     * @param parentPath the parent path
382     * @param defaultLocale The default locale
383     * @throws SAXException if an error occurs
384     * @throws IOException if an error occurs
385     * @throws ProcessingException if an error occurs
386     */
387    protected void saxCourseList(CourseList courseList, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException, IOException
388    {
389        AttributesImpl attrs = new AttributesImpl();
390        attrs.addCDATAAttribute("title", courseList.getTitle());
391        _addAttrIfNotEmpty(attrs, "code", courseList.getCode());
392        
393        ChoiceType type = courseList.getType();
394        if (type != null)
395        {
396            attrs.addCDATAAttribute("type", courseList.getType().toString());
397        }
398        
399        if (ChoiceType.CHOICE.equals(type))
400        {
401            attrs.addCDATAAttribute("min", String.valueOf(courseList.getMinNumberOfCourses()));
402            attrs.addCDATAAttribute("max", String.valueOf(courseList.getMaxNumberOfCourses()));
403        }
404        
405        XMLUtils.startElement(contentHandler, "courseList", attrs);
406        
407        for (Course course : courseList.getCourses())
408        {
409            try
410            {
411                saxCourse(course, parentPath, defaultLocale);
412            }
413            catch (UnknownAmetysObjectException e)
414            {
415                // The content can reference a non-existing course (in live for example): ignore the exception.
416            }
417        }
418        XMLUtils.endElement(contentHandler, "courseList");
419    }
420    
421    /**
422     * SAX a course
423     * @param course the course to SAX
424     * @param parentPath the parent path
425     * @param defaultLocale The default locale
426     * @throws SAXException if an error occurs
427     * @throws IOException if an error occurs
428     * @throws ProcessingException if an error occurs
429     */
430    protected void saxCourse(Course course, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException, IOException
431    {
432        AttributesImpl attrs = new AttributesImpl();
433        attrs.addCDATAAttribute("title", course.getTitle());
434        attrs.addCDATAAttribute("code", course.getCode());
435        attrs.addCDATAAttribute("id", course.getId());
436        attrs.addCDATAAttribute("name", course.getName());
437        
438        if (parentPath != null)
439        {
440            attrs.addCDATAAttribute("path", parentPath + "/" + course.getName() + "-" + course.getCode());
441        }
442        
443        MetadataSet metadataSet = _cTypesHelper.getMetadataSetForView("courseList", course.getTypes(), course.getMixinTypes());
444        
445        XMLUtils.startElement(contentHandler, "course", attrs);
446        
447        try
448        {
449            if (metadataSet != null)
450            {
451                XMLUtils.startElement(contentHandler, "metadata");
452                _metadataManager.saxMetadata(contentHandler, course, metadataSet, null);
453                XMLUtils.endElement(contentHandler, "metadata");
454            }
455        }
456        catch (IOException e)
457        {
458            throw new SAXException(e);
459        }
460        
461        // Course list
462        for (CourseList childCl : course.getCourseLists())
463        {
464            saxCourseList(childCl, parentPath, defaultLocale);
465        }
466        
467        // Course parts
468        for (CoursePart coursePart : course.getCourseParts())
469        {
470            saxCoursePart(coursePart, defaultLocale);
471        }
472        
473        XMLUtils.endElement(contentHandler, "course");
474    }
475    
476    /**
477     * SAX the HTML content of a {@link Content}
478     * @param content the content
479     * @param metadataSetName the metadata set name
480     * @throws SAXException If an error occurred saxing the content
481     * @throws IOException If an error occurred resolving the content
482     */
483    protected void saxContent(Content content, String metadataSetName) throws SAXException, IOException
484    {
485        String format = parameters.getParameter("output-format", "html");
486        if (StringUtils.isEmpty(format))
487        {
488            format = "html";
489        }
490        
491        saxContent(content, metadataSetName, format, true, false);
492    }
493    
494    /**
495     * SAX a {@link Content} to given format
496     * @param content the content
497     * @param metadataSetName the metadata set name
498     * @param format the output format
499     * @param withContentRoot true to wrap content stream into a root content tag
500     * @param ignoreChildren true to not SAX sub contents
501     * @throws SAXException If an error occurred saxing the content
502     * @throws IOException If an error occurred resolving the content
503     */
504    protected void saxContent(Content content, String metadataSetName, String format, boolean withContentRoot, boolean ignoreChildren) throws SAXException, IOException
505    {
506        SitemapSource src = null;      
507        try
508        {
509            String uri = "cocoon://_content." + format + "?contentId=" + content.getId() + "&metadataSetName=" + metadataSetName + "&output-format=" + format;
510            if (ignoreChildren)
511            {
512                uri += "&ignoreChildren=true";
513            }
514            
515            src = (SitemapSource) _srcResolver.resolveURI(uri);
516            
517            if (withContentRoot)
518            {
519                AttributesImpl attrs = new AttributesImpl();
520                attrs.addCDATAAttribute("id", content.getId());
521                attrs.addCDATAAttribute("name", content.getName());
522                attrs.addCDATAAttribute("title", content.getTitle());
523                attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
524                
525                XMLUtils.startElement(contentHandler, "content", attrs);
526            }
527            
528            src.toSAX(new IgnoreRootHandler(contentHandler));
529            
530            if (withContentRoot)
531            {
532                XMLUtils.endElement(contentHandler, "content");
533            }
534        }
535        catch (UnknownAmetysObjectException e)
536        {
537            // The content may be archived
538        }
539        finally
540        {
541            _srcResolver.release(src);
542        }
543    }
544    
545    /**
546     * Add an attribute if its not null or empty.
547     * @param attrs The attributes
548     * @param attrName The attribute name
549     * @param attrValue The attribute value
550     */
551    protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue)
552    {
553        if (StringUtils.isNotEmpty(attrValue))
554        {
555            attrs.addCDATAAttribute(attrName, attrValue);
556        }
557    }
558    
559    /**
560     * SAX a course part
561     * @param coursePart The course part to SAX
562     * @param defaultLocale The default locale
563     * @throws SAXException if an error occurs
564     * @throws IOException if an error occurs
565     * @throws ProcessingException if an error occurs
566     */
567    protected void saxCoursePart(CoursePart coursePart, Locale defaultLocale) throws SAXException, ProcessingException, IOException
568    {
569        AttributesImpl attrs = new AttributesImpl();
570        attrs.addCDATAAttribute("id", coursePart.getId());
571        attrs.addCDATAAttribute("title", coursePart.getTitle());
572        _addAttrIfNotEmpty(attrs, "code", coursePart.getCode());
573
574        XMLUtils.startElement(contentHandler, "coursePart", attrs);
575        MetadataSet metadataSet = _cTypesHelper.getMetadataSetForView("sax", coursePart.getTypes(), coursePart.getMixinTypes());
576        _saxMetadata(coursePart, metadataSet, defaultLocale);
577        XMLUtils.endElement(contentHandler, "coursePart");
578    }
579}