001: package net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent;
002:
003: /*
004: * Copyright (C) 2001-2003 Colin Bell
005: * colbell@users.sourceforge.net
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this library; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020: */
021: import java.awt.event.*;
022:
023: import java.io.FileInputStream;
024: import java.io.FileOutputStream;
025: import java.io.InputStreamReader;
026: import java.io.OutputStreamWriter;
027: import java.io.IOException;
028:
029: import javax.swing.JTable;
030: import javax.swing.JTextField;
031: import javax.swing.JTextArea;
032: import javax.swing.SwingUtilities;
033: import javax.swing.text.JTextComponent;
034: import java.sql.PreparedStatement;
035: import java.sql.ResultSet;
036: import java.text.NumberFormat;
037: import java.math.BigDecimal;
038:
039: import net.sourceforge.squirrel_sql.fw.datasetviewer.CellDataPopup;
040: import net.sourceforge.squirrel_sql.fw.datasetviewer.ColumnDisplayDefinition;
041: import net.sourceforge.squirrel_sql.fw.sql.ISQLDatabaseMetaData;
042: import net.sourceforge.squirrel_sql.fw.util.log.ILogger;
043: import net.sourceforge.squirrel_sql.fw.util.log.LoggerController;
044:
045: /**
046: * @author gwg
047: *
048: * This class provides the display components for handling Float data types,
049: * specifically SQL types REAL.
050: * Note: The java.sun.com site recommends using float for SQL type REAL
051: * and data type double for SQL type FLOAT.
052: * The display components are for:
053: * <UL>
054: * <LI> read-only display within a table cell
055: * <LI> editing within a table cell
056: * <LI> read-only or editing display within a separate window
057: * </UL>
058: * The class also contains
059: * <UL>
060: * <LI> a function to compare two display values
061: * to see if they are equal. This is needed because the display format
062: * may not be the same as the internal format, and all internal object
063: * types may not provide an appropriate equals() function.
064: * <LI> a function to return a printable text form of the cell contents,
065: * which is used in the text version of the table.
066: * </UL>
067: * <P>
068: * The components returned from this class extend RestorableJTextField
069: * and RestorableJTextArea for use in editing table cells that
070: * contain values of this data type. It provides the special behavior for null
071: * handling and resetting the cell to the original value.
072: */
073:
074: public class DataTypeFloat extends FloatingPointBase implements
075: IDataTypeComponent {
076:
077: /** Logger for this class. */
078: private static ILogger s_log = LoggerController
079: .createLogger(DataTypeFloat.class);
080:
081: /* the whole column definition */
082: private ColumnDisplayDefinition _colDef;
083:
084: /* whether nulls are allowed or not */
085: private boolean _isNullable;
086:
087: /* the total number of digits allowed in the field */
088: private int _precision;
089:
090: /* the number of decimal digits allowed flollowing the decimal point */
091: private int _scale;
092:
093: /* table of which we are part (needed for creating popup dialog) */
094: private JTable _table;
095:
096: /* The JTextComponent that is being used for editing */
097: private IRestorableTextComponent _textComponent;
098:
099: /* The CellRenderer used for this data type */
100: //??? For now, use the same renderer as everyone else.
101: //??
102: //?? IN FUTURE: change this to use a new instance of renederer
103: //?? for this data type.
104: private DefaultColumnRenderer _renderer = DefaultColumnRenderer
105: .getInstance();
106:
107: // The NumberFormat object to use for all locale-dependent formatting.
108: private NumberFormat _numberFormat;
109: private boolean _renderExceptionHasBeenLogged;
110:
111: /**
112: * Constructor - save the data needed by this data type.
113: */
114: public DataTypeFloat(JTable table, ColumnDisplayDefinition colDef) {
115: _table = table;
116: _colDef = colDef;
117: _isNullable = colDef.isNullable();
118: _precision = colDef.getPrecision();
119: _scale = colDef.getScale();
120:
121: _numberFormat = NumberFormat.getInstance();
122:
123: // This is a bit hard coded but if we use _scale here
124: // some number displays go crazy.
125: _numberFormat.setMaximumFractionDigits(5);
126:
127: _numberFormat.setMinimumFractionDigits(0);
128:
129: }
130:
131: /**
132: * Return the name of the java class used to hold this data type.
133: */
134: public String getClassName() {
135: return "java.lang.Float";
136: }
137:
138: /**
139: * Determine if two objects of this data type contain the same value.
140: * Neither of the objects is null
141: */
142: public boolean areEqual(Object obj1, Object obj2) {
143: return obj1.equals(obj2);
144: }
145:
146: /*
147: * First we have the methods for in-cell and Text-table operations
148: */
149:
150: /**
151: * Render a value into text for this DataType.
152: */
153: public String renderObject(Object value) {
154:
155: //return (String)_renderer.renderObject(value);
156:
157: if (value == null || useJavaDefaultFormat) {
158: return (String) _renderer.renderObject(value);
159: } else {
160:
161: try {
162: return (String) _renderer.renderObject(_numberFormat
163: .format(value));
164: } catch (Exception e) {
165: if (false == _renderExceptionHasBeenLogged) {
166: _renderExceptionHasBeenLogged = true;
167: s_log.error("Could not format \"" + value
168: + "\" as number type", e);
169: }
170: return (String) _renderer.renderObject(value);
171: }
172:
173: }
174:
175: }
176:
177: /**
178: * This Data Type can be edited in a table cell.
179: */
180: public boolean isEditableInCell(Object originalValue) {
181: return true;
182: }
183:
184: /**
185: * See if a value in a column has been limited in some way and
186: * needs to be re-read before being used for editing.
187: * For read-only tables this may actually return true since we want
188: * to be able to view the entire contents of the cell even if it was not
189: * completely loaded during the initial table setup.
190: */
191: public boolean needToReRead(Object originalValue) {
192: // this DataType does not limit the data read during the initial load of the table,
193: // so there is no need to re-read the complete data later
194: return false;
195: }
196:
197: /**
198: * Return a JTextField usable in a CellEditor.
199: */
200: public JTextField getJTextField() {
201: _textComponent = new RestorableJTextField();
202:
203: // special handling of operations while editing this data type
204: ((RestorableJTextField) _textComponent)
205: .addKeyListener(new KeyTextHandler());
206:
207: //
208: // handle mouse events for double-click creation of popup dialog.
209: // This happens only in the JTextField, not the JTextArea, so we can
210: // make this an inner class within this method rather than a separate
211: // inner class as is done with the KeyTextHandler class.
212: //
213: ((RestorableJTextField) _textComponent)
214: .addMouseListener(new MouseAdapter() {
215: public void mousePressed(MouseEvent evt) {
216: if (evt.getClickCount() == 2) {
217: MouseEvent tableEvt = SwingUtilities
218: .convertMouseEvent(
219: (RestorableJTextField) DataTypeFloat.this ._textComponent,
220: evt,
221: DataTypeFloat.this ._table);
222: CellDataPopup.showDialog(
223: DataTypeFloat.this ._table,
224: DataTypeFloat.this ._colDef,
225: tableEvt, true);
226: }
227: }
228: }); // end of mouse listener
229:
230: return (JTextField) _textComponent;
231: }
232:
233: /**
234: * Implement the interface for validating and converting to internal object.
235: * Null is a valid successful return, so errors are indicated only by
236: * existance or not of a message in the messageBuffer.
237: */
238: public Object validateAndConvert(String value,
239: Object originalValue, StringBuffer messageBuffer) {
240: // handle null, which is shown as the special string "<null>"
241: if (value.equals("<null>") || value.equals(""))
242: return null;
243:
244: // Do the conversion into the object in a safe manner
245: try {
246: Float obj;
247:
248: if (useJavaDefaultFormat) {
249: obj = new Float(value);
250: } else {
251: obj = new Float("" + _numberFormat.parse(value));
252: }
253: return obj;
254: } catch (Exception e) {
255: messageBuffer.append(e.toString() + "\n");
256: //?? do we need the message also, or is it automatically part of the toString()?
257: //messageBuffer.append(e.getMessage());
258: return null;
259: }
260: }
261:
262: /**
263: * If true, this tells the PopupEditableIOPanel to use the
264: * binary editing panel rather than a pure text panel.
265: * The binary editing panel assumes the data is an array of bytes,
266: * converts it into text form, allows the user to change how that
267: * data is displayed (e.g. Hex, Decimal, etc.), and converts
268: * the data back from text to bytes when the user editing is completed.
269: * If this returns false, this DataType class must
270: * convert the internal data into a text string that
271: * can be displayed (and edited, if allowed) in a TextField
272: * or TextArea, and must handle all
273: * user key strokes related to editing of that data.
274: */
275: public boolean useBinaryEditingPanel() {
276: return false;
277: }
278:
279: /*
280: * Now the functions for the Popup-related operations.
281: */
282:
283: /**
284: * Returns true if data type may be edited in the popup,
285: * false if not.
286: */
287: public boolean isEditableInPopup(Object originalValue) {
288: return true;
289: }
290:
291: /*
292: * Return a JTextArea usable in the CellPopupDialog
293: * and fill in the value.
294: */
295: public JTextArea getJTextArea(Object value) {
296: _textComponent = new RestorableJTextArea();
297:
298: // value is a simple string representation of the data,
299: // the same one used in Text and in-cell operations.
300: ((RestorableJTextArea) _textComponent)
301: .setText(renderObject(value));
302:
303: // special handling of operations while editing this data type
304: ((RestorableJTextArea) _textComponent)
305: .addKeyListener(new KeyTextHandler());
306:
307: return (RestorableJTextArea) _textComponent;
308: }
309:
310: /**
311: * Validating and converting in Popup is identical to cell-related operation.
312: */
313: public Object validateAndConvertInPopup(String value,
314: Object originalValue, StringBuffer messageBuffer) {
315: return validateAndConvert(value, originalValue, messageBuffer);
316: }
317:
318: /*
319: * The following is used in both cell and popup operations.
320: */
321:
322: /*
323: * Internal class for handling key events during editing
324: * of both JTextField and JTextArea.
325: */
326: private class KeyTextHandler extends BaseKeyTextHandler {
327: public void keyTyped(KeyEvent e) {
328: char c = e.getKeyChar();
329:
330: // as a coding convenience, create a reference to the text component
331: // that is typecast to JTextComponent. this is not essential, as we
332: // could typecast every reference, but this makes the code cleaner
333: JTextComponent _theComponent = (JTextComponent) DataTypeFloat.this ._textComponent;
334: String text = _theComponent.getText();
335:
336: // tabs and newlines get put into the text before this check,
337: // so remove them
338: // This only applies to Popup editing since these chars are
339: // not passed to this level by the in-cell editor.
340: if (c == KeyEvent.VK_TAB || c == KeyEvent.VK_ENTER) {
341: // remove all instances of the offending char
342: int index = text.indexOf(c);
343: if (index != -1) {
344: if (index == text.length() - 1) {
345: text = text.substring(0, text.length() - 1); // truncate string
346: } else {
347: text = text.substring(0, index)
348: + text.substring(index + 1);
349: }
350: ((IRestorableTextComponent) _theComponent)
351: .updateText(text);
352: _theComponent.getToolkit().beep();
353: }
354: e.consume();
355: }
356:
357: if (!(Character.isDigit(c) || (c == '-') || (c == '+')
358: || (c == 'e') || (c == 'E') || (c == 'f')
359: || (c == 'F') || (c == '.') || (c == ',') || // several number formats use '.' as decimal separator, others use ','
360: (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
361: _theComponent.getToolkit().beep();
362: e.consume();
363: }
364:
365: // handle cases of null
366: // The processing is different when nulls are allowed and when they are not.
367: //
368:
369: if (DataTypeFloat.this ._isNullable) {
370:
371: // user enters something when field is null
372: if (text.equals("<null>")) {
373: if ((c == KeyEvent.VK_BACK_SPACE)
374: || (c == KeyEvent.VK_DELETE)) {
375: // delete when null => original value
376: DataTypeFloat.this ._textComponent.restoreText();
377: e.consume();
378: } else {
379: // non-delete when null => clear field and add text
380: DataTypeFloat.this ._textComponent
381: .updateText("");
382: // fall through to normal processing of this key stroke
383: }
384: } else {
385: // check for user deletes last thing in field
386: if ((c == KeyEvent.VK_BACK_SPACE)
387: || (c == KeyEvent.VK_DELETE)) {
388: if (text.length() <= 1) {
389: // about to delete last thing in field, so replace with null
390: DataTypeFloat.this ._textComponent
391: .updateText("<null>");
392: e.consume();
393: }
394: }
395: }
396: } else {
397: // field is not nullable
398: //
399: handleNotNullableField(text, c, e, _textComponent);
400: }
401: }
402: }
403:
404: /*
405: * DataBase-related functions
406: */
407:
408: /**
409: * On input from the DB, read the data from the ResultSet into the appropriate
410: * type of object to be stored in the table cell.
411: */
412: public Object readResultSet(ResultSet rs, int index,
413: boolean limitDataRead) throws java.sql.SQLException {
414:
415: float data = rs.getFloat(index);
416: if (rs.wasNull())
417: return null;
418: else
419: return new Float(data);
420: }
421:
422: /**
423: * When updating the database, generate a string form of this object value
424: * that can be used in the WHERE clause to match the value in the database.
425: * A return value of null means that this column cannot be used in the WHERE
426: * clause, while a return of "null" (or "is null", etc) means that the column
427: * can be used in the WHERE clause and the value is actually a null value.
428: * This function must also include the column label so that its output
429: * is of the form:
430: * "columnName = value"
431: * or
432: * "columnName is null"
433: * or whatever is appropriate for this column in the database.
434: */
435: public String getWhereClauseValue(Object value,
436: ISQLDatabaseMetaData md) {
437: if (value == null || value.toString() == null
438: || value.toString().length() == 0)
439: return _colDef.getLabel() + " IS NULL";
440: else
441: // since we cannot do exact matches on floating point
442: // numbers, we cannot use this field in the WHERE clause.
443: return "";
444: // return _colDef.getLabel() + "=" + value.toString();
445: }
446:
447: /**
448: * When updating the database, insert the appropriate datatype into the
449: * prepared statment at the given variable position.
450: */
451: public void setPreparedStatementValue(PreparedStatement pstmt,
452: Object value, int position) throws java.sql.SQLException {
453: if (value == null) {
454: pstmt.setNull(position, _colDef.getSqlType());
455: } else {
456: pstmt.setFloat(position, ((Float) value).floatValue());
457: }
458: }
459:
460: /**
461: * Get a default value for the table used to input data for a new row
462: * to be inserted into the DB.
463: */
464: public Object getDefaultValue(String dbDefaultValue) {
465: if (dbDefaultValue != null) {
466: // try to use the DB default value
467: StringBuffer mbuf = new StringBuffer();
468: Object newObject = validateAndConvert(dbDefaultValue, null,
469: mbuf);
470:
471: // if there was a problem with converting, then just fall through
472: // and continue as if there was no default given in the DB.
473: // Otherwise, use the converted object
474: if (mbuf.length() == 0)
475: return newObject;
476: }
477:
478: // no default in DB. If nullable, use null.
479: if (_isNullable)
480: return null;
481:
482: // field is not nullable, so create a reasonable default value
483: return new Float(0);
484: }
485:
486: /*
487: * File IO related functions
488: */
489:
490: /**
491: * Say whether or not object can be exported to and imported from
492: * a file. We put both export and import together in one test
493: * on the assumption that all conversions can be done both ways.
494: */
495: public boolean canDoFileIO() {
496: return true;
497: }
498:
499: /**
500: * Read a file and construct a valid object from its contents.
501: * Errors are returned by throwing an IOException containing the
502: * cause of the problem as its message.
503: * <P>
504: * DataType is responsible for validating that the imported
505: * data can be converted to an object, and then must return
506: * a text string that can be used in the Popup window text area.
507: * This object-to-text conversion is the same as is done by
508: * the DataType object internally in the getJTextArea() method.
509: *
510: * <P>
511: * File is assumed to be and ASCII string of digits
512: * representing a value of this data type.
513: */
514: public String importObject(FileInputStream inStream)
515: throws IOException {
516:
517: InputStreamReader inReader = new InputStreamReader(inStream);
518:
519: int fileSize = inStream.available();
520:
521: char charBuf[] = new char[fileSize];
522:
523: int count = inReader.read(charBuf, 0, fileSize);
524:
525: if (count != fileSize)
526: throw new IOException("Could read only " + count
527: + " chars from a total file size of " + fileSize
528: + ". Import failed.");
529:
530: // convert file text into a string
531: // Special case: some systems tack a newline at the end of
532: // the text read. Assume that if last char is a newline that
533: // we want everything else in the line.
534: String fileText;
535: if (charBuf[count - 1] == KeyEvent.VK_ENTER)
536: fileText = new String(charBuf, 0, count - 1);
537: else
538: fileText = new String(charBuf);
539:
540: // test that the string is valid by converting it into an
541: // object of this data type
542: StringBuffer messageBuffer = new StringBuffer();
543: validateAndConvertInPopup(fileText, null, messageBuffer);
544: if (messageBuffer.length() > 0) {
545: // convert number conversion issue into IO issue for consistancy
546: throw new IOException(
547: "Text does not represent data of type "
548: + getClassName() + ". Text was:\n"
549: + fileText);
550: }
551:
552: // return the text from the file since it does
553: // represent a valid data value
554: return fileText;
555: }
556:
557: /**
558: * Construct an appropriate external representation of the object
559: * and write it to a file.
560: * Errors are returned by throwing an IOException containing the
561: * cause of the problem as its message.
562: * <P>
563: * DataType is responsible for validating that the given text
564: * text from a Popup JTextArea can be converted to an object.
565: * This text-to-object conversion is the same as validateAndConvertInPopup,
566: * which may be used internally by the object to do the validation.
567: * <P>
568: * The DataType object must flush and close the output stream before returning.
569: * Typically it will create another object (e.g. an OutputWriter), and
570: * that is the object that must be flushed and closed.
571: *
572: * <P>
573: * File is assumed to be and ASCII string of digits
574: * representing a value of this data type.
575: */
576: public void exportObject(FileOutputStream outStream, String text)
577: throws IOException {
578:
579: OutputStreamWriter outWriter = new OutputStreamWriter(outStream);
580:
581: // check that the text is a valid representation
582: StringBuffer messageBuffer = new StringBuffer();
583: validateAndConvertInPopup(text, null, messageBuffer);
584: if (messageBuffer.length() > 0) {
585: // there was an error in the conversion
586: throw new IOException(new String(messageBuffer));
587: }
588:
589: // just send the text to the output file
590: outWriter.write(text);
591: outWriter.flush();
592: outWriter.close();
593: }
594: }
|