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