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