001/*
002 *  Copyright 2025 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.linkdirectory.tracking;
017
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.avalon.framework.service.ServiceException;
023import org.apache.avalon.framework.service.ServiceManager;
024import org.apache.avalon.framework.service.Serviceable;
025import org.apache.commons.lang3.StringUtils;
026
027import org.ametys.cms.tag.Tag;
028import org.ametys.core.userpref.UserPreferencesException;
029import org.ametys.core.userpref.UserPreferencesManager;
030import org.ametys.core.util.I18nUtils;
031import org.ametys.plugins.linkdirectory.DirectoryHelper;
032import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper;
033import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper.ConfiguredThemesInputData;
034import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper.ThemesInputData;
035import org.ametys.plugins.linkdirectory.theme.ThemesDAO;
036import org.ametys.plugins.repository.AmetysObjectIterable;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
039import org.ametys.plugins.webanalytics.matomo.tracking.AbstractMatomoEventTrackingProvider;
040import org.ametys.web.repository.page.ModifiableZoneItem;
041import org.ametys.web.repository.site.Site;
042import org.ametys.web.repository.sitemap.Sitemap;
043import org.ametys.web.userpref.FOUserPreferencesConstants;
044
045/**
046 * The Matomo tracking provider for the link directory
047 */
048public class LinkDirectoryMatomoTrackingProvider extends AbstractMatomoEventTrackingProvider implements Serviceable
049{
050    /** The user preferences manager. */
051    protected UserPreferencesManager _userPreferencesManager;
052    
053    /** The link directory themes input data helper */
054    protected LinkDirectoryThemesInputDataHelper _linkDirectoryThemesInputDataHelper;
055    
056    /** Themes DAO */
057    protected ThemesDAO _themesDAO;
058    
059    /** The i18n utils */
060    protected I18nUtils _i18nUtils;
061    
062    /** The ametys object resolver */
063    protected AmetysObjectResolver _resolver;
064    
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE + ".FO");
068        _linkDirectoryThemesInputDataHelper = (LinkDirectoryThemesInputDataHelper) manager.lookup(LinkDirectoryThemesInputDataHelper.ROLE);
069        _themesDAO = (ThemesDAO) manager.lookup(ThemesDAO.ROLE);
070        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
071        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
072    }
073    
074    @Override
075    protected List<MatomoEvent> getEvents(Site site) 
076    {
077        List<MatomoEvent> requests = new ArrayList<>();
078        
079        AmetysObjectIterable<Sitemap> sitemaps = site.getSitemaps();
080        for (Sitemap sitemap : sitemaps)
081        {
082            requests.addAll(_getConfigurableLinkServiceEvents(site, sitemap));
083            requests.addAll(_getConfigurableLinkInputDataEvents(site, sitemap));
084        }
085        
086        return requests;
087    }
088    
089    private List<MatomoEvent> _getConfigurableLinkServiceEvents(Site site, Sitemap sitemap)
090    {
091        List<MatomoEvent> requests = new ArrayList<>();
092
093        String siteName = site.getName();
094        String language = sitemap.getSitemapName();
095        
096        for (ModifiableZoneItem zoneItem : _getLinkDirectoryServiceZoneItems(siteName))
097        {
098            ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
099            String[] themeIds = serviceParameters.getValue("themes");
100            for (String themeId : themeIds)
101            {
102                try
103                {
104                    String themeLabel = _getThemeLabel(themeId, siteName, language);
105                    if (StringUtils.isNotBlank(themeLabel))
106                    {
107                        requests.add(new MatomoEvent(
108                            language + "/" + themeLabel, 
109                            _getNbUsersWithCustomLinks(siteName, language, zoneItem.getId())
110                        ));
111                    }
112                }
113                catch (UserPreferencesException e)
114                {
115                    getLogger().error("An error occurred getting user preferences for link directories. No request is sent to Matomo.", e);
116                }
117            }
118            
119        }
120
121        return requests;
122    }
123    
124    private AmetysObjectIterable<ModifiableZoneItem> _getLinkDirectoryServiceZoneItems(String siteName)
125    {
126        String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.plugins.linkdirectory.DirectoryService' and ametys:service_parameters/@ametys:configurable = 'true']";
127        return _resolver.query(xpathQuery);
128    }
129
130    private List<MatomoEvent>  _getConfigurableLinkInputDataEvents(Site site, Sitemap sitemap)
131    {
132        List<MatomoEvent> requests = new ArrayList<>();
133
134        String siteName = site.getName();
135        String language = sitemap.getSitemapName();
136
137        ConfiguredThemesInputData configuredThemes = _linkDirectoryThemesInputDataHelper.getThemesInputData(site.getSkinId());
138        String error = configuredThemes.error();
139        if (StringUtils.isBlank(error) && configuredThemes.themesInputDatas() != null)
140        {
141            for (ThemesInputData themesInputData : configuredThemes.themesInputDatas())
142            {
143                try
144                {
145                    if (themesInputData.configurable()) // Send only configurable themes
146                    {
147                        String themeId = themesInputData.themes()
148                                .stream()
149                                .filter(t -> t.get("lang") == null || t.get("lang").equals(language))
150                                .map(t -> t.get("id"))
151                                .findFirst()
152                                .orElse(null);
153                        
154                        String themeLabel = _getThemeLabel(themeId, siteName, language);
155                        if (StringUtils.isNotBlank(themeLabel))
156                        {
157                            requests.add(new MatomoEvent(
158                                language + "/" + themeLabel, 
159                                _getNbUsersWithCustomLinks(siteName, language, themesInputData.id())
160                            ));
161                        }
162                    }
163                }
164                catch (UserPreferencesException e)
165                {
166                    getLogger().error("An error occurred getting user preferences for link directories. No request is sent to Matomo.", e);
167                }
168            }
169        }
170        
171        return requests;
172    }
173    
174    private String _getThemeLabel(String themeId, String siteName, String language)
175    {
176        Tag theme = StringUtils.isNotBlank(themeId)
177                ? _themesDAO.getTag(themeId, Map.of("language", language, "siteName", siteName))  
178                : null;
179        
180        return theme != null ? _i18nUtils.translate(theme.getTitle(), language) : null;
181    }
182    
183    private long _getNbUsersWithCustomLinks(String siteName, String language, String thematicId) throws UserPreferencesException
184    {
185        String storageContext = siteName + "/" + language + "/" + thematicId;
186        Map<String, String> contextVars = Map.of(
187            FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName,
188            FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language
189        );
190        
191        return _userPreferencesManager.getAllUnTypedUserPrefs(storageContext, contextVars)
192                .values()
193                .stream()
194                .filter(this::_hasCustomLinks)
195                .count();
196    }
197    
198    private boolean _hasCustomLinks(Map<String, String> userPrefs)
199    {
200        String orderedLinksAsString = userPrefs.get(DirectoryHelper.USER_PREF_ORDERED_LINK_ATTR);
201        String[] orderedLinks = StringUtils.split(orderedLinksAsString, ",");
202        
203        String hiddenLinksAsString = userPrefs.get(DirectoryHelper.USER_PREF_HIDDEN_LINK_ATTR);
204        String[] hiddenLinks = StringUtils.split(hiddenLinksAsString, ",");
205        
206        // Links are customized if there is some ordered links or some hidden links in the users preferences
207        return orderedLinks != null && orderedLinks.length > 0 || hiddenLinks != null && hiddenLinks.length > 0;
208    }
209}