2 * Copyright (C) 2012 Openismus GmbH
4 * This file is part of GWT-Glom.
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.
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
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/>.
20 package org.glom.web.server;
23 import java.sql.ResultSet;
24 import java.sql.SQLException;
26 import java.text.DateFormat;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Locale;
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;
55 * @author Murray Cumming <murrayc@openismus.com>
58 public class SqlUtils {
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) {
64 Condition whereClause = null; // Note that we ignore quickFind.
65 if (primaryKeyValue != null) {
66 whereClause = buildSimpleWhereExpression(tableName, primaryKey, primaryKeyValue);
69 final SortClause sortClause = null; // Ignored.
70 return buildSqlSelectWithWhereClause(tableName, fieldsToGet, whereClause, sortClause);
73 public static Condition buildSimpleWhereExpression(final String tableName, final Field primaryKey,
74 final TypedDataItem primaryKeyValue) {
76 Condition result = null;
78 if (primaryKey == null) {
82 final String fieldName = primaryKey.getName();
83 if (StringUtils.isEmpty(fieldName)) {
87 final org.jooq.Field<Object> field = createField(tableName, fieldName);
88 result = field.equal(primaryKeyValue.getValue());
93 * private static String buildSqlSelectWithWhereClause(final String tableName, final LayoutFieldVector fieldsToGet)
94 * { final Condition whereClause = null; return buildSqlSelectWithWhereClause(tableName, fieldsToGet, whereClause);
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); }
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);
111 final String query = step.getQuery().getSQL(true);
112 // Log.info("Query: " + query);
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.
122 final SelectSelectStep selectStep = factory.select();
126 private static SelectFinalStep buildSqlSelectStepWithWhereClause(final String tableName,
127 final List<LayoutItemField> fieldsToGet, final Condition whereClause, final SortClause sortClause) {
129 final SelectSelectStep selectStep = createSelect();
131 // Add the fields, and any necessary joins:
132 final List<UsesRelationship> listRelationships = buildSqlSelectAddFieldsToGet(selectStep, tableName,
133 fieldsToGet, sortClause, false /* extraJoin */);
135 final Table<Record> table = Factory.tableByName(tableName);
136 final SelectJoinStep joinStep = selectStep.from(table);
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);
144 SelectFinalStep finalStep = joinStep;
145 if (whereClause != null) {
146 finalStep = joinStep.where(whereClause);
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);
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);
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();
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";
175 private static List<UsesRelationship> buildSqlSelectAddFieldsToGet(SelectSelectStep step, final String tableName,
176 final List<LayoutItemField> fieldsToGet, final SortClause sortClause, final boolean extraJoin) {
178 // Get all relationships used in the query:
179 final List<UsesRelationship> listRelationships = new ArrayList<UsesRelationship>();
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);
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);
196 boolean oneAdded = false;
197 for (int i = 0; i < layoutFieldsSize; i++) {
198 final LayoutItemField layoutItem = fieldsToGet.get(i);
200 if (layoutItem == null) {
201 // g_warn_if_reached();
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);
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); }
215 final org.jooq.Field<?> field = createField(tableName, layoutItem);
217 step = step.select(field);
219 // Avoid duplicate records with doubly-related fields:
220 // TODO: if(extra_join)
221 // builder->select_group_by(id);
229 // TODO: std::cerr << G_STRFUNC << ": No fields added: fieldsToGet.size()=" << fieldsToGet.size() <<
231 return listRelationships;
234 return listRelationships;
237 private static org.jooq.Field<Object> createField(final String tableName, final String fieldName) {
238 if (StringUtils.isEmpty(tableName)) {
242 if (StringUtils.isEmpty(fieldName)) {
246 return Factory.fieldByName(tableName, fieldName);
249 private static org.jooq.Field<Object> createField(final String tableName, final LayoutItemField layoutField) {
250 if (StringUtils.isEmpty(tableName)) {
254 if (layoutField == null) {
258 return createField(layoutField.getSqlTableOrJoinAliasName(tableName), layoutField.getName());
261 private static void addToRelationshipsList(final List<UsesRelationship> listRelationships,
262 final UsesRelationship layoutItem) {
264 if (layoutItem == null) {
268 if (!layoutItem.getHasRelationshipName()) {
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());
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());
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);
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);
302 * @param listRelationships
303 * @param relationship
306 * private static void listRemoveIfUsesRelationship(final List<UsesRelationship> listRelationships, final
307 * Relationship relationship) { if (relationship == null) { return; }
309 * final Iterator<UsesRelationship> i = listRelationships.iterator(); while (i.hasNext()) { final UsesRelationship
310 * eachUsesRel = i.next(); if (eachUsesRel == null) continue;
312 * // Ignore these: if (eachUsesRel.getHasRelatedRelationshipName()) { continue; }
314 * final Relationship eachRel = eachUsesRel.getRelationship(); if (eachRel == null) { continue; }
316 * Log.info("Checking: rel name=" + relationship.get_name() + ", eachRel name=" + eachRel.get_name());
318 * if (UsesRelationship.relationship_equals(relationship, eachRel)) { i.remove(); Log.info(" Removed"); } else {
319 * Log.info(" not equal"); }
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:
330 // Table<Record> toTable = Factory.tableByName(relationship.getToTable());
331 // TODO: stepResult = step.from(toTable);
337 // Define the alias name as returned by getSqlJoinAliasName():
339 // Specify an alias, to avoid ambiguity when using 2 relationships to the same table.
340 final String aliasName = usesRelationship.getSqlJoinAliasName();
343 if (!usesRelationship.getHasRelatedRelationshipName()) {
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);
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);
354 final UsesRelationship parentRelationship = new UsesRelationshipImpl();
355 parentRelationship.setRelationship(relationship);
356 final Relationship relatedRelationship = usesRelationship.getRelatedRelationship();
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);
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);
369 public static Condition getFindWhereClauseQuick(final Document document, final String tableName,
370 final TypedDataItem quickFindValue) {
371 if (StringUtils.isEmpty(tableName)) {
375 // TODO: if(Conversions::value_is_empty(quick_search))
376 // return Gnome::Gda::SqlExpr();
378 Condition condition = null;
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);
383 final int fieldsSize = Utils.safeLongToInt(fields.size());
384 for (int i = 0; i < fieldsSize; i++) {
385 final Field field = fields.get(i);
390 if (field.getGlomType() != Field.GlomFieldType.TYPE_TEXT) {
394 final org.jooq.Field<Object> jooqField = createField(tableName, field.getName());
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());
401 if (condition == null) {
402 condition = thisCondition;
404 condition = condition.or(thisCondition);
416 * @param primaryKeyValue
417 * @throws SQLException
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 {
422 switch (field.getGlomType()) {
424 final String text = rs.getString(rsIndex);
425 dataItem.setText(text != null ? text : "");
428 dataItem.setBoolean(rs.getBoolean(rsIndex));
431 dataItem.setNumber(rs.getDouble(rsIndex));
434 final Date date = rs.getDate(rsIndex);
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));
441 dataItem.setText("");
445 final Time time = rs.getTime(rsIndex);
447 final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.ROOT);
448 dataItem.setText(timeFormat.format(time));
450 dataItem.setText("");
454 //We don't get the data here.
455 //Instead we provide a way for the client to get the image separately.
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;
464 final String url = Utils.buildImageDataUrl(primaryKeyValue, documentID, tableName, field);
465 dataItem.setImageDataUrl(url);
469 Log.warn(documentID, tableName, "Invalid LayoutItem Field type. Using empty string for value.");
470 dataItem.setText("");