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.CmsConstants;
044import org.ametys.cms.contenttype.ContentAttributeDefinition;
045import org.ametys.cms.contenttype.ContentType;
046import org.ametys.cms.data.ContentDataHelper;
047import org.ametys.cms.data.ContentValue;
048import org.ametys.cms.repository.Content;
049import org.ametys.core.cache.AbstractCacheManager;
050import org.ametys.core.cache.Cache;
051import org.ametys.core.util.DateUtils;
052import org.ametys.core.util.IgnoreRootHandler;
053import org.ametys.odf.NoLiveVersionException;
054import org.ametys.odf.ODFHelper;
055import org.ametys.odf.course.Course;
056import org.ametys.odf.courselist.CourseList;
057import org.ametys.odf.courselist.CourseList.ChoiceType;
058import org.ametys.odf.coursepart.CoursePart;
059import org.ametys.odf.orgunit.OrgUnit;
060import org.ametys.odf.orgunit.OrgUnitFactory;
061import org.ametys.odf.person.Person;
062import org.ametys.odf.person.PersonFactory;
063import org.ametys.odf.program.AbstractProgram;
064import org.ametys.odf.program.Container;
065import org.ametys.odf.program.Program;
066import org.ametys.odf.program.ProgramPart;
067import org.ametys.odf.program.SubProgram;
068import org.ametys.odf.program.TraversableProgramPart;
069import org.ametys.odf.translation.TranslationHelper;
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 ProcessingException if an error occurs
431     */
432    protected void saxCourseList(CourseList courseList, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException
433    {
434        AttributesImpl attrs = new AttributesImpl();
435        attrs.addCDATAAttribute("title", courseList.getTitle());
436        attrs.addCDATAAttribute("id", courseList.getId());
437        _addAttrIfNotEmpty(attrs, "code", courseList.getCode());
438        
439        ChoiceType type = courseList.getType();
440        if (type != null)
441        {
442            attrs.addCDATAAttribute("type", type.toString());
443        }
444        
445        if (ChoiceType.CHOICE.equals(type))
446        {
447            attrs.addCDATAAttribute("min", String.valueOf(courseList.getMinNumberOfCourses()));
448            attrs.addCDATAAttribute("max", String.valueOf(courseList.getMaxNumberOfCourses()));
449        }
450        
451        XMLUtils.startElement(contentHandler, "courseList", attrs);
452        
453        for (Course course : courseList.getCourses())
454        {
455            try
456            {
457                saxCourse(course, parentPath, defaultLocale);
458            }
459            catch (UnknownAmetysObjectException e)
460            {
461                // The content can reference a non-existing course (in live for example): ignore the exception.
462            }
463        }
464        XMLUtils.endElement(contentHandler, "courseList");
465    }
466    
467    /**
468     * SAX a course
469     * @param course the course to SAX
470     * @param parentPath the parent path
471     * @param defaultLocale The default locale
472     * @throws SAXException if an error occurs
473     * @throws ProcessingException if an error occurs
474     */
475    protected void saxCourse(Course course, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException
476    {
477        AttributesImpl attrs = new AttributesImpl();
478        attrs.addCDATAAttribute("title", course.getTitle());
479        attrs.addCDATAAttribute("code", course.getCode());
480        attrs.addCDATAAttribute("id", course.getId());
481        attrs.addCDATAAttribute("name", course.getName());
482        
483        if (parentPath != null)
484        {
485            attrs.addCDATAAttribute("path", parentPath + "/" + course.getName() + "-" + course.getCode());
486        }
487        
488        View view = _cTypesHelper.getView("courseList", course.getTypes(), course.getMixinTypes());
489        
490        XMLUtils.startElement(contentHandler, "course", attrs);
491        
492        if (view != null)
493        {
494            XMLUtils.startElement(contentHandler, "metadata");
495            course.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false));
496            XMLUtils.endElement(contentHandler, "metadata");
497        }
498        
499        // Course list
500        for (CourseList childCl : course.getCourseLists())
501        {
502            saxCourseList(childCl, parentPath, defaultLocale);
503        }
504        
505        // Course parts
506        for (CoursePart coursePart : course.getCourseParts())
507        {
508            saxCoursePart(coursePart, defaultLocale);
509        }
510        
511        XMLUtils.endElement(contentHandler, "course");
512    }
513    
514    /**
515     * SAX the HTML content of a {@link Content}
516     * @param content the content
517     * @param handler the {@link ContentHandler} to send SAX events to.
518     * @param viewName the view name
519     * @throws SAXException If an error occurred saxing the content
520     * @throws IOException If an error occurred resolving the content
521     */
522    protected void saxContent(Content content, ContentHandler handler, String viewName) throws SAXException, IOException
523    {
524        String format = parameters.getParameter("output-format", "html");
525        if (StringUtils.isEmpty(format))
526        {
527            format = "html";
528        }
529        
530        saxContent(content, handler, viewName, format, true, false);
531    }
532    
533    /**
534     * SAX a {@link Content} to given format
535     * @param content the content
536     * @param handler the {@link ContentHandler} to send SAX events to.
537     * @param viewName the view name
538     * @param format the output format
539     * @param withContentRoot true to wrap content stream into a root content tag
540     * @param ignoreChildren true to not SAX sub contents
541     * @throws SAXException If an error occurred saxing the content
542     * @throws IOException If an error occurred resolving the content
543     */
544    protected void saxContent(Content content, ContentHandler handler, String viewName, String format, boolean withContentRoot, boolean ignoreChildren) throws SAXException, IOException
545    {
546        Triple<String, String, String> cacheKey = Triple.of(content.getId(), viewName, format);
547        SaxBuffer buffer = _cache.get(cacheKey);
548        
549        if (buffer != null)
550        {
551            buffer.toSAX(handler);
552            return;
553        }
554        
555        buffer = new SaxBuffer();
556
557        Request request = ObjectModelHelper.getRequest(objectModel);
558        
559        SitemapSource src = null;      
560        try
561        {
562            String uri = "cocoon://_content." + format + "?contentId=" + content.getId() + "&viewName=" + viewName + "&output-format=" + format;
563            if (ignoreChildren)
564            {
565                uri += "&ignoreChildren=true";
566            }
567            
568            if (request.getAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL) != null)
569            {
570                uri += "&versionLabel=" + CmsConstants.LIVE_LABEL;
571            }
572            
573            src = (SitemapSource) _srcResolver.resolveURI(uri);
574            
575            if (withContentRoot)
576            {
577                AttributesImpl attrs = new AttributesImpl();
578                attrs.addCDATAAttribute("id", content.getId());
579                attrs.addCDATAAttribute("name", content.getName());
580                attrs.addCDATAAttribute("title", content.getTitle());
581                attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
582                
583                XMLUtils.startElement(buffer, "content", attrs);
584            }
585            
586            src.toSAX(new IgnoreRootHandler(buffer));
587            
588            if (withContentRoot)
589            {
590                XMLUtils.endElement(buffer, "content");
591            }
592            
593            _cache.put(cacheKey, buffer);
594            buffer.toSAX(handler);
595        }
596        catch (UnknownAmetysObjectException e)
597        {
598            // The content may be archived
599        }
600        finally
601        {
602            _srcResolver.release(src);
603        }
604    }
605    
606    /**
607     * Add an attribute if its not null or empty.
608     * @param attrs The attributes
609     * @param attrName The attribute name
610     * @param attrValue The attribute value
611     */
612    protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue)
613    {
614        if (StringUtils.isNotEmpty(attrValue))
615        {
616            attrs.addCDATAAttribute(attrName, attrValue);
617        }
618    }
619    
620    /**
621     * SAX a course part
622     * @param coursePart The course part to SAX
623     * @param defaultLocale The default locale
624     * @throws SAXException if an error occurs
625     * @throws ProcessingException if an error occurs
626     */
627    protected void saxCoursePart(CoursePart coursePart, Locale defaultLocale) throws SAXException, ProcessingException
628    {
629        AttributesImpl attrs = new AttributesImpl();
630        attrs.addCDATAAttribute("id", coursePart.getId());
631        attrs.addCDATAAttribute("title", coursePart.getTitle());
632        _addAttrIfNotEmpty(attrs, "code", coursePart.getCode());
633
634        XMLUtils.startElement(contentHandler, "coursePart", attrs);
635        View view = _cTypesHelper.getView("sax", coursePart.getTypes(), coursePart.getMixinTypes());
636        coursePart.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false).withLocale(new Locale(coursePart.getLanguage())));
637        XMLUtils.endElement(contentHandler, "coursePart");
638    }
639}