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