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.solr;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Stream;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.avalon.framework.configuration.DefaultConfiguration;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.commons.lang3.StringUtils;
035import org.slf4j.Logger;
036
037import org.ametys.cms.search.model.SystemProperty;
038import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
039import org.ametys.cms.search.query.Query.Operator;
040import org.ametys.cms.search.ui.model.AbstractSearchUIModel;
041import org.ametys.cms.search.ui.model.SearchUIColumn;
042import org.ametys.cms.search.ui.model.SearchUICriterion;
043import org.ametys.cms.search.ui.model.SearchUIModel;
044import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint;
045import org.ametys.cms.search.ui.model.impl.IndexingFieldSearchUICriterion;
046import org.ametys.cms.search.ui.model.impl.MetadataSearchUIColumn;
047import org.ametys.cms.search.ui.model.impl.SystemSearchUIColumn;
048import org.ametys.cms.search.ui.model.impl.SystemSearchUICriterion;
049import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
050
051/**
052 * Search model wrapper which handles custom on-the-fly columns and facets.
053 */
054public class CriteriaSearchUIModelWrapper extends AbstractSearchUIModel
055{
056    
057    /** ComponentManager for {@link SearchUICriterion}s. */
058    protected ThreadSafeComponentManager<SearchUICriterion> _searchUICriterionManager;
059    
060    /** ComponentManager for {@link SearchUIColumn}s. */
061    protected ThreadSafeComponentManager<SearchUIColumn> _searchUIColumnManager;
062    
063    private SearchUIModelExtensionPoint _searchModelEP;
064    private SystemPropertyExtensionPoint _sysPropEP;
065    
066    private SearchUIModel _wrappedModel;
067    
068    private int _criteriaIndex;
069    
070    /**
071     * Build a model wrapper.
072     * @param model the search model to wrap.
073     * @param manager the service manager.
074     * @param context the component context.
075     * @param logger the logger.
076     */
077    public CriteriaSearchUIModelWrapper(SearchUIModel model, ServiceManager manager, Context context, Logger logger)
078    {
079        _wrappedModel = model;
080        
081        _logger = logger;
082        
083        try
084        {
085            _searchUICriterionManager = new ThreadSafeComponentManager<>();
086            _searchUICriterionManager.setLogger(logger);
087            _searchUICriterionManager.contextualize(context);
088            _searchUICriterionManager.service(manager);
089            
090            _searchUIColumnManager = new ThreadSafeComponentManager<>();
091            _searchUIColumnManager.setLogger(logger);
092            _searchUIColumnManager.contextualize(context);
093            _searchUIColumnManager.service(manager);
094        }
095        catch (Exception e)
096        {
097            _logger.error("Error initializing the SearchModel", e);
098        }
099    }
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        super.service(manager);
105        _searchModelEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE);
106        _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
107    }
108    
109    /**
110     * Set the custom faceted criteria.
111     * @param contentTypeId the reference content type ID, can be null.
112     * @param criteriaIds the criteria IDs
113     * @param contextualParameters the contextual parameters
114     * @return the valid criteria IDs
115     * @throws Exception if an error occurs initializing criteria.
116     */
117    public Collection<String> setFacetedCriteria(String contentTypeId, Collection<String> criteriaIds, Map<String, Object> contextualParameters) throws Exception
118    {
119        Collection<String> resultCriteriaIds = null;
120        
121        if (criteriaIds != null)
122        {
123            _facetedCriteria = new LinkedHashMap<>(criteriaIds.size());
124            
125            List<Object> searchToolCriterionRoles = new ArrayList<>();
126            
127            resultCriteriaIds = configureFacets(searchToolCriterionRoles, contentTypeId, criteriaIds, _wrappedModel, contextualParameters);
128            
129            _searchUICriterionManager.initialize();
130            
131            for (Object critObj : searchToolCriterionRoles)
132            {
133                SearchUICriterion criterion = null;
134                if (critObj instanceof SearchUICriterion)
135                {
136                    // Already existing SearchUICriterion object (taken from the wrapped model).
137                    criterion = (SearchUICriterion) critObj;
138                }
139                else if (critObj instanceof String)
140                {
141                    // Criterion just added in the local component manager, we have to look it up.
142                    criterion = _searchUICriterionManager.lookup((String) critObj);
143                }
144                
145                if (criterion != null && criterion.isFacetable())
146                {
147                    _facetedCriteria.put(criterion.getId(), criterion);
148                }
149            }
150        }
151        
152        return resultCriteriaIds != null ? resultCriteriaIds : Collections.emptySet();
153    }
154    
155    /**
156     * Set the custom columns.
157     * @param contentTypeId the reference content type ID, can be null.
158     * @param columns The columns
159     * @param contextualParameters the contextual parameters
160     * @throws Exception if an error occurs initializing columns.
161     */
162    public void setResultColumns(String contentTypeId, Collection<Column> columns, Map<String, Object> contextualParameters) throws Exception
163    {
164        String wrappedModelId = (String) contextualParameters.get("wrappedModelId");
165        if (StringUtils.isNotEmpty(wrappedModelId))
166        {
167            // Dashboard
168            SearchUIModel model = _searchModelEP.getExtension(wrappedModelId);
169            _columns = model.getResultFields(contextualParameters);
170        }
171        else if (columns != null)
172        {
173            _columns = new LinkedHashMap<>(columns.size());
174            
175            List<Object> columnRoles = new ArrayList<>();
176            
177            configureColumns(columnRoles, contentTypeId, columns, _wrappedModel, contextualParameters);
178            
179            _searchUIColumnManager.initialize();
180            
181            for (Object col : columnRoles)
182            {
183                SearchUIColumn column = null;
184                if (col instanceof SearchUIColumn)
185                {
186                    // Already existing SearchUIColumn object (taken from the wrapped model).
187                    column = (SearchUIColumn) col;
188                }
189                else if (col instanceof String)
190                {
191                    // Column just added in the local component manager, we have to look it up.
192                    column = _searchUIColumnManager.lookup((String) col);
193                }
194
195                if (column != null)
196                {
197                    _columns.put(column.getId(), column);
198                }
199            }
200        }
201    }
202    
203    /**
204     * Configure the list of faceted criteria.
205     * @param criteriaRoles the roles of criteria to lookup (or the already existing SearchUICriterion objects).
206     * @param contentTypeId The reference content type ID.
207     * @param criteriaIds the criteria IDs.
208     * @param referenceModel the reference model.
209     * @param contextualParameters the contextual parameters
210     * @return the valid criteria IDs
211     * @throws ConfigurationException if an error occurs creating a component configuration.
212     */
213    protected Collection<String> configureFacets(List<Object> criteriaRoles, String contentTypeId, Collection<String> criteriaIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException
214    {
215        Collection<String> resultCriteriaIds = new ArrayList<>();
216        
217        for (String criterionId : criteriaIds)
218        {
219            String[] facetPathSegments = StringUtils.split(criterionId, '/');
220            String lastSegmentOfFacetPath = facetPathSegments[facetPathSegments.length - 1];
221            
222            SearchUICriterion referenceCriterion = null;
223            if (referenceModel != null)
224            {
225                referenceCriterion = getCriterion(referenceModel, criterionId, contextualParameters);
226            }
227            
228            if (referenceCriterion != null)
229            {
230                criteriaRoles.add(referenceCriterion);
231            }
232            else if (_sysPropEP.hasExtension(lastSegmentOfFacetPath))
233            {
234                SystemProperty systemProperty = _sysPropEP.getExtension(lastSegmentOfFacetPath);
235                if (systemProperty.isFacetable())
236                {
237                    addSystemCriteriaComponents(criteriaRoles, contentTypeId, criterionId);
238                }
239                else
240                {
241                    getLogger().warn("The declared facet '{}' is a system property but is not facetable. Thus, it will not be added to the facets.", criterionId);
242                    break;
243                }
244            }
245            else
246            {
247                if (contentTypeId != null && _cTypeEP.hasExtension(contentTypeId))
248                {
249                    // Metadata property.
250                    addIndexingFieldCriteriaComponents(criteriaRoles, contentTypeId, criterionId);
251                }
252                else if ("title".equals(criterionId))
253                {
254                    // title property of a random ContentType.
255                    String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next();
256                    addIndexingFieldCriteriaComponents(criteriaRoles, firstCTypeId, criterionId);
257                }
258                else
259                {
260                    break;
261                }
262            }
263            
264            resultCriteriaIds.add(criterionId);
265        }
266        
267        return resultCriteriaIds;
268    }
269    
270    /**
271     * Search a criterion in the reference model from its criterion identifier.
272     * @param searchModel the reference search model.
273     * @param criterionId the criterion identifier.
274     * @param contextualParameters the contextual parameters
275     * @return the criterion if found, null otherwise.
276     */
277    protected SearchUICriterion getCriterion(SearchUIModel searchModel, String criterionId, Map<String, Object> contextualParameters)
278    {
279        Map<String, SearchUICriterion> criteria = searchModel.getFacetedCriteria(contextualParameters);
280        
281        for (SearchUICriterion criterion : criteria.values())
282        {
283            if (criterion instanceof IndexingFieldSearchUICriterion && ((IndexingFieldSearchUICriterion) criterion).getFieldPath().equals(criterionId))
284            {
285                return criterion;
286            }
287            else if (criterion instanceof SystemSearchUICriterion && ((SystemSearchUICriterion) criterion).getSystemPropertyId().equals(criterionId))
288            {
289                return criterion;
290            }
291            else if (criterion.getId().equals(criterionId))
292            {
293                return criterion;
294            }
295        }
296        
297        return null;
298    }
299    
300    /**
301     * Configure the list of search columns.
302     * @param columnRoles the roles of columns to lookup (or the already existing SearchUIColumn objects).
303     * @param contentTypeId The reference content type ID.
304     * @param columnIds The columns
305     * @param referenceModel the reference model.
306     * @param contextualParameters the contextual parameters
307     * @throws ConfigurationException if an error occurs creating a component configuration.
308     */
309    protected void configureColumns(List<Object> columnRoles, String contentTypeId, Collection<Column> columnIds, SearchUIModel referenceModel, Map<String, Object> contextualParameters) throws ConfigurationException
310    {
311        for (Column column : columnIds)
312        {
313            String columnId = column.getId();
314            Optional<String> columnLabel = column.getLabel();
315            String[] columnPathSegments = StringUtils.split(columnId, '/');
316            String lastSegmentOfColumnPath = columnPathSegments[columnPathSegments.length - 1];
317            
318            SearchUIColumn referenceColumn = null;
319            if (referenceModel != null && !columnLabel.isPresent() /* cannot temporary change the label of a reference column on the fly => re-create a component */)
320            {
321                referenceColumn = getColumn(referenceModel, columnId, contextualParameters);
322            }
323            
324            if (referenceColumn != null)
325            {
326                columnRoles.add(referenceColumn);
327            }
328            else if (_sysPropEP.isDisplayable(lastSegmentOfColumnPath))
329            {
330                // System property.
331                addSystemColumnComponent(columnRoles, contentTypeId, columnId, columnLabel);
332            }
333            else if (_sysPropEP.hasExtension(lastSegmentOfColumnPath))
334            {
335                getLogger().warn("The column '{}' is a system property but is not displayable. Thus, it will not be displayed.", columnId);
336            }
337            else 
338            {
339                // Metadata property.
340                if (contentTypeId != null && _cTypeEP.hasExtension(contentTypeId))
341                {
342                    addMetadataColumnComponents(columnRoles, contentTypeId, columnId, columnLabel);
343                }
344                else if ("title".equals(columnId))
345                {
346                    // Get the title property of a random ContentType.
347                    String firstCTypeId = _cTypeEP.getExtensionsIds().iterator().next();
348                    addMetadataColumnComponents(columnRoles, firstCTypeId, columnId, columnLabel);
349                }
350            }
351        }
352    }
353    
354    /**
355     * Search a column in the reference model from its column identifier.
356     * @param searchModel the reference search model.
357     * @param columnId the column identifier.
358     * @param contextualParameters the contextual parameters
359     * @return the column if found, null otherwise.
360     */
361    protected SearchUIColumn getColumn(SearchUIModel searchModel, String columnId, Map<String, Object> contextualParameters)
362    {
363        Map<String, SearchUIColumn> columns = searchModel.getResultFields(contextualParameters);
364        
365        for (SearchUIColumn column : columns.values())
366        {
367            if (column instanceof MetadataSearchUIColumn && ((MetadataSearchUIColumn) column).getFieldPath().equals(columnId))
368            {
369                return column;
370            }
371            else if (column instanceof SystemSearchUIColumn && ((SystemSearchUIColumn) column).getSystemPropertyId().equals(columnId))
372            {
373                return column;
374            }
375//            else if (column instanceof CustomSearchToolColumn && column.getId().equals(columnId))
376//            {
377//                return column;
378//            }
379        }
380        
381        return null;
382    }
383
384    /**
385     * Add a indexing field criteria component to the manager.
386     * @param searchToolCriterionRoles the criteria role list to fill.
387     * @param contentTypeId the reference content type ID, can be null.
388     * @param fieldRef the field path.
389     * @throws ConfigurationException if an error occurs.
390     */
391    protected void addIndexingFieldCriteriaComponents(List<Object> searchToolCriterionRoles, String contentTypeId, String fieldRef) throws ConfigurationException
392    {
393        try
394        {
395            String slashPath = fieldRef.replace('.', '/');
396            
397            String role = fieldRef + _criteriaIndex;
398            _criteriaIndex++;
399            Configuration criteriaConf = getIndexingFieldCriteriaConfiguration(contentTypeId, slashPath, Operator.EQ);
400            
401            _searchUICriterionManager.addComponent("search", null, role, IndexingFieldSearchUICriterion.class, criteriaConf);
402            
403            searchToolCriterionRoles.add(role);
404        }
405        catch (Exception e)
406        {
407            throw new ConfigurationException("Unable to instanciate IndexingFieldSearchUICriterion for field " + fieldRef, e);
408        }        
409    }
410    
411    /**
412     * Add a system criteria component to the manager.
413     * @param searchToolCriterionRoles the criteria role list to fill.
414     * @param contentTypeId the reference content type ID, can be null.
415     * @param property the system property id.
416     * @throws ConfigurationException if an error occurs.
417     */
418    protected void addSystemCriteriaComponents(List<Object> searchToolCriterionRoles, String contentTypeId, String property) throws ConfigurationException
419    {
420        try
421        {
422            String role = property + _criteriaIndex;
423            _criteriaIndex++;
424            
425            Configuration criteriaConf = getSystemCriteriaConfiguration(contentTypeId, property);
426            _searchUICriterionManager.addComponent("search", null, role, SystemSearchUICriterion.class, criteriaConf);
427            
428            searchToolCriterionRoles.add(role);
429        }
430        catch (Exception e)
431        {
432            throw new ConfigurationException("Unable to instanciate SystemSearchUICriterion for property " + property, e);
433        }
434    }
435    
436    private Configuration addLabelInColumnConf(Configuration originalConf, String label) throws ConfigurationException
437    {
438        DefaultConfiguration confWithLabel = new DefaultConfiguration(originalConf);
439        Stream.of(confWithLabel.getChildren("label")).forEach(confWithLabel::removeChild);
440        DefaultConfiguration labelConf = new DefaultConfiguration("label");
441        labelConf.setValue(label);
442        confWithLabel.addChild(labelConf);
443        return confWithLabel;
444    }
445    
446    /**
447     * Add a metadata column component to the manager.
448     * @param columnsRolesToLookup the columns roles
449     * @param contentTypeId the reference content type ID, can be null.
450     * @param metadataPath the metadata path.
451     * @param columnLabel The (optional) label of the column. If not present, the column label will be the metadata one.
452     * @throws ConfigurationException if an error occurs.
453     */
454    protected void addMetadataColumnComponents(List<Object> columnsRolesToLookup, String contentTypeId, String metadataPath, Optional<String> columnLabel) throws ConfigurationException
455    {
456        try
457        {
458            Configuration columnConf = getMetadataColumnConfiguration(contentTypeId, metadataPath);
459            if (columnLabel.isPresent())
460            {
461                columnConf = addLabelInColumnConf(columnConf, columnLabel.get());
462            }
463            
464            _searchUIColumnManager.addComponent("search", null, metadataPath, MetadataSearchUIColumn.class, columnConf);
465            columnsRolesToLookup.add(metadataPath);
466        }
467        catch (Exception e)
468        {
469            throw new ConfigurationException("Unable to instanciate MetadataSearchUIColumn for metadata " + metadataPath, e);
470        }
471    }
472    
473    /**
474     * Add a system column component to the manager.
475     * @param columnsRolesToLookup the columns roles
476     * @param contentTypeId the reference content type ID, can be null.
477     * @param property the system property.
478     * @param columnLabel The (optional) label of the column. If not present, the column label will be the metadata one.
479     * @throws ConfigurationException if an error occurs.
480     */
481    protected void addSystemColumnComponent(List<Object> columnsRolesToLookup, String contentTypeId, String property, Optional<String> columnLabel) throws ConfigurationException
482    {
483        try
484        {
485            Configuration conf = getSystemColumnConfiguration(contentTypeId, property);
486            if (columnLabel.isPresent())
487            {
488                conf = addLabelInColumnConf(conf, columnLabel.get());
489            }
490            
491            _searchUIColumnManager.addComponent("cms", null, property, SystemSearchUIColumn.class, conf);
492            columnsRolesToLookup.add(property);
493        }
494        catch (Exception e)
495        {
496            throw new ConfigurationException("Unable to instanciate SystemSearchUIColumn for property " + property, e);
497        }
498    }
499    
500    @Override
501    public Map<String, SearchUICriterion> getFacetedCriteria(Map<String, Object> contextualParameters)
502    {
503        if (_facetedCriteria != null && !_facetedCriteria.isEmpty())
504        {
505            return Collections.unmodifiableMap(_facetedCriteria);
506        }
507        else
508        {
509            return _wrappedModel.getFacetedCriteria(contextualParameters);
510        }
511    }
512    
513    @Override
514    public Map<String, SearchUIColumn> getResultFields(Map<String, Object> contextualParameters)
515    {
516        if (_columns != null && !_columns.isEmpty())
517        {
518            return Collections.unmodifiableMap(_columns);
519        }
520        else
521        {
522            return _wrappedModel.getResultFields(contextualParameters);
523        }
524    }
525    
526    @Override
527    public SearchUIColumn getResultField(String id, Map<String, Object> contextualParameters)
528    {
529        if (_columns != null && !_columns.isEmpty())
530        {
531            return getResultFields(contextualParameters).get(id);
532        }
533        else
534        {
535            return _wrappedModel.getResultField(id, contextualParameters);
536        }
537    }
538    
539    //// PROXY METHODS ////
540    
541    @Override
542    public Set<String> getContentTypes(Map<String, Object> contextualParameters)
543    {
544        return _wrappedModel.getContentTypes(contextualParameters);
545    }
546    
547    @Override
548    public Set<String> getExcludedContentTypes(Map<String, Object> contextualParameters)
549    {
550        return _wrappedModel.getExcludedContentTypes(contextualParameters);
551    }
552    
553    @Override
554    public String getSearchUrl(Map<String, Object> contextualParameters)
555    {
556        return _wrappedModel.getSearchUrl(contextualParameters);
557    }
558
559    @Override
560    public String getSearchUrlPlugin(Map<String, Object> contextualParameters)
561    {
562        return _wrappedModel.getSearchUrlPlugin(contextualParameters);
563    }
564
565    @Override
566    public String getExportCSVUrl(Map<String, Object> contextualParameters)
567    {
568        return _wrappedModel.getExportCSVUrl(contextualParameters);
569    }
570
571    @Override
572    public String getExportCSVUrlPlugin(Map<String, Object> contextualParameters)
573    {
574        return _wrappedModel.getExportCSVUrlPlugin(contextualParameters);
575    }
576
577    @Override
578    public String getExportDOCUrl(Map<String, Object> contextualParameters)
579    {
580        return _wrappedModel.getExportDOCUrl(contextualParameters);
581    }
582
583    @Override
584    public String getExportDOCUrlPlugin(Map<String, Object> contextualParameters)
585    {
586        return _wrappedModel.getExportDOCUrlPlugin(contextualParameters);
587    }
588
589    @Override
590    public String getExportXMLUrl(Map<String, Object> contextualParameters)
591    {
592        return _wrappedModel.getExportXMLUrl(contextualParameters);
593    }
594
595    @Override
596    public String getExportXMLUrlPlugin(Map<String, Object> contextualParameters)
597    {
598        return _wrappedModel.getExportXMLUrlPlugin(contextualParameters);
599    }
600
601    @Override
602    public String getPrintUrl(Map<String, Object> contextualParameters)
603    {
604        return _wrappedModel.getPrintUrl(contextualParameters);
605    }
606
607    @Override
608    public String getPrintUrlPlugin(Map<String, Object> contextualParameters)
609    {
610        return _wrappedModel.getPrintUrlPlugin(contextualParameters);
611    }
612    
613    @Override
614    public boolean allowSortOnMultipleJoin()
615    {
616        return _wrappedModel.allowSortOnMultipleJoin();
617    }
618
619    @Override
620    public Map<String, SearchUICriterion> getCriteria(Map<String, Object> contextualParameters)
621    {
622        return _wrappedModel.getCriteria(contextualParameters);
623    }
624    
625    @Override
626    public SearchUICriterion getCriterion(String id, Map<String, Object> contextualParameters)
627    {
628        return _wrappedModel.getCriterion(id, contextualParameters);
629    }
630    
631    @Override
632    public Map<String, SearchUICriterion> getAdvancedCriteria(Map<String, Object> contextualParameters)
633    {
634        return _wrappedModel.getAdvancedCriteria(contextualParameters);
635    }
636    
637    /**
638     * A column and its (optional) label
639     */
640    public static final class Column
641    {
642        private String _id;
643        private Optional<String> _label;
644
645        /**
646         * Creates a {@link Column} object, wrapping a column id and its (optional) label.
647         * <br>If the provided label is <code>null</code>, then a default label will be applied to the column.
648         * @param columnId The id of the column
649         * @param columnLabel The label of the column. Cannot contain the comma character (<b>,</b>). Can be null
650         */
651        public Column(String columnId, String columnLabel)
652        {
653            _id = columnId;
654            _label = Optional.ofNullable(columnLabel);
655            if (_label.filter(l -> l.contains(",")).isPresent())
656            {
657                throw new IllegalArgumentException("The label cannot contain a comma.");
658            }
659        }
660        
661        /**
662        * Gets the id of the column
663        * @return the id of the column
664        */
665        public String getId()
666        {
667            return _id;
668        }
669        
670        /**
671        * Gets the label of the column
672        * @return the label of the column 
673        */
674        public Optional<String> getLabel()
675        {
676            return _label;
677        }
678        
679        @Override
680        public String toString()
681        {
682            return "Column<" + _id + ", \"" + _label.orElse("<NO LABEL>") + "\">";
683        }
684
685        @Override
686        public int hashCode()
687        {
688            final int prime = 31;
689            int result = 1;
690            result = prime * result + ((_id == null) ? 0 : _id.hashCode());
691            return result;
692        }
693
694        // only based on the column id
695        @Override
696        public boolean equals(Object obj)
697        {
698            if (this == obj)
699            {
700                return true;
701            }
702            if (obj == null)
703            {
704                return false;
705            }
706            if (getClass() != obj.getClass())
707            {
708                return false;
709            }
710            Column other = (Column) obj;
711            if (_id == null)
712            {
713                if (other._id != null)
714                {
715                    return false;
716                }
717            }
718            else if (!_id.equals(other._id))
719            {
720                return false;
721            }
722            return true;
723        }
724    }
725}