You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

QueryImpl.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. /*
  2. Copyright (c) 2008 Health Market Science, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package com.healthmarketscience.jackcess.impl.query;
  14. import java.util.ArrayList;
  15. import java.util.Arrays;
  16. import java.util.Collection;
  17. import java.util.Iterator;
  18. import java.util.LinkedHashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import com.healthmarketscience.jackcess.RowId;
  22. import com.healthmarketscience.jackcess.impl.DatabaseImpl;
  23. import com.healthmarketscience.jackcess.impl.RowIdImpl;
  24. import com.healthmarketscience.jackcess.impl.RowImpl;
  25. import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*;
  26. import com.healthmarketscience.jackcess.query.Query;
  27. import org.apache.commons.lang.builder.ToStringBuilder;
  28. import org.apache.commons.logging.Log;
  29. import org.apache.commons.logging.LogFactory;
  30. /**
  31. * Base class for classes which encapsulate information about an Access query.
  32. * The {@link #toSQLString()} method can be used to convert this object into
  33. * the actual SQL string which this query data represents.
  34. *
  35. * @author James Ahlborn
  36. */
  37. public abstract class QueryImpl implements Query
  38. {
  39. protected static final Log LOG = LogFactory.getLog(QueryImpl.class);
  40. private static final Row EMPTY_ROW = new Row();
  41. private final String _name;
  42. private final List<Row> _rows;
  43. private final int _objectId;
  44. private final Type _type;
  45. private final int _objectFlag;
  46. protected QueryImpl(String name, List<Row> rows, int objectId, int objectFlag,
  47. Type type)
  48. {
  49. _name = name;
  50. _rows = rows;
  51. _objectId = objectId;
  52. _type = type;
  53. _objectFlag = objectFlag;
  54. if(type != Type.UNKNOWN) {
  55. short foundType = getShortValue(getQueryType(rows),
  56. _type.getValue());
  57. if(foundType != _type.getValue()) {
  58. throw new IllegalStateException(withErrorContext(
  59. "Unexpected query type " + foundType));
  60. }
  61. }
  62. }
  63. /**
  64. * Returns the name of the query.
  65. */
  66. public String getName() {
  67. return _name;
  68. }
  69. /**
  70. * Returns the type of the query.
  71. */
  72. public Type getType() {
  73. return _type;
  74. }
  75. public boolean isHidden() {
  76. return((_objectFlag & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0);
  77. }
  78. /**
  79. * Returns the unique object id of the query.
  80. */
  81. public int getObjectId() {
  82. return _objectId;
  83. }
  84. public int getObjectFlag() {
  85. return _objectFlag;
  86. }
  87. /**
  88. * Returns the rows from the system query table from which the query
  89. * information was derived.
  90. */
  91. public List<Row> getRows() {
  92. return _rows;
  93. }
  94. protected List<Row> getRowsByAttribute(Byte attribute) {
  95. return getRowsByAttribute(getRows(), attribute);
  96. }
  97. protected Row getRowByAttribute(Byte attribute) {
  98. return getUniqueRow(getRowsByAttribute(getRows(), attribute));
  99. }
  100. public Row getTypeRow() {
  101. return getRowByAttribute(TYPE_ATTRIBUTE);
  102. }
  103. protected List<Row> getParameterRows() {
  104. return getRowsByAttribute(PARAMETER_ATTRIBUTE);
  105. }
  106. protected Row getFlagRow() {
  107. return getRowByAttribute(FLAG_ATTRIBUTE);
  108. }
  109. protected Row getRemoteDatabaseRow() {
  110. return getRowByAttribute(REMOTEDB_ATTRIBUTE);
  111. }
  112. protected List<Row> getTableRows() {
  113. return getRowsByAttribute(TABLE_ATTRIBUTE);
  114. }
  115. protected List<Row> getColumnRows() {
  116. return getRowsByAttribute(COLUMN_ATTRIBUTE);
  117. }
  118. protected List<Row> getJoinRows() {
  119. return getRowsByAttribute(JOIN_ATTRIBUTE);
  120. }
  121. protected Row getWhereRow() {
  122. return getRowByAttribute(WHERE_ATTRIBUTE);
  123. }
  124. protected List<Row> getGroupByRows() {
  125. return getRowsByAttribute(GROUPBY_ATTRIBUTE);
  126. }
  127. protected Row getHavingRow() {
  128. return getRowByAttribute(HAVING_ATTRIBUTE);
  129. }
  130. protected List<Row> getOrderByRows() {
  131. return getRowsByAttribute(ORDERBY_ATTRIBUTE);
  132. }
  133. protected abstract void toSQLString(StringBuilder builder);
  134. protected void toSQLParameterString(StringBuilder builder) {
  135. // handle any parameters
  136. List<String> params = getParameters();
  137. if(!params.isEmpty()) {
  138. builder.append("PARAMETERS ").append(params)
  139. .append(';').append(NEWLINE);
  140. }
  141. }
  142. public List<String> getParameters()
  143. {
  144. return (new RowFormatter(getParameterRows()) {
  145. @Override protected void format(StringBuilder builder, Row row) {
  146. String typeName = PARAM_TYPE_MAP.get(row.flag);
  147. if(typeName == null) {
  148. throw new IllegalStateException(withErrorContext(
  149. "Unknown param type " + row.flag));
  150. }
  151. builder.append(row.name1).append(' ').append(typeName);
  152. if((TEXT_FLAG.equals(row.flag)) && (getIntValue(row.extra, 0) > 0)) {
  153. builder.append('(').append(row.extra).append(')');
  154. }
  155. }
  156. }).format();
  157. }
  158. protected List<String> getFromTables()
  159. {
  160. List<Join> joinExprs = new ArrayList<Join>();
  161. for(Row table : getTableRows()) {
  162. StringBuilder builder = new StringBuilder();
  163. if(table.expression != null) {
  164. toQuotedExpr(builder, table.expression).append(IDENTIFIER_SEP_CHAR);
  165. }
  166. if(table.name1 != null) {
  167. toOptionalQuotedExpr(builder, table.name1, true);
  168. }
  169. toAlias(builder, table.name2);
  170. String key = ((table.name2 != null) ? table.name2 : table.name1);
  171. joinExprs.add(new Join(key, builder.toString()));
  172. }
  173. List<Row> joins = getJoinRows();
  174. if(!joins.isEmpty()) {
  175. // combine any multi-column joins
  176. Collection<List<Row>> comboJoins = combineJoins(joins);
  177. for(List<Row> comboJoin : comboJoins) {
  178. Row join = comboJoin.get(0);
  179. String joinExpr = join.expression;
  180. if(comboJoin.size() > 1) {
  181. // combine all the join expressions with "AND"
  182. AppendableList<String> comboExprs = new AppendableList<String>() {
  183. private static final long serialVersionUID = 0L;
  184. @Override
  185. protected String getSeparator() {
  186. return ") AND (";
  187. }
  188. };
  189. for(Row tmpJoin : comboJoin) {
  190. comboExprs.add(tmpJoin.expression);
  191. }
  192. joinExpr = new StringBuilder().append("(")
  193. .append(comboExprs).append(")").toString();
  194. }
  195. String fromTable = join.name1;
  196. String toTable = join.name2;
  197. Join fromExpr = getJoinExpr(fromTable, joinExprs);
  198. Join toExpr = getJoinExpr(toTable, joinExprs);
  199. String joinType = JOIN_TYPE_MAP.get(join.flag);
  200. if(joinType == null) {
  201. throw new IllegalStateException(withErrorContext(
  202. "Unknown join type " + join.flag));
  203. }
  204. String expr = new StringBuilder().append(fromExpr)
  205. .append(joinType).append(toExpr).append(" ON ")
  206. .append(joinExpr).toString();
  207. fromExpr.join(toExpr, expr);
  208. joinExprs.add(fromExpr);
  209. }
  210. }
  211. List<String> result = new AppendableList<String>();
  212. for(Join joinExpr : joinExprs) {
  213. result.add(joinExpr.expression);
  214. }
  215. return result;
  216. }
  217. private Join getJoinExpr(String table, List<Join> joinExprs)
  218. {
  219. for(Iterator<Join> iter = joinExprs.iterator(); iter.hasNext(); ) {
  220. Join joinExpr = iter.next();
  221. if(joinExpr.tables.contains(table)) {
  222. iter.remove();
  223. return joinExpr;
  224. }
  225. }
  226. throw new IllegalStateException(withErrorContext(
  227. "Cannot find join table " + table));
  228. }
  229. private Collection<List<Row>> combineJoins(List<Row> joins)
  230. {
  231. // combine joins with the same to/from tables
  232. Map<List<String>,List<Row>> comboJoinMap =
  233. new LinkedHashMap<List<String>,List<Row>>();
  234. for(Row join : joins) {
  235. List<String> key = Arrays.asList(join.name1, join.name2);
  236. List<Row> comboJoins = comboJoinMap.get(key);
  237. if(comboJoins == null) {
  238. comboJoins = new ArrayList<Row>();
  239. comboJoinMap.put(key, comboJoins);
  240. } else {
  241. if(comboJoins.get(0).flag != (short)join.flag) {
  242. throw new IllegalStateException(withErrorContext(
  243. "Mismatched join flags for combo joins"));
  244. }
  245. }
  246. comboJoins.add(join);
  247. }
  248. return comboJoinMap.values();
  249. }
  250. protected String getFromRemoteDbPath()
  251. {
  252. return getRemoteDatabaseRow().name1;
  253. }
  254. protected String getFromRemoteDbType()
  255. {
  256. return getRemoteDatabaseRow().expression;
  257. }
  258. protected String getWhereExpression()
  259. {
  260. return getWhereRow().expression;
  261. }
  262. protected List<String> getOrderings()
  263. {
  264. return (new RowFormatter(getOrderByRows()) {
  265. @Override protected void format(StringBuilder builder, Row row) {
  266. builder.append(row.expression);
  267. if(DESCENDING_FLAG.equalsIgnoreCase(row.name1)) {
  268. builder.append(" DESC");
  269. }
  270. }
  271. }).format();
  272. }
  273. public String getOwnerAccessType() {
  274. return(hasFlag(OWNER_ACCESS_SELECT_TYPE) ?
  275. "WITH OWNERACCESS OPTION" : DEFAULT_TYPE);
  276. }
  277. protected boolean hasFlag(int flagMask)
  278. {
  279. return hasFlag(getFlagRow(), flagMask);
  280. }
  281. protected boolean supportsStandardClauses() {
  282. return true;
  283. }
  284. /**
  285. * Returns the actual SQL string which this query data represents.
  286. */
  287. public String toSQLString()
  288. {
  289. StringBuilder builder = new StringBuilder();
  290. if(supportsStandardClauses()) {
  291. toSQLParameterString(builder);
  292. }
  293. toSQLString(builder);
  294. if(supportsStandardClauses()) {
  295. String accessType = getOwnerAccessType();
  296. if(!DEFAULT_TYPE.equals(accessType)) {
  297. builder.append(NEWLINE).append(accessType);
  298. }
  299. builder.append(';');
  300. }
  301. return builder.toString();
  302. }
  303. @Override
  304. public String toString() {
  305. return ToStringBuilder.reflectionToString(this);
  306. }
  307. /**
  308. * Creates a concrete Query instance from the given query data.
  309. *
  310. * @param objectFlag the flag indicating the type of the query
  311. * @param name the name of the query
  312. * @param rows the rows from the system query table containing the data
  313. * describing this query
  314. * @param objectId the unique object id of this query
  315. *
  316. * @return a Query instance for the given query data
  317. */
  318. public static QueryImpl create(int objectFlag, String name, List<Row> rows,
  319. int objectId)
  320. {
  321. // remove other object flags before testing for query type
  322. int typeFlag = objectFlag & OBJECT_FLAG_MASK;
  323. try {
  324. switch(typeFlag) {
  325. case SELECT_QUERY_OBJECT_FLAG:
  326. return new SelectQueryImpl(name, rows, objectId, objectFlag);
  327. case MAKE_TABLE_QUERY_OBJECT_FLAG:
  328. return new MakeTableQueryImpl(name, rows, objectId, objectFlag);
  329. case APPEND_QUERY_OBJECT_FLAG:
  330. return new AppendQueryImpl(name, rows, objectId, objectFlag);
  331. case UPDATE_QUERY_OBJECT_FLAG:
  332. return new UpdateQueryImpl(name, rows, objectId, objectFlag);
  333. case DELETE_QUERY_OBJECT_FLAG:
  334. return new DeleteQueryImpl(name, rows, objectId, objectFlag);
  335. case CROSS_TAB_QUERY_OBJECT_FLAG:
  336. return new CrossTabQueryImpl(name, rows, objectId, objectFlag);
  337. case DATA_DEF_QUERY_OBJECT_FLAG:
  338. return new DataDefinitionQueryImpl(name, rows, objectId, objectFlag);
  339. case PASSTHROUGH_QUERY_OBJECT_FLAG:
  340. return new PassthroughQueryImpl(name, rows, objectId, objectFlag);
  341. case UNION_QUERY_OBJECT_FLAG:
  342. return new UnionQueryImpl(name, rows, objectId, objectFlag);
  343. default:
  344. // unknown querytype
  345. throw new IllegalStateException(withErrorContext(
  346. "unknown query object flag " + typeFlag, name));
  347. }
  348. } catch(IllegalStateException e) {
  349. LOG.warn(withErrorContext("Failed parsing query", name), e);
  350. }
  351. // return unknown query
  352. return new UnknownQueryImpl(name, rows, objectId, objectFlag);
  353. }
  354. private Short getQueryType(List<Row> rows)
  355. {
  356. return getUniqueRow(getRowsByAttribute(rows, TYPE_ATTRIBUTE)).flag;
  357. }
  358. private static List<Row> getRowsByAttribute(List<Row> rows, Byte attribute) {
  359. List<Row> result = new ArrayList<Row>();
  360. for(Row row : rows) {
  361. if(attribute.equals(row.attribute)) {
  362. result.add(row);
  363. }
  364. }
  365. return result;
  366. }
  367. protected Row getUniqueRow(List<Row> rows) {
  368. if(rows.size() == 1) {
  369. return rows.get(0);
  370. }
  371. if(rows.isEmpty()) {
  372. return EMPTY_ROW;
  373. }
  374. throw new IllegalStateException(withErrorContext(
  375. "Unexpected number of rows for" + rows));
  376. }
  377. protected static List<Row> filterRowsByFlag(
  378. List<Row> rows, final short flag)
  379. {
  380. return new RowFilter() {
  381. @Override protected boolean keep(Row row) {
  382. return hasFlag(row, flag);
  383. }
  384. }.filter(rows);
  385. }
  386. protected static List<Row> filterRowsByNotFlag(
  387. List<Row> rows, final short flag)
  388. {
  389. return new RowFilter() {
  390. @Override protected boolean keep(Row row) {
  391. return !hasFlag(row, flag);
  392. }
  393. }.filter(rows);
  394. }
  395. protected static boolean hasFlag(Row row, int flagMask)
  396. {
  397. return((getShortValue(row.flag, 0) & flagMask) != 0);
  398. }
  399. protected static short getShortValue(Short s, int def) {
  400. return ((s != null) ? (short)s : (short)def);
  401. }
  402. protected static int getIntValue(Integer i, int def) {
  403. return ((i != null) ? (int)i : def);
  404. }
  405. protected static StringBuilder toOptionalQuotedExpr(StringBuilder builder,
  406. String fullExpr,
  407. boolean isIdentifier)
  408. {
  409. String[] exprs = (isIdentifier ?
  410. IDENTIFIER_SEP_PAT.split(fullExpr) :
  411. new String[]{fullExpr});
  412. for(int i = 0; i < exprs.length; ++i) {
  413. String expr = exprs[i];
  414. if(QUOTABLE_CHAR_PAT.matcher(expr).find()) {
  415. toQuotedExpr(builder, expr);
  416. } else {
  417. builder.append(expr);
  418. }
  419. if(i < (exprs.length - 1)) {
  420. builder.append(IDENTIFIER_SEP_CHAR);
  421. }
  422. }
  423. return builder;
  424. }
  425. protected static StringBuilder toQuotedExpr(StringBuilder builder,
  426. String expr)
  427. {
  428. return (!isQuoted(expr) ?
  429. builder.append('[').append(expr).append(']') :
  430. builder.append(expr));
  431. }
  432. protected static boolean isQuoted(String expr) {
  433. return ((expr.length() >= 2) &&
  434. (expr.charAt(0) == '[') && (expr.charAt(expr.length() - 1) == ']'));
  435. }
  436. protected static StringBuilder toRemoteDb(StringBuilder builder,
  437. String remoteDbPath,
  438. String remoteDbType) {
  439. if((remoteDbPath != null) || (remoteDbType != null)) {
  440. // note, always include path string, even if empty
  441. builder.append(" IN '");
  442. if(remoteDbPath != null) {
  443. builder.append(remoteDbPath);
  444. }
  445. builder.append('\'');
  446. if(remoteDbType != null) {
  447. builder.append(" [").append(remoteDbType).append(']');
  448. }
  449. }
  450. return builder;
  451. }
  452. protected static StringBuilder toAlias(StringBuilder builder,
  453. String alias) {
  454. if(alias != null) {
  455. toOptionalQuotedExpr(builder.append(" AS "), alias, false);
  456. }
  457. return builder;
  458. }
  459. private String withErrorContext(String msg) {
  460. return withErrorContext(msg, getName());
  461. }
  462. private static String withErrorContext(String msg, String queryName) {
  463. return msg + " (Query: " + queryName + ")";
  464. }
  465. private static final class UnknownQueryImpl extends QueryImpl
  466. {
  467. private UnknownQueryImpl(String name, List<Row> rows, int objectId,
  468. int objectFlag)
  469. {
  470. super(name, rows, objectId, objectFlag, Type.UNKNOWN);
  471. }
  472. @Override
  473. protected void toSQLString(StringBuilder builder) {
  474. throw new UnsupportedOperationException();
  475. }
  476. }
  477. /**
  478. * Struct containing the information from a single row of the system query
  479. * table.
  480. */
  481. public static final class Row
  482. {
  483. private final RowId _id;
  484. public final Byte attribute;
  485. public final String expression;
  486. public final Short flag;
  487. public final Integer extra;
  488. public final String name1;
  489. public final String name2;
  490. public final Integer objectId;
  491. public final byte[] order;
  492. private Row() {
  493. this._id = null;
  494. this.attribute = null;
  495. this.expression = null;
  496. this.flag = null;
  497. this.extra = null;
  498. this.name1 = null;
  499. this.name2= null;
  500. this.objectId = null;
  501. this.order = null;
  502. }
  503. public Row(com.healthmarketscience.jackcess.Row tableRow) {
  504. this(tableRow.getId(),
  505. tableRow.getByte(COL_ATTRIBUTE),
  506. tableRow.getString(COL_EXPRESSION),
  507. tableRow.getShort(COL_FLAG),
  508. tableRow.getInt(COL_EXTRA),
  509. tableRow.getString(COL_NAME1),
  510. tableRow.getString(COL_NAME2),
  511. tableRow.getInt(COL_OBJECTID),
  512. tableRow.getBytes(COL_ORDER));
  513. }
  514. public Row(RowId id, Byte attribute, String expression, Short flag,
  515. Integer extra, String name1, String name2,
  516. Integer objectId, byte[] order)
  517. {
  518. this._id = id;
  519. this.attribute = attribute;
  520. this.expression = expression;
  521. this.flag = flag;
  522. this.extra = extra;
  523. this.name1 = name1;
  524. this.name2= name2;
  525. this.objectId = objectId;
  526. this.order = order;
  527. }
  528. public com.healthmarketscience.jackcess.Row toTableRow()
  529. {
  530. com.healthmarketscience.jackcess.Row tableRow = new RowImpl((RowIdImpl)_id);
  531. tableRow.put(COL_ATTRIBUTE, attribute);
  532. tableRow.put(COL_EXPRESSION, expression);
  533. tableRow.put(COL_FLAG, flag);
  534. tableRow.put(COL_EXTRA, extra);
  535. tableRow.put(COL_NAME1, name1);
  536. tableRow.put(COL_NAME2, name2);
  537. tableRow.put(COL_OBJECTID, objectId);
  538. tableRow.put(COL_ORDER, order);
  539. return tableRow;
  540. }
  541. @Override
  542. public String toString() {
  543. return ToStringBuilder.reflectionToString(this);
  544. }
  545. }
  546. protected static abstract class RowFormatter
  547. {
  548. private final List<Row> _list;
  549. protected RowFormatter(List<Row> list) {
  550. _list = list;
  551. }
  552. public List<String> format() {
  553. return format(new AppendableList<String>());
  554. }
  555. public List<String> format(List<String> strs) {
  556. for(Row row : _list) {
  557. StringBuilder builder = new StringBuilder();
  558. format(builder, row);
  559. strs.add(builder.toString());
  560. }
  561. return strs;
  562. }
  563. protected abstract void format(StringBuilder builder, Row row);
  564. }
  565. protected static abstract class RowFilter
  566. {
  567. protected RowFilter() {
  568. }
  569. public List<Row> filter(List<Row> list) {
  570. for(Iterator<Row> iter = list.iterator(); iter.hasNext(); ) {
  571. if(!keep(iter.next())) {
  572. iter.remove();
  573. }
  574. }
  575. return list;
  576. }
  577. protected abstract boolean keep(Row row);
  578. }
  579. protected static class AppendableList<E> extends ArrayList<E>
  580. {
  581. private static final long serialVersionUID = 0L;
  582. protected AppendableList() {
  583. }
  584. protected AppendableList(Collection<? extends E> c) {
  585. super(c);
  586. }
  587. protected String getSeparator() {
  588. return ", ";
  589. }
  590. @Override
  591. public String toString() {
  592. StringBuilder builder = new StringBuilder();
  593. for(Iterator<E> iter = iterator(); iter.hasNext(); ) {
  594. builder.append(iter.next().toString());
  595. if(iter.hasNext()) {
  596. builder.append(getSeparator());
  597. }
  598. }
  599. return builder.toString();
  600. }
  601. }
  602. private static final class Join
  603. {
  604. public final List<String> tables = new ArrayList<String>();
  605. public boolean isJoin;
  606. public String expression;
  607. private Join(String table, String expr) {
  608. tables.add(table);
  609. expression = expr;
  610. }
  611. public void join(Join other, String newExpr) {
  612. tables.addAll(other.tables);
  613. isJoin = true;
  614. expression = newExpr;
  615. }
  616. @Override
  617. public String toString() {
  618. return (isJoin ? ("(" + expression + ")") : expression);
  619. }
  620. }
  621. }