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