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