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