001/*
002 *  Copyright 2012 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.time.ZonedDateTime;
019import java.util.List;
020import java.util.Locale;
021import java.util.Map;
022
023import javax.jcr.Node;
024import javax.jcr.RepositoryException;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.lang3.Strings;
031import org.apache.commons.lang3.tuple.Pair;
032
033import org.ametys.cms.content.references.OutgoingReferences;
034import org.ametys.cms.repository.comment.AbstractComment;
035import org.ametys.core.user.UserIdentity;
036import org.ametys.plugins.repository.AmetysObjectResolver;
037import org.ametys.plugins.repository.AmetysRepositoryException;
038import org.ametys.plugins.repository.RepositoryConstants;
039import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
040import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
041import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
042import org.ametys.plugins.repository.lock.LockableAmetysObject;
043import org.ametys.runtime.model.ElementDefinition;
044import org.ametys.runtime.model.ModelItem;
045import org.ametys.runtime.model.type.ModelItemTypeConstants;
046import org.ametys.runtime.plugin.component.AbstractLogEnabled;
047
048
049/**
050 * Provides helper methods to use the {@link ModifiableContent} API on {@link DefaultContent}s.
051 */
052public class ModifiableContentHelper extends AbstractLogEnabled implements Component, Serviceable
053{
054    /** The Avalon role */
055    public static final String ROLE = ModifiableContentHelper.class.getName();
056    
057    /** Constants for the root of comments */
058    public static final String METADATA_COMMENTS = "comments";
059    /** Constants for the root of contributor comments */
060    public static final String METADATA_CONTRIBUTOR_COMMENTS = "contributor-comments";
061
062    /** The Ametys object resolver */
063    protected AmetysObjectResolver _resolver;
064    
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
068    }
069    
070    /**
071     * Set a {@link DefaultContent} title for the given locale.
072     * @param content the {@link DefaultContent} to set.
073     * @param title the title to set.
074     * @param locale The locale. Can be null if the content is not a multilingual content.
075     * @throws AmetysRepositoryException if an error occurs.
076     */
077    public void setTitle(DefaultContent content, String title, Locale locale) throws AmetysRepositoryException
078    {
079        ModelItem titleDefinition = content.getDefinition(Content.ATTRIBUTE_TITLE);
080        ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
081        if (titleDefinition instanceof ElementDefinition elementDef && org.ametys.plugins.repository.data.type.ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(elementDef.getType().getId()))
082        {
083            if (locale == null)
084            {
085                throw new IllegalArgumentException("Cannot set a title with null locale on a multilingual title");
086            }
087            
088            _addLockToken(content);
089            ModifiableRepositoryData titleRepositoryData;
090            if (repositoryData.hasValue(Content.ATTRIBUTE_TITLE))
091            {
092                titleRepositoryData = repositoryData.getRepositoryData(Content.ATTRIBUTE_TITLE);
093            }
094            else
095            {
096                titleRepositoryData = repositoryData.addRepositoryData(Content.ATTRIBUTE_TITLE, RepositoryConstants.MULTILINGUAL_STRING_METADATA_NODETYPE);
097            }
098            titleRepositoryData.setValue(locale.toString(), title);
099        }
100        else
101        {
102            _addLockToken(content);
103            repositoryData.setValue(Content.ATTRIBUTE_TITLE, title);
104        }
105    }
106    
107    /**
108     * Set the title of non-multilingual {@link DefaultContent}.
109     * Be careful! Use only if content's title is not a multilingual string. If not sure use {@link #setTitle(DefaultContent, String, Locale)} instead.
110     * @param content the {@link DefaultContent} to set.
111     * @param title the title to set.
112     * @throws AmetysRepositoryException if an error occurs.
113     */
114    public void setTitle(DefaultContent content, String title) throws AmetysRepositoryException
115    {
116        setTitle(content, title, null);
117    }
118    
119    /**
120     * Copy the title of the source content to the target content
121     * @param srcContent The source content
122     * @param targetContent The target content
123     * @throws AmetysRepositoryException if an error occurs.
124     */
125    public void copyTitle(Content srcContent, ModifiableContent targetContent) throws AmetysRepositoryException
126    {
127        targetContent.setValue(Content.ATTRIBUTE_TITLE, srcContent.getValue(Content.ATTRIBUTE_TITLE));
128    }
129    
130    /**
131     * Set a {@link DefaultContent} user.
132     * @param content the {@link DefaultContent} to set.
133     * @param user the user to set.
134     * @throws AmetysRepositoryException if an error occurs.
135     */
136    public void setCreator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
137    {
138        _storeUserMetadata(content, DefaultContent.METADATA_CREATOR, user);
139    }
140
141    /**
142     * Set a {@link DefaultContent} creation date.
143     * @param content the {@link DefaultContent} to set.
144     * @param creationDate the creation date to set.
145     * @throws AmetysRepositoryException if an error occurs.
146     */
147    public void setCreationDate(DefaultContent content, ZonedDateTime creationDate) throws AmetysRepositoryException
148    {
149        _storeDatetimeMetadata(content, DefaultContent.METADATA_CREATION, creationDate);
150    }
151    
152    /**
153     * Set a {@link DefaultContent} contributor.
154     * @param content the {@link DefaultContent} to set.
155     * @param user the contributor to set.
156     * @throws AmetysRepositoryException if an error occurs.
157     */
158    public void setLastContributor(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
159    {
160        _storeUserMetadata(content, DefaultContent.METADATA_CONTRIBUTOR, user);
161    }
162    
163    /**
164     * Set a {@link DefaultContent} last modification date.
165     * @param content the {@link DefaultContent} to set.
166     * @param lastModified the last modification date to set.
167     * @throws AmetysRepositoryException if an error occurs.
168     */
169    public void setLastModified(DefaultContent content, ZonedDateTime lastModified) throws AmetysRepositoryException
170    {
171        _storeDatetimeMetadata(content, DefaultContent.METADATA_MODIFIED, lastModified);
172    }
173    
174    /**
175     * Set a {@link DefaultContent} first validator.
176     * @param content the {@link DefaultContent} to set.
177     * @param user the validator to set.
178     * @throws AmetysRepositoryException if an error occurs.
179     */
180    public void setFirstValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
181    {
182        _storeUserMetadata(content, DefaultContent.METADATA_FIRST_VALIDATOR, user);
183    }
184    
185    /**
186     * Set a {@link DefaultContent} first validation date.
187     * @param content the {@link DefaultContent} to set.
188     * @param validationDate the first validation date.
189     * @throws AmetysRepositoryException if an error occurs.
190     */
191    public void setFirstValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
192    {
193        _storeDatetimeMetadata(content, DefaultContent.METADATA_FIRST_VALIDATION, validationDate);
194    }
195    
196    /**
197     * Set a {@link DefaultContent} last validator.
198     * @param content the {@link DefaultContent} to set.
199     * @param user the validator to set.
200     * @throws AmetysRepositoryException if an error occurs.
201     */
202    public void setLastValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
203    {
204        _storeUserMetadata(content, DefaultContent.METADATA_LAST_VALIDATOR, user);
205    }
206    
207    /**
208     * Set a {@link DefaultContent} last validation date.
209     * @param content the {@link DefaultContent} to set.
210     * @param validationDate the last validation date.
211     * @throws AmetysRepositoryException if an error occurs.
212     */
213    public void setLastValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
214    {
215        _storeDatetimeMetadata(content, DefaultContent.METADATA_LAST_VALIDATION, validationDate);
216    }
217    
218    /**
219     * Set a {@link DefaultContent} last major validator.
220     * @param content the {@link DefaultContent} to set.
221     * @param user the validator to set.
222     * @throws AmetysRepositoryException if an error occurs.
223     */
224    public void setLastMajorValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
225    {
226        _storeUserMetadata(content, DefaultContent.METADATA_LAST_MAJOR_VALIDATOR, user);
227    }
228    
229    /**
230     * Set a {@link DefaultContent} last major validation date.
231     * @param content the {@link DefaultContent} to set.
232     * @param validationDate the last major validation date.
233     * @throws AmetysRepositoryException if an error occurs.
234     */
235    public void setLastMajorValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
236    {
237        _storeDatetimeMetadata(content, DefaultContent.METADATA_LAST_MAJORVALIDATION, validationDate);
238    }
239    
240    /**
241     * Store the outgoing references on the content.
242     * @param content The content concerned by these outgoing references.
243     * @param references A non null map of outgoing references grouped by metadata (key are metadata path)
244     * @throws AmetysRepositoryException if an error occurs.
245     */
246    public void setOutgoingReferences(DefaultContent content, Map<String, OutgoingReferences> references) throws AmetysRepositoryException
247    {
248        try
249        {
250            Node contentNode = content.getNode();
251            if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES))
252            {
253                contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES).remove();
254            }
255            
256            if (!references.isEmpty())
257            {
258                Node rootOutgoingRefsNode = contentNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES);
259                for (String path : references.keySet())
260                {
261                    // Reference nodes per type
262                    OutgoingReferences outgoingReferences = references.get(path);
263                    
264                    Node outgoingRefsNode = null;
265                    
266                    for (String type : outgoingReferences.keySet())
267                    {
268                        List<String> referenceValues = outgoingReferences.get(type);
269                        if (referenceValues != null && !referenceValues.isEmpty())
270                        {
271                            if (outgoingRefsNode == null)
272                            {
273                                // Outgoing references node creation (for the given path)
274                                outgoingRefsNode = rootOutgoingRefsNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES);
275                                outgoingRefsNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES_PATH_PROPERTY, path);
276                            }
277                            
278                            Node outgoingReferenceNode = outgoingRefsNode.addNode(type, RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_NODETYPE);
279                            outgoingReferenceNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_PROPERTY, referenceValues.toArray(new String[referenceValues.size()]));
280                        }
281                    }
282                }
283            }
284        }
285        catch (RepositoryException e)
286        {
287            throw new AmetysRepositoryException(e);
288        }
289    }
290    
291    /**
292     * Store a metadata of type datetime in the content
293     * @param content the content described by the metadata
294     * @param metadataName the name of the metadata
295     * @param date the value to set
296     * @throws AmetysRepositoryException when an error occurs while locking content
297     */
298    protected void _storeDatetimeMetadata(DefaultContent content, String metadataName, ZonedDateTime date) throws AmetysRepositoryException
299    {
300        _addLockToken(content);
301        content.getInternalDataHolder().setValue(metadataName, date, ModelItemTypeConstants.DATETIME_TYPE_ID);
302    }
303
304    /**
305     * Store a metadata of type user in the content
306     * @param content the content described by the metadata
307     * @param metadataName the name of the metadata
308     * @param user the value to set
309     * @throws AmetysRepositoryException when an error occurs while locking content
310     */
311    protected void _storeUserMetadata(DefaultContent content, String metadataName, UserIdentity user) throws AmetysRepositoryException
312    {
313        _addLockToken(content);
314        content.getInternalDataHolder().setValue(metadataName, user, ModelItemTypeConstants.USER_ELEMENT_TYPE_ID);
315    }
316    
317    private void _addLockToken(DefaultContent content)
318    {
319        if (content instanceof LockableAmetysObject lockableContent)
320        {
321            lockableContent.setLockInfoOnCurrentContext();
322        }
323    }
324    
325    /**
326     * Retrieves the data holder for the given content's comments
327     * @param content the content
328     * @param createNew <code>true</code> to create the comments' data holder if it does not already exist, <code>false</code> otherwise
329     * @return the data holder for content's comments, or <code>null</code> if the data holder does not already exist and createNew is set to <code>false</code>
330     */
331    public ModifiableDataHolder getCommentsDataHolder(DefaultContent content, boolean createNew)
332    {
333        return content.getUnversionedDataHolder().getComposite(METADATA_COMMENTS, createNew);
334    }
335    
336    /**
337     * Retrieves the data holder for the given content's contributor comments
338     * @param content the content
339     * @param createNew <code>true</code> to create the contributor comments' data holder if it does not already exist, <code>false</code> otherwise
340     * @return the data holder for content's contributor comments, or <code>null</code> if the data holder does not already exist and createNew is set to <code>false</code>
341     */
342    public ModifiableDataHolder getContributorCommentsDataHolder(DefaultContent content, boolean createNew)
343    {
344        return content.getUnversionedDataHolder().getComposite(METADATA_CONTRIBUTOR_COMMENTS, createNew);
345    }
346    
347    /**
348     * Get the content and the comment identifier from a comment node.
349     * This supports both comment from contributor and visitor
350     * @param commentNode the comment node
351     * @return the content and the comment id or null if it wasn't possible to retrieve the content
352     */
353    public Pair<DefaultContent, String> getContentAndCommentId(Node commentNode)
354    {
355        String commentNodePath = null;
356        try
357        {
358            commentNodePath = commentNode.getPath();
359            
360            // Retrieve the comment id by going up the hierarchy
361            // …/ametys-internal:unversioned/ametys:[contributor-]comments/ametys:comment-x/ametys:comments/ametys:comment-y
362            // => comment-x_comment-y
363            
364            // We start at the comment node
365            StringBuilder commentId = new StringBuilder(Strings.CS.removeStart(commentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":"));
366            
367            // Reconstructing the ID of comment
368            // we go up twice for as long has we don't find the unversioned node
369            Node parentNode = commentNode.getParent().getParent();
370            while (parentNode != null && !Strings.CS.equals(parentNode.getName(), "ametys-internal:unversioned"))
371            {
372                commentId
373                    .insert(0, AbstractComment.ID_SEPARATOR)
374                    .insert(0, Strings.CS.removeStart(parentNode.getName(), RepositoryConstants.NAMESPACE_PREFIX + ":"));
375                
376                parentNode = parentNode.getParent().getParent(); // continue iterating
377            }
378            
379            if (parentNode == null)
380            {
381                getLogger().error("Parent content not found from comment node '{}'", commentNodePath);
382                return null;
383            }
384            
385            return Pair.of(_resolver.resolve(parentNode.getParent(), false), commentId.toString());
386        }
387        catch (RepositoryException | AmetysRepositoryException e)
388        {
389            getLogger().error("Failed to retrieve content and comment from comment node '{}'", commentNodePath,  e);
390            return null;
391        }
392    }
393}