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