001/*
002 *  Copyright 2016 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.solr;
017
018import java.text.Normalizer;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.acting.ServiceableAction;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Redirector;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.environment.SourceResolver;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.contenttype.ContentType;
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.contenttype.MetadataDefinition;
039import org.ametys.cms.contenttype.indexing.IndexingField;
040import org.ametys.cms.contenttype.indexing.MetadataIndexingField;
041import org.ametys.cms.search.model.SearchCriterion;
042import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
043import org.ametys.cms.search.ui.model.SearchUICriterion;
044import org.ametys.cms.search.ui.model.SearchUIModel;
045import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
046import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
047import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
048import org.ametys.core.cocoon.JSonReader;
049import org.ametys.core.util.ServerCommHelper;
050
051/**
052 * Get search facets available to a given list of content types.
053 */
054public class GetSearchFacetsAction extends ServiceableAction
055{
056    
057    /** The content type extension point */
058    protected ContentTypeExtensionPoint _cTypeEP;
059    
060    /** The content type helper */
061    protected ContentTypesHelper _cTypeHelper;
062    
063    /** The search model helper. */
064    protected SearchUIModelExtensionPoint _searchModelManager;
065    
066    /** The system property extension point. */
067    protected SystemPropertyExtensionPoint _sysPropEP;
068    
069    /** The ServerComm helper. */
070    protected ServerCommHelper _serverCommHelper;
071    
072    @Override
073    public void service(ServiceManager serviceManager) throws ServiceException
074    {
075        super.service(serviceManager);
076        _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
077        _cTypeHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
078        _searchModelManager = (SearchUIModelExtensionPoint) serviceManager.lookup(SearchUIModelExtensionPoint.ROLE);
079        _sysPropEP = (SystemPropertyExtensionPoint) serviceManager.lookup(SystemPropertyExtensionPoint.ROLE);
080        _serverCommHelper = (ServerCommHelper) serviceManager.lookup(ServerCommHelper.ROLE);
081    }
082    
083    @Override
084    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
085    {
086        Request request = ObjectModelHelper.getRequest(objectModel);
087        
088        Map<String, Object> results = new HashMap<>();
089        request.setAttribute(JSonReader.OBJECT_TO_READ, results);
090        
091        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
092        
093        String modelId = (String) jsParameters.get("modelId");
094        
095        @SuppressWarnings("unchecked")
096        List<String> cTypes = (List<String>) jsParameters.get("cTypes");
097        
098        List<Map<String, String>> properties = new ArrayList<>();
099        results.put("properties", properties);
100        
101        String startFilter = (String) jsParameters.get("startsWith");
102        if (StringUtils.isNotEmpty(startFilter))
103        {
104            startFilter = normalize(startFilter);
105        }
106        
107        SearchUIModel model = _searchModelManager.getExtension(modelId);
108        
109        List<String> propertyIds = new ArrayList<>();
110        for (String systemProp : _sysPropEP.getSearchProperties())
111        {
112            addSystemProperty(propertyIds, systemProp, model);
113        }
114        
115        // Get the common content type.
116        ContentType cType = null;
117        if (cTypes != null && !cTypes.isEmpty())
118        {
119            String commonCTypeId = _cTypeHelper.getCommonAncestor(cTypes);
120            if (commonCTypeId != null)
121            {
122                cType = _cTypeEP.getExtension(commonCTypeId);
123            }
124        }
125
126        if (cType != null)
127        {
128            for (String meta : cType.getMetadataNames())
129            {
130                addMetadata(propertyIds, meta, cType);
131            }
132            for (IndexingField field : cType.getIndexingModel().getFields())
133            {
134                addIndexingField(propertyIds, field);
135            }
136        }
137        
138        for (String propertyId : propertyIds)
139        {
140            if (StringUtils.isEmpty(startFilter) || normalize(propertyId).startsWith(startFilter))
141            {
142                properties.add(Collections.singletonMap("id", propertyId));
143            }
144        }
145        
146        return EMPTY_MAP;
147    }
148    
149    private void addMetadata(List<String> properties, String metaPath, ContentType cType)
150    {
151        if (cType != null)
152        {
153            MetadataDefinition metaDef = cType.getMetadataDefinitionByPath(metaPath);
154            if (metaDef != null && SearchCriterion.isFacetable(metaDef.getType(), metaDef.getEnumerator() != null))
155            {
156                properties.add(metaPath);
157            }
158        }
159        else
160        {
161            properties.add(metaPath);
162        }
163    }
164    
165    private void addIndexingField(List<String> properties, IndexingField field)
166    {
167        if (field instanceof MetadataIndexingField)
168        {
169            MetadataIndexingField metaField = (MetadataIndexingField) field;
170            MetadataDefinition metaDef = metaField.getMetadataDefinition();
171            
172            if (metaDef != null && SearchCriterion.isFacetable(metaDef.getType(), metaDef.getEnumerator() != null))
173            {
174                properties.add(metaField.getName());
175            }
176        }
177    }
178    
179    private void addSystemProperty(List<String> properties, String propertyId, SearchUIModel model)
180    {
181        if (model != null)
182        {
183            SearchCriterion criterion = _getCriteria(model, propertyId);
184            
185            if (criterion != null && criterion.isFacetable())
186            {
187                properties.add(propertyId);
188            }
189        }
190    }
191    
192    private SearchCriterion _getCriteria(SearchUIModel searchModel, String criterionId)
193    {
194        Map<String, SearchUICriterion> criteria = searchModel.getFacetedCriteria(Collections.emptyMap());
195        
196        for (SearchCriterion criterion : criteria.values())
197        {
198            if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId))
199            {
200                return criterion;
201            }
202            else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId))
203            {
204                return criterion;
205            }
206            else if (criterion.getId().equals(criterionId))
207            {
208                return criterion;
209            }
210        }
211        
212        return null;
213    }
214    
215    private static String normalize(String str)
216    {
217        return Normalizer.normalize(str.toLowerCase(), Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").trim();
218    }
219    
220}