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                // Child containers, subprograms or course lists
156                saxChildProgramPart(program, defaultLocale);
157                
158                // OrgUnits
159                saxOrgUnits(program);
160                
161                // Translations
162                saxTranslation(program);
163            }
164        }
165        
166        Request request = ObjectModelHelper.getRequest(objectModel);
167        request.setAttribute(Content.class.getName(), content);
168    }
169    
170    /**
171     * SAX the referenced {@link Person}s
172     * @param content The content to analyze
173     * @throws SAXException if an error occurs while saxing
174     */
175    protected void saxPersons(Content content) throws SAXException
176    {
177        saxLinkedContents(content, "persons", PersonFactory.PERSON_CONTENT_TYPE, "abstract");
178    }
179    
180    /**
181     * SAX the referenced {@link OrgUnit}s
182     * @param content The content to analyze
183     * @throws SAXException if an error occurs while saxing
184     */
185    protected void saxOrgUnits(Content content) throws SAXException
186    {
187        saxLinkedContents(content, "orgunits", OrgUnitFactory.ORGUNIT_CONTENT_TYPE, "link");
188    }
189    
190    /**
191     * SAX the referenced {@link ProgramPart}s
192     * @param program The program or subprogram
193     * @param defaultLocale The default locale
194     * @throws SAXException if an error occurs while saxing
195     * @throws IOException if an error occurs
196     * @throws ProcessingException if an error occurs
197     */
198    protected void saxChildProgramPart(AbstractProgram program, Locale defaultLocale) throws SAXException, ProcessingException, IOException
199    {
200        ContentValue[] children = program.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]);
201        for (ContentValue child : children)
202        {
203            try
204            {
205                Content content = child.getContent();
206                _odfHelper.switchToLiveVersionIfNeeded((DefaultAmetysObject) content);
207                
208                if (content instanceof SubProgram)
209                {
210                    saxSubProgram((SubProgram) content, program.getContextPath());
211                }
212                else if (content instanceof Container)
213                {
214                    saxContainer ((Container) content, program.getContextPath(), defaultLocale);
215                }
216                else if (content instanceof CourseList)
217                {
218                    saxCourseList((CourseList) content, program.getContextPath(), defaultLocale);
219                }
220            }
221            catch (UnknownAmetysObjectException | NoLiveVersionException e)
222            {
223                // The content can reference a non-existing child: ignore the exception.
224                // Or the content has no live version: ignore too
225            }
226        }
227    }
228    
229    /**
230     * SAX the existing translation
231     * @param content The content
232     * @throws SAXException if an error occurs while saxing
233     */
234    protected void saxTranslation(Content content) throws SAXException
235    {
236        Map<String, String> translations = TranslationHelper.getTranslations(content);
237        if (!translations.isEmpty())
238        {
239            saxTranslations(translations);
240        }
241    }
242    
243    /**
244     * SAX the referenced content types.
245     * @param content The content to analyze
246     * @param tagName The root tagName
247     * @param linkedContentType The content type to search
248     * @param viewName The view to parse the found contents
249     * @throws SAXException if an error occurs while saxing
250     */
251    protected void saxLinkedContents(Content content, String tagName, String linkedContentType, String viewName) throws SAXException
252    {
253        Set<String> contentIds = getLinkedContents(content, linkedContentType);
254        
255        XMLUtils.startElement(contentHandler, tagName);
256        for (String id : contentIds)
257        {
258            if (StringUtils.isNotEmpty(id))
259            {
260                try
261                {
262                    Content subContent = _resolver.resolveById(id);
263                    saxContent(subContent, contentHandler, viewName);
264                }
265                catch (IOException e)
266                {
267                    throw new SAXException(e);
268                }
269                catch (UnknownAmetysObjectException e)
270                {
271                    // The program can reference a non-existing person: ignore the exception.
272                }
273            }
274        }
275        
276        XMLUtils.endElement(contentHandler, tagName);
277    }
278    
279    /**
280     * Get the linked contents of the defined content type.
281     * @param content The content to analyze
282     * @param linkedContentType The content type to search
283     * @return A {@link Set} of content ids
284     */
285    protected Set<String> getLinkedContents(Content content, String linkedContentType)
286    {
287        Set<String> contentIds = new LinkedHashSet<>();
288        
289        for (String cTypeId : content.getTypes())
290        {
291            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
292            contentIds.addAll(_getContentIds(content, cType, linkedContentType));
293        }
294        
295        return contentIds;
296    }
297    
298    private Set<String> _getContentIds(ModelAwareDataHolder dataHolder, ModelItemContainer modelItemContainer, String contentType)
299    {
300        Set<String> contentIds = new LinkedHashSet<>();
301        
302        for (ModelItem modelItem : modelItemContainer.getModelItems())
303        {
304            if (modelItem instanceof ContentAttributeDefinition)
305            {
306                if (contentType.equals(((ContentAttributeDefinition) modelItem).getContentTypeId()))
307                {
308                    String modelItemName = modelItem.getName();
309                    if (dataHolder.isMultiple(modelItemName))
310                    {
311                        List<String> values = ContentDataHelper.getContentIdsListFromMultipleContentData(dataHolder, modelItemName);
312                        contentIds.addAll(values);
313                    }
314                    else
315                    {
316                        contentIds.add(ContentDataHelper.getContentIdFromContentData(dataHolder, modelItemName));
317                    }
318                }
319            }
320            else if (modelItem instanceof ModelItemContainer)
321            {
322
323                if (dataHolder.hasValue(modelItem.getName()))
324                {
325                    if (modelItem instanceof RepeaterDefinition)
326                    {
327                        ModelAwareRepeater repeater = dataHolder.getRepeater(modelItem.getName());
328                        for (ModelAwareRepeaterEntry entry : repeater.getEntries())
329                        {
330                            contentIds.addAll(_getContentIds(entry, (ModelItemContainer) modelItem, contentType));
331                        }
332                    }
333                    else
334                    {
335                        ModelAwareComposite composite = dataHolder.getComposite(modelItem.getName());
336                        contentIds.addAll(_getContentIds(composite, (ModelItemContainer) modelItem, contentType));
337                    }
338                }
339            }
340        }
341        
342        return contentIds;
343    }
344    
345    /**
346     * SAX a container
347     * @param container the container to SAX
348     * @param parentPath the parent path
349     * @param defaultLocale The default locale
350     * @throws SAXException if an error occurs
351     * @throws IOException if an error occurs
352     * @throws ProcessingException if an error occurs
353     */
354    protected void saxContainer(Container container, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException, IOException
355    {
356        AttributesImpl attrs = new AttributesImpl();
357        attrs.addCDATAAttribute("title", container.getTitle());
358        attrs.addCDATAAttribute("id", container.getId());
359        _addAttrIfNotEmpty(attrs, "code", container.getCode());
360        _addAttrIfNotEmpty(attrs, "nature", container.getNature());
361        double ects = container.getEcts();
362        if (ects > 0)
363        {
364            attrs.addCDATAAttribute("ects", String.valueOf(ects));
365        }
366        
367        XMLUtils.startElement(contentHandler, "container", attrs);
368        
369        // Children
370        ContentValue[] children = container.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]);
371        for (ContentValue child : children)
372        {
373            try
374            {
375                Content content = child.getContent();
376                if (content instanceof SubProgram)
377                {
378                    saxSubProgram((SubProgram) content, parentPath);
379                }
380                else if (content instanceof Container)
381                {
382                    saxContainer ((Container) content, parentPath, defaultLocale);
383                }
384                else if (content instanceof CourseList)
385                {
386                    saxCourseList((CourseList) content, parentPath, defaultLocale);
387                }
388            }
389            catch (UnknownAmetysObjectException e)
390            {
391                // The content can reference a non-existing child (in live for example): ignore the exception.
392            }
393        }
394        
395        XMLUtils.endElement(contentHandler, "container");
396    }
397    
398    /**
399     * SAX a sub program
400     * @param subProgram the sub program to SAX
401     * @param parentPath the parent path
402     * @throws SAXException if an error occurs
403     */
404    protected void saxSubProgram(SubProgram subProgram, String parentPath) throws SAXException
405    {
406        AttributesImpl attrs = new AttributesImpl();
407        attrs.addCDATAAttribute("title", subProgram.getTitle());
408        attrs.addCDATAAttribute("id", subProgram.getId());
409        _addAttrIfNotEmpty(attrs, "code", subProgram.getCode());
410        _addAttrIfNotEmpty(attrs, "ects", subProgram.getEcts());
411        
412        if (parentPath != null)
413        {
414            attrs.addCDATAAttribute("path", parentPath + "/" + subProgram.getName());
415        }
416        XMLUtils.startElement(contentHandler, "subprogram", attrs);
417        
418        try
419        {
420            // SAX the "parcours" view of a subprogram
421            saxContent(subProgram, contentHandler, "parcours", "xml", false, true);
422        }
423        catch (IOException e)
424        {
425            throw new SAXException(e);
426        }
427        
428        XMLUtils.endElement(contentHandler, "subprogram");
429    }
430    
431    /**
432     * SAX a course list
433     * @param courseList The course list to SAX
434     * @param parentPath the parent path
435     * @param defaultLocale The default locale
436     * @throws SAXException if an error occurs
437     * @throws ProcessingException if an error occurs
438     */
439    protected void saxCourseList(CourseList courseList, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException
440    {
441        AttributesImpl attrs = new AttributesImpl();
442        attrs.addCDATAAttribute("title", courseList.getTitle());
443        attrs.addCDATAAttribute("id", courseList.getId());
444        _addAttrIfNotEmpty(attrs, "code", courseList.getCode());
445        
446        ChoiceType type = courseList.getType();
447        if (type != null)
448        {
449            attrs.addCDATAAttribute("type", type.toString());
450        }
451        
452        if (ChoiceType.CHOICE.equals(type))
453        {
454            attrs.addCDATAAttribute("min", String.valueOf(courseList.getMinNumberOfCourses()));
455            attrs.addCDATAAttribute("max", String.valueOf(courseList.getMaxNumberOfCourses()));
456        }
457        
458        XMLUtils.startElement(contentHandler, "courseList", attrs);
459        
460        for (Course course : courseList.getCourses())
461        {
462            try
463            {
464                saxCourse(course, parentPath, defaultLocale);
465            }
466            catch (UnknownAmetysObjectException e)
467            {
468                // The content can reference a non-existing course (in live for example): ignore the exception.
469            }
470        }
471        XMLUtils.endElement(contentHandler, "courseList");
472    }
473    
474    /**
475     * SAX a course
476     * @param course the course to SAX
477     * @param parentPath the parent path
478     * @param defaultLocale The default locale
479     * @throws SAXException if an error occurs
480     * @throws ProcessingException if an error occurs
481     */
482    protected void saxCourse(Course course, String parentPath, Locale defaultLocale) throws SAXException, ProcessingException
483    {
484        AttributesImpl attrs = new AttributesImpl();
485        attrs.addCDATAAttribute("title", course.getTitle());
486        attrs.addCDATAAttribute("code", course.getCode());
487        attrs.addCDATAAttribute("id", course.getId());
488        attrs.addCDATAAttribute("name", course.getName());
489        
490        if (parentPath != null)
491        {
492            attrs.addCDATAAttribute("path", parentPath + "/" + course.getName() + "-" + course.getCode());
493        }
494        
495        View view = _cTypesHelper.getView("courseList", course.getTypes(), course.getMixinTypes());
496        
497        XMLUtils.startElement(contentHandler, "course", attrs);
498        
499        if (view != null)
500        {
501            XMLUtils.startElement(contentHandler, "metadata");
502            course.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false));
503            XMLUtils.endElement(contentHandler, "metadata");
504        }
505        
506        // Course list
507        for (CourseList childCl : course.getCourseLists())
508        {
509            saxCourseList(childCl, parentPath, defaultLocale);
510        }
511        
512        // Course parts
513        for (CoursePart coursePart : course.getCourseParts())
514        {
515            saxCoursePart(coursePart, defaultLocale);
516        }
517        
518        XMLUtils.endElement(contentHandler, "course");
519    }
520    
521    /**
522     * SAX the HTML content of a {@link Content}
523     * @param content the content
524     * @param handler the {@link ContentHandler} to send SAX events to.
525     * @param viewName the view name
526     * @throws SAXException If an error occurred saxing the content
527     * @throws IOException If an error occurred resolving the content
528     */
529    protected void saxContent(Content content, ContentHandler handler, String viewName) throws SAXException, IOException
530    {
531        String format = parameters.getParameter("output-format", "html");
532        if (StringUtils.isEmpty(format))
533        {
534            format = "html";
535        }
536        
537        saxContent(content, handler, viewName, format, true, false);
538    }
539    
540    /**
541     * SAX a {@link Content} to given format
542     * @param content the content
543     * @param handler the {@link ContentHandler} to send SAX events to.
544     * @param viewName the view name
545     * @param format the output format
546     * @param withContentRoot true to wrap content stream into a root content tag
547     * @param ignoreChildren true to not SAX sub contents
548     * @throws SAXException If an error occurred saxing the content
549     * @throws IOException If an error occurred resolving the content
550     */
551    protected void saxContent(Content content, ContentHandler handler, String viewName, String format, boolean withContentRoot, boolean ignoreChildren) throws SAXException, IOException
552    {
553        Triple<String, String, String> cacheKey = Triple.of(content.getId(), viewName, format);
554        SaxBuffer buffer = _cache.get(cacheKey);
555        
556        if (buffer != null)
557        {
558            buffer.toSAX(handler);
559            return;
560        }
561        
562        buffer = new SaxBuffer();
563
564        Request request = ObjectModelHelper.getRequest(objectModel);
565        
566        SitemapSource src = null;
567        try
568        {
569            Map<String, String> params = new HashMap<>();
570            if (ignoreChildren)
571            {
572                params.put("ignoreChildren", "true");
573            }
574            
575            if (request.getAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL) != null)
576            {
577                params.put("versionLabel", CmsConstants.LIVE_LABEL);
578            }
579            
580            String uri = _contentHelper.getContentHtmlViewUrl(content, viewName, params);
581            src = (SitemapSource) _srcResolver.resolveURI(uri);
582            
583            if (withContentRoot)
584            {
585                AttributesImpl attrs = new AttributesImpl();
586                attrs.addCDATAAttribute("id", content.getId());
587                attrs.addCDATAAttribute("name", content.getName());
588                attrs.addCDATAAttribute("title", content.getTitle());
589                attrs.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(content.getLastModified()));
590                
591                XMLUtils.startElement(buffer, "content", attrs);
592            }
593            
594            src.toSAX(new IgnoreRootHandler(buffer));
595            
596            if (withContentRoot)
597            {
598                XMLUtils.endElement(buffer, "content");
599            }
600            
601            _cache.put(cacheKey, buffer);
602            buffer.toSAX(handler);
603        }
604        catch (UnknownAmetysObjectException e)
605        {
606            // The content may be archived
607        }
608        finally
609        {
610            _srcResolver.release(src);
611        }
612    }
613    
614    /**
615     * Add an attribute if its not null or empty.
616     * @param attrs The attributes
617     * @param attrName The attribute name
618     * @param attrValue The attribute value
619     */
620    protected void _addAttrIfNotEmpty(AttributesImpl attrs, String attrName, String attrValue)
621    {
622        if (StringUtils.isNotEmpty(attrValue))
623        {
624            attrs.addCDATAAttribute(attrName, attrValue);
625        }
626    }
627    
628    /**
629     * SAX a course part
630     * @param coursePart The course part to SAX
631     * @param defaultLocale The default locale
632     * @throws SAXException if an error occurs
633     * @throws ProcessingException if an error occurs
634     */
635    protected void saxCoursePart(CoursePart coursePart, Locale defaultLocale) throws SAXException, ProcessingException
636    {
637        AttributesImpl attrs = new AttributesImpl();
638        attrs.addCDATAAttribute("id", coursePart.getId());
639        attrs.addCDATAAttribute("title", coursePart.getTitle());
640        _addAttrIfNotEmpty(attrs, "code", coursePart.getCode());
641
642        XMLUtils.startElement(contentHandler, "coursePart", attrs);
643        View view = _cTypesHelper.getView("sax", coursePart.getTypes(), coursePart.getMixinTypes());
644        coursePart.dataToSAX(contentHandler, view, DataContext.newInstance().withEmptyValues(false).withLocale(LocaleUtils.toLocale(coursePart.getLanguage())));
645        XMLUtils.endElement(contentHandler, "coursePart");
646    }
647}