001/* 002 * Copyright 2015 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.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026 027import javax.jcr.RepositoryException; 028import javax.jcr.Session; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.core.group.GroupDirectoryContextHelper; 037import org.ametys.core.observation.Event; 038import org.ametys.core.observation.ObservationManager; 039import org.ametys.core.ui.Callable; 040import org.ametys.core.user.CurrentUserProvider; 041import org.ametys.core.user.population.PopulationContextHelper; 042import org.ametys.core.util.I18nUtils; 043import org.ametys.plugins.repository.AmetysObject; 044import org.ametys.plugins.repository.AmetysObjectIterable; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.repository.AmetysRepositoryException; 047import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 048import org.ametys.plugins.repository.UnknownAmetysObjectException; 049import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 050import org.ametys.plugins.repository.jcr.JCRAmetysObject; 051import org.ametys.runtime.i18n.I18nizableText; 052import org.ametys.runtime.model.ElementDefinition; 053import org.ametys.runtime.model.ModelHelper; 054import org.ametys.runtime.model.disableconditions.DefaultDisableConditionsEvaluator; 055import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator; 056import org.ametys.runtime.model.type.DataContext; 057import org.ametys.runtime.model.type.ElementType; 058import org.ametys.runtime.parameter.ValidationResult; 059import org.ametys.runtime.plugin.component.AbstractLogEnabled; 060import org.ametys.web.ObservationConstants; 061import org.ametys.web.cache.CacheHelper; 062import org.ametys.web.cache.pageelement.PageElementCache; 063import org.ametys.web.repository.sitemap.Sitemap; 064import org.ametys.web.site.SiteConfigurationManager; 065 066/** 067 * DAO for manipulating sites 068 * 069 */ 070public class SiteDAO extends AbstractLogEnabled implements Serviceable, Component 071{ 072 /** Avalon Role */ 073 public static final String ROLE = SiteDAO.class.getName(); 074 075 /** Id of the default site type */ 076 public static final String DEFAULT_SITE_TYPE_ID = "org.ametys.web.sitetype.Default"; 077 078 private static final List<String> __FORBIDDEN_SITE_NAMES = Arrays.asList("preview", "live", "archives", "generate"); 079 080 private SiteManager _siteManager; 081 private AmetysObjectResolver _resolver; 082 private ObservationManager _observationManager; 083 private CurrentUserProvider _currentUserProvider; 084 private PageElementCache _inputDataCache; 085 private PageElementCache _zoneItemCache; 086 private SiteConfigurationManager _siteConfigurationManager; 087 private SiteTypesExtensionPoint _siteTypesEP; 088 private I18nUtils _i18nUtils; 089 private PopulationContextHelper _populationContextHelper; 090 private GroupDirectoryContextHelper _groupDirectoryContextHelper; 091 private DisableConditionsEvaluator _disableConditionsEvaluator; 092 093 @Override 094 public void service(ServiceManager smanager) throws ServiceException 095 { 096 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 097 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 098 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 099 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 100 _inputDataCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/inputData"); 101 _zoneItemCache = (PageElementCache) smanager.lookup(PageElementCache.ROLE + "/zoneItem"); 102 _siteConfigurationManager = (SiteConfigurationManager) smanager.lookup(SiteConfigurationManager.ROLE); 103 _siteTypesEP = (SiteTypesExtensionPoint) smanager.lookup(SiteTypesExtensionPoint.ROLE); 104 _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE); 105 _populationContextHelper = (PopulationContextHelper) smanager.lookup(PopulationContextHelper.ROLE); 106 _groupDirectoryContextHelper = (GroupDirectoryContextHelper) smanager.lookup(GroupDirectoryContextHelper.ROLE); 107 _disableConditionsEvaluator = (DisableConditionsEvaluator) smanager.lookup(DefaultDisableConditionsEvaluator.ROLE); 108 } 109 110 /** 111 * Get the root id 112 * @return the root id 113 */ 114 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 115 public String getRootId () 116 { 117 return _siteManager.getRoot().getId(); 118 } 119 120 /** 121 * Get the properties of given sites 122 * @param names the site names 123 * @return the properties of the sites in a result map 124 */ 125 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 126 public Map<String, Object> getSitesInfos(List<String> names) 127 { 128 Map<String, Object> result = new HashMap<>(); 129 130 List<Map<String, Object>> sites = new ArrayList<>(); 131 List<String> sitesNotFound = new ArrayList<>(); 132 133 for (String name : names) 134 { 135 try 136 { 137 Site site = _siteManager.getSite(name); 138 sites.add(getSiteInfos(site)); 139 } 140 catch (UnknownAmetysObjectException e) 141 { 142 sitesNotFound.add(name); 143 } 144 } 145 146 result.put("sites", sites); 147 result.put("sitesNotFound", sitesNotFound); 148 149 return result; 150 } 151 152 /** 153 * Get the site's properties 154 * @param name the site name 155 * @return the properties 156 */ 157 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 158 public Map<String, Object> getSiteInfos(String name) 159 { 160 Site site = _siteManager.getSite(name); 161 return getSiteInfos(site); 162 } 163 164 /** 165 * Get the site's properties 166 * @param site the site 167 * @return the properties 168 */ 169 public Map<String, Object> getSiteInfos(Site site) 170 { 171 Map<String, Object> infos = new HashMap<>(); 172 173 infos.put("id", site.getId()); 174 infos.put("title", site.getTitle()); 175 infos.put("description", site.getDescription()); 176 infos.put("name", site.getName()); 177 infos.put("path", site.getSitePath()); 178 infos.put("url", site.getUrl()); 179 180 SiteType siteType = _siteTypesEP.getExtension(site.getType()); 181 infos.put("type", _i18nUtils.translate(siteType.getLabel())); 182 183 return infos; 184 } 185 186 /** 187 * Creates a new site 188 * @param parentId The id of parent site. Can be null to create a root site. 189 * @param name The site's name 190 * @param type The site's type 191 * @param renameIfExists Set to true to automatically rename the site if already exists 192 * @return The result map with id of created site 193 */ 194 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 195 public Map<String, Object> createSite(String parentId, String name, String type, boolean renameIfExists) 196 { 197 Map<String, Object> result = new HashMap<>(); 198 199 if (__FORBIDDEN_SITE_NAMES.contains(name)) 200 { 201 // Name is invalid 202 result.put("name", name); 203 result.put("invalid-name", true); 204 } 205 else if (_siteManager.hasSite(name) && !renameIfExists) 206 { 207 // A site with same name already exists 208 result.put("name", name); 209 result.put("already-exists", true); 210 } 211 else 212 { 213 String siteParentId = null; 214 if (StringUtils.isNotEmpty(parentId)) 215 { 216 AmetysObject parent = _resolver.resolveById(parentId); 217 if (parent instanceof Site) 218 { 219 siteParentId = parent.getId(); 220 } 221 } 222 223 String siteName = name; 224 int index = 2; 225 while (_siteManager.hasSite(siteName)) 226 { 227 siteName = name + "-" + (index++); 228 } 229 230 // Create site 231 Site site = _siteManager.createSite(siteName, siteParentId); 232 site.setType(type); 233 site.saveChanges(); 234 235 result.put("id", site.getId()); 236 result.put("name", site.getName()); 237 238 if (siteParentId != null) 239 { 240 result.put("parentId", siteParentId); 241 } 242 243 // Notify observers 244 Map<String, Object> eventParams = new HashMap<>(); 245 eventParams.put(ObservationConstants.ARGS_SITE, site); 246 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_ADDED, _currentUserProvider.getUser(), eventParams)); 247 } 248 249 return result; 250 } 251 252 /** 253 * Create a site by copy of another. 254 * @param parentId The id of parent site. Can be null to create a root site. 255 * @param name the name of site to create 256 * @param id the id of site to copy 257 * @return The result map with id of created site 258 * @throws Exception if an error ocurred while populating new site 259 */ 260 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 261 public Map<String, Object> copySite (String parentId, String name, String id) throws Exception 262 { 263 Map<String, Object> result = new HashMap<>(); 264 265 if (__FORBIDDEN_SITE_NAMES.contains(name)) 266 { 267 // Name is invalid 268 result.put("name", name); 269 result.put("invalid-name", true); 270 } 271 else if (_siteManager.hasSite(name)) 272 { 273 // A site with same name already exists 274 result.put("name", name); 275 result.put("already-exists", true); 276 } 277 else 278 { 279 Site site = _resolver.resolveById(id); 280 281 // Create site by copy 282 Site cSite = _siteManager.copySite(site, parentId, name); 283 cSite.saveChanges(); 284 285 // Notify observers 286 Map<String, Object> eventParams = new HashMap<>(); 287 eventParams.put(ObservationConstants.ARGS_SITE, cSite); 288 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_ADDED, _currentUserProvider.getUser(), eventParams)); 289 290 result.put("id", cSite.getId()); 291 result.put("name", cSite.getName()); 292 } 293 294 return result; 295 } 296 297 /** 298 * Delete a site 299 * @param siteId The id of site to delete 300 * @throws RepositoryException if an error occurred during deletion 301 */ 302 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 303 public void deleteSite(String siteId) throws RepositoryException 304 { 305 Site site = _resolver.resolveById(siteId); 306 String siteName = site.getName(); 307 String jcrPath = site.getNode().getPath().substring(1); 308 Session session = site.getNode().getSession(); 309 310 Collection<String> siteNames = _getChildrenSiteNames(site); 311 312 site.remove(); 313 session.save(); 314 _siteManager.clearCache(); 315 316 _siteConfigurationManager.removeSiteConfiguration(site); 317 318 // Notify observers of site deletion 319 Map<String, Object> eventParams = new HashMap<>(); 320 eventParams.put(ObservationConstants.ARGS_SITE_ID, siteId); 321 eventParams.put(ObservationConstants.ARGS_SITE_NAME, siteName); 322 eventParams.put(ObservationConstants.ARGS_SITE_PATH, jcrPath); 323 eventParams.put("site.children", siteNames.toArray(new String[siteNames.size()])); 324 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_DELETED, _currentUserProvider.getUser(), eventParams)); 325 326 // Remove the links between this site and the populations and the group directories 327 String context = "/sites/" + siteName; 328 _populationContextHelper.link(context, Collections.EMPTY_LIST); 329 _groupDirectoryContextHelper.link(context, Collections.EMPTY_LIST); 330 } 331 332 333 /** 334 * Move sites 335 * @param targetId The target 336 * @param ids the ids of sites to move 337 * @return The result with the ids of moved sites 338 * @throws AmetysRepositoryException if an error occurs 339 * @throws RepositoryException if an error occurs 340 */ 341 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 342 public Map<String, Object> moveSite (String targetId, List<String> ids) throws AmetysRepositoryException, RepositoryException 343 { 344 Map<String, Object> result = new HashMap<>(); 345 List<String> movedSites = new ArrayList<>(); 346 347 ModifiableTraversableAmetysObject root = _siteManager.getRoot(); 348 ModifiableTraversableAmetysObject target = _resolver.resolveById(targetId); 349 350 for (String id : ids) 351 { 352 Site site = _resolver.resolveById(id); 353 if (!site.getParent().equals(target)) 354 { 355 AmetysObject oldParent = site.getParent(); 356 site.moveTo(target, true); 357 358 // Path is modified 359 String newPath = site.getPath(); 360 361 if (root.needsSave()) 362 { 363 root.saveChanges(); 364 } 365 366 // Notify observers 367 Map<String, Object> eventParams = new HashMap<>(); 368 eventParams.put(ObservationConstants.ARGS_SITE, site); 369 eventParams.put(ObservationConstants.ARGS_SITE_PATH, newPath); 370 eventParams.put("site.old.path", ((JCRAmetysObject) oldParent).getNode().getPath() + "/" + site.getName()); 371 eventParams.put("site.parent", target); 372 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_MOVED, _currentUserProvider.getUser(), eventParams)); 373 374 movedSites.add(site.getId()); 375 } 376 } 377 378 result.put("ids", movedSites); 379 result.put("target", targetId); 380 381 return result; 382 } 383 384 385 /** 386 * Clear site's cache 387 * @param siteName The site name 388 * @throws Exception if an error occurred during cache deletion 389 */ 390 @Callable(rights = "Web_Rights_Admin_Sites", context = "/admin") 391 public void clearCache (String siteName) throws Exception 392 { 393 assert StringUtils.isEmpty(siteName); 394 395 Site site = _siteManager.getSite(siteName); 396 assert site != null; 397 398 clearCache(site); 399 } 400 401 /** 402 * Clear cache of all sites. 403 * @return The list of sites which failed 404 */ 405 @Callable(rights = "Web_Rights_Admin_Sites", context = "/admin") 406 public Map<String, Object> clearAllCaches () 407 { 408 int count = 0; 409 List<String> errors = new ArrayList<>(); 410 411 AmetysObjectIterable<Site> sites = _siteManager.getSites(); 412 for (Site site : sites) 413 { 414 count++; 415 try 416 { 417 clearCache(site); 418 } 419 catch (Exception e) 420 { 421 getLogger().error("Unable to clear cache of site " + site.getName(), e); 422 errors.add(site.getName()); 423 } 424 } 425 426 return Map.of( 427 "errors", errors, 428 "count", count, 429 "front", CacheHelper.getFrontURLS().length 430 ); 431 } 432 433 /** 434 * Clear cache of a site 435 * @param site the site 436 * @throws Exception if an error occurred 437 */ 438 public void clearCache (Site site) throws Exception 439 { 440 String siteName = site.getName(); 441 442 if (getLogger().isInfoEnabled()) 443 { 444 getLogger().info("Clearing cache for site " + siteName); 445 } 446 447 CacheHelper.invalidateCache(site, getLogger()); 448 _inputDataCache.clear(null, siteName); 449 _zoneItemCache.clear(null, siteName); 450 } 451 452 /** 453 * Configure site 454 * @param siteName The site name. 455 * @param values the configuration's values 456 * @return The result map. Contains the possible errors 457 * @throws Exception if an error occurred 458 */ 459 @Callable (rights = "Web_Rights_Admin_Sites", context = "/admin") 460 public Map<String, Object> configureSite (String siteName, Map<String, Object> values) throws Exception 461 { 462 Map<String, Object> result = new HashMap<>(); 463 464 Site site = _siteManager.getSite(siteName); 465 466 // Site updating event 467 Map<String, Object> eventParams = new HashMap<>(); 468 eventParams.put(ObservationConstants.ARGS_SITE, site); 469 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATING, _currentUserProvider.getUser(), eventParams)); 470 471 Map<String, List<I18nizableText>> errors = _setParameterValues(site, values); 472 473 if (!errors.isEmpty()) 474 { 475 List<Map<String, Object>> allErrors = new ArrayList<>(); 476 477 for (Map.Entry<String, List<I18nizableText>> entry : errors.entrySet()) 478 { 479 Map<String, Object> error = new HashMap<>(); 480 481 error.put("name", entry.getKey()); 482 error.put("errorMessages", entry.getValue()); 483 484 allErrors.add(error); 485 } 486 487 result.put("errors", allErrors); 488 return result; 489 } 490 491 if (values.containsKey("lang")) 492 { 493 @SuppressWarnings("unchecked") 494 List<String> codes = (List<String>) values.get("lang"); 495 setLanguages(site, codes); 496 } 497 498 site.getNode().getSession().save(); 499 500 // Reload this site's configuration. 501 _siteConfigurationManager.reloadSiteConfiguration(site); 502 503 // Site updated event 504 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 505 506 clearCache(site); 507 508 return result; 509 } 510 511 /** 512 * Set the languages of a site 513 * @param site The site to edit 514 * @param codes The list of new codes. Such as "fr", "en". 515 */ 516 public void setLanguages(Site site, List<String> codes) 517 { 518 Map<String, Object> eventParams = new HashMap<>(); 519 eventParams.put(ObservationConstants.ARGS_SITE, site); 520 521 for (Sitemap sitemap : site.getSitemaps()) 522 { 523 String sitemapName = sitemap.getName(); 524 525 if (!codes.contains(sitemapName)) 526 { 527 sitemap.remove(); 528 529 eventParams.put(ObservationConstants.ARGS_SITEMAP_NAME, sitemapName); 530 _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_DELETED, _currentUserProvider.getUser(), eventParams)); 531 } 532 } 533 534 for (String code : codes) 535 { 536 if (!site.hasSitemap(code)) 537 { 538 Sitemap sitemap = site.addSitemap(code); 539 540 eventParams.put(ObservationConstants.ARGS_SITEMAP, sitemap); 541 _observationManager.notify(new Event(ObservationConstants.EVENT_SITEMAP_ADDED, _currentUserProvider.getUser(), eventParams)); 542 } 543 } 544 } 545 546 /** 547 * Set the site parameters 548 * @param site the site 549 * @param values the parameters' values 550 * @return the parameters' errors 551 */ 552 protected Map<String, List<I18nizableText>> _setParameterValues(Site site, Map<String, Object> values) 553 { 554 String siteTypeId = site.getType(); 555 SiteType siteType = _siteTypesEP.getExtension(siteTypeId); 556 557 Map<String, Object> typedValues = new HashMap<>(); 558 for (ElementDefinition definition: siteType.getModelItems()) 559 { 560 // TODO WORKSPACES-566: the filter to ignore site's illustration should be remove when this parameter is managed like the other ones 561 if (!Site.ILLUSTRATION_PARAMETER.equals(definition.getName())) 562 { 563 Object typedValue = _getTypedValue(values, definition); 564 typedValues.put(definition.getName(), typedValue); // Unable to use streams because typedVaue can be null 565 } 566 } 567 568 Map<String, List<I18nizableText>> allErrors = new HashMap<>(); 569 for (String parameterName : typedValues.keySet()) 570 { 571 Object value = typedValues.get(parameterName); 572 if (!(value instanceof UntouchedValue) && !"lang".equals(parameterName)) 573 { 574 List<I18nizableText> errors = _setParameterValue(site, typedValues, siteType.getModelItem(parameterName)); 575 if (!errors.isEmpty()) 576 { 577 allErrors.put(parameterName, errors); 578 } 579 } 580 } 581 return allErrors; 582 } 583 584 private Object _getTypedValue(Map<String, Object> jsonValues, ElementDefinition definition) 585 { 586 Object jsonValue = jsonValues.get(definition.getName()); 587 ElementType parameterType = definition.getType(); 588 return parameterType.fromJSONForClient(jsonValue, DataContext.newInstance().withDataPath(definition.getName())); 589 } 590 591 private List<I18nizableText> _setParameterValue(Site site, Map<String, Object> values, ElementDefinition definition) 592 { 593 boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values); 594 boolean isDisabled = _disableConditionsEvaluator.evaluateDisableConditions(definition, definition.getName(), Optional.empty(), values, site, new HashMap<>()); 595 596 List<I18nizableText> errors = new ArrayList<>(); 597 if (isGroupSwitchOn && !isDisabled) 598 { 599 Object value = values.get(definition.getName()); 600 601 ValidationResult validationResult = ModelHelper.validateValue(definition, value); 602 if (validationResult.hasErrors()) 603 { 604 errors.addAll(validationResult.getErrors()); 605 } 606 else 607 { 608 site.setValue(definition.getName(), value); 609 } 610 } 611 612 return errors; 613 } 614 615 /** 616 * Get all children site's names of a site 617 * @param site The site 618 * @return the children site's names. 619 */ 620 private Collection<String> _getChildrenSiteNames (Site site) 621 { 622 ArrayList<String> result = new ArrayList<>(); 623 624 result.add(site.getName()); 625 626 AmetysObjectIterable<Site> sites = site.getChildrenSites(); 627 for (Site child : sites) 628 { 629 result.addAll(_getChildrenSiteNames(child)); 630 } 631 632 return result; 633 } 634}