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.project.objects;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Date;
021import java.util.Iterator;
022import java.util.Objects;
023import java.util.stream.Collectors;
024import java.util.stream.StreamSupport;
025
026import javax.jcr.Node;
027import javax.jcr.NodeIterator;
028import javax.jcr.Property;
029import javax.jcr.RepositoryException;
030import javax.jcr.Value;
031import javax.jcr.ValueFactory;
032
033import org.apache.commons.lang3.ArrayUtils;
034
035import org.ametys.core.user.UserIdentity;
036import org.ametys.plugins.explorer.ExplorerNode;
037import org.ametys.plugins.repository.AmetysObject;
038import org.ametys.plugins.repository.AmetysObjectIterable;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.repository.RepositoryConstants;
041import org.ametys.plugins.repository.TraversableAmetysObject;
042import org.ametys.plugins.repository.events.EventHolder;
043import org.ametys.plugins.repository.events.JCREventHelper;
044import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
045import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
046import org.ametys.plugins.repository.metadata.UnknownMetadataException;
047import org.ametys.plugins.repository.metadata.jcr.JCRCompositeMetadata;
048import org.ametys.web.repository.site.Site;
049import org.ametys.web.repository.site.SiteManager;
050
051/**
052 * {@link AmetysObject} for storing project informations.
053 */
054public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ProjectsTreeNode, EventHolder
055{
056    /** Project node type name. */
057    public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project";
058    
059    /** Metadata name for project 's sites */
060    public static final String METADATA_SITES = "sites";
061
062    private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources";
063    private static final String __METADATA_TITLE = "title";
064    private static final String __METADATA_DESCRIPTION = "description";
065    private static final String __METADATA_URL = "url";
066    
067    private static final String __METADATA_MAILING_LIST = "mailingList";
068    private static final String __METADATA_CREATION = "creationDate";
069    private static final String __METADATA_MANAGER = "manager";
070    private static final String __METADATA_MODULES = "modules";
071    private static final String __METADATA_INSCRIPTION_STATUS = "inscriptionStatus";
072    private static final String __METADATA_DEFAULT_PROFILE = "defaultProfile";
073    
074    /**
075     * The inscription status of the project
076     */
077    public enum InscriptionStatus
078    {
079        /** Inscriptions are opened to anyone */
080        OPEN("open"),
081        /** Inscriptions are moderated */
082        MODERATED("moderated"),
083        /** Inscriptions are private */
084        PRIVATE("private");
085        
086        private String _value;
087        
088        private InscriptionStatus(String value)
089        {
090            this._value = value;
091        }  
092           
093        @Override
094        public String toString() 
095        {
096            return _value;
097        }
098        
099        /**
100         * Converts a string to an Inscription
101         * @param status The status to convert
102         * @return The status corresponding to the string or null if unknown
103         */
104        public static InscriptionStatus createsFromString(String status)
105        {
106            for (InscriptionStatus v : InscriptionStatus.values())
107            {
108                if (v.toString().equals(status))
109                {
110                    return v;
111                }
112            }
113            return null;
114        }
115    }
116    
117    /**
118     * Creates a {@link Project}.
119     * @param node the node backing this {@link AmetysObject}.
120     * @param parentPath the parent path in the Ametys hierarchy.
121     * @param factory the {@link ProjectFactory} which creates the AmetysObject.
122     */
123    public Project(Node node, String parentPath, ProjectFactory factory)
124    {
125        super(node, parentPath, factory);
126    }
127    
128    /**
129     * Retrieves the title.
130     * @return the title.
131     * @throws AmetysRepositoryException if an error occurs.
132     */
133    public String getTitle() throws AmetysRepositoryException
134    {
135        try
136        {
137            return getMetadataHolder().getString(__METADATA_TITLE);
138        }
139        catch (UnknownMetadataException e)
140        {
141            return null;
142        }
143    }
144    
145    /**
146     * Set the title.
147     * @param title the title.
148     * @throws AmetysRepositoryException if an error occurs.
149     */
150    public void setTitle(String title) throws AmetysRepositoryException
151    {
152        getMetadataHolder().setMetadata(__METADATA_TITLE, title);
153    }
154    
155    /**
156     * Retrieves the description.
157     * @return the description.
158     * @throws AmetysRepositoryException if an error occurs.
159     */
160    public String getDescription() throws AmetysRepositoryException
161    {
162        try
163        {
164            return getMetadataHolder().getString(__METADATA_DESCRIPTION);
165        }
166        catch (UnknownMetadataException e)
167        {
168            return null;
169        }
170    }
171    
172    /**
173     * Set the description.
174     * @param description the description.
175     * @throws AmetysRepositoryException if an error occurs.
176     */
177    public void setDescription(String description) throws AmetysRepositoryException
178    {
179        getMetadataHolder().setMetadata(__METADATA_DESCRIPTION, description);
180    }
181    
182    /**
183     * Remove the description.
184     * @throws AmetysRepositoryException if an error occurs.
185     */
186    public void removeDescription() throws AmetysRepositoryException
187    {
188        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
189        if (metadataHolder.hasMetadata(__METADATA_DESCRIPTION))
190        {
191            getMetadataHolder().removeMetadata(__METADATA_DESCRIPTION);
192        }
193    }
194    
195    /**
196     * Retrieves the URL.
197     * @return the URL.
198     * @throws AmetysRepositoryException if an error occurs.
199     */
200    public String getURL() throws AmetysRepositoryException
201    {
202        try
203        {
204            return getMetadataHolder().getString(__METADATA_URL);
205        }
206        catch (UnknownMetadataException e)
207        {
208            return null;
209        }
210    }
211    
212    /**
213     * Set the URL.
214     * @param url the URL.
215     * @throws AmetysRepositoryException if an error occurs.
216     */
217    public void setURL(String url) throws AmetysRepositoryException
218    {
219        getMetadataHolder().setMetadata(__METADATA_URL, url);
220    }
221    
222    /**
223     * Retrieves the explorer nodes.
224     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
225     * @throws AmetysRepositoryException if an error occurs.
226     */
227    public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException
228    {
229        return (ExplorerNode) getChild(__EXPLORER_NODE_NAME);
230    }
231    
232    /**
233     * Retrieves the explorer nodes.
234     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
235     * @throws AmetysRepositoryException if an error occurs.
236     */
237    public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException
238    {
239        return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren();
240    }
241    
242    
243    /**
244     * Retrieves the mailing list.
245     * @return the mailing list.
246     * @throws AmetysRepositoryException if an error occurs.
247     */
248    public String getMailingList() throws AmetysRepositoryException
249    {
250        try
251        {
252            return getMetadataHolder().getString(__METADATA_MAILING_LIST);
253        }
254        catch (UnknownMetadataException e)
255        {
256            return null;
257        }
258    }
259    
260    /**
261     * Set the mailing list.
262     * @param mailingList the mailing list.
263     * @throws AmetysRepositoryException if an error occurs.
264     */
265    public void setMailingList(String mailingList) throws AmetysRepositoryException
266    {
267        getMetadataHolder().setMetadata(__METADATA_MAILING_LIST, mailingList);
268    }
269    
270    /**
271     * Remove the mailing list.
272     * @throws AmetysRepositoryException if an error occurs.
273     */
274    public void removeMailingList() throws AmetysRepositoryException
275    {
276        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
277        if (metadataHolder.hasMetadata(__METADATA_MAILING_LIST))
278        {
279            getMetadataHolder().removeMetadata(__METADATA_MAILING_LIST);
280        }
281    }
282    
283    /**
284     * Retrieves the date of creation.
285     * @return the date of creation.
286     * @throws AmetysRepositoryException if an error occurs.
287     */
288    public Date getCreationDate() throws AmetysRepositoryException
289    {
290        try
291        {
292            return getMetadataHolder().getDate(__METADATA_CREATION);
293        }
294        catch (UnknownMetadataException e)
295        {
296            return null;
297        }
298    }
299    
300    /**
301     * Set the date of creation.
302     * @param creationDate the date of creation
303     * @throws AmetysRepositoryException if an error occurs.
304     */
305    public void setCreationDate(Date creationDate) throws AmetysRepositoryException
306    {
307        getMetadataHolder().setMetadata(__METADATA_CREATION, creationDate);
308    }
309    
310    @Override
311    public Node getEventsRootNode() throws RepositoryException
312    {
313        return JCREventHelper.getEventsRootNode(getNode());
314    }
315    
316    @Override
317    public NodeIterator getEvents() throws RepositoryException
318    {
319        return JCREventHelper.getEvents(this);
320    }
321    
322    /**
323     * Get the sites of the project
324     * @return The collection of sites
325     */
326    public Collection<Site> getSites()
327    {
328        try
329        {
330            ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
331            
332            if (!metadataHolder.hasMetadata(METADATA_SITES))
333            {
334                return new ArrayList<>();
335            }
336            else
337            {
338                SiteManager siteManager = _getFactory()._getSiteManager();
339                
340                // Stream over the properties to retrieves the corresponding sites.
341                JCRCompositeMetadata sitesHolder = (JCRCompositeMetadata) metadataHolder.getCompositeMetadata(METADATA_SITES);
342                Node jcrSitesNode = sitesHolder.getNode();
343                Iterator<Property> propertyIterator = jcrSitesNode.getProperties();
344                Iterable<Property> propertyIterable = () -> propertyIterator;
345                
346                return StreamSupport.stream(propertyIterable.spliterator(), false)
347                    .map(p -> 
348                    {
349                        try
350                        {
351                            return p.getNode().getName();
352                        }
353                        catch (Exception e)
354                        {
355                            // site might not exist (anymore...)
356                            return null;
357                        }
358                    })
359                    .filter(Objects::nonNull)
360                    .map(siteName -> siteManager.getSite(siteName))
361                    .collect(Collectors.toList());
362            }
363        }
364        catch (RepositoryException e)
365        {
366            return new ArrayList<>();
367        }
368    }
369    
370    /**
371     * Set the sites of the project
372     * @param sites The names of the site
373     */
374    public void setSites(Collection<String> sites)
375    {
376        ModifiableCompositeMetadata metadataHolder = getMetadataHolder();
377        
378        if (metadataHolder.hasMetadata(METADATA_SITES))
379        {
380            metadataHolder.removeMetadata(METADATA_SITES);
381        }
382        
383        JCRCompositeMetadata sitesHolder = (JCRCompositeMetadata) metadataHolder.getCompositeMetadata(METADATA_SITES, true);
384        Node jcrSitesNode = sitesHolder.getNode();
385        SiteManager siteManager = _getFactory()._getSiteManager();
386        
387        // create weak references to site nodes
388        int[] propIdx = {0};
389        sites.forEach(siteName -> 
390        {
391            if (siteManager.hasSite(siteName))
392            {
393                Site site = siteManager.getSite(siteName);
394
395                try
396                {
397                    ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory();
398                    Value weakRefValue = valueFactory.createValue(site.getNode(), true);
399
400                    propIdx[0]++; // increment index
401                    jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue);
402                }
403                catch (RepositoryException e)
404                {
405                    throw new AmetysRepositoryException("Unexpected repository exception", e);
406                }
407            }
408        });
409        
410        if (needsSave())
411        {
412            saveChanges();
413        }
414    }
415
416//    /**
417//     * Get the project path of the project
418//     * The project path is composed of the project category names and the project name separated by slashes.
419//     * e.g. cat1/cat2/project-name
420//     * @return he project path of the project
421//     */
422//    public String getProjectPath()
423//    {
424//        Deque<String> path = new ArrayDeque<>(); 
425//        path.addFirst(getName());
426//        
427//        try
428//        {
429//            Node parentNode = getNode().getParent();
430//            
431//            while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory"))
432//            {
433//                path.addFirst(parentNode.getName());
434//                parentNode = parentNode.getParent();
435//            }
436//            
437//            return String.join("/", path);
438//        }
439//        catch (RepositoryException e)
440//        {
441//            throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e);
442//        }
443//    }
444    
445    /**
446     * Get the project manager user identity
447     * @return The manager
448     */
449    public UserIdentity getManager()
450    {
451        return getMetadataHolder().getUser(__METADATA_MANAGER, null);
452    }
453    
454    /**
455     * Set the project manager
456     * @param user The manager
457     */
458    public void setManager(UserIdentity user)
459    {
460        getMetadataHolder().setMetadata(__METADATA_MANAGER, user);
461    }
462    
463    /**
464     * Retrieve the list of activated modules for the project
465     * @return The list of modules ids
466     */
467    public String[] getModules()
468    {
469        return getMetadataHolder().getStringArray(__METADATA_MODULES, new String[]{});
470    }
471    
472    /**
473     * Set the list of activated modules for the project
474     * @param modules The list of modules
475     */
476    public void setModules(String[] modules)
477    {
478        getMetadataHolder().setMetadata(__METADATA_MODULES, modules);
479    }
480    
481    /**
482     * Add a module to the list of activated modules
483     * @param moduleId The module id
484     */
485    public void addModule(String moduleId)
486    {
487        String[] modules = getMetadataHolder().getStringArray(__METADATA_MODULES, null);
488        if (!ArrayUtils.contains(modules, moduleId))
489        {
490            getMetadataHolder().setMetadata(__METADATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId));
491        }
492    }
493    
494    /**
495     * Remove a module from the list of activated modules
496     * @param moduleId The module id
497     */
498    public void removeModule(String moduleId)
499    {
500        String[] modules = getMetadataHolder().getStringArray(__METADATA_MODULES, null);
501        if (ArrayUtils.contains(modules, moduleId))
502        {
503            getMetadataHolder().setMetadata(__METADATA_MODULES, ArrayUtils.removeElement(modules, moduleId));
504        }
505    }
506    
507    /**
508     * Get the inscription status of the project
509     * @return The inscription status
510     */
511    public InscriptionStatus getInscriptionStatus()
512    {
513        if (getMetadataHolder().hasMetadata(__METADATA_INSCRIPTION_STATUS))
514        {
515            return InscriptionStatus.createsFromString(getMetadataHolder().getString(__METADATA_INSCRIPTION_STATUS));
516        }
517        return InscriptionStatus.PRIVATE;
518    }
519    
520    /**
521     * Set the inscription status of the project
522     * @param inscriptionStatus The inscription status
523     */
524    public void setInscriptionStatus(String inscriptionStatus)
525    {
526        if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null)
527        {
528            getMetadataHolder().setMetadata(__METADATA_INSCRIPTION_STATUS, inscriptionStatus);
529        }
530        else if (getMetadataHolder().hasMetadata(__METADATA_INSCRIPTION_STATUS))
531        {
532            getMetadataHolder().removeMetadata(__METADATA_INSCRIPTION_STATUS);
533        }
534    }
535    
536    /**
537     * Get the default profile for new members of the project
538     * @return The default profile
539     */
540    public String getDefaultProfile()
541    {
542        if (getMetadataHolder().hasMetadata(__METADATA_DEFAULT_PROFILE))
543        {
544            return getMetadataHolder().getString(__METADATA_DEFAULT_PROFILE);
545        }
546        return null;
547    }
548     
549    
550    /**
551     * Set the default profile for the members of the project
552     * @param profileId The ID of the profile
553     */
554    public void setDefaultProfile(String profileId)
555    {
556        if (profileId != null)
557        {
558            getMetadataHolder().setMetadata(__METADATA_DEFAULT_PROFILE, profileId);
559        }
560        else if (getMetadataHolder().hasMetadata(__METADATA_DEFAULT_PROFILE))
561        {
562            getMetadataHolder().removeMetadata(__METADATA_DEFAULT_PROFILE);
563        }
564    }
565}