001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.search.builder.sql; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider; 026import ca.uhn.fhir.jpa.model.config.PartitionSettings; 027import ca.uhn.fhir.jpa.model.dao.JpaPid; 028import ca.uhn.fhir.jpa.model.entity.StorageSettings; 029import ca.uhn.fhir.jpa.search.builder.QueryStack; 030import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder; 031import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder; 032import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder; 033import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; 034import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; 035import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; 036import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; 037import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; 038import ca.uhn.fhir.jpa.search.builder.predicate.ResourceHistoryPredicateBuilder; 039import ca.uhn.fhir.jpa.search.builder.predicate.ResourceHistoryProvenancePredicateBuilder; 040import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; 041import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; 042import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; 043import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder; 044import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder; 045import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder; 046import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder; 047import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder; 048import ca.uhn.fhir.rest.param.DateParam; 049import ca.uhn.fhir.rest.param.DateRangeParam; 050import ca.uhn.fhir.rest.param.ParamPrefixEnum; 051import com.healthmarketscience.sqlbuilder.BinaryCondition; 052import com.healthmarketscience.sqlbuilder.ComboCondition; 053import com.healthmarketscience.sqlbuilder.ComboExpression; 054import com.healthmarketscience.sqlbuilder.Condition; 055import com.healthmarketscience.sqlbuilder.FunctionCall; 056import com.healthmarketscience.sqlbuilder.InCondition; 057import com.healthmarketscience.sqlbuilder.OrderObject; 058import com.healthmarketscience.sqlbuilder.SelectQuery; 059import com.healthmarketscience.sqlbuilder.dbspec.Join; 060import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; 061import com.healthmarketscience.sqlbuilder.dbspec.basic.DbJoin; 062import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema; 063import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec; 064import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable; 065import jakarta.annotation.Nonnull; 066import jakarta.annotation.Nullable; 067import org.hibernate.dialect.Dialect; 068import org.hibernate.dialect.SQLServerDialect; 069import org.hibernate.dialect.pagination.AbstractLimitHandler; 070import org.hibernate.query.internal.QueryOptionsImpl; 071import org.hibernate.query.spi.Limit; 072import org.hibernate.query.spi.QueryOptions; 073import org.slf4j.Logger; 074import org.slf4j.LoggerFactory; 075 076import java.util.ArrayList; 077import java.util.Collection; 078import java.util.List; 079import java.util.Set; 080import java.util.UUID; 081import java.util.stream.Collectors; 082 083import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN; 084import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; 085import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN; 086import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 087import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL; 088import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 089 090public class SearchQueryBuilder { 091 092 private static final Logger ourLog = LoggerFactory.getLogger(SearchQueryBuilder.class); 093 private final String myBindVariableSubstitutionBase; 094 private final ArrayList<Object> myBindVariableValues; 095 private final DbSpec mySpec; 096 private final DbSchema mySchema; 097 private final SelectQuery mySelect; 098 private final PartitionSettings myPartitionSettings; 099 private final RequestPartitionId myRequestPartitionId; 100 private final String myResourceType; 101 private final StorageSettings myStorageSettings; 102 private final FhirContext myFhirContext; 103 private final SqlObjectFactory mySqlBuilderFactory; 104 private final boolean myCountQuery; 105 private final Dialect myDialect; 106 private final boolean mySelectPartitionId; 107 private boolean myMatchNothing; 108 private ResourceTablePredicateBuilder myResourceTableRoot; 109 private boolean myHaveAtLeastOnePredicate; 110 private BaseJoiningPredicateBuilder myFirstPredicateBuilder; 111 private boolean dialectIsMsSql; 112 private boolean dialectIsMySql; 113 private boolean myNeedResourceTableRoot; 114 private int myNextNearnessColumnId = 0; 115 private DbColumn mySelectedResourceIdColumn; 116 private DbColumn mySelectedPartitionIdColumn; 117 118 /** 119 * Constructor 120 */ 121 public SearchQueryBuilder( 122 FhirContext theFhirContext, 123 StorageSettings theStorageSettings, 124 PartitionSettings thePartitionSettings, 125 RequestPartitionId theRequestPartitionId, 126 String theResourceType, 127 SqlObjectFactory theSqlBuilderFactory, 128 HibernatePropertiesProvider theDialectProvider, 129 boolean theCountQuery) { 130 this( 131 theFhirContext, 132 theStorageSettings, 133 thePartitionSettings, 134 theRequestPartitionId, 135 theResourceType, 136 theSqlBuilderFactory, 137 UUID.randomUUID() + "-", 138 theDialectProvider.getDialect(), 139 theCountQuery, 140 new ArrayList<>(), 141 thePartitionSettings.isPartitioningEnabled()); 142 } 143 144 /** 145 * Constructor for child SQL Builders 146 */ 147 private SearchQueryBuilder( 148 FhirContext theFhirContext, 149 StorageSettings theStorageSettings, 150 PartitionSettings thePartitionSettings, 151 RequestPartitionId theRequestPartitionId, 152 String theResourceType, 153 SqlObjectFactory theSqlBuilderFactory, 154 String theBindVariableSubstitutionBase, 155 Dialect theDialect, 156 boolean theCountQuery, 157 ArrayList<Object> theBindVariableValues, 158 boolean theSelectPartitionId) { 159 myFhirContext = theFhirContext; 160 myStorageSettings = theStorageSettings; 161 myPartitionSettings = thePartitionSettings; 162 myRequestPartitionId = theRequestPartitionId; 163 myResourceType = theResourceType; 164 mySqlBuilderFactory = theSqlBuilderFactory; 165 myCountQuery = theCountQuery; 166 myDialect = theDialect; 167 if (myDialect instanceof org.hibernate.dialect.MySQLDialect) { 168 dialectIsMySql = true; 169 } 170 if (myDialect instanceof org.hibernate.dialect.SQLServerDialect) { 171 dialectIsMsSql = true; 172 } 173 174 mySpec = new DbSpec(); 175 mySchema = mySpec.addDefaultSchema(); 176 mySelect = new SelectQuery(); 177 178 myBindVariableSubstitutionBase = theBindVariableSubstitutionBase; 179 myBindVariableValues = theBindVariableValues; 180 mySelectPartitionId = theSelectPartitionId; 181 } 182 183 public FhirContext getFhirContext() { 184 return myFhirContext; 185 } 186 187 /** 188 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 189 */ 190 public ComboUniqueSearchParameterPredicateBuilder addComboUniquePredicateBuilder() { 191 ComboUniqueSearchParameterPredicateBuilder retVal = 192 mySqlBuilderFactory.newComboUniqueSearchParameterPredicateBuilder(this); 193 addTable(retVal, null); 194 return retVal; 195 } 196 197 /** 198 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter 199 */ 200 public ComboNonUniqueSearchParameterPredicateBuilder addComboNonUniquePredicateBuilder() { 201 ComboNonUniqueSearchParameterPredicateBuilder retVal = 202 mySqlBuilderFactory.newComboNonUniqueSearchParameterPredicateBuilder(this); 203 addTable(retVal, null); 204 return retVal; 205 } 206 207 /** 208 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a COORDS search parameter 209 */ 210 public CoordsPredicateBuilder addCoordsPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 211 CoordsPredicateBuilder retVal = mySqlBuilderFactory.coordsPredicateBuilder(this); 212 addTable(retVal, theSourceJoinColumn); 213 return retVal; 214 } 215 216 /** 217 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a DATE search parameter 218 */ 219 public DatePredicateBuilder addDatePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 220 DatePredicateBuilder retVal = mySqlBuilderFactory.dateIndexTable(this); 221 addTable(retVal, theSourceJoinColumn); 222 return retVal; 223 } 224 225 /** 226 * Create a predicate builder for selecting on a DATE search parameter 227 */ 228 public DatePredicateBuilder createDatePredicateBuilder() { 229 return mySqlBuilderFactory.dateIndexTable(this); 230 } 231 232 /** 233 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a NUMBER search parameter 234 */ 235 public NumberPredicateBuilder addNumberPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 236 NumberPredicateBuilder retVal = createNumberPredicateBuilder(); 237 addTable(retVal, theSourceJoinColumn); 238 return retVal; 239 } 240 241 /** 242 * Create a predicate builder for selecting on a NUMBER search parameter 243 */ 244 public NumberPredicateBuilder createNumberPredicateBuilder() { 245 return mySqlBuilderFactory.numberIndexTable(this); 246 } 247 248 /** 249 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on the Resource table 250 */ 251 public ResourceTablePredicateBuilder addResourceTablePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 252 ResourceTablePredicateBuilder retVal = mySqlBuilderFactory.resourceTable(this); 253 addTable(retVal, theSourceJoinColumn); 254 return retVal; 255 } 256 257 /** 258 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter 259 */ 260 public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 261 QuantityPredicateBuilder retVal = createQuantityPredicateBuilder(); 262 addTable(retVal, theSourceJoinColumn); 263 264 return retVal; 265 } 266 267 /** 268 * Create a predicate builder for selecting on a QUANTITY search parameter 269 */ 270 public QuantityPredicateBuilder createQuantityPredicateBuilder() { 271 return mySqlBuilderFactory.quantityIndexTable(this); 272 } 273 274 public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder( 275 @Nullable DbColumn[] theSourceJoinColumn) { 276 277 QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this); 278 addTable(retVal, theSourceJoinColumn); 279 280 return retVal; 281 } 282 283 /** 284 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_source</code> search parameter 285 */ 286 public ResourceHistoryProvenancePredicateBuilder addResourceHistoryProvenancePredicateBuilder( 287 @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 288 ResourceHistoryProvenancePredicateBuilder retVal = 289 mySqlBuilderFactory.newResourceHistoryProvenancePredicateBuilder(this); 290 addTable(retVal, theSourceJoinColumn, theJoinType); 291 return retVal; 292 } 293 294 /** 295 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_source</code> search parameter 296 */ 297 public ResourceHistoryPredicateBuilder addResourceHistoryPredicateBuilder( 298 @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) { 299 ResourceHistoryPredicateBuilder retVal = mySqlBuilderFactory.newResourceHistoryPredicateBuilder(this); 300 addTable(retVal, theSourceJoinColumn, theJoinType); 301 return retVal; 302 } 303 304 /** 305 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a REFERENCE search parameter 306 */ 307 public ResourceLinkPredicateBuilder addReferencePredicateBuilder( 308 QueryStack theQueryStack, @Nullable DbColumn[] theSourceJoinColumn) { 309 ResourceLinkPredicateBuilder retVal = createReferencePredicateBuilder(theQueryStack); 310 addTable(retVal, theSourceJoinColumn); 311 return retVal; 312 } 313 314 /** 315 * Create a predicate builder for selecting on a REFERENCE search parameter 316 */ 317 public ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack) { 318 return mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, false); 319 } 320 321 /** 322 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a resource link where the 323 * source and target are reversed. This is used for _has queries. 324 */ 325 public ResourceLinkPredicateBuilder addReferencePredicateBuilderReversed( 326 QueryStack theQueryStack, DbColumn[] theSourceJoinColumn) { 327 ResourceLinkPredicateBuilder retVal = mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, true); 328 addTable(retVal, theSourceJoinColumn); 329 return retVal; 330 } 331 332 /** 333 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a STRING search parameter 334 */ 335 public StringPredicateBuilder addStringPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 336 StringPredicateBuilder retVal = createStringPredicateBuilder(); 337 addTable(retVal, theSourceJoinColumn); 338 return retVal; 339 } 340 341 /** 342 * Create a predicate builder for selecting on a STRING search parameter 343 */ 344 public StringPredicateBuilder createStringPredicateBuilder() { 345 return mySqlBuilderFactory.stringIndexTable(this); 346 } 347 348 /** 349 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_tag</code> search parameter 350 */ 351 public TagPredicateBuilder addTagPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 352 TagPredicateBuilder retVal = mySqlBuilderFactory.newTagPredicateBuilder(this); 353 addTable(retVal, theSourceJoinColumn); 354 return retVal; 355 } 356 357 /** 358 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a TOKEN search parameter 359 */ 360 public TokenPredicateBuilder addTokenPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 361 TokenPredicateBuilder retVal = createTokenPredicateBuilder(); 362 addTable(retVal, theSourceJoinColumn); 363 return retVal; 364 } 365 366 /** 367 * Create a predicate builder for selecting on a TOKEN search parameter 368 */ 369 public TokenPredicateBuilder createTokenPredicateBuilder() { 370 return mySqlBuilderFactory.tokenIndexTable(this); 371 } 372 373 public void addCustomJoin( 374 SelectQuery.JoinType theJoinType, DbTable theFromTable, DbTable theToTable, Condition theCondition) { 375 mySelect.addCustomJoin(theJoinType, theFromTable, theToTable, theCondition); 376 } 377 378 public ComboCondition createOnCondition(DbColumn[] theSourceColumn, DbColumn[] theTargetColumn) { 379 ComboCondition onCondition = ComboCondition.and(); 380 for (int i = 0; i < theSourceColumn.length; i += 1) { 381 onCondition.addCondition(BinaryCondition.equalTo(theSourceColumn[i], theTargetColumn[i])); 382 } 383 return onCondition; 384 } 385 386 /** 387 * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>:missing</code> search parameter 388 */ 389 public SearchParamPresentPredicateBuilder addSearchParamPresentPredicateBuilder( 390 @Nullable DbColumn[] theSourceJoinColumn) { 391 SearchParamPresentPredicateBuilder retVal = mySqlBuilderFactory.searchParamPresentPredicateBuilder(this); 392 addTable(retVal, theSourceJoinColumn); 393 return retVal; 394 } 395 396 /** 397 * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a URI search parameter 398 */ 399 public UriPredicateBuilder addUriPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) { 400 UriPredicateBuilder retVal = createUriPredicateBuilder(); 401 addTable(retVal, theSourceJoinColumn); 402 return retVal; 403 } 404 405 /** 406 * Create a predicate builder for selecting on a URI search parameter 407 */ 408 public UriPredicateBuilder createUriPredicateBuilder() { 409 return mySqlBuilderFactory.uriIndexTable(this); 410 } 411 412 public SqlObjectFactory getSqlBuilderFactory() { 413 return mySqlBuilderFactory; 414 } 415 416 public ResourceIdPredicateBuilder newResourceIdBuilder() { 417 return mySqlBuilderFactory.resourceId(this); 418 } 419 420 /** 421 * Add and return a predicate builder (or a root query if no root query exists yet) for an arbitrary table 422 */ 423 private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn[] theSourceJoinColumn) { 424 addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.INNER); 425 } 426 427 private void addTable( 428 BaseJoiningPredicateBuilder thePredicateBuilder, 429 @Nullable DbColumn[] theSourceJoinColumns, 430 SelectQuery.JoinType theJoinType) { 431 if (theSourceJoinColumns != null) { 432 DbTable fromTable = theSourceJoinColumns[0].getTable(); 433 DbTable toTable = thePredicateBuilder.getTable(); 434 DbColumn[] toColumn = toJoinColumns(thePredicateBuilder); 435 addJoin(fromTable, toTable, theSourceJoinColumns, toColumn, theJoinType); 436 } else { 437 if (myFirstPredicateBuilder == null) { 438 439 BaseJoiningPredicateBuilder root; 440 if (!myNeedResourceTableRoot) { 441 root = thePredicateBuilder; 442 } else { 443 if (thePredicateBuilder instanceof ResourceTablePredicateBuilder) { 444 root = thePredicateBuilder; 445 } else { 446 root = mySqlBuilderFactory.resourceTable(this); 447 } 448 } 449 450 if (myCountQuery) { 451 mySelect.addCustomColumns( 452 FunctionCall.count().setIsDistinct(true).addColumnParams(root.getResourceIdColumn())); 453 } else { 454 if (mySelectPartitionId) { 455 mySelectedResourceIdColumn = root.getResourceIdColumn(); 456 mySelectedPartitionIdColumn = root.getPartitionIdColumn(); 457 mySelect.addColumns(mySelectedPartitionIdColumn, mySelectedResourceIdColumn); 458 } else { 459 mySelectedResourceIdColumn = root.getResourceIdColumn(); 460 mySelect.addColumns(mySelectedResourceIdColumn); 461 } 462 } 463 mySelect.addFromTable(root.getTable()); 464 myFirstPredicateBuilder = root; 465 466 if (!myNeedResourceTableRoot || (thePredicateBuilder instanceof ResourceTablePredicateBuilder)) { 467 return; 468 } 469 } 470 471 DbTable fromTable = myFirstPredicateBuilder.getTable(); 472 DbTable toTable = thePredicateBuilder.getTable(); 473 DbColumn[] fromColumn = toJoinColumns(myFirstPredicateBuilder); 474 DbColumn[] toColumn = toJoinColumns(thePredicateBuilder); 475 addJoin(fromTable, toTable, fromColumn, toColumn, theJoinType); 476 } 477 } 478 479 @Nonnull 480 public DbColumn[] toJoinColumns(BaseJoiningPredicateBuilder theBuilder) { 481 DbColumn partitionIdColumn = theBuilder.getPartitionIdColumn(); 482 DbColumn resourceIdColumn = theBuilder.getResourceIdColumn(); 483 return toJoinColumns(partitionIdColumn, resourceIdColumn); 484 } 485 486 /** 487 * Remove or keep partition_id columns depending on settings. 488 */ 489 @Nonnull 490 public DbColumn[] toJoinColumns(DbColumn partitionIdColumn, DbColumn resourceIdColumn) { 491 if (isIncludePartitionIdInJoins()) { 492 return new DbColumn[] {partitionIdColumn, resourceIdColumn}; 493 } else { 494 return new DbColumn[] {resourceIdColumn}; 495 } 496 } 497 498 public boolean isIncludePartitionIdInJoins() { 499 return mySelectPartitionId && myPartitionSettings.isDatabasePartitionMode(); 500 } 501 502 public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn[] theFromColumn, DbColumn[] theToColumn) { 503 addJoin(theFromTable, theToTable, theFromColumn, theToColumn, SelectQuery.JoinType.INNER); 504 } 505 506 public void addJoin( 507 DbTable theFromTable, 508 DbTable theToTable, 509 DbColumn[] theFromColumn, 510 DbColumn[] theToColumn, 511 SelectQuery.JoinType theJoinType) { 512 assert theFromColumn.length == theToColumn.length; 513 Join join = new DbJoin(mySpec, theFromTable, theToTable, theFromColumn, theToColumn); 514 mySelect.addJoins(theJoinType, join); 515 } 516 517 public boolean isSelectPartitionId() { 518 return mySelectPartitionId; 519 } 520 521 /** 522 * Generate and return the SQL generated by this builder 523 */ 524 public GeneratedSql generate(@Nullable Integer theOffset, @Nullable Integer theMaxResultsToFetch) { 525 getOrCreateFirstPredicateBuilder(); 526 527 mySelect.validate(); 528 String sql = mySelect.toString(); 529 530 List<Object> bindVariables = new ArrayList<>(); 531 while (true) { 532 533 int idx = sql.indexOf(myBindVariableSubstitutionBase); 534 if (idx == -1) { 535 break; 536 } 537 538 int endIdx = sql.indexOf("'", idx + myBindVariableSubstitutionBase.length()); 539 String substitutionIndexString = sql.substring(idx + myBindVariableSubstitutionBase.length(), endIdx); 540 int substitutionIndex = Integer.parseInt(substitutionIndexString); 541 bindVariables.add(myBindVariableValues.get(substitutionIndex)); 542 543 sql = sql.substring(0, idx - 1) + "?" + sql.substring(endIdx + 1); 544 } 545 546 Integer maxResultsToFetch = theMaxResultsToFetch; 547 Integer offset = theOffset; 548 if (offset != null && offset == 0) { 549 offset = null; 550 } 551 if (maxResultsToFetch != null || offset != null) { 552 553 maxResultsToFetch = defaultIfNull(maxResultsToFetch, 10000); 554 String selectedResourceIdColumn = mySelectedResourceIdColumn.getColumnNameSQL(); 555 556 sql = applyLimitToSql(myDialect, offset, maxResultsToFetch, sql, selectedResourceIdColumn, bindVariables); 557 } 558 559 return new GeneratedSql(myMatchNothing, sql, bindVariables); 560 } 561 562 /** 563 * This method applies the theDialect limiter (select first NNN offset MMM etc etc..) to 564 * a SQL string. It enhances the built-in Hibernate dialect version with some additional 565 * enhancements. 566 */ 567 public static String applyLimitToSql( 568 Dialect theDialect, 569 Integer theOffset, 570 Integer theMaxResultsToFetch, 571 String theInputSql, 572 @Nullable String theSelectedColumnOrNull, 573 List<Object> theBindVariables) { 574 AbstractLimitHandler limitHandler = (AbstractLimitHandler) theDialect.getLimitHandler(); 575 Limit selection = new Limit(); 576 selection.setFirstRow(theOffset); 577 selection.setMaxRows(theMaxResultsToFetch); 578 QueryOptions queryOptions = new QueryOptionsImpl(); 579 theInputSql = limitHandler.processSql(theInputSql, selection, queryOptions); 580 581 int startOfQueryParameterIndex = 0; 582 583 boolean isSqlServer = (theDialect instanceof SQLServerDialect); 584 if (isSqlServer) { 585 586 /* 587 * SQL server requires an ORDER BY clause to be present in the SQL if there is 588 * an OFFSET/FETCH FIRST clause, so if there isn't already an ORDER BY clause, 589 * the theDialect will automatically add an order by with a pseudo-column name. This 590 * happens in SQLServer2012LimitHandler. 591 * 592 * But, SQL Server also pukes if you include an ORDER BY on a column that you 593 * aren't also SELECTing, if the select statement contains a UNION, INTERSECT or EXCEPT operator. 594 * Who knows why SQL Server is so picky.. but anyhow, this causes an issue, so we manually replace 595 * the pseudo-column with an actual selected column. 596 */ 597 if (theInputSql.contains("order by @@version")) { 598 if (theSelectedColumnOrNull != null) { 599 theInputSql = theInputSql.replace("order by @@version", "order by " + theSelectedColumnOrNull); 600 } else { 601 // not certain if this case can happen, but ordering by the ordinal first column should always 602 // be syntactically valid and seems like a better option than ordering by a static value 603 // regardless 604 theInputSql = theInputSql.replace("order by @@version", "order by 1"); 605 } 606 } 607 608 // The SQLServerDialect has a bunch of one-off processing to deal with rules on when 609 // a limit can be used, so we can't rely on the flags that the limithandler exposes since 610 // the exact structure of the query depends on the parameters 611 if (theInputSql.contains("top(?)")) { 612 theBindVariables.add(0, theMaxResultsToFetch); 613 } 614 if (theInputSql.contains("offset 0 rows fetch first ? rows only")) { 615 theBindVariables.add(theMaxResultsToFetch); 616 } 617 if (theInputSql.contains("offset ? rows fetch next ? rows only")) { 618 theBindVariables.add(theOffset); 619 theBindVariables.add(theMaxResultsToFetch); 620 } 621 if (theOffset != null && theInputSql.contains("rownumber_")) { 622 theBindVariables.add(theOffset + 1); 623 theBindVariables.add(theOffset + theMaxResultsToFetch + 1); 624 } 625 626 } else if (limitHandler.supportsVariableLimit()) { 627 628 boolean bindLimitParametersFirst = limitHandler.bindLimitParametersFirst(); 629 if (limitHandler.useMaxForLimit() && theOffset != null) { 630 theMaxResultsToFetch = theMaxResultsToFetch + theOffset; 631 } 632 633 if (limitHandler.bindLimitParametersInReverseOrder()) { 634 startOfQueryParameterIndex = bindCountParameter( 635 theBindVariables, 636 theMaxResultsToFetch, 637 limitHandler, 638 startOfQueryParameterIndex, 639 bindLimitParametersFirst); 640 bindOffsetParameter( 641 theBindVariables, 642 theOffset, 643 limitHandler, 644 startOfQueryParameterIndex, 645 bindLimitParametersFirst); 646 } else { 647 startOfQueryParameterIndex = bindOffsetParameter( 648 theBindVariables, 649 theOffset, 650 limitHandler, 651 startOfQueryParameterIndex, 652 bindLimitParametersFirst); 653 bindCountParameter( 654 theBindVariables, 655 theMaxResultsToFetch, 656 limitHandler, 657 startOfQueryParameterIndex, 658 bindLimitParametersFirst); 659 } 660 } 661 return theInputSql; 662 } 663 664 private static int bindCountParameter( 665 List<Object> bindVariables, 666 Integer maxResultsToFetch, 667 AbstractLimitHandler limitHandler, 668 int startOfQueryParameterIndex, 669 boolean bindLimitParametersFirst) { 670 if (limitHandler.supportsLimit()) { 671 if (bindLimitParametersFirst) { 672 bindVariables.add(startOfQueryParameterIndex++, maxResultsToFetch); 673 } else { 674 bindVariables.add(maxResultsToFetch); 675 } 676 } 677 return startOfQueryParameterIndex; 678 } 679 680 public static int bindOffsetParameter( 681 List<Object> theBindVariables, 682 @Nullable Integer theOffset, 683 AbstractLimitHandler theLimitHandler, 684 int theStartOfQueryParameterIndex, 685 boolean theBindLimitParametersFirst) { 686 if (theLimitHandler.supportsLimitOffset() && theOffset != null) { 687 if (theBindLimitParametersFirst) { 688 theBindVariables.add(theStartOfQueryParameterIndex++, theOffset); 689 } else { 690 theBindVariables.add(theOffset); 691 } 692 } 693 return theStartOfQueryParameterIndex; 694 } 695 696 /** 697 * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it. 698 */ 699 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() { 700 return getOrCreateFirstPredicateBuilder(true); 701 } 702 703 /** 704 * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it. 705 */ 706 public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder( 707 boolean theIncludeResourceTypeAndNonDeletedFlag) { 708 if (myFirstPredicateBuilder == null) { 709 getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag); 710 } 711 return myFirstPredicateBuilder; 712 } 713 714 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() { 715 return getOrCreateResourceTablePredicateBuilder(true); 716 } 717 718 public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder( 719 boolean theIncludeResourceTypeAndNonDeletedFlag) { 720 if (myResourceTableRoot == null) { 721 ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this); 722 addTable(resourceTable, null); 723 if (theIncludeResourceTypeAndNonDeletedFlag) { 724 Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates(); 725 addPredicate(typeAndDeletionPredicate); 726 } 727 myResourceTableRoot = resourceTable; 728 } 729 return myResourceTableRoot; 730 } 731 732 /** 733 * The SQL Builder library has one annoying limitation, which is that it does not use/understand bind variables 734 * for its generated SQL. So we work around this by replacing our contents with a string in the SQL consisting 735 * of <code>[random UUID]-[value index]</code> and then 736 */ 737 public String generatePlaceholder(Object theValue) { 738 String placeholder = myBindVariableSubstitutionBase + myBindVariableValues.size(); 739 myBindVariableValues.add(theValue); 740 return placeholder; 741 } 742 743 public List<String> generatePlaceholders(Collection<?> theValues) { 744 return theValues.stream().map(this::generatePlaceholder).collect(Collectors.toList()); 745 } 746 747 public int countBindVariables() { 748 return myBindVariableValues.size(); 749 } 750 751 public void setMatchNothing() { 752 myMatchNothing = true; 753 } 754 755 public DbTable addTable(String theTableName) { 756 return mySchema.addTable(theTableName); 757 } 758 759 public PartitionSettings getPartitionSettings() { 760 return myPartitionSettings; 761 } 762 763 public RequestPartitionId getRequestPartitionId() { 764 return myRequestPartitionId; 765 } 766 767 public String getResourceType() { 768 return myResourceType; 769 } 770 771 public StorageSettings getStorageSettings() { 772 return myStorageSettings; 773 } 774 775 public void addPredicate(@Nonnull Condition theCondition) { 776 assert theCondition != null; 777 mySelect.addCondition(theCondition); 778 myHaveAtLeastOnePredicate = true; 779 } 780 781 public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) { 782 ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false); 783 return addPredicateLastUpdated(theDateRange, resourceTableRoot); 784 } 785 786 public ComboCondition addPredicateLastUpdated( 787 DateRangeParam theDateRange, ResourceTablePredicateBuilder theResourceTablePredicateBuilder) { 788 List<Condition> conditions = new ArrayList<>(2); 789 BinaryCondition condition; 790 791 if (isNotEqualsComparator(theDateRange)) { 792 condition = createConditionForValueWithComparator( 793 LESSTHAN, 794 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 795 theDateRange.getLowerBoundAsInstant()); 796 conditions.add(condition); 797 condition = createConditionForValueWithComparator( 798 GREATERTHAN, 799 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 800 theDateRange.getUpperBoundAsInstant()); 801 conditions.add(condition); 802 return ComboCondition.or(conditions.toArray(new Condition[0])); 803 } 804 805 if (theDateRange.getLowerBoundAsInstant() != null) { 806 condition = createConditionForValueWithComparator( 807 GREATERTHAN_OR_EQUALS, 808 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 809 theDateRange.getLowerBoundAsInstant()); 810 conditions.add(condition); 811 } 812 813 if (theDateRange.getUpperBoundAsInstant() != null) { 814 condition = createConditionForValueWithComparator( 815 LESSTHAN_OR_EQUALS, 816 theResourceTablePredicateBuilder.getLastUpdatedColumn(), 817 theDateRange.getUpperBoundAsInstant()); 818 conditions.add(condition); 819 } 820 821 return ComboCondition.and(conditions.toArray(new Condition[0])); 822 } 823 824 private boolean isNotEqualsComparator(DateRangeParam theDateRange) { 825 if (theDateRange != null) { 826 DateParam lb = theDateRange.getLowerBound(); 827 DateParam ub = theDateRange.getUpperBound(); 828 829 return lb != null && ub != null && NOT_EQUAL.equals(lb.getPrefix()) && NOT_EQUAL.equals(ub.getPrefix()); 830 } 831 return false; 832 } 833 834 public void addResourceIdsPredicate(List<JpaPid> thePidList) { 835 List<Long> pidList = thePidList.stream().map(JpaPid::getId).collect(Collectors.toList()); 836 837 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 838 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(pidList)); 839 addPredicate(predicate); 840 } 841 842 public void excludeResourceIdsPredicate(Set<JpaPid> theExistingPidSetToExclude) { 843 844 // Do nothing if it's empty 845 if (theExistingPidSetToExclude == null || theExistingPidSetToExclude.isEmpty()) return; 846 847 List<Long> excludePids = JpaPid.toLongList(theExistingPidSetToExclude); 848 849 ourLog.trace("excludePids = {}", excludePids); 850 851 DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn(); 852 InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(excludePids)); 853 predicate.setNegate(true); 854 addPredicate(predicate); 855 } 856 857 public BinaryCondition createConditionForValueWithComparator( 858 ParamPrefixEnum theComparator, DbColumn theColumn, Object theValue) { 859 switch (theComparator) { 860 case LESSTHAN: 861 return BinaryCondition.lessThan(theColumn, generatePlaceholder(theValue)); 862 case LESSTHAN_OR_EQUALS: 863 return BinaryCondition.lessThanOrEq(theColumn, generatePlaceholder(theValue)); 864 case GREATERTHAN: 865 return BinaryCondition.greaterThan(theColumn, generatePlaceholder(theValue)); 866 case GREATERTHAN_OR_EQUALS: 867 return BinaryCondition.greaterThanOrEq(theColumn, generatePlaceholder(theValue)); 868 case NOT_EQUAL: 869 return BinaryCondition.notEqualTo(theColumn, generatePlaceholder(theValue)); 870 case EQUAL: 871 // NB: fhir searches are always range searches; 872 // which is why we do not use "EQUAL" 873 case STARTS_AFTER: 874 case APPROXIMATE: 875 case ENDS_BEFORE: 876 default: 877 throw new IllegalArgumentException(Msg.code(1263)); 878 } 879 } 880 881 public SearchQueryBuilder newChildSqlBuilder(boolean theSelectPartitionId) { 882 return new SearchQueryBuilder( 883 myFhirContext, 884 myStorageSettings, 885 myPartitionSettings, 886 myRequestPartitionId, 887 myResourceType, 888 mySqlBuilderFactory, 889 myBindVariableSubstitutionBase, 890 myDialect, 891 false, 892 myBindVariableValues, 893 theSelectPartitionId); 894 } 895 896 public SelectQuery getSelect() { 897 return mySelect; 898 } 899 900 public boolean haveAtLeastOnePredicate() { 901 return myHaveAtLeastOnePredicate; 902 } 903 904 public void addSortCoordsNear( 905 CoordsPredicateBuilder theCoordsBuilder, 906 double theLatitudeValue, 907 double theLongitudeValue, 908 boolean theAscending) { 909 FunctionCall absLatitude = new FunctionCall("ABS"); 910 String latitudePlaceholder = generatePlaceholder(theLatitudeValue); 911 ComboExpression absLatitudeMiddle = new ComboExpression( 912 ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLatitude(), latitudePlaceholder); 913 absLatitude = absLatitude.addCustomParams(absLatitudeMiddle); 914 915 FunctionCall absLongitude = new FunctionCall("ABS"); 916 String longitudePlaceholder = generatePlaceholder(theLongitudeValue); 917 ComboExpression absLongitudeMiddle = new ComboExpression( 918 ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLongitude(), longitudePlaceholder); 919 absLongitude = absLongitude.addCustomParams(absLongitudeMiddle); 920 921 ComboExpression sum = new ComboExpression(ComboExpression.Op.ADD, absLatitude, absLongitude); 922 String ordering; 923 if (theAscending) { 924 ordering = ""; 925 } else { 926 ordering = " DESC"; 927 } 928 929 String columnName = "MHD" + (myNextNearnessColumnId++); 930 mySelect.addAliasedColumn(sum, columnName); 931 mySelect.addCustomOrderings(columnName + ordering); 932 } 933 934 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending) { 935 addSortString(theColumnValueNormalized, theAscending, false); 936 } 937 938 public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 939 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 940 addSortString(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 941 } 942 943 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending) { 944 addSortNumeric(theColumnValueNormalized, theAscending, false); 945 } 946 947 public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 948 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 949 addSortNumeric(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 950 } 951 952 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending) { 953 addSortDate(theColumnValueNormalized, theAscending, false); 954 } 955 956 public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) { 957 OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST; 958 addSortDate(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate); 959 } 960 961 public void addSortString( 962 DbColumn theTheColumnValueNormalized, 963 boolean theTheAscending, 964 OrderObject.NullOrder theNullOrder, 965 boolean theUseAggregate) { 966 if ((dialectIsMySql || dialectIsMsSql)) { 967 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 968 String direction = theTheAscending ? " ASC" : " DESC"; 969 String sortColumnName = 970 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 971 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 972 // The following block has been commented out for performance. 973 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 974 /* 975 // Null values are always treated as less than non-null values. 976 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 977 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 978 // In this case, precede the "order by" column with a case statement that returns 979 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 980 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 981 } 982 */ 983 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 984 sortColumnNameBuilder.append(sortColumnName).append(direction); 985 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 986 } else { 987 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 988 } 989 } 990 991 private static String formatColumnNameForAggregate( 992 boolean theTheAscending, boolean theUseAggregate, String sortColumnName) { 993 if (theUseAggregate) { 994 String aggregateFunction; 995 if (theTheAscending) { 996 aggregateFunction = "MIN"; 997 } else { 998 aggregateFunction = "MAX"; 999 } 1000 sortColumnName = aggregateFunction + "(" + sortColumnName + ")"; 1001 } 1002 return sortColumnName; 1003 } 1004 1005 public void addSortNumeric( 1006 DbColumn theTheColumnValueNormalized, 1007 boolean theAscending, 1008 OrderObject.NullOrder theNullOrder, 1009 boolean theUseAggregate) { 1010 if ((dialectIsMySql || dialectIsMsSql)) { 1011 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 1012 // Null values are always treated as less than non-null values. 1013 // As such special handling is required here. 1014 String direction; 1015 String sortColumnName = 1016 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 1017 if ((theAscending && theNullOrder == OrderObject.NullOrder.LAST) 1018 || (!theAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 1019 // Negating the numeric column value and reversing the sort order will ensure that the rows appear 1020 // in the correct order with nulls appearing first or last as needed. 1021 direction = theAscending ? " DESC" : " ASC"; 1022 sortColumnName = "-" + sortColumnName; 1023 } else { 1024 direction = theAscending ? " ASC" : " DESC"; 1025 } 1026 sortColumnName = formatColumnNameForAggregate(theAscending, theUseAggregate, sortColumnName); 1027 mySelect.addCustomOrderings(sortColumnName + direction); 1028 } else { 1029 addSort(theTheColumnValueNormalized, theAscending, theNullOrder, theUseAggregate); 1030 } 1031 } 1032 1033 public void addSortDate( 1034 DbColumn theTheColumnValueNormalized, 1035 boolean theTheAscending, 1036 OrderObject.NullOrder theNullOrder, 1037 boolean theUseAggregate) { 1038 if ((dialectIsMySql || dialectIsMsSql)) { 1039 // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax. 1040 String direction = theTheAscending ? " ASC" : " DESC"; 1041 String sortColumnName = 1042 theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName(); 1043 final StringBuilder sortColumnNameBuilder = new StringBuilder(); 1044 // The following block has been commented out for performance. 1045 // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL 1046 /* 1047 // Null values are always treated as less than non-null values. 1048 if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST) 1049 || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) { 1050 // In this case, precede the "order by" column with a case statement that returns 1051 // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls. 1052 sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", "); 1053 } 1054 */ 1055 sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName); 1056 sortColumnNameBuilder.append(sortColumnName).append(direction); 1057 mySelect.addCustomOrderings(sortColumnNameBuilder.toString()); 1058 } else { 1059 addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate); 1060 } 1061 } 1062 1063 private void addSort( 1064 DbColumn theTheColumnValueNormalized, 1065 boolean theTheAscending, 1066 OrderObject.NullOrder theNullOrder, 1067 boolean theUseAggregate) { 1068 OrderObject.Dir direction = theTheAscending ? OrderObject.Dir.ASCENDING : OrderObject.Dir.DESCENDING; 1069 Object columnToOrder = theTheColumnValueNormalized; 1070 if (theUseAggregate) { 1071 if (theTheAscending) { 1072 columnToOrder = FunctionCall.min().addColumnParams(theTheColumnValueNormalized); 1073 } else { 1074 columnToOrder = FunctionCall.max().addColumnParams(theTheColumnValueNormalized); 1075 } 1076 } 1077 OrderObject orderObject = new OrderObject(direction, columnToOrder); 1078 orderObject.setNullOrder(theNullOrder); 1079 mySelect.addCustomOrderings(orderObject); 1080 } 1081 1082 /** 1083 * If set to true (default is false), force the generated SQL to start 1084 * with the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE} 1085 * table at the root of the query. 1086 * <p> 1087 * This seems to perform better if there are multiple joins on the 1088 * resource ID table. 1089 */ 1090 public void setNeedResourceTableRoot(boolean theNeedResourceTableRoot) { 1091 myNeedResourceTableRoot = theNeedResourceTableRoot; 1092 } 1093}