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     * Get the default profile for new members of the project
500     * @return The default profile
501     */
502    public String getDefaultProfile()
503    {
504        return getValue(__DATA_DEFAULT_PROFILE);
505    }
506     
507    
508    /**
509     * Set the default profile for the members of the project
510     * @param profileId The ID of the profile
511     */
512    public void setDefaultProfile(String profileId)
513    {
514        if (profileId != null)
515        {
516            setValue(__DATA_DEFAULT_PROFILE, profileId);
517        }
518        else if (hasValue(__DATA_DEFAULT_PROFILE))
519        {
520            removeValue(__DATA_DEFAULT_PROFILE);
521        }
522    }
523    
524    /**
525     * Get the root for plugins
526     * @return the root for plugins
527     * @throws AmetysRepositoryException if an error occurs.
528     */
529    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
530    {
531        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
532    }
533
534    /**
535     * Retrieve the list of tags 
536     * @return The list of tags
537     * @throws AmetysRepositoryException if an error occurs
538     */
539    public Set<String> getTags() throws AmetysRepositoryException
540    {
541        return TaggableAmetysObjectHelper.getTags(this);
542    }
543    
544    /**
545     * Add a tag to the project
546     * @param tag The tag
547     * @throws AmetysRepositoryException if an error occurs
548     */
549    public void tag(String tag) throws AmetysRepositoryException
550    {
551        TaggableAmetysObjectHelper.tag(this, tag);
552    }
553
554    /**
555     * Remove a tag from the project
556     * @param tag The tag
557     * @throws AmetysRepositoryException if an error occurs
558     */
559    public void untag(String tag) throws AmetysRepositoryException
560    {
561        TaggableAmetysObjectHelper.untag(this, tag);
562    }
563
564    /**
565     * Set the project tags
566     * @param tags The list of tags
567     */
568    public void setTags(List<String> tags)
569    {
570        Set<String> currentTags = getTags();
571        // remove old tags not selected
572        currentTags.stream()
573                .filter(tag -> !tags.contains(tag))
574                .forEach(tag -> untag(tag));
575        
576        // add new selected tags
577        tags.stream()
578                .filter(tag -> !currentTags.contains(tag))
579                .forEach(tag -> tag(tag));
580    }
581
582
583    /**
584     * Retrieve the list of categories
585     * @return The categories
586     * @throws AmetysRepositoryException if an error occurs
587     */
588    public Set<String> getCategories() throws AmetysRepositoryException
589    {
590        return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES);
591    }
592    
593    /**
594     * Add a category to the project
595     * @param category The category
596     * @throws AmetysRepositoryException if an error occurs
597     */
598    public void addCategory(String category) throws AmetysRepositoryException
599    {
600        TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES);
601    }
602
603    /**
604     * Remove a category from the project
605     * @param category The category
606     * @throws AmetysRepositoryException if an error occurs
607     */
608    public void removeCategory(String category) throws AmetysRepositoryException
609    {
610        TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES);
611    }
612    
613    /**
614     * Set the category tags of the project
615     * @param categoryTags The category tags
616     */
617    public void setCategoryTags(List<String> categoryTags)
618    {
619        Set<String> currentCategories = getCategories();
620        // remove old tags not selected
621        currentCategories.stream()
622                .filter(category -> !categoryTags.contains(category))
623                .forEach(category -> removeCategory(category));
624        
625        // add new selected tags
626        categoryTags.stream()
627                .filter(category -> !currentCategories.contains(category))
628                .forEach(category -> addCategory(category));
629    }
630    
631    /**
632     * Set the cover image of the site
633     * @param is The input stream of the cover image
634     * @param mimeType The mimetype of the cover image
635     * @param filename The filename of the cover image
636     * @param lastModificationDate The last modification date of the cover image
637     * @throws IOException if an error occurs while setting the cover image
638     */
639    public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException
640    {
641        if (is != null)
642        {
643            Binary coverImage = new Binary();
644            coverImage.setInputStream(is);
645            Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType);
646            Optional.ofNullable(filename).ifPresent(coverImage::setFilename);
647            Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate);
648            setValue(__DATA_COVERIMAGE, coverImage);
649        }
650        else if (hasValue(__DATA_COVERIMAGE))
651        {
652            removeValue(__DATA_COVERIMAGE);
653        }
654    }
655    
656    /**
657     * Returns the cover image of the project
658     * @return the cover image of the project
659     */
660    public Binary getCoverImage()
661    {
662        return getValue(__DATA_COVERIMAGE);
663    }
664    
665    /**
666     * Generates SAX events for the project's cover image
667     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
668     * @throws SAXException if error occurs during the SAX events generation
669     * @throws IOException if an I/O error occurs while reading the cover image
670     */
671    public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException
672    {
673        dataToSAX(contentHandler, __DATA_COVERIMAGE);
674    }
675    
676    /**
677     * Retrieve the list of keywords for the project
678     * @return The list of keywords
679     */
680    public String[] getKeywords()
681    {
682        return getValue(__DATA_KEYWORDS, new String[0]);
683    }
684    
685    /**
686     * Set the list of keywordss for the project
687     * @param keywords The list of keywords
688     */
689    public void setKeywords(String[] keywords)
690    {
691        setValue(__DATA_KEYWORDS, keywords);
692    }
693}