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