001/*
002 *  Copyright 2019 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.web.frontoffice.search.metamodel.impl;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Comparator;
021import java.util.Map;
022import java.util.stream.Stream;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.commons.lang3.LocaleUtils;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.commons.lang3.tuple.Pair;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import org.ametys.cms.content.ContentHelper;
032import org.ametys.cms.contenttype.ContentType;
033import org.ametys.cms.data.ContentValue;
034import org.ametys.cms.data.type.ModelItemTypeConstants;
035import org.ametys.cms.model.ContentElementDefinition;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ContentTypeExpression;
038import org.ametys.cms.repository.LanguageExpression;
039import org.ametys.core.util.LambdaUtils;
040import org.ametys.plugins.repository.AmetysObjectIterable;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.RepositoryConstants;
043import org.ametys.plugins.repository.query.QueryHelper;
044import org.ametys.plugins.repository.query.expression.AndExpression;
045import org.ametys.plugins.repository.query.expression.Expression;
046import org.ametys.plugins.repository.query.expression.Expression.Operator;
047import org.ametys.plugins.repository.query.expression.OrExpression;
048import org.ametys.runtime.i18n.I18nizableText;
049import org.ametys.runtime.model.ElementDefinition;
050import org.ametys.runtime.model.Enumerator;
051import org.ametys.runtime.model.ModelItem;
052import org.ametys.web.frontoffice.search.metamodel.RestrictedEnumerator;
053
054/**
055 * {@link ReferencingSearchServiceCriterionDefinition} for a {@link ModelItemTypeConstants#CONTENT_ELEMENT_TYPE_ID} attribute.
056 */
057public class ContentReferencingSearchServiceCriterionDefinition extends ReferencingSearchServiceCriterionDefinition<ContentValue> implements ContentElementDefinition
058{
059    private static final Logger __LOGGER = LoggerFactory.getLogger(ContentReferencingSearchServiceCriterionDefinition.class);
060    
061    /** The resolver */
062    protected AmetysObjectResolver _resolver;
063    /** The content helper */
064    protected ContentHelper _contentHelper;
065    
066    /**
067     * Constructor used to create a FO criterion definition on a referenced item of type content
068     * @param reference the item referenced by this criterion
069     * @param referencePath the path of the criterion's reference
070     * @param baseContentType the content type defining the reference
071     */
072    public ContentReferencingSearchServiceCriterionDefinition(ElementDefinition reference, String referencePath, ContentType baseContentType)
073    {
074        super(reference, referencePath, baseContentType);
075    }
076    
077    @Override
078    public boolean isEnumerated()
079    {
080        return true;
081    }
082    
083    @Override
084    public RestrictedEnumerator<ContentValue> getRestrictedEnumerator(Map<String, Object> contextualParameters)
085    {
086        Enumerator<ContentValue> enumerator = new ContentEnumerator(this, contextualParameters);
087        return new RestrictedWrappedEnumerator<>(enumerator, getName());
088    }
089    
090    /**
091     * Gets the content type expression for retrieving enumeration of contents
092     * @param parentCTypeId The parent content type id
093     * @return The {@link Expression}
094     */
095    protected Expression getContentTypeExpression(String parentCTypeId)
096    {
097        Stream<String> subCTypesIds = _getContentTypeExtensionPoint().getSubTypes(parentCTypeId).stream();
098        Expression[] exprs = Stream.concat(Stream.of(parentCTypeId), subCTypesIds)
099                .map(cTypeId -> new ContentTypeExpression(Operator.EQ, cTypeId))
100                .toArray(Expression[]::new);
101        return new OrExpression(exprs);
102    }
103    
104
105    /**
106     * Get the order of the content id value for a search criterion definition referencing a model item of type content
107     * @param contentValue the content value
108     * @return the order or null if there is no order
109     */
110    public Long getOrder(ContentValue contentValue)
111    {
112        return contentValue.getContentIfExists()
113                           .filter(_getContentHelper()::isReferenceTable)
114                           .filter(content -> content.hasDefinition("order"))
115                           .filter(content -> content.hasValue("order"))
116                           .map(content -> content.<Long>getValue("order"))
117                           .orElse(null);
118    }
119    
120    @Override
121    public ContentElementDefinition getReference()
122    {
123        return (ContentElementDefinition) super.getReference();
124    }
125    
126    public String getContentTypeId()
127    {
128        return getReference().getContentTypeId();
129    }
130    
131    public void setContentTypeId(String contentTypeId)
132    {
133        throw new UnsupportedOperationException("The criterion definition '" + getName() + "' references a criterion of type content. The content type id can not be set to the criterion definition.");
134    }
135    
136    public Collection< ? extends ModelItem> getModelItems()
137    {
138        return getReference().getModelItems();
139    }
140    
141    /**
142     * Retrieves the {@link AmetysObjectResolver} 
143     * @return the {@link AmetysObjectResolver}
144     */
145    protected AmetysObjectResolver _getAmetysObjectResolver()
146    {
147        if (_resolver == null)
148        {
149            try
150            {
151                _resolver = (AmetysObjectResolver) __serviceManager.lookup(AmetysObjectResolver.ROLE);
152            }
153            catch (ServiceException e)
154            {
155                throw new RuntimeException("Unable to lookup after the ametys object resolver", e);
156            }
157        }
158        
159        return _resolver;
160    }
161    
162    /**
163     * Retrieves the {@link ContentHelper} 
164     * @return the {@link ContentHelper}
165     */
166    protected ContentHelper _getContentHelper()
167    {
168        if (_contentHelper == null)
169        {
170            try
171            {
172                _contentHelper = (ContentHelper) __serviceManager.lookup(ContentHelper.ROLE);
173            }
174            catch (ServiceException e)
175            {
176                throw new RuntimeException("Unable to lookup after the content helper", e);
177            }
178        }
179        
180        return _contentHelper;
181    }
182    
183    static class ContentEnumerator implements Enumerator<ContentValue>
184    {
185        ContentReferencingSearchServiceCriterionDefinition _criterionDefinition;
186        Map<String, Object> _contextualParameters;
187        
188        ContentEnumerator(ContentReferencingSearchServiceCriterionDefinition criterionDefinition, Map<String, Object> contextualParameters)
189        {
190            _criterionDefinition = criterionDefinition;
191            _contextualParameters = contextualParameters;
192        }
193        
194        public Map<ContentValue, I18nizableText> getEntries() throws Exception
195        {
196            String language = (String) _contextualParameters.get("lang");
197            
198            String contentTypeId = _criterionDefinition.getContentTypeId();
199            if (contentTypeId != null)
200            {
201                try
202                {
203                    @SuppressWarnings("synthetic-access")
204                    boolean multilingual = _criterionDefinition._getContentTypeExtensionPoint().getExtension(contentTypeId).isMultilingual();
205                    Expression expr = new AndExpression(
206                            _criterionDefinition.getContentTypeExpression(contentTypeId),
207                            multilingual || StringUtils.isEmpty(language) ? null : new LanguageExpression(Operator.EQ, language));
208                    AmetysObjectIterable<Content> contents = _criterionDefinition._getAmetysObjectResolver().query(QueryHelper.getXPathQuery(null, RepositoryConstants.NAMESPACE_PREFIX + ":content", expr));
209                    
210                    return contents.stream()
211                            .map(content -> Pair.of(new ContentValue(_criterionDefinition._getAmetysObjectResolver(), content.getId()), content.getTitle(LocaleUtils.toLocale(language))))
212                            .sorted(Comparator.comparing(Pair::getRight)) // sort by title
213                            .collect(LambdaUtils.Collectors.toLinkedHashMap(Pair::getLeft, pair -> new I18nizableText(pair.getRight())));
214                }
215                catch (Exception e)
216                {
217                    __LOGGER.error("Failed to get content enumeration for content type {}", contentTypeId, e);
218                    return Collections.EMPTY_MAP;
219                }
220            }
221            
222            return Collections.EMPTY_MAP;
223        }
224    }
225}
226