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.List;
022import java.util.Optional;
023import java.util.Set;
024
025import javax.jcr.Node;
026import javax.jcr.RepositoryException;
027
028import org.apache.commons.lang3.ArrayUtils;
029import org.xml.sax.ContentHandler;
030import org.xml.sax.SAXException;
031
032import org.ametys.cms.data.Binary;
033import org.ametys.cms.indexing.solr.SolrAclCacheUninfluentialObject;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.plugins.explorer.ExplorerNode;
036import org.ametys.plugins.repository.AmetysObject;
037import org.ametys.plugins.repository.AmetysObjectIterable;
038import org.ametys.plugins.repository.AmetysRepositoryException;
039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
040import org.ametys.plugins.repository.RepositoryConstants;
041import org.ametys.plugins.repository.TraversableAmetysObject;
042import org.ametys.plugins.repository.activities.ActivityHolder;
043import org.ametys.plugins.repository.activities.ActivityHolderAmetysObject;
044import org.ametys.plugins.repository.activities.DefaultActivityHolder;
045import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
046import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
047import org.ametys.web.repository.SiteAwareAmetysObject;
048import org.ametys.web.repository.site.Site;
049import org.ametys.web.repository.site.SiteManager;
050
051/**
052 * {@link AmetysObject} for storing project informations.
053 */
054@SolrAclCacheUninfluentialObject
055public class Project extends DefaultTraversableAmetysObject<ProjectFactory> implements ActivityHolderAmetysObject, SiteAwareAmetysObject
056{
057    /** Project node type name. */
058    public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project";
059    
060    private static final String __EXPLORER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources";
061    private static final String __DATA_TITLE = "title";
062    private static final String __DATA_DESCRIPTION = "description";
063    
064    private static final String __DATA_MAILING_LIST = "mailingList";
065    private static final String __DATA_CREATION = "creationDate";
066    private static final String __DATA_MANAGERS = "managers";
067    private static final String __DATA_MODULES = "modules";
068    private static final String __DATA_INSCRIPTION_STATUS = "inscriptionStatus";
069    private static final String __DATA_DEFAULT_PROFILE = "defaultProfile";
070    private static final String __DATA_COVERIMAGE = "coverImage";
071    private static final String __DATA_KEYWORDS = "keywords";
072    private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins";
073    private static final String __DATA_CATEGORIES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":categories";
074
075    /**
076     * The inscription status of the project
077     */
078    public enum InscriptionStatus
079    {
080        /** Inscriptions are opened to anyone */
081        OPEN("open"),
082        /** Inscriptions are moderated */
083        MODERATED("moderated"),
084        /** Inscriptions are private */
085        PRIVATE("private");
086        
087        private String _value;
088        
089        private InscriptionStatus(String value)
090        {
091            this._value = value;
092        }
093           
094        @Override
095        public String toString()
096        {
097            return _value;
098        }
099        
100        /**
101         * Converts a string to an Inscription
102         * @param status The status to convert
103         * @return The status corresponding to the string or null if unknown
104         */
105        public static InscriptionStatus createsFromString(String status)
106        {
107            for (InscriptionStatus v : InscriptionStatus.values())
108            {
109                if (v.toString().equals(status))
110                {
111                    return v;
112                }
113            }
114            return null;
115        }
116    }
117    
118    /**
119     * Creates a {@link Project}.
120     * @param node the node backing this {@link AmetysObject}.
121     * @param parentPath the parent path in the Ametys hierarchy.
122     * @param factory the {@link ProjectFactory} which creates the AmetysObject.
123     */
124    public Project(Node node, String parentPath, ProjectFactory factory)
125    {
126        super(node, parentPath, factory);
127    }
128
129    public ActivityHolder getActivityHolder() throws RepositoryException
130    {
131        // The child node is provided by the activity-holder nodetype so we are sure it is there
132        return new DefaultActivityHolder(getChild(ACTIVITIES_ROOT_NODE_NAME), _getFactory());
133    }
134
135    /**
136     * Retrieves the title.
137     * @return the title.
138     * @throws AmetysRepositoryException if an error occurs.
139     */
140    public String getTitle() throws AmetysRepositoryException
141    {
142        return getValue(__DATA_TITLE);
143    }
144    
145    /**
146     * Set the title.
147     * @param title the title.
148     * @throws AmetysRepositoryException if an error occurs.
149     */
150    public void setTitle(String title) throws AmetysRepositoryException
151    {
152        setValue(__DATA_TITLE, title);
153    }
154    
155    /**
156     * Retrieves the description.
157     * @return the description.
158     * @throws AmetysRepositoryException if an error occurs.
159     */
160    public String getDescription() throws AmetysRepositoryException
161    {
162        return getValue(__DATA_DESCRIPTION);
163    }
164    
165    /**
166     * Set the description.
167     * @param description the description.
168     * @throws AmetysRepositoryException if an error occurs.
169     */
170    public void setDescription(String description) throws AmetysRepositoryException
171    {
172        setValue(__DATA_DESCRIPTION, description);
173    }
174    
175    /**
176     * Remove the description.
177     * @throws AmetysRepositoryException if an error occurs.
178     */
179    public void removeDescription() throws AmetysRepositoryException
180    {
181        removeValue(__DATA_DESCRIPTION);
182    }
183    
184    /**
185     * Retrieves the explorer nodes.
186     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
187     * @throws AmetysRepositoryException if an error occurs.
188     */
189    public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException
190    {
191        return (ExplorerNode) getChild(__EXPLORER_NODE_NAME);
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 AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException
200    {
201        return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren();
202    }
203    
204    
205    /**
206     * Retrieves the mailing list.
207     * @return the mailing list.
208     * @throws AmetysRepositoryException if an error occurs.
209     */
210    public String getMailingList() throws AmetysRepositoryException
211    {
212        return getValue(__DATA_MAILING_LIST);
213    }
214    
215    /**
216     * Set the mailing list.
217     * @param mailingList the mailing list.
218     * @throws AmetysRepositoryException if an error occurs.
219     */
220    public void setMailingList(String mailingList) throws AmetysRepositoryException
221    {
222        setValue(__DATA_MAILING_LIST, mailingList);
223    }
224    
225    /**
226     * Remove the mailing list.
227     * @throws AmetysRepositoryException if an error occurs.
228     */
229    public void removeMailingList() throws AmetysRepositoryException
230    {
231        removeValue(__DATA_MAILING_LIST);
232    }
233    
234    /**
235     * Retrieves the date of creation.
236     * @return the date of creation.
237     * @throws AmetysRepositoryException if an error occurs.
238     */
239    public ZonedDateTime getCreationDate() throws AmetysRepositoryException
240    {
241        return getValue(__DATA_CREATION);
242    }
243    
244    /**
245     * Set the date of creation.
246     * @param creationDate the date of creation
247     * @throws AmetysRepositoryException if an error occurs.
248     */
249    public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException
250    {
251        setValue(__DATA_CREATION, creationDate);
252    }
253    
254    @Override
255    public String getSiteName() throws AmetysRepositoryException
256    {
257        return getValue(METADATA_SITE);
258    }
259    
260    /**
261     * Get the site of the project
262     * @return The site. Can be null if the reference is broken.
263     */
264    public Site getSite()
265    {
266        SiteManager siteManager = _getFactory()._getSiteManager();
267        return siteManager.getSite(getValue(METADATA_SITE));
268    }
269
270    /**
271     * Set the site of the project
272     * @param site the site to set
273     */
274    public void setSite(Site site)
275    {
276        setValue(METADATA_SITE, site.getName());
277        
278        if (needsSave())
279        {
280            saveChanges();
281        }
282    }
283        
284    /**
285     * Get the project managers user identities
286     * @return The managers
287     */
288    public UserIdentity[] getManagers()
289    {
290        return getValueOrDefault(__DATA_MANAGERS, new UserIdentity[0]);
291    }
292    
293    /**
294     * Set the project managers
295     * @param user The managers
296     */
297    public void setManagers(UserIdentity[] user)
298    {
299        setValue(__DATA_MANAGERS, user);
300    }
301    
302    /**
303     * Retrieve the list of activated modules for the project
304     * @return The list of modules ids
305     */
306    public String[] getModules()
307    {
308        return getValueOrDefault(__DATA_MODULES, new String[0]);
309    }
310    
311    /**
312     * Set the list of activated modules for the project
313     * @param modules The list of modules
314     */
315    public void setModules(String[] modules)
316    {
317        setValue(__DATA_MODULES, modules);
318    }
319    
320    /**
321     * Add a module to the list of activated modules
322     * @param moduleId The module id
323     */
324    public void addModule(String moduleId)
325    {
326        String[] modules = getValue(__DATA_MODULES);
327        if (!ArrayUtils.contains(modules, moduleId))
328        {
329            setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId));
330        }
331    }
332    
333    /**
334     * Remove a module from the list of activated modules
335     * @param moduleId The module id
336     */
337    public void removeModule(String moduleId)
338    {
339        String[] modules = getValue(__DATA_MODULES);
340        if (ArrayUtils.contains(modules, moduleId))
341        {
342            setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId));
343        }
344    }
345    
346    /**
347     * Get the inscription status of the project
348     * @return The inscription status
349     */
350    public InscriptionStatus getInscriptionStatus()
351    {
352        if (hasValue(__DATA_INSCRIPTION_STATUS))
353        {
354            return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS));
355        }
356        return InscriptionStatus.PRIVATE;
357    }
358    
359    /**
360     * Set the inscription status of the project
361     * @param inscriptionStatus The inscription status
362     */
363    public void setInscriptionStatus(String inscriptionStatus)
364    {
365        if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null)
366        {
367            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus);
368        }
369        else
370        {
371            removeValue(__DATA_INSCRIPTION_STATUS);
372        }
373    }
374
375    /**
376     * Set the inscription status of the project
377     * @param inscriptionStatus The inscription status
378     */
379    public void setInscriptionStatus(InscriptionStatus inscriptionStatus)
380    {
381        if (inscriptionStatus != null)
382        {
383            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString());
384        }
385        else
386        {
387            removeValue(__DATA_INSCRIPTION_STATUS);
388        }
389    }
390
391    /**
392     * Get the default profile for new members of the project
393     * @return The default profile
394     */
395    public String getDefaultProfile()
396    {
397        return getValue(__DATA_DEFAULT_PROFILE);
398    }
399     
400    
401    /**
402     * Set the default profile for the members of the project
403     * @param profileId The ID of the profile
404     */
405    public void setDefaultProfile(String profileId)
406    {
407        if (profileId != null)
408        {
409            setValue(__DATA_DEFAULT_PROFILE, profileId);
410        }
411        else
412        {
413            removeValue(__DATA_DEFAULT_PROFILE);
414        }
415    }
416    
417    /**
418     * Get the root for plugins
419     * @return the root for plugins
420     * @throws AmetysRepositoryException if an error occurs.
421     */
422    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
423    {
424        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
425    }
426
427    /**
428     * Retrieve the list of tags
429     * @return The list of tags
430     * @throws AmetysRepositoryException if an error occurs
431     */
432    public Set<String> getTags() throws AmetysRepositoryException
433    {
434        return TaggableAmetysObjectHelper.getTags(this);
435    }
436    
437    /**
438     * Add a tag to the project
439     * @param tag The tag
440     * @throws AmetysRepositoryException if an error occurs
441     */
442    public void tag(String tag) throws AmetysRepositoryException
443    {
444        TaggableAmetysObjectHelper.tag(this, tag);
445    }
446
447    /**
448     * Remove a tag from the project
449     * @param tag The tag
450     * @throws AmetysRepositoryException if an error occurs
451     */
452    public void untag(String tag) throws AmetysRepositoryException
453    {
454        TaggableAmetysObjectHelper.untag(this, tag);
455    }
456
457    /**
458     * Set the project tags
459     * @param tags The list of tags
460     */
461    public void setTags(List<String> tags)
462    {
463        Set<String> currentTags = getTags();
464        // remove old tags not selected
465        currentTags.stream()
466                .filter(tag -> !tags.contains(tag))
467                .forEach(tag -> untag(tag));
468        
469        // add new selected tags
470        tags.stream()
471                .filter(tag -> !currentTags.contains(tag))
472                .forEach(tag -> tag(tag));
473    }
474
475
476    /**
477     * Retrieve the list of categories
478     * @return The categories
479     * @throws AmetysRepositoryException if an error occurs
480     */
481    public Set<String> getCategories() throws AmetysRepositoryException
482    {
483        return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES);
484    }
485    
486    /**
487     * Add a category to the project
488     * @param category The category
489     * @throws AmetysRepositoryException if an error occurs
490     */
491    public void addCategory(String category) throws AmetysRepositoryException
492    {
493        TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES);
494    }
495
496    /**
497     * Remove a category from the project
498     * @param category The category
499     * @throws AmetysRepositoryException if an error occurs
500     */
501    public void removeCategory(String category) throws AmetysRepositoryException
502    {
503        TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES);
504    }
505    
506    /**
507     * Set the category tags of the project
508     * @param categoryTags The category tags
509     */
510    public void setCategoryTags(List<String> categoryTags)
511    {
512        Set<String> currentCategories = getCategories();
513        // remove old tags not selected
514        currentCategories.stream()
515                .filter(category -> !categoryTags.contains(category))
516                .forEach(category -> removeCategory(category));
517        
518        // add new selected tags
519        categoryTags.stream()
520                .filter(category -> !currentCategories.contains(category))
521                .forEach(category -> addCategory(category));
522    }
523    
524    /**
525     * Set the cover image of the site
526     * @param is The input stream of the cover image
527     * @param mimeType The mimetype of the cover image
528     * @param filename The filename of the cover image
529     * @param lastModificationDate The last modification date of the cover image
530     * @throws IOException if an error occurs while setting the cover image
531     */
532    public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException
533    {
534        if (is != null)
535        {
536            Binary coverImage = new Binary();
537            coverImage.setInputStream(is);
538            Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType);
539            Optional.ofNullable(filename).ifPresent(coverImage::setFilename);
540            Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate);
541            setValue(__DATA_COVERIMAGE, coverImage);
542        }
543        else
544        {
545            removeValue(__DATA_COVERIMAGE);
546        }
547    }
548    
549    /**
550     * Returns the cover image of the project
551     * @return the cover image of the project
552     */
553    public Binary getCoverImage()
554    {
555        return getValue(__DATA_COVERIMAGE);
556    }
557    
558    /**
559     * Generates SAX events for the project's cover image
560     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
561     * @throws SAXException if error occurs during the SAX events generation
562     * @throws IOException if an I/O error occurs while reading the cover image
563     */
564    public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException
565    {
566        dataToSAX(contentHandler, __DATA_COVERIMAGE);
567    }
568    
569    /**
570     * Retrieve the list of keywords for the project
571     * @return The list of keywords
572     */
573    public String[] getKeywords()
574    {
575        return getValueOrDefault(__DATA_KEYWORDS, new String[0]);
576    }
577    
578    /**
579     * Set the list of keywordss for the project
580     * @param keywords The list of keywords
581     */
582    public void setKeywords(String[] keywords)
583    {
584        setValue(__DATA_KEYWORDS, keywords);
585    }
586}