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.Comparator;
019import java.util.Map;
020import java.util.Optional;
021import java.util.Set;
022import java.util.stream.Collectors;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.cocoon.components.ContextHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.commons.lang.StringUtils;
034import org.slf4j.Logger;
035
036import org.ametys.cms.contenttype.ContentAttributeDefinition;
037import org.ametys.cms.contenttype.ContentType;
038import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
039import org.ametys.cms.contenttype.ContentTypesHelper;
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.ContentTypeExpression;
043import org.ametys.cms.repository.LanguageExpression;
044import org.ametys.core.util.I18nUtils;
045import org.ametys.core.util.LambdaUtils;
046import org.ametys.plugins.repository.AmetysObjectIterable;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.UnknownAmetysObjectException;
050import org.ametys.plugins.repository.query.QueryHelper;
051import org.ametys.plugins.repository.query.SortCriteria;
052import org.ametys.plugins.repository.query.expression.AndExpression;
053import org.ametys.plugins.repository.query.expression.Expression;
054import org.ametys.plugins.repository.query.expression.Expression.Operator;
055import org.ametys.plugins.repository.query.expression.MetadataExpression;
056import org.ametys.plugins.repository.query.expression.NotExpression;
057import org.ametys.plugins.repository.query.expression.OrExpression;
058import org.ametys.plugins.repository.query.expression.StringExpression;
059import org.ametys.plugins.repository.query.expression.VirtualFactoryExpression;
060import org.ametys.runtime.model.ElementDefinition;
061import org.ametys.runtime.model.Enumerator;
062import org.ametys.runtime.model.ModelItem;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064import org.ametys.web.WebConstants;
065import org.ametys.web.WebHelper;
066import org.ametys.web.repository.SiteAwareAmetysObject;
067import org.ametys.web.repository.page.Page;
068import org.ametys.web.repository.page.PageQueryHelper;
069
070/**
071 * Component providing methods to retrieve ugc virtual pages, such as the ugc root,
072 * transitional page and ugc content page.
073 */
074public class UGCPageHandler extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
075{
076    /** The attribute to get the name of transitional page */
077    public static final String ATTRIBUTE_TRANSITIONAL_PAGE_METADATA_VALUE = "metadata_value";
078    
079    /** The attribute to get the title of transitional page */
080    public static final String ATTRIBUTE_TRANSITIONAL_PAGE_TITLE = "title";
081    
082    /** The avalon role. */
083    public static final String ROLE = UGCPageHandler.class.getName();
084    
085    /** The data name for the content type of the ugc */
086    public static final String CONTENT_TYPE_DATA_NAME = "ugc-root-contenttype";
087    
088    /** The data name for the classification attribute of the ugc */
089    public static final String CLASSIFICATION_ATTRIBUTE_DATA_NAME = "ugc-root-classification-metadata";
090    
091    /** The data name for the visibility of transitional page of the ugc */
092    public static final String CLASSIFICATION_PAGE_VISIBLE_DATA_NAME = "ugc-root-classification-page-visible";
093    
094    /** The ametys object resolver */
095    protected AmetysObjectResolver _resolver;
096    
097    /** The content type extension point */
098    protected ContentTypeExtensionPoint _cTypeEP;
099    
100    /** The content types helper */
101    protected ContentTypesHelper _cTypeHelper;
102    
103    /** The i18n utils */
104    protected I18nUtils _i18nUtils;
105
106    /** The avalon context */
107    protected Context _context;
108
109    
110    @Override
111    public void service(ServiceManager manager) throws ServiceException
112    {
113        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
114        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
115        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
116        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
117    }
118    
119    public void contextualize(Context context) throws ContextException
120    {
121        _context = context;
122    }
123    
124    @Override
125    protected Logger getLogger()
126    {
127        return super.getLogger();
128    }
129    
130    /**
131     * Gets the path of the classification attribute
132     * @param rootPage The ugc root page
133     * @return the path of the classification attribute
134     */
135    public String getClassificationAttribute(Page rootPage)
136    {
137        return rootPage.getValue(CLASSIFICATION_ATTRIBUTE_DATA_NAME);
138    }
139    
140    /**
141     * Gets the content type id
142     * @param rootPage The ugc root page
143     * @return the content type id
144     */
145    public String getContentTypeId(Page rootPage)
146    {
147        return rootPage.getValue(CONTENT_TYPE_DATA_NAME);
148    }
149    
150    /**
151     * <code>true</code> if the classification pages are visible
152     * @param rootPage The ugc root page
153     * @return <code>true</code> if the classification pages are visible
154     */
155    public boolean isClassificationPagesVisible(Page rootPage)
156    {
157        return rootPage.getValue(CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, false);
158    }
159    
160    /**
161     * Gets the ugc root pages from the given content type id.
162     * @param siteName the site name
163     * @param sitemapName the sitemap name
164     * @param contentTypeId The content type id
165     * @return the ugc root page.
166     * @throws AmetysRepositoryException  if an error occured.
167     */
168    public Page getUGCRootPage(String siteName, String sitemapName, String contentTypeId) throws AmetysRepositoryException
169    {
170        Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName());
171        Expression contentTypeExp = new StringExpression(CONTENT_TYPE_DATA_NAME, Operator.EQ, contentTypeId);
172        
173        AndExpression andExp = new AndExpression(expression, contentTypeExp);
174        
175        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, andExp, null);
176        
177        AmetysObjectIterable<Page> pages = _resolver.query(query);
178        
179        return pages.iterator().hasNext() ? pages.iterator().next() : null;
180    }
181    
182    /**
183     * Get the ugc root pages
184     * @param siteName the current site.
185     * @param sitemapName the sitemap name.
186     * @return the ugc root pages
187     * @throws AmetysRepositoryException if an error occured.
188     */
189    public Set<Page> getUGCRootPages(String siteName, String sitemapName) throws AmetysRepositoryException
190    {
191        Expression expression = new VirtualFactoryExpression(VirtualUGCPageFactory.class.getName());
192        
193        String query = PageQueryHelper.getPageXPathQuery(siteName, sitemapName, null, expression, null);
194        
195        AmetysObjectIterable<Page> pages = _resolver.query(query);
196        
197        return pages.stream().collect(Collectors.toSet());
198    }
199    
200    /**
201     * Get UGC contents from rootPage
202     * @param rootPage the root page
203     * @return the list of UGC contents
204     */
205    public AmetysObjectIterable<Content> getContentsForRootPage(Page rootPage)
206    {
207        String lang = rootPage.getSitemapName();
208        String contentType = getContentTypeId(rootPage);
209        
210        ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType);
211        
212        StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName());
213        Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE));
214        Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr);
215        
216        Expression finalExpr = new AndExpression(contentTypeExp, new LanguageExpression(Operator.EQ, lang), fullSiteExpr);
217        
218        SortCriteria sort = new SortCriteria();
219        sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
220        
221        String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort);
222        
223        return _resolver.query(xPathQuery);
224    }
225    
226    /**
227     * Determines if content is part of UGC root page
228     * @param rootPage The root page
229     * @param content the content
230     * @return true if content is part of UGC root page
231     */
232    public boolean hasContentForRootPage(Page rootPage, Content content)
233    {
234        String contentType = getContentTypeId(rootPage);
235        String siteName = rootPage.getSiteName();
236        String classificationMetadata = getClassificationAttribute(rootPage);
237        
238        return _cTypeHelper.isInstanceOf(content, contentType) // match content type
239                && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site
240                && StringUtils.isBlank(classificationMetadata); // no classification attribute
241    }
242    
243    /**
244     * Get the map of transitional page (name : (id, title))
245     * @param rootPage the root page
246     * @return The map of transitional page
247     */
248    public Map<String, Map<String, String>> getTransitionalPage(Page rootPage)
249    {
250        return _getClassificationType(rootPage)
251                .allTransitionalPages()
252                .stream()
253                .sorted(Comparator.comparing(TransitionalPageInformation::getTitle))
254                .collect(LambdaUtils.Collectors.toLinkedHashMap(
255                        TransitionalPageInformation::getKey, 
256                        TransitionalPageInformation::getInfo));
257    }
258    
259    private ClassificationType _getClassificationType(Page rootPage)
260    {
261        String classificationAttributePath = getClassificationAttribute(rootPage);
262        if (StringUtils.isBlank(classificationAttributePath))
263        {
264            // No classification attribute defined, so no transitional page
265            return new ClassificationType.None();
266        }
267        String contentTypeId = getContentTypeId(rootPage);
268        ContentType contentType = _cTypeEP.getExtension(contentTypeId);
269        
270        if (contentType == null)
271        {
272            getLogger().warn("Can not classify UGC content of type '" + contentTypeId + "' on root page " + rootPage.getId());
273        }
274        else if (contentType.hasModelItem(classificationAttributePath))
275        {
276            ModelItem modelItem = contentType.getModelItem(classificationAttributePath);
277            if (modelItem instanceof ContentAttributeDefinition)
278            {
279                String attributeContentType = ((ContentAttributeDefinition) modelItem).getContentTypeId();
280                return new ClassificationType.TypeContent(this, rootPage, attributeContentType);
281            }
282            else if (modelItem instanceof ElementDefinition<?>)
283            {
284                @SuppressWarnings("unchecked")
285                Enumerator<String> enumerator = ((ElementDefinition<String>) modelItem).getEnumerator();
286                if (enumerator != null)
287                {
288                    return new ClassificationType.TypeEnum(this, rootPage, enumerator);
289                }
290            }
291        }
292        
293        return new ClassificationType.None();
294    }
295    
296    /**
297     * Get contents under transitional page
298     * @param rootPage the root page
299     * @param metadataValue the metadata value (linked to the transitional page)
300     * @return list of contents under transitional page
301     */
302    public AmetysObjectIterable<Content> getContentsForTransitionalPage(Page rootPage, String metadataValue)
303    {
304        String classificationMetadata = getClassificationAttribute(rootPage);
305        
306        String lang = rootPage.getSitemapName();
307        String contentType = getContentTypeId(rootPage);
308        
309        ContentTypeExpression contentTypeExp = new ContentTypeExpression(Operator.EQ, contentType);
310        StringExpression metadataExpression = new StringExpression(classificationMetadata, Operator.EQ, metadataValue);
311        
312        StringExpression siteExpr = new StringExpression(SiteAwareAmetysObject.METADATA_SITE, Operator.EQ, rootPage.getSiteName());
313        Expression noSiteExpr = new NotExpression(new MetadataExpression(SiteAwareAmetysObject.METADATA_SITE));
314        Expression fullSiteExpr = new OrExpression(siteExpr, noSiteExpr);
315        
316        Expression finalExpr = new AndExpression(contentTypeExp, metadataExpression, new LanguageExpression(Operator.EQ, lang), fullSiteExpr);
317        
318        SortCriteria sort = new SortCriteria();
319        sort.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
320        
321        String xPathQuery = QueryHelper.getXPathQuery(null, "ametys:content", finalExpr, sort);
322        
323        return _resolver.query(xPathQuery);
324    }
325    
326    /**
327     * Determines if given content is part of a transitional page
328     * @param rootPage the root page
329     * @param metadataValue the metadata value (linked to the transitional page). Cannot be null.
330     * @param content the content
331     * @return true if content is part of the transitional page
332     */
333    public boolean hasContentForTransitionalPage(Page rootPage, String metadataValue, Content content)
334    {
335        String contentType = getContentTypeId(rootPage);
336        String siteName = rootPage.getSiteName();
337        String classificationMetadata = getClassificationAttribute(rootPage);
338        
339        return _cTypeHelper.isInstanceOf(content, contentType) // match content type
340                && (!(content instanceof SiteAwareAmetysObject) || siteName.equals(((SiteAwareAmetysObject) content).getSiteName())) // match site
341                && StringUtils.isNotEmpty(classificationMetadata) && metadataValue.equals(((ContentValue) content.getValue(classificationMetadata)).getContentId()); // match classification attribute
342                
343    }
344    
345    /**
346     * Computes a page id
347     * @param path The path
348     * @param root The root page
349     * @param ugcContent The UGC content
350     * @return The id
351     */
352    public String computePageId(String path, Page root, Content ugcContent)
353    {
354        // E.g: ugccontent://path?rootId=...&contentId=...
355        return "ugccontent://" + path + "?rootId=" + root.getId() + "&contentId=" + ugcContent.getId();
356    }
357    
358    /**
359     * Gets the UGC page related to the given UG Content id
360     * @param contentId the id of UG Content
361     * @param siteName The site name. Can be nul to get site from content or current site.
362     * @return the UGC page or null if not found
363     */
364    public UGCPage getUgcPage(String contentId, String siteName)
365    {
366        if (contentId == null)
367        {
368            return null;
369        }
370        
371        Content content;
372        try
373        {
374            content = _resolver.resolveById(contentId);
375        }
376        catch (UnknownAmetysObjectException e)
377        {
378            return null;
379        }
380        
381        Request request = ContextHelper.getRequest(_context);
382        String site = StringUtils.isNotBlank(siteName) ? siteName : WebHelper.getSiteName(request, content);
383        
384        String sitemap = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
385        
386        for (String type : content.getTypes())
387        {
388            Optional<UGCPage> ugcPage = getUgcPage(content, site, sitemap, type);
389            if (ugcPage.isPresent())
390            {
391                return ugcPage.get();
392            }
393        }
394        
395        return null;
396    }
397    
398    /**
399     * Gets the UGC page related to the given UG Content for given site, sitemap and type
400     * @param ugcContent the UG Content
401     * @param site the site name
402     * @param sitemap the sitemap name
403     * @param contentType the content type id
404     * @return the UGC page
405     */
406    public Optional<UGCPage> getUgcPage(Content ugcContent, String site, String sitemap, String contentType)
407    {
408        String language = Optional.of(ugcContent)
409                .map(Content::getLanguage)
410                .orElse(sitemap);
411        Page ugcRootPage = getUGCRootPage(site, language, contentType);
412        
413        return Optional.ofNullable(ugcRootPage)
414                .flatMap(root -> getUgcPage(root, ugcContent));
415    }
416    
417    /**
418     * Gets the UGC page related to the given UG Content for given UGC root
419     * @param ugcRootPage the UGC root page
420     * @param ugcContent the UG Content
421     * @return the UGC page
422     */
423    public Optional<UGCPage> getUgcPage(Page ugcRootPage, Content ugcContent)
424    {
425        String path = _getPath(ugcRootPage, ugcContent);
426        return Optional.ofNullable(path)
427                .map(p -> computePageId(p, ugcRootPage, ugcContent))
428                .map(this::_silentResolve);
429    }
430    
431    private String _getPath(Page ugcRootPage, Content ugcContent)
432    {
433        try
434        {
435            ClassificationType transtionalPageType = _getClassificationType(ugcRootPage);
436            if (transtionalPageType instanceof ClassificationType.None)
437            {
438                return "_root";
439            }
440            else
441            {
442                TransitionalPageInformation transitionalPageInfo = transtionalPageType.getTransitionalPage(ugcContent);
443                return transitionalPageInfo.getKey();
444            }
445        }
446        catch (Exception e)
447        {
448            getLogger().error("Cannot get path for root {} and content {}", ugcRootPage, ugcContent, e);
449            return null;
450        }
451    }
452    
453    private UGCPage _silentResolve(String id)
454    {
455        try
456        {
457            return _resolver.resolveById(id);
458        }
459        catch (UnknownAmetysObjectException e)
460        {
461            return null;
462        }
463    }
464}