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}