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