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.instance.model;
017
018import java.util.List;
019import java.util.Map;
020import java.util.Optional;
021
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025import org.ametys.cms.search.advanced.WrappedValue;
026import org.ametys.cms.search.model.CriterionDefinition;
027import org.ametys.web.frontoffice.search.metamodel.SearchServiceCriterionDefinition;
028
029/**
030 * The mode of a {@link SearchServiceCriterion}
031 */
032public enum SearchServiceCriterionMode
033{
034    /**
035     * The criterion is static, i.e. valued by the contributor  and is not a proposed criterion to the final user.
036     */
037    STATIC
038    {
039        @Override
040        public <T> CriterionWrappedValue getValue(SearchServiceCriterion<T> criterion, Map<String, Object> userCriteria, Map<String, Object> contextualParameters)
041        {
042            Object staticValue = criterion.getStaticValue().get();
043            Object convertedValue = criterion.getCriterionDefinition().convertQueryValue(staticValue, contextualParameters);
044            CriterionWrappedValue result = new CriterionWrappedValue(convertedValue);
045            __LOGGER.debug("Value for static criterion '{}': {}", criterion.getName(), result);
046            return result;
047        }
048    },
049    
050    /**
051     * The criterion is proposed to the final user.
052     */
053    USER_INPUT
054    {
055        @Override
056        public <T> CriterionWrappedValue getValue(SearchServiceCriterion<T> criterion, Map<String, Object> userCriteria, Map<String, Object> contextualParameters)
057        {
058            String criterionId = criterion.getName();
059            Object val = userCriteria.get(criterionId);
060            SearchServiceCriterionDefinition criterionDefinition = criterion.getCriterionDefinition();
061            if (val != null)
062            {
063                boolean hasValue = !val.equals(NONE_VALUE);
064                boolean requestEmptyValue = !hasValue
065                        && !_isMandatory(criterion)
066                        && criterionDefinition.isEnumerated();
067                
068                Object convertedValue = criterion.getCriterionDefinition().convertQueryValue(val, contextualParameters);
069                CriterionWrappedValue result = new CriterionWrappedValue(convertedValue, hasValue, requestEmptyValue);
070                __LOGGER.debug("Value from user for criterion '{}': {}", criterionId, result);
071                return result;
072            }
073            
074            // val is null
075            // no need to build a collection of all existing entries of the enumerated values as all are allowed, and empty is allowed too => avoid an unnecessary query on the indexed field
076            __LOGGER.debug("Value from user for criterion '{}': null", criterionId);
077            return new CriterionWrappedValue(null);
078        }
079    },
080    
081    /**
082     * The criterion is proposed to the final user, with a restricted enumeration of choices.
083     */
084    RESTRICTED_USER_INPUT
085    {
086        @Override
087        public <T> CriterionWrappedValue getValue(SearchServiceCriterion<T> criterion, Map<String, Object> userCriteria, Map<String, Object> contextualParameters)
088        {
089            String criterionId = criterion.getName();
090            Object val = userCriteria.get(criterionId);
091            if (val != null)
092            {
093                boolean hasValue = !val.equals(NONE_VALUE);
094                boolean requestEmptyValue = !hasValue && !_isMandatory(criterion);
095                Object convertedValue = criterion.getCriterionDefinition().convertQueryValue(val, contextualParameters);
096                CriterionWrappedValue result = new CriterionWrappedValue(convertedValue, hasValue, requestEmptyValue);
097                __LOGGER.debug("Value from user for (restricted) criterion '{}': {}", criterionId, result);
098                return result;
099            }
100            else
101            {
102                try
103                {
104                    // The criterion was not filled by the visitor
105                    // Consider it as the 'All' option to filter results matching with at least one value in the restricted values
106                    List<T> vals = criterion
107                            .getRestrictedValues()
108                            .get()
109                            .values()
110                            .keySet()
111                            .stream()
112                            .toList();
113                    
114                    boolean requestEmptyValue = !_isMandatory(criterion);
115                    Object convertedValue = criterion.getCriterionDefinition().convertQueryValue(vals, contextualParameters);
116                    CriterionWrappedValue result = new CriterionWrappedValue(convertedValue, true, requestEmptyValue);
117                    __LOGGER.debug("Value for criterion '{}' (computed because not filled by user but is restricted): {}", criterionId, result);
118                    return result;
119                }
120                catch (Exception e)
121                {
122                    // An error occurred while retrieving restricted values
123                    throw new IllegalStateException("An unexpected error occured. Unable to compute restricted values for criterion '" + criterionId + "'", e);
124                }
125            }
126        }
127    },
128    
129    /**
130     * The criterion is proposed for content profiled by user group tag.
131     */
132    PROFILED_GROUPS_TAGS_INPUT
133    {
134        @Override
135        public <T> CriterionWrappedValue getValue(SearchServiceCriterion<T> criterion, Map<String, Object> userCriteria, Map<String, Object> contextualParameters)
136        {
137            return new CriterionWrappedValue(PROFILED_GROUPS_TAGS_VALUE);
138        }
139    };
140    
141    /** The none value */
142    public static final String NONE_VALUE = "__ametys_none";
143    
144    /** The profiled groups tags value */
145    public static final String PROFILED_GROUPS_TAGS_VALUE = "__ametys_profiled_groups_tags";
146    
147    static final Logger __LOGGER = LoggerFactory.getLogger(SearchServiceCriterionMode.class);
148
149    /**
150     * Gets the value from the {@link SearchServiceCriterion criterion} and the final user criteria.
151     * @param criterion The {@link SearchServiceCriterion} which is in this mode
152     * @param finalUserCriteria The criteria from the final user
153     * @param contextualParameters The contextual parameters
154     * @return the value
155     * @param <T> Type of the criterion value
156     */
157    public abstract <T> CriterionWrappedValue getValue(SearchServiceCriterion<T> criterion, Map<String, Object> finalUserCriteria, Map<String, Object> contextualParameters);
158    
159    /**
160     * <code>true</code> if the criterion is static
161     * @return <code>true</code> if the criterion is static
162     */
163    public boolean isStatic()
164    {
165        return this.equals(STATIC) || this.equals(PROFILED_GROUPS_TAGS_INPUT);
166    }
167    
168    private static boolean _isMandatory(SearchServiceCriterion criterion)
169    {
170        return Optional.of(criterion.getCriterionDefinition())
171                       .map(CriterionDefinition::getValidator)
172                       .map(validator -> validator.getConfiguration().containsKey("mandatory") && (Boolean) validator.getConfiguration().get("mandatory"))
173                       .orElse(false);
174    }
175    
176    /**
177     * A {@link WrappedValue}, being the return type of {@link SearchServiceCriterionMode#getValue}. It can notify for enumerated criteria if empty value need to be requested, through {@link #requestEmptyValue()}
178     */
179    public static final class CriterionWrappedValue extends WrappedValue
180    {
181        private final boolean _hasValue;
182        private final boolean _requestEmptyValue;
183        
184        private CriterionWrappedValue(Object value)
185        {
186            this(value, true, false);
187        }
188        
189        private CriterionWrappedValue(Object values, boolean hasValue, boolean requestEmptyValue)
190        {
191            super(values);
192            _hasValue = hasValue;
193            _requestEmptyValue = requestEmptyValue;
194        }
195        
196        /**
197         * Determines if an empty value request is necessary
198         * An empty value request is necessary if the wrapped value is from enumerated and not mandatory data, or if the user has selected none or all values for restricted user input.
199         * @return <code>true</code> if an empty value request is necessary, <code>false</code> otherwise
200         */
201        public boolean requestEmptyValue()
202        {
203            return _requestEmptyValue;
204        }
205        
206        /**
207         * Determines if a real value (not the none one) has been selected
208         * @return <code>true</code> if a value has been selected, <code>false</code> otherwise
209         */
210        public boolean hasValue()
211        {
212            return _hasValue;
213        }
214        
215        @Override
216        public String toString()
217        {
218            return super.toString() + "{_hasValue:" + _hasValue + ", requestEmptyValue:" + _requestEmptyValue + "}";
219        }
220    }
221}