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.HashSet; 021import java.util.Map; 022import java.util.Set; 023import java.util.stream.Collectors; 024 025import javax.jcr.RepositoryException; 026import javax.jcr.Session; 027import javax.jcr.observation.Event; 028import javax.jcr.observation.ObservationManager; 029 030import org.apache.avalon.framework.activity.Initializable; 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.logger.AbstractLogEnabled; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.avalon.framework.service.Serviceable; 039import org.apache.cocoon.components.ContextHelper; 040import org.apache.cocoon.environment.Request; 041import org.apache.commons.lang.StringUtils; 042 043import org.ametys.core.cache.AbstractCacheManager; 044import org.ametys.core.cache.AbstractCacheManager.CacheType; 045import org.ametys.core.cache.Cache; 046import org.ametys.core.cache.CacheException; 047import org.ametys.core.right.RightManager; 048import org.ametys.core.user.CurrentUserProvider; 049import org.ametys.core.user.UserIdentity; 050import org.ametys.core.user.population.PopulationContextHelper; 051import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 052import org.ametys.plugins.repository.AmetysObject; 053import org.ametys.plugins.repository.AmetysObjectIterable; 054import org.ametys.plugins.repository.AmetysObjectResolver; 055import org.ametys.plugins.repository.AmetysRepositoryException; 056import org.ametys.plugins.repository.CollectionIterable; 057import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 058import org.ametys.plugins.repository.RepositoryConstants; 059import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 060import org.ametys.plugins.repository.TraversableAmetysObject; 061import org.ametys.plugins.repository.UnknownAmetysObjectException; 062import org.ametys.plugins.repository.activities.ActivityHolder; 063import org.ametys.plugins.repository.provider.AbstractRepository; 064import org.ametys.plugins.repository.provider.JackrabbitRepository; 065import org.ametys.plugins.repository.provider.WorkspaceSelector; 066import org.ametys.runtime.i18n.I18nizableText; 067import org.ametys.web.live.LiveWorkspaceExcludedPathExtensionPoint; 068import org.ametys.web.live.LiveWorkspaceListener; 069import org.ametys.web.repository.page.CopySiteComponent; 070import org.ametys.web.synchronization.SynchronizeComponent; 071 072/** 073 * Helper component for managing sites. 074 */ 075public class SiteManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable 076{ 077 /** Avalon Role */ 078 public static final String ROLE = SiteManager.class.getName(); 079 080 /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Site}s objects in cache by {@link RequestSiteCacheKey} (composition of site name and workspace name). */ 081 public static final String REQUEST_CACHE = SiteManager.class.getName() + "$Request"; 082 083 /** Constant for the {@link Cache} id for the {@link Site} ids (as {@link String}s) in cache by site name (for whole application). */ 084 public static final String MEMORY_CACHE = SiteManager.class.getName() + "$UUID"; 085 086 /** sites root's JCR node name */ 087 public static final String ROOT_SITES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sites"; 088 089 /** sites root's JCR path */ 090 public static final String ROOT_SITES_PATH = "/" + ROOT_SITES; 091 092 private static final String __IS_CACHE_FILLED = "###iscachefilled###"; 093 094 private AmetysObjectResolver _resolver; 095 private JackrabbitRepository _repository; 096 private SynchronizeComponent _synchronizeComponent; 097 private CopySiteComponent _copySiteComponent; 098 private WorkspaceSelector _workspaceSelector; 099 private RightManager _rightManager; 100 private CurrentUserProvider _currentUserProvider; 101 private PopulationContextHelper _populationContextHelper; 102 private AbstractCacheManager _cacheManager; 103 private LiveWorkspaceExcludedPathExtensionPoint _siteExcludedPathEP; 104 105 private Context _context; 106 107 @Override 108 public void service(ServiceManager manager) throws ServiceException 109 { 110 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 111 _repository = (JackrabbitRepository) manager.lookup(AbstractRepository.ROLE); 112 _synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE); 113 _copySiteComponent = (CopySiteComponent) manager.lookup(CopySiteComponent.ROLE); 114 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 115 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 116 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 117 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 118 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 119 _siteExcludedPathEP = (LiveWorkspaceExcludedPathExtensionPoint) manager.lookup(LiveWorkspaceExcludedPathExtensionPoint.ROLE + ".site"); 120 } 121 122 @Override 123 public void initialize() throws Exception 124 { 125 _createCaches(); 126 } 127 128 /** 129 * Creates the caches 130 */ 131 protected void _createCaches() 132 { 133 _cacheManager.createMemoryCache(MEMORY_CACHE, 134 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_UUID_CACHE_LABEL"), 135 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_UUID_CACHE_DESCRIPTION"), 136 true, 137 null); 138 _cacheManager.createRequestCache(REQUEST_CACHE, 139 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_REQUEST_CACHE_LABEL"), 140 new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_REQUEST_CACHE_DESCRIPTION"), 141 false); 142 } 143 144 public void contextualize(Context context) throws ContextException 145 { 146 _context = context; 147 } 148 149 private synchronized Map<String, String> _getUUIDCache() 150 { 151 if (!_getMemoryCache().hasKey(__IS_CACHE_FILLED)) 152 { 153 Session defaultSession = null; 154 try 155 { 156 // Force default workspace to execute query 157 defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE); 158 159 String jcrQuery = "//element(*, ametys:site)"; 160 161 AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery, defaultSession); 162 163 for (Site site : sites) 164 { 165 _getMemoryCache().put(site.getName(), site.getId()); 166 } 167 168 _getMemoryCache().put(__IS_CACHE_FILLED, null); 169 } 170 catch (RepositoryException e) 171 { 172 throw new AmetysRepositoryException(e); 173 } 174 finally 175 { 176 if (defaultSession != null) 177 { 178 defaultSession.logout(); 179 } 180 } 181 } 182 183 Map<String, String> cacheAsMap = _getMemoryCache().asMap(); 184 cacheAsMap.remove(__IS_CACHE_FILLED); 185 return cacheAsMap; 186 } 187 188 private Request _getRequest () 189 { 190 try 191 { 192 return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT); 193 } 194 catch (ContextException ce) 195 { 196 getLogger().info("Unable to get the request", ce); 197 return null; 198 } 199 } 200 201 /** 202 * Returns the sites names. 203 * @return the sites names. 204 */ 205 public Collection<String> getRootSiteNames() 206 { 207 TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH); 208 AmetysObjectIterable<AmetysObject> sites = root.getChildren(); 209 210 ArrayList<String> result = new ArrayList<>(); 211 212 for (AmetysObject object : sites) 213 { 214 result.add(object.getName()); 215 } 216 217 return result; 218 } 219 220 /** 221 * Returns the sites names. 222 * @return the sites names. 223 */ 224 public Collection<String> getSiteNames() 225 { 226 // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 227 return _getUUIDCache().entrySet().stream() 228 .filter(e -> _resolver.hasAmetysObjectForId(e.getValue())) 229 .map(Map.Entry::getKey) 230 .collect(Collectors.toList()); 231 } 232 233 /** 234 * Get the granted site names for the current user 235 * @return The name of sites the current user can access 236 */ 237 public Set<String> getGrantedSites() 238 { 239 return getGrantedSites(_currentUserProvider.getUser()); 240 } 241 242 /** 243 * Get the granted site names for user 244 * @param user The user 245 * @return The name of sites the user can access 246 */ 247 public Set<String> getGrantedSites(UserIdentity user) 248 { 249 Set<String> grantedSiteNames = new HashSet<>(); 250 251 Request request = _getRequest(); 252 253 for (String siteName : getSiteNames()) 254 { 255 if (_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(user.getPopulationId())) 256 { 257 try 258 { 259 request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site 260 if (!_rightManager.getUserRights(user, "/cms").isEmpty()) 261 { 262 grantedSiteNames.add(siteName); 263 } 264 } 265 finally 266 { 267 request.setAttribute("siteName", null); 268 } 269 } 270 } 271 272 return grantedSiteNames; 273 } 274 275 /** 276 * Determines if the user has granted access to the site 277 * @param user The user 278 * @param siteName The site name 279 * @return <code>true</code> if the user can access to the site 280 */ 281 public boolean isGrantedSite (UserIdentity user, String siteName) 282 { 283 Object oldValue = null; 284 285 Request request = _getRequest(); 286 try 287 { 288 oldValue = request.getAttribute("siteName"); 289 request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site 290 return !_rightManager.getUserRights(user, "/cms").isEmpty(); 291 } 292 finally 293 { 294 request.setAttribute("siteName", oldValue); 295 } 296 } 297 298 /** 299 * Returns the root for sites. 300 * @return the root for sites. 301 */ 302 public ModifiableTraversableAmetysObject getRoot() 303 { 304 return _resolver.resolveByPath(ROOT_SITES_PATH); 305 } 306 307 /** 308 * Returns the root sites. 309 * @return the root sites. 310 */ 311 public AmetysObjectIterable<Site> getRootSites() 312 { 313 TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH); 314 return root.getChildren(); 315 } 316 317 /** 318 * Returns the all sites. 319 * @return the all sites. 320 */ 321 public AmetysObjectIterable<Site> getSites() 322 { 323 // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 324 Set<Site> sites = _getUUIDCache().values().stream() 325 .filter(_resolver::hasAmetysObjectForId) 326 .map(_resolver::<Site>resolveById) 327 .collect(Collectors.toSet()); 328 329 return new CollectionIterable<>(sites); 330 } 331 332 /** 333 * Creates a site with the given name. 334 * @param siteName the site name. 335 * @param parentId the id of the parent site. Can be null. 336 * @return the newly created {@link Site}. 337 * @throws RepositoryIntegrityViolationException if the named site already exists. 338 */ 339 public Site createSite(String siteName, String parentId) throws RepositoryIntegrityViolationException 340 { 341 ModifiableTraversableAmetysObject root = null; 342 if (parentId != null) 343 { 344 Site site = _resolver.resolveById(parentId); 345 if (!site.hasChild(ROOT_SITES)) 346 { 347 site.createChild(ROOT_SITES, "ametys:sites"); 348 } 349 root = site.getChild(ROOT_SITES); 350 } 351 else 352 { 353 root = _resolver.resolveByPath(ROOT_SITES_PATH); 354 } 355 356 Site site = (Site) root.createChild(siteName, "ametys:site"); 357 String sitePath; 358 try 359 { 360 sitePath = site.getNode().getPath(); 361 362 // Create the resource explorer root. 363 site.getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources", "ametys:resources-collection"); 364 } 365 catch (RepositoryException ex) 366 { 367 throw new AmetysRepositoryException(ex); 368 } 369 finally 370 { 371 _getMemoryCache().invalidateAll(); 372 } 373 374 try 375 { 376 Session session = _repository.getAdminSession(); 377 ObservationManager manager = session.getWorkspace().getObservationManager(); 378 379 manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger(), _siteExcludedPathEP.getExcludedPaths()), 380 Event.NODE_ADDED 381 | Event.NODE_REMOVED 382 | Event.NODE_MOVED 383 | Event.PROPERTY_ADDED 384 | Event.PROPERTY_CHANGED 385 | Event.PROPERTY_REMOVED, 386 sitePath + "/ametys-internal:plugins", true, null, null, false); 387 388 manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 389 Event.NODE_ADDED 390 | Event.NODE_REMOVED 391 | Event.NODE_MOVED 392 | Event.PROPERTY_ADDED 393 | Event.PROPERTY_CHANGED 394 | Event.PROPERTY_REMOVED, 395 sitePath + "/ametys-internal:resources", true, null, null, false); 396 397 manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 398 Event.NODE_ADDED 399 | Event.NODE_REMOVED 400 | Event.NODE_MOVED 401 | Event.PROPERTY_ADDED 402 | Event.PROPERTY_CHANGED 403 | Event.PROPERTY_REMOVED, 404 sitePath + "/" + ActivityHolder.ACTIVITIES_ROOT_NODE_NAME, true, null, null, false); 405 } 406 catch (RepositoryException ex) 407 { 408 throw new AmetysRepositoryException(ex); 409 } 410 finally 411 { 412 _getMemoryCache().invalidateAll(); 413 } 414 415 return site; 416 } 417 418 /** 419 * Creates a site with the given name. 420 * @param siteName the site name. 421 * @return the newly created {@link Site}. 422 * @throws RepositoryIntegrityViolationException if the named site already exists. 423 */ 424 public Site createSite(String siteName) throws RepositoryIntegrityViolationException 425 { 426 return createSite(siteName, null); 427 } 428 429 /** 430 * Creates a site with the given name, from another site to copy 431 * @param site the site to be copied 432 * @param siteName the site name 433 * @return the newly created {@link Site}. 434 * @throws RepositoryIntegrityViolationException if the named site already exists. 435 */ 436 public Site copySite(Site site, String siteName) throws RepositoryIntegrityViolationException 437 { 438 return copySite(site, null, siteName); 439 } 440 441 /** 442 * Creates a site with the given name, from another site to copy 443 * @param site the site to be copied 444 * @param parentId the id of the parent site. Can be null. 445 * @param siteName the site name 446 * @return the newly created {@link Site}. 447 * @throws RepositoryIntegrityViolationException if the named site already exists. 448 */ 449 public Site copySite(Site site, String parentId, String siteName) throws RepositoryIntegrityViolationException 450 { 451 ModifiableTraversableAmetysObject root = null; 452 if (parentId != null) 453 { 454 root = _resolver.resolveById(parentId); 455 } 456 else 457 { 458 root = _resolver.resolveByPath(ROOT_SITES_PATH); 459 } 460 461 Site cSite = site.copyTo(root, siteName); 462 463 _getMemoryCache().invalidateAll(); 464 465 // Clear url and title 466 cSite.removeValue("url"); 467 cSite.removeValue("title"); 468 469 // Change reference to ametys object, re-init contents workflow, update site name, ... 470 _copySiteComponent.updateSiteAfterCopy(site, cSite); 471 472 return cSite; 473 474 } 475 476 /** 477 * Returns true if the given site exists. 478 * @param siteName the site name. 479 * @return true if the given site exists. 480 */ 481 public boolean hasSite(String siteName) 482 { 483 Map<String, String> uuidCache = _getUUIDCache(); 484 if (uuidCache.containsKey(siteName)) 485 { 486 // As cache is computed from default JCR workspace, we need to check if the site exists into the current JCR workspace 487 return _resolver.hasAmetysObjectForId(uuidCache.get(siteName)); 488 } 489 return false; 490 } 491 492 /** 493 * Returns the named {@link Site}. 494 * @param siteName the site name. 495 * @return the named {@link Site} or null if the siteName is null 496 * @throws UnknownAmetysObjectException if the named site does not exist 497 */ 498 public Site getSite(String siteName) throws UnknownAmetysObjectException 499 { 500 return getSite(siteName, null); 501 } 502 503 /** 504 * Returns the named {@link Site}. 505 * @param siteName the site name. 506 * @param session the JCR Session to use to retrieve the site 507 * @return the named {@link Site} or null if the siteName is null 508 * @throws UnknownAmetysObjectException if the named site does not exist 509 */ 510 public Site getSite(String siteName, Session session) throws UnknownAmetysObjectException 511 { 512 if (StringUtils.isBlank(siteName)) 513 { 514 return null; 515 } 516 517 Request request = _getRequest(); 518 if (request == null) 519 { 520 // There is no request to store cache 521 return _computeSite(siteName, session); 522 } 523 524 Cache<RequestSiteCacheKey, Site> sitesCache = _getRequestCache(); 525 526 // The site key in the cache is of the form {site + workspace}. 527 String workspaceName = session != null 528 ? session.getWorkspace().getName() 529 : _workspaceSelector.getWorkspace(); 530 RequestSiteCacheKey siteKey = RequestSiteCacheKey.of(siteName, workspaceName); 531 532 try 533 { 534 Site site = sitesCache.get(siteKey, __ -> _computeSite(siteName, session)); 535 return site; 536 } 537 catch (CacheException e) 538 { 539 if (e.getCause() instanceof UnknownAmetysObjectException) 540 { 541 throw (UnknownAmetysObjectException) e.getCause(); 542 } 543 else 544 { 545 throw e; 546 } 547 } 548 } 549 550 private Site _computeSite(String siteName, Session session) 551 { 552 try 553 { 554 if (hasSite(siteName)) 555 { 556 String uuid = _getUUIDCache().get(siteName); 557 return _resolver.<Site>resolveById(uuid, session); 558 } 559 else 560 { 561 // Site may be present in cache for 'default' workspace but does not exist in current JCR workspace 562 throw new UnknownAmetysObjectException("There is no site named '" + siteName + "'"); 563 } 564 } 565 catch (RepositoryException e) 566 { 567 throw new AmetysRepositoryException(e); 568 } 569 } 570 571 /** 572 * Clear the site cache 573 */ 574 public void clearCache () 575 { 576 _getMemoryCache().invalidateAll(); 577 _getRequestCache().invalidateAll(); 578 } 579 580 private Cache<String, String> _getMemoryCache() 581 { 582 return _cacheManager.get(MEMORY_CACHE); 583 } 584 585 private Cache<RequestSiteCacheKey, Site> _getRequestCache() 586 { 587 return _cacheManager.get(REQUEST_CACHE); 588 } 589 590 private static final class RequestSiteCacheKey extends AbstractCacheKey 591 { 592 private RequestSiteCacheKey(String siteName, String workspaceName) 593 { 594 super(siteName, workspaceName); 595 } 596 597 static RequestSiteCacheKey of(String siteName, String workspaceName) 598 { 599 return new RequestSiteCacheKey(siteName, workspaceName); 600 } 601 } 602}