001/*
002 *  Copyright 2017 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.systemprop;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.configuration.DefaultConfiguration;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.contenttype.ContentType;
038import org.ametys.cms.contenttype.ContentTypeEnumerator;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.contenttype.ContentTypesHelper;
041import org.ametys.cms.contenttype.MetadataType;
042import org.ametys.cms.repository.Content;
043import org.ametys.cms.search.SearchField;
044import org.ametys.cms.search.model.SystemProperty;
045import org.ametys.cms.search.query.ContentTypeOrMixinTypeQuery;
046import org.ametys.cms.search.query.ContentTypeQuery;
047import org.ametys.cms.search.query.MixinTypeQuery;
048import org.ametys.cms.search.query.Query;
049import org.ametys.cms.search.query.Query.Operator;
050import org.ametys.cms.search.solr.field.ContentTypeSearchField;
051import org.ametys.cms.search.solr.field.MixinTypeSearchField;
052import org.ametys.runtime.i18n.I18nizableText;
053
054/**
055 * {@link SystemProperty} which represents the content types (and optionally mixins) of a content.
056 */
057public class ContentTypeSystemProperty extends AbstractSystemProperty
058{
059    
060    /** The content type extension point. */
061    protected ContentTypeExtensionPoint _cTypeEP;
062    
063    /** The content types helper. */
064    protected ContentTypesHelper _cTypeHelper;
065    
066    /** True to include content types. */
067    protected boolean _includeCTypes;
068    
069    /** True to include mixins. */
070    protected boolean _includeMixins;
071    
072    /** True to recursively include supertypes. */
073    protected boolean _includeSupertypes;
074    
075    @Override
076    public void service(ServiceManager manager) throws ServiceException
077    {
078        super.service(manager);
079        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
080        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
081    }
082    
083    @Override
084    public void configure(Configuration configuration) throws ConfigurationException
085    {
086        super.configure(configuration);
087        
088        _includeCTypes = configuration.getChild("contentTypes").getValueAsBoolean(true);
089        _includeMixins = configuration.getChild("mixins").getValueAsBoolean(false);
090        _includeSupertypes = configuration.getChild("includeSupertypes").getValueAsBoolean(false);
091    }
092    
093    @Override
094    public MetadataType getType()
095    {
096        return MetadataType.STRING;
097    }
098    
099    @Override
100    public boolean isMultiple()
101    {
102        return true;
103    }
104    
105    @Override
106    public boolean isSortable()
107    {
108        return true;
109    }
110    
111    @Override
112    public Query getQuery(Object value, Operator operator, String language, Map<String, Object> contextualParameters)
113    {
114        String[] contentTypes = value != null ? parseStringArray(value) : new String[0];
115        
116        if (_includeCTypes && _includeMixins)
117        {
118            return new ContentTypeOrMixinTypeQuery(operator, contentTypes);
119        }
120        else if (_includeCTypes)
121        {
122            return new ContentTypeQuery(operator, contentTypes);
123        }
124        else if (_includeMixins)
125        {
126            return new MixinTypeQuery(operator, contentTypes);
127        }
128        
129        return null;
130    }
131    
132    @Override
133    public String getRenderer()
134    {
135        return "Ametys.plugins.cms.search.SearchGridHelper.renderMultipleString";
136    }
137    
138//    @Override
139//    public String getField()
140//    {
141//        // TODO Specific field?
142//        return getId();
143//    }
144    
145//    @Override
146//    public String getSortField()
147//    {
148//        return getField() + SolrFieldHelper.getSystemPropSortFieldSuffix();
149//    }
150    
151    @Override
152    public SearchField getSearchField()
153    {
154        if (_includeCTypes && _includeMixins)
155        {
156            // Not searchable, not facetable.
157//            return new ContentTypeOrMixinSearchField(_includeSupertypes);
158            return null;
159        }
160        else if (_includeCTypes)
161        {
162            return new ContentTypeSearchField(_includeSupertypes);
163        }
164        else if (_includeMixins)
165        {
166            return new MixinTypeSearchField(_includeSupertypes);
167        }
168        
169        return null;
170    }
171    
172    @Override
173    public Object getValue(Content content)
174    {
175        Set<String> types = new LinkedHashSet<>();
176        
177        if (_includeCTypes)
178        {
179            _addAll(types, content.getTypes());
180        }
181        if (_includeMixins)
182        {
183            _addAll(types, content.getMixinTypes());
184        }
185        
186        return types.toArray(new String[types.size()]);
187    }
188    
189    public Object getJsonValue(Content content, boolean full)
190    {
191        String[] cTypeIds = (String[]) getValue(content);
192        
193        List<I18nizableText> cTypes = new ArrayList<>();
194        for (String cTypeId : cTypeIds)
195        {
196            ContentType cType = _cTypeEP.getExtension(cTypeId);
197            if (cType != null)
198            {
199                cTypes.add(cType.getLabel());
200            }
201            else if (getLogger().isWarnEnabled())
202            {
203                getLogger().warn(String.format("Trying to get the label for an unknown content type : '%s'.", cTypeId));
204            }
205        }
206        
207        return cTypes;
208    }
209    
210    @Override
211    public Object getSortValue(Content content)
212    {
213        String[] cTypeIds = (String[]) getValue(content);
214        if (cTypeIds.length > 0)
215        {
216            ContentType cType = _cTypeEP.getExtension(cTypeIds[0]);
217            if (cType != null)
218            {
219                return _i18nUtils.translate(cType.getLabel(), content.getLanguage());
220            }
221        }
222        
223        return null;
224    }
225    
226    private void _addAll(Set<String> allTypes, String[] types)
227    {
228        for (String cType : types)
229        {
230            allTypes.add(cType);
231            if (_includeSupertypes && _cTypeEP.hasExtension(cType))
232            {
233                allTypes.addAll(_cTypeHelper.getAncestors(cType));
234            }
235        }
236    }
237    
238    @Override
239    public void saxValue(ContentHandler handler, Content content) throws SAXException
240    {
241        String[] cTypeIds = (String[]) getValue(content);
242        
243        XMLUtils.startElement(handler, getId());
244        
245        for (String cTypeId : cTypeIds)
246        {
247            AttributesImpl attr = new AttributesImpl();
248            attr.addCDATAAttribute("id", cTypeId);
249            
250            XMLUtils.startElement(handler, "contentType", attr);
251            ContentType cType = _cTypeEP.getExtension(cTypeId);
252            cType.getLabel().toSAX(handler);
253            XMLUtils.endElement(handler, "contentType");
254        }
255        
256        XMLUtils.endElement(handler, getId());
257    }
258    
259    /**
260     * Get the definition of enumerator.
261     * @param configuration The enumerator configuration.
262     * @return The enumerator definition
263     */
264    @Override
265    public EnumeratorDefinition getEnumeratorDefinition(Configuration configuration)
266    {
267        DefaultConfiguration conf = new DefaultConfiguration("criteria");
268        
269        DefaultConfiguration enumConf = new DefaultConfiguration("enumeration");
270        
271        DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
272        customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
273        
274        try
275        {
276            boolean containsRefTable = false;
277            Set<String> strictContentTypes = new HashSet<>();
278            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
279            {
280                String cTypeId = cTypeConf.getAttribute("id");
281                ContentType cType = _cTypeEP.getExtension(cTypeId);
282                if (cType == null)
283                {   
284                    getLogger().error("A search model references a non-existing content type with id '{}', it will be ignored for system property {} ", cTypeId, _id);
285                }
286                else
287                {
288                    strictContentTypes.add(cTypeId);
289                    containsRefTable = cType.isReferenceTable() || containsRefTable;
290                }
291            }
292            if (!strictContentTypes.isEmpty() && _includeCTypes)
293            {
294                DefaultConfiguration cTypeConf = new DefaultConfiguration("strictContentTypes");
295                cTypeConf.setValue(StringUtils.join(strictContentTypes, ","));
296                customEnumerator.addChild(cTypeConf);
297                
298                if (containsRefTable)
299                {
300                    // Search model focuses on at least one reference table, do not exclude reference table for content type enumerator
301                    DefaultConfiguration excludeRefTableConf = new DefaultConfiguration("excludeReferenceTable");
302                    excludeRefTableConf.setValue(false);
303                    customEnumerator.addChild(excludeRefTableConf);
304                }
305            }
306        }
307        catch (ConfigurationException e)
308        {
309            getLogger().error("Failed to configure strict content types for system property {}", _id, e);
310        }
311        
312        if (!_includeMixins)
313        {
314            DefaultConfiguration excludeConf = new DefaultConfiguration("excludeMixin");
315            excludeConf.setValue(true);
316            customEnumerator.addChild(excludeConf);
317        }
318        else if (!_includeCTypes) // Mixins and not CTypes
319        {
320            DefaultConfiguration includeConf = new DefaultConfiguration("includeMixinOnly");
321            includeConf.setValue(true);
322            customEnumerator.addChild(includeConf);
323        }
324        
325        DefaultConfiguration allOptionConf = new DefaultConfiguration("all-option");
326        allOptionConf.setValue("disabled");
327        customEnumerator.addChild(allOptionConf);
328        
329        enumConf.addChild(customEnumerator);
330        conf.addChild(enumConf);
331        
332        return new EnumeratorDefinition(ContentTypeEnumerator.class, conf);
333    }
334
335    /**
336     * Get the default widget to use when rendering this property as a criterion.
337     * @return The default widget to use.
338     */
339    @Override
340    public String getWidget()
341    {
342        return "edition.select-content-types";
343    }
344    
345    @Override
346    public Map<String, I18nizableText> getWidgetParameters(Configuration configuration)
347    {
348        Map<String, I18nizableText> parameters = new HashMap<>();
349        
350        try
351        {
352            Set<String> strictContentTypes = new HashSet<>();
353            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
354            {
355                strictContentTypes.add(cTypeConf.getAttribute("id"));
356            }
357            
358            if (!strictContentTypes.isEmpty() && _includeCTypes)
359            {
360                parameters.put("strictContentTypes", new I18nizableText(StringUtils.join(strictContentTypes, ",")));
361            }
362        }
363        catch (ConfigurationException e)
364        {
365            getLogger().error("Failed to configure strict content types for system property {}", _id, e);
366        }
367        
368        parameters.put("excludeMixin", new I18nizableText(_includeMixins ? "false" :  "true"));
369        parameters.put("includeSupertype", new I18nizableText(_includeSupertypes ? "true" : "false"));
370        parameters.put("includeContentTypes", new I18nizableText(_includeCTypes ? "true" : "false"));
371        parameters.put("excludeReferenceTable", new I18nizableText("true"));
372        parameters.put("excludePrivate", new I18nizableText("true"));
373        parameters.put("emptyText", new I18nizableText("plugin.cms", "WIDGET_COMBOBOX_ALL_OPTIONS"));
374        
375        return parameters;
376    }
377}