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