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