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}