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