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