001/* 002 * Copyright 2014 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.ui.model.impl; 017 018import java.util.ArrayList; 019import java.util.LinkedHashMap; 020import java.util.LinkedList; 021import java.util.List; 022import java.util.Map; 023import java.util.Optional; 024import java.util.Set; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.activity.Disposable; 028import org.apache.avalon.framework.component.ComponentException; 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.configuration.DefaultConfiguration; 032import org.apache.avalon.framework.configuration.MutableConfiguration; 033import org.apache.avalon.framework.context.Context; 034import org.apache.avalon.framework.context.ContextException; 035import org.apache.avalon.framework.context.Contextualizable; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.commons.lang3.tuple.ImmutablePair; 039import org.apache.commons.lang3.tuple.Pair; 040import org.slf4j.Logger; 041 042import org.ametys.cms.contenttype.ContentTypesHelper; 043import org.ametys.cms.contenttype.MetadataType; 044import org.ametys.cms.search.query.AndQuery; 045import org.ametys.cms.search.query.OrQuery; 046import org.ametys.cms.search.query.Query; 047import org.ametys.cms.search.query.Query.Operator; 048import org.ametys.cms.search.ui.model.SearchUICriterion; 049import org.ametys.runtime.plugin.component.LogEnabled; 050import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 051 052/** 053 * Aggregate multiple indexing field as a unique search criteria. 054 * The resulting query is an OR query on all the terms. 055 */ 056public class IndexingFieldAggregatorSearchUICriterion extends AbstractSearchUICriterion implements Contextualizable, Disposable, LogEnabled 057{ 058 /** Prefix for id of indexing field aggregator search criteria */ 059 public static final String SEARCH_CRITERIA_METADATA_AGGREGATOR_PREFIX = "metadata-aggregator-"; 060 061 /** ComponentManager for aggregated {@link SearchUICriterion}s. */ 062 protected ThreadSafeComponentManager<SearchUICriterion> _fieldCriteriaManager; 063 064 /** The map of aggregated criteria */ 065 protected Map<String, SearchUICriterion> _fieldCriteriaMap; 066 067 /** The logger */ 068 protected Logger _logger; 069 070 /** The service manager */ 071 protected ServiceManager _manager; 072 073 /** The helper for convenient methods on content types */ 074 protected ContentTypesHelper _contentTypesHelper; 075 076 /** The context. */ 077 protected Context _context; 078 079 /** The type of the aggregated fields */ 080 protected MetadataType _type; 081 082 @Override 083 public void setLogger(Logger logger) 084 { 085 _logger = logger; 086 } 087 088 @Override 089 public void contextualize(Context context) throws ContextException 090 { 091 _context = context; 092 } 093 094 @Override 095 public void service(ServiceManager manager) throws ServiceException 096 { 097 super.service(manager); 098 _manager = manager; 099 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 100 } 101 102 @Override 103 public void configure(Configuration configuration) throws ConfigurationException 104 { 105 try 106 { 107 super.configure(configuration); 108 109 _fieldCriteriaManager = new ThreadSafeComponentManager<>(); 110 _fieldCriteriaManager.setLogger(_logger); 111 _fieldCriteriaManager.contextualize(_context); 112 _fieldCriteriaManager.service(_manager); 113 114 MutableConfiguration baseConf = new DefaultConfiguration(configuration); 115 baseConf.removeChild(baseConf.getChild("field")); 116 117 Configuration fieldsConf = configuration.getChild("fields"); 118 Configuration[] fieldConfs = fieldsConf.getChildren("field"); 119 120 List<Pair<String, Configuration>> fieldSearchUICriterionToLookup = _fieldSearchUICriterionToLookup(fieldConfs, baseConf); 121 122 _initializeFieldCriteria(fieldSearchUICriterionToLookup, fieldsConf); 123 124 _configureWidgetAndWidgetParameters(configuration); 125 } 126 catch (ConfigurationException e) 127 { 128 throw e; 129 } 130 catch (Exception e) 131 { 132 throw new ConfigurationException("Unable to create local component manager.", configuration, e); 133 } 134 } 135 136 private List<Pair<String, Configuration>> _fieldSearchUICriterionToLookup(Configuration[] fieldConfs, MutableConfiguration baseConf) throws ConfigurationException 137 { 138 List<Pair<String, Configuration>> fieldSearchUICriterionToLookup = new ArrayList<>(); 139 140 for (Configuration fieldConf : fieldConfs) 141 { 142 String path = fieldConf.getAttribute("path"); 143 144 MutableConfiguration indexingFieldConf = new DefaultConfiguration(baseConf); 145 DefaultConfiguration newFieldConf = new DefaultConfiguration(fieldConf); 146 indexingFieldConf.addChild(newFieldConf); 147 newFieldConf.setAttribute("path", path); 148 149 String role = path; 150 _fieldCriteriaManager.addComponent("cms", null, role, IndexingFieldSearchUICriterion.class, indexingFieldConf); 151 152 fieldSearchUICriterionToLookup.add(ImmutablePair.of(role, fieldConf)); 153 } 154 155 return fieldSearchUICriterionToLookup; 156 } 157 158 private void _initializeFieldCriteria(List<Pair<String, Configuration>> fieldSearchUICriterionToLookup, Configuration fieldsConf) throws Exception 159 { 160 String typeAttr = fieldsConf.getAttribute("type", null); 161 162 _fieldCriteriaManager.initialize(); 163 164 _fieldCriteriaMap = new LinkedHashMap<>(); 165 _type = Optional.ofNullable(typeAttr) 166 .map(String::toUpperCase) 167 .map(MetadataType::valueOf) 168 .orElse(null); 169 for (Pair<String, Configuration> pair : fieldSearchUICriterionToLookup) 170 { 171 String role = pair.getLeft(); 172 SearchUICriterion criterion; 173 try 174 { 175 criterion = _fieldCriteriaManager.lookup(role); 176 } 177 catch (ComponentException e) 178 { 179 throw new ConfigurationException("Impossible to lookup the search criterion of role: " + role, e); 180 } 181 182 MetadataType criterionType = criterion.getType(); 183 if (_type == null) 184 { 185 _type = criterionType; 186 } 187 else if (_type != criterionType) 188 { 189 String location = pair.getRight().getLocation(); 190 throw new ConfigurationException("Search criterion of role '" + role + "' is of type '" + criterionType + "' but should be of type '" + _type + "' at " + location + "."); 191 } 192 _fieldCriteriaMap.put(criterion.getId(), criterion); 193 } 194 } 195 196 private void _configureWidgetAndWidgetParameters(Configuration configuration) throws ConfigurationException 197 { 198 setWidget(configureWidget(configuration, null, _type)); 199 200 String commonContentTypeId = null; 201 if (_type == MetadataType.CONTENT || _type == MetadataType.SUB_CONTENT) 202 { 203 Set<String> contentTypeIds = _fieldCriteriaMap.values().stream() 204 .map(SearchUICriterion::getContentTypeId) 205 .collect(Collectors.toSet()); 206 Set<String> commonContentTypeIds = _contentTypesHelper.getCommonAncestors(contentTypeIds); 207 208 if (commonContentTypeIds.size() == 1) 209 { 210 commonContentTypeId = commonContentTypeIds.iterator().next(); 211 } 212 } 213 setWidgetParameters(configureWidgetParameters(configuration, null, _type, commonContentTypeId)); 214 } 215 216 @Override 217 protected void configureId(Configuration configuration) throws ConfigurationException 218 { 219 String customRef = configuration.getAttribute("custom-ref"); 220 setId(SEARCH_CRITERIA_METADATA_AGGREGATOR_PREFIX + customRef); 221 } 222 223 @Override 224 public MetadataType getType() 225 { 226 return _type; 227 } 228 229 @Override 230 public Query getQuery(Object value, Operator customOperator, Map<String, Object> allValues, String language, Map<String, Object> contextualParameters) 231 { 232 List<Query> fieldQueries = new LinkedList<>(); 233 234 for (String role : _fieldCriteriaMap.keySet()) 235 { 236 SearchUICriterion searchCriteria = _fieldCriteriaMap.get(role); 237 Query query = searchCriteria.getQuery(value, customOperator, allValues, language, contextualParameters); 238 239 if (query != null) 240 { 241 fieldQueries.add(query); 242 } 243 } 244 245 if (fieldQueries.isEmpty()) 246 { 247 return null; 248 } 249 else 250 { 251 return (customOperator != null && Operator.NE.equals(customOperator)) ? new AndQuery(fieldQueries) : new OrQuery(fieldQueries); 252 } 253 } 254 255 public void dispose() 256 { 257 _fieldCriteriaManager.dispose(); 258 _fieldCriteriaManager = null; 259 } 260 261 @Override 262 public String getFieldId() 263 { 264 return getId(); 265 } 266 267 @Override 268 public boolean isSortable() 269 { 270 // Not destined to be displayed. 271 return false; 272 } 273 274 @Override 275 public Operator getOperator() 276 { 277 return null; 278 } 279 280}