001/*
002 *  Copyright 2015 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.Collection;
020import java.util.Date;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024
025import org.apache.avalon.framework.configuration.Configurable;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.solr.common.SolrInputDocument;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.content.indexing.solr.SolrFieldHelper;
038import org.ametys.cms.content.indexing.solr.SolrIndexer;
039import org.ametys.cms.contenttype.MetadataType;
040import org.ametys.cms.repository.Content;
041import org.ametys.cms.search.SearchField;
042import org.ametys.cms.search.model.SystemProperty;
043import org.ametys.cms.search.solr.schema.CopyFieldDefinition;
044import org.ametys.cms.search.solr.schema.FieldDefinition;
045import org.ametys.cms.search.solr.schema.SchemaDefinition;
046import org.ametys.cms.search.solr.schema.SchemaHelper;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.user.UserManager;
050import org.ametys.core.util.DateUtils;
051import org.ametys.core.util.I18nUtils;
052import org.ametys.core.util.date.AdaptableDate;
053import org.ametys.core.util.date.AdaptableDateParser;
054import org.ametys.plugins.core.user.UserHelper;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.runtime.i18n.I18nizableText;
058import org.ametys.runtime.parameter.ParameterHelper;
059import org.ametys.runtime.plugin.component.AbstractLogEnabled;
060import org.ametys.runtime.plugin.component.PluginAware;
061
062/**
063 * Abstract class providing standard behavior and helper methods for a
064 * {@link SystemProperty}.
065 */
066public abstract class AbstractSystemProperty extends AbstractLogEnabled implements SystemProperty, Serviceable, Configurable, PluginAware
067{
068    /** The i18n utils. */
069    protected I18nUtils _i18nUtils;
070
071    /** The property ID. */
072    protected String _id;
073
074    /** The property label. */
075    protected I18nizableText _label;
076
077    /** The property description. */
078    protected I18nizableText _description;
079
080    /** The property plugin name. */
081    protected String _pluginName;
082
083    /** The user helper */
084    protected UserHelper _userHelper;
085    /** The ametys object resolver */
086    protected AmetysObjectResolver _resolver;
087    /** The user manager */
088    protected UserManager _userManager;
089
090    @Override
091    public void setPluginInfo(String pluginName, String featureName, String id)
092    {
093        _pluginName = pluginName;
094        _id = id;
095    }
096
097    public void service(ServiceManager manager) throws ServiceException
098    {
099        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
100        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
101        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
102        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
103    }
104
105    @Override
106    public void configure(Configuration configuration) throws ConfigurationException
107    {
108        _label = _parseI18nizableText(configuration, _pluginName, "label");
109        _description = _parseI18nizableText(configuration, _pluginName, "description");
110    }
111
112    @Override
113    public String getId()
114    {
115        return _id;
116    }
117
118    @Override
119    public I18nizableText getLabel()
120    {
121        return _label;
122    }
123
124    @Override
125    public I18nizableText getDescription()
126    {
127        return _description;
128    }
129
130    @SuppressWarnings("unchecked")
131    @Override
132    public void index(Content content, SolrInputDocument document)
133    {
134        Object value = getValue(content);
135        
136        SearchField searchField = getSearchField();
137        
138        if (value == null || searchField == null || searchField.getName() == null)
139        {
140            // Nothing to index
141            return;
142        }
143
144        MetadataType type = getType();
145        switch (type)
146        {
147            case STRING:
148            case CONTENT:
149                if (isMultiple())
150                {
151                    _indexStringValues(searchField, (String[]) value, document);
152                }
153                else
154                {
155                    _indexStringValue(searchField, (String) value, document);
156                }
157                break;
158            case DOUBLE:
159                if (isMultiple())
160                {
161                    _indexDoubleValues(searchField, (Double[]) value, document);
162                }
163                else
164                {
165                    _indexDoubleValue(searchField, (Double) value, document);
166                }
167                break;
168            case LONG:
169                if (isMultiple())
170                {
171                    _indexLongValues(searchField, (Long[]) value, document);
172                }
173                else
174                {
175                    _indexLongValue(searchField, (Long) value, document);
176                }
177                break;
178            case GEOCODE:
179                _indexGeocodeValue(searchField, (Map<String, Double>) value, document);
180                break;
181            case BOOLEAN:
182                if (isMultiple())
183                {
184                    _indexBooleanValues(searchField, (Boolean[]) value, document);
185                }
186                else
187                {
188                    _indexBooleanValue(searchField, (Boolean) value, document);
189                }
190                break;
191            case DATE:
192            case DATETIME:
193                if (isMultiple())
194                {
195                    _indexDateValues(searchField, (Date[]) value, document);
196                }
197                else
198                {
199                    _indexDateValue(searchField, (Date) value, document);
200                }
201                break;
202            case USER:
203                if (isMultiple())
204                {
205                    _indexUserValues(searchField, (UserIdentity[]) value, document);
206                }
207                else
208                {
209                    _indexUserValue(searchField, (UserIdentity) value, document);
210                }
211                break;
212            case FILE:
213            case BINARY:
214            case COMPOSITE:
215            case RICH_TEXT:
216            case REFERENCE:
217            case SUB_CONTENT:
218            case MULTILINGUAL_STRING:
219                getLogger().warn("Type '" + type + "' is not supported for indexation on a system indexing field");
220                break;
221            default:
222                break;
223        }
224        
225        // Index sort field
226        Object sortValue = getSortValue(content);
227        if (isSortable() && sortValue != null && searchField.getSortField() != null && !searchField.getName().equals(searchField.getSortField()))
228        {
229            document.setField(searchField.getSortField(), sortValue);
230        }
231    }
232
233    @Override
234    public Object getSortValue(Content content)
235    {
236        // Default to getValue(), override to provide a specific sort value.
237        Object value = getValue(content);
238        
239        if (value == null)
240        {
241            return null;
242        }
243
244        if (isMultiple() && value instanceof Object[] && ((Object[]) value).length > 0)
245        {
246            return ((Object[]) value)[0]; // return the first value
247        }
248        else
249        {
250            return value;
251        }
252    }
253    
254    @SuppressWarnings("unchecked")
255    public void saxValue(ContentHandler handler, Content content) throws SAXException
256    {
257        Object value = getValue(content);
258        
259        if (value == null)
260        {
261            return;
262        }
263        
264        MetadataType type = getType();
265        switch (type)
266        {
267            case USER:
268                if (value instanceof UserIdentity[])
269                {
270                    for (UserIdentity identity : (UserIdentity[]) value)
271                    {
272                        _saxUserIdentityValue(handler, identity);
273                    }
274                }
275                else if (value instanceof UserIdentity)
276                {
277                    _saxUserIdentityValue(handler, (UserIdentity) value);
278                }
279                break;
280            case CONTENT:
281                Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : null;
282                if (isMultiple())
283                {
284                    for (String id : (String[]) value)
285                    {
286                        _saxContentValue(handler, id, locale);
287                    }
288                }
289                else
290                {
291                    _saxContentValue(handler, (String) value, locale);
292                }
293                break;
294            case GEOCODE:
295                _saxGeocodeValue(handler, (Map<String, Double>) value);
296                break;
297            case BOOLEAN:
298            case DOUBLE:
299            case LONG:
300            case DATE:
301            case DATETIME:
302            case STRING:
303                if (isMultiple())
304                {
305                    for (Object v : (Object[]) value)
306                    {
307                        _saxSingleValue(handler, v);
308                    }
309                }
310                else
311                {
312                    _saxSingleValue(handler, value);
313                }
314                break;
315            case BINARY:
316            case COMPOSITE:
317            case REFERENCE:
318            case RICH_TEXT:
319            case SUB_CONTENT:
320            case MULTILINGUAL_STRING:
321            default:
322                getLogger().warn("Type '" + type + "' is not supported for saxing on a system indexing field");
323                break;
324                
325        }
326    }
327    
328    private void _saxContentValue(ContentHandler handler, String value, Locale locale) throws SAXException
329    {
330        try
331        {
332            Content content = _resolver.resolveById(value);
333            
334            AttributesImpl attrs = new AttributesImpl();
335            attrs.addCDATAAttribute("id", content.getId());
336            attrs.addCDATAAttribute("name", content.getName());
337            attrs.addCDATAAttribute("title", content.getTitle(locale));
338            if (content.getLanguage() != null)
339            {
340                attrs.addCDATAAttribute("language", content.getLanguage());
341            }
342            attrs.addCDATAAttribute("createdAt", DateUtils.dateToString(content.getCreationDate()));
343            attrs.addCDATAAttribute("creator", content.getCreator().getLogin());
344            attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
345            XMLUtils.createElement(handler, getId(), attrs);
346        }
347        catch (UnknownAmetysObjectException e)
348        {
349            if (getLogger().isWarnEnabled())
350            {
351                getLogger().warn("The system property'" + getId() + "' references a non-existing content '" + value + "'. It will be ignored.", e);
352            }
353        }
354    }
355    
356    private void _saxUserIdentityValue(ContentHandler handler, UserIdentity value) throws SAXException
357    {
358        User user = _userManager.getUser(value);
359        if (user != null)
360        {
361            AttributesImpl attrs = new AttributesImpl();
362            attrs.addCDATAAttribute("login", value.getLogin());
363            attrs.addCDATAAttribute("populationId", value.getPopulationId());
364            attrs.addCDATAAttribute("email", user.getEmail());
365            XMLUtils.createElement(handler, getId(), attrs, user.getFullName());
366        }
367    }
368    
369    private void _saxGeocodeValue(ContentHandler handler, Map<String, Double> value) throws SAXException
370    {
371        AttributesImpl attrs = new AttributesImpl();
372        attrs.addCDATAAttribute("longitude", String.valueOf(value.get("longitude")));
373        attrs.addCDATAAttribute("latitude", String.valueOf(value.get("latitude")));
374
375        XMLUtils.createElement(handler, getId(), attrs);
376    }
377    
378    private void _saxSingleValue(ContentHandler handler, Object value) throws SAXException
379    {
380        XMLUtils.createElement(handler, getId(), ParameterHelper.valueToString(value));
381    }
382    
383    @Override
384    public Collection<SchemaDefinition> getSchemaDefinitions()
385    {
386        List<SchemaDefinition> definitions = new ArrayList<>();
387
388        SearchField searchField = getSearchField();
389        if (searchField != null)
390        {
391            String name = searchField.getName();
392            String sortFieldName = searchField.getSortField();
393            String facetFieldName = searchField.getFacetField();
394            boolean multiple = isMultiple();
395            String type = SchemaHelper.getSchemaType(getType());
396
397            if (type != null)
398            {
399                definitions.add(new FieldDefinition(name, type, multiple, false));
400
401                if (sortFieldName != null && !sortFieldName.equals(name))
402                {
403                    definitions.add(new FieldDefinition(sortFieldName, type, false, false));
404                }
405
406                if (facetFieldName != null && !facetFieldName.equals(name))
407                {
408                    // By default the index value in field name will be automatically copy in facet field
409                    // So we do not need the index facet field manually
410                    definitions.add(new FieldDefinition(facetFieldName, type, multiple, true));
411                    definitions.add(new CopyFieldDefinition(name, facetFieldName));
412                }
413            }
414        }
415
416        return definitions;
417    }
418
419    /**
420     * Parse an i18n text.
421     * 
422     * @param config the configuration to use.
423     * @param pluginName the current plugin name.
424     * @param name the child name.
425     * @return the i18n text.
426     * @throws ConfigurationException if the configuration is not valid.
427     */
428    protected I18nizableText _parseI18nizableText(Configuration config, String pluginName, String name) throws ConfigurationException
429    {
430        return I18nizableText.parseI18nizableText(config.getChild(name), "plugin." + pluginName);
431    }
432
433    /**
434     * Get the value as a long.
435     * 
436     * @param value The value as an object.
437     * @return The value as a long.
438     */
439    protected String parseString(Object value)
440    {
441        if (value instanceof String)
442        {
443            return (String) value;
444        }
445        else
446        {
447            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid String value.");
448        }
449    }
450
451    /**
452     * Get the value as a long.
453     * 
454     * @param value The value as an object.
455     * @return The value as a long.
456     */
457    protected long parseLong(Object value)
458    {
459        if (value instanceof Number)
460        {
461            return ((Number) value).longValue();
462        }
463        else if (value instanceof String)
464        {
465            return Long.parseLong((String) value);
466        }
467        else
468        {
469            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid long value.");
470        }
471    }
472
473    /**
474     * Get the value as a double.
475     * 
476     * @param value The value as an object.
477     * @return The value as a double.
478     */
479    protected double parseDouble(Object value)
480    {
481        if (value instanceof Number)
482        {
483            return ((Number) value).doubleValue();
484        }
485        else if (value instanceof String)
486        {
487            return Double.parseDouble((String) value);
488        }
489        else
490        {
491            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid double value.");
492        }
493    }
494
495    /**
496     * Get the value as a boolean.
497     * 
498     * @param value The value as an object, can be a Boolean or a String.
499     * @return The value as a boolean.
500     */
501    protected boolean parseBoolean(Object value)
502    {
503        if (value instanceof Boolean)
504        {
505            return (Boolean) value;
506        }
507        else if (value instanceof String)
508        {
509            return Boolean.parseBoolean((String) value);
510        }
511        else
512        {
513            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid boolean value.");
514        }
515    }
516
517    /**
518     * Get the value as a date.
519     * 
520     * @param value The value as an object, can be a Date or a String.
521     * @return The value as a Date object.
522     */
523    protected AdaptableDate parseDate(Object value)
524    {
525        if (value instanceof AdaptableDate)
526        {
527            return (AdaptableDate) value;
528        }
529        else if (value instanceof String)
530        {
531            return AdaptableDateParser.parse((String) value);
532        }
533        else
534        {
535            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid date value.");
536        }
537    }
538    
539    /**
540     * Get the value as array of {@link UserIdentity}
541     * @param value The value to parse
542     * @return A array of {@link UserIdentity}
543     */
544    @SuppressWarnings("unchecked")
545    protected UserIdentity[] parseUserArray (Object value)
546    {
547        if (value instanceof UserIdentity)
548        {
549            return new UserIdentity[] {(UserIdentity) value};
550        }
551        else if (value instanceof UserIdentity[])
552        {
553            return (UserIdentity[]) value;
554        }
555        else if (value instanceof List<?>)
556        {
557            return (UserIdentity[]) ((List<?>) value).stream().map(v -> _userHelper.json2userIdentity((Map<String, String>) v)).toArray();
558        }
559        else if (value instanceof Map)
560        {
561            UserIdentity userIdentity = _userHelper.json2userIdentity((Map<String, String>) value);
562            if (userIdentity != null)
563            {
564                return new UserIdentity[] {userIdentity};
565            }
566            return new UserIdentity[0];
567        }
568        else
569        {
570            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid UserIdentity values.");
571        }
572    }
573
574    /**
575     * Get the value as a array of String
576     * @param value The value as an object to parse
577     * @return The values as a String array
578     */
579    protected String[] parseStringArray(Object value)
580    {
581        if (value instanceof String)
582        {
583            return new String[] {(String) value};
584        }
585        else if (value instanceof String[])
586        {
587            return (String[]) value;
588        }
589        else if (value instanceof List<?>)
590        {
591            return ((List<?>) value).stream()
592                    .map(v -> String.valueOf(v))
593                    .toArray(String[]::new);
594        }
595        else
596        {
597            throw new IllegalArgumentException("The value " + value + " for criterion " + getId() + " is not a valid String[] values.");
598        }
599    }
600    
601    // ------------------------------------------------------
602    //                  INDEX
603    // ------------------------------------------------------
604    /**
605     * Index multiple String values
606     * @param field The search field
607     * @param values The values
608     * @param document The Solr document to index into
609     */
610    protected void _indexStringValues (SearchField field, String[] values, SolrInputDocument document)
611    {
612        document.setField(field.getName(), values);
613    }
614    
615    /**
616     * Index String value
617     * @param field The search field
618     * @param value The value
619     * @param document The Solr document to index into
620     */
621    protected void _indexStringValue (SearchField field, String value, SolrInputDocument document)
622    {
623        document.setField(field.getName(), value);
624        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
625    }
626    
627    /**
628     * Index multiple Date values
629     * @param field The search field
630     * @param values The values
631     * @param document The Solr document to index into
632     */
633    protected void _indexDateValues (SearchField field, Date[] values, SolrInputDocument document)
634    {
635        document.setField(field.getName(), values);
636    }
637    
638    /**
639     * Index Date value
640     * @param field The search field
641     * @param value The value
642     * @param document The Solr document to index into
643     */
644    protected void _indexDateValue (SearchField field, Date value, SolrInputDocument document)
645    {
646        document.setField(field.getName(), SolrIndexer.dateFormat().format(value));
647        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
648    }
649    
650    /**
651     * Index multiple Long values
652     * @param field The search field
653     * @param values The values
654     * @param document The Solr document to index into
655     */
656    protected void _indexLongValues(SearchField field, Long[] values, SolrInputDocument document)
657    {
658        document.setField(field.getName(), values);
659    }
660    
661    /**
662     * Index Long value
663     * @param field The search field
664     * @param value The value
665     * @param document The Solr document to index into
666     */
667    protected void _indexLongValue(SearchField field, Long value, SolrInputDocument document)
668    {
669        document.setField(field.getName(), value);
670        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
671    }
672    
673    /**
674     * Index multiple Double values
675     * @param field The search field
676     * @param values The values
677     * @param document The Solr document to index into
678     */
679    protected void _indexDoubleValues(SearchField field, Double[] values, SolrInputDocument document)
680    {
681        document.setField(field.getName(), values);
682    }
683    
684    /**
685     * Index Double value
686     * @param field The search field
687     * @param value The value
688     * @param document The Solr document to index into
689     */
690    protected void _indexDoubleValue(SearchField field, Double value, SolrInputDocument document)
691    {
692        document.setField(field.getName(), value);
693        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
694    }
695    
696    /**
697     * Index multiple Boolean values
698     * @param field The search field
699     * @param values The values
700     * @param document The Solr document to index into
701     */
702    protected void _indexBooleanValues(SearchField field, Boolean[] values, SolrInputDocument document)
703    {
704        document.setField(field.getName(), values);
705    }
706    
707    /**
708     * Index Boolean value
709     * @param field The search field
710     * @param value The value
711     * @param document The Solr document to index into
712     */
713    protected void _indexBooleanValue(SearchField field, Boolean value, SolrInputDocument document)
714    {
715        document.setField(field.getName(), value);
716        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
717    }
718    
719    /**
720     * Index multiple UserIdentity values
721     * @param field The search field
722     * @param values The values
723     * @param document The Solr document to index into
724     */
725    protected void _indexUserValues(SearchField field, UserIdentity[] values, SolrInputDocument document)
726    {
727        document.setField(field.getName(), values);
728    }
729    
730    /**
731     * Index UserIdentity value
732     * @param field The search field
733     * @param value The value
734     * @param document The Solr document to index into
735     */
736    protected void _indexUserValue(SearchField field, UserIdentity value, SolrInputDocument document)
737    {
738        document.setField(field.getName(), UserIdentity.userIdentityToString(value));
739        // NB : facet field if present will be indexed automatically thanks to solr copy field (see #getSchemaDefinitions)
740    }
741    
742    /**
743     * Index geocode value
744     * @param field The search field
745     * @param value The value
746     * @param document The Solr document to index into
747     */
748    protected void _indexGeocodeValue(SearchField field, Map<String, Double> value, SolrInputDocument document)
749    {
750        double longitude = value.get("longitude");
751        double latitude = value.get("latitude");
752        
753        document.setField(field.getName() + "$longitude_d", longitude);
754        document.setField(field.getName() + "$latitude_d", latitude);
755        
756        String geoFieldName = SolrFieldHelper.getIndexingFieldName(MetadataType.GEOCODE, field.getName());
757        document.setField(geoFieldName, longitude + " " + latitude);
758    }
759    
760    
761}