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}