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 && ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(((ElementDefinition) titleDefinition).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.size() != 0)
243            {
244                Node rootOutgoingRefsNode = contentNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_ROOT_OUTGOING_REFERENCES);
245                for (String path : references.keySet())
246                {
247                    // Outgoing references node creation (for the given path)
248                    Node outgoingRefsNode = rootOutgoingRefsNode.addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES);
249                    outgoingRefsNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES_PATH_PROPERTY, path);
250                    
251                    // Reference nodes per type
252                    OutgoingReferences outgoingReferences = references.get(path);
253                    
254                    for (String type : outgoingReferences.keySet())
255                    {
256                        List<String> referenceValues = outgoingReferences.get(type);
257                        if (referenceValues != null && !referenceValues.isEmpty())
258                        {
259                            Node outgoingReferenceNode = outgoingRefsNode.addNode(type, RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_NODETYPE);
260                            outgoingReferenceNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + DefaultContent.METADATA_OUTGOING_REFERENCE_PROPERTY, referenceValues.toArray(new String[referenceValues.size()]));
261                        }
262                    }
263                }
264            }
265        }
266        catch (RepositoryException e)
267        {
268            throw new AmetysRepositoryException(e);
269        }
270    }
271    
272    /**
273     * Store a metadata of type datetime in the content
274     * @param content the content described by the metadata
275     * @param metadataName the name of the metadata
276     * @param date the value to set
277     * @throws AmetysRepositoryException when an error occurred
278     */
279    protected void _storeDatetimeMetadata(DefaultContent content, String metadataName, ZonedDateTime date)
280    {
281        _addLockToken(content);
282        
283        ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
284        Calendar calendar = DateUtils.asCalendar(date);
285        repositoryData.setValue(metadataName, calendar);
286    }
287
288    /**
289     * Store a metadata of type user in the content
290     * @param content the content described by the metadata
291     * @param metadataName the name of the metadata
292     * @param user the value to set
293     * @throws AmetysRepositoryException when an error occurred
294     */
295    protected void _storeUserMetadata(DefaultContent content, String metadataName, UserIdentity user) throws AmetysRepositoryException
296    {
297        try
298        {
299            _addLockToken(content);
300            
301            // TODO CMS-9336 All the metatadata here should be stored using types
302            ModifiableRepositoryData repositoryData = new JCRRepositoryData(content.getNode());
303            ModifiableRepositoryData creatorRepositoryData;
304            if (repositoryData.hasValue(metadataName))
305            {
306                creatorRepositoryData = repositoryData.getRepositoryData(metadataName);
307            }
308            else
309            {
310                creatorRepositoryData = repositoryData.addRepositoryData(metadataName, RepositoryConstants.USER_NODETYPE);
311            }
312            creatorRepositoryData.setValue("login", user.getLogin());
313            creatorRepositoryData.setValue("population", user.getPopulationId());
314        }
315        catch (AmetysRepositoryException e)
316        {
317            throw new AmetysRepositoryException("Error setting the metadata '" + metadataName + "' for content '" + content.getId() + "' with value '" + UserIdentity.userIdentityToString(user) + "'.", e);
318        }
319    }
320    
321    private void _addLockToken(DefaultContent content)
322    {
323        if (content instanceof LockableAmetysObject lockableContent)
324        {
325            lockableContent.setLockInfoOnCurrentContext();
326        }
327    }
328    
329    /**
330     * Retrieves the data holder for the given content's comments
331     * @param content the content
332     * @param createNew <code>true</code> to create the comments' data holder if it does not already exist, <code>false</code> otherwise
333     * @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> 
334     */
335    public ModifiableModelLessDataHolder getCommentsDataHolder(DefaultContent content, boolean createNew)
336    {
337        return content.getUnversionedDataHolder().getComposite(METADATA_COMMENTS, createNew);
338    }
339    
340    /**
341     * Retrieves the data holder for the given content's contributor comments
342     * @param content the content
343     * @param createNew <code>true</code> to create the contributor comments' data holder if it does not already exist, <code>false</code> otherwise
344     * @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>
345     */
346    public ModifiableModelLessDataHolder getContributorCommentsDataHolder(DefaultContent content, boolean createNew)
347    {
348        return content.getUnversionedDataHolder().getComposite(METADATA_CONTRIBUTOR_COMMENTS, createNew);
349    }
350}