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