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}