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