001/*
002 *  Copyright 2018 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.plugins.ugc.page;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import javax.jcr.Node;
029import javax.jcr.RepositoryException;
030import javax.jcr.Value;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.cocoon.components.ContextHelper;
040import org.apache.cocoon.environment.Request;
041import org.apache.commons.lang3.StringUtils;
042import org.apache.jackrabbit.value.StringValue;
043import org.slf4j.Logger;
044
045import org.ametys.cms.contenttype.ContentAttributeDefinition;
046import org.ametys.cms.contenttype.ContentType;
047import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
048import org.ametys.cms.contenttype.ContentTypesHelper;
049import org.ametys.cms.data.ContentValue;
050import org.ametys.cms.repository.Content;
051import org.ametys.cms.repository.ContentTypeExpression;
052import org.ametys.cms.repository.LanguageExpression;
053import org.ametys.core.observation.Event;
054import org.ametys.core.observation.ObservationManager;
055import org.ametys.core.user.CurrentUserProvider;
056import org.ametys.core.util.I18nUtils;
057import org.ametys.core.util.LambdaUtils;
058import org.ametys.plugins.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.repository.AmetysRepositoryException;
061import org.ametys.plugins.repository.UnknownAmetysObjectException;
062import org.ametys.plugins.repository.jcr.JCRAmetysObject;
063import org.ametys.plugins.repository.query.QueryHelper;
064import org.ametys.plugins.repository.query.SortCriteria;
065import org.ametys.plugins.repository.query.expression.AndExpression;
066import org.ametys.plugins.repository.query.expression.Expression;
067import org.ametys.plugins.repository.query.expression.Expression.Operator;
068import org.ametys.plugins.repository.query.expression.ExpressionContext;
069import org.ametys.plugins.ugc.observation.ObservationConstants;
070import org.ametys.plugins.repository.query.expression.MetadataExpression;
071import org.ametys.plugins.repository.query.expression.NotExpression;
072import org.ametys.plugins.repository.query.expression.OrExpression;
073import org.ametys.plugins.repository.query.expression.StringExpression;
074import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression;
075import org.ametys.runtime.model.ElementDefinition;
076import org.ametys.runtime.model.Enumerator;
077import org.ametys.runtime.model.ModelItem;
078import org.ametys.runtime.plugin.component.AbstractLogEnabled;
079import org.ametys.web.WebConstants;
080import org.ametys.web.WebHelper;
081import org.ametys.web.repository.SiteAwareAmetysObject;
082import org.ametys.web.repository.page.ModifiablePage;
083import org.ametys.web.repository.page.Page;
084import org.ametys.web.repository.page.PageQueryHelper;
085import org.ametys.web.repository.page.jcr.DefaultPage;
086
087/**
088 * Component providing methods to retrieve ugc virtual pages, such as the ugc root,
089 * transitional page and ugc content page.
090 */
091public class UGCPageHandler extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
092{
093    /** The attribute to get the name of transitional page */
094    public static final String ATTRIBUTE_TRANSITIONAL_PAGE_METADATA_VALUE = "metadata_value";
095    
096    /** The attribute to get the title of transitional page */
097    public static final String ATTRIBUTE_TRANSITIONAL_PAGE_TITLE = "title";
098    
099    /** The avalon role. */
100    public static final String ROLE = UGCPageHandler.class.getName();
101    
102    /** The data name for the content type of the ugc */
103    public static final String CONTENT_TYPE_DATA_NAME = "ugc-root-contenttype";
104    
105    /** The data name for the classification attribute of the ugc */
106    public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "ugc-root-classification-metadata";
107    
108    /** The data name for the visibility of transitional page of the ugc */
109    public static final String CLASSIFICATION_PAGE_VISIBLE_DATA_NAME = "ugc-root-classification-page-visible";
110    
111    /** The ametys object resolver */
112    protected AmetysObjectResolver _resolver;
113    
114    /** The content type extension point */
115    protected ContentTypeExtensionPoint _cTypeEP;
116    
117    /** The content types helper */
118    protected ContentTypesHelper _cTypeHelper;
119    
120    /** The i18n utils */
121    protected I18nUtils _i18nUtils;
122
123    /** The avalon context */
124    protected Context _context;
125
126    /** Observer manager. */
127    protected ObservationManager _observationManager;
128    
129    /** Current user provider */
130    protected CurrentUserProvider _currentUserProvider;
131    
132    @Override
133    public void service(ServiceManager manager) throws ServiceException
134    {
135        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
136        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
137        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
138        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
139        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
140        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
141    }
142    
143    public void contextualize(Context context) throws ContextException
144    {
145        _context = context;
146    }
147    
148    @Override
149    protected Logger getLogger()
150    {
151        return super.getLogger();
152    }
153    
154    /**
155     * Gets the path of the classification attribute
156     * @param rootPage The ugc root page
157     * @return the path of the classification attribute
158     */
159    public String getClassificationAttribute(Page rootPage)
160    {
161        return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME);
162    }
163    
164    /**
165     * Gets the content type id
166     * @param rootPage The ugc root page
167     * @return the content type id
168     */
169    public String getContentTypeId(Page rootPage)
170    {
171        return rootPage.getValue(CONTENT_TYPE_DATA_NAME);
172    }
173    
174    /**
175     * <code>true</code> if the classification pages are visible
176     * @param rootPage The ugc root page
177     * @return <code>true</code> if the classification pages are visible
178     */
179    public boolean isClassificationPagesVisible(Page rootPage)
180    {
181        return rootPage.getValueOrDefault(CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, false);
182    }
183    
184    /**
185     * True if the page a UGC root page for the given content type
186     * @param page the page
187     * @param contentTypeId The id of content type. Cannot be null.
188     * @return true if the page is a UGC root page for the given content type
189     */
190    public boolean isUGCRootPage(DefaultPage page, String contentTypeId)
191    {
192        try
193        {
194            Node node = page.getNode();
195            
196            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
197            {
198                List<Value> values = Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues());
199                
200                boolean isUGCRootPage = values.stream()
201                        .map(LambdaUtils.wrap(Value::getString))
202                        .anyMatch(v -> VirtualUGCPageFactory.class.getName().equals(v));
203                
204                if (isUGCRootPage)
205                {
206                    return contentTypeId.equals(page.getValue(CONTENT_TYPE_DATA_NAME));
207                }
208            }
209        }
210        catch (RepositoryException e)
211        {
212            getLogger().warn("Unable to determine if page '" + page.getId() + "' is a UGC root page", e);
213        }
214        
215        return false;
216    }
217    
218    /**
219     * Gets the ugc root pages from the given content type id.
220     * @param siteName the site name
221     * @param sitemapName the sitemap name
222     * @param contentTypeId The content type id
223     * @return the ugc root page.
224     * @throws AmetysRepositoryException  if an error occured.
225     */
226    public Page getUGCRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException
227    {
228        Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName());
229        Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId);
230        
231        AndExpression andExp = new AndExpression(expression, contentTypeExp);
232        
233        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, andExp, null);
234        
235        AmetysObjectIterable<Page> pages = _resolver.query(query);
236        
237        return pages.iterator().hasNext() ? pages.iterator().next() : null;
238    }
239    
240    /**
241     * Get the ugc root pages
242     * @param siteName the current site.
243     * @param sitemapName the sitemap name.
244     * @return the ugc root pages
245     * @throws AmetysRepositoryException if an error occured.
246     */
247    public Set<Page> getUGCRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
248    {
249        Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName());
250        
251        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null);
252        
253        AmetysObjectIterable<Page> pages = _resolver.query(query);
254        
255        return pages.stream().collect(Collectors.toSet());
256    }
257    
258    /**
259     * Set the ugc root page
260     * @param page the page to set as root
261     * @param contentTypeId the type of content root
262     * @param attributePath path to classification attribute
263     * @param classificationPageVisible true to show classification
264     * @throws RepositoryException if a repository error occurred
265     */
266    public void setUGCRoot(Page page, String contentTypeId, String attributePath, boolean classificationPageVisible) throws RepositoryException
267    {
268        Page currentUGCPage = getUGCRootPage(page.getSiteName(), page.getSitemapName(), contentTypeId);
269        
270        Map<String, Object> eventParams = new HashMap<>();
271        eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
272        
273        if (currentUGCPage != null && currentUGCPage.getId().equals(page.getId()))
274        {
275            // Unindex pages for all workspaces before the properties changed 
276            _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_UPDATING, _currentUserProvider.getUser(), eventParams));
277            
278            _updateUGCRootProperty(page, contentTypeId, attributePath, classificationPageVisible);
279        }
280        else
281        {
282            _addUGCRootProperty(page, contentTypeId, attributePath, classificationPageVisible);
283        }
284        
285        // Live synchronization
286        _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
287        
288        // Indexation
289        _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_UPDATED, _currentUserProvider.getUser(), eventParams));
290    }
291    
292    private void _addUGCRootProperty(Page page, String contentType, String metadata, boolean classificationPageVisible) throws RepositoryException
293    {
294        if (page instanceof JCRAmetysObject)
295        {
296            JCRAmetysObject jcrPage = (JCRAmetysObject) page;
297            Node node = jcrPage.getNode();
298            
299            List<Value> values = new ArrayList<>();
300            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
301            {
302                values.addAll(Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues()));
303            }
304            
305            StringValue virtualUGCPageFactoryClassName = new StringValue(VirtualUGCPageFactory.class.getName());
306            if (!values.contains(virtualUGCPageFactoryClassName))
307            {
308                values.add(virtualUGCPageFactoryClassName);
309            }
310            
311            node.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, values.toArray(new Value[values.size()]));
312
313            // Set the ugc root property
314            if (page instanceof ModifiablePage)
315            {
316                ((ModifiablePage) page).setValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME, contentType);
317                ((ModifiablePage) page).setValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME, metadata);
318                ((ModifiablePage) page).setValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, classificationPageVisible);
319            }
320            
321            jcrPage.saveChanges();
322        }
323    }
324    
325    private void _updateUGCRootProperty(Page page, String contentType, String metadata, boolean classificationPageVisible)
326    {
327        if (page instanceof ModifiablePage)
328        {
329            ModifiablePage modifiablePage = (ModifiablePage) page;
330            
331            // Set the ugc root property
332            modifiablePage.setValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME, contentType);
333            modifiablePage.setValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME, metadata);
334            modifiablePage.setValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, classificationPageVisible);
335            
336            modifiablePage.saveChanges();
337        }
338    }
339    
340    /**
341     * Get UGC contents from rootPage
342     * @param rootPage the root page
343     * @return the list of UGC contents
344     */
345    public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage)
346    {
347        String lang = rootPage.getSitemapName();
348        String contentType = getContentTypeId(rootPage);
349        
350        ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType);
351        
352        ExpressionContext siteExpressionContext = ExpressionContext.newInstance().withInternal(true);
353        StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName(), siteExpressionContext);
354        Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE, siteExpressionContext));
355        Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr);
356        
357        Expression finalExpr = new AndExpression(contentTypeExp, new LanguageExpression(Operator.EQ, lang), fullSiteExpr);
358        
359        SortCriteria sort = new SortCriteria();
360        sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
361        
362        String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort);
363        
364        return _resolver.query(xPathQuery);
365    }
366    
367    /**
368     * Determines if content is part of UGC root page
369     * @param rootPage The root page
370     * @param content the content
371     * @return true if content is part of UGC root page
372     */
373    public boolean hasContentForRootPage(Page rootPage, Content content)
374    {
375        String contentType = getContentTypeId(rootPage);
376        String siteName = rootPage.getSiteName();
377        String classificationMetadata = getClassificationAttribute(rootPage);
378        
379        return _cTypeHelper.isInstanceOf(content, contentType) // match content type
380                && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site
381                && StringUtils.isBlank(classificationMetadata); // no classification attribute
382    }
383    
384    /**
385     * Get the map of transitional page (name : (id, title))
386     * @param rootPage the root page
387     * @return The map of transitional page
388     */
389    public Map<String, Map<String, String>> getTransitionalPage(Page rootPage)
390    {
391        return _getClassificationType(rootPage)
392                .allTransitionalPages()
393                .stream()
394                .sorted(Comparator.comparing(TransitionalPageInformation::getTitle))
395                .collect(LambdaUtils.Collectors.toLinkedHashMap(
396                        TransitionalPageInformation::getKey, 
397                        TransitionalPageInformation::getInfo));
398    }
399    
400    private ClassificationType _getClassificationType(Page rootPage)
401    {
402        String classificationAttributePath = getClassificationAttribute(rootPage);
403        if (StringUtils.isBlank(classificationAttributePath))
404        {
405            // No classification attribute defined, so no transitional page
406            return new ClassificationType.None();
407        }
408        String contentTypeId = getContentTypeId(rootPage);
409        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
410        
411        if (contentType == null)
412        {
413            getLogger().warn("Can not classify UGC content of type '" + contentTypeId + "' on root page " + rootPage.getId());
414        }
415        else if (contentType.hasModelItem(classificationAttributePath))
416        {
417            ModelItem modelItem = contentType.getModelItem(classificationAttributePath);
418            if (modelItem instanceof ContentAttributeDefinition)
419            {
420                String attributeContentType = ((ContentAttributeDefinition) modelItem).getContentTypeId();
421                return new ClassificationType.TypeContent(this, rootPage, attributeContentType);
422            }
423            else if (modelItem instanceof ElementDefinition<?>)
424            {
425                @SuppressWarnings("unchecked")
426                Enumerator<String> enumerator = ((ElementDefinition<String>) modelItem).getEnumerator();
427                if (enumerator != null)
428                {
429                    return new ClassificationType.TypeEnum(this, rootPage, enumerator);
430                }
431            }
432        }
433        
434        return new ClassificationType.None();
435    }
436    
437    /**
438     * Get contents under transitional page
439     * @param rootPage the root page
440     * @param metadataValue the metadata value (linked to the transitional page)
441     * @return list of contents under transitional page
442     */
443    public AmetysObjectIterable<Content> getContentsForTransitionalPage(Page rootPage, String metadataValue)
444    {
445        String classificationMetadata = getClassificationAttribute(rootPage);
446        
447        String lang = rootPage.getSitemapName();
448        String contentType = getContentTypeId(rootPage);
449        
450        ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType);
451        StringExpression metadataExpression = new StringExpression(classificationMetadata, Operator.EQ, metadataValue);
452        
453        ExpressionContext siteExpressionContext = ExpressionContext.newInstance().withInternal(true);
454        StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName(), siteExpressionContext);
455        Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE, siteExpressionContext));
456        Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr);
457        
458        Expression finalExpr = new AndExpression(contentTypeExp, metadataExpression, new LanguageExpression(Operator.EQ, lang), fullSiteExpr);
459        
460        SortCriteria sort = new SortCriteria();
461        sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
462        
463        String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort);
464        
465        return _resolver.query(xPathQuery);
466    }
467    
468    /**
469     * Determines if given content is part of a transitional page
470     * @param rootPage the root page
471     * @param metadataValue the metadata value (linked to the transitional page). Cannot be null.
472     * @param content the content
473     * @return true if content is part of the transitional page
474     */
475    public boolean hasContentForTransitionalPage(Page rootPage, String metadataValue, Content content)
476    {
477        String contentType = getContentTypeId(rootPage);
478        String siteName = rootPage.getSiteName();
479        String classificationMetadata = getClassificationAttribute(rootPage);
480        
481        return _cTypeHelper.isInstanceOf(content, contentType) // match content type
482                && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site
483                && StringUtils.isNotEmpty(classificationMetadata) && metadataValue.equals(((ContentValue) content.getValue(classificationMetadata)).getContentId()); // match classification attribute
484                
485    }
486    
487    /**
488     * Computes a page id
489     * @param path The path
490     * @param root The root page
491     * @param ugcContent The UGC content
492     * @return The id
493     */
494    public String computePageId(String path, Page root, Content ugcContent)
495    {
496        // E.g: ugccontent://path?rootId=...&contentId=...
497        return "ugccontent://" + path + "?rootId=" + root.getId() + "&contentId=" + ugcContent.getId();
498    }
499    
500    /**
501     * Gets the UGC page related to the given UG Content id
502     * @param contentId the id of UG Content
503     * @param siteName The site name. Can be nul to get site from content or current site.
504     * @return the UGC page or null if not found
505     */
506    public UGCPage getUgcPage(String contentId, String siteName)
507    {
508        if (contentId == null)
509        {
510            return null;
511        }
512        
513        Content content;
514        try
515        {
516            content = _resolver.resolveById(contentId);
517        }
518        catch (UnknownAmetysObjectException e)
519        {
520            return null;
521        }
522        
523        Request request = ContextHelper.getRequest(_context);
524        String site = StringUtils.isNotBlank(siteName) ? siteName : WebHelper.getSiteName(request, content);
525        
526        String sitemap = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
527        
528        for (String type : content.getTypes())
529        {
530            Optional<UGCPage> ugcPage = getUgcPage(content, site, sitemap, type);
531            if (ugcPage.isPresent())
532            {
533                return ugcPage.get();
534            }
535        }
536        
537        return null;
538    }
539    
540    /**
541     * Gets the UGC page related to the given UG Content for given site, sitemap and type
542     * @param ugcContent the UG Content
543     * @param site the site name
544     * @param sitemap the sitemap name
545     * @param contentType the content type id
546     * @return the UGC page
547     */
548    public Optional<UGCPage> getUgcPage(Content ugcContent, String site, String sitemap, String contentType)
549    {
550        String language = Optional.of(ugcContent)
551                .map(Content::getLanguage)
552                .orElse(sitemap);
553        Page ugcRootPage = getUGCRootPage(site, language, contentType);
554        
555        return Optional.ofNullable(ugcRootPage)
556                .flatMap(root -> getUgcPage(root, ugcContent));
557    }
558    
559    /**
560     * Gets the UGC page related to the given UG Content for given UGC root
561     * @param ugcRootPage the UGC root page
562     * @param ugcContent the UG Content
563     * @return the UGC page
564     */
565    public Optional<UGCPage> getUgcPage(Page ugcRootPage, Content ugcContent)
566    {
567        String path = _getPath(ugcRootPage, ugcContent);
568        return Optional.ofNullable(path)
569                .map(p -> computePageId(p, ugcRootPage, ugcContent))
570                .map(this::_silentResolve);
571    }
572    
573    private String _getPath(Page ugcRootPage, Content ugcContent)
574    {
575        try
576        {
577            ClassificationType transtionalPageType = _getClassificationType(ugcRootPage);
578            if (transtionalPageType instanceof ClassificationType.None)
579            {
580                return "_root";
581            }
582            else
583            {
584                TransitionalPageInformation transitionalPageInfo = transtionalPageType.getTransitionalPage(ugcContent);
585                return transitionalPageInfo.getKey();
586            }
587        }
588        catch (Exception e)
589        {
590            getLogger().error("Cannot get path for root {} and content {}", ugcRootPage, ugcContent, e);
591            return null;
592        }
593    }
594    
595    private UGCPage _silentResolve(String id)
596    {
597        try
598        {
599            return _resolver.resolveById(id);
600        }
601        catch (UnknownAmetysObjectException e)
602        {
603            return null;
604        }
605    }
606}