001/*
002 *  Copyright 2019 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.plugins.odfweb.cart;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.nio.charset.StandardCharsets;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.stream.Collectors;
030
031import javax.mail.MessagingException;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.context.Context;
035import org.apache.avalon.framework.context.ContextException;
036import org.apache.avalon.framework.context.Contextualizable;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.cocoon.components.ContextHelper;
041import org.apache.cocoon.components.source.impl.SitemapSource;
042import org.apache.cocoon.environment.Request;
043import org.apache.cocoon.xml.AttributesImpl;
044import org.apache.cocoon.xml.XMLUtils;
045import org.apache.commons.io.IOUtils;
046import org.apache.commons.lang.StringUtils;
047import org.apache.excalibur.source.Source;
048import org.apache.excalibur.source.SourceResolver;
049import org.apache.excalibur.source.SourceUtil;
050import org.xml.sax.ContentHandler;
051import org.xml.sax.SAXException;
052
053import org.ametys.cms.contenttype.ContentType;
054import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
055import org.ametys.cms.contenttype.ContentTypesHelper;
056import org.ametys.cms.repository.Content;
057import org.ametys.cms.transformation.xslt.ResolveURIComponent;
058import org.ametys.core.user.CurrentUserProvider;
059import org.ametys.core.user.User;
060import org.ametys.core.user.UserIdentity;
061import org.ametys.core.userpref.UserPreferencesException;
062import org.ametys.core.userpref.UserPreferencesManager;
063import org.ametys.core.util.DateUtils;
064import org.ametys.core.util.I18nUtils;
065import org.ametys.core.util.IgnoreRootHandler;
066import org.ametys.core.util.mail.SendMailHelper;
067import org.ametys.odf.course.Course;
068import org.ametys.odf.coursepart.CoursePart;
069import org.ametys.odf.program.AbstractProgram;
070import org.ametys.odf.program.Program;
071import org.ametys.odf.program.SubProgram;
072import org.ametys.plugins.core.user.UserHelper;
073import org.ametys.plugins.odfweb.repository.OdfPageResolver;
074import org.ametys.plugins.repository.AmetysObjectResolver;
075import org.ametys.plugins.repository.UnknownAmetysObjectException;
076import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
077import org.ametys.runtime.i18n.I18nizableText;
078import org.ametys.runtime.plugin.component.AbstractLogEnabled;
079import org.ametys.web.WebConstants;
080import org.ametys.web.renderingcontext.RenderingContext;
081import org.ametys.web.renderingcontext.RenderingContextHandler;
082import org.ametys.web.repository.page.Page;
083import org.ametys.web.repository.site.Site;
084import org.ametys.web.repository.site.SiteManager;
085import org.ametys.web.userpref.FOUserPreferencesConstants;
086
087/**
088 * Component to handle ODF cart items
089 *
090 */
091public class ODFCartManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
092{
093    /** The avalon role */
094    public static final String ROLE = ODFCartManager.class.getName();
095    
096    /** The id of user preference for cart's elements */
097    public static final String CART_USER_PREF_CONTENT_IDS = "cartOdfContentIds";
098    
099    /** The id of user preference for subscription */
100    public static final String SUBSCRIPTION_USER_PREF_CONTENT_IDS = "subscriptionOdfContentIds";
101    
102    private UserPreferencesManager _userPrefManager;
103    private AmetysObjectResolver _resolver;
104    private SourceResolver _srcResolver;
105    private OdfPageResolver _odfPageResolver;
106    private ContentTypeExtensionPoint _cTypeEP;
107    private ContentTypesHelper _cTypesHelper;
108    private ODFCartUserPreferencesStorage _odfUserPrefStorage;
109    private I18nUtils _i18nUtils;
110    private UserHelper _userHelper;
111    private SiteManager _siteManager;
112    private RenderingContextHandler _renderingContextHandler;
113    private CurrentUserProvider _currentUserProvider;
114
115    private Context _context;
116
117
118
119
120    @Override
121    public void service(ServiceManager serviceManager) throws ServiceException
122    {
123        _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE + ".FO");
124        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
125        _srcResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
126        _odfPageResolver = (OdfPageResolver) serviceManager.lookup(OdfPageResolver.ROLE);
127        _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
128        _cTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
129        _odfUserPrefStorage = (ODFCartUserPreferencesStorage) serviceManager.lookup(ODFCartUserPreferencesStorage.ROLE);
130        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
131        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
132        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
133        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
134        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
135    }
136    
137    public void contextualize(Context context) throws ContextException
138    {
139        _context = context;
140    }
141    
142    /**
143     * Get the id of ODF's cart items for a given user
144     * @param user the user
145     * @param siteName the current site name
146     * @return the list of contents' id
147     * @throws UserPreferencesException if failed to get cart items
148     */
149    public List<String> getCartItemIds(UserIdentity user, String siteName) throws UserPreferencesException
150    {
151        return getCartItemIds(user, siteName, CART_USER_PREF_CONTENT_IDS);
152    }
153    
154    /**
155     * Get the id of ODF's cart items for which a given user is a subscriber
156     * @param user the user
157     * @param siteName the current site name
158     * @return the list of contents' id
159     * @throws UserPreferencesException if failed to get cart items
160     */
161    public List<String> getItemIdsWithSubscription(UserIdentity user, String siteName) throws UserPreferencesException
162    {
163        return getCartItemIds(user, siteName, SUBSCRIPTION_USER_PREF_CONTENT_IDS);
164    }
165    
166    /**
167     * Get the id of items for a given user
168     * @param user the user
169     * @param siteName the current site name
170     * @param userPrefsId The id of user preferences
171     * @return the list of contents' id
172     * @throws UserPreferencesException if failed to get cart items
173     */
174    protected List<String> getCartItemIds(UserIdentity user, String siteName, String userPrefsId) throws UserPreferencesException
175    {
176        Map<String, String> contextVars = new HashMap<>();
177        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
178        
179        String contentIdsAsStr = _userPrefManager.getUserPreferenceAsString(user, "/sites/" + siteName, contextVars, userPrefsId);
180        if (StringUtils.isNotBlank(contentIdsAsStr))
181        {
182            return Arrays.asList(StringUtils.split(contentIdsAsStr, ","));
183        }
184        
185        return Collections.emptyList();
186    }
187    
188    /**
189     * Get the ODF's cart items for a given user
190     * @param owner the user
191     * @param siteName the current site name
192     * @return the list of contents
193     * @throws UserPreferencesException if failed to get cart items
194     */
195    public List<ODFCartItem> getCartItems(UserIdentity owner, String siteName) throws UserPreferencesException
196    {
197        List<ODFCartItem> items = new ArrayList<>();
198        
199        List<String> itemIds = getCartItemIds(owner, siteName);
200        for (String itemId : itemIds)
201        {
202            ODFCartItem item = getCartItem(itemId);
203            if (item != null)
204            {
205                items.add(item);
206            }
207            else
208            {
209                getLogger().warn("The item with id '{}' stored in cart of user {} does not match an existing content anymore. It will be ignored", itemId, owner);
210            }
211        }
212        
213        return items;
214    }
215    
216    /**
217     * Get the ODF's cart items for which the given user is a subscriber
218     * @param owner the user
219     * @param siteName the current site name
220     * @return the list of contents
221     * @throws UserPreferencesException if failed to get subscriptions
222     */
223    public List<ODFCartItem> getItemsWithSubscription(UserIdentity owner, String siteName) throws UserPreferencesException
224    {
225        List<ODFCartItem> items = new ArrayList<>();
226        
227        List<String> itemIds = getItemIdsWithSubscription(owner, siteName);
228        for (String itemId : itemIds)
229        {
230            ODFCartItem item = getCartItem(itemId);
231            if (item != null)
232            {
233                items.add(item);
234            }
235            else
236            {
237                getLogger().warn("The item with id '{}' stored in subscription of user {} does not match an existing content anymore. It will be ignored", itemId, owner);
238            }
239        }
240        
241        return items;
242    }
243    
244    /**
245     * Get a cart item from its id
246     * @param itemId the item's id
247     * @return the cart item or null if no content was found
248     */
249    public ODFCartItem getCartItem(String itemId)
250    {
251        int i = itemId.indexOf(';');
252        
253        String contentId = itemId;
254        String parentId = null;
255        
256        if (i != -1)
257        {
258            contentId = itemId.substring(0, i);
259            parentId = itemId.substring(i + 1);
260        }
261        
262        try
263        {
264            return new ODFCartItem(_resolver.resolveById(contentId), parentId != null ? _resolver.resolveById(parentId) : null);
265        }
266        catch (UnknownAmetysObjectException e)
267        {
268            return null;
269        }
270    }
271    
272    /**
273     * Set the cart's items
274     * @param owner The cart owner
275     * @param itemIds the id of items to set in the cart
276     * @param siteName the site name
277     * @throws UserPreferencesException if failed to save cart
278     */
279    public void setCartItems(UserIdentity owner, List<String> itemIds, String siteName) throws UserPreferencesException
280    {
281        saveItemsInUserPreference(owner, itemIds, siteName, CART_USER_PREF_CONTENT_IDS);
282    }
283    
284    
285    /**
286     * Subscribe to a list of items. All subscriptions are replaced by the given items.
287     * @param owner The cart owner
288     * @param itemIds the id of items to subscribe to.
289     * @param siteName the site name
290     * @throws UserPreferencesException if failed to save subscriptions
291     */
292    public void setSubscription(UserIdentity owner, List<String> itemIds, String siteName) throws UserPreferencesException
293    {
294        saveItemsInUserPreference(owner, itemIds, siteName, SUBSCRIPTION_USER_PREF_CONTENT_IDS);
295    }
296    
297    /**
298     * Save the given items into the given user preference
299     * @param owner the user
300     * @param itemIds the id of items 
301     * @param siteName the site name
302     * @param userPrefsId the id of user preference
303     * @throws UserPreferencesException if failed to save user preference
304     */
305    public void saveItemsInUserPreference(UserIdentity owner, List<String> itemIds, String siteName, String userPrefsId) throws UserPreferencesException
306    {
307        Map<String, String> contextVars = new HashMap<>();
308        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
309        
310        Map<String, String> preferences = new HashMap<>();
311        preferences.put(userPrefsId, StringUtils.join(itemIds, ","));
312        
313        _odfUserPrefStorage.setUserPreferences(owner, "/sites/" + siteName, contextVars, preferences);
314    }
315    
316    /**
317     * Add a content to the cart
318     * @param owner the cart owner
319     * @param itemId the id of content to add into the cart
320     * @param siteName the site name 
321     * @return true if the content was successfully added
322     * @throws UserPreferencesException if failed to save cart
323     */
324    public boolean addCartItem(UserIdentity owner, String itemId, String siteName) throws UserPreferencesException
325    {
326        List<ODFCartItem> items = getCartItems(owner, siteName);
327        
328        ODFCartItem item = getCartItem(itemId);
329        if (item != null)
330        {
331            items.add(item);
332        }
333        else
334        {
335            getLogger().warn("Unknown item with id {}. It cannot be added to user cart", itemId);
336            return false;
337        }
338        
339        List<String> itemIds = items.stream()
340                .map(c -> c.getId())
341                .collect(Collectors.toList());
342        
343        setCartItems(owner, itemIds, siteName);
344        
345        return true;
346    }
347    
348    /**
349     * determines if the user subscribes to this item
350     * @param owner the user
351     * @param itemId the id of content
352     * @param siteName the site name 
353     * @return true if the user subscribes to this item, false otherwise
354     * @throws UserPreferencesException if failed to check subscription
355     */
356    public boolean isSubscriber(UserIdentity owner, String itemId, String siteName) throws UserPreferencesException
357    {
358        List<String> subscriptions = getItemIdsWithSubscription(owner, siteName);
359        return subscriptions.contains(itemId);
360    }
361    
362    /**
363     * Subscribe to a content
364     * @param owner the cart owner 
365     * @param itemId the id of content
366     * @param siteName the site name 
367     * @return if the content was successfuly added to subscriptions
368     * @throws UserPreferencesException if failed to subscribe to content
369     */
370    public boolean subscribe(UserIdentity owner, String itemId, String siteName) throws UserPreferencesException
371    {
372        List<ODFCartItem> items = getItemsWithSubscription(owner, siteName);
373        
374        ODFCartItem item = getCartItem(itemId);
375        if (item != null)
376        {
377            items.add(item);
378        }
379        else
380        {
381            getLogger().warn("Unknown item with id {}. It cannot be added to user subscriptions", itemId);
382            return false;
383        }
384        
385        List<String> itemIds = items.stream()
386                .map(c -> c.getId())
387                .collect(Collectors.toList());
388        
389        setSubscription(owner, itemIds, siteName);
390        
391        return true;
392    }
393    
394    /**
395     * Unsubscribe to a content
396     * @param owner the cart owner 
397     * @param itemId the id of content
398     * @param siteName the site name 
399     * @return if the content was successfuly added to subscriptions
400     * @throws UserPreferencesException if failed to subscribe to content
401     */
402    public boolean unsubscribe(UserIdentity owner, String itemId, String siteName) throws UserPreferencesException
403    {
404        List<ODFCartItem> items = getItemsWithSubscription(owner, siteName);
405        
406        ODFCartItem item = getCartItem(itemId);
407        if (item != null)
408        {
409            items.remove(item);
410        }
411        else
412        {
413            getLogger().warn("Unknown item with id {}. It cannot be remove from user subscriptions", itemId);
414            return false;
415        }
416        
417        List<String> itemIds = items.stream()
418                .map(c -> c.getId())
419                .collect(Collectors.toList());
420        
421        setSubscription(owner, itemIds, siteName);
422        
423        return true;
424    }
425    
426    /**
427     * Share the cart's items by mail
428     * @param owner The cart owner
429     * @param itemIds the id of contents to set in the cart
430     * @param recipients the mails to share with
431     * @param siteName the site name
432     * @param language the language
433     * @param message the message to add to selection
434     * @return the results
435     */
436    public Map<String, Object> shareCartItems(UserIdentity owner, List<String> itemIds, List<String> recipients, String siteName, String language, String message)
437    {
438        Map<String, Object> result = new HashMap<>();
439        
440        User user = _userHelper.getUser(owner);
441        String sender = user.getEmail();
442        
443        if (StringUtils.isEmpty(sender))
444        {
445            getLogger().error("Cart's owner has no email, his ODF cart selection can not be shared");
446            result.put("success", false);
447            result.put("error", "no-owner-mail");
448            return result;
449        }
450        
451        List<ODFCartItem> items = itemIds.stream()
452                .map(i -> getCartItem(i))
453                .filter(Objects::nonNull)
454                .collect(Collectors.toList());
455        
456        Site site = _siteManager.getSite(siteName);
457        Map<String, I18nizableText> i18nparam = new HashMap<>();
458        i18nparam.put("siteTitle", new I18nizableText(site.getTitle())); // {siteTitle}
459        
460        I18nizableText i18nTextSubject = new I18nizableText("plugin.odf-web", "PLUGINS_ODFWEB_CART_SHARE_MAIL_SUBJECT", i18nparam);
461        String subject = _i18nUtils.translate(i18nTextSubject);
462        
463        String htmlBody = null;
464        String textBody = null;
465        try
466        {
467            
468            htmlBody = getMailBody(items, message, owner, siteName, language, false);
469            textBody = getMailBody(items, message, owner, siteName, language, true);
470        }
471        catch (IOException e)
472        {
473            getLogger().error("Fail to get mail body to share ODF cart selection", e);
474            result.put("success", false);
475            return result;
476        }
477        
478        List<String> mailsInError = new ArrayList<>();
479        
480        for (String recipient : recipients)
481        {
482            try
483            {
484                SendMailHelper.sendMail(subject, htmlBody, textBody, Collections.singletonList(recipient), sender);
485            }
486            catch (MessagingException e)
487            {
488                getLogger().error("Failed to send ODF cart selection to '" + recipient + "'", e);
489                mailsInError.add(recipient);
490            }   
491        }
492        
493        if (mailsInError.size() > 0)
494        {
495            result.put("success", false);
496            result.put("mailsInError", mailsInError);
497        }
498        else
499        {
500            result.put("success", true);
501        }
502        
503        return result;
504    }
505    
506    /**
507     * Get the mail subject for sharing cart
508     * @param siteName The site name
509     * @param language the language
510     * @return the mail subject
511     */
512    protected String getMailSubject(String siteName, String language)
513    {
514        Site site = _siteManager.getSite(siteName);
515        Map<String, I18nizableText> i18nparam = new HashMap<>();
516        i18nparam.put("siteTitle", new I18nizableText(site.getTitle())); // {siteTitle}
517        
518        I18nizableText i18nTextSubject = new I18nizableText("plugin.odf-web", "PLUGINS_ODFWEB_CART_SHARE_MAIL_SUBJECT", i18nparam);
519        return _i18nUtils.translate(i18nTextSubject, language);
520    }
521    
522    /**
523     * Get the mail body to sharing cart
524     * @param items The cart's items
525     * @param message The message
526     * @param owner The cart's owner
527     * @param siteName The site name
528     * @param language the language
529     * @param text true to get the body to text body (html otherwise)
530     * @return the cart items to HTML format
531     * @throws IOException if failed to mail body
532     */
533    protected String getMailBody(List<ODFCartItem> items, String message, UserIdentity owner, String siteName, String language, boolean text) throws IOException
534    {
535        Request request = ContextHelper.getRequest(_context);
536        
537        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
538        RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
539        
540        Source source = null;
541        try
542        {
543            // Force live workspace and FRONT context to resolve page
544            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, WebConstants.LIVE_WORKSPACE);
545            _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
546            
547            Map<String, Object> parameters = new HashMap<>();
548            
549            parameters.put("items", items);
550            parameters.put("message", message);
551            parameters.put("owner", owner);
552            parameters.put("siteName", siteName);
553            parameters.put("lang", language); 
554            parameters.put("format", text ? "text" : "html");
555            
556            source = _srcResolver.resolveURI("cocoon://_plugins/odf-web/cart/mail/body", null, parameters);
557            
558            try (InputStream is = source.getInputStream())
559            {
560                ByteArrayOutputStream bos = new ByteArrayOutputStream();
561                SourceUtil.copy(is, bos);
562                
563                return bos.toString("UTF-8");
564            }
565        }
566        finally
567        {
568            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
569            _renderingContextHandler.setRenderingContext(currentContext);
570            
571            if (source != null)
572            {
573                _srcResolver.release(source);
574            }
575        }
576    }
577    
578    /**
579     * SAX the cart's items
580     * @param contentHandler The content handler to sax into
581     * @param owner the cart owner
582     * @param siteName the site name
583     * @throws SAXException if an error occurred while saxing
584     * @throws IOException if an I/O exception occurred
585     * @throws UserPreferencesException if failed to get cart items
586     */
587    public void saxCartItems(ContentHandler contentHandler, UserIdentity owner, String siteName) throws SAXException, IOException, UserPreferencesException
588    {
589        List<ODFCartItem> items = getCartItems(owner, siteName);
590        
591        XMLUtils.startElement(contentHandler, "items");
592        for (ODFCartItem item : items)
593        {
594            saxCartItem(contentHandler, item, siteName);
595            
596        }
597        XMLUtils.endElement(contentHandler, "items");
598        
599    }
600    
601    /**
602     * SAX a cart's item
603     * @param contentHandler The content handler to sax into
604     * @param item the cart's item
605     * @param siteName the site name
606     * @throws SAXException if an error occurred while saxing
607     * @throws IOException if an I/O exception occurred
608     */
609    public void saxCartItem(ContentHandler contentHandler, ODFCartItem item, String siteName) throws SAXException, IOException
610    {
611        AttributesImpl attrs = new AttributesImpl();
612        attrs.addCDATAAttribute("id", item.getId());
613        XMLUtils.startElement(contentHandler, "item", attrs);
614        
615        Content content = item.getContent();
616        saxTypes(contentHandler, content.getTypes());
617        saxContent(contentHandler, content, "cart");
618        saxPage(contentHandler, item, siteName);
619        
620        Program parentProgram = item.getParentProgram();
621        if (parentProgram != null)
622        {
623            attrs = new AttributesImpl();
624            attrs.addCDATAAttribute("id", parentProgram.getId());
625            attrs.addCDATAAttribute("title", parentProgram.getTitle());
626            Page parentPage = _odfPageResolver.getProgramPage(parentProgram, siteName);
627            if (parentPage != null)
628            {
629                attrs.addCDATAAttribute("pageId", parentPage.getId());
630            }
631            XMLUtils.createElement(contentHandler, "parent", attrs);
632            
633        }
634        XMLUtils.endElement(contentHandler, "item");
635    }
636    
637    /**
638     * Get the JSON representation of a cart item
639     * @param item The cart's item
640     * @param siteName The site name
641     * @param viewName The name of content view to use
642     * @return The cart items properties
643     * @throws IOException if failed to read content view
644     */
645    public Map<String, Object> cartItem2Json(ODFCartItem item, String siteName, String viewName) throws IOException
646    {
647        Map<String, Object> result = new HashMap<>();
648        
649        Content content = item.getContent();
650        
651        result.put("id", item.getId());
652        result.put("contentId", content.getId());
653        result.put("title", content.getTitle());
654        result.put("name", content.getName());
655        
656        Program parentProgram = item.getParentProgram();
657        if (parentProgram != null)
658        {
659            result.put("parentProgramId", parentProgram.getId());
660            result.put("parentProgramTitle", parentProgram.getTitle());
661        }
662        
663        Page page = getPage(item, siteName);
664        if (page != null)
665        {
666            result.put("pageId", page.getId());
667            result.put("pageTitle", page.getTitle());
668            result.put("pagePath", page.getPathInSitemap());
669        }
670        
671        String cTypeId = content.getTypes()[0];
672        ContentType cType = _cTypeEP.getExtension(cTypeId);
673        
674        result.put("contentTypeId", cTypeId);
675        result.put("contentTypeLabel", cType.getLabel());
676        
677        if (viewName != null && _cTypesHelper.getView(viewName, content.getTypes(), content.getMixinTypes()) != null)
678        {
679            String uri = "cocoon://_content.html?contentId=" + content.getId() + "&metadataSetName=" + viewName + (parentProgram != null ? "&parentProgramId=" + parentProgram.getId() : "");
680            SitemapSource src = null;
681            
682            try
683            {
684                src = (SitemapSource) _srcResolver.resolveURI(uri);
685                try (InputStream is = src.getInputStream())
686                {
687                    String view = IOUtils.toString(is, StandardCharsets.UTF_8);
688                    result.put("view", view);
689                }
690            }
691            finally
692            {
693                _srcResolver.release(src);
694            }
695        }
696        
697        if (content instanceof AbstractProgram)
698        {
699            try
700            {
701                boolean subscriber = _currentUserProvider.getUser() != null && isSubscriber(_currentUserProvider.getUser(), item.getId(), siteName);
702                result.put("subscriber", subscriber);
703            }
704            catch (UserPreferencesException e)
705            {
706                getLogger().error("Fail to check if current user subscribes to content {}. It supposes he is not.", content.getId(), e);
707                result.put("subscriber", false);
708            }
709            
710            additionalItemInfo(item, (AbstractProgram) content, result);
711        }
712        else if (content instanceof Course)
713        {
714            additionalItemInfo(item, (Course) content, result);
715        }
716        
717        return result;
718    }
719    
720    /**
721     * Get the additional information for {@link AbstractProgram} cart item
722     * @param item the odf cart item
723     * @param abstractProgram the abstract program
724     * @param infos the information to be completed
725     */
726    protected void additionalItemInfo(ODFCartItem item, AbstractProgram abstractProgram, Map<String, Object> infos)
727    {
728        // Nothing
729    }
730    
731    /**
732     * Get the additional information for {@link Course} cart item
733     * @param item the odf cart item
734     * @param course the abstract program
735     * @param infos the information to be completed
736     */
737    protected void additionalItemInfo(ODFCartItem item, Course course, Map<String, Object> infos)
738    {
739        double ects = course.getEcts();
740        if (ects > 0D)
741        {
742            infos.put("ects", ects);
743        }
744        
745        double numberOfHours = course.getNumberOfHours();
746        if (numberOfHours > 0D)
747        {
748            infos.put("nbHours", numberOfHours);
749        }
750        
751        List<CoursePart> courseParts = course.getCourseParts();
752        if (!courseParts.isEmpty())
753        {
754            List<Map<String, Object>> courseparts = new ArrayList<>();
755            for (CoursePart coursePart : course.getCourseParts())
756            {
757                Map<String, Object> coursepart = new HashMap<>();
758                coursepart.put("nature", coursePart.getNature());
759                coursepart.put("nbHours", coursePart.getNumberOfHours());
760            }
761            
762            infos.put("courseparts", courseparts);
763        }
764    }
765    
766    /**
767     * Sax the content types
768     * @param handler The content handler to sax into
769     * @param types The content types
770     * @throws SAXException if an error occurred while saxing
771     */
772    protected void saxTypes(ContentHandler handler, String[] types) throws SAXException
773    {
774        XMLUtils.startElement(handler, "types");
775        
776        for (String id : types)
777        {
778            ContentType cType = _cTypeEP.getExtension(id);
779            if (cType != null)
780            {
781                AttributesImpl attrs = new AttributesImpl();
782                attrs.addCDATAAttribute("id", cType.getId());
783                
784                XMLUtils.startElement(handler, "type", attrs);
785                cType.getLabel().toSAX(handler);
786                XMLUtils.endElement(handler, "type");
787            }
788        }
789        XMLUtils.endElement(handler, "types");
790    }
791    
792    /**
793     * SAX the content view
794     * @param handler The content handler to sax into
795     * @param content The content
796     * @param viewName The view name
797     * @throws SAXException if an error occurred while saxing
798     * @throws IOException if an I/O exception occurred
799     */
800    protected void saxContent (ContentHandler handler, Content content, String viewName) throws SAXException, IOException
801    {
802        AttributesImpl attrs = new AttributesImpl();
803        attrs.addCDATAAttribute("id", content.getId());
804        attrs.addCDATAAttribute("name", content.getName());
805        attrs.addCDATAAttribute("title", content.getTitle(null));
806        attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
807        
808        XMLUtils.startElement(handler, "content", attrs);
809        
810        if (_cTypesHelper.getView("cart", content.getTypes(), content.getMixinTypes()) != null)
811        {
812            String uri = "cocoon://_content.html?contentId=" + content.getId() + "&metadataSetName=" + viewName;
813            SitemapSource src = null;
814            
815            try
816            {
817                src = (SitemapSource) _srcResolver.resolveURI(uri);
818                src.toSAX(new IgnoreRootHandler(handler));
819            }
820            finally
821            {
822                _srcResolver.release(src);
823            }
824        }
825        
826        XMLUtils.endElement(handler, "content");
827    }
828    
829    /**
830     * Sax the content's page
831     * @param handler The content handler to sax into
832     * @param item The cart's item
833     * @param siteName The current site name
834     * @throws SAXException if an error occurred while saxing
835     */
836    protected void saxPage(ContentHandler handler, ODFCartItem item, String siteName) throws SAXException
837    {
838        Page page = getPage(item, siteName);
839        if (page != null)
840        {
841            String pageId = page.getId();
842            
843            AttributesImpl attrs = new AttributesImpl();
844            attrs.addCDATAAttribute("id", pageId);
845            attrs.addCDATAAttribute("path", ResolveURIComponent.resolve("page", pageId));
846            XMLUtils.createElement(handler, "page", attrs, page.getTitle());
847        }
848    }
849    
850    /**
851     * Get the page associated to this cart's item
852     * @param item The item
853     * @param siteName The site name
854     * @return the page or <code>null</code> if not found
855     */
856    protected Page getPage(ODFCartItem item, String siteName)
857    {
858        Content content  = item.getContent();
859        if (content instanceof Course)
860        {
861            return _odfPageResolver.getCoursePage((Course) content, (AbstractProgram) item.getParentProgram(), siteName);
862        }
863        else if (content instanceof Program)
864        {
865            return _odfPageResolver.getProgramPage((Program) content, siteName);
866        }
867        else if (content instanceof SubProgram)
868        {
869            return _odfPageResolver.getSubProgramPage((SubProgram) content, item.getParentProgram(), siteName);
870        }
871        
872        getLogger().info("No page found of content {} in ODF cart", content.getId());
873        return null;
874    }
875    
876    class ODFCartItem
877    {
878        private Content _content;
879        private Program _parentProgram;
880        
881        public ODFCartItem(Content content)
882        {
883            this(content, null);
884        }
885        
886        public ODFCartItem(Content content, Program parentProgram)
887        {
888            _content = content;
889            _parentProgram = parentProgram;
890        }
891        
892        String getId()
893        {
894            return _content.getId() + (_parentProgram != null ? ";" + _parentProgram.getId() : "");
895        }
896        
897        Content getContent()
898        {
899            return _content;
900        }
901        
902        Program getParentProgram()
903        {
904            return _parentProgram;
905        }
906        
907        @Override
908        public int hashCode()
909        {
910            return Objects.hash(getId());
911        }
912        
913        @Override
914        public boolean equals(Object other)
915        {
916            return other != null && getClass() == other.getClass() && getId().equals(((ODFCartItem) other).getId());
917        }
918    }
919}