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.web.frontoffice.search.requesttime.impl;
017
018import java.lang.reflect.Field;
019import java.lang.reflect.Type;
020import java.util.Map;
021import java.util.Objects;
022import java.util.Optional;
023import java.util.function.Function;
024import java.util.stream.Stream;
025
026import org.apache.avalon.framework.activity.Initializable;
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.configuration.Configurable;
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang3.ArrayUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.commons.lang3.tuple.Pair;
039import org.slf4j.Logger;
040import org.xml.sax.ContentHandler;
041
042import org.ametys.cms.search.SearchResults;
043import org.ametys.cms.search.advanced.AbstractTreeNode;
044import org.ametys.cms.search.advanced.TreeInternalNode;
045import org.ametys.cms.search.advanced.TreeLeaf;
046import org.ametys.cms.search.solr.SearcherFactory.Searcher;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.model.type.ModelItemType;
052import org.ametys.runtime.model.type.ModelItemTypeConstants;
053import org.ametys.web.frontoffice.search.SearchService;
054import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
055import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap;
056import org.ametys.web.frontoffice.search.metamodel.FacetDefinition;
057import org.ametys.web.frontoffice.search.metamodel.Returnable;
058import org.ametys.web.frontoffice.search.metamodel.SearchCriterionDefinition;
059import org.ametys.web.frontoffice.search.metamodel.Searchable;
060import org.ametys.web.frontoffice.search.metamodel.SortDefinition;
061import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
062import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
063import org.ametys.web.frontoffice.search.requesttime.SearchServiceDebugModeHelper;
064import org.ametys.web.frontoffice.search.requesttime.SearchServiceDebugModeHelper.DebugMode;
065import org.ametys.web.repository.page.ZoneItem;
066import org.ametys.web.service.ServiceParameter;
067
068import com.google.gson.ExclusionStrategy;
069import com.google.gson.FieldAttributes;
070import com.google.gson.Gson;
071import com.google.gson.GsonBuilder;
072import com.google.gson.JsonElement;
073import com.google.gson.JsonObject;
074import com.google.gson.JsonParser;
075import com.google.gson.JsonPrimitive;
076import com.google.gson.JsonSerializationContext;
077import com.google.gson.JsonSerializer;
078
079/**
080 * {@link SearchComponent} for debugging.
081 */
082public class DebugSearchComponent implements SearchComponent, Configurable, Serviceable, Initializable
083{
084    private static final String __VALUE_TO_DISPLAY_FOR_HIDDEN_PARAMETERS = "**** (hidden value, please check the value directly in the repository)";
085    
086    private int _part;
087    private AmetysObjectResolver _resolver;
088    private Gson _gson;
089    
090    @Override
091    public void configure(Configuration configuration) throws ConfigurationException
092    {
093        _part = configuration.getChild("part").getValueAsInteger();
094    }
095
096    @Override
097    public void service(ServiceManager manager) throws ServiceException
098    {
099        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
100    }
101    
102    @Override
103    public void initialize() throws Exception
104    {
105        if (_part == 2)
106        {
107            _createGson();
108        }
109    }
110    
111    private void _createGson()
112    {
113        _gson = _registerTypeAdapters(new GsonBuilder())
114                .setPrettyPrinting()
115                .disableHtmlEscaping()
116                .setFieldNamingStrategy((Field f) -> StringUtils.removeStart(f.getName(), "_"))
117                .addSerializationExclusionStrategy(_exclusionStrategy())
118                .create();
119    }
120    
121    private static ExclusionStrategy _exclusionStrategy()
122    {
123        return new ExclusionStrategy()
124        {
125            @Override public boolean shouldSkipField(FieldAttributes f)
126            { return false; }
127            
128            @Override public boolean shouldSkipClass(Class< ? > clazz)
129            {
130                return ArrayUtils.contains(clazz.getInterfaces(), Component.class);
131            }
132        };
133    }
134    
135    private static GsonBuilder _registerTypeAdapters(GsonBuilder gsonBuilder)
136    {
137        JsonSerializer<Pair<?, ?>> pairSerializer = (Pair<?, ?> src, Type typeOfSrc, JsonSerializationContext context) -> 
138        {
139            var obj = new JsonObject();
140            obj.add("Pair::getLeft", context.serialize(src.getLeft()));
141            obj.add("Pair::getRight", context.serialize(src.getRight()));
142            return obj;
143        };
144        JsonSerializer<I18nizableText> i18nTextSerializer = (I18nizableText src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.toString());
145        JsonSerializer<Returnable> returnableSerializer = (Returnable src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getId());
146        JsonSerializer<Searchable> searchableSerializer = (Searchable src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getClass().getName());
147        JsonSerializer<ServiceParameter<?>> serviceParameterSerializer = (ServiceParameter<?> src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getName());
148        JsonSerializer<AdditionalParameterValueMap> additionalParameterValueMapSerializer = (AdditionalParameterValueMap src, Type typeOfSrc, JsonSerializationContext context) ->
149        {
150            JsonObject obj = new JsonObject();
151            for (String parameterId : src.getParameterIds())
152            {
153                obj.addProperty(parameterId, src.getDisplayableValue(parameterId, __VALUE_TO_DISPLAY_FOR_HIDDEN_PARAMETERS));
154            }
155            return obj;
156        };
157        JsonSerializer<AbstractTreeNode<?>> treeNodeSerializer = (AbstractTreeNode<?> src, Type typeOfSrc, JsonSerializationContext context) ->
158        {
159            if (src instanceof TreeLeaf<?>)
160            {
161                var obj = new JsonObject();
162                JsonElement value = context.serialize(((TreeLeaf<?>) src).getValue());
163                obj.add("value", value);
164                return obj;
165            }
166            else
167            {
168                var node = (TreeInternalNode<?>) src;
169                var obj = new JsonObject();
170                obj.add("children", context.serialize(node.getChildren()));
171                obj.add("logicalOperator", context.serialize(node.getLogicalOperator()));
172                return obj;
173            }
174        };
175        JsonSerializer<SearchCriterionDefinition> criterionDefinitionSerializer = (SearchCriterionDefinition src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getId());
176        JsonSerializer<FacetDefinition> facetDefinitionSerializer = (FacetDefinition src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getId());
177        JsonSerializer<SortDefinition> sortDefinitionSerializer = (SortDefinition src, Type typeOfSrc, JsonSerializationContext context) -> new JsonPrimitive(src.getId());
178        
179        return gsonBuilder
180            .registerTypeAdapter(Pair.class, pairSerializer)
181            .registerTypeAdapter(I18nizableText.class, i18nTextSerializer)
182            .registerTypeAdapter(Returnable.class, returnableSerializer)
183            .registerTypeAdapter(Searchable.class, searchableSerializer)
184            .registerTypeAdapter(ServiceParameter.class, serviceParameterSerializer)
185            .registerTypeAdapter(AdditionalParameterValueMap.class, additionalParameterValueMapSerializer)
186            .registerTypeHierarchyAdapter(AbstractTreeNode.class, treeNodeSerializer)
187            .registerTypeAdapter(SearchCriterionDefinition.class, criterionDefinitionSerializer)
188            .registerTypeAdapter(SortDefinition.class, sortDefinitionSerializer)
189            .registerTypeAdapter(FacetDefinition.class, facetDefinitionSerializer);
190    }
191    
192    @Override
193    public int priority()
194    {
195        return _part == 1
196                ? SEARCH_PRIORITY - 20000
197                : SEARCH_PRIORITY + 20000;
198    }
199
200    @Override
201    public boolean supports(SearchComponentArguments args)
202    {
203        return args.isDebug();
204    }
205    
206    static String appendDebugRequestParameters(String url, SearchComponentArguments args)
207    {
208        if (args.debugMode().orElse(null) == DebugMode.DEBUG_VIEW_AFTER_VALIDATE)
209        {
210            StringBuilder modifiedUrl = new StringBuilder(url)
211                    .append(url.contains("?") ? "" : "?")
212                    .append("&cocoon-view=service.debug")
213                    .append("&").append(SearchServiceDebugModeHelper.DEBUG_MODE).append("=").append(DebugMode.NORMAL.getInt());
214            return modifiedUrl.toString();
215        }
216        else
217        {
218            return url;
219        }
220    }
221
222    @Override
223    public void execute(SearchComponentArguments args) throws Exception
224    {
225        if (_part == 1)
226        {
227            _executePart1(args);
228        }
229        else
230        {
231            _executePart2(args);
232        }
233    }
234    
235    private void _executePart1(SearchComponentArguments args)
236    {
237        _setSolrDebug(args.searcher());
238    }
239    
240    private void _executePart2(SearchComponentArguments args) throws Exception
241    {
242        if (_mustSaxDebug(args))
243        {
244            _saxDebug(args);
245        }
246    }
247    
248    private void _setSolrDebug(Searcher searcher)
249    {
250        searcher.setDebugOn();
251    }
252    
253    private boolean _mustSaxDebug(SearchComponentArguments args)
254    {
255        return args.debugMode().get() == DebugMode.NORMAL;
256    }
257    
258    private void _saxDebug(SearchComponentArguments args) throws Exception
259    {
260        Logger logger = args.logger();
261        ContentHandler contentHandler = args.contentHandler();
262        SearchService service = args.service();
263        SearchServiceInstance serviceInstance = args.serviceInstance();
264        String id = serviceInstance.getId();
265        ZoneItem zoneItem = _resolver.resolveById(id);
266        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
267        XMLUtils.startElement(contentHandler, "debug");
268        _saxServiceParameters(logger, contentHandler, service, serviceParameters);
269        _saxSearchServiceInstance(contentHandler, serviceInstance);
270        _saxUserCriteria(contentHandler, args.userInputs().criteria());
271        _saxDebugMap(contentHandler, args.results());
272        XMLUtils.endElement(contentHandler, "debug");
273    }
274    
275    private void _saxServiceParameters(Logger logger, ContentHandler contentHandler, SearchService service, ModelAwareDataHolder serviceParameters) throws Exception
276    {
277        var parametersAtts = new AttributesImpl();
278        parametersAtts.addCDATAAttribute("description", "Here are the values of the parameters, as they are stored in JCR (they are NOT the service instance parameters).");
279        XMLUtils.startElement(contentHandler, "parameters", parametersAtts);
280        XMLUtils.data(contentHandler, "The valued parameters are:");
281        
282        for (String serviceParam : service.getParameters().keySet())
283        {
284            if (!serviceParameters.hasValue(serviceParam))
285            {
286                logger.warn("No value stored for service parameter with name '{}'", serviceParam);
287                continue;
288            }
289            
290            XMLUtils.startElement(contentHandler, "parameter");
291            XMLUtils.createElement(contentHandler, "name", serviceParam);
292            
293            ModelItemType type = serviceParameters.getType(serviceParam);
294            boolean isMultiple = serviceParameters.isMultiple(serviceParam);
295            if (isMultiple)
296            {
297                Object[] values = serviceParameters.getValue(serviceParam);
298                String[] strVals = Stream.of(values)
299                        .map(v -> _parameterValueToString(v, type))
300                        .toArray(String[]::new);
301                AttributesImpl atts = new AttributesImpl();
302                atts.addCDATAAttribute("multiple", "true");
303                XMLUtils.startElement(contentHandler, "value", atts);
304                for (int i = 0; i < strVals.length; i++)
305                {
306                    String val = _prettifyJson(strVals[i], logger);
307                    XMLUtils.createElement(contentHandler, String.valueOf(i), val);
308                }
309                XMLUtils.endElement(contentHandler, "value");
310            }
311            else
312            {
313                Object value = serviceParameters.getValue(serviceParam);
314                String strValue = _parameterValueToString(value,  type);
315                String prettyStrVal = _prettifyJson(strValue, logger);
316                XMLUtils.createElement(contentHandler, "value", prettyStrVal);
317            }
318            
319            XMLUtils.endElement(contentHandler, "parameter");
320        }
321        
322        XMLUtils.endElement(contentHandler, "parameters");
323    }
324    
325    private String _parameterValueToString(Object value, ModelItemType type)
326    {
327        if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.contentEquals(type.getId()))
328        {
329            // hide password !
330            return __VALUE_TO_DISPLAY_FOR_HIDDEN_PARAMETERS;
331        }
332        return Objects.toString(value);
333    }
334    
335    private String _prettifyJson(String input, Logger logger)
336    {
337        if (_seemsLikeJson(input))
338        {
339            JsonParser parser = new JsonParser();
340            try
341            {
342                JsonElement parsedElement = parser.parse(input);
343                String prettyJson = _gson.toJson(parsedElement);
344                return prettyJson.contains("\n") ? "\n" + prettyJson + "\n" : prettyJson;
345            }
346            catch (Exception e)
347            {
348                logger.warn("Cannot jsonify input '{}'", input, e);
349            }
350        }
351        return input;
352    }
353    
354    private boolean _seemsLikeJson(String input)
355    {
356        return input.startsWith("{") && input.endsWith("}");
357    }
358    
359    private void _saxSearchServiceInstance(ContentHandler contentHandler, SearchServiceInstance searchServiceInstance) throws Exception
360    {
361        var atts = new AttributesImpl();
362        atts.addCDATAAttribute("description", "Here is the representation of the search service instance");
363        String asJson = "\n" + _gson.toJson(searchServiceInstance) + "\n";
364        XMLUtils.createElement(contentHandler, "searchServiceInstance", atts, asJson);
365    }
366    
367    private void _saxUserCriteria(ContentHandler contentHandler, Map<String, Object> userCriteria) throws Exception
368    {
369        var atts = new AttributesImpl();
370        atts.addCDATAAttribute("description", "Here are the criterion values given by the user");
371        XMLUtils.startElement(contentHandler, "userInputCriteria", atts);
372        for (String userInputKey : userCriteria.keySet())
373        {
374            XMLUtils.startElement(contentHandler, "userInputCriterion");
375            XMLUtils.createElement(contentHandler, "name", userInputKey);
376            XMLUtils.createElement(contentHandler, "value", userCriteria.get(userInputKey).toString());
377            XMLUtils.endElement(contentHandler, "userInputCriterion");
378        }
379        XMLUtils.endElement(contentHandler, "userInputCriteria");
380    }
381    
382    private void _saxDebugMap(ContentHandler contentHandler, Optional<SearchResults<AmetysObject>> results) throws Exception
383    {
384        Optional<Map<String, Object>> debugMap = results
385            .map(SearchResults::getDebugMap)
386            .flatMap(Function.identity());
387        
388        if (debugMap.isPresent())
389        {
390            String prettyMap = "\n" + _gson.toJson(debugMap.get()) + "\n";
391            var atts = new AttributesImpl();
392            atts.addCDATAAttribute("description", "Here is the DebugMap returned by the Solr server");
393            XMLUtils.createElement(contentHandler, "debugMap", atts, prettyMap);
394        }
395    }
396}