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