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