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.web.repository.page.jcr; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.List; 023import java.util.NoSuchElementException; 024import java.util.Set; 025 026import javax.jcr.ItemExistsException; 027import javax.jcr.Node; 028import javax.jcr.RepositoryException; 029import javax.jcr.Value; 030 031import org.apache.commons.lang.StringUtils; 032 033import org.ametys.plugins.explorer.resources.ResourceCollection; 034import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 035import org.ametys.plugins.repository.AmetysObject; 036import org.ametys.plugins.repository.AmetysObjectIterable; 037import org.ametys.plugins.repository.AmetysObjectIterator; 038import org.ametys.plugins.repository.AmetysRepositoryException; 039import org.ametys.plugins.repository.CollectionIterable; 040import org.ametys.plugins.repository.CopiableAmetysObject; 041import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 042import org.ametys.plugins.repository.RepositoryConstants; 043import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 044import org.ametys.plugins.repository.TraversableAmetysObject; 045import org.ametys.plugins.repository.UnknownAmetysObjectException; 046import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 047import org.ametys.plugins.repository.jcr.SimpleAmetysObject; 048import org.ametys.web.repository.content.jcr.TaggableAmetysObjectHelper; 049import org.ametys.web.repository.page.ModifiablePage; 050import org.ametys.web.repository.page.ModifiableZone; 051import org.ametys.web.repository.page.MoveablePage; 052import org.ametys.web.repository.page.Page; 053import org.ametys.web.repository.page.PagesContainer; 054import org.ametys.web.repository.page.UnknownZoneException; 055import org.ametys.web.repository.site.Site; 056import org.ametys.web.repository.sitemap.Sitemap; 057 058/** 059 * {@link Page} implementation stored into a JCR node using 060 * <code>ametys:defaultPage</code> node type. 061 * 062 * @param <F> the actual type of factory. 063 */ 064public class DefaultPage<F extends DefaultPageFactory> extends DefaultTraversableAmetysObject<F> implements ModifiablePage, CopiableAmetysObject, MoveablePage 065{ 066 /** Constant for title metadata. */ 067 public static final String METADATA_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title"; 068 069 /** Constant for long title metadata. */ 070 public static final String METADATA_LONG_TITLE = "long-title"; 071 072 /** Constant for title metadata. */ 073 public static final String METADATA_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type"; 074 075 /** Constant for title metadata. */ 076 public static final String METADATA_URL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url"; 077 078 /** Constant for title metadata. */ 079 public static final String METADATA_URLTYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url-type"; 080 081 /** Constant for template metadata. */ 082 public static final String METADATA_TEMPLATE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":template"; 083 084 /** Constant for referers metadata. */ 085 public static final String METADATA_REFERERS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":referers"; 086 087 /** Constant for the visible attribute. */ 088 public static final String METADATA_VISIBLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":visible"; 089 090 /** Constant for robots metadata. */ 091 public static final String METADATA_ROBOTS_DISALLOW = "robots-disallow"; 092 093 /** Constant for the zones node name. */ 094 public static final String ZONES_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":zones"; 095 096 /** Constant for the zones node type. */ 097 public static final String ZONES_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":zones"; 098 099 /** Constant for the zone node type. */ 100 public static final String ZONE_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":zone"; 101 102 /** Constant for the attachment node name. */ 103 public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments"; 104 105 /** Constants for site Metadata* */ 106 public static final String METADATA_SITE = "site"; 107 108 /** Constants for sitemap Metadata* */ 109 public static final String METADATA_SITEMAP = "sitemap"; 110 111 /** Constant for publication start date metadata. */ 112 public static final String METADATA_PUBLICATION_START_DATE = "publicationStartDate"; 113 114 /** Constant for publication end date metadata. */ 115 public static final String METADATA_PUBLICATION_END_DATE = "publicationEndDate"; 116 117 /** 118 * Creates a {@link DefaultPage}. 119 * 120 * @param node the node backing this {@link AmetysObject}. 121 * @param parentPath the parent path in the Ametys hierarchy. 122 * @param factory the {@link DefaultPageFactory} which creates the 123 * AmetysObject. 124 */ 125 public DefaultPage(Node node, String parentPath, F factory) 126 { 127 super(node, parentPath, factory); 128 } 129 130 @Override 131 public Set<String> getTags() throws AmetysRepositoryException 132 { 133 return TaggableAmetysObjectHelper.getTags(this); 134 } 135 136 @Override 137 public void tag(String tag) throws AmetysRepositoryException 138 { 139 TaggableAmetysObjectHelper.tag(this, tag); 140 } 141 142 @Override 143 public void untag(String tag) throws AmetysRepositoryException 144 { 145 TaggableAmetysObjectHelper.untag(this, tag); 146 } 147 148 @Override 149 public String getTitle() throws AmetysRepositoryException 150 { 151 try 152 { 153 return getNode().getProperty(METADATA_TITLE).getString(); 154 } 155 catch (RepositoryException e) 156 { 157 throw new AmetysRepositoryException("Unable to get title property", e); 158 } 159 } 160 161 @Override 162 public void setTitle(String title) throws AmetysRepositoryException 163 { 164 try 165 { 166 getNode().setProperty(METADATA_TITLE, title); 167 } 168 catch (RepositoryException e) 169 { 170 throw new AmetysRepositoryException("Unable to set title property", e); 171 } 172 } 173 174 @Override 175 public String getLongTitle() throws AmetysRepositoryException 176 { 177 if (getMetadataHolder().hasMetadata(METADATA_LONG_TITLE)) 178 { 179 String longTitle = getMetadataHolder().getString(METADATA_LONG_TITLE); 180 if (StringUtils.isNotBlank(longTitle)) 181 { 182 return longTitle; 183 } 184 } 185 return this.getTitle(); 186 } 187 188 @Override 189 public void setLongTitle(String title) throws AmetysRepositoryException 190 { 191 String newTitle = StringUtils.trimToNull(title); 192 if (newTitle != null) 193 { 194 getMetadataHolder().setMetadata(METADATA_LONG_TITLE, newTitle); 195 } 196 else if (getMetadataHolder().hasMetadata(METADATA_LONG_TITLE)) 197 { 198 getMetadataHolder().removeMetadata(METADATA_LONG_TITLE); 199 } 200 } 201 202 @Override 203 public Sitemap getSitemap() throws AmetysRepositoryException 204 { 205 try 206 { 207 Node node = getNode(); 208 209 do 210 { 211 node = node.getParent(); 212 } 213 while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE)); 214 215 return _getFactory().resolveAmetysObject(node); 216 } 217 catch (RepositoryException e) 218 { 219 throw new AmetysRepositoryException("Unable to get sitemap", e); 220 } 221 } 222 223 @Override 224 public int getDepth() throws AmetysRepositoryException 225 { 226 try 227 { 228 int depth = 1; 229 Node node = getNode().getParent(); 230 while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE)) 231 { 232 depth++; 233 node = node.getParent(); 234 } 235 236 return depth; 237 } 238 catch (RepositoryException e) 239 { 240 throw new AmetysRepositoryException("Unable to get depth", e); 241 } 242 } 243 244 @Override 245 public Site getSite() throws AmetysRepositoryException 246 { 247 return getSitemap().getSite(); 248 } 249 250 @Override 251 public String getSiteName() 252 { 253 return getMetadataHolder().getString(METADATA_SITE); 254 } 255 256 @Override 257 public void setSiteName(String siteName) 258 { 259 getMetadataHolder().setMetadata(METADATA_SITE, siteName); 260 } 261 262 @Override 263 public String getSitemapName() throws AmetysRepositoryException 264 { 265 return getMetadataHolder().getString(METADATA_SITEMAP); 266 } 267 268 @Override 269 public void setSitemapName(String sitemapName) throws AmetysRepositoryException 270 { 271 getMetadataHolder().setMetadata(METADATA_SITEMAP, sitemapName); 272 } 273 274 @Override 275 public String getPathInSitemap() throws AmetysRepositoryException 276 { 277 String nodePath = getPath(); 278 String sitemapPath = getSitemap().getPath(); 279 return nodePath.substring(sitemapPath.length() + 1); 280 } 281 282 @Override 283 public PageType getType() throws AmetysRepositoryException 284 { 285 try 286 { 287 return PageType.valueOf(getNode().getProperty(METADATA_TYPE).getString()); 288 } 289 catch (RepositoryException e) 290 { 291 throw new AmetysRepositoryException("Unable to get type property", e); 292 } 293 } 294 295 @Override 296 public void setType(PageType type) throws AmetysRepositoryException 297 { 298 try 299 { 300 getNode().setProperty(METADATA_TYPE, type.name()); 301 } 302 catch (RepositoryException e) 303 { 304 throw new AmetysRepositoryException("Unable to set type property", e); 305 } 306 } 307 308 @Override 309 public String getURL() throws AmetysRepositoryException 310 { 311 try 312 { 313 return getNode().getProperty(METADATA_URL).getString(); 314 } 315 catch (RepositoryException e) 316 { 317 throw new AmetysRepositoryException("Unable to get url property", e); 318 } 319 } 320 321 @Override 322 public LinkType getURLType() throws AmetysRepositoryException 323 { 324 try 325 { 326 return LinkType.valueOf(getNode().getProperty(METADATA_URLTYPE).getString()); 327 } 328 catch (RepositoryException e) 329 { 330 throw new AmetysRepositoryException("Unable to get url type property", e); 331 } 332 } 333 334 @Override 335 public void setURL(LinkType type, String url) throws AmetysRepositoryException 336 { 337 try 338 { 339 getNode().setProperty(METADATA_URLTYPE, type.toString()); 340 getNode().setProperty(METADATA_URL, url); 341 } 342 catch (RepositoryException e) 343 { 344 throw new AmetysRepositoryException("Unable to set url property", e); 345 } 346 } 347 348 @Override 349 public String getTemplate() throws AmetysRepositoryException 350 { 351 try 352 { 353 Node node = getNode(); 354 355 if (node.hasProperty(METADATA_TEMPLATE)) 356 { 357 return node.getProperty(METADATA_TEMPLATE).getString(); 358 } 359 else 360 { 361 return null; 362 } 363 } 364 catch (RepositoryException e) 365 { 366 throw new AmetysRepositoryException("Unable to get template property", e); 367 } 368 } 369 370 @Override 371 public void setTemplate(String template) throws AmetysRepositoryException 372 { 373 try 374 { 375 getNode().setProperty(METADATA_TEMPLATE, template); 376 } 377 catch (RepositoryException e) 378 { 379 throw new AmetysRepositoryException("Unable to set template property", e); 380 } 381 } 382 383 @Override 384 public AmetysObjectIterable<ModifiableZone> getZones() throws AmetysRepositoryException 385 { 386 try 387 { 388 return ((TraversableAmetysObject) getChild(ZONES_NODE_NAME)).getChildren(); 389 } 390 catch (UnknownAmetysObjectException e) 391 { 392 Collection<ModifiableZone> emptyCollection = Collections.emptyList(); 393 return new CollectionIterable<>(emptyCollection); 394 } 395 } 396 397 @Override 398 public boolean hasZone(String name) throws AmetysRepositoryException 399 { 400 if (hasChild(ZONES_NODE_NAME)) 401 { 402 return ((TraversableAmetysObject) getChild(ZONES_NODE_NAME)).hasChild(name); 403 } 404 else 405 { 406 return false; 407 } 408 } 409 410 @Override 411 public ModifiableZone getZone(String name) throws UnknownZoneException, AmetysRepositoryException 412 { 413 return ((TraversableAmetysObject) getChild(ZONES_NODE_NAME)).getChild(name); 414 } 415 416 @Override 417 public ModifiableZone createZone(String name) throws AmetysRepositoryException 418 { 419 ModifiableTraversableAmetysObject zones; 420 if (hasChild(ZONES_NODE_NAME)) 421 { 422 zones = getChild(ZONES_NODE_NAME); 423 } 424 else 425 { 426 zones = createChild(ZONES_NODE_NAME, ZONES_NODE_TYPE); 427 } 428 429 return zones.createChild(name, ZONE_NODE_TYPE); 430 } 431 432 @Override 433 public ResourceCollection getRootAttachments() throws AmetysRepositoryException 434 { 435 ResourceCollection attachments; 436 if (hasChild(ATTACHMENTS_NODE_NAME)) 437 { 438 attachments = getChild(ATTACHMENTS_NODE_NAME); 439 } 440 else 441 { 442 attachments = createChild(ATTACHMENTS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE); 443 444 try 445 { 446 getNode().getSession().save(); 447 } 448 catch (RepositoryException e) 449 { 450 throw new AmetysRepositoryException("Unable to save session", e); 451 } 452 } 453 454 return attachments; 455 } 456 457 @Override 458 public Set<String> getReferers() throws AmetysRepositoryException 459 { 460 try 461 { 462 Node node = getNode(); 463 464 if (!node.hasProperty(METADATA_REFERERS)) 465 { 466 return Collections.emptySet(); 467 } 468 469 Value[] values = getNode().getProperty(METADATA_REFERERS).getValues(); 470 Set<String> ids = new HashSet<>(values.length); 471 472 for (Value value : values) 473 { 474 ids.add(value.getString()); 475 } 476 477 return ids; 478 } 479 catch (RepositoryException e) 480 { 481 throw new AmetysRepositoryException("Unable to get referers property", e); 482 } 483 } 484 485 @Override 486 public void addReferer(String ametysObjectId) throws AmetysRepositoryException 487 { 488 try 489 { 490 Set<String> ids = null; 491 Node node = getNode(); 492 493 if (!node.hasProperty(METADATA_REFERERS)) 494 { 495 ids = Collections.singleton(ametysObjectId); 496 } 497 else 498 { 499 Value[] values = getNode().getProperty(METADATA_REFERERS).getValues(); 500 ids = new HashSet<>(values.length + 1); 501 502 for (Value value : values) 503 { 504 ids.add(value.getString()); 505 } 506 507 ids.add(ametysObjectId); 508 } 509 510 node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()])); 511 } 512 catch (RepositoryException e) 513 { 514 throw new AmetysRepositoryException("Unable to add an id into referers property", e); 515 } 516 } 517 518 @Override 519 public void removeReferer(String ametysObjectId) throws AmetysRepositoryException 520 { 521 try 522 { 523 Node node = getNode(); 524 525 if (node.hasProperty(METADATA_REFERERS)) 526 { 527 Value[] values = getNode().getProperty(METADATA_REFERERS).getValues(); 528 Set<String> ids = new HashSet<>(values.length + 1); 529 530 for (Value value : values) 531 { 532 ids.add(value.getString()); 533 } 534 535 ids.remove(ametysObjectId); 536 node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()])); 537 } 538 } 539 catch (RepositoryException e) 540 { 541 throw new AmetysRepositoryException("Unable to remove an id from referers property", e); 542 } 543 } 544 545 @Override 546 public AmetysObjectIterable< ? extends Page> getChildrenPages() throws AmetysRepositoryException 547 { 548 return getChildrenPages(true); 549 } 550 551 @Override 552 public AmetysObjectIterable< ? extends Page> getChildrenPages(boolean includeInvisiblePage) throws AmetysRepositoryException 553 { 554 List<Page> childrenPages = new ArrayList<>(); 555 for (AmetysObject childObject : getChildren()) 556 { 557 if (childObject instanceof Page) 558 { 559 Page page = (Page) childObject; 560 if (includeInvisiblePage || page.isVisible()) 561 { 562 childrenPages.add(page); 563 } 564 } 565 } 566 return new CollectionIterable<>(childrenPages); 567 } 568 569 @Override 570 public Page getChildPageAt(int index) throws UnknownAmetysObjectException, AmetysRepositoryException 571 { 572 if (index < 0) 573 { 574 throw new AmetysRepositoryException("Child page index cannot be negative"); 575 } 576 577 AmetysObjectIterable< ? extends Page> childPages = getChildrenPages(); 578 AmetysObjectIterator< ? extends Page> it = childPages.iterator(); 579 580 try 581 { 582 it.skip(index); 583 return it.next(); 584 } 585 catch (NoSuchElementException e) 586 { 587 throw new UnknownAmetysObjectException("There's no child page at index " + index + " for page " + this.getId()); 588 } 589 } 590 591 @Override 592 public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException 593 { 594 try 595 { 596 int index = 1; 597 String originalName = name == null ? getName() : name; 598 String pageName = originalName; 599 while (parent.hasChild(pageName)) 600 { 601 // Find unused name on new parent node 602 pageName = originalName + "-" + index++; 603 } 604 605 DefaultPage cPage = parent.createChild(pageName, "ametys:defaultPage"); 606 cPage.setType(getType()); 607 cPage.setTitle(getTitle()); 608 609 if (PageType.CONTAINER.equals(getType())) 610 { 611 cPage.setTemplate(getTemplate()); 612 } 613 else if (PageType.LINK.equals(getType())) 614 { 615 cPage.setURL(getURLType(), getURL()); 616 } 617 618 // Copy metadata 619 getMetadataHolder().copyTo(cPage.getMetadataHolder()); 620 621 // Copy zones 622 for (ModifiableZone zone : getZones()) 623 { 624 if (zone instanceof CopiableAmetysObject) 625 { 626 ((CopiableAmetysObject) zone).copyTo(cPage, null); 627 } 628 } 629 630 // Copy tags 631 Set<String> tags = getTags(); 632 for (String tag : tags) 633 { 634 cPage.tag(tag); 635 } 636 637 // Update sitemap name 638 if (parent instanceof PagesContainer) 639 { 640 cPage.setSitemapName(((PagesContainer) parent).getSitemapName()); 641 } 642 643 parent.saveChanges(); 644 645 // Copy attachments 646 ResourceCollection rootAttachments = getRootAttachments(); 647 if (rootAttachments instanceof SimpleAmetysObject) 648 { 649 Node resourcesNode = ((SimpleAmetysObject) rootAttachments).getNode(); 650 getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), cPage.getNode().getPath() + "/" + resourcesNode.getName()); 651 } 652 653 // Copy sub-pages 654 AmetysObjectIterable< ? extends Page> childrenPages = getChildrenPages(); 655 for (Page page : childrenPages) 656 { 657 // Avoid infinite loop : do not copy the page itself 658 if (page instanceof CopiableAmetysObject && restrictTo.contains(page.getId())) 659 { 660 ((CopiableAmetysObject) page).copyTo(cPage, null, restrictTo); 661 } 662 } 663 664 return cPage; 665 } 666 catch (RepositoryException e) 667 { 668 throw new AmetysRepositoryException(e); 669 } 670 } 671 672 @Override 673 public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException 674 { 675 return copyTo (parent, name, new ArrayList<String>()); 676 } 677 678 @Override 679 public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException 680 { 681 Node node = getNode(); 682 683 try 684 { 685 if (getParent().equals(newParent)) 686 { 687 // Just order current node to the end 688 node.getParent().orderBefore(node.getName(), null); 689 } 690 else 691 { 692 if (!canMoveTo(newParent)) 693 { 694 throw new AmetysRepositoryException("DefaultPage " + toString() + " can only be moved to a CompositePages and "); 695 } 696 PagesContainer newParentPage = (PagesContainer) newParent; 697 698 String sitemapNodePath = getSitemap().getNode().getPath(); 699 String newPath = sitemapNodePath + (newParentPage instanceof Sitemap ? "" : '/' + newParentPage.getPathInSitemap()); 700 701 String name = node.getName(); 702 703 if (renameIfExist) 704 { 705 // Find unused name on new parent node 706 int index = 1; 707 while (node.getSession().getRootNode().hasNode(newPath.substring(1) + "/" + name)) 708 { 709 name = name + "-" + index++; 710 } 711 } 712 713 try 714 { 715 // Move node 716 node.getSession().move(node.getPath(), newPath + "/" + name); 717 } 718 catch (ItemExistsException e) 719 { 720 throw new AmetysRepositoryException(String.format("A page already exists for in parent path '%s'", newPath), e); 721 } 722 723 // recompute name in case it has changed 724 _invalidateName(); 725 726 // Invalidate parent path as the parent path has changed 727 _invalidateParentPath(); 728 } 729 } 730 catch (RepositoryException e) 731 { 732 throw new AmetysRepositoryException(String.format("Unable to move page '%s' to node '%s'", this, newParent.getId()), e); 733 } 734 } 735 736 @Override 737 public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException 738 { 739 return newParent instanceof PagesContainer && ((PagesContainer) newParent).getSitemap().equals(getSitemap()); 740 } 741 742 @Override 743 public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException 744 { 745 Node node = getNode(); 746 try 747 { 748 node.getParent().orderBefore(node.getName(), siblingNode != null ? siblingNode.getName() : null); 749 } 750 catch (RepositoryException e) 751 { 752 throw new AmetysRepositoryException(String.format("Unable to order page '%s' before sibling '%s'", this, siblingNode != null ? siblingNode.getName() : ""), e); 753 } 754 } 755 756 @Override 757 public boolean isVisible() throws AmetysRepositoryException 758 { 759 try 760 { 761 if (getNode().hasProperty(METADATA_VISIBLE)) 762 { 763 return getNode().getProperty(METADATA_VISIBLE).getBoolean(); 764 } 765 else 766 { 767 return true; 768 } 769 } 770 catch (RepositoryException e) 771 { 772 throw new AmetysRepositoryException("Unable to get visible property", e); 773 } 774 } 775 776 @Override 777 public void setVisible(boolean isVisible) throws AmetysRepositoryException 778 { 779 try 780 { 781 getNode().setProperty(METADATA_VISIBLE, isVisible); 782 } 783 catch (RepositoryException e) 784 { 785 throw new AmetysRepositoryException("Unable to set visible property", e); 786 } 787 } 788}