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}