001/*
002 *  Copyright 2010 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.explorer.resources.jcr;
017
018import java.io.InputStream;
019import java.util.Calendar;
020import java.util.Date;
021import java.util.GregorianCalendar;
022import java.util.List;
023import java.util.Set;
024
025import javax.jcr.Binary;
026import javax.jcr.Node;
027import javax.jcr.NodeIterator;
028import javax.jcr.RepositoryException;
029import javax.jcr.Value;
030import javax.jcr.version.VersionHistory;
031
032import org.apache.commons.lang.StringUtils;
033
034import org.ametys.core.user.UserIdentity;
035import org.ametys.plugins.explorer.ExplorerNode;
036import org.ametys.plugins.explorer.resources.ModifiableResource;
037import org.ametys.plugins.explorer.resources.Resource;
038import org.ametys.plugins.explorer.threads.Thread;
039import org.ametys.plugins.explorer.threads.jcr.JCRThread;
040import org.ametys.plugins.explorer.threads.jcr.JCRThreadFactory;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.plugins.repository.AmetysRepositoryException;
043import org.ametys.plugins.repository.CopiableAmetysObject;
044import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
045import org.ametys.plugins.repository.RepositoryConstants;
046import org.ametys.plugins.repository.dublincore.DCMITypes;
047import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
048import org.ametys.plugins.repository.jcr.DublinCoreHelper;
049import org.ametys.plugins.repository.jcr.NodeTypeHelper;
050import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
051
052/**
053 * Default implementation of an {@link Resource}, backed by a JCR node.<br>
054 * @param <F> the actual type of factory.
055 */
056public class JCRResource<F extends JCRResourceFactory> extends DefaultAmetysObject<F> implements ModifiableResource, CopiableAmetysObject
057{
058    /** The name of node holding the creator */
059    public static final String CREATOR_NODE_NAME = "creator";
060    /** Constants for lastModified Metadata */
061    public static final String CREATION_DATE = "creationDate";
062    /** The name of node holding the last contributor */
063    public static final String CONTRIBUTOR_NODE_NAME = "contributor";
064    
065    /**
066     * Creates an {@link JCRResource}.
067     * @param node the node backing this {@link AmetysObject}
068     * @param parentPath the parentPath in the Ametys hierarchy
069     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
070     */
071    public JCRResource(Node node, String parentPath, F factory)
072    {
073        super(node, parentPath, factory);
074    }
075    
076    @Override
077    public void setData(InputStream stream, String mimeType, Date lastModified, UserIdentity author)
078    {
079        Node fileNode = getNode();
080        
081        try
082        {
083            setLastContributor(author);
084            
085            Node resourceNode = null;
086            
087            if (fileNode.hasNode("jcr:content"))
088            {
089                // Already exists
090                resourceNode = fileNode.getNode("jcr:content");
091            }
092            else
093            {
094                resourceNode = fileNode.addNode("jcr:content", "nt:resource");
095                setCreator(author);
096                setCreationDate(new Date());
097            }
098            
099            GregorianCalendar gc = new GregorianCalendar();
100            gc.setTime(lastModified);
101            resourceNode.setProperty("jcr:lastModified", gc);
102            
103            resourceNode.setProperty("jcr:mimeType", mimeType);
104            
105            Binary binary = resourceNode.getSession().getValueFactory().createBinary(stream);
106            resourceNode.setProperty("jcr:data", binary);
107        }
108        catch (RepositoryException e)
109        {
110            throw new AmetysRepositoryException("Cannot set data for resource " + this.getName() + " (" + this.getId() + ")", e);
111        }
112    }
113    
114    @Override
115    public void setLastModified(Date lastModified)
116    {
117        Node fileNode = getNode();
118        
119        try
120        {
121            Node resourceNode = fileNode.getNode("jcr:content");
122            
123            GregorianCalendar gc = new GregorianCalendar();
124            gc.setTime(lastModified);
125            resourceNode.setProperty("jcr:lastModified", gc);
126        }
127        catch (RepositoryException e)
128        {
129            throw new AmetysRepositoryException("Cannot set lastmodified for resource " + this.getName() + " (" + this.getId() + ")", e);
130        }
131    }
132    
133    @Override
134    public void setKeywords(String keywords)
135    {
136        String[] words = StringUtils.stripAll(StringUtils.split(keywords, ','));
137        
138        String[] trimWords = new String[words.length];
139        for (int i = 0; i < words.length; i++)
140        {
141            trimWords[i] = words[i].trim();
142        }
143        
144        Node fileNode = getNode();
145        try
146        {
147            fileNode.setProperty("ametys:keywords", trimWords);
148        }
149        catch (RepositoryException e)
150        {
151            throw new AmetysRepositoryException("Cannot set keywords for resource " + this.getName() + " (" + this.getId() + ")", e);
152        }
153    }
154    
155    @Override
156    public void setKeywords(String[] keywords)
157    {
158        Node fileNode = getNode();
159        try
160        {
161            fileNode.setProperty("ametys:keywords", keywords);
162        }
163        catch (RepositoryException e)
164        {
165            throw new AmetysRepositoryException("Cannot set keywords for resource " + this.getName() + " (" + this.getId() + ")", e);
166        }
167    }
168    
169    @Override
170    public void setMimeType(String mimeType)
171    {
172        Node fileNode = getNode();
173        
174        try
175        {
176            Node resourceNode = fileNode.getNode("jcr:content");
177            
178            resourceNode.setProperty("jcr:mimeType", mimeType);
179        }
180        catch (RepositoryException e)
181        {
182            throw new AmetysRepositoryException("Cannot set mimetype for resource " + this.getName() + " (" + this.getId() + ")", e);
183        }
184    }
185    
186    @Override
187    public void setCreator(UserIdentity author)
188    {
189        try
190        {
191            Node authorNode = null;
192            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME))
193            {
194                authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME);
195            }
196            else
197            {
198                authorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME, RepositoryConstants.USER_NODETYPE);
199            }
200            authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin());
201            authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId());
202        }
203        catch (RepositoryException e)
204        {
205            throw new AmetysRepositoryException("Cannot set creator for resource " + this.getName() + " (" + this.getId() + ")", e);
206        }
207    }
208    
209    @Override
210    public InputStream getInputStream () throws AmetysRepositoryException
211    {
212        Node fileNode = getNode();
213        try
214        {
215            Node resourceNode = null;
216            
217            if (fileNode.hasNode("jcr:content"))
218            {
219                // Already exists
220                resourceNode = fileNode.getNode("jcr:content");
221                return resourceNode.getProperty("jcr:data").getBinary().getStream();
222            }
223            return null;
224        }
225        catch (RepositoryException e)
226        {
227            throw new AmetysRepositoryException("Cannot get inputstream for resource " + this.getName() + " (" + this.getId() + ")", e);
228        }
229    }
230    
231    @Override
232    public String getMimeType ()  throws AmetysRepositoryException
233    {
234        Node fileNode = getNode();
235        try
236        {
237            Node resourceNode = null;
238            
239            if (fileNode.hasNode("jcr:content"))
240            {
241                // Already exists
242                resourceNode = fileNode.getNode("jcr:content");
243                return resourceNode.getProperty("jcr:mimeType").getString();
244            }
245            
246            return null;
247        }
248        catch (RepositoryException e)
249        {
250            throw new AmetysRepositoryException("Cannot get mimetype for resource " + this.getName() + " (" + this.getId() + ")", e);
251        }
252    }
253    
254    @Override
255    public Date getLastModified () throws AmetysRepositoryException
256    {
257        Node fileNode = getNode();
258        try
259        {
260            Node resourceNode = null;
261            
262            if (fileNode.hasNode("jcr:content"))
263            {
264                resourceNode = fileNode.getNode("jcr:content");
265                return resourceNode.getProperty("jcr:lastModified").getDate().getTime();
266            }
267            
268            return null;
269        }
270        catch (RepositoryException e)
271        {
272            throw new AmetysRepositoryException("Cannot get lastmodified for resource " + this.getName() + " (" + this.getId() + ")", e);
273        }
274    }
275    
276    @Override
277    public long getLength() throws AmetysRepositoryException
278    {
279        Node fileNode = getNode();
280        try
281        {
282            Node resourceNode = null;
283            
284            if (fileNode.hasNode("jcr:content"))
285            {
286                resourceNode = fileNode.getNode("jcr:content");
287                return resourceNode.getProperty("jcr:data").getLength();
288            }
289            
290            return 0;
291        }
292        catch (RepositoryException e)
293        {
294            throw new AmetysRepositoryException("Cannot get length for resource " + this.getName() + " (" + this.getId() + ")", e);
295        }
296    }
297    
298    @Override
299    public UserIdentity getCreator() throws AmetysRepositoryException
300    {
301        try
302        {
303            Node authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME);
304            return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
305        }
306        catch (RepositoryException e)
307        {
308            throw new AmetysRepositoryException("Cannot get creator for resource " + this.getName() + " (" + this.getId() + ")", e);
309        }
310    }
311    
312    /**
313     * Get the author from old revision
314     * @param revision The revision
315     * @return The user identity of the author or <code>null</code> if not found
316     * @throws RepositoryException If an error occurred
317     */
318    public UserIdentity getAuthorFromRevision (String revision) throws RepositoryException
319    {
320        try
321        {
322            switchToRevision(revision);
323            
324            VersionHistory history = getVersionHistory();
325            Node versionNode = history.getVersion(revision);
326            Node frozenNode = versionNode.getNode("jcr:frozenNode");
327            
328            if (frozenNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME))
329            {
330                Node authorNode = frozenNode.getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME);
331                return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
332            }
333            return null;
334        }
335        catch (RepositoryException e)
336        {
337            throw new AmetysRepositoryException("Unable to get author from revision: " + revision + " of resource " + this.getName() + " (" + this.getId() + ")", e);
338        }
339    }
340    
341    @Override
342    public String[] getKeywords() throws AmetysRepositoryException
343    {
344        Node fileNode = getNode();
345        try
346        {
347            if (!fileNode.hasProperty("ametys:keywords"))
348            {
349                return new String[0];
350            }
351            
352            Value[] values = fileNode.getProperty("ametys:keywords").getValues();
353            String[] result = new String[values.length];
354            
355            for (int i = 0; i < values.length; i++)
356            {
357                result[i] = values[i].getString();
358            }
359            
360            return result;
361        }
362        catch (RepositoryException e)
363        {
364            throw new AmetysRepositoryException("Cannot get keywords for resource " + this.getName() + " (" + this.getId() + ")", e);
365        }
366    }
367    
368    @Override
369    public String getKeywordsAsString() throws AmetysRepositoryException
370    {
371        Node fileNode = getNode();
372        try
373        {
374            if (!fileNode.hasProperty("ametys:keywords"))
375            {
376                return StringUtils.EMPTY;
377            }
378            
379            StringBuilder sb = new StringBuilder();
380            Value[] values = fileNode.getProperty("ametys:keywords").getValues();
381            
382            for (Value value : values)
383            {
384                if (sb.length() > 0)
385                {
386                    sb.append(", ");
387                }
388                
389                sb.append(value.getString());
390            }
391            
392            return sb.toString();
393        }
394        catch (RepositoryException e)
395        {
396            throw new AmetysRepositoryException("Cannot get keywords for resource " + this.getName() + " (" + this.getId() + ")", e);
397        }
398    }
399    
400    @Override
401    protected void restoreFromNode(Node node) throws RepositoryException
402    {
403        super.restoreFromNode(node);
404        
405        // First remove node
406        NodeIterator nit = getBaseNode().getNodes("jcr:content");
407        while (nit.hasNext())
408        {
409            nit.nextNode().remove();
410        }
411        
412        NodeIterator new_nit = node.getNodes("jcr:content");
413        while (new_nit.hasNext())
414        {
415            copyNode(getBaseNode(), new_nit.nextNode());
416        }
417    }
418    
419    @Override
420    public String getResourcePath() throws AmetysRepositoryException
421    {
422        return ((ExplorerNode) getParent()).getExplorerPath() + "/" + getName();
423    }
424    
425    @Override
426    public void lock() throws AmetysRepositoryException
427    {
428        _getFactory().getLockComponent().lock(this);
429    }
430
431    @Override
432    public void unlock() throws AmetysRepositoryException
433    {
434        _getFactory().getLockComponent().unlock(this);
435    }
436
437    @Override
438    public boolean isLocked() throws AmetysRepositoryException
439    {
440        return _getFactory().getLockComponent().isLocked(this);
441    }
442
443    @Override
444    public UserIdentity getLockOwner() throws AmetysRepositoryException
445    {
446        return _getFactory().getLockComponent().getLockOwner(this);
447    }
448
449    @Override
450    public Thread getComments(boolean createThread)
451    {
452        try
453        {
454            Node node = getNode();
455            if (!node.hasNode("ametys:comments"))
456            {
457                if (createThread)
458                {
459                    Node commentsRootNode = node.addNode("ametys:comments", JCRThreadFactory.THREAD_NODETYPE);
460                    
461                    JCRThread thread = _getFactory().getResolver().resolve(commentsRootNode, false);
462                    thread.setTitle(getName());
463                    thread.setDescription("");
464                    thread.setAuthor(getCreator());
465                    thread.setCreationDate(new Date());
466                    
467                    return thread;
468                }
469                
470                return null;
471            }
472            else
473            {
474                Node commentsRootNode = node.getNode("ametys:comments");
475                return _getFactory().getResolver().resolve(commentsRootNode, false);
476            }
477        }
478        catch (RepositoryException e)
479        {
480            throw new AmetysRepositoryException("Cannot get comments for resource " + this.getName() + " (" + this.getId() + ")", e);
481        }
482    }
483    
484    @Override
485    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
486    {
487        try
488        {
489            String nodeTypeName = NodeTypeHelper.getNodeTypeName(getNode());
490            
491            JCRResource copiedResource = parent.createChild(name, nodeTypeName);
492            
493            copiedResource.setKeywords(getKeywords());
494            copiedResource.setData(getInputStream(), getMimeType(), getLastModified(), getCreator());
495            
496            parent.saveChanges();
497            
498            return copiedResource;
499        }
500        catch (RepositoryException e)
501        {
502            throw new AmetysRepositoryException("Error copying the collection " + getId() + " into " + parent.getId(), e);
503        }
504    }
505    
506    @Override
507    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
508    {
509        return copyTo(parent, name);
510    }
511    
512    // Dublin Core metadata. //
513    
514    @Override
515    public String getDCTitle() throws AmetysRepositoryException
516    {
517        return DublinCoreHelper.getDCTitle(this, getName());
518    }
519    
520    @Override
521    public void setDCTitle(String title) throws AmetysRepositoryException
522    {
523        DublinCoreHelper.setDCTitle(this, title);
524    }
525    
526    @Override
527    public String getDCCreator() throws AmetysRepositoryException
528    {
529        return DublinCoreHelper.getDCCreator(this);
530    }
531    
532    @Override
533    public void setDCCreator(String creator) throws AmetysRepositoryException
534    {
535        DublinCoreHelper.setDCCreator(this, creator);
536    }
537    
538    @Override
539    public String[] getDCSubject() throws AmetysRepositoryException
540    {
541        return DublinCoreHelper.getDCSubject(this, getKeywords());
542    }
543    
544    @Override
545    public void setDCSubject(String[] subject) throws AmetysRepositoryException
546    {
547        DublinCoreHelper.setDCSubject(this, subject);
548    }
549    
550    @Override
551    public String getDCDescription() throws AmetysRepositoryException
552    {
553        return DublinCoreHelper.getDCDescription(this);
554    }
555    
556    @Override
557    public void setDCDescription(String description) throws AmetysRepositoryException
558    {
559        DublinCoreHelper.setDCDescription(this, description);
560    }
561    
562    @Override
563    public String getDCPublisher() throws AmetysRepositoryException
564    {
565        return DublinCoreHelper.getDCPublisher(this);
566    }
567    
568    @Override
569    public void setDCPublisher(String publisher) throws AmetysRepositoryException
570    {
571        DublinCoreHelper.setDCPublisher(this, publisher);
572    }
573    
574    @Override
575    public String getDCContributor() throws AmetysRepositoryException
576    {
577        return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getCreator()));
578    }
579    
580    @Override
581    public void setDCContributor(String contributor) throws AmetysRepositoryException
582    {
583        DublinCoreHelper.setDCContributor(this, contributor);
584    }
585    
586    @Override
587    public Date getDCDate() throws AmetysRepositoryException
588    {
589        return DublinCoreHelper.getDCDate(this, getLastModified());
590    }
591    
592    @Override
593    public void setDCDate(Date date) throws AmetysRepositoryException
594    {
595        DublinCoreHelper.setDCDate(this, date);
596    }
597    
598    @Override
599    public String getDCType() throws AmetysRepositoryException
600    {
601        return DublinCoreHelper.getDCType(this, _getDefaultDCType());
602    }
603    
604    private String _getDefaultDCType ()
605    {
606        String mimetype = getMimeType();
607        
608        if (mimetype == null)
609        {
610            return DCMITypes.TEXT;
611        }
612        else if (mimetype.startsWith("image"))
613        {
614            return DCMITypes.IMAGE;
615        }
616        else if (mimetype.startsWith("video") || "application/x-shockwave-flash".equals(mimetype))
617        {
618            return DCMITypes.INTERACTIVERESOURCE;
619        }
620        else if (mimetype.startsWith("audio"))
621        {
622            return DCMITypes.SOUND;
623        }
624        
625        return DCMITypes.TEXT;
626    }
627    
628    
629    @Override
630    public void setDCType(String type) throws AmetysRepositoryException
631    {
632        DublinCoreHelper.setDCType(this, type);
633    }
634    
635    @Override
636    public String getDCFormat() throws AmetysRepositoryException
637    {
638        return DublinCoreHelper.getDCFormat(this, getMimeType());
639    }
640    
641    @Override
642    public void setDCFormat(String format) throws AmetysRepositoryException
643    {
644        DublinCoreHelper.setDCFormat(this, format);
645    }
646    
647    @Override
648    public String getDCIdentifier() throws AmetysRepositoryException
649    {
650        return DublinCoreHelper.getDCIdentifier(this, getId());
651    }
652    
653    @Override
654    public void setDCIdentifier(String identifier) throws AmetysRepositoryException
655    {
656        DublinCoreHelper.setDCIdentifier(this, identifier);
657    }
658    
659    @Override
660    public String getDCSource() throws AmetysRepositoryException
661    {
662        return DublinCoreHelper.getDCSource(this);
663    }
664    
665    @Override
666    public void setDCSource(String source) throws AmetysRepositoryException
667    {
668        DublinCoreHelper.setDCSource(this, source);
669    }
670    
671    @Override
672    public String getDCLanguage() throws AmetysRepositoryException
673    {
674        return DublinCoreHelper.getDCLanguage(this);
675    }
676    
677    @Override
678    public void setDCLanguage(String language) throws AmetysRepositoryException
679    {
680        DublinCoreHelper.setDCLanguage(this, language);
681    }
682    
683    @Override
684    public String getDCRelation() throws AmetysRepositoryException
685    {
686        return DublinCoreHelper.getDCRelation(this);
687    }
688    
689    @Override
690    public void setDCRelation(String relation) throws AmetysRepositoryException
691    {
692        DublinCoreHelper.setDCRelation(this, relation);
693    }
694    
695    @Override
696    public String getDCCoverage() throws AmetysRepositoryException
697    {
698        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
699    }
700    
701    @Override
702    public void setDCCoverage(String coverage) throws AmetysRepositoryException
703    {
704        DublinCoreHelper.setDCCoverage(this, coverage);
705    }
706    
707    @Override
708    public String getDCRights() throws AmetysRepositoryException
709    {
710        return DublinCoreHelper.getDCRights(this);
711    }
712    
713    @Override
714    public void setDCRights(String rights) throws AmetysRepositoryException
715    {
716        DublinCoreHelper.setDCRights(this, rights);
717    }
718
719    public Date getCreationDate() throws AmetysRepositoryException
720    {
721        Node fileNode = getNode();
722        try
723        {
724            if (!fileNode.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE))
725            {
726                return null;
727            }
728            
729            return fileNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE).getDate().getTime();
730        }
731        catch (RepositoryException e)
732        {
733            throw new AmetysRepositoryException("Cannot get creation date for resource " + this.getName() + " (" + this.getId() + ")", e);
734        }
735    }
736
737    public void setCreationDate(Date creationDate)
738    {
739        Node fileNode = getNode();
740        try
741        {
742            Calendar calendar = new GregorianCalendar();
743            calendar.setTime(creationDate);
744            fileNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE, calendar);
745        }
746        catch (RepositoryException e)
747        {
748            throw new AmetysRepositoryException("Cannot set create date for resource " + this.getName() + " (" + this.getId() + ")", e);
749        }
750    }
751
752    public UserIdentity getLastContributor() throws AmetysRepositoryException
753    {
754        try
755        {
756            Node authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME);
757            return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
758        }
759        catch (RepositoryException e)
760        {
761            throw new AmetysRepositoryException("Cannot get last contributor for resource " + this.getName() + " (" + this.getId() + ")", e);
762        }
763    }
764
765    public void setLastContributor(UserIdentity lastContributor)
766    {
767        try
768        {
769            Node lastContributorNode = null;
770            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME))
771            {
772                lastContributorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME);
773            }
774            else
775            {
776                lastContributorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME, RepositoryConstants.USER_NODETYPE);
777            }
778            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", lastContributor.getLogin());
779            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", lastContributor.getPopulationId());
780        }
781        catch (RepositoryException e)
782        {
783            throw new AmetysRepositoryException("Cannot set last contributor for resource " + this.getName() + " (" + this.getId() + ")", e);
784        }
785    }
786    
787    public void tag(String tag) throws AmetysRepositoryException
788    {
789        TaggableAmetysObjectHelper.tag(this, tag);
790    }
791
792    public void untag(String tag) throws AmetysRepositoryException
793    {
794        TaggableAmetysObjectHelper.untag(this, tag);
795    }
796
797    public Set<String> getTags() throws AmetysRepositoryException
798    {
799        return TaggableAmetysObjectHelper.getTags(this);
800    }
801}