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.cms.repository;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import javax.jcr.Node;
028import javax.jcr.NodeIterator;
029import javax.jcr.PropertyIterator;
030import javax.jcr.RepositoryException;
031import javax.jcr.Value;
032import javax.jcr.lock.Lock;
033import javax.jcr.lock.LockManager;
034
035import org.ametys.cms.content.references.OutgoingReferences;
036import org.ametys.cms.tag.jcr.TaggableAmetysObjectHelper;
037import org.ametys.core.user.UserIdentity;
038import org.ametys.plugins.explorer.resources.ResourceCollection;
039import org.ametys.plugins.repository.AmetysObject;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041import org.ametys.plugins.repository.AmetysRepositoryException;
042import org.ametys.plugins.repository.CopiableAmetysObject;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.repository.RepositoryConstants;
045import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
046import org.ametys.plugins.repository.UnknownAmetysObjectException;
047import org.ametys.plugins.repository.dublincore.DCMITypes;
048import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
049import org.ametys.plugins.repository.jcr.DublinCoreHelper;
050import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
051import org.ametys.plugins.repository.jcr.NodeTypeHelper;
052import org.ametys.plugins.repository.metadata.UnknownMetadataException;
053
054/**
055 * Default implementation of a {@link Content}, also versionable, lockable and workflow-aware.
056 * @param <F> the actual type of factory.
057 */
058public class DefaultContent<F extends ContentFactory> extends DefaultAmetysObject<F> implements Content, CopiableAmetysObject, JCRTraversableAmetysObject
059{
060    /** Constants for the root outgoing references node */
061    public static final String METADATA_ROOT_OUTGOING_REFERENCES = "root-outgoing-references";
062    
063    /** Constants for the outgoing references node */
064    public static final String METADATA_OUTGOING_REFERENCES = "outgoing-references";
065    
066    /** Constants for the outgoing references path property */
067    public static final String METADATA_OUTGOING_REFERENCES_PATH_PROPERTY = "path";
068    
069    /** Constants for the outgoing reference property */
070    public static final String METADATA_OUTGOING_REFERENCE_PROPERTY = "reference";
071    
072    /** Constants for language Metadata* */
073    public static final String METADATA_LANGUAGE = "language";
074    
075    /** Constants for title Metadata* */
076    public static final String METADATA_TITLE = "title";
077    
078    /** Constants for author Metadata* */
079    public static final String METADATA_CREATOR = "creator";
080    
081    /** Constants for lastModified Metadata* */
082    public static final String METADATA_CREATION = "creationDate";
083
084    /** Constants for lastValidationDate Metadata* */
085    public static final String METADATA_LAST_VALIDATION = "lastValidationDate";
086    
087    /** Constants for lastMajorValidationDate Metadata* */
088    public static final String METADATA_LAST_MAJORVALIDATION = "lastMajorValidationDate";
089    
090    /** Constants for last contributor Metadata* */
091    public static final String METADATA_CONTRIBUTOR = "contributor";
092    
093    /** Constants for lastModified Metadata* */
094    public static final String METADATA_MODIFIED = "lastModified";
095    
096    /** Constants for contentType Metadata* */
097    public static final String METADATA_CONTENTTYPE = "contentType";
098    
099    /** Constants for contentType Metadata* */
100    public static final String METADATA_MIXINCONTENTTYPES = "mixins";
101    
102    /** Constant for the attachment node name. */
103    public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
104    
105    private boolean _lockAlreadyChecked;
106    
107    /**
108     * Creates a JCR-based Content.
109     * @param node the JCR Node backing this Content.
110     * @param parentPath the parent path in the Ametys hierarchy.
111     * @param factory the corresponding {@link ContentFactory}.
112     */
113    public DefaultContent(Node node, String parentPath, F factory)
114    {
115        super(node, parentPath, factory);
116    }
117    
118    @Override
119    public String[] getTypes() throws AmetysRepositoryException
120    {
121        try
122        {
123            if (getNode().hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE))
124            {
125                Value[] values = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE).getValues();
126                
127                String[] types = new String[values.length];
128                for (int i = 0; i < values.length; i++)
129                {
130                    Value value = values[i];
131                    types[i] = value.getString();
132                }
133                return types;
134            }
135            else
136            {
137                return new String[0];
138            }
139        }
140        catch (javax.jcr.RepositoryException ex)
141        {
142            throw new AmetysRepositoryException("Unable to get contentType property", ex);
143        }
144    }
145    
146    @Override
147    public String[] getMixinTypes() throws AmetysRepositoryException
148    {
149        try
150        {
151            if (getNode().hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_MIXINCONTENTTYPES))
152            {
153                Value[] values = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_MIXINCONTENTTYPES).getValues();
154                
155                String[] mixins = new String[values.length];
156                for (int i = 0; i < values.length; i++)
157                {
158                    Value value = values[i];
159                    mixins[i] = value.getString();
160                }
161                return mixins;
162            }
163            else
164            {
165                return new String[0];
166            }
167            
168        }
169        catch (javax.jcr.RepositoryException ex)
170        {
171            throw new AmetysRepositoryException("Unable to get contentType property", ex);
172        }
173    }
174    
175    @Override
176    public String getLanguage() throws AmetysRepositoryException
177    {
178        try
179        {
180            return getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_LANGUAGE).getString();
181        }
182        catch (javax.jcr.RepositoryException ex)
183        {
184            throw new AmetysRepositoryException("Unable to get language property", ex);
185        }
186    }
187    
188    @Override
189    public String getTitle() throws UnknownMetadataException, AmetysRepositoryException
190    {
191        return getMetadataHolder().getString(METADATA_TITLE);
192    }
193    
194    @Override
195    public UserIdentity getCreator() throws UnknownMetadataException, AmetysRepositoryException
196    {
197        try
198        {
199            Node creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + METADATA_CREATOR);
200            return new UserIdentity(creatorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), creatorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
201        }
202        catch (RepositoryException e)
203        {
204            throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
205        }
206    }
207    
208    @Override
209    public Date getCreationDate() throws UnknownMetadataException, AmetysRepositoryException
210    {
211        return getMetadataHolder().getDate(METADATA_CREATION);
212    }
213    
214    @Override
215    public UserIdentity getLastContributor() throws UnknownMetadataException, AmetysRepositoryException
216    {
217        try
218        {
219            Node contributorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + METADATA_CONTRIBUTOR);
220            return new UserIdentity(contributorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), contributorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString());
221        }
222        catch (RepositoryException e)
223        {
224            throw new AmetysRepositoryException("Error while getting last contributor property for content " + this, e);
225        }
226    }
227    
228    @Override
229    public Date getLastModified() throws UnknownMetadataException, AmetysRepositoryException
230    {
231        return getMetadataHolder().getDate(METADATA_MODIFIED);
232    }
233    
234    @Override
235    public Date getLastValidationDate() throws UnknownMetadataException, AmetysRepositoryException
236    {
237        if (getMetadataHolder().hasMetadata(METADATA_LAST_VALIDATION))
238        {
239            return getMetadataHolder().getDate(METADATA_LAST_VALIDATION);
240        }
241        return null;
242    }
243    
244    @Override
245    public Date getLastMajorValidationDate() throws AmetysRepositoryException
246    {
247        if (getMetadataHolder().hasMetadata(METADATA_LAST_MAJORVALIDATION))
248        {
249            return getMetadataHolder().getDate(METADATA_LAST_MAJORVALIDATION);
250        }
251        return null;
252    }
253    
254    // Tag management.
255    @Override
256    public Set<String> getTags() throws AmetysRepositoryException
257    {
258        return TaggableAmetysObjectHelper.getTags(this);
259    }
260    
261    @Override
262    public Map<String, OutgoingReferences> getOutgoingReferences() throws AmetysRepositoryException
263    {
264        Map<String, OutgoingReferences> outgoingReferencesByPath = new HashMap<>();
265        
266        try
267        {
268            Node contentNode = getNode();
269            if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES))
270            {
271                Node rootOutgoingRefsNode = contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES);
272                
273                // Loop on outgoing ref node by (metadata) path.
274                NodeIterator outgoingRefsNodeIterator = rootOutgoingRefsNode.getNodes(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES);
275                while (outgoingRefsNodeIterator.hasNext())
276                {
277                    Node outgoingRefsNode = outgoingRefsNodeIterator.nextNode();
278                    String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString();
279                    
280                    // Loop on each outgoing ref node values for each reference type and collecting outgoing references.
281                    OutgoingReferences outgoingReferences = new OutgoingReferences();
282                    NodeIterator outgoingReferenceNodeIterator = outgoingRefsNode.getNodes();
283                    while (outgoingReferenceNodeIterator.hasNext())
284                    {
285                        Node outgoingReferenceNode = outgoingReferenceNodeIterator.nextNode();
286                        Value[] referenceValues = outgoingReferenceNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_OUTGOING_REFERENCE_PROPERTY).getValues();
287                        
288                        List<String> referenceValuesAsList = new ArrayList<>(referenceValues.length);
289                        for (Value value : referenceValues)
290                        {
291                            referenceValuesAsList.add(value.getString());
292                        }
293                        
294                        outgoingReferences.put(outgoingReferenceNode.getName(), referenceValuesAsList);
295                    }
296                    
297                    // Updating the outgoing references map
298                    if (!outgoingReferences.isEmpty())
299                    {
300                        if (outgoingReferencesByPath.containsKey(path))
301                        {
302                            outgoingReferencesByPath.get(path).merge(outgoingReferences);
303                        }
304                        else
305                        {
306                            outgoingReferencesByPath.put(path, outgoingReferences);
307                        }
308                    }
309                }
310            }
311        }
312        catch (RepositoryException e)
313        {
314            throw new AmetysRepositoryException(e);
315        }
316        
317        return outgoingReferencesByPath;
318    }
319    
320    @Override
321    public Collection<Content> getReferencingContents() throws AmetysRepositoryException
322    {
323        LinkedHashSet<Content> contents = new LinkedHashSet<>();
324        
325        try
326        {
327            PropertyIterator itReferences = getNode().getReferences();
328            
329            while (itReferences.hasNext())
330            {
331                Node node = itReferences.nextProperty().getParent();
332                
333                // Go up the tree while we are on a composite metadata.
334                while (NodeTypeHelper.isNodeType(node, "ametys:compositeMetadata") && !NodeTypeHelper.isNodeType(node, "ametys:content"))
335                {
336                    node = node.getParent();
337                }
338                
339                // If the node is a content, resolve and add to the referrer list.
340                if (NodeTypeHelper.isNodeType(node, "ametys:content"))
341                {
342                    Content content = _getFactory()._getAOResolver().resolve(node, false);
343                    contents.add(content);
344                }
345            }
346        }
347        catch (RepositoryException e)
348        {
349            throw new AmetysRepositoryException("Unable to resolve references for content " + getId(), e);
350        }
351        
352        return contents;
353    }
354        
355    @Override
356    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
357    {
358        return copyTo(parent, name);
359    }
360    
361    @Override
362    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
363    {
364        return copyTo(parent, name, 0);
365    }
366    
367    /**
368     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
369     * @param parent The parent of the new object. Can not be null.
370     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
371     * @param initWorkflowActionId The initial workflow action id
372     * @return the created object
373     * @throws AmetysRepositoryException if an error occurs.
374     */
375    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException
376    {
377        return _getFactory()._getContentDAO().copy(this, parent, name, initWorkflowActionId);
378    }
379    
380    /**
381     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
382     * @param parent The parent of the new object. Can not be null.
383     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
384     * @param lang Language of the new object. Can be null. If null, the new language will be get from the copied object.
385     * @param initWorkflowActionId The initial workflow action id
386     * @return the created object
387     * @throws AmetysRepositoryException if an error occurs.
388     */
389    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException
390    {
391        return _getFactory()._getContentDAO().copy(this, parent, name, lang, initWorkflowActionId);
392    }
393    
394    void _checkLock() throws RepositoryException
395    {
396        Node node = getNode();
397        if (!_lockAlreadyChecked && getNode().isLocked())
398        {
399            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
400            
401            Lock lock = lockManager.getLock(node.getPath());
402            Node lockHolder = lock.getNode();
403            
404            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
405            _lockAlreadyChecked = true;
406        }
407    }
408    
409    // Dublin Core metadata. //
410    
411    @Override
412    public String getDCTitle() throws AmetysRepositoryException
413    {
414        return DublinCoreHelper.getDCTitle(this, getTitle());
415    }
416    
417    @Override
418    public String getDCCreator() throws AmetysRepositoryException
419    {
420        return DublinCoreHelper.getDCCreator(this, getCreator().getLogin());
421    }
422
423    @Override
424    public String[] getDCSubject() throws AmetysRepositoryException
425    {
426        return DublinCoreHelper.getDCSubject(this);
427    }
428
429    @Override
430    public String getDCDescription() throws AmetysRepositoryException
431    {
432        String seoDesc = null;
433        try
434        {
435            seoDesc = getMetadataHolder().getCompositeMetadata("seo").getString("description", null);
436        }
437        catch (UnknownMetadataException me)
438        {
439            // Ignore, just leave seoDesc null.
440        }
441        
442        return DublinCoreHelper.getDCDescription(this, seoDesc);
443    }
444
445    @Override
446    public String getDCPublisher() throws AmetysRepositoryException
447    {
448        return DublinCoreHelper.getDCPublisher(this);
449    }
450    
451    @Override
452    public String getDCContributor() throws AmetysRepositoryException
453    {
454        return DublinCoreHelper.getDCContributor(this, getLastContributor().getLogin());
455    }
456    
457    @Override
458    public Date getDCDate() throws AmetysRepositoryException
459    {
460        return DublinCoreHelper.getDCDate(this, getLastValidationDate());
461        
462    }
463    
464    @Override
465    public String getDCType() throws AmetysRepositoryException
466    {
467        return DublinCoreHelper.getDCType(this, DCMITypes.TEXT);
468    }
469    
470    @Override
471    public String getDCFormat() throws AmetysRepositoryException
472    {
473        return DublinCoreHelper.getDCFormat(this, "text/html");
474    }
475    
476    @Override
477    public String getDCIdentifier() throws AmetysRepositoryException
478    {
479        return DublinCoreHelper.getDCIdentifier(this, getId());
480    }
481    
482    @Override
483    public String getDCSource() throws AmetysRepositoryException
484    {
485        return DublinCoreHelper.getDCSource(this);
486    }
487    
488    @Override
489    public String getDCLanguage() throws AmetysRepositoryException
490    {
491        return DublinCoreHelper.getDCLanguage(this, getLanguage());
492    }
493    
494    @Override
495    public String getDCRelation() throws AmetysRepositoryException
496    {
497        return DublinCoreHelper.getDCRelation(this);
498    }
499    
500    @Override
501    public String getDCCoverage() throws AmetysRepositoryException
502    {
503        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
504    }
505    
506    @Override
507    public String getDCRights() throws AmetysRepositoryException
508    {
509        return DublinCoreHelper.getDCRights(this);
510    }
511
512    @Override
513    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
514    {
515        ResourceCollection attachments = null;
516        
517        if (hasChild(ATTACHMENTS_NODE_NAME))
518        {
519            attachments = getChild(ATTACHMENTS_NODE_NAME);
520        }
521        
522        return attachments;
523    }
524    
525    @Override
526    public boolean hasChild(String name) throws AmetysRepositoryException
527    {
528        return _getFactory().hasChild(this, name);
529    }
530    
531    @SuppressWarnings("unchecked")
532    @Override
533    public <A extends AmetysObject> A createChild(String name, String type) throws AmetysRepositoryException, RepositoryIntegrityViolationException
534    {
535        return (A) _getFactory().createChild(this, name, type);
536    }
537    
538    @SuppressWarnings("unchecked")
539    @Override
540    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
541    {
542        return (A) _getFactory().getChild(this, path);
543    }
544    
545    @Override
546    public <A extends AmetysObject> AmetysObjectIterable<A> getChildren() throws AmetysRepositoryException
547    {
548        return _getFactory().getChildren(this);
549    }
550}