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