Rename GlomField to DataItem and update associated methods.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / database / DBAccess.java
1 /*
2  * Copyright (C) 2011 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.database;
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.text.NumberFormat;
28 import java.util.ArrayList;
29 import java.util.Currency;
30 import java.util.Locale;
31
32 import org.glom.libglom.Document;
33 import org.glom.libglom.Field;
34 import org.glom.libglom.FieldFormatting;
35 import org.glom.libglom.FieldVector;
36 import org.glom.libglom.LayoutFieldVector;
37 import org.glom.libglom.LayoutGroupVector;
38 import org.glom.libglom.LayoutItem;
39 import org.glom.libglom.LayoutItemVector;
40 import org.glom.libglom.LayoutItem_Field;
41 import org.glom.libglom.LayoutItem_Portal;
42 import org.glom.libglom.NumericFormat;
43 import org.glom.web.server.Log;
44 import org.glom.web.server.Utils;
45 import org.glom.web.shared.DataItem;
46
47 import com.mchange.v2.c3p0.ComboPooledDataSource;
48
49 /**
50  * @author Ben Konrath <ben@bagu.org>
51  * 
52  */
53 abstract class DBAccess {
54         protected Document document;
55         protected String documentID;
56         protected String tableName;
57         protected ComboPooledDataSource cpds;
58
59         protected DBAccess(Document document, String documentID, ComboPooledDataSource cpds, String tableName) {
60                 this.document = document;
61                 this.documentID = documentID;
62                 this.cpds = cpds;
63                 this.tableName = tableName;
64         }
65
66         /*
67          * Converts data from a ResultSet to an ArrayList of DataItem array suitable for sending back to the client.
68          */
69         final protected ArrayList<DataItem[]> convertResultSetToDTO(int length, LayoutFieldVector layoutFields, ResultSet rs)
70                         throws SQLException {
71
72                 // get the data we've been asked for
73                 int rowCount = 0;
74                 ArrayList<DataItem[]> rowsList = new ArrayList<DataItem[]>();
75                 while (rs.next() && rowCount <= length) {
76                         int layoutFieldsSize = Utils.safeLongToInt(layoutFields.size());
77                         DataItem[] rowArray = new DataItem[layoutFieldsSize];
78                         for (int i = 0; i < layoutFieldsSize; i++) {
79                                 // make a new DataItem to set the text and colours
80                                 rowArray[i] = new DataItem();
81
82                                 // get foreground and background colours
83                                 LayoutItem_Field field = layoutFields.get(i);
84                                 FieldFormatting formatting = field.get_formatting_used();
85                                 String fgcolour = formatting.get_text_format_color_foreground();
86                                 if (!fgcolour.isEmpty())
87                                         rowArray[i].setFGColour(convertGdkColorToHtmlColour(fgcolour));
88                                 String bgcolour = formatting.get_text_format_color_background();
89                                 if (!bgcolour.isEmpty())
90                                         rowArray[i].setBGColour(convertGdkColorToHtmlColour(bgcolour));
91
92                                 // Convert the field value to a string based on the glom type. We're doing the formatting on the
93                                 // server side for now but it might be useful to move this to the client side.
94                                 switch (field.get_glom_type()) {
95                                 case TYPE_TEXT:
96                                         String text = rs.getString(i + 1);
97                                         rowArray[i].setText(text != null ? text : "");
98                                         break;
99                                 case TYPE_BOOLEAN:
100                                         rowArray[i].setBoolean(rs.getBoolean(i + 1));
101                                         break;
102                                 case TYPE_NUMERIC:
103                                         // Take care of the numeric formatting before converting the number to a string.
104                                         NumericFormat numFormatGlom = formatting.getM_numeric_format();
105                                         // There's no isCurrency() method in the glom NumericFormat class so we're assuming that the
106                                         // number should be formatted as a currency if the currency code string is not empty.
107                                         String currencyCode = numFormatGlom.getM_currency_symbol();
108                                         NumberFormat numFormatJava = null;
109                                         boolean useGlomCurrencyCode = false;
110                                         if (currencyCode.length() == 3) {
111                                                 // Try to format the currency using the Java Locales system.
112                                                 try {
113                                                         Currency currency = Currency.getInstance(currencyCode);
114                                                         Log.info(documentID, tableName, "A valid ISO 4217 currency code is being used."
115                                                                         + " Overriding the numeric formatting with information from the locale.");
116                                                         int digits = currency.getDefaultFractionDigits();
117                                                         numFormatJava = NumberFormat.getCurrencyInstance(Locale.ROOT);
118                                                         numFormatJava.setCurrency(currency);
119                                                         numFormatJava.setMinimumFractionDigits(digits);
120                                                         numFormatJava.setMaximumFractionDigits(digits);
121                                                 } catch (IllegalArgumentException e) {
122                                                         Log.warn(documentID, tableName, currencyCode + " is not a valid ISO 4217 code."
123                                                                         + " Manually setting currency code with this value.");
124                                                         // The currency code is not this is not an ISO 4217 currency code.
125                                                         // We're going to manually set the currency code and use the glom numeric formatting.
126                                                         useGlomCurrencyCode = true;
127                                                         numFormatJava = convertToJavaNumberFormat(numFormatGlom);
128                                                 }
129                                         } else if (currencyCode.length() > 0) {
130                                                 Log.warn(documentID, tableName, currencyCode + " is not a valid ISO 4217 code."
131                                                                 + " Manually setting currency code with this value.");
132                                                 // The length of the currency code is > 0 and != 3; this is not an ISO 4217 currency code.
133                                                 // We're going to manually set the currency code and use the glom numeric formatting.
134                                                 useGlomCurrencyCode = true;
135                                                 numFormatJava = convertToJavaNumberFormat(numFormatGlom);
136                                         } else {
137                                                 // The length of the currency code is 0; the number is not a currency.
138                                                 numFormatJava = convertToJavaNumberFormat(numFormatGlom);
139                                         }
140
141                                         // TODO: Do I need to do something with NumericFormat.get_default_precision() from libglom?
142
143                                         double number = rs.getDouble(i + 1);
144                                         if (number < 0) {
145                                                 if (formatting.getM_numeric_format().getM_alt_foreground_color_for_negatives())
146                                                         // overrides the set foreground colour
147                                                         rowArray[i].setFGColour(convertGdkColorToHtmlColour(NumericFormat
148                                                                         .get_alternative_color_for_negatives()));
149                                         }
150
151                                         // Finally convert the number to text using the glom currency string if required.
152                                         if (useGlomCurrencyCode) {
153                                                 rowArray[i].setText(currencyCode + " " + numFormatJava.format(number));
154                                         } else {
155                                                 rowArray[i].setText(numFormatJava.format(number));
156                                         }
157                                         break;
158                                 case TYPE_DATE:
159                                         Date date = rs.getDate(i + 1);
160                                         if (date != null) {
161                                                 DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ROOT);
162                                                 rowArray[i].setText(dateFormat.format(date));
163                                         } else {
164                                                 rowArray[i].setText("");
165                                         }
166                                         break;
167                                 case TYPE_TIME:
168                                         Time time = rs.getTime(i + 1);
169                                         if (time != null) {
170                                                 DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.ROOT);
171                                                 rowArray[i].setText(timeFormat.format(time));
172                                         } else {
173                                                 rowArray[i].setText("");
174                                         }
175                                         break;
176                                 case TYPE_IMAGE:
177                                         byte[] image = rs.getBytes(i + 1);
178                                         if (image != null) {
179                                                 // TODO implement field TYPE_IMAGE
180                                                 rowArray[i].setText("Image (FIXME)");
181                                         } else {
182                                                 rowArray[i].setText("");
183                                         }
184                                         break;
185                                 case TYPE_INVALID:
186                                 default:
187                                         Log.warn(documentID, tableName, "Invalid LayoutItem Field type. Using empty string for value.");
188                                         rowArray[i].setText("");
189                                         break;
190                                 }
191                         }
192
193                         // add the row of DataItems to the ArrayList we're going to return and update the row count
194                         rowsList.add(rowArray);
195                         rowCount++;
196                 }
197
198                 return rowsList;
199         }
200
201         /*
202          * Gets a LayoutFieldVector to use when generating an SQL query.
203          */
204         protected LayoutFieldVector getFieldsToShowForSQLQuery(LayoutGroupVector layoutGroupVec) {
205                 LayoutFieldVector layoutFieldVector = new LayoutFieldVector();
206
207                 // We will show the fields that the document says we should:
208                 for (int i = 0; i < layoutGroupVec.size(); i++) {
209                         org.glom.libglom.LayoutGroup layoutGroup = layoutGroupVec.get(i);
210
211                         // satisfy the precondition of getDetailsLayoutGroup(String tableName, org.glom.libglom.LayoutGroup
212                         // libglomLayoutGroup)
213                         if (layoutGroup == null)
214                                 continue;
215
216                         // Get the fields:
217                         ArrayList<LayoutItem_Field> layoutItemFields = getFieldsToShowForSQLQueryAddGroup(layoutGroup);
218                         for (LayoutItem_Field layoutItem_Field : layoutItemFields) {
219                                 layoutFieldVector.add(layoutItem_Field);
220                         }
221                 }
222                 return layoutFieldVector;
223         }
224
225         /*
226          * Gets an ArrayList of LayoutItem_Field objects to use when generating an SQL query.
227          * 
228          * @precondition libglomLayoutGroup must not be null
229          */
230         private ArrayList<LayoutItem_Field> getFieldsToShowForSQLQueryAddGroup(
231                         org.glom.libglom.LayoutGroup libglomLayoutGroup) {
232
233                 ArrayList<LayoutItem_Field> layoutItemFields = new ArrayList<LayoutItem_Field>();
234                 LayoutItemVector items = libglomLayoutGroup.get_items();
235                 int numItems = Utils.safeLongToInt(items.size());
236                 for (int i = 0; i < numItems; i++) {
237                         LayoutItem layoutItem = items.get(i);
238
239                         LayoutItem_Field layoutItemField = LayoutItem_Field.cast_dynamic(layoutItem);
240                         if (layoutItemField != null) {
241                                 // the layoutItem is a LayoutItem_Field
242                                 FieldVector fields;
243                                 if (layoutItemField.get_has_relationship_name()) {
244                                         // layoutItemField is a field in a related table
245                                         fields = document.get_table_fields(layoutItemField.get_table_used(tableName));
246                                 } else {
247                                         // layoutItemField is a field in this table
248                                         fields = document.get_table_fields(tableName);
249                                 }
250
251                                 // set the layoutItemFeild with details from its Field in the document and
252                                 // add it to the list to be returned
253                                 for (int j = 0; j < fields.size(); j++) {
254                                         // check the names to see if they're the same
255                                         // this works because we're using the field list from the related table if necessary
256                                         if (layoutItemField.get_name().equals(fields.get(j).get_name())) {
257                                                 Field field = fields.get(j);
258                                                 if (field != null) {
259                                                         layoutItemField.set_full_field_details(field);
260                                                         layoutItemFields.add(layoutItemField);
261                                                 } else {
262                                                         Log.warn(document.get_database_title(), tableName,
263                                                                         "LayoutItem_Field " + layoutItemField.get_layout_display_name()
264                                                                                         + " not found in document field list.");
265                                                 }
266                                                 break;
267                                         }
268                                 }
269
270                         } else {
271                                 // the layoutItem is not a LayoutItem_Field
272                                 org.glom.libglom.LayoutGroup subLayoutGroup = org.glom.libglom.LayoutGroup.cast_dynamic(layoutItem);
273                                 if (subLayoutGroup != null) {
274                                         // the layoutItem is a LayoutGroup
275                                         LayoutItem_Portal layoutItemPortal = LayoutItem_Portal.cast_dynamic(layoutItem);
276                                         if (layoutItemPortal == null) {
277                                                 // The subGroup is not a LayoutItem_Portal.
278                                                 // We're ignoring portals because they are filled by means of a separate SQL query.
279                                                 layoutItemFields.addAll(getFieldsToShowForSQLQueryAddGroup(subLayoutGroup));
280                                         }
281                                 }
282                         }
283                 }
284                 return layoutItemFields;
285         }
286
287         /**
288          * Gets the primary key Field for this table.
289          * 
290          * @return primary key Field
291          */
292         public Field getPrimaryKeyField() {
293                 Field primaryKey = null;
294                 FieldVector fieldsVec = document.get_table_fields(tableName);
295                 for (int i = 0; i < Utils.safeLongToInt(fieldsVec.size()); i++) {
296                         Field field = fieldsVec.get(i);
297                         if (field.get_primary_key()) {
298                                 primaryKey = field;
299                                 break;
300                         }
301                 }
302                 return primaryKey;
303         }
304
305         /**
306          * Gets the primary key LayoutItem_Field for this table.
307          * 
308          * @return primary key LayoutItem_Field
309          */
310         public LayoutItem_Field getPrimaryKeyLayoutItemField() {
311                 Field primaryKey = getPrimaryKeyField();
312
313                 LayoutItem_Field libglomLayoutItemField = new LayoutItem_Field();
314
315                 if (primaryKey != null) {
316                         libglomLayoutItemField.set_full_field_details(primaryKey);
317                 } else {
318                         Log.error(document.get_database_title(), tableName,
319                                         "A primary key was not found in the FieldVector for this table.");
320                 }
321
322                 return libglomLayoutItemField;
323         }
324
325         /*
326          * Converts a Gdk::Color (16-bits per channel) to an HTML colour (8-bits per channel) by discarding the least
327          * significant 8-bits in each channel.
328          */
329         private String convertGdkColorToHtmlColour(String gdkColor) {
330                 if (gdkColor.length() == 13)
331                         return gdkColor.substring(0, 3) + gdkColor.substring(5, 7) + gdkColor.substring(9, 11);
332                 else if (gdkColor.length() == 7) {
333                         // This shouldn't happen but let's deal with it if it does.
334                         Log.warn("Expected a 13 character string but received a 7 character string. Returning received string.");
335                         return gdkColor;
336                 } else {
337                         Log.error("Did not receive a 13 or 7 character string. Returning black HTML colour code.");
338                         return "#000000";
339                 }
340         }
341
342         private static NumberFormat convertToJavaNumberFormat(NumericFormat numFormatGlom) {
343                 // TODO implement locale
344                 NumberFormat numFormatJava = NumberFormat.getInstance(Locale.ROOT);
345                 if (numFormatGlom.getM_decimal_places_restricted()) {
346                         int digits = Utils.safeLongToInt(numFormatGlom.getM_decimal_places());
347                         numFormatJava.setMinimumFractionDigits(digits);
348                         numFormatJava.setMaximumFractionDigits(digits);
349                 }
350                 numFormatJava.setGroupingUsed(numFormatGlom.getM_use_thousands_separator());
351                 return numFormatJava;
352         }
353
354 }