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.Calendar;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023
024import javax.jcr.Node;
025import javax.jcr.RepositoryException;
026
027import org.apache.avalon.framework.component.Component;
028
029import org.ametys.cms.content.references.OutgoingReferences;
030import org.ametys.cms.data.type.ModelItemTypeConstants;
031import org.ametys.core.user.UserIdentity;
032import org.ametys.core.util.DateUtils;
033import org.ametys.plugins.repository.AmetysRepositoryException;
034import org.ametys.plugins.repository.RepositoryConstants;
035import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
036import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
037import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
038import org.ametys.plugins.repository.lock.LockableAmetysObject;
039import org.ametys.runtime.model.ElementDefinition;
040import org.ametys.runtime.model.ModelItem;
041
042
043/**
044 * Provides helper methods to use the {@link ModifiableContent} API on {@link DefaultContent}s.
045 */
046public class ModifiableContentHelper implements Component
047{
048    /** The Avalon role */
049    public static final String ROLE = ModifiableContentHelper.class.getName();
050    
051    /** Constants for the root of comments */
052    public static final String METADATA_COMMENTS = "comments";
053    /** Constants for the root of contributor comments */
054    public static final String METADATA_CONTRIBUTOR_COMMENTS = "contributor-comments";
055    
056    /**
057     * Set a {@link DefaultContent} title for the given locale.
058     * @param content the {@link DefaultContent} to set.
059     * @param title the title to set.
060     * @param locale The locale. Can be null if the content is not a multilingual content.
061     * @throws AmetysRepositoryException if an error occurs.
062     */
063    public void setTitle(DefaultContent content, String title, Locale locale) throws AmetysRepositoryException
064    {
065        ModelItem titleDefinition = content.getDefinition(Content.ATTRIBUTE_TITLE);
066        ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
067        if (titleDefinition instanceof ElementDefinition elementDef && ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(elementDef.getType().getId()))
068        {
069            if (locale == null)
070            {
071                throw new IllegalArgumentException("Cannot set a title with null locale on a multilingual title");
072            }
073            
074            _addLockToken(content);
075            ModifiableRepositoryData titleRepositoryData;
076            if (repositoryData.hasValue(Content.ATTRIBUTE_TITLE))
077            {
078                titleRepositoryData = repositoryData.getRepositoryData(Content.ATTRIBUTE_TITLE);
079            }
080            else
081            {
082                titleRepositoryData = repositoryData.addRepositoryData(Content.ATTRIBUTE_TITLE, RepositoryConstants.MULTILINGUAL_STRING_METADATA_NODETYPE);
083            }
084            titleRepositoryData.setValue(locale.toString(), title);
085        }
086        else
087        {
088            _addLockToken(content);
089            repositoryData.setValue(Content.ATTRIBUTE_TITLE, title);
090        }
091    }
092    
093    /**
094     * Set the title of non-multilingual {@link DefaultContent}.
095     * Be careful! Use only if content's title is not a multilingual string. If not sure use {@link #setTitle(DefaultContent, String, Locale)} instead.
096     * @param content the {@link DefaultContent} to set.
097     * @param title the title to set.
098     * @throws AmetysRepositoryException if an error occurs.
099     */
100    public void setTitle(DefaultContent content, String title) throws AmetysRepositoryException
101    {
102        setTitle(content, title, null);
103    }
104    
105    /**
106     * Copy the title of the source content to the target content
107     * @param srcContent The source content
108     * @param targetContent The target content
109     * @throws AmetysRepositoryException if an error occurs.
110     */
111    public void copyTitle(Content srcContent, ModifiableContent targetContent) throws AmetysRepositoryException
112    {
113        targetContent.setValue(Content.ATTRIBUTE_TITLE, srcContent.getValue(Content.ATTRIBUTE_TITLE));
114    }
115    
116    /**
117     * Set a {@link DefaultContent} user.
118     * @param content the {@link DefaultContent} to set.
119     * @param user the user to set.
120     * @throws AmetysRepositoryException if an error occurs.
121     */
122    public void setCreator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
123    {
124        _storeUserMetadata(content, DefaultContent.METADATA_CREATOR, user);
125    }
126
127    /**
128     * Set a {@link DefaultContent} creation date.
129     * @param content the {@link DefaultContent} to set.
130     * @param creationDate the creation date to set.
131     * @throws AmetysRepositoryException if an error occurs.
132     */
133    public void setCreationDate(DefaultContent content, ZonedDateTime creationDate) throws AmetysRepositoryException
134    {
135        _storeDatetimeMetadata(content, DefaultContent.METADATA_CREATION, creationDate);
136    }
137    
138    /**
139     * Set a {@link DefaultContent} contributor.
140     * @param content the {@link DefaultContent} to set.
141     * @param user the contributor to set.
142     * @throws AmetysRepositoryException if an error occurs.
143     */
144    public void setLastContributor(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
145    {
146        _storeUserMetadata(content, DefaultContent.METADATA_CONTRIBUTOR, user);
147    }
148    
149    /**
150     * Set a {@link DefaultContent} last modification date.
151     * @param content the {@link DefaultContent} to set.
152     * @param lastModified the last modification date to set.
153     * @throws AmetysRepositoryException if an error occurs.
154     */
155    public void setLastModified(DefaultContent content, ZonedDateTime lastModified) throws AmetysRepositoryException
156    {
157        _storeDatetimeMetadata(content, DefaultContent.METADATA_MODIFIED, lastModified);
158    }
159    
160    /**
161     * Set a {@link DefaultContent} first validator.
162     * @param content the {@link DefaultContent} to set.
163     * @param user the validator to set.
164     * @throws AmetysRepositoryException if an error occurs.
165     */
166    public void setFirstValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
167    {
168        _storeUserMetadata(content, DefaultContent.METADATA_FIRST_VALIDATOR, user);
169    }
170    
171    /**
172     * Set a {@link DefaultContent} first validation date.
173     * @param content the {@link DefaultContent} to set.
174     * @param validationDate the first validation date.
175     * @throws AmetysRepositoryException if an error occurs.
176     */
177    public void setFirstValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
178    {
179        _storeDatetimeMetadata(content, DefaultContent.METADATA_FIRST_VALIDATION, validationDate);
180    }
181    
182    /**
183     * Set a {@link DefaultContent} last validator.
184     * @param content the {@link DefaultContent} to set.
185     * @param user the validator to set.
186     * @throws AmetysRepositoryException if an error occurs.
187     */
188    public void setLastValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
189    {
190        _storeUserMetadata(content, DefaultContent.METADATA_LAST_VALIDATOR, user);
191    }
192    
193    /**
194     * Set a {@link DefaultContent} last validation date.
195     * @param content the {@link DefaultContent} to set.
196     * @param validationDate the last validation date.
197     * @throws AmetysRepositoryException if an error occurs.
198     */
199    public void setLastValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
200    {
201        _storeDatetimeMetadata(content, DefaultContent.METADATA_LAST_VALIDATION, validationDate);
202    }
203    
204    /**
205     * Set a {@link DefaultContent} last major validator.
206     * @param content the {@link DefaultContent} to set.
207     * @param user the validator to set.
208     * @throws AmetysRepositoryException if an error occurs.
209     */
210    public void setLastMajorValidator(DefaultContent content, UserIdentity user) throws AmetysRepositoryException
211    {
212        _storeUserMetadata(content, DefaultContent.METADATA_LAST_MAJOR_VALIDATOR, user);
213    }
214    
215    /**
216     * Set a {@link DefaultContent} last major validation date.
217     * @param content the {@link DefaultContent} to set.
218     * @param validationDate the last major validation date.
219     * @throws AmetysRepositoryException if an error occurs.
220     */
221    public void setLastMajorValidationDate(DefaultContent content, ZonedDateTime validationDate) throws AmetysRepositoryException
222    {
223        _storeDatetimeMetadata(content, DefaultContent.METADATA_LAST_MAJORVALIDATION, validationDate);
224    }
225    
226    /**
227     * Store the outgoing references on the content.
228     * @param content The content concerned by these outgoing references.
229     * @param references A non null map of outgoing references grouped by metadata (key are metadata path)
230     * @throws AmetysRepositoryException if an error occurs.
231     */
232    public void setOutgoingReferences(DefaultContent content, Map<String, OutgoingReferences> references) throws AmetysRepositoryException
233    {
234        try
235        {
236            Node contentNode = content.getNode();
237            if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES))
238            {
239                contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES).remove();
240            }
241            
242            if (!references.isEmpty())
243            {
244                Node rootOutgoingRefsNode = contentNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES);
245                for (String path : references.keySet())
246                {
247                    // Reference nodes per type
248                    OutgoingReferences outgoingReferences = references.get(path);
249                    
250                    Node outgoingRefsNode = null;
251                    
252                    for (String type : outgoingReferences.keySet())
253                    {
254                        List<String> referenceValues = outgoingReferences.get(type);
255                        if (referenceValues != null && !referenceValues.isEmpty())
256                        {
257                            if (outgoingRefsNode == null)
258                            {
259                                // Outgoing references node creation (for the given path)
260                                outgoingRefsNode = rootOutgoingRefsNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES);
261                                outgoingRefsNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES_PATH_PROPERTY, path);
262                            }
263                            
264                            Node outgoingReferenceNode = outgoingRefsNode.addNode(type, RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_NODETYPE);
265                            outgoingReferenceNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_PROPERTY, referenceValues.toArray(new String[referenceValues.size()]));
266                        }
267                    }
268                }
269            }
270        }
271        catch (RepositoryException e)
272        {
273            throw new AmetysRepositoryException(e);
274        }
275    }
276    
277    /**
278     * Store a metadata of type datetime in the content
279     * @param content the content described by the metadata
280     * @param metadataName the name of the metadata
281     * @param date the value to set
282     * @throws AmetysRepositoryException when an error occurred
283     */
284    protected void _storeDatetimeMetadata(DefaultContent content, String metadataName, ZonedDateTime date)
285    {
286        _addLockToken(content);
287        
288        ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
289        Calendar calendar = DateUtils.asCalendar(date);
290        repositoryData.setValue(metadataName, calendar);
291    }
292
293    /**
294     * Store a metadata of type user in the content
295     * @param content the content described by the metadata
296     * @param metadataName the name of the metadata
297     * @param user the value to set
298     * @throws AmetysRepositoryException when an error occurred
299     */
300    protected void _storeUserMetadata(DefaultContent content, String metadataName, UserIdentity user) throws AmetysRepositoryException
301    {
302        try
303        {
304            _addLockToken(content);
305            
306            // TODO CMS-9336 All the metatadata here should be stored using types
307            ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
308            ModifiableRepositoryData creatorRepositoryData;
309            if (repositoryData.hasValue(metadataName))
310            {
311                creatorRepositoryData = repositoryData.getRepositoryData(metadataName);
312            }
313            else
314            {
315                creatorRepositoryData = repositoryData.addRepositoryData(metadataName, RepositoryConstants.USER_NODETYPE);
316            }
317            creatorRepositoryData.setValue("login", user.getLogin());
318            creatorRepositoryData.setValue("population", user.getPopulationId());
319        }
320        catch (AmetysRepositoryException e)
321        {
322            throw new AmetysRepositoryException("Error setting the metadata '" + metadataName + "' for content '" + content.getId() + "' with value '" + UserIdentity.userIdentityToString(user) + "'.", e);
323        }
324    }
325    
326    private void _addLockToken(DefaultContent content)
327    {
328        if (content instanceof LockableAmetysObject lockableContent)
329        {
330            lockableContent.setLockInfoOnCurrentContext();
331        }
332    }
333    
334    /**
335     * Retrieves the data holder for the given content's comments
336     * @param content the content
337     * @param createNew <code>true</code> to create the comments' data holder if it does not already exist, <code>false</code> otherwise
338     * @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>
339     */
340    public ModifiableModelLessDataHolder getCommentsDataHolder(DefaultContent content, boolean createNew)
341    {
342        return content.getUnversionedDataHolder().getComposite(METADATA_COMMENTS, createNew);
343    }
344    
345    /**
346     * Retrieves the data holder for the given content's contributor comments
347     * @param content the content
348     * @param createNew <code>true</code> to create the contributor comments' data holder if it does not already exist, <code>false</code> otherwise
349     * @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>
350     */
351    public ModifiableModelLessDataHolder getContributorCommentsDataHolder(DefaultContent content, boolean createNew)
352    {
353        return content.getUnversionedDataHolder().getComposite(METADATA_CONTRIBUTOR_COMMENTS, createNew);
354    }
355}