001/* 002 * Copyright 2013 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.cms.search.model; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.UUID; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.cocoon.ProcessingException; 034import org.apache.commons.collections.MapUtils; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 038import org.ametys.cms.search.ui.model.DynamicWrappedSearchUIModel; 039import org.ametys.cms.search.ui.model.SearchUIColumn; 040import org.ametys.cms.search.ui.model.SearchUICriterion; 041import org.ametys.cms.search.ui.model.SearchUIModel; 042import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 043import org.ametys.cms.search.ui.model.impl.DefaultSearchUIModel; 044import org.ametys.cms.search.ui.model.impl.DefaultSolrFilterSearchUICriterion; 045import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion; 046import org.ametys.core.ui.Callable; 047import org.ametys.runtime.i18n.I18nizableText; 048import org.ametys.runtime.model.DefinitionContext; 049import org.ametys.runtime.model.ViewItem; 050import org.ametys.runtime.model.ViewItemAccessor; 051import org.ametys.runtime.parameter.Enumerator; 052import org.ametys.runtime.parameter.ParameterHelper; 053import org.ametys.runtime.parameter.Validator; 054import org.ametys.runtime.plugin.component.AbstractLogEnabled; 055 056/** 057 * Helper for {@link SearchModel}. 058 */ 059public class SearchModelHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 060{ 061 /** The component role. */ 062 public static final String ROLE = SearchModelHelper.class.getName(); 063 064 private static final String __SOLR_REQUEST_PARAMETER_NAME = "solrRequest"; 065 private static final String __SOLR_REQUEST_CRITERION_ID = "solr-filter-criterion"; 066 067 private ServiceManager _serviceManager; 068 private SearchUIModelExtensionPoint _searchUIModelExtensionPoint; 069 private ContentTypeExtensionPoint _contentTypeExtensionPoint; 070 private Context _context; 071 072 073 @Override 074 public void service(ServiceManager manager) throws ServiceException 075 { 076 _serviceManager = manager; 077 _searchUIModelExtensionPoint = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE); 078 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 079 } 080 081 @Override 082 public void contextualize(Context context) throws ContextException 083 { 084 _context = context; 085 } 086 087 /** 088 * Get the search model configuration as JSON object 089 * @param modelId The id of search model 090 * @param restrictedContentTypes The restricted content types. Can be null. 091 * @param contextualParameters the contextual parameters 092 * @return The search model configuration in a Map 093 * @throws ProcessingException if an error occurred 094 */ 095 @Callable 096 public Map<String, Object> getSearchModelConfiguration(String modelId, List<String> restrictedContentTypes, Map<String, Object> contextualParameters) throws ProcessingException 097 { 098 SearchUIModel model = getSearchUIModel(modelId, restrictedContentTypes, contextualParameters); 099 return getSearchModelInfo(model, contextualParameters); 100 } 101 102 /** 103 * Get the column configurations of search model as JSON object 104 * @param modelId The id of search model 105 * @param restrictedContentTypes The restricted content types. Can be null. 106 * @param contextualParameters the contextual parameters 107 * @return The column configurations in a List 108 * @throws ProcessingException if an error occurred 109 */ 110 @Callable 111 public List<Object> getColumnConfigurations(String modelId, List<String> restrictedContentTypes, Map<String, Object> contextualParameters) throws ProcessingException 112 { 113 SearchUIModel model = getSearchUIModel(modelId, restrictedContentTypes, contextualParameters); 114 return getColumnsInfo(model.getResultItems(contextualParameters)); 115 } 116 117 /** 118 * Retrieves the {@link SearchUIModel} with the given model identifier, with restrictions on content types 119 * @param modelId the model identifier 120 * @param restrictedContentTypes the restricted content types 121 * @param contextualParameters the contextual parameters 122 * @return the {@link SearchUIModel} 123 */ 124 public SearchUIModel getSearchUIModel(String modelId, List<String> restrictedContentTypes, Map<String, Object> contextualParameters) 125 { 126 SearchUIModel model = _searchUIModelExtensionPoint.getExtension(modelId); 127 if (model == null) 128 { 129 throw new IllegalArgumentException("The search model '" + modelId + "' does not exist"); 130 } 131 132 // TODO Replace DynamicWrappedSearchUIModel? 133 if (restrictedContentTypes != null) 134 { 135 model = new DynamicWrappedSearchUIModel(model, restrictedContentTypes, _contentTypeExtensionPoint, getLogger(), _context, _serviceManager); 136 } 137 138 Optional<String> solrRequest = Optional.ofNullable(contextualParameters.get(__SOLR_REQUEST_PARAMETER_NAME)) 139 .filter(String.class::isInstance) 140 .map(String.class::cast) 141 .filter(StringUtils::isNotEmpty); 142 if (solrRequest.isPresent()) 143 { 144 DefaultSearchUIModel modelCopy = new DefaultSearchUIModel(model, contextualParameters); 145 146 // Create a criterion with solr request 147 DefaultSolrFilterSearchUICriterion criterion = new DefaultSolrFilterSearchUICriterion(); 148 criterion.setId(__SOLR_REQUEST_CRITERION_ID); 149 criterion.setQuery(solrRequest.get()); 150 151 // Add the criterion to the search model 152 modelCopy.addCriterion(criterion); 153 154 model = modelCopy; 155 } 156 157 return model; 158 } 159 160 /** 161 * Return information on a {@link SearchUIModel} object serialized in a Map. 162 * @param model The search model. 163 * @param contextualParameters The contextual parameters 164 * @return the detailed information serialized in a Map. 165 * @throws ProcessingException if an error occurs. 166 */ 167 public Map<String, Object> getSearchModelInfo(SearchUIModel model, Map<String, Object> contextualParameters) throws ProcessingException 168 { 169 Map<String, Object> jsonObject = new HashMap<>(); 170 171 jsonObject.put("pageSize", model.getPageSize(contextualParameters)); 172 jsonObject.put("workspace", model.getWorkspace(contextualParameters)); 173 jsonObject.put("searchUrl", model.getSearchUrl(contextualParameters)); 174 jsonObject.put("searchUrlPlugin", model.getSearchUrlPlugin(contextualParameters)); 175 jsonObject.put("exportCSVUrl", model.getExportCSVUrl(contextualParameters)); 176 jsonObject.put("exportCSVUrlPlugin", model.getExportCSVUrlPlugin(contextualParameters)); 177 jsonObject.put("exportDOCUrl", model.getExportDOCUrl(contextualParameters)); 178 jsonObject.put("exportDOCUrlPlugin", model.getExportDOCUrlPlugin(contextualParameters)); 179 jsonObject.put("exportXMLUrl", model.getExportXMLUrl(contextualParameters)); 180 jsonObject.put("exportXMLUrlPlugin", model.getExportXMLUrlPlugin(contextualParameters)); 181 jsonObject.put("exportPDFUrl", model.getExportPDFUrl(contextualParameters)); 182 jsonObject.put("exportPDFUrlPlugin", model.getExportPDFUrlPlugin(contextualParameters)); 183 jsonObject.put("printUrl", model.getPrintUrl(contextualParameters)); 184 jsonObject.put("printUrlPlugin", model.getPrintUrlPlugin(contextualParameters)); 185 jsonObject.put("summaryView", model.getSummaryView()); 186 187 jsonObject.put("simple-criteria", getCriteriaListInfo(model.getCriteria(contextualParameters))); 188 jsonObject.put("advanced-criteria", getAdvancedCriteriaListInfo(model.getAdvancedCriteria(contextualParameters))); 189 190 191 jsonObject.put("columns", getColumnsInfo(model.getResultItems(contextualParameters))); 192 193 jsonObject.put("hasFacets", !model.getFacetedCriteria(contextualParameters).isEmpty()); 194 195 return jsonObject; 196 } 197 198 /** 199 * Return information of columns in the given view item accessor, serialized as a Map. 200 * @param viewItemAccessor the view item accessor containing the columns. 201 * @return the detailed information serialized in a Map. 202 * @throws ProcessingException if an error occurs. 203 */ 204 public List<Object> getColumnsInfo(ViewItemAccessor viewItemAccessor) throws ProcessingException 205 { 206 List<Object> jsonObject = new ArrayList<>(); 207 208 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 209 { 210 if (viewItem instanceof SearchUIColumn searchUIColumn) 211 { 212 jsonObject.add(searchUIColumn.toJSON(DefinitionContext.newInstance())); 213 } 214 else if (viewItem instanceof ViewItemAccessor itemAccessor) 215 { 216 jsonObject.addAll(getColumnsInfo(itemAccessor)); 217 } 218 } 219 220 return jsonObject; 221 } 222 223 /** 224 * Return information on a list of {@link SearchUICriterion}, serialized as a Map. 225 * @param criteria a map of search criteria. 226 * @return the detailed information serialized in a Map. 227 * @throws ProcessingException if an error occurs. 228 */ 229 public Map<String, Object> getCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException 230 { 231 Map<String, Object> jsonObject = new LinkedHashMap<>(); 232 233 Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = _getCriteriaByGroup(criteria); 234 235 for (I18nizableText group : criteriaByGroup.keySet()) 236 { 237 Map<String, Object> elements = new LinkedHashMap<>(); 238 239 for (SearchUICriterion sc : criteriaByGroup.get(group)) 240 { 241 elements.put(sc.getId(), getCriterionInfo(sc)); 242 } 243 244 Map<String, Object> groupAsJSON = new LinkedHashMap<>(); 245 if (group != null) 246 { 247 groupAsJSON.put("label", group); 248 } 249 groupAsJSON.put("role", "fieldset"); 250 groupAsJSON.put("elements", elements); 251 252 String groupUUID = UUID.randomUUID().toString(); 253 jsonObject.put(groupUUID, groupAsJSON); 254 } 255 return jsonObject; 256 } 257 258 /** 259 * Return information on a list of advanced {@link SearchUICriterion}, serialized as a Map. 260 * @param criteria A map of advanced search criteria. 261 * @return the detailed information serialized in a Map. 262 * @throws ProcessingException if an error occurs. 263 */ 264 public Map<String, Object> getAdvancedCriteriaListInfo(Map<String, ? extends SearchUICriterion> criteria) throws ProcessingException 265 { 266 // No groups for advanced search 267 Map<String, Object> jsonObject = new LinkedHashMap<>(); 268 Map<String, Object> criteriaObject = new LinkedHashMap<>(); 269 270 for (SearchUICriterion sc : criteria.values()) 271 { 272 if (sc instanceof SystemSearchCriterion) 273 { 274 SystemSearchUICriterion sysCrit = (SystemSearchUICriterion) sc; 275 if (sysCrit.getSystemPropertyId().equals("contentLanguage")) 276 { 277 // Separate the language from the other criteria since it is mandatory 278 jsonObject.put("language", getCriterionInfo(sc)); 279 } 280 else 281 { 282 criteriaObject.put(sc.getId(), getCriterionInfo(sc)); 283 } 284 } 285 else 286 { 287 criteriaObject.put(sc.getId(), getCriterionInfo(sc)); 288 } 289 } 290 291 if (MapUtils.isNotEmpty(criteriaObject)) 292 { 293 jsonObject.put("criteria", criteriaObject); 294 } 295 296 return jsonObject; 297 } 298 299 /** 300 * Return information on a {@link SearchUICriterion}, serialized as a Map. 301 * @param criterion a search criterion. 302 * @return the detailed information serialized in a Map. 303 * @throws ProcessingException if an error occurs. 304 */ 305 public Map<String, Object> getCriterionInfo(SearchUICriterion criterion) throws ProcessingException 306 { 307 Map<String, Object> jsonObject = new HashMap<>(); 308 309 // TODO Parameter interface 310 _putCriterionParameter(jsonObject, criterion); 311 312 jsonObject.put("multiple", criterion.isMultiple()); 313 jsonObject.put("hidden", criterion.isHidden()); 314 315 String contentTypeId = criterion.getContentTypeId(); 316 if (contentTypeId != null) 317 { 318 jsonObject.put("contentType", contentTypeId); 319 } 320 321 jsonObject.put("criterionProperty", criterion.getFieldId()); 322 // TODO use operator.getName()? 323 if (criterion.getOperator() != null) 324 { 325 jsonObject.put("criterionOperator", criterion.getOperator().toString().toLowerCase()); 326 } 327 328// if (criterion.getValue() != null) 329// { 330// jsonObject.put("value", criterion.getValue()); 331// } 332 333 return jsonObject; 334 } 335 336 private Map<I18nizableText, List<SearchUICriterion>> _getCriteriaByGroup(Map<String, ? extends SearchUICriterion> criteria) 337 { 338 Map<I18nizableText, List<SearchUICriterion>> criteriaByGroup = new LinkedHashMap<>(); 339 340 for (SearchUICriterion sc : criteria.values()) 341 { 342 I18nizableText group = sc.getGroup(); 343 344 if (!criteriaByGroup.containsKey(group)) 345 { 346 criteriaByGroup.put(group, new ArrayList<>()); 347 } 348 349 criteriaByGroup.get(group).add(sc); 350 } 351 352 return criteriaByGroup; 353 } 354 355 private void _putCriterionParameter(Map<String, Object> jsonObject, SearchUICriterion criterion) throws ProcessingException 356 { 357 jsonObject.put("id", criterion.getId()); 358 jsonObject.put("label", criterion.getLabel()); 359 jsonObject.put("description", criterion.getDescription()); 360 jsonObject.put("type", criterion.getType().getId()); 361 362 Object defaultValue = criterion.getDefaultValue(); 363 if (defaultValue != null) 364 { 365 jsonObject.put("default-value", defaultValue); 366 } 367 368 _putEnumerator(jsonObject, criterion.getEnumerator()); 369 _putValidator(jsonObject, criterion.getValidator()); 370 371 String widget = criterion.getWidget(); 372 if (widget != null) 373 { 374 jsonObject.put("widget", widget); 375 } 376 377 Map<String, I18nizableText> widgetParameters = criterion.getWidgetParameters(); 378 if (widgetParameters != null && !widgetParameters.isEmpty()) 379 { 380 jsonObject.put("widget-params", criterion.getWidgetParameters()); 381 } 382 } 383 384 private void _putEnumerator(Map<String, Object> jsonObject, Enumerator enumerator) throws ProcessingException 385 { 386 if (enumerator != null) 387 { 388 try 389 { 390 List<Map<String, Object>> options = new ArrayList<>(); 391 392 for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet()) 393 { 394 String valueAsString = ParameterHelper.valueToString(entry.getKey()); 395 I18nizableText entryLabel = entry.getValue(); 396 397 Map<String, Object> option = new HashMap<>(); 398 option.put("label", entryLabel != null ? entryLabel : valueAsString); 399 option.put("value", valueAsString); 400 options.add(option); 401 } 402 403 jsonObject.put("enumeration", options); 404 jsonObject.put("enumerationConfig", enumerator.getConfiguration()); 405 } 406 catch (Exception e) 407 { 408 throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e); 409 } 410 } 411 } 412 413 private void _putValidator(Map<String, Object> jsonObject, Validator validator) 414 { 415 if (validator != null) 416 { 417 jsonObject.put("validation", validator.getConfiguration()); 418 } 419 } 420}