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