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.core.user.UserIdentity;
045import org.ametys.plugins.explorer.ExplorerNode;
046import org.ametys.plugins.repository.AmetysObject;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
050import org.ametys.plugins.repository.RepositoryConstants;
051import org.ametys.plugins.repository.TraversableAmetysObject;
052import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject;
053import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
054import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
055import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
056import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
057import org.ametys.plugins.repository.events.EventHolder;
058import org.ametys.plugins.repository.events.JCREventHelper;
059import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
060import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
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        removeValue(__DATA_DESCRIPTION);
198    }
199    
200    /**
201     * Retrieves the explorer nodes.
202     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
203     * @throws AmetysRepositoryException if an error occurs.
204     */
205    public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException
206    {
207        return (ExplorerNode) getChild(__EXPLORER_NODE_NAME);
208    }
209    
210    /**
211     * Retrieves the explorer nodes.
212     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
213     * @throws AmetysRepositoryException if an error occurs.
214     */
215    public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException
216    {
217        return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren();
218    }
219    
220    
221    /**
222     * Retrieves the mailing list.
223     * @return the mailing list.
224     * @throws AmetysRepositoryException if an error occurs.
225     */
226    public String getMailingList() throws AmetysRepositoryException
227    {
228        return getValue(__DATA_MAILING_LIST);
229    }
230    
231    /**
232     * Set the mailing list.
233     * @param mailingList the mailing list.
234     * @throws AmetysRepositoryException if an error occurs.
235     */
236    public void setMailingList(String mailingList) throws AmetysRepositoryException
237    {
238        setValue(__DATA_MAILING_LIST, mailingList);
239    }
240    
241    /**
242     * Remove the mailing list.
243     * @throws AmetysRepositoryException if an error occurs.
244     */
245    public void removeMailingList() throws AmetysRepositoryException
246    {
247        removeValue(__DATA_MAILING_LIST);
248    }
249    
250    /**
251     * Retrieves the date of creation.
252     * @return the date of creation.
253     * @throws AmetysRepositoryException if an error occurs.
254     */
255    public ZonedDateTime getCreationDate() throws AmetysRepositoryException
256    {
257        return getValue(__DATA_CREATION);
258    }
259    
260    /**
261     * Set the date of creation.
262     * @param creationDate the date of creation
263     * @throws AmetysRepositoryException if an error occurs.
264     */
265    public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException
266    {
267        setValue(__DATA_CREATION, creationDate);
268    }
269    
270    @Override
271    public Node getEventsRootNode() throws RepositoryException
272    {
273        return JCREventHelper.getEventsRootNode(getNode());
274    }
275    
276    @Override
277    public NodeIterator getEvents() throws RepositoryException
278    {
279        return JCREventHelper.getEvents(this);
280    }
281    
282    /**
283     * Get the sites of the project
284     * @return The collection of sites
285     */
286    public Collection<Site> getSites()
287    {
288        try
289        {
290            if (!hasValueOrEmpty(DATA_SITES))
291            {
292                return new ArrayList<>();
293            }
294            else
295            {
296                SiteManager siteManager = _getFactory()._getSiteManager();
297                
298                // Stream over the properties to retrieve the corresponding sites.
299                JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).getRepositoryData(DATA_SITES);
300                Node jcrSitesNode = sitesData.getNode();
301                Iterator<Property> sitesIterator = jcrSitesNode.getProperties();
302                Iterable<Property> sitesIterable = () -> sitesIterator;
303                
304                return StreamSupport.stream(sitesIterable.spliterator(), false)
305                    .map(p -> 
306                    {
307                        try
308                        {
309                            return p.getNode().getName();
310                        }
311                        catch (Exception e)
312                        {
313                            // site might not exist (anymore...)
314                            return null;
315                        }
316                    })
317                    .filter(Objects::nonNull)
318                    .map(siteName -> siteManager.getSite(siteName))
319                    .collect(Collectors.toList());
320            }
321        }
322        catch (RepositoryException e)
323        {
324            return new ArrayList<>();
325        }
326    }
327    
328    /**
329     * Set the sites of the project
330     * @param sites The names of the site
331     */
332    public void setSites(Collection<String> sites)
333    {
334        removeValue(DATA_SITES);
335        
336        JCRRepositoryData sitesData = (JCRRepositoryData) new JCRRepositoryData(getNode()).addRepositoryData(DATA_SITES, RepositoryConstants.NAMESPACE_PREFIX + ":compositeMetadata");
337        Node jcrSitesNode = sitesData.getNode();
338        SiteManager siteManager = _getFactory()._getSiteManager();
339        
340        // create weak references to site nodes
341        int[] propIdx = {0};
342        sites.forEach(siteName -> 
343        {
344            if (siteManager.hasSite(siteName))
345            {
346                Site site = siteManager.getSite(siteName);
347
348                try
349                {
350                    ValueFactory valueFactory = jcrSitesNode.getSession().getValueFactory();
351                    Value weakRefValue = valueFactory.createValue(site.getNode(), true);
352
353                    propIdx[0]++; // increment index
354                    jcrSitesNode.setProperty(Integer.toString(propIdx[0]), weakRefValue);
355                }
356                catch (RepositoryException e)
357                {
358                    throw new AmetysRepositoryException("Unexpected repository exception", e);
359                }
360            }
361        });
362        
363        if (needsSave())
364        {
365            saveChanges();
366        }
367    }
368
369//    /**
370//     * Get the project path of the project
371//     * The project path is composed of the project category names and the project name separated by slashes.
372//     * e.g. cat1/cat2/project-name
373//     * @return he project path of the project
374//     */
375//    public String getProjectPath()
376//    {
377//        Deque<String> path = new ArrayDeque<>(); 
378//        path.addFirst(getName());
379//        
380//        try
381//        {
382//            Node parentNode = getNode().getParent();
383//            
384//            while (NodeTypeHelper.isNodeType(parentNode, "ametys:projectCategory"))
385//            {
386//                path.addFirst(parentNode.getName());
387//                parentNode = parentNode.getParent();
388//            }
389//            
390//            return String.join("/", path);
391//        }
392//        catch (RepositoryException e)
393//        {
394//            throw new AmetysRepositoryException("Unexpected repository exception while retrieving project path", e);
395//        }
396//    }
397    
398    /**
399     * Get the project managers user identities
400     * @return The managers
401     */
402    public UserIdentity[] getManagers()
403    {
404        return getValue(__DATA_MANAGERS, new UserIdentity[0]);
405    }
406    
407    /**
408     * Set the project managers
409     * @param user The managers
410     */
411    public void setManagers(UserIdentity[] user)
412    {
413        setValue(__DATA_MANAGERS, user);
414    }
415    
416    /**
417     * Retrieve the list of activated modules for the project
418     * @return The list of modules ids
419     */
420    public String[] getModules()
421    {
422        return getValue(__DATA_MODULES, new String[0]);
423    }
424    
425    /**
426     * Set the list of activated modules for the project
427     * @param modules The list of modules
428     */
429    public void setModules(String[] modules)
430    {
431        setValue(__DATA_MODULES, modules);
432    }
433    
434    /**
435     * Add a module to the list of activated modules
436     * @param moduleId The module id
437     */
438    public void addModule(String moduleId)
439    {
440        String[] modules = getValue(__DATA_MODULES);
441        if (!ArrayUtils.contains(modules, moduleId))
442        {
443            setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId));
444        }
445    }
446    
447    /**
448     * Remove a module from the list of activated modules
449     * @param moduleId The module id
450     */
451    public void removeModule(String moduleId)
452    {
453        String[] modules = getValue(__DATA_MODULES);
454        if (ArrayUtils.contains(modules, moduleId))
455        {
456            setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId));
457        }
458    }
459    
460    /**
461     * Get the inscription status of the project
462     * @return The inscription status
463     */
464    public InscriptionStatus getInscriptionStatus()
465    {
466        if (hasValue(__DATA_INSCRIPTION_STATUS))
467        {
468            return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS));
469        }
470        return InscriptionStatus.PRIVATE;
471    }
472    
473    /**
474     * Set the inscription status of the project
475     * @param inscriptionStatus The inscription status
476     */
477    public void setInscriptionStatus(String inscriptionStatus)
478    {
479        if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null)
480        {
481            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus);
482        }
483        else
484        {
485            removeValue(__DATA_INSCRIPTION_STATUS);
486        }
487    }
488
489    /**
490     * Set the inscription status of the project
491     * @param inscriptionStatus The inscription status
492     */
493    public void setInscriptionStatus(InscriptionStatus inscriptionStatus)
494    {
495        if (inscriptionStatus != null)
496        {
497            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString());
498        }
499        else
500        {
501            removeValue(__DATA_INSCRIPTION_STATUS);
502        }
503    }
504
505    /**
506     * Get the default profile for new members of the project
507     * @return The default profile
508     */
509    public String getDefaultProfile()
510    {
511        return getValue(__DATA_DEFAULT_PROFILE);
512    }
513     
514    
515    /**
516     * Set the default profile for the members of the project
517     * @param profileId The ID of the profile
518     */
519    public void setDefaultProfile(String profileId)
520    {
521        if (profileId != null)
522        {
523            setValue(__DATA_DEFAULT_PROFILE, profileId);
524        }
525        else
526        {
527            removeValue(__DATA_DEFAULT_PROFILE);
528        }
529    }
530    
531    /**
532     * Get the root for plugins
533     * @return the root for plugins
534     * @throws AmetysRepositoryException if an error occurs.
535     */
536    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
537    {
538        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
539    }
540
541    /**
542     * Retrieve the list of tags 
543     * @return The list of tags
544     * @throws AmetysRepositoryException if an error occurs
545     */
546    public Set<String> getTags() throws AmetysRepositoryException
547    {
548        return TaggableAmetysObjectHelper.getTags(this);
549    }
550    
551    /**
552     * Add a tag to the project
553     * @param tag The tag
554     * @throws AmetysRepositoryException if an error occurs
555     */
556    public void tag(String tag) throws AmetysRepositoryException
557    {
558        TaggableAmetysObjectHelper.tag(this, tag);
559    }
560
561    /**
562     * Remove a tag from the project
563     * @param tag The tag
564     * @throws AmetysRepositoryException if an error occurs
565     */
566    public void untag(String tag) throws AmetysRepositoryException
567    {
568        TaggableAmetysObjectHelper.untag(this, tag);
569    }
570
571    /**
572     * Set the project tags
573     * @param tags The list of tags
574     */
575    public void setTags(List<String> tags)
576    {
577        Set<String> currentTags = getTags();
578        // remove old tags not selected
579        currentTags.stream()
580                .filter(tag -> !tags.contains(tag))
581                .forEach(tag -> untag(tag));
582        
583        // add new selected tags
584        tags.stream()
585                .filter(tag -> !currentTags.contains(tag))
586                .forEach(tag -> tag(tag));
587    }
588
589
590    /**
591     * Retrieve the list of categories
592     * @return The categories
593     * @throws AmetysRepositoryException if an error occurs
594     */
595    public Set<String> getCategories() throws AmetysRepositoryException
596    {
597        return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES);
598    }
599    
600    /**
601     * Add a category to the project
602     * @param category The category
603     * @throws AmetysRepositoryException if an error occurs
604     */
605    public void addCategory(String category) throws AmetysRepositoryException
606    {
607        TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES);
608    }
609
610    /**
611     * Remove a category from the project
612     * @param category The category
613     * @throws AmetysRepositoryException if an error occurs
614     */
615    public void removeCategory(String category) throws AmetysRepositoryException
616    {
617        TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES);
618    }
619    
620    /**
621     * Set the category tags of the project
622     * @param categoryTags The category tags
623     */
624    public void setCategoryTags(List<String> categoryTags)
625    {
626        Set<String> currentCategories = getCategories();
627        // remove old tags not selected
628        currentCategories.stream()
629                .filter(category -> !categoryTags.contains(category))
630                .forEach(category -> removeCategory(category));
631        
632        // add new selected tags
633        categoryTags.stream()
634                .filter(category -> !currentCategories.contains(category))
635                .forEach(category -> addCategory(category));
636    }
637    
638    /**
639     * Set the cover image of the site
640     * @param is The input stream of the cover image
641     * @param mimeType The mimetype of the cover image
642     * @param filename The filename of the cover image
643     * @param lastModificationDate The last modification date of the cover image
644     * @throws IOException if an error occurs while setting the cover image
645     */
646    public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException
647    {
648        if (is != null)
649        {
650            Binary coverImage = new Binary();
651            coverImage.setInputStream(is);
652            Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType);
653            Optional.ofNullable(filename).ifPresent(coverImage::setFilename);
654            Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate);
655            setValue(__DATA_COVERIMAGE, coverImage);
656        }
657        else
658        {
659            removeValue(__DATA_COVERIMAGE);
660        }
661    }
662    
663    /**
664     * Returns the cover image of the project
665     * @return the cover image of the project
666     */
667    public Binary getCoverImage()
668    {
669        return getValue(__DATA_COVERIMAGE);
670    }
671    
672    /**
673     * Generates SAX events for the project's cover image
674     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
675     * @throws SAXException if error occurs during the SAX events generation
676     * @throws IOException if an I/O error occurs while reading the cover image
677     */
678    public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException
679    {
680        dataToSAX(contentHandler, __DATA_COVERIMAGE);
681    }
682    
683    /**
684     * Retrieve the list of keywords for the project
685     * @return The list of keywords
686     */
687    public String[] getKeywords()
688    {
689        return getValue(__DATA_KEYWORDS, new String[0]);
690    }
691    
692    /**
693     * Set the list of keywordss for the project
694     * @param keywords The list of keywords
695     */
696    public void setKeywords(String[] keywords)
697    {
698        setValue(__DATA_KEYWORDS, keywords);
699    }
700}