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