001/* 002 * Copyright 2010 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.util.ArrayList; 019import java.util.Collection; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import javax.jcr.Node; 028import javax.jcr.NodeIterator; 029import javax.jcr.PropertyIterator; 030import javax.jcr.RepositoryException; 031import javax.jcr.Value; 032import javax.jcr.lock.Lock; 033import javax.jcr.lock.LockManager; 034 035import org.ametys.cms.content.references.OutgoingReferences; 036import org.ametys.cms.tag.jcr.TaggableAmetysObjectHelper; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.plugins.explorer.resources.ResourceCollection; 039import org.ametys.plugins.repository.AmetysObject; 040import org.ametys.plugins.repository.AmetysObjectIterable; 041import org.ametys.plugins.repository.AmetysRepositoryException; 042import org.ametys.plugins.repository.CopiableAmetysObject; 043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 044import org.ametys.plugins.repository.RepositoryConstants; 045import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 046import org.ametys.plugins.repository.UnknownAmetysObjectException; 047import org.ametys.plugins.repository.dublincore.DCMITypes; 048import org.ametys.plugins.repository.jcr.DefaultAmetysObject; 049import org.ametys.plugins.repository.jcr.DublinCoreHelper; 050import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 051import org.ametys.plugins.repository.jcr.NodeTypeHelper; 052import org.ametys.plugins.repository.metadata.UnknownMetadataException; 053 054/** 055 * Default implementation of a {@link Content}, also versionable, lockable and workflow-aware. 056 * @param <F> the actual type of factory. 057 */ 058public class DefaultContent<F extends ContentFactory> extends DefaultAmetysObject<F> implements Content, CopiableAmetysObject, JCRTraversableAmetysObject 059{ 060 /** Constants for the root outgoing references node */ 061 public static final String METADATA_ROOT_OUTGOING_REFERENCES = "root-outgoing-references"; 062 063 /** Constants for the outgoing references node */ 064 public static final String METADATA_OUTGOING_REFERENCES = "outgoing-references"; 065 066 /** Constants for the outgoing references path property */ 067 public static final String METADATA_OUTGOING_REFERENCES_PATH_PROPERTY = "path"; 068 069 /** Constants for the outgoing reference property */ 070 public static final String METADATA_OUTGOING_REFERENCE_PROPERTY = "reference"; 071 072 /** Constants for language Metadata* */ 073 public static final String METADATA_LANGUAGE = "language"; 074 075 /** Constants for title Metadata* */ 076 public static final String METADATA_TITLE = "title"; 077 078 /** Constants for author Metadata* */ 079 public static final String METADATA_CREATOR = "creator"; 080 081 /** Constants for lastModified Metadata* */ 082 public static final String METADATA_CREATION = "creationDate"; 083 084 /** Constants for lastValidationDate Metadata* */ 085 public static final String METADATA_LAST_VALIDATION = "lastValidationDate"; 086 087 /** Constants for lastMajorValidationDate Metadata* */ 088 public static final String METADATA_LAST_MAJORVALIDATION = "lastMajorValidationDate"; 089 090 /** Constants for last contributor Metadata* */ 091 public static final String METADATA_CONTRIBUTOR = "contributor"; 092 093 /** Constants for lastModified Metadata* */ 094 public static final String METADATA_MODIFIED = "lastModified"; 095 096 /** Constants for contentType Metadata* */ 097 public static final String METADATA_CONTENTTYPE = "contentType"; 098 099 /** Constants for contentType Metadata* */ 100 public static final String METADATA_MIXINCONTENTTYPES = "mixins"; 101 102 /** Constant for the attachment node name. */ 103 public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments"; 104 105 private boolean _lockAlreadyChecked; 106 107 /** 108 * Creates a JCR-based Content. 109 * @param node the JCR Node backing this Content. 110 * @param parentPath the parent path in the Ametys hierarchy. 111 * @param factory the corresponding {@link ContentFactory}. 112 */ 113 public DefaultContent(Node node, String parentPath, F factory) 114 { 115 super(node, parentPath, factory); 116 } 117 118 @Override 119 public String[] getTypes() throws AmetysRepositoryException 120 { 121 try 122 { 123 if (getNode().hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE)) 124 { 125 Value[] values = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE).getValues(); 126 127 String[] types = new String[values.length]; 128 for (int i = 0; i < values.length; i++) 129 { 130 Value value = values[i]; 131 types[i] = value.getString(); 132 } 133 return types; 134 } 135 else 136 { 137 return new String[0]; 138 } 139 } 140 catch (javax.jcr.RepositoryException ex) 141 { 142 throw new AmetysRepositoryException("Unable to get contentType property", ex); 143 } 144 } 145 146 @Override 147 public String[] getMixinTypes() throws AmetysRepositoryException 148 { 149 try 150 { 151 if (getNode().hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_MIXINCONTENTTYPES)) 152 { 153 Value[] values = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_MIXINCONTENTTYPES).getValues(); 154 155 String[] mixins = new String[values.length]; 156 for (int i = 0; i < values.length; i++) 157 { 158 Value value = values[i]; 159 mixins[i] = value.getString(); 160 } 161 return mixins; 162 } 163 else 164 { 165 return new String[0]; 166 } 167 168 } 169 catch (javax.jcr.RepositoryException ex) 170 { 171 throw new AmetysRepositoryException("Unable to get contentType property", ex); 172 } 173 } 174 175 @Override 176 public String getLanguage() throws AmetysRepositoryException 177 { 178 try 179 { 180 return getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_LANGUAGE).getString(); 181 } 182 catch (javax.jcr.RepositoryException ex) 183 { 184 throw new AmetysRepositoryException("Unable to get language property", ex); 185 } 186 } 187 188 @Override 189 public String getTitle() throws UnknownMetadataException, AmetysRepositoryException 190 { 191 return getMetadataHolder().getString(METADATA_TITLE); 192 } 193 194 @Override 195 public UserIdentity getCreator() throws UnknownMetadataException, AmetysRepositoryException 196 { 197 try 198 { 199 Node creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + METADATA_CREATOR); 200 return new UserIdentity(creatorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), creatorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 201 } 202 catch (RepositoryException e) 203 { 204 throw new AmetysRepositoryException("Error while getting creator property for content " + this, e); 205 } 206 } 207 208 @Override 209 public Date getCreationDate() throws UnknownMetadataException, AmetysRepositoryException 210 { 211 return getMetadataHolder().getDate(METADATA_CREATION); 212 } 213 214 @Override 215 public UserIdentity getLastContributor() throws UnknownMetadataException, AmetysRepositoryException 216 { 217 try 218 { 219 Node contributorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + METADATA_CONTRIBUTOR); 220 return new UserIdentity(contributorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), contributorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 221 } 222 catch (RepositoryException e) 223 { 224 throw new AmetysRepositoryException("Error while getting last contributor property for content " + this, e); 225 } 226 } 227 228 @Override 229 public Date getLastModified() throws UnknownMetadataException, AmetysRepositoryException 230 { 231 return getMetadataHolder().getDate(METADATA_MODIFIED); 232 } 233 234 @Override 235 public Date getLastValidationDate() throws UnknownMetadataException, AmetysRepositoryException 236 { 237 if (getMetadataHolder().hasMetadata(METADATA_LAST_VALIDATION)) 238 { 239 return getMetadataHolder().getDate(METADATA_LAST_VALIDATION); 240 } 241 return null; 242 } 243 244 @Override 245 public Date getLastMajorValidationDate() throws AmetysRepositoryException 246 { 247 if (getMetadataHolder().hasMetadata(METADATA_LAST_MAJORVALIDATION)) 248 { 249 return getMetadataHolder().getDate(METADATA_LAST_MAJORVALIDATION); 250 } 251 return null; 252 } 253 254 // Tag management. 255 @Override 256 public Set<String> getTags() throws AmetysRepositoryException 257 { 258 return TaggableAmetysObjectHelper.getTags(this); 259 } 260 261 @Override 262 public Map<String, OutgoingReferences> getOutgoingReferences() throws AmetysRepositoryException 263 { 264 Map<String, OutgoingReferences> outgoingReferencesByPath = new HashMap<>(); 265 266 try 267 { 268 Node contentNode = getNode(); 269 if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES)) 270 { 271 Node rootOutgoingRefsNode = contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES); 272 273 // Loop on outgoing ref node by (metadata) path. 274 NodeIterator outgoingRefsNodeIterator = rootOutgoingRefsNode.getNodes(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES); 275 while (outgoingRefsNodeIterator.hasNext()) 276 { 277 Node outgoingRefsNode = outgoingRefsNodeIterator.nextNode(); 278 String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString(); 279 280 // Loop on each outgoing ref node values for each reference type and collecting outgoing references. 281 OutgoingReferences outgoingReferences = new OutgoingReferences(); 282 NodeIterator outgoingReferenceNodeIterator = outgoingRefsNode.getNodes(); 283 while (outgoingReferenceNodeIterator.hasNext()) 284 { 285 Node outgoingReferenceNode = outgoingReferenceNodeIterator.nextNode(); 286 Value[] referenceValues = outgoingReferenceNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_OUTGOING_REFERENCE_PROPERTY).getValues(); 287 288 List<String> referenceValuesAsList = new ArrayList<>(referenceValues.length); 289 for (Value value : referenceValues) 290 { 291 referenceValuesAsList.add(value.getString()); 292 } 293 294 outgoingReferences.put(outgoingReferenceNode.getName(), referenceValuesAsList); 295 } 296 297 // Updating the outgoing references map 298 if (!outgoingReferences.isEmpty()) 299 { 300 if (outgoingReferencesByPath.containsKey(path)) 301 { 302 outgoingReferencesByPath.get(path).merge(outgoingReferences); 303 } 304 else 305 { 306 outgoingReferencesByPath.put(path, outgoingReferences); 307 } 308 } 309 } 310 } 311 } 312 catch (RepositoryException e) 313 { 314 throw new AmetysRepositoryException(e); 315 } 316 317 return outgoingReferencesByPath; 318 } 319 320 @Override 321 public Collection<Content> getReferencingContents() throws AmetysRepositoryException 322 { 323 LinkedHashSet<Content> contents = new LinkedHashSet<>(); 324 325 try 326 { 327 PropertyIterator itReferences = getNode().getReferences(); 328 329 while (itReferences.hasNext()) 330 { 331 Node node = itReferences.nextProperty().getParent(); 332 333 // Go up the tree while we are on a composite metadata. 334 while (NodeTypeHelper.isNodeType(node, "ametys:compositeMetadata") && !NodeTypeHelper.isNodeType(node, "ametys:content")) 335 { 336 node = node.getParent(); 337 } 338 339 // If the node is a content, resolve and add to the referrer list. 340 if (NodeTypeHelper.isNodeType(node, "ametys:content")) 341 { 342 Content content = _getFactory()._getAOResolver().resolve(node, false); 343 contents.add(content); 344 } 345 } 346 } 347 catch (RepositoryException e) 348 { 349 throw new AmetysRepositoryException("Unable to resolve references for content " + getId(), e); 350 } 351 352 return contents; 353 } 354 355 @Override 356 public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException 357 { 358 return copyTo(parent, name); 359 } 360 361 @Override 362 public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException 363 { 364 return copyTo(parent, name, 0); 365 } 366 367 /** 368 * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint) 369 * @param parent The parent of the new object. Can not be null. 370 * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object. 371 * @param initWorkflowActionId The initial workflow action id 372 * @return the created object 373 * @throws AmetysRepositoryException if an error occurs. 374 */ 375 public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException 376 { 377 return _getFactory()._getContentDAO().copy(this, parent, name, initWorkflowActionId); 378 } 379 380 /** 381 * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint) 382 * @param parent The parent of the new object. Can not be null. 383 * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object. 384 * @param lang Language of the new object. Can be null. If null, the new language will be get from the copied object. 385 * @param initWorkflowActionId The initial workflow action id 386 * @return the created object 387 * @throws AmetysRepositoryException if an error occurs. 388 */ 389 public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException 390 { 391 return _getFactory()._getContentDAO().copy(this, parent, name, lang, initWorkflowActionId); 392 } 393 394 void _checkLock() throws RepositoryException 395 { 396 Node node = getNode(); 397 if (!_lockAlreadyChecked && getNode().isLocked()) 398 { 399 LockManager lockManager = node.getSession().getWorkspace().getLockManager(); 400 401 Lock lock = lockManager.getLock(node.getPath()); 402 Node lockHolder = lock.getNode(); 403 404 lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString()); 405 _lockAlreadyChecked = true; 406 } 407 } 408 409 // Dublin Core metadata. // 410 411 @Override 412 public String getDCTitle() throws AmetysRepositoryException 413 { 414 return DublinCoreHelper.getDCTitle(this, getTitle()); 415 } 416 417 @Override 418 public String getDCCreator() throws AmetysRepositoryException 419 { 420 return DublinCoreHelper.getDCCreator(this, getCreator().getLogin()); 421 } 422 423 @Override 424 public String[] getDCSubject() throws AmetysRepositoryException 425 { 426 return DublinCoreHelper.getDCSubject(this); 427 } 428 429 @Override 430 public String getDCDescription() throws AmetysRepositoryException 431 { 432 String seoDesc = null; 433 try 434 { 435 seoDesc = getMetadataHolder().getCompositeMetadata("seo").getString("description", null); 436 } 437 catch (UnknownMetadataException me) 438 { 439 // Ignore, just leave seoDesc null. 440 } 441 442 return DublinCoreHelper.getDCDescription(this, seoDesc); 443 } 444 445 @Override 446 public String getDCPublisher() throws AmetysRepositoryException 447 { 448 return DublinCoreHelper.getDCPublisher(this); 449 } 450 451 @Override 452 public String getDCContributor() throws AmetysRepositoryException 453 { 454 return DublinCoreHelper.getDCContributor(this, getLastContributor().getLogin()); 455 } 456 457 @Override 458 public Date getDCDate() throws AmetysRepositoryException 459 { 460 return DublinCoreHelper.getDCDate(this, getLastValidationDate()); 461 462 } 463 464 @Override 465 public String getDCType() throws AmetysRepositoryException 466 { 467 return DublinCoreHelper.getDCType(this, DCMITypes.TEXT); 468 } 469 470 @Override 471 public String getDCFormat() throws AmetysRepositoryException 472 { 473 return DublinCoreHelper.getDCFormat(this, "text/html"); 474 } 475 476 @Override 477 public String getDCIdentifier() throws AmetysRepositoryException 478 { 479 return DublinCoreHelper.getDCIdentifier(this, getId()); 480 } 481 482 @Override 483 public String getDCSource() throws AmetysRepositoryException 484 { 485 return DublinCoreHelper.getDCSource(this); 486 } 487 488 @Override 489 public String getDCLanguage() throws AmetysRepositoryException 490 { 491 return DublinCoreHelper.getDCLanguage(this, getLanguage()); 492 } 493 494 @Override 495 public String getDCRelation() throws AmetysRepositoryException 496 { 497 return DublinCoreHelper.getDCRelation(this); 498 } 499 500 @Override 501 public String getDCCoverage() throws AmetysRepositoryException 502 { 503 return DublinCoreHelper.getDCCoverage(this, getDCLanguage()); 504 } 505 506 @Override 507 public String getDCRights() throws AmetysRepositoryException 508 { 509 return DublinCoreHelper.getDCRights(this); 510 } 511 512 @Override 513 public ResourceCollection getRootAttachments() throws AmetysRepositoryException 514 { 515 ResourceCollection attachments = null; 516 517 if (hasChild(ATTACHMENTS_NODE_NAME)) 518 { 519 attachments = getChild(ATTACHMENTS_NODE_NAME); 520 } 521 522 return attachments; 523 } 524 525 @Override 526 public boolean hasChild(String name) throws AmetysRepositoryException 527 { 528 return _getFactory().hasChild(this, name); 529 } 530 531 @SuppressWarnings("unchecked") 532 @Override 533 public <A extends AmetysObject> A createChild(String name, String type) throws AmetysRepositoryException, RepositoryIntegrityViolationException 534 { 535 return (A) _getFactory().createChild(this, name, type); 536 } 537 538 @SuppressWarnings("unchecked") 539 @Override 540 public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException 541 { 542 return (A) _getFactory().getChild(this, path); 543 } 544 545 @Override 546 public <A extends AmetysObject> AmetysObjectIterable<A> getChildren() throws AmetysRepositoryException 547 { 548 return _getFactory().getChildren(this); 549 } 550}