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