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.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 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 sender = user.getEmail();
445        
446        if (StringUtils.isEmpty(sender))
447        {
448            getLogger().error("Cart's owner has no email, his ODF cart selection can not be shared");
449            result.put("success", false);
450            result.put("error", "no-owner-mail");
451            return result;
452        }
453        
454        List<ODFCartItem> items = itemIds.stream()
455                .map(i -> getCartItem(i))
456                .filter(Objects::nonNull)
457                .collect(Collectors.toList());
458        
459        Site site = _siteManager.getSite(siteName);
460        Map<String, I18nizableTextParameter> i18nparam = new HashMap<>();
461        i18nparam.put("siteTitle", new I18nizableText(site.getTitle())); // {siteTitle}
462        
463        I18nizableText i18nTextSubject = new I18nizableText("plugin.odf-web", "PLUGINS_ODFWEB_CART_SHARE_MAIL_SUBJECT", i18nparam);
464        String subject = _i18nUtils.translate(i18nTextSubject, language);
465        
466        String htmlBody = null;
467        String textBody = null;
468        try
469        {
470            
471            htmlBody = getMailBody(items, message, owner, siteName, language, false);
472            textBody = getMailBody(items, message, owner, siteName, language, true);
473        }
474        catch (IOException e)
475        {
476            getLogger().error("Fail to get mail body to share ODF cart selection", e);
477            result.put("success", false);
478            return result;
479        }
480        
481        List<String> mailsInError = new ArrayList<>();
482        
483        for (String recipient : recipients)
484        {
485            try
486            {
487                String prettyHTMLBody;
488                try
489                {
490                    prettyHTMLBody = StandardMailBodyHelper.newHTMLBody()
491                            .withLanguage(language)
492                            .withTitle(subject)
493                            .withMessage(htmlBody.replaceAll("\n", "")) // remove breaklines
494                            .withLink(site.getUrl(), new I18nizableText("plugin.odf-web", "PLUGINS_ODFWEB_CART_SHARE_MAIL_HTML_BODY_SITE_LINK"))
495                            .build();
496                }
497                catch (IOException e)
498                {
499                    getLogger().warn("Failed to build wrapped HTML body for ODF cart. Fallback to no wrapped mail", e);
500                    prettyHTMLBody = htmlBody;
501                }
502                
503                
504                SendMailHelper.newMail()
505                              .withSubject(subject)
506                              .withHTMLBody(prettyHTMLBody)
507                              .withTextBody(textBody)
508                              .withRecipient(recipient)
509                              .withSender(sender)
510                              .sendMail();
511            }
512            catch (MessagingException | IOException e)
513            {
514                getLogger().error("Failed to send ODF cart selection to '" + recipient + "'", e);
515                mailsInError.add(recipient);
516            }   
517        }
518        
519        if (mailsInError.size() > 0)
520        {
521            result.put("success", false);
522            result.put("mailsInError", mailsInError);
523        }
524        else
525        {
526            result.put("success", true);
527        }
528        
529        return result;
530    }
531    
532    /**
533     * Get the mail subject for sharing cart
534     * @param siteName The site name
535     * @param language the language
536     * @return the mail subject
537     */
538    protected String getMailSubject(String siteName, String language)
539    {
540        Site site = _siteManager.getSite(siteName);
541        Map<String, I18nizableTextParameter> i18nparam = new HashMap<>();
542        i18nparam.put("siteTitle", new I18nizableText(site.getTitle())); // {siteTitle}
543        
544        I18nizableText i18nTextSubject = new I18nizableText("plugin.odf-web", "PLUGINS_ODFWEB_CART_SHARE_MAIL_SUBJECT", i18nparam);
545        return _i18nUtils.translate(i18nTextSubject, language);
546    }
547    
548    /**
549     * Get the mail body to sharing cart
550     * @param items The cart's items
551     * @param message The message
552     * @param owner The cart's owner
553     * @param siteName The site name
554     * @param language the language
555     * @param text true to get the body to text body (html otherwise)
556     * @return the cart items to HTML format
557     * @throws IOException if failed to mail body
558     */
559    protected String getMailBody(List<ODFCartItem> items, String message, UserIdentity owner, String siteName, String language, boolean text) throws IOException
560    {
561        Request request = ContextHelper.getRequest(_context);
562        
563        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
564        RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
565        
566        Source source = null;
567        try
568        {
569            // Force live workspace and FRONT context to resolve page
570            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, WebConstants.LIVE_WORKSPACE);
571            _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
572            
573            Map<String, Object> parameters = new HashMap<>();
574            
575            parameters.put("items", items);
576            parameters.put("message", message);
577            parameters.put("owner", owner);
578            parameters.put("siteName", siteName);
579            parameters.put("lang", language); 
580            parameters.put("format", text ? "text" : "html");
581            
582            source = _srcResolver.resolveURI("cocoon://_plugins/odf-web/cart/mail/body", null, parameters);
583            
584            try (InputStream is = source.getInputStream())
585            {
586                ByteArrayOutputStream bos = new ByteArrayOutputStream();
587                SourceUtil.copy(is, bos);
588                
589                return bos.toString("UTF-8");
590            }
591        }
592        finally
593        {
594            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
595            _renderingContextHandler.setRenderingContext(currentContext);
596            
597            if (source != null)
598            {
599                _srcResolver.release(source);
600            }
601        }
602    }
603    
604    /**
605     * SAX the cart's items
606     * @param contentHandler The content handler to sax into
607     * @param owner the cart owner
608     * @param siteName the site name
609     * @throws SAXException if an error occurred while saxing
610     * @throws IOException if an I/O exception occurred
611     * @throws UserPreferencesException if failed to get cart items
612     */
613    public void saxCartItems(ContentHandler contentHandler, UserIdentity owner, String siteName) throws SAXException, IOException, UserPreferencesException
614    {
615        List<ODFCartItem> items = getCartItems(owner, siteName);
616        
617        XMLUtils.startElement(contentHandler, "items");
618        for (ODFCartItem item : items)
619        {
620            saxCartItem(contentHandler, item, siteName);
621            
622        }
623        XMLUtils.endElement(contentHandler, "items");
624        
625    }
626    
627    /**
628     * SAX a cart's item
629     * @param contentHandler The content handler to sax into
630     * @param item the cart's item
631     * @param siteName the site name
632     * @throws SAXException if an error occurred while saxing
633     * @throws IOException if an I/O exception occurred
634     */
635    public void saxCartItem(ContentHandler contentHandler, ODFCartItem item, String siteName) throws SAXException, IOException
636    {
637        AttributesImpl attrs = new AttributesImpl();
638        attrs.addCDATAAttribute("id", item.getId());
639        XMLUtils.startElement(contentHandler, "item", attrs);
640        
641        Content content = item.getContent();
642        saxTypes(contentHandler, content.getTypes());
643        saxContent(contentHandler, content, "cart");
644        saxPage(contentHandler, item, siteName);
645        
646        Program parentProgram = item.getParentProgram();
647        if (parentProgram != null)
648        {
649            attrs = new AttributesImpl();
650            attrs.addCDATAAttribute("id", parentProgram.getId());
651            attrs.addCDATAAttribute("title", parentProgram.getTitle());
652            Page parentPage = _odfPageResolver.getProgramPage(parentProgram, siteName);
653            if (parentPage != null)
654            {
655                attrs.addCDATAAttribute("pageId", parentPage.getId());
656            }
657            XMLUtils.createElement(contentHandler, "parent", attrs);
658            
659        }
660        XMLUtils.endElement(contentHandler, "item");
661    }
662    
663    /**
664     * Get the JSON representation of a cart item
665     * @param item The cart's item
666     * @param siteName The site name
667     * @param viewName The name of content view to use
668     * @return The cart items properties
669     * @throws IOException if failed to read content view
670     */
671    public Map<String, Object> cartItem2Json(ODFCartItem item, String siteName, String viewName) throws IOException
672    {
673        Map<String, Object> result = new HashMap<>();
674        
675        Content content = item.getContent();
676        
677        result.put("id", item.getId());
678        result.put("contentId", content.getId());
679        result.put("title", content.getTitle());
680        result.put("name", content.getName());
681        
682        Program parentProgram = item.getParentProgram();
683        if (parentProgram != null)
684        {
685            result.put("parentProgramId", parentProgram.getId());
686            result.put("parentProgramTitle", parentProgram.getTitle());
687        }
688        
689        Page page = getPage(item, siteName);
690        if (page != null)
691        {
692            result.put("pageId", page.getId());
693            result.put("pageTitle", page.getTitle());
694            result.put("pagePath", page.getPathInSitemap());
695        }
696        
697        String cTypeId = content.getTypes()[0];
698        ContentType cType = _cTypeEP.getExtension(cTypeId);
699        
700        result.put("contentTypeId", cTypeId);
701        result.put("contentTypeLabel", cType.getLabel());
702        
703        if (viewName != null && _cTypesHelper.getView(viewName, content.getTypes(), content.getMixinTypes()) != null)
704        {
705            String uri = _contentHelper.getContentHtmlViewUrl(content, viewName, parentProgram != null ? Map.of("parentProgramId", parentProgram.getId()) : Map.of());
706            SitemapSource src = null;
707            
708            try
709            {
710                src = (SitemapSource) _srcResolver.resolveURI(uri);
711                try (InputStream is = src.getInputStream())
712                {
713                    String view = IOUtils.toString(is, StandardCharsets.UTF_8);
714                    result.put("view", view);
715                }
716            }
717            finally
718            {
719                _srcResolver.release(src);
720            }
721        }
722        
723        if (content instanceof AbstractProgram)
724        {
725            try
726            {
727                boolean subscriber = _currentUserProvider.getUser() != null && isSubscriber(_currentUserProvider.getUser(), item.getId(), siteName);
728                result.put("subscriber", subscriber);
729            }
730            catch (UserPreferencesException e)
731            {
732                getLogger().error("Fail to check if current user subscribes to content {}. It supposes he is not.", content.getId(), e);
733                result.put("subscriber", false);
734            }
735            
736            additionalItemInfo(item, (AbstractProgram) content, result);
737        }
738        else if (content instanceof Course)
739        {
740            additionalItemInfo(item, (Course) content, result);
741        }
742        
743        return result;
744    }
745    
746    /**
747     * Get the additional information for {@link AbstractProgram} cart item
748     * @param item the odf cart item
749     * @param abstractProgram the abstract program
750     * @param infos the information to be completed
751     */
752    protected void additionalItemInfo(ODFCartItem item, AbstractProgram abstractProgram, Map<String, Object> infos)
753    {
754        // Nothing
755    }
756    
757    /**
758     * Get the additional information for {@link Course} cart item
759     * @param item the odf cart item
760     * @param course the abstract program
761     * @param infos the information to be completed
762     */
763    protected void additionalItemInfo(ODFCartItem item, Course course, Map<String, Object> infos)
764    {
765        double ects = course.getEcts();
766        if (ects > 0D)
767        {
768            infos.put("ects", ects);
769        }
770        
771        double numberOfHours = course.getNumberOfHours();
772        if (numberOfHours > 0D)
773        {
774            infos.put("nbHours", numberOfHours);
775        }
776        
777        List<CoursePart> courseParts = course.getCourseParts();
778        if (!courseParts.isEmpty())
779        {
780            List<Map<String, Object>> courseparts = new ArrayList<>();
781            for (CoursePart coursePart : course.getCourseParts())
782            {
783                Map<String, Object> coursepart = new HashMap<>();
784                coursepart.put("nature", coursePart.getNature());
785                coursepart.put("nbHours", coursePart.getNumberOfHours());
786            }
787            
788            infos.put("courseparts", courseparts);
789        }
790    }
791    
792    /**
793     * Sax the content types
794     * @param handler The content handler to sax into
795     * @param types The content types
796     * @throws SAXException if an error occurred while saxing
797     */
798    protected void saxTypes(ContentHandler handler, String[] types) throws SAXException
799    {
800        XMLUtils.startElement(handler, "types");
801        
802        for (String id : types)
803        {
804            ContentType cType = _cTypeEP.getExtension(id);
805            if (cType != null)
806            {
807                AttributesImpl attrs = new AttributesImpl();
808                attrs.addCDATAAttribute("id", cType.getId());
809                
810                XMLUtils.startElement(handler, "type", attrs);
811                cType.getLabel().toSAX(handler);
812                XMLUtils.endElement(handler, "type");
813            }
814        }
815        XMLUtils.endElement(handler, "types");
816    }
817    
818    /**
819     * SAX the content view
820     * @param handler The content handler to sax into
821     * @param content The content
822     * @param viewName The view name
823     * @throws SAXException if an error occurred while saxing
824     * @throws IOException if an I/O exception occurred
825     */
826    protected void saxContent (ContentHandler handler, Content content, String viewName) throws SAXException, IOException
827    {
828        AttributesImpl attrs = new AttributesImpl();
829        attrs.addCDATAAttribute("id", content.getId());
830        attrs.addCDATAAttribute("name", content.getName());
831        attrs.addCDATAAttribute("title", content.getTitle(null));
832        attrs.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(content.getLastModified()));
833        
834        XMLUtils.startElement(handler, "content", attrs);
835        
836        if (_cTypesHelper.getView("cart", content.getTypes(), content.getMixinTypes()) != null)
837        {
838            String uri = _contentHelper.getContentHtmlViewUrl(content, viewName);
839            SitemapSource src = null;
840            
841            try
842            {
843                src = (SitemapSource) _srcResolver.resolveURI(uri);
844                src.toSAX(new IgnoreRootHandler(handler));
845            }
846            finally
847            {
848                _srcResolver.release(src);
849            }
850        }
851        
852        XMLUtils.endElement(handler, "content");
853    }
854    
855    /**
856     * Sax the content's page
857     * @param handler The content handler to sax into
858     * @param item The cart's item
859     * @param siteName The current site name
860     * @throws SAXException if an error occurred while saxing
861     */
862    protected void saxPage(ContentHandler handler, ODFCartItem item, String siteName) throws SAXException
863    {
864        Page page = getPage(item, siteName);
865        if (page != null)
866        {
867            String pageId = page.getId();
868            
869            AttributesImpl attrs = new AttributesImpl();
870            attrs.addCDATAAttribute("id", pageId);
871            attrs.addCDATAAttribute("path", ResolveURIComponent.resolve("page", pageId));
872            XMLUtils.createElement(handler, "page", attrs, page.getTitle());
873        }
874    }
875    
876    /**
877     * Get the page associated to this cart's item
878     * @param item The item
879     * @param siteName The site name
880     * @return the page or <code>null</code> if not found
881     */
882    protected Page getPage(ODFCartItem item, String siteName)
883    {
884        Content content  = item.getContent();
885        if (content instanceof Course)
886        {
887            return _odfPageResolver.getCoursePage((Course) content, (AbstractProgram) item.getParentProgram(), siteName);
888        }
889        else if (content instanceof Program)
890        {
891            return _odfPageResolver.getProgramPage((Program) content, siteName);
892        }
893        else if (content instanceof SubProgram)
894        {
895            return _odfPageResolver.getSubProgramPage((SubProgram) content, item.getParentProgram(), siteName);
896        }
897        
898        getLogger().info("No page found of content {} in ODF cart", content.getId());
899        return null;
900    }
901    
902    class ODFCartItem
903    {
904        private Content _content;
905        private Program _parentProgram;
906        
907        public ODFCartItem(Content content)
908        {
909            this(content, null);
910        }
911        
912        public ODFCartItem(Content content, Program parentProgram)
913        {
914            _content = content;
915            _parentProgram = parentProgram;
916        }
917        
918        String getId()
919        {
920            return _content.getId() + (_parentProgram != null ? ";" + _parentProgram.getId() : "");
921        }
922        
923        Content getContent()
924        {
925            return _content;
926        }
927        
928        Program getParentProgram()
929        {
930            return _parentProgram;
931        }
932        
933        @Override
934        public int hashCode()
935        {
936            return Objects.hash(getId());
937        }
938        
939        @Override
940        public boolean equals(Object other)
941        {
942            return other != null && getClass() == other.getClass() && getId().equals(((ODFCartItem) other).getId());
943        }
944    }
945}