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.Collections;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Optional;
024import java.util.function.Function;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import org.ametys.cms.contenttype.ContentType;
029import org.ametys.cms.search.model.SystemProperty;
030import org.ametys.cms.search.query.Query;
031import org.ametys.cms.search.query.Query.Operator;
032import org.ametys.cms.search.ui.model.SearchUICriterion;
033import org.ametys.cms.tag.CMSTag;
034import org.ametys.cms.tag.Tag;
035import org.ametys.cms.tag.TagProvider;
036import org.ametys.cms.tag.TagProviderExtensionPoint;
037import org.ametys.core.util.JSONUtils;
038import org.ametys.core.util.LambdaUtils;
039import org.ametys.runtime.i18n.I18nizableText;
040import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues;
041import org.ametys.web.frontoffice.search.metamodel.EnumeratedValues.RestrictedValues;
042import org.ametys.web.frontoffice.search.metamodel.Searchable;
043
044import com.google.common.collect.ImmutableMap;
045
046/**
047 * {@link ContentSearchCriterionDefinition} for the 'tag' {@link SystemProperty}.
048 */
049public class TagSearchCriterionDefinition extends ContentSearchCriterionDefinition
050{
051    TagProviderExtensionPoint _tagProviderEP;
052    JSONUtils _jsonUtils;
053    
054    /**
055     * Default constructor
056     * @param id The id
057     * @param pluginName The plugin name
058     * @param searchable the {@link Searchable}
059     * @param criterion The linked {@link SearchUICriterion}
060     * @param contentType The content type on which this criterion definition applies. Can be empty if it applies to all types of contents.
061     * @param tagProviderEP The extension point for tag providers
062     * @param jsonUtils The JSON utils
063     */
064    public TagSearchCriterionDefinition(
065            String id,
066            String pluginName,
067            Optional<Searchable> searchable,
068            SearchUICriterion criterion,
069            Optional<ContentType> contentType,
070            TagProviderExtensionPoint tagProviderEP,
071            JSONUtils jsonUtils)
072    {
073        super(id, pluginName, searchable, criterion, contentType, Optional.empty());
074        _tagProviderEP = tagProviderEP;
075        _jsonUtils = jsonUtils;
076        setWidget(_widget());
077        setWidgetParameters(_widgetParameters(criterion));
078    }
079    
080    private static String _widget()
081    {
082        return "edition.select-tags-criterion";
083    }
084    
085    private static Map<String, I18nizableText> _widgetParameters(SearchUICriterion criterion)
086    {
087        Map<String, I18nizableText> widgetParameters = new HashMap<>(criterion.getWidgetParameters());
088//        widgetParameters.put("onlyTagsWithChildren", new I18nizableText(Boolean.TRUE.toString())); // we would like this conf only for USER_RESTRICTED_VALUES
089        return widgetParameters;
090    }
091    
092    @Override
093    public boolean isEnumerated()
094    {
095        return true;
096    }
097    
098    @Override
099    public Optional<EnumeratedValues> getEnumeratedValues(Map<String, Object> contextualParameters)
100    {
101        return Optional.of(new TagEnumeratedValues(this, contextualParameters));
102    }
103    
104    @Override
105    public Query getQuery(Object value, Operator operator, String language, Map<String, Object> contextualParameters)
106    {
107        Object modifiedValue = _tryToParseJson(value);
108        
109        if (modifiedValue instanceof List<?> && !((List<?>) modifiedValue).isEmpty())
110        {
111            modifiedValue = ((List<?>) modifiedValue).stream()
112                    .map(this::_tryToParseJson)
113                    .collect(Collectors.toList());
114        }
115        
116        if (modifiedValue instanceof List<?> && !((List<?>) modifiedValue).isEmpty())
117        {
118            List<?> valueAsList = (List<?>) modifiedValue;
119            Object firstValue = valueAsList.get(0);
120            if (firstValue instanceof Map<?, ?>)
121            {
122                Boolean autoposting = (Boolean) ((Map<?, ?>) firstValue).get("autoposting");
123                String[] realValues = valueAsList
124                        .stream()
125                        .filter(Map.class::isInstance)
126                        .map(Map.class::cast)
127                        .map(map -> map.get("value"))
128                        .filter(String.class::isInstance)
129                        .map(String.class::cast)
130                        .toArray(String[]::new);
131                modifiedValue = ImmutableMap.of("autoposting", autoposting, "value", realValues);
132            }
133        }
134        
135        return super.getQuery(modifiedValue, operator, language, contextualParameters);
136    }
137    
138    private Object _tryToParseJson(Object value)
139    {
140        if (value instanceof String)
141        {
142            try
143            {
144                return _jsonUtils.convertJsonToMap((String) value);
145            }
146            catch (Exception e)
147            {
148                // was not json
149            }
150        }
151        return value;
152    }
153    
154    static class TagEnumeratedValues implements EnumeratedValues
155    {
156        TagSearchCriterionDefinition _tagSearchCriterionDef;
157        Map<String, Object> _contextualParameters;
158        
159        TagEnumeratedValues(TagSearchCriterionDefinition tagSearchCriterionDef, Map<String, Object> contextualParameters)
160        {
161            _tagSearchCriterionDef = tagSearchCriterionDef;
162            _contextualParameters = contextualParameters;
163        }
164        
165        @Override
166        public Map<Object, I18nizableText> getAllValues()
167        {
168            Map<Object, I18nizableText> tags = _tagSearchCriterionDef._tagProviderEP
169                    .getExtensionsIds()
170                    .stream()
171                    .map(_tagSearchCriterionDef._tagProviderEP::getExtension)
172                    .map(this::_getAllTags)
173                    .flatMap(Function.identity())
174                    .filter(this::_isTargetingContents)
175                    .collect(LambdaUtils.Collectors.toLinkedHashMap(
176                        Tag::getName, 
177                        Tag::getTitle));
178            return tags;
179        }
180        
181        private Stream<Tag> _getAllTags(TagProvider<? extends Tag> tagProvider)
182        {
183            return Optional.ofNullable(tagProvider.getTags(_contextualParameters))
184                    .orElseGet(Collections::emptyMap)
185                    .values()
186                    .stream()
187                    .map(this::_getDescendantAndSelfTags)
188                    .flatMap(Function.identity());
189        }
190        
191        private Stream<Tag> _getDescendantAndSelfTags(Tag parentTag)
192        {
193            return Stream.concat(
194                    Stream.of(parentTag),
195                    _getDescendantTags(parentTag));
196        }
197        
198        Stream<Tag> _getDescendantTags(Tag parentTag)
199        {
200            return _getChildrenTags(parentTag)
201                    .map(this::_getDescendantAndSelfTags)
202                    .flatMap(Function.identity());
203        }
204        
205        private Stream<? extends Tag> _getChildrenTags(Tag parentTag)
206        {
207            return parentTag == null
208                    ? Stream.empty()
209                    : parentTag.getTags()
210                        .values()
211                        .stream();
212        }
213        
214        private boolean _isTargetingContents(Tag tag)
215        {
216            return (tag instanceof CMSTag) && "CONTENT".equals(((CMSTag) tag).getTarget().getName());
217        }
218        
219        @Override
220        public RestrictedValues getRestrictedValuesFor(List<Object> objs)
221        {
222            return new TagRestrictedValues(this, objs);
223        }
224    }
225    
226    static class TagRestrictedValues implements RestrictedValues
227    {
228        private TagEnumeratedValues _enumeratedValues;
229        private List<Object> _objs;
230
231        TagRestrictedValues(TagEnumeratedValues enumeratedValues, List<Object> objs)
232        {
233            _enumeratedValues = enumeratedValues;
234            _objs = objs;
235        }
236        
237        @Override
238        public Map<Object, I18nizableText> values()
239        {
240            Boolean autoposting;
241            if (!_objs.isEmpty())
242            {
243                autoposting = _objectToAutoposting(_objs.get(0));
244            }
245            else
246            {
247                autoposting = Boolean.FALSE;
248            }
249            
250            Map<Object, I18nizableText> restrictedTags = _objs.stream()
251                    .map(this::_objectToTagId)
252                    .filter(Objects::nonNull)
253                    .map(this::_getRestrictedValuesFor)
254                    .flatMap(Function.identity())
255                    .collect(LambdaUtils.Collectors.toLinkedHashMap(
256                        tag -> _tagToSerializedJson(tag, autoposting), 
257                        Tag::getTitle));
258            return restrictedTags;
259        }
260        
261        private Boolean _objectToAutoposting(Object obj)
262        {
263            if (obj instanceof Map<?, ?>)
264            {
265                return (Boolean) ((Map<?, ?>) obj).get("autoposting");
266            }
267            return Boolean.FALSE;
268        }
269        
270        private String _objectToTagId(Object obj)
271        {
272            if (obj instanceof Map<?, ?>)
273            {
274                return (String) ((Map<?, ?>) obj).get("value");
275            }
276            else if (obj instanceof String)
277            {
278                return (String) obj;
279            }
280            return null;
281        }
282        
283        private Stream<Tag> _getRestrictedValuesFor(String tagName)
284        {
285            Tag tag = _enumeratedValues._tagSearchCriterionDef._tagProviderEP.getTag(tagName, _enumeratedValues._contextualParameters);
286            // Gets its descendants
287            return tag == null ? Stream.empty() : _enumeratedValues._getDescendantTags(tag);
288        }
289        
290        private String _tagToSerializedJson(Tag tag, Boolean autoposting)
291        {
292            Map<String, Object> tagAsJson = _tagToJson(tag, autoposting);
293            return _enumeratedValues._tagSearchCriterionDef._jsonUtils.convertObjectToJson(tagAsJson);
294        }
295        
296        private Map<String, Object> _tagToJson(Tag tag, Boolean autoposting)
297        {
298            return ImmutableMap.of(
299                    "value", tag.getName(), 
300                    "autoposting", autoposting);
301        }
302    }
303}
304