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