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