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