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