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