001/*
002 *  Copyright 2016 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.content;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033
034import org.ametys.cms.content.indexing.solr.SolrFieldNames;
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.contenttype.ContentTypesHelper;
037import org.ametys.cms.repository.Content;
038import org.ametys.cms.search.QueryBuilder;
039import org.ametys.cms.search.SearchField;
040import org.ametys.cms.search.SearchResults;
041import org.ametys.cms.search.Sort;
042import org.ametys.cms.search.Sort.Order;
043import org.ametys.cms.search.model.ResultField;
044import org.ametys.cms.search.model.SearchCriterion;
045import org.ametys.cms.search.model.SearchModel;
046import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
047import org.ametys.cms.search.query.ContentTypeQuery;
048import org.ametys.cms.search.query.DocumentTypeQuery;
049import org.ametys.cms.search.query.Query;
050import org.ametys.cms.search.solr.SearcherFactory;
051import org.ametys.cms.search.solr.SearcherFactory.Searcher;
052import org.ametys.cms.search.ui.model.SearchUIModel;
053import org.ametys.plugins.repository.AmetysObject;
054import org.ametys.plugins.repository.AmetysObjectIterable;
055import org.ametys.runtime.plugin.component.AbstractLogEnabled;
056
057/**
058 * Component creating content searchers from {@link SearchModel}s or content type IDs.
059 */
060public class ContentSearcherFactory extends AbstractLogEnabled implements Component, Serviceable
061{
062    
063    /** The component role. */
064    public static final String ROLE = ContentSearcherFactory.class.getName();
065    
066    /** The searcher factory. */
067    protected SearcherFactory _searcherFactory;
068    
069    /** The query builder. */
070    protected QueryBuilder _queryBuilder;
071    
072    /** The content type extension point. */
073    protected ContentTypeExtensionPoint _cTypeEP;
074    
075    /** The content type helper. */
076    protected ContentTypesHelper _cTypeHelper;
077    
078    /** The system property extension point. */
079    protected SystemPropertyExtensionPoint _sysPropEP;
080    
081    /** The search helper. */
082    protected ContentSearchHelper _searchHelper;
083    
084    @Override
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE);
088        _queryBuilder = (QueryBuilder) manager.lookup(QueryBuilder.ROLE);
089        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
090        _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
091        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
092        
093        _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE);
094    }
095    
096    /**
097     * Create a ContentSearcher from a search model.
098     * @param searchModel The reference search model.
099     * @return a ContentSearcher backed by the given search model.
100     */
101    public SearchModelContentSearcher create(SearchModel searchModel)
102    {
103        return new SearchModelContentSearcher(searchModel);
104    }
105    
106    /**
107     * Create a simple ContentSearcher from a list of content types.
108     * @param contentTypes The content types to search on.
109     * @return a ContentSearcher referencing the given content types.
110     */
111    public SimpleContentSearcher create(String... contentTypes)
112    {
113        return new SimpleContentSearcher(Arrays.asList(contentTypes));
114    }
115    
116    /**
117     * Create a simple ContentSearcher from a list of content types.
118     * @param contentTypes The content types to search on.
119     * @return a ContentSearcher referencing the given content types.
120     */
121    public SimpleContentSearcher create(Collection<String> contentTypes)
122    {
123        return new SimpleContentSearcher(contentTypes);
124    }
125    
126    /**
127     * A ContentSearcher backed by a {@link SearchModel}.
128     */
129    public class SearchModelContentSearcher
130    {
131        private SearchUIModel _searchModel;
132        private List<Sort> _sort;
133        private String _searchMode;
134        private int _start;
135        private int _maxResults;
136        private boolean _checkRights;
137        
138        /**
139         * Build a ContentSearcher referencing a {@link SearchModel}.
140         * @param searchModel the {@link SearchModel}.
141         */
142        public SearchModelContentSearcher(SearchModel searchModel)
143        {
144            // TODO Do not cast.
145            this._searchModel = (SearchUIModel) searchModel;
146            this._sort = new ArrayList<>();
147            this._searchMode = "simple";
148            this._start = 0;
149            this._maxResults = Integer.MAX_VALUE;
150            this._checkRights = true;
151        }
152        
153        /**
154         * Add a sort criterion.
155         * @param fieldRef The field reference (name of a SearchField).
156         * @param order The sort order.
157         * @return The ContentSearcher itself.
158         */
159        public SearchModelContentSearcher addSort(String fieldRef, Order order)
160        {
161            _sort.add(new Sort(fieldRef, order));
162            return this;
163        }
164        
165        /**
166         * Set the sort criteria.
167         * @param sortCriteria The sort criteria as a List.
168         * @return The ContentSearcher itself.
169         */
170        public SearchModelContentSearcher withSort(List<Sort> sortCriteria)
171        {
172            _sort = new ArrayList<>(sortCriteria);
173            return this;
174        }
175        
176        /**
177         * Set the search mode.
178         * @param searchMode The search mode.
179         * @return The ContentSearcher itself.
180         */
181        public SearchModelContentSearcher withSearchMode(String searchMode)
182        {
183            _searchMode = searchMode;
184            return this;
185        }
186        
187        /**
188         * Set the limits to use.
189         * @param start The start index.
190         * @param maxResults The maximum number of results.
191         * @return The ContentSearcher itself.
192         */
193        public SearchModelContentSearcher withLimits(int start, int maxResults)
194        {
195            this._start = start;
196            this._maxResults = maxResults;
197            return this;
198        }
199        
200        /**
201         * Whether to check rights when searching, false otherwise.
202         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
203         * @return The ContentSearcher itself.
204         */
205        public SearchModelContentSearcher setCheckRights(boolean checkRights)
206        {
207            _checkRights = checkRights;
208            return this;
209        }
210        
211        /**
212         * Search the contents.
213         * @param values The values for search criteria defined in the model.
214         * @param <C> The type Content
215         * @return The search results as {@link AmetysObject}s.
216         * @throws Exception if an error occurs.
217         */
218        public <C extends Content> AmetysObjectIterable<C> search(Map<String, Object> values) throws Exception
219        {
220            return _searcher(values, Collections.emptyMap(), Collections.emptyMap()).search();
221        }
222        
223        /**
224         * Search the contents.
225         * @param values The values for search criteria defined in the model.
226         * @param <C> The type Content         * 
227         * @return The search results.
228         * @throws Exception if an error occurs.
229         */
230        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values) throws Exception
231        {
232            return searchWithFacets(values, Collections.emptyMap());
233        }
234        
235        /**
236         * Search the contents.
237         * @param <C> The type Content
238         * @param values The values for search criteria defined in the model.
239         * @param contextualParameters The search contextual parameters.
240         * @return The search results.
241         * @throws Exception if an error occurs.
242         */
243        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, Object> contextualParameters) throws Exception
244        {
245            return searchWithFacets(values, Collections.emptyMap(), contextualParameters);
246        }
247        
248        /**
249         * Search the contents.
250         * @param <C> The type Content
251         * @param values The values for search criteria defined in the model.
252         * @param facetValues The facet values, indexed 
253         * @param contextualParameters The search contextual parameters. 
254         * @return The search results.
255         * @throws Exception if an error occurs.
256         */
257        public <C extends Content> SearchResults<C> searchWithFacets(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters) throws Exception
258        {
259            return _searcher(values, facetValues, contextualParameters).searchWithFacets();
260        }
261        
262        private Searcher _searcher(Map<String, Object> values, Map<String, List<String>> facetValues, Map<String, Object> contextualParameters)
263        {
264            Query query = _queryBuilder.build(_searchModel, _searchMode, true, values, contextualParameters);
265            
266            List<Sort> sort = getSort(contextualParameters);
267            List<SearchField> facets = getFacets(contextualParameters);
268            
269            return _searcherFactory.create()
270                                    .withQuery(query)
271                                    .withFilterQueries(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
272                                    .withSort(sort)
273                                    .withFacets(facets)
274                                    .withFacetValues(facetValues)
275                                    .withLimits(_start, _maxResults)
276                                    .setCheckRights(_checkRights);
277        }
278        
279        /**
280         * Get the sort criteria.
281         * @param contextualParameters The search contextual parameters.
282         * @return The sort criteria.
283         */
284        @SuppressWarnings("synthetic-access")
285        protected List<Sort> getSort(Map<String, Object> contextualParameters)
286        {
287            List<Sort> sort = new ArrayList<>();
288            
289            if (!_sort.isEmpty())
290            {
291                // Index criterion and results by search field name.
292                Map<String, SearchCriterion> criteriaByName = new HashMap<>();
293                for (SearchCriterion criterion : _searchModel.getCriteria(contextualParameters).values())
294                {
295                    if (criterion.getSearchField() != null)
296                    {
297                        criteriaByName.put(criterion.getSearchField().getName(), criterion);
298                    }
299                }
300                Map<String, ResultField> resultsByName = new HashMap<>();
301                for (ResultField resultField : _searchModel.getResultFields(contextualParameters).values())
302                {
303                    if (resultField.getSearchField() != null)
304                    {
305                        resultsByName.put(resultField.getSearchField().getName(), resultField);
306                    }
307                }
308                
309                for (Sort sortCriterion : _sort)
310                {
311                    String id = sortCriterion.getField();
312                    
313                    SearchField searchField = null;
314                    if (criteriaByName.containsKey(id))
315                    {
316                        searchField = criteriaByName.get(id).getSearchField();
317                    }
318                    else if (resultsByName.containsKey(id))
319                    {
320                        searchField = resultsByName.get(id).getSearchField();
321                    }
322                    
323                    if (searchField == null)
324                    {
325                        throw new IllegalArgumentException("The field '" + id + "' can't be found in the selected search model.");
326                    }
327                    else if (searchField.getSortField() == null)
328                    {
329                        getLogger().warn("The field '{}' is not sortable. The search will execute, but without the sort on this field.", id);
330                    }
331                    else
332                    {
333                        sort.add(new Sort(searchField, sortCriterion.getOrder()));
334                    }
335                }
336            }
337            else
338            {
339                // Get the default sort from the search model.
340            }
341            
342            return sort;
343        }
344        
345        /**
346         * Get the facet fields.
347         * @param contextualParameters The search contextual parameters.
348         * @return The facet fields as a List.
349         */
350        protected List<SearchField> getFacets(Map<String, Object> contextualParameters)
351        {
352            List<SearchField> facets = new ArrayList<>();
353            
354            for (SearchCriterion criterion : _searchModel.getFacetedCriteria(contextualParameters).values())
355            {
356                if (criterion.getSearchField() != null)
357                {
358                    facets.add(criterion.getSearchField());
359                }
360            }
361            
362            return facets;
363        }
364        
365    }
366    
367    /**
368     * A ContentSearcher on a list of content types.
369     */
370    public class SimpleContentSearcher
371    {
372        
373        private Set<String> _contentTypes;
374        private List<Sort> _sort;
375        private List<String> _facets;
376        private int _start;
377        private int _maxResults;
378        private boolean _checkRights;
379        private List<String> _filterQueryStrings;
380        private List<Query> _filterQueries;
381        
382        /**
383         * Build a content searcher on a list of content types.
384         * @param contentTypes A collection of content types to search on.
385         */
386        public SimpleContentSearcher(Collection<String> contentTypes)
387        {
388            this._contentTypes = contentTypes != null ? new HashSet<>(contentTypes) : Collections.emptySet();
389            this._sort = new ArrayList<>();
390            this._facets = new ArrayList<>();
391            this._start = 0;
392            this._maxResults = Integer.MAX_VALUE;
393            this._checkRights = true;
394        }
395        
396        /**
397         * Set the filter queries.
398         * @param filterQueries the filter queries.
399         * @return The ContentSearcher itself.
400         */
401        public SimpleContentSearcher withFilterQueries(List<Query> filterQueries)
402        {
403            _filterQueries = filterQueries;
404            return this;
405        }
406        
407        /**
408         * Set the filter queries.
409         * @param filterQueryStrings the filter queries.
410         * @return The ContentSearcher itself.
411         */
412        public SimpleContentSearcher withFilterQueryStrings(List<String> filterQueryStrings)
413        {
414            _filterQueryStrings = filterQueryStrings;
415            return this;
416        }
417        
418        /**
419         * Set the sort criteria.
420         * @param sortCriteria The sort criteria as a List.
421         * @return The ContentSearcher itself.
422         */
423        public SimpleContentSearcher withSort(List<Sort> sortCriteria)
424        {
425            _sort = new ArrayList<>(sortCriteria);
426            return this;
427        }
428        
429        /**
430         * Add a sort criterion.
431         * @param fieldRef The field reference (name of a SearchField).
432         * @param order The sort order.
433         * @return The ContentSearcher itself.
434         */
435        public SimpleContentSearcher addSort(String fieldRef, Order order)
436        {
437            _sort.add(new Sort(fieldRef, order));
438            return this;
439        }
440        
441        /**
442         * Set the facets.
443         * @param facets The facets list.
444         * @return The ContentSearcher itself.
445         */
446        public SimpleContentSearcher withFacets(Collection<String> facets)
447        {
448            _facets = new ArrayList<>(facets);
449            return this;
450        }
451        
452        /**
453         * Set the facets.
454         * @param facets The facets list.
455         * @return The ContentSearcher itself.
456         */
457        public SimpleContentSearcher withFacets(String... facets)
458        {
459            _facets = Arrays.asList(facets);
460            return this;
461        }
462        
463        /**
464         * Set the limits to use.
465         * @param start The start index.
466         * @param maxResults The maximum number of results.
467         * @return The ContentSearcher itself.
468         */
469        public SimpleContentSearcher withLimits(int start, int maxResults)
470        {
471            this._start = start;
472            this._maxResults = maxResults;
473            return this;
474        }
475        
476        /**
477         * Whether to check rights when searching, false otherwise.
478         * @param checkRights <code>true</code> to check rights, <code>false</code> otherwise.
479         * @return The ContentSearcher itself.
480         */
481        public SimpleContentSearcher setCheckRights(boolean checkRights)
482        {
483            _checkRights = checkRights;
484            return this;
485        }
486        
487        /**
488         * Search the contents.
489         * @param <C> The type Content
490         * @param query The query object to execute.
491         * @return The search results as {@link AmetysObject}s.
492         * @throws Exception if an error occurs.
493         */
494        public <C extends Content> AmetysObjectIterable<C> search(Query query) throws Exception
495        {
496            return _searcher(query, Collections.emptyMap()).search();
497        }
498        
499        /**
500         * Search the contents.
501         * @param <C> The type Content
502         * @param query The query string to execute.
503         * @return The search results as {@link AmetysObject}s.
504         * @throws Exception if an error occurs.
505         */
506        public <C extends Content> AmetysObjectIterable<C> search(String query) throws Exception
507        {
508            return _searcher(query, Collections.emptyMap()).search();
509        }
510        
511        /**
512         * Search the contents.
513         * @param <C> The type Content
514         * @param query The query objet to execute.
515         * @return The search results.
516         * @throws Exception if an error occurs.
517         */
518        public <C extends Content> SearchResults<C> searchWithFacets(Query query) throws Exception
519        {
520            return searchWithFacets(query, Collections.emptyMap());
521        }
522        
523        /**
524         * Search the contents.
525         * @param <C> The type Content
526         * @param query The query string to execute.
527         * @return The search results.
528         * @throws Exception if an error occurs.
529         */
530        public <C extends Content> SearchResults<C> searchWithFacets(String query) throws Exception
531        {
532            return searchWithFacets(query, Collections.emptyMap());
533        }
534        
535        /**
536         * Search the contents.
537         * @param <C> The type Content
538         * @param query The query object to execute.
539         * @param facetValues The facet values.
540         * @return The search results.
541         * @throws Exception if an error occurs.
542         */
543        public <C extends Content> SearchResults<C> searchWithFacets(Query query, Map<String, List<String>> facetValues) throws Exception
544        {
545            return _searcher(query, facetValues).searchWithFacets();
546        }
547        
548        /**
549         * Search the contents.
550         * @param <C> The type Content
551         * @param query The query string to execute.
552         * @param facetValues The facet values.
553         * @return The search results.
554         * @throws Exception if an error occurs.
555         */
556        public <C extends Content> SearchResults<C> searchWithFacets(String query, Map<String, List<String>> facetValues) throws Exception
557        {
558            return _searcher(query, facetValues).searchWithFacets();
559        }
560        
561        private Searcher _searcher(String query, Map<String, List<String>> facetValues)
562        {
563            return _searcher(facetValues).withQueryString(query);
564        }
565        
566        private Searcher _searcher(Query query, Map<String, List<String>> facetValues)
567        {
568            return _searcher(facetValues).withQuery(query);
569        }
570        
571        private Searcher _searcher(Map<String, List<String>> facetValues)
572        {
573            List<Sort> sort = getSort();
574            List<SearchField> facets = getFacets();
575            
576            List<Query> filterQueries = new ArrayList<>();
577            filterQueries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
578            
579            if (!_contentTypes.isEmpty())
580            {
581                filterQueries.add(new ContentTypeQuery(_contentTypes));
582            }
583            
584            if (_filterQueries != null)
585            {
586                filterQueries.addAll(_filterQueries);
587            }
588            
589            List<String> filterQueryStrings = new ArrayList<>();
590            
591            if (_filterQueryStrings != null)
592            {
593                filterQueryStrings.addAll(_filterQueryStrings);
594            }
595            
596            return _searcherFactory.create()
597                                   .withFilterQueries(filterQueries)
598                                   .withFilterQueryStrings(filterQueryStrings)
599                                   .withSort(sort)
600                                   .withFacets(facets)
601                                   .withFacetValues(facetValues)
602                                   .withLimits(_start, _maxResults)
603                                   .setCheckRights(_checkRights);
604        }
605        
606        /**
607         * Get the sort criteria from the specified field names.
608         * @return The sort criteria.
609         */
610        protected List<Sort> getSort()
611        {
612            List<Sort> sortCriteria = new ArrayList<>();
613            
614            for (Sort sort : _sort)
615            {
616                String fieldName = sort.getField();
617                Order order = sort.getOrder();
618                
619                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
620//                SearchField searchField = getSearchField(_contentTypes, fieldName);
621                if (searchField.isPresent())
622                {
623                    sortCriteria.add(new Sort(searchField.get(), order));
624                }
625                else
626                {
627                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
628                }
629            }
630            
631            return sortCriteria;
632        }
633        
634        /**
635         * Get the facet criteria as a list of SearchField from the specified field names.
636         * @return The facets as a List of SearchField. 
637         */
638        protected List<SearchField> getFacets()
639        {
640            List<SearchField> facets = new ArrayList<>();
641            
642            for (String fieldName : _facets)
643            {
644                Optional<SearchField> searchField = _searchHelper.getSearchField(_contentTypes, fieldName);
645//                SearchField searchField = getSearchField(_contentTypes, fieldName);
646                if (searchField.isPresent())
647                {
648                    facets.add(searchField.get());
649                }
650                else
651                {
652                    throw new IllegalArgumentException(_exceptionMessageForEmptySearchField(fieldName));
653                }
654            }
655            
656            return facets;
657        }
658        
659        private String _exceptionMessageForEmptySearchField(String fieldName)
660        {
661            return "The field '" + fieldName + "' can't be found in the selected content types.";
662        }
663        
664//        /**
665//         * Get a {@link SearchField} from a field name.
666//         * @param fieldName The field name, can be either a system property ID or a metadata path (not joined).
667//         * @return The {@link SearchField} corresponding to the 
668//         */
669//        public SearchField getSearchField(Collection<String> contentTypes, String fieldName)
670//        {
671//            SearchField searchField = null;
672//            
673//            if (_sysPropEP.hasExtension(fieldName))
674//            {
675//                SystemProperty property = _sysPropEP.getExtension(fieldName);
676//                searchField = property.getSearchField();
677//            }
678//            else
679//            {
680//                String metadataPath = fieldName.replace('.', '/');
681//                _searchHelper.getMetadataSearchField(contentTypes, metadataPath);
682//            }
683//            
684//            return searchField;
685//        }
686        
687//        /**
688//         * Get a {@link SearchField} from a field name.
689//         * @param fieldName The field name, can be either a system property ID or a metadata path (not joined).
690//         * @return The {@link SearchField} corresponding to the 
691//         */
692//        protected SearchField getSearchField(String fieldName)
693//        {
694//            SearchField searchField = null;
695//            
696//            if (_sysPropEP.hasExtension(fieldName))
697//            {
698//                SystemProperty property = _sysPropEP.getExtension(fieldName);
699//                searchField = property.getSearchField();
700//            }
701//            else
702//            {
703//                String metaPath = fieldName.replace('.', '/');
704//                if (_contentTypeId != null)
705//                {
706//                    ContentType cType = _cTypeEP.getExtension(_contentTypeId);
707//                    List<MetadataDefinition> metaDefs = _cTypeHelper.getMetadataDefinitionsByPath(cType, metaPath);
708//                    
709//                    boolean joinedMetadata = isJoinedMetadata(metaDefs);
710//                    
711//                    if (!joinedMetadata)
712//                    {
713//                        MetadataType type = metaDefs.get(metaDefs.size() - 1).getType();
714//                        searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, type);
715//                    }
716//                    else
717//                    {
718//                        throw new IllegalArgumentException("The metadata '" + fieldName + "' can't be used as it is joined.");
719//                    }
720//                }
721//                else if (fieldName.equals("title"))
722//                {
723//                    // No specific content type: allow only title.
724//                    // TODO Provide a standard "title" metadata definition to avoid getting it on a random content type.
725//                    ContentType cType = _cTypeEP.getExtension(_contentTypes.iterator().next());
726//                    MetadataDefinition metaDef = cType.getMetadataDefinition("title");
727//                    searchField = IndexingFieldSearchUICriterion.getSearchField(fieldName, metaDef.getType());
728//                }
729//            }
730//            
731//            if (searchField == null)
732//            {
733//                throw new IllegalArgumentException("The field '" + fieldName + "' can't be found in the selected content types.");
734//            }
735//            
736//            return searchField;
737//        }
738//        
739//        /**
740//         * Test if a metadata, represented by a list of successive definitions, is joined.
741//         * @param metaDefs The list of successive definitions.
742//         * @return <code>true</code> if the metadata is joined, <code>false</code> otherwise.
743//         */
744//        protected boolean isJoinedMetadata(List<MetadataDefinition> metaDefs)
745//        {
746//            boolean joinedMetadata = false;
747//            Iterator<MetadataDefinition> metaDefIt = metaDefs.iterator();
748//            while (metaDefIt.hasNext())
749//            {
750//                MetadataType type = metaDefIt.next().getType();
751//                // The column represents a "joined" value if it has a content metadata (except if it's the last one).
752//                if ((type == MetadataType.CONTENT || type == MetadataType.SUB_CONTENT) && metaDefIt.hasNext())
753//                {
754//                    joinedMetadata = true;
755//                }
756//            }
757//            return joinedMetadata;
758//        }
759        
760    }
761    
762}