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