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.DefaultLockableAmetysObject;
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 DefaultLockableAmetysObject<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 Thread getComments(boolean createThread)
427    {
428        try
429        {
430            Node node = getNode();
431            if (!node.hasNode("ametys:comments"))
432            {
433                if (createThread)
434                {
435                    Node commentsRootNode = node.addNode("ametys:comments", JCRThreadFactory.THREAD_NODETYPE);
436                    
437                    JCRThread thread = _getFactory().getResolver().resolve(commentsRootNode, false);
438                    thread.setTitle(getName());
439                    thread.setDescription("");
440                    thread.setAuthor(getCreator());
441                    thread.setCreationDate(new Date());
442                    
443                    return thread;
444                }
445                
446                return null;
447            }
448            else
449            {
450                Node commentsRootNode = node.getNode("ametys:comments");
451                return _getFactory().getResolver().resolve(commentsRootNode, false);
452            }
453        }
454        catch (RepositoryException e)
455        {
456            throw new AmetysRepositoryException("Cannot get comments for resource " + this.getName() + " (" + this.getId() + ")", e);
457        }
458    }
459    
460    @Override
461    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
462    {
463        try
464        {
465            String nodeTypeName = NodeTypeHelper.getNodeTypeName(getNode());
466            
467            JCRResource copiedResource = parent.createChild(name, nodeTypeName);
468            
469            copiedResource.setKeywords(getKeywords());
470            copiedResource.setData(getInputStream(), getMimeType(), getLastModified(), getCreator());
471            
472            parent.saveChanges();
473            
474            return copiedResource;
475        }
476        catch (RepositoryException e)
477        {
478            throw new AmetysRepositoryException("Error copying the collection " + getId() + " into " + parent.getId(), e);
479        }
480    }
481    
482    @Override
483    public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
484    {
485        return copyTo(parent, name);
486    }
487    
488    // Dublin Core metadata. //
489    
490    @Override
491    public String getDCTitle() throws AmetysRepositoryException
492    {
493        return DublinCoreHelper.getDCTitle(this, getName());
494    }
495    
496    @Override
497    public void setDCTitle(String title) throws AmetysRepositoryException
498    {
499        DublinCoreHelper.setDCTitle(this, title);
500    }
501    
502    @Override
503    public String getDCCreator() throws AmetysRepositoryException
504    {
505        return DublinCoreHelper.getDCCreator(this);
506    }
507    
508    @Override
509    public void setDCCreator(String creator) throws AmetysRepositoryException
510    {
511        DublinCoreHelper.setDCCreator(this, creator);
512    }
513    
514    @Override
515    public String[] getDCSubject() throws AmetysRepositoryException
516    {
517        return DublinCoreHelper.getDCSubject(this, getKeywords());
518    }
519    
520    @Override
521    public void setDCSubject(String[] subject) throws AmetysRepositoryException
522    {
523        DublinCoreHelper.setDCSubject(this, subject);
524    }
525    
526    @Override
527    public String getDCDescription() throws AmetysRepositoryException
528    {
529        return DublinCoreHelper.getDCDescription(this);
530    }
531    
532    @Override
533    public void setDCDescription(String description) throws AmetysRepositoryException
534    {
535        DublinCoreHelper.setDCDescription(this, description);
536    }
537    
538    @Override
539    public String getDCPublisher() throws AmetysRepositoryException
540    {
541        return DublinCoreHelper.getDCPublisher(this);
542    }
543    
544    @Override
545    public void setDCPublisher(String publisher) throws AmetysRepositoryException
546    {
547        DublinCoreHelper.setDCPublisher(this, publisher);
548    }
549    
550    @Override
551    public String getDCContributor() throws AmetysRepositoryException
552    {
553        return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getCreator()));
554    }
555    
556    @Override
557    public void setDCContributor(String contributor) throws AmetysRepositoryException
558    {
559        DublinCoreHelper.setDCContributor(this, contributor);
560    }
561    
562    @Override
563    public Date getDCDate() throws AmetysRepositoryException
564    {
565        return DublinCoreHelper.getDCDate(this, getLastModified());
566    }
567    
568    @Override
569    public void setDCDate(Date date) throws AmetysRepositoryException
570    {
571        DublinCoreHelper.setDCDate(this, date);
572    }
573    
574    @Override
575    public String getDCType() throws AmetysRepositoryException
576    {
577        return DublinCoreHelper.getDCType(this, _getDefaultDCType());
578    }
579    
580    private String _getDefaultDCType ()
581    {
582        String mimetype = getMimeType();
583        
584        if (mimetype == null)
585        {
586            return DCMITypes.TEXT;
587        }
588        else if (mimetype.startsWith("image"))
589        {
590            return DCMITypes.IMAGE;
591        }
592        else if (mimetype.startsWith("video") || "application/x-shockwave-flash".equals(mimetype))
593        {
594            return DCMITypes.INTERACTIVERESOURCE;
595        }
596        else if (mimetype.startsWith("audio"))
597        {
598            return DCMITypes.SOUND;
599        }
600        
601        return DCMITypes.TEXT;
602    }
603    
604    
605    @Override
606    public void setDCType(String type) throws AmetysRepositoryException
607    {
608        DublinCoreHelper.setDCType(this, type);
609    }
610    
611    @Override
612    public String getDCFormat() throws AmetysRepositoryException
613    {
614        return DublinCoreHelper.getDCFormat(this, getMimeType());
615    }
616    
617    @Override
618    public void setDCFormat(String format) throws AmetysRepositoryException
619    {
620        DublinCoreHelper.setDCFormat(this, format);
621    }
622    
623    @Override
624    public String getDCIdentifier() throws AmetysRepositoryException
625    {
626        return DublinCoreHelper.getDCIdentifier(this, getId());
627    }
628    
629    @Override
630    public void setDCIdentifier(String identifier) throws AmetysRepositoryException
631    {
632        DublinCoreHelper.setDCIdentifier(this, identifier);
633    }
634    
635    @Override
636    public String getDCSource() throws AmetysRepositoryException
637    {
638        return DublinCoreHelper.getDCSource(this);
639    }
640    
641    @Override
642    public void setDCSource(String source) throws AmetysRepositoryException
643    {
644        DublinCoreHelper.setDCSource(this, source);
645    }
646    
647    @Override
648    public String getDCLanguage() throws AmetysRepositoryException
649    {
650        return DublinCoreHelper.getDCLanguage(this);
651    }
652    
653    @Override
654    public void setDCLanguage(String language) throws AmetysRepositoryException
655    {
656        DublinCoreHelper.setDCLanguage(this, language);
657    }
658    
659    @Override
660    public String getDCRelation() throws AmetysRepositoryException
661    {
662        return DublinCoreHelper.getDCRelation(this);
663    }
664    
665    @Override
666    public void setDCRelation(String relation) throws AmetysRepositoryException
667    {
668        DublinCoreHelper.setDCRelation(this, relation);
669    }
670    
671    @Override
672    public String getDCCoverage() throws AmetysRepositoryException
673    {
674        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
675    }
676    
677    @Override
678    public void setDCCoverage(String coverage) throws AmetysRepositoryException
679    {
680        DublinCoreHelper.setDCCoverage(this, coverage);
681    }
682    
683    @Override
684    public String getDCRights() throws AmetysRepositoryException
685    {
686        return DublinCoreHelper.getDCRights(this);
687    }
688    
689    @Override
690    public void setDCRights(String rights) throws AmetysRepositoryException
691    {
692        DublinCoreHelper.setDCRights(this, rights);
693    }
694
695    public Date getCreationDate() throws AmetysRepositoryException
696    {
697        Node fileNode = getNode();
698        try
699        {
700            if (!fileNode.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE))
701            {
702                return null;
703            }
704            
705            return fileNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE).getDate().getTime();
706        }
707        catch (RepositoryException e)
708        {
709            throw new AmetysRepositoryException("Cannot get creation date for resource " + this.getName() + " (" + this.getId() + ")", e);
710        }
711    }
712
713    public void setCreationDate(Date creationDate)
714    {
715        Node fileNode = getNode();
716        try
717        {
718            Calendar calendar = new GregorianCalendar();
719            calendar.setTime(creationDate);
720            fileNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE, calendar);
721        }
722        catch (RepositoryException e)
723        {
724            throw new AmetysRepositoryException("Cannot set create date for resource " + this.getName() + " (" + this.getId() + ")", e);
725        }
726    }
727
728    public UserIdentity getLastContributor() throws AmetysRepositoryException
729    {
730        try
731        {
732            Node authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME);
733            return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
734        }
735        catch (RepositoryException e)
736        {
737            throw new AmetysRepositoryException("Cannot get last contributor for resource " + this.getName() + " (" + this.getId() + ")", e);
738        }
739    }
740
741    public void setLastContributor(UserIdentity lastContributor)
742    {
743        try
744        {
745            Node lastContributorNode = null;
746            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME))
747            {
748                lastContributorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME);
749            }
750            else
751            {
752                lastContributorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME, RepositoryConstants.USER_NODETYPE);
753            }
754            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", lastContributor.getLogin());
755            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", lastContributor.getPopulationId());
756        }
757        catch (RepositoryException e)
758        {
759            throw new AmetysRepositoryException("Cannot set last contributor for resource " + this.getName() + " (" + this.getId() + ")", e);
760        }
761    }
762    
763    public void tag(String tag) throws AmetysRepositoryException
764    {
765        TaggableAmetysObjectHelper.tag(this, tag);
766    }
767
768    public void untag(String tag) throws AmetysRepositoryException
769    {
770        TaggableAmetysObjectHelper.untag(this, tag);
771    }
772
773    public Set<String> getTags() throws AmetysRepositoryException
774    {
775        return TaggableAmetysObjectHelper.getTags(this);
776    }
777}