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