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