001/*
002 *  Copyright 2020 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.workspaces.report;
017
018import java.io.IOException;
019import java.text.SimpleDateFormat;
020import java.time.format.DateTimeFormatter;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Calendar;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.List;
027import java.util.Objects;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import javax.jcr.Node;
032import javax.jcr.NodeIterator;
033import javax.jcr.Repository;
034import javax.jcr.RepositoryException;
035import javax.jcr.Session;
036import javax.jcr.query.Query;
037
038import org.apache.avalon.framework.context.Context;
039import org.apache.avalon.framework.context.ContextException;
040import org.apache.avalon.framework.context.Contextualizable;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.cocoon.ProcessingException;
044import org.apache.cocoon.components.ContextHelper;
045import org.apache.cocoon.environment.Request;
046import org.apache.cocoon.generation.ServiceableGenerator;
047import org.apache.cocoon.xml.XMLUtils;
048import org.apache.commons.lang3.StringUtils;
049import org.xml.sax.SAXException;
050
051import org.ametys.cms.data.ContentValue;
052import org.ametys.cms.repository.Content;
053import org.ametys.cms.tag.Tag;
054import org.ametys.core.group.GroupManager;
055import org.ametys.core.user.User;
056import org.ametys.core.user.UserIdentity;
057import org.ametys.core.user.UserManager;
058import org.ametys.core.util.I18nUtils;
059import org.ametys.plugins.repository.AmetysRepositoryException;
060import org.ametys.plugins.repository.activities.ActivityHelper;
061import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
062import org.ametys.plugins.repository.provider.AbstractRepository;
063import org.ametys.plugins.repository.query.expression.Expression.Operator;
064import org.ametys.plugins.repository.query.expression.StringExpression;
065import org.ametys.plugins.userdirectory.UserDirectoryHelper;
066import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType;
067import org.ametys.plugins.workspaces.keywords.KeywordsDAO;
068import org.ametys.plugins.workspaces.members.ProjectMemberManager;
069import org.ametys.plugins.workspaces.project.ProjectManager;
070import org.ametys.plugins.workspaces.project.objects.Project;
071import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
072import org.ametys.runtime.i18n.I18nizableText;
073import org.ametys.web.repository.site.Site;
074import org.ametys.web.repository.site.SiteManager;
075import org.ametys.web.repository.sitemap.Sitemap;
076
077/**
078 * Report projects with theirs managers/users
079 */
080public class ReportGenerator extends ServiceableGenerator implements Contextualizable
081{
082    /** Time format for csv export */
083    protected static final String __DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
084    
085    /** Zoned dateTime formatter */
086    protected static final DateTimeFormatter __ZONED_DATE_FORMATER = DateTimeFormatter.ofPattern(__DATE_TIME_FORMAT);
087
088    /** DateTime formatter */
089    protected static final SimpleDateFormat __DATE_FORMATER = new SimpleDateFormat(__DATE_TIME_FORMAT);
090    
091    
092    /** number of columns for a project */
093    protected static final int __REPORT_COLUMNS_SIZE_PROJECT = 8;
094    
095    /** number of columns for a member */
096    protected static final int __REPORT_COLUMNS_SIZE_MEMBER = 8;
097    
098    /** Report Project Manager */
099    protected ReportHelper _reportProjectManager;
100    
101    /** The project manager */
102    protected ProjectManager _projectManager;
103    
104    /** Site manager */
105    protected SiteManager _siteManager;
106
107    /** Repository */
108    protected Repository _repository;
109
110    /** User Manager */
111    protected UserManager _userManager;
112    
113    /** Group Manager */
114    protected GroupManager _groupManager;
115    
116    /** Project Member Manager */
117    protected ProjectMemberManager _projectMemberManager;
118    
119    /** User Directory Helper */
120    protected UserDirectoryHelper _userDirectoryHelper;
121    
122    /** I18n Utils */
123    protected I18nUtils _i18nUtils;
124    
125    /** The keywords DAO */
126    protected KeywordsDAO _keywordsDAO;
127    
128    /** Avalon context */
129    protected Context _context;
130
131    @Override
132    public void contextualize(Context context) throws ContextException
133    {
134        _context = context;
135    }
136    
137    @Override
138    public void service(ServiceManager smanager) throws ServiceException
139    {
140        super.service(smanager);
141        _reportProjectManager = (ReportHelper) smanager.lookup(ReportHelper.ROLE);
142        _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE);
143        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
144        _repository = (Repository) smanager.lookup(AbstractRepository.ROLE);
145        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
146        _groupManager = (GroupManager) smanager.lookup(GroupManager.ROLE);
147        _projectMemberManager = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE);
148        _userDirectoryHelper = (UserDirectoryHelper) smanager.lookup(UserDirectoryHelper.ROLE);
149        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
150        _keywordsDAO = (KeywordsDAO) manager.lookup(KeywordsDAO.ROLE);
151    }
152
153    public void generate() throws IOException, SAXException, ProcessingException
154    {
155        Request request = ContextHelper.getRequest(_context);
156        
157        String lang = parameters.getParameter("lang", null);
158        String[] projectsValues = request.getParameterValues("project");
159        String[] categoriesValues = request.getParameterValues("category");
160
161        // Filters because the js may send the parameter empty (which will create a non-emply list with an empty string)
162        List<String> projects = projectsValues == null ? Collections.EMPTY_LIST : Arrays.asList(projectsValues)
163                .stream()
164                .filter(StringUtils::isNotBlank)
165                .collect(Collectors.toList());
166        
167        List<String> categories = categoriesValues == null ? Collections.EMPTY_LIST : Arrays.asList(categoriesValues)
168                .stream()
169                .filter(StringUtils::isNotBlank)
170                .collect(Collectors.toList());
171
172        boolean reportWithMembers = "true".equals(request.getParameter("with-members"));
173        boolean reportWithManagers = "true".equals(request.getParameter("with-managers"));
174        
175        contentHandler.startDocument();
176        List<Project> availableProjects = _reportProjectManager.getAvailableProjects(projects, categories);
177        if (availableProjects != null && !availableProjects.isEmpty())
178        {
179            XMLUtils.createElement(contentHandler, "text", generateProjects(availableProjects, reportWithMembers, reportWithManagers, lang));
180        }
181        else
182        {
183            XMLUtils.createElement(contentHandler, "text");
184        }
185        contentHandler.endDocument();
186    }
187
188    /**
189     * Generate the list of projects
190     * @param projects list of projects to generate
191     * @param reportWithMembers also report members
192     * @param reportWithManagers also report managers
193     * @param lang language used to parse agents (if not available, another langage will still be used)
194     * @return A String that will represent the csv
195     */
196    protected String generateProjects(List<Project> projects, boolean reportWithMembers, boolean reportWithManagers, String lang)
197    {
198        // Languages used to use one of them on each content to report
199        List<String> langages = getCatalogSiteLangages();
200        
201        StringBuilder sb = new StringBuilder();
202        sb.append("\ufeff"); // BOM character
203        sb.append("\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_TYPE", lang) + "\";");
204        sb.append(getProjectCsvHeader(reportWithMembers, reportWithManagers, lang));
205        
206        sb.append("\n");
207        
208        // categories
209        sb.append(projects.stream()
210                .map(project -> translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT", lang) + ";" + projectToCsv(project, reportWithMembers, reportWithManagers, lang, langages))
211                .collect(Collectors.joining("\n")));
212
213        return sb.toString();
214    }
215    
216    /**
217     * Get the languages of the catalog site
218     * @return the list of catalog site languages
219     */
220    protected List<String> getCatalogSiteLangages()
221    {
222        String catalogSiteName = _projectManager.getCatalogSiteName();
223        if (StringUtils.isNotEmpty(catalogSiteName))
224        {
225            Site siteCatalog = _siteManager.getSite(catalogSiteName);
226            if (siteCatalog != null)
227            {
228                return siteCatalog.getSitemaps().stream()
229                    .map(Sitemap::getSitemapName)
230                    .collect(Collectors.toList());
231            }
232        }
233        return null;
234    }
235    
236    /**
237     * Format a String to csv
238     * @param line strings to transform
239     * @return a string representing the input strings
240     */
241    protected static String formatToCsv(String... line)
242    {
243        return Arrays.stream(line).collect(Collectors.joining("\";\"", "\"", "\""));
244    }
245    
246    /**
247     * Generate the header for projects
248     * @param reportWithMembers also report members
249     * @param reportWithManagers also report managers
250     * @param lang language to use to create header
251     * @return a String representing the csv header
252     */
253    protected String getProjectCsvHeader(boolean reportWithMembers, boolean reportWithManagers, String lang)
254    {
255        StringBuilder sb = new StringBuilder(formatToCsv(
256                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_NAME", lang),
257                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_DESC", lang),
258                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_CATEGORY", lang),
259                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_CREATION_DATE", lang),
260                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_ACTIVITY_DATE", lang),
261                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_KEYWONDS", lang),
262                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MANAGERS", lang),
263                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_VISIBILITY", lang)));
264        
265        if (reportWithMembers || reportWithManagers)
266        {
267            sb.append(";");
268            sb.append(getMemberCsvHeader(lang));
269        }
270        
271        
272        return sb.toString();
273    }
274    
275    private String getMemberCsvHeader(String lang)
276    {
277        return formatToCsv(
278                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_TITLE", lang),
279                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_FIRSTNAME", lang),
280                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_LASTNAME", lang),
281                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_FUNCTION", lang),
282                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_ORGANISATION", lang),
283                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_PHONE", lang),
284                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_MAIL", lang),
285                translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_SKILLS", lang));
286    }
287    
288    private String projectToCsv(Project project, boolean reportWithMembers, boolean reportWithManagers, String lang, List<String> langages)
289    {
290        Set<String> projectCategories = project.getCategories();
291        String mainCategory = projectCategories.size() > 0 ? projectCategories.iterator().next() : "";
292                
293        String lastActivityDate = getProjectLastActivityDate(project);
294        
295        String projectmanager = Arrays.stream(project.getManagers())
296            .map(_userManager::getUser)
297            .filter(Objects::nonNull)
298            .map(user -> user.getFullName() + " (" + user.getIdentity().getPopulationId() + ")")
299            .collect(Collectors.joining(","));
300        
301        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
302        String inscriptionStatusString = InscriptionStatus.PRIVATE.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_PRIVATE", lang)
303                : InscriptionStatus.MODERATED.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_MODERATED", lang)
304                        : InscriptionStatus.OPEN.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_OPEN", lang) : "";
305        
306        
307        StringBuilder sb = new StringBuilder(formatToCsv(
308                project.getTitle(),
309                StringUtils.defaultString(project.getDescription(), "").replaceAll("[\r\n]+", " / "),
310                mainCategory,
311                __ZONED_DATE_FORMATER.format(project.getCreationDate()),
312                lastActivityDate,
313                getKeywords(project, lang),
314                projectmanager,
315                inscriptionStatusString
316            ));
317        
318        int projectPadding = (reportWithMembers || reportWithManagers) ? __REPORT_COLUMNS_SIZE_MEMBER : 0;
319        
320        sb.append(StringUtils.repeat(";-", projectPadding));
321        
322        if (reportWithManagers)
323        {
324            // project managers
325            String managerPrefix = "\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MANAGER", lang) + "\";\"" + project.getTitle() + "\"" + StringUtils.repeat(";-", __REPORT_COLUMNS_SIZE_PROJECT - 1) + ";";
326                    
327            String managersCsv = Arrays.stream(project.getManagers())
328                .map(userIdentity -> createProjectMemberFromUser(userIdentity, lang,  langages))
329                .filter(Objects::nonNull)
330                .map(ProjectMember::toCsv)
331                .map(managerCsv -> managerPrefix + managerCsv)
332                .collect(Collectors.joining("\n"));
333            if (StringUtils.isNotEmpty(managersCsv))
334            {
335                sb.append("\n");
336                sb.append(managersCsv);
337            }
338        }
339        
340        if (reportWithMembers)
341        {
342            // project members
343            String memberPrefix = "\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MEMBER", lang) + "\";\"" + project.getTitle() + "\"" + StringUtils.repeat(";-", __REPORT_COLUMNS_SIZE_PROJECT - 1) + ";";
344            
345            String projectMembersCsv = getProjectMembersCsv(project, lang, langages).stream()
346                    .map(memberCsv -> memberPrefix + memberCsv)
347                    .collect(Collectors.joining("\n"));
348            
349            if (StringUtils.isNotEmpty(projectMembersCsv))
350            {
351                sb.append("\n");
352                sb.append(projectMembersCsv);
353            }
354        }
355        
356
357        
358        return sb.toString();
359    }
360    
361    /**
362     * Get the project's keywords
363     * @param project the project
364     * @param lang the current language
365     * @return the keywords as label
366     */
367    protected String getKeywords(Project project, String lang)
368    {
369        List<String> keywordLabels = new ArrayList<>();
370        
371        for (String keyword : project.getKeywords())
372        {
373            Tag tag = _keywordsDAO.getTag(keyword, Collections.emptyMap());
374            if (tag != null)
375            {
376                String title = _i18nUtils.translate(tag.getTitle(), lang);
377                keywordLabels.add(title);
378            }
379        }
380        
381        return StringUtils.join(keywordLabels, ",");
382    }
383    
384    /**
385     * Get the last activity date of the project
386     * @param project project to analyze
387     * @return a formatted date
388     */
389    protected String getProjectLastActivityDate(Project project)
390    {
391        StringExpression projectExpression = new StringExpression(AbstractWorkspacesActivityType.PROJECT_NAME, Operator.EQ, project.getName());
392        String xpathQuery = ActivityHelper.getActivityXPathQuery(projectExpression);
393        
394        Session session = null;
395        try
396        {
397            session  = _repository.login();
398            @SuppressWarnings("deprecation")
399            Query query = session.getWorkspace().getQueryManager().createQuery(xpathQuery, Query.XPATH);
400            NodeIterator nodes = query.execute().getNodes();
401            if (nodes.hasNext())
402            {
403                Node node = (Node) nodes.next();
404                Calendar date = node.getProperty("ametys:date").getValue().getDate();
405                return __DATE_FORMATER.format(date.getTime());
406            }
407        }
408        catch (RepositoryException ex)
409        {
410            throw new AmetysRepositoryException("An error occured executing the JCR query : " + xpathQuery, ex);
411        }
412        finally
413        {
414            if (session != null)
415            {
416                session.logout();
417            }
418        }
419        
420        return "";
421    }
422    
423    /**
424     * Get a {@link ProjectMember} from a user
425     * @param userIdentity user
426     * @param lang language to use to parse user
427     * @param projectLanguages languages of the catalog site
428     * @return the user as a {@link ProjectMember}
429     */
430    protected ProjectMember createProjectMemberFromUser(UserIdentity userIdentity, String lang, List<String> projectLanguages)
431    {
432        User user = _userManager.getUser(userIdentity);
433        if (user == null)
434        {
435            return null;
436        }
437        Content userContent = null;
438        if (StringUtils.isNotEmpty(lang))
439        {
440            userContent = _userDirectoryHelper.getUserContent(userIdentity, lang);
441        }
442        
443        // If requested language is not available, find one that is available
444        if (userContent == null)
445        {
446            userContent = projectLanguages != null ? projectLanguages.stream()
447                .map(l -> _userDirectoryHelper.getUserContent(userIdentity, l))
448                .filter(Objects::nonNull)
449                .findFirst()
450                .orElse(null) : null;
451        }
452        
453        if (userContent == null)
454        {
455            return new ProjectMember(UserIdentity.userIdentityToString(userIdentity),
456                    "", 
457                    user.getFirstName(), 
458                    user.getLastName(), 
459                    "",
460                    "", 
461                    "", 
462                    StringUtils.defaultIfEmpty(user.getEmail(), ""), 
463                    "");
464        }
465        
466        ModelAwareDataHolder dataHolder = userContent.getDataHolder();
467        ContentValue[] skillsContents = dataHolder.getValue("skills");
468        String skills = skillsContents != null ? Arrays.stream(skillsContents)
469                .filter(Objects::nonNull)
470                .map(ContentValue::getContent)
471                .filter(Objects::nonNull)
472                .map(skill -> (String) skill.getDataHolder().getValue("title"))
473                .collect(Collectors.joining(","))
474                : "";
475                
476        
477        return new ProjectMember(UserIdentity.userIdentityToString(userIdentity),
478                StringUtils.defaultIfEmpty(dataHolder.getValue("title"), ""), 
479                user.getFirstName(), 
480                user.getLastName(), 
481                StringUtils.defaultIfEmpty(dataHolder.getValue("function"), ""),
482                StringUtils.defaultIfEmpty(dataHolder.getValue("organisation"), ""), 
483                StringUtils.defaultIfEmpty(dataHolder.getValue("phone"), ""), 
484                StringUtils.defaultIfEmpty(user.getEmail(), ""), 
485                skills);
486    }
487    
488    /**
489     * Get the csv for members
490     * @param project project to use
491     * @param lang language to use to parse user
492     * @param languages languages of the catalog site
493     * @return users as csv
494     */
495    protected List<String> getProjectMembersCsv(Project project, String lang, List<String> languages)
496    {
497        
498        
499        return _projectMemberManager.getProjectMembers(project, true)
500                .stream()
501                .map(member -> createProjectMemberFromUser(member.getUser().getIdentity(), lang, languages))
502                .sorted(Comparator.comparing(ProjectMember::getSortableName))
503                .map(ProjectMember::toCsv)
504                .collect(Collectors.toList());
505    }
506        
507    /**
508     * Small shortcut to translate a key
509     * @param key i18n key
510     * @param lang requested language
511     * @return the translated key
512     */
513    protected String translateKey(String key, String lang)
514    {
515        I18nizableText i18n = new I18nizableText("plugin.workspaces", key);
516        return _i18nUtils.translate(i18n, lang);
517    }
518    
519    static class ProjectMember
520    {
521        private String _id;
522        private String _title;
523        private String _firstname;
524        private String _lastname;
525        private String _role;
526        private String _structure;
527        private String _telephone;
528        private String _email;
529        private String _skills;
530        
531        ProjectMember(String id, String title, String firstname, String lastname, String role, String structure, String telephone, String email, String skills)
532        {
533            _id = id;
534            _title = title;
535            _firstname = firstname;
536            _lastname = lastname;
537            _role = role;
538            _structure = structure;
539            _telephone = telephone;
540            _email = email;
541            _skills = skills;
542        }
543        
544        public String getId()
545        {
546            return _id;
547        }
548        
549        public String getSortableName()
550        {
551            return _lastname + " " + _firstname;
552        }
553        
554        @Override
555        public boolean equals(Object obj)
556        {
557            return _id != null && obj instanceof ProjectMember && _id.equals(((ProjectMember) obj).getId());
558        }
559        
560        /**
561         * Format a {@link ProjectMember} to a csv line
562         * @return a String representing a {@link ProjectMember} in a csv format
563         */
564        public String toCsv()
565        {
566            return formatToCsv(_title,
567                _firstname,
568                _lastname,
569                _role,
570                _structure,
571                _telephone,
572                _email,
573                _skills);
574        }
575
576        @Override
577        public int hashCode()
578        {
579            return _id.hashCode();
580        }
581    }
582}