Allow primary keys to be any type, and test it.
[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.Date;
23 import java.sql.ResultSet;
24 import java.sql.SQLException;
25 import java.sql.Time;
26 import java.text.DateFormat;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Locale;
30
31 import org.apache.commons.lang3.StringUtils;
32 import org.glom.web.server.libglom.Document;
33 import org.glom.web.shared.DataItem;
34 import org.glom.web.shared.TypedDataItem;
35 import org.glom.web.shared.libglom.Field;
36 import org.glom.web.shared.libglom.Relationship;
37 import org.glom.web.shared.libglom.layout.LayoutItemField;
38 import org.glom.web.shared.libglom.layout.SortClause;
39 import org.glom.web.shared.libglom.layout.UsesRelationship;
40 import org.glom.web.shared.libglom.layout.UsesRelationshipImpl;
41 import org.jooq.AggregateFunction;
42 import org.jooq.Condition;
43 import org.jooq.Record;
44 import org.jooq.SQLDialect;
45 import org.jooq.SelectFinalStep;
46 import org.jooq.SelectJoinStep;
47 import org.jooq.SelectSelectStep;
48 import org.jooq.Table;
49 import org.jooq.conf.RenderKeywordStyle;
50 import org.jooq.conf.RenderNameStyle;
51 import org.jooq.conf.Settings;
52 import org.jooq.impl.Factory;
53
54 /**
55  * @author Murray Cumming <murrayc@openismus.com>
56  * 
57  */
58 public class SqlUtils {
59
60         // TODO: Change to final ArrayList<LayoutItem_Field> fieldsToGet
61         public static String buildSqlSelectWithKey(final String tableName, final List<LayoutItemField> fieldsToGet,
62                         final Field primaryKey, final TypedDataItem primaryKeyValue) {
63
64                 Condition whereClause = null; // Note that we ignore quickFind.
65                 if (primaryKeyValue != null) {
66                         whereClause = buildSimpleWhereExpression(tableName, primaryKey, primaryKeyValue);
67                 }
68
69                 final SortClause sortClause = null; // Ignored.
70                 return buildSqlSelectWithWhereClause(tableName, fieldsToGet, whereClause, sortClause);
71         }
72
73         public static Condition buildSimpleWhereExpression(final String tableName, final Field primaryKey,
74                         final TypedDataItem primaryKeyValue) {
75
76                 Condition result = null;
77
78                 if (primaryKey == null) {
79                         return result;
80                 }
81
82                 final String fieldName = primaryKey.getName();
83                 if (StringUtils.isEmpty(fieldName)) {
84                         return result;
85                 }
86
87                 final org.jooq.Field<Object> field = createField(tableName, fieldName);
88                 result = field.equal(primaryKeyValue.getValue());
89                 return result;
90         }
91
92         /*
93          * private static String buildSqlSelectWithWhereClause(final String tableName, final LayoutFieldVector fieldsToGet)
94          * { final Condition whereClause = null; return buildSqlSelectWithWhereClause(tableName, fieldsToGet, whereClause);
95          * }
96          */
97
98         /*
99          * private static String buildSqlSelectWithWhereClause(final String tableName, final LayoutFieldVector fieldsToGet,
100          * final Condition whereClause) { final SortClause sortClause = null; return
101          * buildSqlSelectWithWhereClause(tableName, fieldsToGet, whereClause, sortClause); }
102          */
103
104         public static String buildSqlSelectWithWhereClause(final String tableName, final List<LayoutItemField> fieldsToGet,
105                         final Condition whereClause, final SortClause sortClause) {
106                 final SelectFinalStep step = buildSqlSelectStepWithWhereClause(tableName, fieldsToGet, whereClause, sortClause);
107                 if (step == null) {
108                         return "";
109                 }
110
111                 final String query = step.getQuery().getSQL(true);
112                 // Log.info("Query: " + query);
113                 return query;
114         }
115
116         private static SelectSelectStep createSelect() {
117                 final Factory factory = new Factory(SQLDialect.POSTGRES);
118                 final Settings settings = factory.getSettings();
119                 settings.setRenderNameStyle(RenderNameStyle.QUOTED); // TODO: This doesn't seem to have any effect.
120                 settings.setRenderKeywordStyle(RenderKeywordStyle.UPPER); // TODO: Just to make debugging nicer.
121
122                 final SelectSelectStep selectStep = factory.select();
123                 return selectStep;
124         }
125
126         private static SelectFinalStep buildSqlSelectStepWithWhereClause(final String tableName,
127                         final List<LayoutItemField> fieldsToGet, final Condition whereClause, final SortClause sortClause) {
128
129                 final SelectSelectStep selectStep = createSelect();
130
131                 // Add the fields, and any necessary joins:
132                 final List<UsesRelationship> listRelationships = buildSqlSelectAddFieldsToGet(selectStep, tableName,
133                                 fieldsToGet, sortClause, false /* extraJoin */);
134
135                 final Table<Record> table = Factory.tableByName(tableName);
136                 final SelectJoinStep joinStep = selectStep.from(table);
137
138                 // LEFT OUTER JOIN will get the field values from the other tables,
139                 // and give us our fields for this table even if there is no corresponding value in the other table.
140                 for (final UsesRelationship usesRelationship : listRelationships) {
141                         builderAddJoin(joinStep, usesRelationship);
142                 }
143
144                 SelectFinalStep finalStep = joinStep;
145                 if (whereClause != null) {
146                         finalStep = joinStep.where(whereClause);
147                 }
148
149                 return finalStep;
150         }
151
152         public static String buildSqlCountSelectWithWhereClause(final String tableName,
153                         final List<LayoutItemField> fieldsToGet) {
154                 final SelectFinalStep selectInner = buildSqlSelectStepWithWhereClause(tableName, fieldsToGet, null, null);
155                 return buildSqlSelectCountRows(selectInner);
156         }
157
158         public static String buildSqlCountSelectWithWhereClause(final String tableName,
159                         final List<LayoutItemField> fieldsToGet, final Condition whereClause) {
160                 final SelectFinalStep selectInner = buildSqlSelectStepWithWhereClause(tableName, fieldsToGet, whereClause, null);
161                 return buildSqlSelectCountRows(selectInner);
162         }
163
164         private static String buildSqlSelectCountRows(final SelectFinalStep selectInner) {
165                 // TODO: Find a way to do this with the jOOQ API:
166                 final SelectSelectStep select = createSelect();
167
168                 final org.jooq.Field<?> field = Factory.field("*");
169                 final AggregateFunction<?> count = Factory.count(field);
170                 select.select(count).from(selectInner);
171                 return select.getQuery().getSQL(true);
172                 // return "SELECT COUNT(*) FROM (" + query + ") AS glomarbitraryalias";
173         }
174
175         private static List<UsesRelationship> buildSqlSelectAddFieldsToGet(SelectSelectStep step, final String tableName,
176                         final List<LayoutItemField> fieldsToGet, final SortClause sortClause, final boolean extraJoin) {
177
178                 // Get all relationships used in the query:
179                 final List<UsesRelationship> listRelationships = new ArrayList<UsesRelationship>();
180
181                 final int layoutFieldsSize = Utils.safeLongToInt(fieldsToGet.size());
182                 for (int i = 0; i < layoutFieldsSize; i++) {
183                         final UsesRelationship layoutItem = fieldsToGet.get(i);
184                         addToRelationshipsList(listRelationships, layoutItem);
185                 }
186
187                 if (sortClause != null) {
188                         final int sortFieldsSize = Utils.safeLongToInt(sortClause.size());
189                         for (int i = 0; i < sortFieldsSize; i++) {
190                                 final SortClause.SortField pair = sortClause.get(i);
191                                 final UsesRelationship layoutItem = pair.field;
192                                 addToRelationshipsList(listRelationships, layoutItem);
193                         }
194                 }
195
196                 boolean oneAdded = false;
197                 for (int i = 0; i < layoutFieldsSize; i++) {
198                         final LayoutItemField layoutItem = fieldsToGet.get(i);
199
200                         if (layoutItem == null) {
201                                 // g_warn_if_reached();
202                                 continue;
203                         }
204
205                         // Get the parent, such as the table name, or the alias name for the join:
206                         // final String parent = layout_item.get_sql_table_or_join_alias_name(tableName);
207
208                         /*
209                          * TODO: const LayoutItem_FieldSummary* fieldsummary = dynamic_cast<const
210                          * LayoutItem_FieldSummary*>(layout_item.obj()); if(fieldsummary) { const Gnome::Gda::SqlBuilder::Id
211                          * id_function = builder->add_function( fieldsummary->get_summary_type_sql(),
212                          * builder->add_field_id(layout_item->get_name(), tableName)); builder->add_field_value_id(id_function); }
213                          * else {
214                          */
215                         final org.jooq.Field<?> field = createField(tableName, layoutItem);
216                         if (field != null) {
217                                 step = step.select(field);
218
219                                 // Avoid duplicate records with doubly-related fields:
220                                 // TODO: if(extra_join)
221                                 // builder->select_group_by(id);
222                         }
223                         // }
224
225                         oneAdded = true;
226                 }
227
228                 if (!oneAdded) {
229                         // TODO: std::cerr << G_STRFUNC << ": No fields added: fieldsToGet.size()=" << fieldsToGet.size() <<
230                         // std::endl;
231                         return listRelationships;
232                 }
233
234                 return listRelationships;
235         }
236
237         private static org.jooq.Field<Object> createField(final String tableName, final String fieldName) {
238                 if (StringUtils.isEmpty(tableName)) {
239                         return null;
240                 }
241
242                 if (StringUtils.isEmpty(fieldName)) {
243                         return null;
244                 }
245
246                 return Factory.fieldByName(tableName, fieldName);
247         }
248
249         private static org.jooq.Field<Object> createField(final String tableName, final LayoutItemField layoutField) {
250                 if (StringUtils.isEmpty(tableName)) {
251                         return null;
252                 }
253
254                 if (layoutField == null) {
255                         return null;
256                 }
257
258                 return createField(layoutField.getSqlTableOrJoinAliasName(tableName), layoutField.getName());
259         }
260
261         private static void addToRelationshipsList(final List<UsesRelationship> listRelationships,
262                         final UsesRelationship layoutItem) {
263
264                 if (layoutItem == null) {
265                         return;
266                 }
267
268                 if (!layoutItem.getHasRelationshipName()) {
269                         return;
270                 }
271
272                 // If this is a related relationship, add the first-level relationship too, so that the related relationship can
273                 // be defined in terms of it:
274                 // TODO: //If the table is not yet in the list:
275                 if (layoutItem.getHasRelatedRelationshipName()) {
276                         final UsesRelationship usesRel = new UsesRelationshipImpl();
277                         usesRel.setRelationship(layoutItem.getRelationship());
278
279                         // Remove any UsesRelationship that has only the same relationship (not related relationship),
280                         // to avoid adding that part of the relationship to the SQL twice (two identical JOINS).
281                         // listRemoveIfUsesRelationship(listRelationships, usesRel.getRelationship());
282
283                         if (!listRelationships.contains(usesRel)) {
284                                 // These need to be at the front, so that related relationships can use
285                                 // them later in the SQL statement.
286                                 listRelationships.add(usesRel);
287                         }
288
289                 }
290
291                 // Add the relationship to the list:
292                 final UsesRelationship usesRel = new UsesRelationshipImpl();
293                 usesRel.setRelationship(layoutItem.getRelationship());
294                 usesRel.setRelatedRelationship(layoutItem.getRelatedRelationship());
295                 if (!listRelationships.contains(usesRel)) {
296                         listRelationships.add(usesRel);
297                 }
298
299         }
300
301         /**
302          * @param listRelationships
303          * @param relationship
304          */
305         /*
306          * private static void listRemoveIfUsesRelationship(final List<UsesRelationship> listRelationships, final
307          * Relationship relationship) { if (relationship == null) { return; }
308          * 
309          * final Iterator<UsesRelationship> i = listRelationships.iterator(); while (i.hasNext()) { final UsesRelationship
310          * eachUsesRel = i.next(); if (eachUsesRel == null) continue;
311          * 
312          * // Ignore these: if (eachUsesRel.getHasRelatedRelationshipName()) { continue; }
313          * 
314          * final Relationship eachRel = eachUsesRel.getRelationship(); if (eachRel == null) { continue; }
315          * 
316          * Log.info("Checking: rel name=" + relationship.get_name() + ", eachRel name=" + eachRel.get_name());
317          * 
318          * if (UsesRelationship.relationship_equals(relationship, eachRel)) { i.remove(); Log.info("  Removed"); } else {
319          * Log.info(" not equal"); }
320          * 
321          * } }
322          */
323
324         private static void builderAddJoin(SelectJoinStep step, final UsesRelationship usesRelationship) {
325                 final Relationship relationship = usesRelationship.getRelationship();
326                 if (!relationship.getHasFields()) { // TODO: Handle related_record has_fields.
327                         if (relationship.getHasToTable()) {
328                                 // It is a relationship that only specifies the table, without specifying linking fields:
329
330                                 // Table<Record> toTable = Factory.tableByName(relationship.getToTable());
331                                 // TODO: stepResult = step.from(toTable);
332                         }
333
334                         return;
335                 }
336
337                 // Define the alias name as returned by getSqlJoinAliasName():
338
339                 // Specify an alias, to avoid ambiguity when using 2 relationships to the same table.
340                 final String aliasName = usesRelationship.getSqlJoinAliasName();
341
342                 // Add the JOIN:
343                 if (!usesRelationship.getHasRelatedRelationshipName()) {
344
345                         final org.jooq.Field<Object> fieldFrom = createField(relationship.getFromTable(),
346                                         relationship.getFromField());
347                         final org.jooq.Field<Object> fieldTo = createField(aliasName, relationship.getToField());
348                         final Condition condition = fieldFrom.equal(fieldTo);
349
350                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
351                         final Table<Record> toTable = Factory.tableByName(relationship.getToTable());
352                         step = step.leftOuterJoin(toTable.as(aliasName)).on(condition);
353                 } else {
354                         final UsesRelationship parentRelationship = new UsesRelationshipImpl();
355                         parentRelationship.setRelationship(relationship);
356                         final Relationship relatedRelationship = usesRelationship.getRelatedRelationship();
357
358                         final org.jooq.Field<Object> fieldFrom = createField(parentRelationship.getSqlJoinAliasName(),
359                                         relatedRelationship.getFromField());
360                         final org.jooq.Field<Object> fieldTo = createField(aliasName, relatedRelationship.getToField());
361                         final Condition condition = fieldFrom.equal(fieldTo);
362
363                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
364                         final Table<Record> toTable = Factory.tableByName(relatedRelationship.getToTable());
365                         step = step.leftOuterJoin(toTable.as(aliasName)).on(condition);
366                 }
367         }
368
369         public static Condition getFindWhereClauseQuick(final Document document, final String tableName,
370                         final TypedDataItem quickFindValue) {
371                 if (StringUtils.isEmpty(tableName)) {
372                         return null;
373                 }
374
375                 // TODO: if(Conversions::value_is_empty(quick_search))
376                 // return Gnome::Gda::SqlExpr();
377
378                 Condition condition = null;
379
380                 // TODO: Cache the list of all fields, as well as caching (m_Fields) the list of all visible fields:
381                 final List<Field> fields = document.getTableFields(tableName);
382
383                 final int fieldsSize = Utils.safeLongToInt(fields.size());
384                 for (int i = 0; i < fieldsSize; i++) {
385                         final Field field = fields.get(i);
386                         if (field == null) {
387                                 continue;
388                         }
389
390                         if (field.getGlomType() != Field.GlomFieldType.TYPE_TEXT) {
391                                 continue;
392                         }
393
394                         final org.jooq.Field<Object> jooqField = createField(tableName, field.getName());
395
396                         // Do a case-insensitive substring search:
397                         // TODO: Use ILIKE: http://sourceforge.net/apps/trac/jooq/ticket/1423
398                         // http://groups.google.com/group/jooq-user/browse_thread/thread/203ae5a1a06ae65f
399                         final Condition thisCondition = jooqField.lower().contains(quickFindValue.getText().toLowerCase());
400
401                         if (condition == null) {
402                                 condition = thisCondition;
403                         } else {
404                                 condition = condition.or(thisCondition);
405                         }
406                 }
407
408                 return condition;
409         }
410
411         /**
412          * @param dataItem
413          * @param field
414          * @param rsIndex
415          * @param rs
416          * @param primaryKeyValue
417          * @throws SQLException
418          */
419         public static void fillDataItemFromResultSet(final DataItem dataItem, final LayoutItemField field, final int rsIndex,
420                         final ResultSet rs, final String documentID, final String tableName, final TypedDataItem primaryKeyValue) throws SQLException {
421                 
422                 switch (field.getGlomType()) {
423                 case TYPE_TEXT:
424                         final String text = rs.getString(rsIndex);
425                         dataItem.setText(text != null ? text : "");
426                         break;
427                 case TYPE_BOOLEAN:
428                         dataItem.setBoolean(rs.getBoolean(rsIndex));
429                         break;
430                 case TYPE_NUMERIC:
431                         dataItem.setNumber(rs.getDouble(rsIndex));
432                         break;
433                 case TYPE_DATE:
434                         final Date date = rs.getDate(rsIndex);
435                         if (date != null) {
436                                 // TODO: Pass Date and Time types instead of converting to text here?
437                                 // TODO: Use a 4-digit-year short form, somehow.
438                                 final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.ROOT);
439                                 dataItem.setText(dateFormat.format(date));
440                         } else {
441                                 dataItem.setText("");
442                         }
443                         break;
444                 case TYPE_TIME:
445                         final Time time = rs.getTime(rsIndex);
446                         if (time != null) {
447                                 final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.ROOT);
448                                 dataItem.setText(timeFormat.format(time));
449                         } else {
450                                 dataItem.setText("");
451                         }
452                         break;
453                 case TYPE_IMAGE:
454                         //We don't get the data here.
455                         //Instead we provide a way for the client to get the image separately.
456                         
457                         //This doesn't seem to work,
458                         //presumably because the base64 encoding is wrong:
459                         //final byte[] imageByteArray = rs.getBytes(rsIndex);
460                         //if (imageByteArray != null) {
461                         //      String base64 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(imageByteArray);
462                         //      base64 = "data:image/png;base64," + base64;
463                         
464                         final String url = Utils.buildImageDataUrl(primaryKeyValue, documentID, tableName, field);
465                         dataItem.setImageDataUrl(url);
466                         break;
467                 case TYPE_INVALID:
468                 default:
469                         Log.warn(documentID, tableName, "Invalid LayoutItem Field type. Using empty string for value.");
470                         dataItem.setText("");
471                         break;
472                 }
473         }
474 }