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.NodeIterator;
028import javax.jcr.PathNotFoundException;
029import javax.jcr.Property;
030import javax.jcr.RepositoryException;
031import javax.jcr.Value;
032import javax.jcr.ValueFactory;
033
034import org.apache.commons.lang3.ArrayUtils;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.data.Binary;
039import org.ametys.cms.indexing.solr.SolrAclCacheUninfluentialObject;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.plugins.explorer.ExplorerNode;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.plugins.repository.AmetysObjectIterable;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
046import org.ametys.plugins.repository.RepositoryConstants;
047import org.ametys.plugins.repository.TraversableAmetysObject;
048import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject;
049import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
050import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
051import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
052import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
053import org.ametys.plugins.repository.events.EventHolder;
054import org.ametys.plugins.repository.events.JCREventHelper;
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, EventHolder
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    /**
148     * Retrieves the title.
149     * @return the title.
150     * @throws AmetysRepositoryException if an error occurs.
151     */
152    public String getTitle() throws AmetysRepositoryException
153    {
154        return getValue(__DATA_TITLE);
155    }
156    
157    /**
158     * Set the title.
159     * @param title the title.
160     * @throws AmetysRepositoryException if an error occurs.
161     */
162    public void setTitle(String title) throws AmetysRepositoryException
163    {
164        setValue(__DATA_TITLE, title);
165    }
166    
167    /**
168     * Retrieves the description.
169     * @return the description.
170     * @throws AmetysRepositoryException if an error occurs.
171     */
172    public String getDescription() throws AmetysRepositoryException
173    {
174        return getValue(__DATA_DESCRIPTION);
175    }
176    
177    /**
178     * Set the description.
179     * @param description the description.
180     * @throws AmetysRepositoryException if an error occurs.
181     */
182    public void setDescription(String description) throws AmetysRepositoryException
183    {
184        setValue(__DATA_DESCRIPTION, description);
185    }
186    
187    /**
188     * Remove the description.
189     * @throws AmetysRepositoryException if an error occurs.
190     */
191    public void removeDescription() throws AmetysRepositoryException
192    {
193        removeValue(__DATA_DESCRIPTION);
194    }
195    
196    /**
197     * Retrieves the explorer nodes.
198     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
199     * @throws AmetysRepositoryException if an error occurs.
200     */
201    public ExplorerNode getExplorerRootNode() throws AmetysRepositoryException
202    {
203        return (ExplorerNode) getChild(__EXPLORER_NODE_NAME);
204    }
205    
206    /**
207     * Retrieves the explorer nodes.
208     * @return the explorer nodes or an empty {@link AmetysObjectIterable}.
209     * @throws AmetysRepositoryException if an error occurs.
210     */
211    public AmetysObjectIterable<ExplorerNode> getExplorerNodes() throws AmetysRepositoryException
212    {
213        return ((TraversableAmetysObject) getChild(__EXPLORER_NODE_NAME)).getChildren();
214    }
215    
216    
217    /**
218     * Retrieves the mailing list.
219     * @return the mailing list.
220     * @throws AmetysRepositoryException if an error occurs.
221     */
222    public String getMailingList() throws AmetysRepositoryException
223    {
224        return getValue(__DATA_MAILING_LIST);
225    }
226    
227    /**
228     * Set the mailing list.
229     * @param mailingList the mailing list.
230     * @throws AmetysRepositoryException if an error occurs.
231     */
232    public void setMailingList(String mailingList) throws AmetysRepositoryException
233    {
234        setValue(__DATA_MAILING_LIST, mailingList);
235    }
236    
237    /**
238     * Remove the mailing list.
239     * @throws AmetysRepositoryException if an error occurs.
240     */
241    public void removeMailingList() throws AmetysRepositoryException
242    {
243        removeValue(__DATA_MAILING_LIST);
244    }
245    
246    /**
247     * Retrieves the date of creation.
248     * @return the date of creation.
249     * @throws AmetysRepositoryException if an error occurs.
250     */
251    public ZonedDateTime getCreationDate() throws AmetysRepositoryException
252    {
253        return getValue(__DATA_CREATION);
254    }
255    
256    /**
257     * Set the date of creation.
258     * @param creationDate the date of creation
259     * @throws AmetysRepositoryException if an error occurs.
260     */
261    public void setCreationDate(ZonedDateTime creationDate) throws AmetysRepositoryException
262    {
263        setValue(__DATA_CREATION, creationDate);
264    }
265    
266    @Override
267    public Node getEventsRootNode() throws RepositoryException
268    {
269        return JCREventHelper.getEventsRootNode(getNode());
270    }
271    
272    @Override
273    public NodeIterator getEvents() throws RepositoryException
274    {
275        return JCREventHelper.getEvents(this);
276    }
277
278    /**
279     * Get the site of the project
280     * @return The site. Can be null if the reference is broken.
281     */
282    public Site getSite()
283    {
284        try
285        {
286            SiteManager siteManager = _getFactory()._getSiteManager();
287            Property weakReference = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE);
288            String siteName = weakReference.getNode().getName();
289            return siteManager.getSite(siteName);
290        }
291        catch (PathNotFoundException e)
292        {
293            _getFactory().getFactoryLogger().debug("Can not found site attribute for project " + getName(), e);
294            return null;
295        }
296        catch (ItemNotFoundException e)
297        {
298            _getFactory().getFactoryLogger().debug("Can not found site reference for project " + getName(), e);
299            return null;
300        }
301        catch (RepositoryException e)
302        {
303            throw new AmetysRepositoryException("Unexpected repository exception", e);
304        }
305    }
306
307    /**
308     * Get the site of the project
309     * @param site the site to set
310     */
311    public void setSite(Site site)
312    {
313        try
314        {
315            Node projectNode = getNode();
316            if (projectNode.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE))
317            {
318                removeValue(DATA_SITE);
319            }
320            ValueFactory valueFactory = projectNode.getSession().getValueFactory();
321            Value weakRefValue = valueFactory.createValue(site.getNode(), true);
322            projectNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + DATA_SITE, weakRefValue);
323        }
324        catch (RepositoryException e)
325        {
326            throw new AmetysRepositoryException("Unexpected repository exception", e);
327        }
328        
329        if (needsSave())
330        {
331            saveChanges();
332        }
333    }
334        
335    /**
336     * Get the project managers user identities
337     * @return The managers
338     */
339    public UserIdentity[] getManagers()
340    {
341        return getValue(__DATA_MANAGERS, new UserIdentity[0]);
342    }
343    
344    /**
345     * Set the project managers
346     * @param user The managers
347     */
348    public void setManagers(UserIdentity[] user)
349    {
350        setValue(__DATA_MANAGERS, user);
351    }
352    
353    /**
354     * Retrieve the list of activated modules for the project
355     * @return The list of modules ids
356     */
357    public String[] getModules()
358    {
359        return getValue(__DATA_MODULES, new String[0]);
360    }
361    
362    /**
363     * Set the list of activated modules for the project
364     * @param modules The list of modules
365     */
366    public void setModules(String[] modules)
367    {
368        setValue(__DATA_MODULES, modules);
369    }
370    
371    /**
372     * Add a module to the list of activated modules
373     * @param moduleId The module id
374     */
375    public void addModule(String moduleId)
376    {
377        String[] modules = getValue(__DATA_MODULES);
378        if (!ArrayUtils.contains(modules, moduleId))
379        {
380            setValue(__DATA_MODULES, modules == null ? new String[]{moduleId} : ArrayUtils.add(modules, moduleId));
381        }
382    }
383    
384    /**
385     * Remove a module from the list of activated modules
386     * @param moduleId The module id
387     */
388    public void removeModule(String moduleId)
389    {
390        String[] modules = getValue(__DATA_MODULES);
391        if (ArrayUtils.contains(modules, moduleId))
392        {
393            setValue(__DATA_MODULES, ArrayUtils.removeElement(modules, moduleId));
394        }
395    }
396    
397    /**
398     * Get the inscription status of the project
399     * @return The inscription status
400     */
401    public InscriptionStatus getInscriptionStatus()
402    {
403        if (hasValue(__DATA_INSCRIPTION_STATUS))
404        {
405            return InscriptionStatus.createsFromString(getValue(__DATA_INSCRIPTION_STATUS));
406        }
407        return InscriptionStatus.PRIVATE;
408    }
409    
410    /**
411     * Set the inscription status of the project
412     * @param inscriptionStatus The inscription status
413     */
414    public void setInscriptionStatus(String inscriptionStatus)
415    {
416        if (inscriptionStatus != null && InscriptionStatus.createsFromString(inscriptionStatus) != null)
417        {
418            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus);
419        }
420        else
421        {
422            removeValue(__DATA_INSCRIPTION_STATUS);
423        }
424    }
425
426    /**
427     * Set the inscription status of the project
428     * @param inscriptionStatus The inscription status
429     */
430    public void setInscriptionStatus(InscriptionStatus inscriptionStatus)
431    {
432        if (inscriptionStatus != null)
433        {
434            setValue(__DATA_INSCRIPTION_STATUS, inscriptionStatus.toString());
435        }
436        else
437        {
438            removeValue(__DATA_INSCRIPTION_STATUS);
439        }
440    }
441
442    /**
443     * Get the default profile for new members of the project
444     * @return The default profile
445     */
446    public String getDefaultProfile()
447    {
448        return getValue(__DATA_DEFAULT_PROFILE);
449    }
450     
451    
452    /**
453     * Set the default profile for the members of the project
454     * @param profileId The ID of the profile
455     */
456    public void setDefaultProfile(String profileId)
457    {
458        if (profileId != null)
459        {
460            setValue(__DATA_DEFAULT_PROFILE, profileId);
461        }
462        else
463        {
464            removeValue(__DATA_DEFAULT_PROFILE);
465        }
466    }
467    
468    /**
469     * Get the root for plugins
470     * @return the root for plugins
471     * @throws AmetysRepositoryException if an error occurs.
472     */
473    public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException
474    {
475        return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME);
476    }
477
478    /**
479     * Retrieve the list of tags 
480     * @return The list of tags
481     * @throws AmetysRepositoryException if an error occurs
482     */
483    public Set<String> getTags() throws AmetysRepositoryException
484    {
485        return TaggableAmetysObjectHelper.getTags(this);
486    }
487    
488    /**
489     * Add a tag to the project
490     * @param tag The tag
491     * @throws AmetysRepositoryException if an error occurs
492     */
493    public void tag(String tag) throws AmetysRepositoryException
494    {
495        TaggableAmetysObjectHelper.tag(this, tag);
496    }
497
498    /**
499     * Remove a tag from the project
500     * @param tag The tag
501     * @throws AmetysRepositoryException if an error occurs
502     */
503    public void untag(String tag) throws AmetysRepositoryException
504    {
505        TaggableAmetysObjectHelper.untag(this, tag);
506    }
507
508    /**
509     * Set the project tags
510     * @param tags The list of tags
511     */
512    public void setTags(List<String> tags)
513    {
514        Set<String> currentTags = getTags();
515        // remove old tags not selected
516        currentTags.stream()
517                .filter(tag -> !tags.contains(tag))
518                .forEach(tag -> untag(tag));
519        
520        // add new selected tags
521        tags.stream()
522                .filter(tag -> !currentTags.contains(tag))
523                .forEach(tag -> tag(tag));
524    }
525
526
527    /**
528     * Retrieve the list of categories
529     * @return The categories
530     * @throws AmetysRepositoryException if an error occurs
531     */
532    public Set<String> getCategories() throws AmetysRepositoryException
533    {
534        return TaggableAmetysObjectHelper.getTags(this, __DATA_CATEGORIES);
535    }
536    
537    /**
538     * Add a category to the project
539     * @param category The category
540     * @throws AmetysRepositoryException if an error occurs
541     */
542    public void addCategory(String category) throws AmetysRepositoryException
543    {
544        TaggableAmetysObjectHelper.tag(this, category, __DATA_CATEGORIES);
545    }
546
547    /**
548     * Remove a category from the project
549     * @param category The category
550     * @throws AmetysRepositoryException if an error occurs
551     */
552    public void removeCategory(String category) throws AmetysRepositoryException
553    {
554        TaggableAmetysObjectHelper.untag(this, category, __DATA_CATEGORIES);
555    }
556    
557    /**
558     * Set the category tags of the project
559     * @param categoryTags The category tags
560     */
561    public void setCategoryTags(List<String> categoryTags)
562    {
563        Set<String> currentCategories = getCategories();
564        // remove old tags not selected
565        currentCategories.stream()
566                .filter(category -> !categoryTags.contains(category))
567                .forEach(category -> removeCategory(category));
568        
569        // add new selected tags
570        categoryTags.stream()
571                .filter(category -> !currentCategories.contains(category))
572                .forEach(category -> addCategory(category));
573    }
574    
575    /**
576     * Set the cover image of the site
577     * @param is The input stream of the cover image
578     * @param mimeType The mimetype of the cover image
579     * @param filename The filename of the cover image
580     * @param lastModificationDate The last modification date of the cover image
581     * @throws IOException if an error occurs while setting the cover image
582     */
583    public void setCoverImage(InputStream is, String mimeType, String filename, ZonedDateTime lastModificationDate) throws IOException
584    {
585        if (is != null)
586        {
587            Binary coverImage = new Binary();
588            coverImage.setInputStream(is);
589            Optional.ofNullable(mimeType).ifPresent(coverImage::setMimeType);
590            Optional.ofNullable(filename).ifPresent(coverImage::setFilename);
591            Optional.ofNullable(lastModificationDate).ifPresent(coverImage::setLastModificationDate);
592            setValue(__DATA_COVERIMAGE, coverImage);
593        }
594        else
595        {
596            removeValue(__DATA_COVERIMAGE);
597        }
598    }
599    
600    /**
601     * Returns the cover image of the project
602     * @return the cover image of the project
603     */
604    public Binary getCoverImage()
605    {
606        return getValue(__DATA_COVERIMAGE);
607    }
608    
609    /**
610     * Generates SAX events for the project's cover image
611     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
612     * @throws SAXException if error occurs during the SAX events generation
613     * @throws IOException if an I/O error occurs while reading the cover image
614     */
615    public void coverImageToSAX(ContentHandler contentHandler) throws SAXException, IOException
616    {
617        dataToSAX(contentHandler, __DATA_COVERIMAGE);
618    }
619    
620    /**
621     * Retrieve the list of keywords for the project
622     * @return The list of keywords
623     */
624    public String[] getKeywords()
625    {
626        return getValue(__DATA_KEYWORDS, new String[0]);
627    }
628    
629    /**
630     * Set the list of keywordss for the project
631     * @param keywords The list of keywords
632     */
633    public void setKeywords(String[] keywords)
634    {
635        setValue(__DATA_KEYWORDS, keywords);
636    }
637}