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.ui.model.impl;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.HashSet;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.activity.Disposable;
029import org.apache.avalon.framework.configuration.Configurable;
030import org.apache.avalon.framework.configuration.Configuration;
031import org.apache.avalon.framework.configuration.ConfigurationException;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.commons.lang3.StringUtils;
039import org.slf4j.Logger;
040
041import org.ametys.cms.content.ContentHelper;
042import org.ametys.cms.contenttype.ContentType;
043import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
044import org.ametys.cms.contenttype.ContentTypesHelper;
045import org.ametys.cms.contenttype.MetadataType;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.search.SearchField;
048import org.ametys.cms.search.model.SystemProperty;
049import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
050import org.ametys.cms.search.model.SystemResultField;
051import org.ametys.cms.search.ui.model.SearchUIColumn;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.model.RepeaterDefinition;
054import org.ametys.runtime.i18n.I18nizableText;
055import org.ametys.runtime.model.ElementDefinition;
056import org.ametys.runtime.model.Enumerator;
057import org.ametys.runtime.model.ModelHelper;
058import org.ametys.runtime.model.ModelItem;
059import org.ametys.runtime.model.type.DataContext;
060import org.ametys.runtime.plugin.component.LogEnabled;
061import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
062
063/**
064 * This class is a search column on a system property (author, lastModified, with-comments, ...)
065 */
066public class SystemSearchUIColumn extends AbstractSearchUIColumn implements SystemResultField, Contextualizable, LogEnabled, Configurable, Serviceable, Disposable
067{
068    /** ComponentManager for {@link Enumerator}s. */
069    protected ThreadSafeComponentManager<Enumerator> _enumeratorManager;
070    
071    /** The system property extension point. */
072    protected SystemPropertyExtensionPoint _systemPropEP;
073    
074    /** The content type extension point. */
075    protected ContentTypeExtensionPoint _cTypeEP;
076    
077    /** The content helper. */
078    protected ContentHelper _contentHelper;
079    
080    /** The content type helper. */
081    protected ContentTypesHelper _contentTypeHelper;
082    
083    /** The Ametys object resolver. */
084    protected AmetysObjectResolver _resolver;
085    
086    /** The system property. */
087    protected SystemProperty _systemProperty;
088    
089    /** The join paths. */
090    protected String _joinPaths;
091    
092    /** The content types */
093    private Set<String> _contentTypes;
094    
095    private ServiceManager _manager;
096    private Logger _logger;
097    private Context _context;
098    
099    @Override
100    public void contextualize(Context context) throws ContextException
101    {
102        _context = context;
103    }
104    
105    @Override
106    public void setLogger(Logger logger)
107    {
108        _logger = logger;
109    }
110    
111    @Override
112    public void service(ServiceManager manager) throws ServiceException
113    {
114        _manager = manager;
115        _systemPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
116        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
117        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
118        _contentTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
119        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
120    }
121    
122    @Override
123    public void dispose()
124    {
125        _enumeratorManager.dispose();
126        _enumeratorManager = null;
127    }
128    
129    @Override
130    public void configure(Configuration configuration) throws ConfigurationException
131    {
132        try
133        {
134            _enumeratorManager = new ThreadSafeComponentManager<>();
135            _enumeratorManager.setLogger(_logger);
136            _enumeratorManager.contextualize(_context);
137            _enumeratorManager.service(_manager);
138            
139            String fullPath = configuration.getChild("systemProperty").getAttribute("name");
140            int pos = fullPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR);
141            
142            String systemPropertyId = pos > -1 ? fullPath.substring(pos + ModelItem.ITEM_PATH_SEPARATOR.length()) : fullPath;
143            
144            if (!_systemPropEP.isDisplayable(systemPropertyId))
145            {
146                throw new ConfigurationException("The property '" + systemPropertyId + "' doesn't exist or is not displayable.");
147            }
148            
149            _joinPaths = pos > -1 ? fullPath.substring(0, pos) : "";
150            _systemProperty = _systemPropEP.getExtension(systemPropertyId);
151            
152            _contentTypes = new HashSet<>();
153            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("type"))
154            {
155                _contentTypes.add(cTypeConf.getAttribute("id"));
156            }
157            
158            setId(fullPath);
159            
160            setLabel(_systemProperty.getLabel());
161            setDescription(_systemProperty.getDescription());
162            setType(MetadataType.fromModelItemType(_systemProperty.getType()));
163            
164            Set<String> baseContentTypeIds = new HashSet<>();
165            for (Configuration cTypeConf : configuration.getChild("contentTypes").getChildren("baseType"))
166            {
167                baseContentTypeIds.add(cTypeConf.getAttribute("id"));
168            }
169            boolean multiple = testMultiple(baseContentTypeIds);
170            setMultiple(multiple);
171            
172            Enumerator enumerator = _systemProperty.getCriterionEnumerator(configuration, _enumeratorManager);
173            if (enumerator instanceof org.ametys.runtime.parameter.Enumerator columnEnumerator)
174            {
175                setEnumerator(columnEnumerator);
176            }
177            
178            setEditable(false);
179            setWidth(_systemProperty.getColumnWidth() != null ? _systemProperty.getColumnWidth() : 150);
180            setHidden(configuration.getChild("hidden").getValueAsBoolean(false));
181            setSortable(configuration.getChild("sortable").getValueAsBoolean(_systemProperty.isSortable()));
182            if (isSortable())
183            {
184                setDefaultSorter(configuration.getChild("default-sorter").getValue(null));
185            }
186            // Potentially replace the standard label and description by the custom ones.
187            I18nizableText userLabel = _configureI18nizableText(configuration.getChild("label", false), null);
188            if (userLabel != null)
189            {
190                setLabel(userLabel);
191            }
192            I18nizableText userDescription = _configureI18nizableText(configuration.getChild("description", false), null);
193            if (userDescription != null)
194            {
195                setDescription(userDescription);
196            }
197            
198            if (_systemProperty.getRenderer() != null)
199            {
200                setRenderer(_systemProperty.getRenderer());
201            }
202            if (_systemProperty.getConverter() != null)
203            {
204                setConverter(_systemProperty.getConverter());
205            }
206        }
207        catch (Exception e)
208        {
209            throw new ConfigurationException("Error configuring the system search column.", configuration, e);
210        }
211    }
212
213    private boolean testMultiple(Set<String> contentTypeIds)
214    {
215        // if system property is multiple, then the column is multiple
216        if (_systemProperty.isMultiple())
217        {
218            return true;
219        }
220        
221        // Otherwise, the column is multiple if at least one of the join path definitions is multiple
222        if (!contentTypeIds.isEmpty() && StringUtils.isNotEmpty(_joinPaths))
223        {
224            Collection<ContentType> contentTypes = contentTypeIds.stream()
225                    .map(_cTypeEP::getExtension)
226                    .collect(Collectors.toList());
227            List<ModelItem> modelItems = ModelHelper.getAllModelItemsInPath(_joinPaths, contentTypes);
228            
229            for (ModelItem definition : modelItems)
230            {
231                if (definition instanceof ElementDefinition && ((ElementDefinition) definition).isMultiple()
232                    || definition instanceof RepeaterDefinition)
233                {
234                    return true;
235                }
236            }
237        }
238        
239        return false;
240    }
241    
242    @Override
243    public String getFieldPath()
244    {
245        return (_joinPaths.isEmpty() ? "" : _joinPaths + ModelItem.ITEM_PATH_SEPARATOR) + getSystemPropertyId();
246    }
247    
248    /**
249     * Get the system property name
250     * @return The property name
251     */
252    public String getSystemPropertyId()
253    {
254        return _systemProperty.getName();
255    }
256    
257    @Override
258    public Object getValue(Content content, Locale defaultLocale)
259    {
260        return _getValue(content, false);
261    }
262    
263    @Override
264    public Object getFullValue(Content content, Locale defaultLocale)
265    {
266        return _getValue(content, true);
267    }
268    
269    @SuppressWarnings("unchecked")
270    private Object _getValue(Content content, boolean full)
271    {
272        if (isMultiple())
273        {
274            Set<Object> values = new LinkedHashSet<>();
275            
276            for (Content targetContent : _contentHelper.getTargetContents(content, _joinPaths))
277            {
278                Object value = _systemProperty.valueToJSON(targetContent, DataContext.newInstance());
279                if (value != null)
280                {
281                    if (value instanceof Collection<?>)
282                    {
283                        // Flatten values
284                        values.addAll((Collection<?>) value);
285                    }
286                    else if (value instanceof Object[])
287                    {
288                        // Flatten values
289                        values.addAll(Arrays.asList((Object[]) value));
290                    }
291                    else
292                    {
293                        values.add(value);
294                    }
295                }
296            }
297            
298            return values;
299        }
300        else
301        {
302            Content targetContent = _contentHelper.getTargetContent(content, _joinPaths);
303            if (targetContent != null)
304            {
305                return _systemProperty.valueToJSON(targetContent, DataContext.newInstance());
306            }
307        }
308        
309        return null;
310    }
311    
312    @Override
313    public SearchField getSearchField()
314    {
315        return _systemProperty.getSearchField();
316    }
317    
318    public Map<String, SearchUIColumn> getSubColumns()
319    {
320        return null;
321    }
322}