Use of jOOQ: Move Field creation into a utility method.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / SqlUtils.java
1 /*
2  * Copyright (C) 2012 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.server;
21
22 import java.sql.Connection;
23 import java.util.ArrayList;
24 import java.util.List;
25
26 import org.apache.commons.lang3.StringUtils;
27 import org.glom.libglom.Document;
28 import org.glom.libglom.Field;
29 import org.glom.libglom.FieldVector;
30 import org.glom.libglom.LayoutFieldVector;
31 import org.glom.libglom.LayoutItem_Field;
32 import org.glom.libglom.Relationship;
33 import org.glom.libglom.SortClause;
34 import org.glom.libglom.SortFieldPair;
35 import org.glom.libglom.Value;
36 import org.jooq.AggregateFunction;
37 import org.jooq.Condition;
38 import org.jooq.SQLDialect;
39 import org.jooq.SelectFinalStep;
40 import org.jooq.SelectJoinStep;
41 import org.jooq.SelectSelectStep;
42 import org.jooq.conf.RenderKeywordStyle;
43 import org.jooq.conf.RenderNameStyle;
44 import org.jooq.conf.Settings;
45 import org.jooq.impl.Factory;
46
47 /**
48  * @author Ben Konrath <ben@bagu.org>
49  * 
50  */
51 public class SqlUtils {
52
53         public static class UsesRelationship {
54
55                 private Relationship relationship;
56                 private Relationship relatedRelationship;
57
58                 public void setRelationship(final Relationship relationship) {
59                         this.relationship = relationship;
60                 }
61
62                 /**
63                  * @param get_related_relationship
64                  */
65                 public void setRelatedRelationship(final Relationship relationship) {
66                         this.relatedRelationship = relationship;
67                 }
68
69                 public Relationship getRelationship() {
70                         return relationship;
71                 }
72
73                 public Relationship getRelatedRelationship() {
74                         return relatedRelationship;
75                 }
76
77                 private boolean getHasRelationshipName() {
78                         if (relationship == null) {
79                                 return false;
80                         }
81
82                         if (StringUtils.isEmpty(relationship.get_name())) {
83                                 return false;
84                         }
85
86                         return true;
87                 }
88
89                 private boolean getHasRelatedRelationshipName() {
90                         if (relatedRelationship == null) {
91                                 return false;
92                         }
93
94                         if (StringUtils.isEmpty(relatedRelationship.get_name())) {
95                                 return false;
96                         }
97
98                         return true;
99                 }
100
101                 public String get_sql_join_alias_name() {
102                         String result = "";
103
104                         if (getHasRelationshipName() && relationship.get_has_fields()) // relationships that link to tables together
105                                                                                                                                                         // via a field
106                         {
107                                 // We use relationship_name.field_name instead of related_tableName.field_name,
108                                 // because, in the JOIN below, will specify the relationship_name as an alias for the related table name
109                                 result += ("relationship_" + relationship.get_name());
110
111                                 if (getHasRelatedRelationshipName() && relatedRelationship.get_has_fields()) {
112                                         result += ('_' + relatedRelationship.get_name());
113                                 }
114                         }
115
116                         return result;
117                 }
118
119                 /*
120                  * (non-Javadoc)
121                  * 
122                  * @see java.lang.Object#hashCode()
123                  */
124                 /*
125                  * @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result +
126                  * ((relatedRelationship == null) ? 0 : relatedRelationship.hashCode()); result = prime * result +
127                  * ((relationship == null) ? 0 : relationship.hashCode()); return result; }
128                  */
129
130                 /*
131                  * (non-Javadoc)
132                  * 
133                  * @see java.lang.Object#equals(java.lang.Object)
134                  * 
135                  * 
136                  * TODO: This causes NullPointerExceptions when used from contains().
137                  */
138                 @Override
139                 public boolean equals(final Object obj) {
140                         if (this == obj) {
141                                 return true;
142                         }
143
144                         if (obj == null) {
145                                 return false;
146                         }
147
148                         if (!(obj instanceof UsesRelationship)) {
149                                 return false;
150                         }
151
152                         final UsesRelationship other = (UsesRelationship) obj;
153                         if (relationship == null) {
154                                 if (other.relationship != null) {
155                                         return false;
156                                 }
157                         } else if (!relationship_equals(relationship, other.relationship)) {
158                                 return false;
159                         }
160
161                         if (relatedRelationship == null) {
162                                 if (other.relatedRelationship != null) {
163                                         return false;
164                                 }
165                         } else if (!relationship_equals(relatedRelationship, other.relatedRelationship)) {
166                                 return false;
167                         }
168
169                         return true;
170                 }
171
172                 /**
173                  * We use this utility function because Relationship.equals() fails in the the generated SWIG C++ code with a
174                  * NullPointerException.
175                  */
176                 public static boolean relationship_equals(final Relationship a, final Relationship b) {
177                         if (a == null) {
178                                 if (b == null) {
179                                         return true;
180                                 } else {
181                                         return false;
182                                 }
183                         }
184
185                         if (b == null) {
186                                 return false;
187                         }
188
189                         final String a_name = a.get_name();
190                         final String b_name = b.get_name();
191
192                         if (!StringUtils.equals(a_name, b_name)) { // TODO: And the rest.
193                                 return false;
194                         }
195
196                         return true;
197                 }
198         }
199
200         // TODO: Change to final ArrayList<LayoutItem_Field> fieldsToGet
201         public static String build_sql_select_with_key(final Connection connection, final String tableName,
202                         final LayoutFieldVector fieldsToGet, final Field primaryKey, final Value gdaPrimaryKeyValue) {
203
204                 Condition whereClause = null; // Note that we ignore quickFind.
205                 if (gdaPrimaryKeyValue != null) {
206                         whereClause = build_simple_where_expression(tableName, primaryKey, gdaPrimaryKeyValue);
207                 }
208
209                 final SortClause sortClause = null; // Ignored.
210                 return build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause, sortClause);
211         }
212
213         public static Condition build_simple_where_expression(final String tableName, final Field primaryKey,
214                         final Value gdaPrimaryKeyValue) {
215
216                 Condition result = null;
217
218                 if (primaryKey == null) {
219                         return result;
220                 }
221
222                 final String fieldName = primaryKey.get_name();
223                 if (StringUtils.isEmpty(fieldName)) {
224                         return result;
225                 }
226
227                 final org.jooq.Field<Object> field = createField(tableName, fieldName);
228                 result = field.equal(gdaPrimaryKeyValue.get_double()); // TODO: Handle other types too.
229                 return result;
230         }
231
232         /*
233          * private static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
234          * final LayoutFieldVector fieldsToGet) { final Condition whereClause = null; return
235          * build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause); }
236          */
237
238         /*
239          * private static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
240          * final LayoutFieldVector fieldsToGet, final Condition whereClause) { final SortClause sortClause = null; return
241          * build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause, sortClause); }
242          */
243
244         public static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
245                         final LayoutFieldVector fieldsToGet, final Condition whereClause, final SortClause sortClause) {
246                 final SelectFinalStep step = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
247                                 whereClause, sortClause);
248                 if (step == null) {
249                         return "";
250                 }
251
252                 final String query = step.getQuery().getSQL(true);
253                 // Log.info("Query: " + query);
254                 return query;
255         }
256
257         private static SelectSelectStep createSelect(final Connection connection) {
258                 final Factory factory = new Factory(connection, SQLDialect.POSTGRES);
259                 final Settings settings = factory.getSettings();
260                 settings.setRenderNameStyle(RenderNameStyle.QUOTED); // TODO: This doesn't seem to have any effect.
261                 settings.setRenderKeywordStyle(RenderKeywordStyle.UPPER); // TODO: Just to make debugging nicer.
262
263                 final SelectSelectStep selectStep = factory.select();
264                 return selectStep;
265         }
266
267         private static SelectFinalStep build_sql_select_step_with_where_clause(final Connection connection,
268                         final String tableName, final LayoutFieldVector fieldsToGet, final Condition whereClause,
269                         final SortClause sortClause) {
270
271                 final SelectSelectStep selectStep = createSelect(connection);
272
273                 // Add the fields, and any necessary joins:
274                 final List<UsesRelationship> listRelationships = build_sql_select_add_fields_to_get(selectStep, tableName,
275                                 fieldsToGet, sortClause, false /* extraJoin */);
276
277                 final SelectJoinStep joinStep = selectStep.from(tableName);
278
279                 // LEFT OUTER JOIN will get the field values from the other tables,
280                 // and give us our fields for this table even if there is no corresponding value in the other table.
281                 for (final UsesRelationship usesRelationship : listRelationships) {
282                         builder_add_join(joinStep, usesRelationship);
283                 }
284
285                 SelectFinalStep finalStep = joinStep;
286                 if (whereClause != null) {
287                         finalStep = joinStep.where(whereClause);
288                 }
289
290                 return finalStep;
291         }
292
293         public static String build_sql_count_select_with_where_clause(final Connection connection, final String tableName,
294                         final LayoutFieldVector fieldsToGet) {
295                 final SelectFinalStep selectInner = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
296                                 null, null);
297                 return build_sql_select_count_rows(connection, selectInner);
298         }
299
300         public static String build_sql_count_select_with_where_clause(final Connection connection, final String tableName,
301                         final LayoutFieldVector fieldsToGet, final Condition whereClause) {
302                 final SelectFinalStep selectInner = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
303                                 whereClause, null);
304                 return build_sql_select_count_rows(connection, selectInner);
305         }
306
307         private static String build_sql_select_count_rows(final Connection connection, final SelectFinalStep selectInner) {
308                 // TODO: Find a way to do this with the jOOQ API:
309                 final SelectSelectStep select = createSelect(connection);
310
311                 final org.jooq.Field<?> field = Factory.field("*");
312                 final AggregateFunction<?> count = Factory.count(field);
313                 select.select(count).from(selectInner);
314                 return select.getQuery().getSQL(true);
315                 // return "SELECT COUNT(*) FROM (" + query + ") AS glomarbitraryalias";
316         }
317
318         private static List<UsesRelationship> build_sql_select_add_fields_to_get(SelectSelectStep step,
319                         final String tableName, final LayoutFieldVector fieldsToGet, final SortClause sortClause,
320                         final boolean extraJoin) {
321
322                 // Get all relationships used in the query:
323                 final List<UsesRelationship> listRelationships = new ArrayList<UsesRelationship>();
324
325                 final int layoutFieldsSize = Utils.safeLongToInt(fieldsToGet.size());
326                 for (int i = 0; i < layoutFieldsSize; i++) {
327                         final LayoutItem_Field layout_item = fieldsToGet.get(i);
328                         add_to_relationships_list(listRelationships, layout_item);
329                 }
330
331                 if (sortClause != null) {
332                         final int sortFieldsSize = Utils.safeLongToInt(sortClause.size());
333                         for (int i = 0; i < sortFieldsSize; i++) {
334                                 final SortFieldPair pair = sortClause.get(i);
335                                 final LayoutItem_Field layout_item = pair.getFirst();
336                                 add_to_relationships_list(listRelationships, layout_item);
337                         }
338                 }
339
340                 boolean one_added = false;
341                 for (int i = 0; i < layoutFieldsSize; i++) {
342                         final LayoutItem_Field layout_item = fieldsToGet.get(i);
343
344                         if (layout_item == null) {
345                                 // g_warn_if_reached();
346                                 continue;
347                         }
348
349                         // Get the parent, such as the table name, or the alias name for the join:
350                         // final String parent = layout_item.get_sql_table_or_join_alias_name(tableName);
351
352                         /*
353                          * TODO: const LayoutItem_FieldSummary* fieldsummary = dynamic_cast<const
354                          * LayoutItem_FieldSummary*>(layout_item.obj()); if(fieldsummary) { const Gnome::Gda::SqlBuilder::Id
355                          * id_function = builder->add_function( fieldsummary->get_summary_type_sql(),
356                          * builder->add_field_id(layout_item->get_name(), tableName)); builder->add_field_value_id(id_function); }
357                          * else {
358                          */
359                         final org.jooq.Field<?> field = createField(tableName, layout_item);
360                         if (field != null) {
361                                 step = step.select(field);
362
363                                 // Avoid duplicate records with doubly-related fields:
364                                 // TODO: if(extra_join)
365                                 // builder->select_group_by(id);
366                         }
367                         // }
368
369                         one_added = true;
370                 }
371
372                 if (!one_added) {
373                         // TODO: std::cerr << G_STRFUNC << ": No fields added: fieldsToGet.size()=" << fieldsToGet.size() <<
374                         // std::endl;
375                         return listRelationships;
376                 }
377
378                 return listRelationships;
379         }
380
381         private static org.jooq.Field<Object> createField(final String tableName, final String fieldName) {
382                 final String sql_field_name = get_sql_field_name(tableName, fieldName);
383                 if (StringUtils.isEmpty(sql_field_name)) {
384                         return null;
385                 }
386
387                 return Factory.field(sql_field_name);
388         }
389
390         private static org.jooq.Field<Object> createField(final String tableName, final LayoutItem_Field layoutField) {
391                 final String sql_field_name = get_sql_field_name(tableName, layoutField);
392                 if (StringUtils.isEmpty(sql_field_name)) {
393                         return null;
394                 }
395
396                 return Factory.field(sql_field_name);
397         }
398
399         private static String get_sql_field_name(final String tableName, final String fieldName) {
400
401                 if (StringUtils.isEmpty(tableName)) {
402                         return "";
403                 }
404
405                 if (StringUtils.isEmpty(fieldName)) {
406                         return "";
407                 }
408
409                 // TODO: Quoting, escaping, etc:
410                 return tableName + "." + fieldName;
411         }
412
413         private static String get_sql_field_name(final String tableName, final LayoutItem_Field layoutItemField) {
414
415                 if (layoutItemField == null) {
416                         return "";
417                 }
418
419                 if (StringUtils.isEmpty(tableName)) {
420                         return "";
421                 }
422
423                 // TODO: Quoting, escaping, etc:
424                 return get_sql_field_name(layoutItemField.get_sql_table_or_join_alias_name(tableName),
425                                 layoutItemField.get_name());
426         }
427
428         private static void add_to_relationships_list(final List<UsesRelationship> listRelationships,
429                         final LayoutItem_Field layout_item) {
430
431                 if (layout_item == null) {
432                         return;
433                 }
434
435                 if (!layout_item.get_has_relationship_name()) {
436                         return;
437                 }
438
439                 // If this is a related relationship, add the first-level relationship too, so that the related relationship can
440                 // be defined in terms of it:
441                 // TODO: //If the table is not yet in the list:
442                 if (layout_item.get_has_related_relationship_name()) {
443                         final UsesRelationship usesRel = new UsesRelationship();
444                         usesRel.setRelationship(layout_item.get_relationship());
445
446                         // Remove any UsesRelationship that has only the same relationship (not related relationship),
447                         // to avoid adding that part of the relationship to the SQL twice (two identical JOINS).
448                         // listRemoveIfUsesRelationship(listRelationships, usesRel.getRelationship());
449
450                         if (!listRelationships.contains(usesRel)) {
451                                 // These need to be at the front, so that related relationships can use
452                                 // them later in the SQL statement.
453                                 listRelationships.add(usesRel);
454                         }
455
456                 }
457
458                 // Add the relationship to the list:
459                 final UsesRelationship usesRel = new UsesRelationship();
460                 usesRel.setRelationship(layout_item.get_relationship());
461                 usesRel.setRelatedRelationship(layout_item.get_related_relationship());
462                 if (!listRelationships.contains(usesRel)) {
463                         listRelationships.add(usesRel);
464                 }
465
466         }
467
468         /**
469          * @param listRelationships
470          * @param relationship
471          */
472         /*
473          * private static void listRemoveIfUsesRelationship(final List<UsesRelationship> listRelationships, final
474          * Relationship relationship) { if (relationship == null) { return; }
475          * 
476          * final Iterator<UsesRelationship> i = listRelationships.iterator(); while (i.hasNext()) { final UsesRelationship
477          * eachUsesRel = i.next(); if (eachUsesRel == null) continue;
478          * 
479          * // Ignore these: if (eachUsesRel.getHasRelatedRelationshipName()) { continue; }
480          * 
481          * final Relationship eachRel = eachUsesRel.getRelationship(); if (eachRel == null) { continue; }
482          * 
483          * Log.info("Checking: rel name=" + relationship.get_name() + ", eachRel name=" + eachRel.get_name());
484          * 
485          * if (UsesRelationship.relationship_equals(relationship, eachRel)) { i.remove(); Log.info("  Removed"); } else {
486          * Log.info(" not equal"); }
487          * 
488          * } }
489          */
490
491         private static void builder_add_join(SelectJoinStep step, final UsesRelationship uses_relationship) {
492                 final Relationship relationship = uses_relationship.getRelationship();
493                 if (!relationship.get_has_fields()) { // TODO: Handle related_record has_fields.
494                         if (relationship.get_has_to_table()) {
495                                 // It is a relationship that only specifies the table, without specifying linking fields:
496
497                                 // TODO: from() takes SQL, not specifically a table name, so this is unsafe.
498                                 // TODO: stepResult = step.from(relationship.get_to_table());
499                         }
500
501                         return;
502                 }
503
504                 // Define the alias name as returned by get_sql_join_alias_name():
505
506                 // Specify an alias, to avoid ambiguity when using 2 relationships to the same table.
507                 final String alias_name = uses_relationship.get_sql_join_alias_name();
508
509                 // Add the JOIN:
510                 if (!uses_relationship.getHasRelatedRelationshipName()) {
511
512                         final org.jooq.Field<Object> fieldFrom = createField(relationship.get_from_table(),
513                                         relationship.get_from_field());
514                         final org.jooq.Field<Object> fieldTo = createField(alias_name, relationship.get_to_field());
515                         final Condition condition = fieldFrom.equal(fieldTo);
516
517                         // TODO: join() takes SQL, not specifically an alias name, so this is unsafe.
518                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
519                         step = step.leftOuterJoin(relationship.get_to_table() + " AS " + alias_name).on(condition);
520                 } else {
521                         final UsesRelationship parent_relationship = new UsesRelationship();
522                         parent_relationship.setRelationship(relationship);
523                         final Relationship relatedRelationship = uses_relationship.getRelatedRelationship();
524
525                         final org.jooq.Field<Object> fieldFrom = createField(parent_relationship.get_sql_join_alias_name(),
526                                         relatedRelationship.get_from_field());
527                         final org.jooq.Field<Object> fieldTo = createField(alias_name, relatedRelationship.get_to_field());
528                         final Condition condition = fieldFrom.equal(fieldTo);
529
530                         // TODO: join() takes SQL, not specifically an alias name, so this is unsafe.
531                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
532                         step = step.leftOuterJoin(relatedRelationship.get_to_table() + " AS " + alias_name).on(condition);
533                 }
534         }
535
536         public static Condition get_find_where_clause_quick(final Document document, final String tableName,
537                         final Value quickFindValue) {
538                 if (StringUtils.isEmpty(tableName)) {
539                         return null;
540                 }
541
542                 // TODO: if(Conversions::value_is_empty(quick_search))
543                 // return Gnome::Gda::SqlExpr();
544
545                 Condition condition = null;
546
547                 // TODO: Cache the list of all fields, as well as caching (m_Fields) the list of all visible fields:
548                 final FieldVector fields = document.get_table_fields(tableName);
549
550                 final int fieldsSize = Utils.safeLongToInt(fields.size());
551                 for (int i = 0; i < fieldsSize; i++) {
552                         final Field field = fields.get(i);
553                         if (field == null) {
554                                 continue;
555                         }
556
557                         if (field.get_glom_type() != Field.glom_field_type.TYPE_TEXT) {
558                                 continue;
559                         }
560
561                         final org.jooq.Field<Object> jooqField = createField(tableName, field.get_name());
562                         final Condition thisCondition = jooqField.equal(quickFindValue.get_string());
563
564                         if (condition == null) {
565                                 condition = thisCondition;
566                         } else {
567                                 condition = condition.or(thisCondition);
568                         }
569                 }
570
571                 return condition;
572         }
573 }