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