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