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