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