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.I18nUtils;
050import org.ametys.plugins.core.user.UserHelper;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.repository.UnknownAmetysObjectException;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.runtime.parameter.ParameterHelper;
055import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057import org.ametys.runtime.plugin.component.PluginAware;
058
059/**
060 * Abstract class providing standard behavior and helper methods for a
061 * {@link SystemProperty}.
062 */
063public abstract class AbstractSystemProperty extends AbstractLogEnabled implements SystemProperty, Serviceable, Configurable, PluginAware
064{
065    /** The i18n utils. */
066    protected I18nUtils _i18nUtils;
067
068    /** The property ID. */
069    protected String _id;
070
071    /** The property label. */
072    protected I18nizableText _label;
073
074    /** The property description. */
075    protected I18nizableText _description;
076
077    /** The property plugin name. */
078    protected String _pluginName;
079
080    /** The user helper */
081    protected UserHelper _userHelper;
082    /** The ametys object resolver */
083    protected AmetysObjectResolver _resolver;
084
085    @Override
086    public void setPluginInfo(String pluginName, String featureName, String id)
087    {
088        _pluginName = pluginName;
089        _id = id;
090    }
091
092    public void service(ServiceManager manager) throws ServiceException
093    {
094        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
095        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
096        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
097    }
098
099    @Override
100    public void configure(Configuration configuration) throws ConfigurationException
101    {
102        _label = _parseI18nizableText(configuration, _pluginName, "label");
103        _description = _parseI18nizableText(configuration, _pluginName, "description");
104    }
105
106    @Override
107    public String getId()
108    {
109        return _id;
110    }
111
112    @Override
113    public I18nizableText getLabel()
114    {
115        return _label;
116    }
117
118    @Override
119    public I18nizableText getDescription()
120    {
121        return _description;
122    }
123
124    @SuppressWarnings("unchecked")
125    @Override
126    public void index(Content content, SolrInputDocument document)
127    {
128        Object value = getValue(content);
129        
130        SearchField searchField = getSearchField();
131        
132        if (value == null || searchField == null || searchField.getName() == null)
133        {
134            // Nothing to index
135            return;
136        }
137
138        MetadataType type = getType();
139        switch (type)
140        {
141            case STRING:
142            case CONTENT:
143                if (isMultiple())
144                {
145                    _indexStringValues(searchField, (String[]) value, document);
146                }
147                else
148                {
149                    _indexStringValue(searchField, (String) value, document);
150                }
151                break;
152            case DOUBLE:
153                if (isMultiple())
154                {
155                    _indexDoubleValues(searchField, (Double[]) value, document);
156                }
157                else
158                {
159                    _indexDoubleValue(searchField, (Double) value, document);
160                }
161                break;
162            case LONG:
163                if (isMultiple())
164                {
165                    _indexLongValues(searchField, (Long[]) value, document);
166                }
167                else
168                {
169                    _indexLongValue(searchField, (Long) value, document);
170                }
171                break;
172            case GEOCODE:
173                _indexGeocodeValue(searchField, (Map<String, Double>) value, document);
174                break;
175            case BOOLEAN:
176                if (isMultiple())
177                {
178                    _indexBooleanValues(searchField, (Boolean[]) value, document);
179                }
180                else
181                {
182                    _indexBooleanValue(searchField, (Boolean) value, document);
183                }
184                break;
185            case DATE:
186            case DATETIME:
187                if (isMultiple())
188                {
189                    _indexDateValues(searchField, (Date[]) value, document);
190                }
191                else
192                {
193                    _indexDateValue(searchField, (Date) value, document);
194                }
195                break;
196            case USER:
197                if (isMultiple())
198                {
199                    _indexUserValues(searchField, (UserIdentity[]) value, document);
200                }
201                else
202                {
203                    _indexUserValue(searchField, (UserIdentity) value, document);
204                }
205                break;
206            case FILE:
207            case BINARY:
208            case COMPOSITE:
209            case RICH_TEXT:
210            case REFERENCE:
211            case SUB_CONTENT:
212            case MULTILINGUAL_STRING:
213                getLogger().warn("Type '" + type + "' is not supported for indexation on a system indexing field");
214                break;
215            default:
216                break;
217        }
218        
219        // Index sort field
220        Object sortValue = getSortValue(content);
221        if (isSortable() && sortValue != null && searchField.getSortField() != null && !searchField.getName().equals(searchField.getSortField()))
222        {
223            document.setField(searchField.getSortField(), sortValue);
224        }
225    }
226
227    @Override
228    public Object getSortValue(Content content)
229    {
230        // Default to getValue(), override to provide a specific sort value.
231        Object value = getValue(content);
232        
233        if (value == null)
234        {
235            return null;
236        }
237
238        if (isMultiple() && value instanceof Object[] && ((Object[]) value).length > 0)
239        {
240            return ((Object[]) value)[0]; // return the first value
241        }
242        else
243        {
244            return value;
245        }
246    }
247    
248    @SuppressWarnings("unchecked")
249    public void saxValue(ContentHandler handler, Content content) throws SAXException
250    {
251        Object value = getValue(content);
252        
253        if (value == null)
254        {
255            return;
256        }
257        
258        MetadataType type = getType();
259        switch (type)
260        {
261            case USER:
262                if (value instanceof UserIdentity[])
263                {
264                    for (UserIdentity identity : (UserIdentity[]) value)
265                    {
266                        _saxUserIdentityValue(handler, identity);
267                    }
268                }
269                else if (value instanceof UserIdentity)
270                {
271                    _saxUserIdentityValue(handler, (UserIdentity) value);
272                }
273                break;
274            case CONTENT:
275                Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : null;
276                if (isMultiple())
277                {
278                    for (String id : (String[]) value)
279                    {
280                        _saxContentValue(handler, id, locale);
281                    }
282                }
283                else
284                {
285                    _saxContentValue(handler, (String) value, locale);
286                }
287                break;
288            case GEOCODE:
289                _saxGeocodeValue(handler, (Map<String, Double>) value);
290                break;
291            case BOOLEAN:
292            case DOUBLE:
293            case LONG:
294            case DATE:
295            case DATETIME:
296            case STRING:
297                if (isMultiple())
298                {
299                    for (Object v : (Object[]) value)
300                    {
301                        _saxSingleValue(handler, v);
302                    }
303                }
304                else
305                {
306                    _saxSingleValue(handler, value);
307                }
308                break;
309            case BINARY:
310            case COMPOSITE:
311            case REFERENCE:
312            case RICH_TEXT:
313            case SUB_CONTENT:
314            case MULTILINGUAL_STRING:
315            default:
316                getLogger().warn("Type '" + type + "' is not supported for saxing on a system indexing field");
317                break;
318                
319        }
320    }
321    
322    private void _saxContentValue(ContentHandler handler, String value, Locale locale) throws SAXException
323    {
324        try
325        {
326            Content content = _resolver.resolveById(value);
327            
328            AttributesImpl attrs = new AttributesImpl();
329            attrs.addCDATAAttribute("id", content.getId());
330            attrs.addCDATAAttribute("name", content.getName());
331            attrs.addCDATAAttribute("title", content.getTitle(locale));
332            if (content.getLanguage() != null)
333            {
334                attrs.addCDATAAttribute("language", content.getLanguage());
335            }
336            attrs.addCDATAAttribute("createdAt", ParameterHelper.valueToString(content.getCreationDate()));
337            attrs.addCDATAAttribute("creator", content.getCreator().getLogin());
338            attrs.addCDATAAttribute("lastModifiedAt", ParameterHelper.valueToString(content.getLastModified()));
339            XMLUtils.createElement(handler, getId(), attrs);
340        }
341        catch (UnknownAmetysObjectException e)
342        {
343            if (getLogger().isWarnEnabled())
344            {
345                getLogger().warn("The system property'" + getId() + "' references a non-existing content '" + value + "'. It will be ignored.", e);
346            }
347        }
348    }
349    
350    private void _saxUserIdentityValue(ContentHandler handler, UserIdentity value) throws SAXException
351    {
352        User user = _userHelper.getUser(value);
353        if (user != null)
354        {
355            AttributesImpl attrs = new AttributesImpl();
356            attrs.addCDATAAttribute("login", value.getLogin());
357            attrs.addCDATAAttribute("populationId", value.getPopulationId());
358            attrs.addCDATAAttribute("email", user.getEmail());
359            XMLUtils.createElement(handler, getId(), attrs, user.getFullName());
360        }
361    }
362    
363    private void _saxGeocodeValue(ContentHandler handler, Map<String, Double> value) throws SAXException
364    {
365        AttributesImpl attrs = new AttributesImpl();
366        attrs.addCDATAAttribute("longitude", String.valueOf(value.get("longitude")));
367        attrs.addCDATAAttribute("latitude", String.valueOf(value.get("latitude")));
368
369        XMLUtils.createElement(handler, getId(), attrs);
370    }
371    
372    private void _saxSingleValue(ContentHandler handler, Object value) throws SAXException
373    {
374        XMLUtils.createElement(handler, getId(), ParameterHelper.valueToString(value));
375    }
376    
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 Date parseDate(Object value)
520    {
521        if (value instanceof Date)
522        {
523            return (Date) value;
524        }
525        else if (value instanceof String)
526        {
527            return (Date) ParameterHelper.castValue((String) value, ParameterType.DATE);
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}