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.IOException;
019import java.io.InputStream;
020import java.time.ZonedDateTime;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Objects;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.StreamSupport;
030
031import javax.jcr.Node;
032import javax.jcr.NodeIterator;
033import javax.jcr.Property;
034import javax.jcr.RepositoryException;
035import javax.jcr.Value;
036import javax.jcr.ValueFactory;
037
038import org.apache.commons.lang3.ArrayUtils;
039import org.xml.sax.ContentHandler;
040import org.xml.sax.SAXException;
041
042import org.ametys.cms.data.Binary;
043import org.ametys.cms.indexing.solr.SolrAclCacheUninfluentialObject;
044import org.ametys.cms.tag.jcr.TaggableAmetysObjectHelper;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.plugins.explorer.ExplorerNode;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysObjectIterable;
049import org.ametys.plugins.repository.AmetysRepositoryException;
050import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
051import org.ametys.plugins.repository.RepositoryConstants;
052import org.ametys.plugins.repository.TraversableAmetysObject;
053import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject;
054import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
055import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
056import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
057import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
058import org.ametys.plugins.repository.events.EventHolder;
059import org.ametys.plugins.repository.events.JCREventHelper;
060import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
061import org.ametys.web.repository.site.Site;
062import org.ametys.web.repository.site.SiteManager;
063
064/**
065 * {@link AmetysObject} for storing project informations.
066 */
067@SolrAclCacheUninfluentialObject
068public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ModifiableModelLessDataAwareAmetysObject, EventHolder
069{
070    /** Project node type name. */
071    public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project";
072    
073    /** Metadata name for project 's sites */
074    public static final String DATA_SITES = "sites";
075
076    private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources";
077    private static final String __DATA_TITLE = "title";
078    private static final String __DATA_DESCRIPTION = "description";
079    
080    private static final String __DATA_MAILING_LIST = "mailingList";
081    private static final String __DATA_CREATION = "creationDate";
082    private static final String __DATA_MANAGERS = "managers";
083    private static final String __DATA_MODULES = "modules";
084    private static final String __DATA_INSCRIPTION_STATUS = "inscriptionStatus";
085    private static final String __DATA_DEFAULT_PROFILE = "defaultProfile";
086    private static final String __DATA_COVERIMAGE = "coverImage";
087    private static final String __DATA_KEYWORDS = "keywords";
088    private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins";
089    private static final String __DATA_CATEGORIES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":categories";
090
091    /**
092     * The inscription status of the project
093     */
094    public enum InscriptionStatus
095    {
096        /** Inscriptions are opened to anyone */
097        OPEN("open"),
098        /** Inscriptions are moderated */
099        MODERATED("moderated"),
100        /** Inscriptions are private */
101        PRIVATE("private");
102        
103        private String _value;
104        
105        private InscriptionStatus(String value)
106        {
107            this._value = value;
108        }  
109           
110        @Override
111        public String toString() 
112        {
113            return _value;
114        }
115        
116        /**
117         * Converts a string to an Inscription
118         * @param status The status to convert
119         * @return The status corresponding to the string or null if unknown
120         */
121        public static InscriptionStatus createsFromString(String status)
122        {
123            for (InscriptionStatus v : InscriptionStatus.values())
124            {
125                if (v.toString().equals(status))
126                {
127                    return v;
128                }
129            }
130            return null;
131        }
132    }
133    
134    /**
135     * Creates a {@link Project}.
136     * @param node the node backing this {@link AmetysObject}.
137     * @param parentPath the parent path in the Ametys hierarchy.
138     * @param factory the {@link ProjectFactory} which creates the AmetysObject.
139     */
140    public Project(Node node, String parentPath, ProjectFactory factory)
141    {
142        super(node, parentPath, factory);
143    }
144
145    public ModifiableModelLessDataHolder getDataHolder()
146    {
147        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode());
148        return new DefaultModifiableModelLessDataHolder(_getFactory().getProjectDataTypeExtensionPoint(), repositoryData);
149    }
150
151    /**
152     * Retrieves the title.
153     * @return the title.
154     * @throws AmetysRepositoryException if an error occurs.
155     */
156    public String getTitle() throws AmetysRepositoryException
157    {
158        return getValue(__DATA_TITLE);
159    }
160    
161    /**
162     * Set the title.
163     * @param title the title.
164     * @throws AmetysRepositoryException if an error occurs.
165     */
166    public void setTitle(String title) throws AmetysRepositoryException
167    {
168        setValue(__DATA_TITLE, title);
169    }
170    
171    /**
172     * Retrieves the description.
173     * @return the description.
174     * @throws AmetysRepositoryException if an error occurs.
175     */
176    public String getDescription() throws AmetysRepositoryException
177    {
178        return getValue(__DATA_DESCRIPTION);
179    }
180    
181    /**
182     * Set the description.
183     * @param description the description.
184     * @throws AmetysRepositoryException if an error occurs.
185     */
186    public void setDescription(String description) throws AmetysRepositoryException
187    {
188        setValue(__DATA_DESCRIPTION, description);
189    }
190    
191    /**
192     * Remove the description.
193     * @throws AmetysRepositoryException if an error occurs.
194     */
195    public void removeDescription() throws AmetysRepositoryException
196    {
197        if (hasValue(__DATA_DESCRIPTION))
198        {
199            removeValue(__DATA_DESCRIPTION);
200        }
201    }
202    
203    /**
204     * Retrieves the explorer nodes.
205     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
206     * @throws AmetysRepositoryException if an error occurs.
207     */
208    public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException
209    {
210        return (ExplorerNode) getChild(__EXPLORER_NODE_NAME);
211    }
212    
213    /**
214     * Retrieves the explorer nodes.
215     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
216     * @throws AmetysRepositoryException if an error occurs.
217     */
218    public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException
219    {
220        return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren();
221    }
222    
223    
224    /**
225     * Retrieves the mailing list.
226     * @return the mailing list.
227     * @throws AmetysRepositoryException if an error occurs.
228     */
229    public String getMailingList() throws AmetysRepositoryException
230    {
231        return getValue(__DATA_MAILING_LIST);
232    }
233    
234    /**
235     * Set the mailing list.
236     * @param mailingList the mailing list.
237     * @throws AmetysRepositoryException if an error occurs.
238     */
239    public void setMailingList(String mailingList) throws AmetysRepositoryException
240    {
241        setValue(__DATA_MAILING_LIST, mailingList);
242    }
243    
244    /**
245     * Remove the mailing list.
246     * @throws AmetysRepositoryException if an error occurs.
247     */
248    public void removeMailingList() throws AmetysRepositoryException
249    {
250        if (hasValue(__DATA_MAILING_LIST))
251        {
252            removeValue(__DATA_MAILING_LIST);
253        }
254    }
255    
256    /**
257     * Retrieves the date of creation.
258     * @return the date of creation.
259     * @throws AmetysRepositoryException if an error occurs.
260     */
261    public ZonedDateTime getCreationDate() throws AmetysRepositoryException
262    {
263        return getValue(__DATA_CREATION);
264    }
265    
266    /**
267     * Set the date of creation.
268     * @param creationDate the date of creation
269     * @throws AmetysRepositoryException if an error occurs.
270     */
271    public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException
272    {
273        setValue(__DATA_CREATION, creationDate);
274    }
275    
276    @Override
277    public Node getEventsRootNode() throws RepositoryException
278    {
279        return JCREventHelper.getEventsRootNode(getNode());
280    }
281    
282    @Override
283    public NodeIterator getEvents() throws RepositoryException
284    {
285        return JCREventHelper.getEvents(this);
286    }
287    
288    /**
289     * Get the sites of the project
290     * @return The collection of sites
291     */
292    public Collection<Site> getSites()
293    {
294        try
295        {
296            if (!hasValue(DATA_SITES))
297            {
298                return new ArrayList<>();
299            }
300            else
301            {
302                SiteManager siteManager = _getFactory()._getSiteManager();
303                
304                // Stream over the properties to retrieve the corresponding sites.
305                JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).getRepositoryData(DATA_SITES);
306                Node jcrSitesNode = sitesData.getNode();
307                Iterator<Property> sitesIterator = jcrSitesNode.getProperties();
308                Iterable<Property> sitesIterable = () -> sitesIterator;
309                
310                return StreamSupport.stream(sitesIterable.spliterator(), false)
311                    .map(p -> 
312                    {
313                        try
314                        {
315                            return p.getNode().getName();
316                        }
317                        catch (Exception e)
318                        {
319                            // site might not exist (anymore...)
320                            return null;
321                        }
322                    })
323                    .filter(Objects::nonNull)
324                    .map(siteName -> siteManager.getSite(siteName))
325                    .collect(Collectors.toList());
326            }
327        }
328        catch (RepositoryException e)
329        {
330            return new ArrayList<>();
331        }
332    }
333    
334    /**
335     * Set the sites of the project
336     * @param sites The names of the site
337     */
338    public void setSites(Collection<String> sites)
339    {
340        if (hasValue(DATA_SITES))
341        {
342            removeValue(DATA_SITES);
343        }
344        
345        JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).addRepositoryData(DATA_SITES, RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata");
346        Node jcrSitesNode = sitesData.getNode();
347        SiteManager siteManager = _getFactory()._getSiteManager();
348        
349        // create weak references to site nodes
350        int[] propIdx = {0};
351        sites.forEach(siteName -> 
352        {
353            if (siteManager.hasSite(siteName))
354            {
355                Site site = siteManager.getSite(siteName);
356
357                try
358                {
359                    ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory();
360                    Value weakRefValue = valueFactory.createValue(site.getNode(), true);
361
362                    propIdx[0]++; // increment index
363                    jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue);
364                }
365                catch (RepositoryException e)
366                {
367                    throw new AmetysRepositoryException("Unexpected repository exception", e);
368                }
369            }
370        });
371        
372        if (needsSave())
373        {
374            saveChanges();
375        }
376    }
377
378//    /**
379//     * Get the project path of the project
380//     * The project path is composed of the project category names and the project name separated by slashes.
381//     * e.g. cat1/cat2/project-name
382//     * @return he project path of the project
383//     */
384//    public String getProjectPath()
385//    {
386//        Deque<String> path = new ArrayDeque<>(); 
387//        path.addFirst(getName());
388//        
389//        try
390//        {
391//            Node parentNode = getNode().getParent();
392//            
393//            while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory"))
394//            {
395//                path.addFirst(parentNode.getName());
396//                parentNode = parentNode.getParent();
397//            }
398//            
399//            return String.join("/", path);
400//        }
401//        catch (RepositoryException e)
402//        {
403//            throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e);
404//        }
405//    }
406    
407    /**
408     * Get the project managers user identities
409     * @return The managers
410     */
411    public UserIdentity[] getManagers()
412    {
413        return getValue(__DATA_MANAGERS, new UserIdentity[0]);
414    }
415    
416    /**
417     * Set the project managers
418     * @param user The managers
419     */
420    public void setManagers(UserIdentity[] user)
421    {
422        setValue(__DATA_MANAGERS, user);
423    }
424    
425    /**
426     * Retrieve the list of activated modules for the project
427     * @return The list of modules ids
428     */
429    public String[] getModules()
430    {
431        return getValue(__DATA_MODULES, new String[0]);
432    }
433    
434    /**
435     * Set the list of activated modules for the project
436     * @param modules The list of modules
437     */
438    public void setModules(String[] modules)
439    {
440        setValue(__DATA_MODULES, modules);
441    }
442    
443    /**
444     * Add a module to the list of activated modules
445     * @param moduleId The module id
446     */
447    public void addModule(String moduleId)
448    {
449        String[] modules = getValue(__DATA_MODULES);
450        if (!ArrayUtils.contains(modules, moduleId))
451        {
452            setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId));
453        }
454    }
455    
456    /**
457     * Remove a module from the list of activated modules
458     * @param moduleId The module id
459     */
460    public void removeModule(String moduleId)
461    {
462        String[] modules = getValue(__DATA_MODULES);
463        if (ArrayUtils.contains(modules, moduleId))
464        {
465            setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId));
466        }
467    }
468    
469    /**
470     * Get the inscription status of the project
471     * @return The inscription status
472     */
473    public InscriptionStatus getInscriptionStatus()
474    {
475        if (hasValue(__DATA_INSCRIPTION_STATUS))
476        {
477            return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS));
478        }
479        return InscriptionStatus.PRIVATE;
480    }
481    
482    /**
483     * Set the inscription status of the project
484     * @param inscriptionStatus The inscription status
485     */
486    public void setInscriptionStatus(String inscriptionStatus)
487    {
488        if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null)
489        {
490            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus);
491        }
492        else if (hasValue(__DATA_INSCRIPTION_STATUS))
493        {
494            removeValue(__DATA_INSCRIPTION_STATUS);
495        }
496    }
497
498    /**
499     * Set the inscription status of the project
500     * @param inscriptionStatus The inscription status
501     */
502    public void setInscriptionStatus(InscriptionStatus inscriptionStatus)
503    {
504        if (inscriptionStatus != null)
505        {
506            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString());
507        }
508        else if (hasValue(__DATA_INSCRIPTION_STATUS))
509        {
510            removeValue(__DATA_INSCRIPTION_STATUS);
511        }
512    }
513
514    /**
515     * Get the default profile for new members of the project
516     * @return The default profile
517     */
518    public String getDefaultProfile()
519    {
520        return getValue(__DATA_DEFAULT_PROFILE);
521    }
522     
523    
524    /**
525     * Set the default profile for the members of the project
526     * @param profileId The ID of the profile
527     */
528    public void setDefaultProfile(String profileId)
529    {
530        if (profileId != null)
531        {
532            setValue(__DATA_DEFAULT_PROFILE, profileId);
533        }
534        else if (hasValue(__DATA_DEFAULT_PROFILE))
535        {
536            removeValue(__DATA_DEFAULT_PROFILE);
537        }
538    }
539    
540    /**
541     * Get the root for plugins
542     * @return the root for plugins
543     * @throws AmetysRepositoryException if an error occurs.
544     */
545    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
546    {
547        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
548    }
549
550    /**
551     * Retrieve the list of tags 
552     * @return The list of tags
553     * @throws AmetysRepositoryException if an error occurs
554     */
555    public Set<String> getTags() throws AmetysRepositoryException
556    {
557        return TaggableAmetysObjectHelper.getTags(this);
558    }
559    
560    /**
561     * Add a tag to the project
562     * @param tag The tag
563     * @throws AmetysRepositoryException if an error occurs
564     */
565    public void tag(String tag) throws AmetysRepositoryException
566    {
567        TaggableAmetysObjectHelper.tag(this, tag);
568    }
569
570    /**
571     * Remove a tag from the project
572     * @param tag The tag
573     * @throws AmetysRepositoryException if an error occurs
574     */
575    public void untag(String tag) throws AmetysRepositoryException
576    {
577        TaggableAmetysObjectHelper.untag(this, tag);
578    }
579
580    /**
581     * Set the project tags
582     * @param tags The list of tags
583     */
584    public void setTags(List<String> tags)
585    {
586        Set<String> currentTags = getTags();
587        // remove old tags not selected
588        currentTags.stream()
589                .filter(tag -> !tags.contains(tag))
590                .forEach(tag -> untag(tag));
591        
592        // add new selected tags
593        tags.stream()
594                .filter(tag -> !currentTags.contains(tag))
595                .forEach(tag -> tag(tag));
596    }
597
598
599    /**
600     * Retrieve the list of categories
601     * @return The categories
602     * @throws AmetysRepositoryException if an error occurs
603     */
604    public Set<String> getCategories() throws AmetysRepositoryException
605    {
606        return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES);
607    }
608    
609    /**
610     * Add a category to the project
611     * @param category The category
612     * @throws AmetysRepositoryException if an error occurs
613     */
614    public void addCategory(String category) throws AmetysRepositoryException
615    {
616        TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES);
617    }
618
619    /**
620     * Remove a category from the project
621     * @param category The category
622     * @throws AmetysRepositoryException if an error occurs
623     */
624    public void removeCategory(String category) throws AmetysRepositoryException
625    {
626        TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES);
627    }
628    
629    /**
630     * Set the category tags of the project
631     * @param categoryTags The category tags
632     */
633    public void setCategoryTags(List<String> categoryTags)
634    {
635        Set<String> currentCategories = getCategories();
636        // remove old tags not selected
637        currentCategories.stream()
638                .filter(category -> !categoryTags.contains(category))
639                .forEach(category -> removeCategory(category));
640        
641        // add new selected tags
642        categoryTags.stream()
643                .filter(category -> !currentCategories.contains(category))
644                .forEach(category -> addCategory(category));
645    }
646    
647    /**
648     * Set the cover image of the site
649     * @param is The input stream of the cover image
650     * @param mimeType The mimetype of the cover image
651     * @param filename The filename of the cover image
652     * @param lastModificationDate The last modification date of the cover image
653     * @throws IOException if an error occurs while setting the cover image
654     */
655    public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException
656    {
657        if (is != null)
658        {
659            Binary coverImage = new Binary();
660            coverImage.setInputStream(is);
661            Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType);
662            Optional.ofNullable(filename).ifPresent(coverImage::setFilename);
663            Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate);
664            setValue(__DATA_COVERIMAGE, coverImage);
665        }
666        else if (hasValue(__DATA_COVERIMAGE))
667        {
668            removeValue(__DATA_COVERIMAGE);
669        }
670    }
671    
672    /**
673     * Returns the cover image of the project
674     * @return the cover image of the project
675     */
676    public Binary getCoverImage()
677    {
678        return getValue(__DATA_COVERIMAGE);
679    }
680    
681    /**
682     * Generates SAX events for the project's cover image
683     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
684     * @throws SAXException if error occurs during the SAX events generation
685     * @throws IOException if an I/O error occurs while reading the cover image
686     */
687    public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException
688    {
689        dataToSAX(contentHandler, __DATA_COVERIMAGE);
690    }
691    
692    /**
693     * Retrieve the list of keywords for the project
694     * @return The list of keywords
695     */
696    public String[] getKeywords()
697    {
698        return getValue(__DATA_KEYWORDS, new String[0]);
699    }
700    
701    /**
702     * Set the list of keywordss for the project
703     * @param keywords The list of keywords
704     */
705    public void setKeywords(String[] keywords)
706    {
707        setValue(__DATA_KEYWORDS, keywords);
708    }
709}