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