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.site; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.List; 021 022import javax.jcr.ItemExistsException; 023import javax.jcr.Node; 024import javax.jcr.NodeIterator; 025import javax.jcr.Property; 026import javax.jcr.PropertyIterator; 027import javax.jcr.RepositoryException; 028 029import org.apache.cocoon.util.HashUtil; 030import org.apache.commons.lang.StringUtils; 031 032import org.ametys.cms.repository.Content; 033import org.ametys.cms.repository.ModifiableContent; 034import org.ametys.plugins.repository.AmetysObject; 035import org.ametys.plugins.repository.AmetysObjectIterable; 036import org.ametys.plugins.repository.AmetysRepositoryException; 037import org.ametys.plugins.repository.CollectionIterable; 038import org.ametys.plugins.repository.CopiableAmetysObject; 039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 040import org.ametys.plugins.repository.MovableAmetysObject; 041import org.ametys.plugins.repository.RepositoryConstants; 042import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 043import org.ametys.plugins.repository.TraversableAmetysObject; 044import org.ametys.plugins.repository.UnknownAmetysObjectException; 045import org.ametys.plugins.repository.collection.AmetysObjectCollection; 046import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory; 047import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject; 048import org.ametys.plugins.repository.jcr.SimpleAmetysObject; 049import org.ametys.plugins.repository.metadata.UnknownMetadataException; 050import org.ametys.web.pageaccess.RestrictedPagePolicy; 051import org.ametys.web.repository.sitemap.Sitemap; 052 053/** 054 * {@link AmetysObject} for storing site informations. 055 */ 056public final class Site extends DefaultTraversableAmetysObject<SiteFactory> implements MovableAmetysObject, CopiableAmetysObject 057{ 058 /** Site node type name. */ 059 public static final String NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":site"; 060 061 /** Constants for title metadata. */ 062 private static final String __METADATA_TITLE = "title"; 063 /** Constants for type metadata. */ 064 private static final String __METADATA_TYPE = "type"; 065 /** Constants for description metadata. */ 066 private static final String __METADATA_DESCRIPTION = "description"; 067 /** Constants for URL metadata. */ 068 private static final String __METADATA_URL = "url"; 069 /** Constants for color metadata. */ 070 private static final String __METADATA_COLOR = "color"; 071 /** Constants for skin metadata. */ 072 private static final String __METADATA_SKIN = "skin"; 073 /** Constants for restricted page policy metadata. */ 074 private static final String __METADATA_DISPLAY_RESTRICTED_PAGES = "display-restricted-pages"; 075 /** Constants for sitemaps node name. */ 076 private static final String __SITEMAPS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps"; 077 /** Constants for contents node name. */ 078 private static final String __CONTENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents"; 079 /** Constants for resources node name. */ 080 private static final String __RESOURCES_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; 081 /** Constants for plugins node name. */ 082 private static final String __PLUGINS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"; 083 084 /** 085 * Creates a {@link Site}. 086 * @param node the node backing this {@link AmetysObject}. 087 * @param parentPath the parent path in the Ametys hierarchy. 088 * @param factory the {@link SiteFactory} which creates the AmetysObject. 089 */ 090 public Site(Node node, String parentPath, SiteFactory factory) 091 { 092 super(node, parentPath, factory); 093 } 094 095 /** 096 * Retrieves the title. 097 * @return the title. 098 * @throws AmetysRepositoryException if an error occurs. 099 */ 100 public String getTitle() throws AmetysRepositoryException 101 { 102 try 103 { 104 return getMetadataHolder().getString(__METADATA_TITLE); 105 } 106 catch (UnknownMetadataException e) 107 { 108 return null; 109 } 110 } 111 112 /** 113 * Retrieves the type. 114 * @return the type. 115 * @throws AmetysRepositoryException if an error occurs. 116 */ 117 public String getType() throws AmetysRepositoryException 118 { 119 // return default site type for null metadata for 3.0 compatibility 120 return getMetadataHolder().getString(__METADATA_TYPE, SiteType.DEFAULT_SITE_TYPE_ID); 121 } 122 123 124 /** 125 * Set the type. 126 * @param type the type. 127 * @throws AmetysRepositoryException if an error occurs. 128 */ 129 public void setType(String type) throws AmetysRepositoryException 130 { 131 getMetadataHolder().setMetadata(__METADATA_TYPE, type); 132 } 133 134 /** 135 * Set the title. 136 * @param title the title. 137 * @throws AmetysRepositoryException if an error occurs. 138 */ 139 public void setTitle(String title) throws AmetysRepositoryException 140 { 141 getMetadataHolder().setMetadata(__METADATA_TITLE, title); 142 } 143 144 /** 145 * Retrieves the description. 146 * @return the description. 147 * @throws AmetysRepositoryException if an error occurs. 148 */ 149 public String getDescription() throws AmetysRepositoryException 150 { 151 try 152 { 153 return getMetadataHolder().getString(__METADATA_DESCRIPTION); 154 } 155 catch (UnknownMetadataException e) 156 { 157 return null; 158 } 159 } 160 161 /** 162 * Set the description. 163 * @param description the description. 164 * @throws AmetysRepositoryException if an error occurs. 165 */ 166 public void setDescription(String description) throws AmetysRepositoryException 167 { 168 getMetadataHolder().setMetadata(__METADATA_DESCRIPTION, description); 169 } 170 171 /** 172 * Retrieves the main url. 173 * @return the main url. 174 * @throws AmetysRepositoryException if an error occurs. 175 */ 176 public String getUrl() throws AmetysRepositoryException 177 { 178 String[] aliases = getUrlAliases(); 179 180 return aliases != null && aliases.length > 0 ? aliases[0] : null; 181 } 182 183 /** 184 * Retrieves the url aliases. 185 * @return the url. 186 * @throws AmetysRepositoryException if an error occurs. 187 */ 188 public String[] getUrlAliases() throws AmetysRepositoryException 189 { 190 try 191 { 192 String url = getMetadataHolder().getString(__METADATA_URL, ""); 193 String[] aliases = StringUtils.split(url, ','); 194 195 for (int i = 0; i < aliases.length; i++) 196 { 197 aliases[i] = aliases[i].trim(); 198 } 199 200 return aliases; 201 } 202 catch (UnknownMetadataException e) 203 { 204 return null; 205 } 206 } 207 208 /** 209 * Set the url. 210 * @param url the url. 211 * @throws AmetysRepositoryException if an error occurs. 212 */ 213 public void setUrl(String url) throws AmetysRepositoryException 214 { 215 getMetadataHolder().setMetadata(__METADATA_URL, url); 216 } 217 218 /** 219 * Retrieves the color 220 * @return The color. 221 * @throws AmetysRepositoryException if an error occurs. 222 */ 223 public String getColor() throws AmetysRepositoryException 224 { 225 return getMetadataHolder().getString(__METADATA_COLOR, ""); 226 } 227 228 229 /** 230 * Retrieves a sitemap. 231 * @param sitemapName the sitemap name. 232 * @return the sitemap found. 233 * @throws AmetysRepositoryException if an error occurs. 234 * @throws UnknownAmetysObjectException if the object does not exist. 235 */ 236 public Sitemap getSitemap(String sitemapName) throws AmetysRepositoryException, UnknownAmetysObjectException 237 { 238 return ((TraversableAmetysObject) getChild(__SITEMAPS_NODE_NAME)).getChild(sitemapName); 239 } 240 241 /** 242 * Determines the existence of a sitemap 243 * @param lang The sitemap language 244 * @return true if the sitemap exists 245 * @throws AmetysRepositoryException if an error occurred 246 */ 247 public boolean hasSitemap (String lang) throws AmetysRepositoryException 248 { 249 DefaultTraversableAmetysObject<?> sitemaps = getChild(__SITEMAPS_NODE_NAME); 250 return sitemaps.hasChild(lang); 251 } 252 253 /** 254 * Add a sitemap language to the site 255 * @param lang The sitemap language 256 * @return The created sitemap 257 * @throws AmetysRepositoryException if an error occurred or if the sitemap already exists 258 */ 259 public Sitemap addSitemap (String lang) throws AmetysRepositoryException 260 { 261 DefaultTraversableAmetysObject<?> sitemaps = getChild(__SITEMAPS_NODE_NAME); 262 if (sitemaps.hasChild(lang)) 263 { 264 throw new AmetysRepositoryException ("The sitemap '" + lang + "' already exists"); 265 } 266 return sitemaps.createChild(lang, "ametys:sitemap"); 267 } 268 269 /** 270 * Retrieves sitemaps. 271 * @return the sitemaps or an empty {@link AmetysObjectIterable}. 272 * @throws AmetysRepositoryException if an error occurs. 273 */ 274 public AmetysObjectIterable<Sitemap> getSitemaps() throws AmetysRepositoryException 275 { 276 return ((TraversableAmetysObject) getChild(__SITEMAPS_NODE_NAME)).getChildren(); 277 } 278 279 /** 280 * Retrieves root contents. 281 * @return the root for contents 282 * @throws AmetysRepositoryException if an error occurs. 283 */ 284 public ModifiableTraversableAmetysObject getRootContents() throws AmetysRepositoryException 285 { 286 return getChild(__CONTENTS_NODE_NAME); 287 } 288 289 /** 290 * Retrieves contents. 291 * @return the contents or an empty {@link AmetysObjectIterable}. 292 * @throws AmetysRepositoryException if an error occurs. 293 */ 294 public AmetysObjectIterable<Content> getContents() throws AmetysRepositoryException 295 { 296 return ((TraversableAmetysObject) getChild(__CONTENTS_NODE_NAME)).getChildren(); 297 } 298 299 /** 300 * Retrieves root resources. 301 * @return the root for resources 302 * @throws AmetysRepositoryException if an error occurs. 303 */ 304 public ModifiableTraversableAmetysObject getRootResources() throws AmetysRepositoryException 305 { 306 return getChild(__RESOURCES_NODE_NAME); 307 } 308 309 /** 310 * Retrieves resources. 311 * @return the resources or an empty {@link AmetysObjectIterable}. 312 * @throws AmetysRepositoryException if an error occurs. 313 */ 314 public AmetysObjectIterable<AmetysObject> getResources() throws AmetysRepositoryException 315 { 316 return ((TraversableAmetysObject) getChild(__RESOURCES_NODE_NAME)).getChildren(); 317 } 318 319 /** 320 * Get the root for plugins 321 * @return the root for plugins 322 * @throws AmetysRepositoryException if an error occurs. 323 */ 324 public ModifiableTraversableAmetysObject getRootPlugins () throws AmetysRepositoryException 325 { 326 return (ModifiableTraversableAmetysObject) getChild(__PLUGINS_NODE_NAME); 327 } 328 329 /** 330 * Set the skin id for this site. 331 * @param skinId ths skin id 332 */ 333 public void setSkinId(String skinId) 334 { 335 getMetadataHolder().setMetadata(__METADATA_SKIN, skinId); 336 } 337 338 /** 339 * Returns the skin id for this site. 340 * @return the skin id for this site. 341 */ 342 public String getSkinId() 343 { 344 return getMetadataHolder().getString(__METADATA_SKIN); 345 } 346 347 /** 348 * Returns the parent site, if any. 349 * @return the parent site, if any. 350 */ 351 public Site getParentSite() 352 { 353 AmetysObject parent = getParent(); 354 if (SiteManager.ROOT_SITES_PATH.equals(parent.getPath())) 355 { 356 return null; 357 } 358 return parent.getParent(); 359 } 360 361 /** 362 * Returns the site path 363 * @return the site path 364 */ 365 public String getSitePath () 366 { 367 String path = getName(); 368 369 Site parentSite = getParentSite(); 370 while (parentSite != null) 371 { 372 path = parentSite.getName() + "/" + path; 373 parentSite = parentSite.getParentSite(); 374 } 375 376 return path; 377 } 378 379 /** 380 * Returns the {@link RestrictedPagePolicy} associated with this Site. 381 * @return the {@link RestrictedPagePolicy} associated with this Site. 382 */ 383 public RestrictedPagePolicy getRestrictedPagePolicy() 384 { 385 boolean displayRestrictedPages = getMetadataHolder().getBoolean(__METADATA_DISPLAY_RESTRICTED_PAGES, true); 386 387 if (displayRestrictedPages) 388 { 389 return RestrictedPagePolicy.DISPLAYED; 390 } 391 else 392 { 393 return RestrictedPagePolicy.HIDDEN; 394 } 395 } 396 397 /** 398 * Returns the named {@link Site}. 399 * @param siteName the site name. 400 * @return the named {@link Site}. 401 * @throws UnknownAmetysObjectException if the named site does not exist. 402 */ 403 public Site getSite (String siteName) throws UnknownAmetysObjectException 404 { 405 DefaultTraversableAmetysObject root = getChild(SiteManager.ROOT_SITES); 406 return (Site) root.getChild(siteName); 407 } 408 409 /** 410 * Return true if the given site if an ancestor 411 * @param siteName the site name. 412 * @return true if the given site if an ancestor 413 */ 414 public boolean hasAncestor (String siteName) 415 { 416 // Is a parent ? 417 Site parentSite = getParentSite(); 418 while (parentSite != null) 419 { 420 if (parentSite.getName().equals(siteName)) 421 { 422 return true; 423 } 424 parentSite = parentSite.getParentSite(); 425 } 426 427 return false; 428 } 429 430 /** 431 * Return true if the given site if a descendant 432 * @param siteName the site name. 433 * @return true if the given site if a descendant 434 */ 435 public boolean hasDescendant (String siteName) 436 { 437 if (hasChildrenSite(siteName)) 438 { 439 return true; 440 } 441 442 AmetysObjectIterable<Site> childrenSites = getChildrenSites(); 443 for (Site child : childrenSites) 444 { 445 if (child.hasDescendant (siteName)) 446 { 447 return true; 448 } 449 } 450 451 return false; 452 } 453 454 /** 455 * Returns true if the given site exists. 456 * @param siteName the site name. 457 * @return true if the given site exists. 458 */ 459 public boolean hasChildrenSite(String siteName) 460 { 461 try 462 { 463 DefaultTraversableAmetysObject root = getChild(SiteManager.ROOT_SITES); 464 return root.hasChild(siteName); 465 } 466 catch (UnknownAmetysObjectException e) 467 { 468 return false; 469 } 470 } 471 472 /** 473 * Returns the sites names. 474 * @return the sites names. 475 */ 476 public Collection<String> getChildrenSiteNames() 477 { 478 AmetysObjectIterable<Site> sites = getChildrenSites(); 479 480 ArrayList<String> result = new ArrayList<>(); 481 482 for (Site site : sites) 483 { 484 result.add(site.getName()); 485 } 486 487 return result; 488 } 489 490 /** 491 * Returns all children sites, or empty if none. 492 * @return all children sites, or empty if none. 493 * @throws AmetysRepositoryException if an error occurs 494 */ 495 public AmetysObjectIterable<Site> getChildrenSites() throws AmetysRepositoryException 496 { 497 try 498 { 499 TraversableAmetysObject rootSites = getChild(SiteManager.ROOT_SITES); 500 return rootSites.getChildren(); 501 } 502 catch (UnknownAmetysObjectException e) 503 { 504 return new CollectionIterable<>(new ArrayList<Site>()); 505 } 506 } 507 508 @Override 509 public void remove() throws AmetysRepositoryException, RepositoryIntegrityViolationException 510 { 511 for (Content content : getContents()) 512 { 513 // FIXME API check if not modifiable 514 if (content instanceof ModifiableContent) 515 { 516 ((ModifiableContent) content).remove(); 517 } 518 } 519 520 for (Site site : getChildrenSites()) 521 { 522 site.remove(); 523 } 524 525 super.remove(); 526 } 527 528 @Override 529 public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException 530 { 531 return newParent instanceof Site || newParent instanceof AmetysObjectCollection; 532 } 533 534 @Override 535 public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException 536 { 537 Node node = getNode(); 538 539 try 540 { 541 if (getParent().equals(newParent)) 542 { 543 // Do nothing 544 } 545 else 546 { 547 if (!canMoveTo(newParent)) 548 { 549 throw new AmetysRepositoryException("Site " + toString() + " can only be moved to a site or the root of site"); 550 } 551 552 String name = node.getName(); 553 554 try 555 { 556 // Move node 557 if (newParent instanceof Site) 558 { 559 Site parentSite = (Site) newParent; 560 if (!parentSite.hasChild("ametys-internal:sites")) 561 { 562 parentSite.createChild("ametys-internal:sites", "ametys:sites"); 563 } 564 node.getSession().move(node.getPath(), parentSite.getNode().getPath() + "/ametys-internal:sites/" + name); 565 } 566 else 567 { 568 Node contextNode = ((AmetysObjectCollection) newParent).getNode(); 569 String[] hash = _getHashedPath (name); 570 for (String hashPart : hash) 571 { 572 if (!contextNode.hasNode(hashPart)) 573 { 574 contextNode = contextNode.addNode(hashPart, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE); 575 } 576 else 577 { 578 contextNode = contextNode.getNode(hashPart); 579 } 580 } 581 node.getSession().move(node.getPath(), contextNode.getPath() + "/" + name); 582 } 583 584 } 585 catch (ItemExistsException e) 586 { 587 throw new AmetysRepositoryException(String.format("A site already exists for in parent path '%s'", newParent.getPath() + "/" + name), e); 588 } 589 590 // Invalidate parent path as the parent path has changed 591 _invalidateParentPath(); 592 } 593 } 594 catch (RepositoryException e) 595 { 596 throw new AmetysRepositoryException(String.format("Unable to move site '%s' to node '%s'", this, newParent.getId()), e); 597 } 598 } 599 600 @Override 601 public Site copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException 602 { 603 try 604 { 605 Node parentNode = _getParentNode(parent, name); 606 Node clonedSite = parentNode.addNode(name, "ametys:site"); 607 608 parent.saveChanges(); 609 610 // Copy properties 611 PropertyIterator properties = getNode().getProperties("ametys:*"); 612 while (properties.hasNext()) 613 { 614 Property property = (Property) properties.next(); 615 616 if (property.getDefinition().isMultiple()) 617 { 618 clonedSite.setProperty(property.getName(), property.getValues()); 619 } 620 else 621 { 622 clonedSite.setProperty(property.getName(), property.getValue()); 623 } 624 } 625 626 // Copy resources 627 Node resourcesNode = getNode().getNode("ametys-internal:resources"); 628 getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), clonedSite.getPath() + "/" + "ametys-internal:resources"); 629 630 // Copy plugins (ametys-internal:plugins is auto-created) 631 NodeIterator plugins = getNode().getNode("ametys-internal:plugins").getNodes(); 632 while (plugins.hasNext()) 633 { 634 Node pluginNode = (Node) plugins.next(); 635 getNode().getSession().getWorkspace().copy(pluginNode.getPath(), clonedSite.getPath() + "/ametys-internal:plugins/" + pluginNode.getName()); 636 } 637 638 // Copy sitemaps (ametys-internal:sitemaps is auto-created) 639 NodeIterator sitemaps = getNode().getNode("ametys-internal:sitemaps").getNodes(); 640 while (sitemaps.hasNext()) 641 { 642 Node sitemapNode = (Node) sitemaps.next(); 643 getNode().getSession().getWorkspace().copy(sitemapNode.getPath(), clonedSite.getPath() + "/ametys-internal:sitemaps/" + sitemapNode.getName()); 644 } 645 646 // Copy contents (ametys-internal:contents is auto-created) 647 NodeIterator contents = getNode().getNode("ametys-internal:contents").getNodes(); 648 while (contents.hasNext()) 649 { 650 Node contentNode = (Node) contents.next(); 651 getNode().getSession().getWorkspace().copy(contentNode.getPath(), clonedSite.getPath() + "/ametys-internal:contents/" + contentNode.getName()); 652 } 653 654 if (parent instanceof Site) 655 { 656 return ((Site) parent).getSite(name); 657 } 658 else 659 { 660 return parent.getChild(name); 661 } 662 } 663 catch (RepositoryException e) 664 { 665 throw new AmetysRepositoryException(e); 666 } 667 } 668 669 @Override 670 public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException 671 { 672 return copyTo(parent, name); 673 } 674 675 private Node _getParentNode (ModifiableTraversableAmetysObject parent, String siteName) throws AmetysRepositoryException 676 { 677 try 678 { 679 if (parent instanceof Site) 680 { 681 return ((Site) parent).getNode().getNode("ametys-internal:sites"); 682 } 683 else if (parent instanceof SimpleAmetysObject) 684 { 685 String[] hash = _getHashedPath(siteName); 686 687 Node contextNode = ((SimpleAmetysObject) parent).getNode(); 688 689 for (String hashPart : hash) 690 { 691 if (!contextNode.hasNode(hashPart)) 692 { 693 contextNode = contextNode.addNode(hashPart, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE); 694 } 695 else 696 { 697 contextNode = contextNode.getNode(hashPart); 698 } 699 } 700 701 return contextNode; 702 } 703 } 704 catch (RepositoryException e) 705 { 706 throw new AmetysRepositoryException("Unable to create child: " + siteName, e); 707 } 708 709 throw new AmetysRepositoryException("Unable to get JCR node of object : " + parent.getId()); 710 } 711 712 private String[] _getHashedPath(String name) 713 { 714 long hash = Math.abs(HashUtil.hash(name)); 715 String hashStr = Long.toString(hash, 16); 716 hashStr = StringUtils.leftPad(hashStr, 4, '0'); 717 718 return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)}; 719 } 720 721 @Override 722 public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException 723 { 724 throw new UnsupportedOperationException("Site ordering is not supported"); 725 } 726}