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