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