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