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