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