001/*
002 *  Copyright 2016 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.members;
017
018import java.util.ArrayList;
019import java.util.Date;
020import java.util.GregorianCalendar;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Set;
026
027import javax.jcr.Node;
028import javax.jcr.NodeIterator;
029import javax.jcr.PathNotFoundException;
030import javax.jcr.Repository;
031import javax.jcr.RepositoryException;
032import javax.jcr.Session;
033import javax.jcr.Value;
034import javax.jcr.ValueFormatException;
035import javax.jcr.query.Query;
036
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.commons.lang.IllegalClassException;
040import org.apache.commons.lang.StringUtils;
041
042import org.ametys.core.user.UserIdentity;
043import org.ametys.plugins.explorer.ExplorerNode;
044import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
045import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection;
046import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
047import org.ametys.plugins.repository.AmetysRepositoryException;
048import org.ametys.plugins.repository.RepositoryConstants;
049import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
050import org.ametys.plugins.repository.jcr.NodeTypeHelper;
051import org.ametys.plugins.repository.provider.AbstractRepository;
052import org.ametys.plugins.repository.query.SortCriteria;
053import org.ametys.plugins.repository.query.expression.Expression;
054import org.ametys.plugins.repository.query.expression.Expression.Operator;
055import org.ametys.plugins.repository.query.expression.StringExpression;
056import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
057import org.ametys.plugins.workspaces.ObservationConstants;
058import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
059import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
060import org.ametys.plugins.workspaces.project.objects.Project;
061import org.ametys.runtime.i18n.I18nizableText;
062import org.ametys.web.repository.page.ModifiablePage;
063import org.ametys.web.repository.page.ModifiableZone;
064import org.ametys.web.repository.page.ModifiableZoneItem;
065import org.ametys.web.repository.page.ZoneItem.ZoneType;
066
067import com.google.common.collect.ImmutableSet;
068
069/**
070 * Helper component for managing members
071 */
072public class MembersWorkspaceModule extends AbstractWorkspaceModule
073{
074    /** The id of members module */
075    public static final String MEMBERS_MODULE_ID = MembersWorkspaceModule.class.getName();
076    
077    /** Id of service of members */
078    public static final String MEMBERS_SERVICE_ID = "org.ametys.plugins.workspaces.module.Members";
079    
080    /** Workspaces members node name */
081    private static final String __WORKSPACES_MEMBERS_NODE_NAME = "members";
082    
083    private static final String __INVITATIONS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":invitations";
084    
085    private static final String __INVITATION_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":invitation";
086    
087    private static final String __NODETYPE_INVITATIONS = RepositoryConstants.NAMESPACE_PREFIX + ":invitations";
088    
089    private static final String __NODETYPE_INVITATION = RepositoryConstants.NAMESPACE_PREFIX + ":invitation";
090
091    /** Constants for invitation's date property */
092    private static final String __INVITATION_DATE = RepositoryConstants.NAMESPACE_PREFIX + ":date";
093    /** Constants for invitation's mail property */
094    private static final String __INVITATION_MAIL = RepositoryConstants.NAMESPACE_PREFIX + ":mail";
095    /** Constants for invitation's author property */
096    private static final String __INVITATION_AUTHOR = RepositoryConstants.NAMESPACE_PREFIX + ":author";
097    /** Constants for invitation's acl node */
098    private static final String __INVITATION_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":temp-acl";
099
100    private WorkspaceModuleExtensionPoint _moduleEP;
101    
102    private Repository _repository;
103    
104    @Override
105    public void service(ServiceManager smanager) throws ServiceException
106    {
107        super.service(smanager);
108        _moduleEP = (WorkspaceModuleExtensionPoint) smanager.lookup(WorkspaceModuleExtensionPoint.ROLE);
109        _repository = (Repository) smanager.lookup(AbstractRepository.ROLE);
110    }
111    
112    @Override
113    public String getId()
114    {
115        return MEMBERS_MODULE_ID;
116    }
117    
118    public int getOrder()
119    {
120        return ORDER_MEMBERS;
121    }
122    
123    public String getModuleName()
124    {
125        return __WORKSPACES_MEMBERS_NODE_NAME;
126    }
127    
128    @Override
129    protected String getModulePageName()
130    {
131        return "members";
132    }
133    
134    public I18nizableText getModuleTitle()
135    {
136        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MEMBERS_LABEL");
137    }
138    public I18nizableText getModuleDescription()
139    {
140        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_MEMBERS_DESCRIPTION");
141    }
142    @Override
143    protected I18nizableText getModulePageTitle()
144    {
145        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_MEMBERS_TITLE");
146    }
147    
148    @Override
149    protected void initializeModulePage(ModifiablePage memberPage)
150    {
151        ModifiableZone defaultZone = memberPage.createZone("default");
152        
153        ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
154        defaultZoneItem.setType(ZoneType.SERVICE);
155        defaultZoneItem.setServiceId(MEMBERS_SERVICE_ID);
156        
157        ModifiableModelAwareDataHolder params = defaultZoneItem.getServiceParameters();
158        
159        params.setValue("header", _i18nUtils.translate(getModulePageTitle(), memberPage.getSitemapName()));
160        // params.setValue("expandGroup", false); FIXME new service
161        // params.setValue("nbMembers", -1); FIXME new service
162        params.setValue("xslt", _getDefaultXslt(MEMBERS_SERVICE_ID));
163    }
164    
165    @Override
166    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
167    {
168        try
169        {
170            ExplorerNode projectRootNode = project.getExplorerRootNode();
171            
172            if (projectRootNode instanceof ModifiableResourceCollection)
173            {
174                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
175                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_MEMBERS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
176            }
177            else
178            {
179                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
180            }
181        }
182        catch (AmetysRepositoryException e)
183        {
184            throw new AmetysRepositoryException("Error getting the members root node.", e);
185        }
186    }
187    
188    @Override
189    public Set<String> getAllowedEventTypes()
190    {
191        return ImmutableSet.of(ObservationConstants.EVENT_MEMBER_ADDED);
192    }
193    
194    // -------------------------------------------------
195    //                  INVITATIONS
196    // -------------------------------------------------
197    
198    /**
199     * Get the node holding the invitations
200     * @param project The project
201     * @param create true to create the node if it does not exist
202     * @return the invitations' root node
203     * @throws RepositoryException if an error occurred
204     */
205    public Node getInvitationsRootNode(Project project, boolean create) throws RepositoryException
206    {
207        ModifiableResourceCollection moduleRoot = getModuleRoot(project, create);
208        
209        Node moduleNode = ((JCRResourcesCollection) moduleRoot).getNode();
210        Node node = null;
211        
212        if (moduleNode.hasNode(__INVITATIONS_NODE_NAME))
213        {
214            node = moduleNode.getNode(__INVITATIONS_NODE_NAME);
215        }
216        else if (create)
217        {
218            node = moduleNode.addNode(__INVITATIONS_NODE_NAME, __NODETYPE_INVITATIONS);
219            moduleNode.getSession().save();
220        }
221        
222        return node;
223    }
224    
225    /**
226     * Get the invitations for a given mail
227     * @param email the email
228     * @return the invitations
229     */
230    public List<Invitation> getInvitations(String email)
231    {
232        Session session = null;
233        try
234        {
235            session = _repository.login();
236            
237            Expression expr = new StringExpression("mail", Operator.EQ, email);
238            String xPathQuery = getInvitationXPathQuery(null, expr, null);
239            
240            @SuppressWarnings("deprecation")
241            Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
242            NodeIterator nodes = query.execute().getNodes();
243     
244            List<Invitation> invitations = new ArrayList<>();
245            
246            while (nodes.hasNext())
247            {
248                Node node = (Node) nodes.next();
249                invitations.add(_getInvitation(node));
250            }
251            
252            return invitations;
253        }
254        catch (RepositoryException ex)
255        {
256            if (session != null)
257            {
258                session.logout();
259            }
260
261            throw new AmetysRepositoryException("An error occurred executing the JCR query to get invitations for email " + email, ex);
262        }
263    }
264    
265    /**
266     * Returns the invitation sorted by ascending date
267     * @param project The project
268     * @return The invitations node
269     * @throws RepositoryException if an error occurred
270     */
271    public List<Invitation> getInvitations(Project project) throws RepositoryException
272    {
273        List<Invitation> invitations = new ArrayList<>();
274        
275        SortCriteria sortCriteria = new SortCriteria();
276        sortCriteria.addJCRPropertyCriterion(__INVITATION_DATE, false, false);
277        
278        String xPathQuery = getInvitationXPathQuery(project, null, sortCriteria);
279        
280        @SuppressWarnings("deprecation")
281        Query query = project.getNode().getSession().getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
282        NodeIterator nodes = query.execute().getNodes();
283        
284        while (nodes.hasNext())
285        {
286            Node node = (Node) nodes.next();
287            invitations.add(_getInvitation(node));
288        }
289        
290        return invitations;
291    }
292    
293    private Invitation _getInvitation(Node node) throws ValueFormatException, PathNotFoundException, RepositoryException
294    {
295        String email = node.getProperty(__INVITATION_MAIL).getString();
296        Date date = node.getProperty(__INVITATION_DATE).getDate().getTime();
297        Node authorNode = node.getNode(__INVITATION_AUTHOR);
298        UserIdentity author = new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
299        
300        Map<String, String> allowedProfileByModules = new HashMap<>();
301        
302        Node aclNode = node.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":temp-acl");
303        NodeIterator children = aclNode.getNodes();
304        while (children.hasNext())
305        {
306            Node child = (Node) children.next();
307            if (child.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles"))
308            {
309                Value[] values = child.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles").getValues();
310                if (values.length > 0)
311                {
312                    String moduleName = child.getName();
313                    WorkspaceModule module = _moduleEP.getModuleByName(moduleName);
314                    if (module != null)
315                    {
316                        allowedProfileByModules.put(module.getId(), values[0].getString());
317                    }
318                }
319            }
320        }
321        
322        return new Invitation(email, date, author, allowedProfileByModules, _getProjectName(node));
323    }
324    
325    private String _getProjectName(Node node)
326    {
327        try
328        {
329            Node parentNode = node.getParent();
330            
331            while (parentNode != null && !NodeTypeHelper.isNodeType(parentNode, "ametys:project"))
332            {
333                parentNode = parentNode.getParent();
334            }
335            
336            return parentNode != null ? parentNode.getName() : null;
337        }
338        catch (RepositoryException e)
339        {
340            getLogger().error("Unable to get parent project", e);
341            return null;
342        }
343    }
344    
345    /**
346     * Creates the XPath query corresponding to specified {@link Expression}.
347     * @param project The project. Can be null to browser all projects
348     * @param invitExpression the query predicates.
349     * @param sortCriteria the sort criteria.
350     * @return the created XPath query.
351     * @throws RepositoryException if an error occurred
352     */
353    public String getInvitationXPathQuery(Project project, Expression invitExpression, SortCriteria sortCriteria) throws RepositoryException
354    {
355        String predicats = null;
356        
357        if (invitExpression != null)
358        {
359            predicats = StringUtils.trimToNull(invitExpression.build());
360        }
361        
362        StringBuilder xpathQuery = new StringBuilder();
363        
364        if (project != null)
365        {
366            xpathQuery.append("/jcr:root")
367                .append(getInvitationsRootNode(project, true).getPath());
368        }
369        
370        xpathQuery.append("//element(*, " + __NODETYPE_INVITATION + ")");
371        
372        if (predicats != null)
373        {
374            xpathQuery.append("[" + predicats + "]");
375        }
376        
377        if (sortCriteria != null)
378        {
379            xpathQuery.append(" " + sortCriteria.build());
380        }
381        
382        return xpathQuery.toString();
383    }
384    
385    /**
386     * Add an invitation
387     * @param project The project
388     * @param mail The mail
389     * @param invitDate The invitation's date
390     * @param author The invitation's author
391     * @param allowedProfileByModules The allowed profiles by modules
392     * @return The created invitation node
393     * @throws RepositoryException if an error occurred
394     */
395    public Invitation addInvitation (Project project, Date invitDate, String mail, UserIdentity author, Map<String, String> allowedProfileByModules) throws RepositoryException
396    {
397        Node contextNode = getInvitationsRootNode(project, true);
398        
399        Node invitNode = contextNode.addNode(__INVITATION_NODE_NAME, __NODETYPE_INVITATION);
400        
401        // Date
402        GregorianCalendar gc = new GregorianCalendar();
403        gc.setTime(invitDate);
404        
405        invitNode.setProperty(__INVITATION_DATE, gc);
406        
407        // Mail
408        invitNode.setProperty(__INVITATION_MAIL, mail);
409        
410        // Author
411        Node authorNode = invitNode.addNode(__INVITATION_AUTHOR);
412        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin());
413        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId());
414        
415        Node aclNode = invitNode.addNode(__INVITATION_ACL);
416        
417        for (Entry<String, String> allowedProfile : allowedProfileByModules.entrySet())
418        {
419            String moduleId = allowedProfile.getKey();
420            String profileId = allowedProfile.getValue();
421            
422            WorkspaceModule module = _moduleEP.getExtension(moduleId);
423            if (module != null)
424            {
425                Node moduleNode = aclNode.addNode(module.getModuleName());
426                moduleNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles", new String[] {profileId});
427            }
428        }
429        
430        return new Invitation(mail, invitDate, author, allowedProfileByModules, project.getName());
431    }
432    
433    /**
434     * Determines if a invitation already exists for this email
435     * @param project the project
436     * @param mail the mail to test
437     * @return true if a invitation exists
438     * @throws RepositoryException if an error occured
439     */
440    public boolean invitationExists(Project project, String mail) throws RepositoryException
441    {
442        Node contextNode = getInvitationsRootNode(project, true);
443        
444        NodeIterator nodes = contextNode.getNodes(__INVITATION_NODE_NAME);
445        while (nodes.hasNext())
446        {
447            Node node = (Node) nodes.next();
448            if (node.hasProperty(__INVITATION_MAIL) && node.getProperty(__INVITATION_MAIL).equals(mail))
449            {
450                return true;
451            }
452        }
453        
454        return false;
455    }
456    
457    /**
458     * Remove a invitation 
459     * @param project the project
460     * @param mail the mail to remove
461     * @return true if a invitation has been removed
462     * @throws RepositoryException if an error occured
463     */
464    public boolean removeInvitation(Project project, String mail) throws RepositoryException
465    {
466        Node contextNode = getInvitationsRootNode(project, true);
467        
468        NodeIterator nodes = contextNode.getNodes(__INVITATION_NODE_NAME);
469        while (nodes.hasNext())
470        {
471            Node node = (Node) nodes.next();
472            if (node.hasProperty(__INVITATION_MAIL) && node.getProperty(__INVITATION_MAIL).getString().equals(mail))
473            {
474                node.remove();
475                return true;
476            }
477        }
478        
479        return false;
480    }
481    
482    /**
483     * Bean representing a invitation by email
484     *
485     */
486    public class Invitation
487    {
488        private Date _date;
489        private UserIdentity _author;
490        private String _email;
491        private Map<String, String> _allowedProfileByModules;
492        private String _projectName;
493        
494        /**
495         * Constructor
496         * @param email The email
497         * @param date The date of invitation
498         * @param author The author of invitation
499         * @param allowedProfileByModules The rights
500         * @param projectName the name of parent project
501         */
502        public Invitation(String email, Date date, UserIdentity author, Map<String, String> allowedProfileByModules, String projectName)
503        {
504            _email = email;
505            _date = date;
506            _author = author;
507            _allowedProfileByModules = allowedProfileByModules;
508            _projectName = projectName;
509        }
510        
511        /**
512         * Get the email
513         * @return the email
514         */
515        public String getEmail()
516        {
517            return _email;
518        }
519        
520        /**
521         * Get the date of invitation
522         * @return the date of invitation
523         */
524        public Date getDate()
525        {
526            return _date;
527        }
528        
529        /**
530         * Get the author of invitation
531         * @return the author of invitation
532         */
533        public UserIdentity getAuthor()
534        {
535            return _author;
536        }
537        
538        /**
539         * Get the allowed profile for each module
540         * @return the allowed profile for each module
541         */
542        public Map<String, String> getAllowedProfileByModules()
543        {
544            return _allowedProfileByModules;
545        }
546        
547        /**
548         * Get the parent project name
549         * @return the parent project name
550         */
551        public String getProjectName()
552        {
553            return _projectName;
554        }
555        
556        @Override
557        public String toString()
558        {
559            return "Invitation [mail=" + _email + ", project=" + _projectName + "]";
560        }
561    }
562    
563
564}
565