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.KeyEvent;
022: import java.awt.event.MouseAdapter;
023: import java.awt.event.MouseEvent;
024: import java.io.FileInputStream;
025: import java.io.FileOutputStream;
026: import java.io.IOException;
027: import java.io.InputStreamReader;
028: import java.io.OutputStreamWriter;
029: import java.sql.PreparedStatement;
030: import java.sql.ResultSet;
031: import java.text.NumberFormat;
032:
033: import javax.swing.JTable;
034: import javax.swing.JTextArea;
035: import javax.swing.JTextField;
036: import javax.swing.SwingUtilities;
037: import javax.swing.text.JTextComponent;
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 Double 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 DataTypeDouble extends FloatingPointBase implements
075: IDataTypeComponent {
076:
077: /** Logger for this class. */
078: private static ILogger s_log = LoggerController
079: .createLogger(DataTypeBigDecimal.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 DataTypeDouble(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.Double";
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) DataTypeDouble.this ._textComponent,
220: evt,
221: DataTypeDouble.this ._table);
222: CellDataPopup.showDialog(
223: DataTypeDouble.this ._table,
224: DataTypeDouble.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: Double obj;
247:
248: if (useJavaDefaultFormat) {
249: obj = new Double(value);
250: } else {
251: obj = new Double("" + _numberFormat.parse(value));
252: }
253:
254: return obj;
255: } catch (Exception e) {
256: messageBuffer.append(e.toString() + "\n");
257: //?? do we need the message also, or is it automatically part of the toString()?
258: //messageBuffer.append(e.getMessage());
259: return null;
260: }
261: }
262:
263: /**
264: * If true, this tells the PopupEditableIOPanel to use the
265: * binary editing panel rather than a pure text panel.
266: * The binary editing panel assumes the data is an array of bytes,
267: * converts it into text form, allows the user to change how that
268: * data is displayed (e.g. Hex, Decimal, etc.), and converts
269: * the data back from text to bytes when the user editing is completed.
270: * If this returns false, this DataType class must
271: * convert the internal data into a text string that
272: * can be displayed (and edited, if allowed) in a TextField
273: * or TextArea, and must handle all
274: * user key strokes related to editing of that data.
275: */
276: public boolean useBinaryEditingPanel() {
277: return false;
278: }
279:
280: /*
281: * Now the functions for the Popup-related operations.
282: */
283:
284: /**
285: * Returns true if data type may be edited in the popup,
286: * false if not.
287: */
288: public boolean isEditableInPopup(Object originalValue) {
289: return true;
290: }
291:
292: /*
293: * Return a JTextArea usable in the CellPopupDialog
294: * and fill in the value.
295: */
296: public JTextArea getJTextArea(Object value) {
297: _textComponent = new RestorableJTextArea();
298:
299: // value is a simple string representation of the data,
300: // the same one used in Text and in-cell operations.
301: ((RestorableJTextArea) _textComponent)
302: .setText(renderObject(value));
303:
304: // special handling of operations while editing this data type
305: ((RestorableJTextArea) _textComponent)
306: .addKeyListener(new KeyTextHandler());
307:
308: return (RestorableJTextArea) _textComponent;
309: }
310:
311: /**
312: * Validating and converting in Popup is identical to cell-related operation.
313: */
314: public Object validateAndConvertInPopup(String value,
315: Object originalValue, StringBuffer messageBuffer) {
316: return validateAndConvert(value, originalValue, messageBuffer);
317: }
318:
319: /*
320: * The following is used in both cell and popup operations.
321: */
322:
323: /*
324: * Internal class for handling key events during editing
325: * of both JTextField and JTextArea.
326: */
327: private class KeyTextHandler extends BaseKeyTextHandler {
328: public void keyTyped(KeyEvent e) {
329: char c = e.getKeyChar();
330:
331: // as a coding convenience, create a reference to the text component
332: // that is typecast to JTextComponent. this is not essential, as we
333: // could typecast every reference, but this makes the code cleaner
334: JTextComponent _theComponent = (JTextComponent) DataTypeDouble.this ._textComponent;
335: String text = _theComponent.getText();
336:
337: // tabs and newlines get put into the text before this check,
338: // so remove them
339: // This only applies to Popup editing since these chars are
340: // not passed to this level by the in-cell editor.
341: if (c == KeyEvent.VK_TAB || c == KeyEvent.VK_ENTER) {
342: // remove all instances of the offending char
343: int index = text.indexOf(c);
344: if (index != -1) {
345: if (index == text.length() - 1) {
346: text = text.substring(0, text.length() - 1); // truncate string
347: } else {
348: text = text.substring(0, index)
349: + text.substring(index + 1);
350: }
351: ((IRestorableTextComponent) _theComponent)
352: .updateText(text);
353: _theComponent.getToolkit().beep();
354: }
355: e.consume();
356: }
357:
358: if (!(Character.isDigit(c) || (c == '-') || (c == '+')
359: || (c == 'e') || (c == 'E') || (c == 'f')
360: || (c == 'F') || (c == 'd') || (c == 'D')
361: || (c == '.') || (c == ',') || // several number formats use '.' as decimal separator, others use ','
362: (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
363: _theComponent.getToolkit().beep();
364: e.consume();
365: }
366:
367: // handle cases of null
368: // The processing is different when nulls are allowed and when they are not.
369: //
370:
371: if (DataTypeDouble.this ._isNullable) {
372:
373: // user enters something when field is null
374: if (text.equals("<null>")) {
375: if ((c == KeyEvent.VK_BACK_SPACE)
376: || (c == KeyEvent.VK_DELETE)) {
377: // delete when null => original value
378: DataTypeDouble.this ._textComponent
379: .restoreText();
380: e.consume();
381: } else {
382: // non-delete when null => clear field and add text
383: DataTypeDouble.this ._textComponent
384: .updateText("");
385: // fall through to normal processing of this key stroke
386: }
387: } else {
388: // check for user deletes last thing in field
389: if ((c == KeyEvent.VK_BACK_SPACE)
390: || (c == KeyEvent.VK_DELETE)) {
391: if (text.length() <= 1) {
392: // about to delete last thing in field, so replace with null
393: DataTypeDouble.this ._textComponent
394: .updateText("<null>");
395: e.consume();
396: }
397: }
398: }
399: } else {
400: // field is not nullable
401: //
402: handleNotNullableField(text, c, e, _textComponent);
403: }
404: }
405: }
406:
407: /*
408: * DataBase-related functions
409: */
410:
411: /**
412: * On input from the DB, read the data from the ResultSet into the appropriate
413: * type of object to be stored in the table cell.
414: */
415: public Object readResultSet(ResultSet rs, int index,
416: boolean limitDataRead) throws java.sql.SQLException {
417:
418: double data = rs.getDouble(index);
419: if (rs.wasNull())
420: return null;
421: else
422: return new Double(data);
423: }
424:
425: /**
426: * When updating the database, generate a string form of this object value
427: * that can be used in the WHERE clause to match the value in the database.
428: * A return value of null means that this column cannot be used in the WHERE
429: * clause, while a return of "null" (or "is null", etc) means that the column
430: * can be used in the WHERE clause and the value is actually a null value.
431: * This function must also include the column label so that its output
432: * is of the form:
433: * "columnName = value"
434: * or
435: * "columnName is null"
436: * or whatever is appropriate for this column in the database.
437: */
438: public String getWhereClauseValue(Object value,
439: ISQLDatabaseMetaData md) {
440: if (value == null || value.toString() == null
441: || value.toString().length() == 0)
442: return _colDef.getLabel() + " IS NULL";
443: else
444: // since we cannot do exact matches on floating point
445: // numbers, we cannot use this field in the WHERE clause.
446: return null;
447: // return _colDef.getLabel() + "=" + value.toString();
448: }
449:
450: /**
451: * When updating the database, insert the appropriate datatype into the
452: * prepared statment at the given variable position.
453: */
454: public void setPreparedStatementValue(PreparedStatement pstmt,
455: Object value, int position) throws java.sql.SQLException {
456: if (value == null) {
457: pstmt.setNull(position, _colDef.getSqlType());
458: } else {
459: pstmt.setDouble(position, ((Double) value).floatValue());
460: }
461: }
462:
463: /**
464: * Get a default value for the table used to input data for a new row
465: * to be inserted into the DB.
466: */
467: public Object getDefaultValue(String dbDefaultValue) {
468: if (dbDefaultValue != null) {
469: // try to use the DB default value
470: StringBuffer mbuf = new StringBuffer();
471: Object newObject = validateAndConvert(dbDefaultValue, null,
472: mbuf);
473:
474: // if there was a problem with converting, then just fall through
475: // and continue as if there was no default given in the DB.
476: // Otherwise, use the converted object
477: if (mbuf.length() == 0)
478: return newObject;
479: }
480:
481: // no default in DB. If nullable, use null.
482: if (_isNullable)
483: return null;
484:
485: // field is not nullable, so create a reasonable default value
486: return new Double(0);
487: }
488:
489: /*
490: * File IO related functions
491: */
492:
493: /**
494: * Say whether or not object can be exported to and imported from
495: * a file. We put both export and import together in one test
496: * on the assumption that all conversions can be done both ways.
497: */
498: public boolean canDoFileIO() {
499: return true;
500: }
501:
502: /**
503: * Read a file and construct a valid object from its contents.
504: * Errors are returned by throwing an IOException containing the
505: * cause of the problem as its message.
506: * <P>
507: * DataType is responsible for validating that the imported
508: * data can be converted to an object, and then must return
509: * a text string that can be used in the Popup window text area.
510: * This object-to-text conversion is the same as is done by
511: * the DataType object internally in the getJTextArea() method.
512: *
513: * <P>
514: * File is assumed to be and ASCII string of digits
515: * representing a value of this data type.
516: */
517: public String importObject(FileInputStream inStream)
518: throws IOException {
519:
520: InputStreamReader inReader = new InputStreamReader(inStream);
521:
522: int fileSize = inStream.available();
523:
524: char charBuf[] = new char[fileSize];
525:
526: int count = inReader.read(charBuf, 0, fileSize);
527:
528: if (count != fileSize)
529: throw new IOException("Could read only " + count
530: + " chars from a total file size of " + fileSize
531: + ". Import failed.");
532:
533: // convert file text into a string
534: // Special case: some systems tack a newline at the end of
535: // the text read. Assume that if last char is a newline that
536: // we want everything else in the line.
537: String fileText;
538: if (charBuf[count - 1] == KeyEvent.VK_ENTER)
539: fileText = new String(charBuf, 0, count - 1);
540: else
541: fileText = new String(charBuf);
542:
543: // test that the string is valid by converting it into an
544: // object of this data type
545: StringBuffer messageBuffer = new StringBuffer();
546: validateAndConvertInPopup(fileText, null, messageBuffer);
547: if (messageBuffer.length() > 0) {
548: // convert number conversion issue into IO issue for consistancy
549: throw new IOException(
550: "Text does not represent data of type "
551: + getClassName() + ". Text was:\n"
552: + fileText);
553: }
554:
555: // return the text from the file since it does
556: // represent a valid data value
557: return fileText;
558: }
559:
560: /**
561: * Construct an appropriate external representation of the object
562: * and write it to a file.
563: * Errors are returned by throwing an IOException containing the
564: * cause of the problem as its message.
565: * <P>
566: * DataType is responsible for validating that the given text
567: * text from a Popup JTextArea can be converted to an object.
568: * This text-to-object conversion is the same as validateAndConvertInPopup,
569: * which may be used internally by the object to do the validation.
570: * <P>
571: * The DataType object must flush and close the output stream before returning.
572: * Typically it will create another object (e.g. an OutputWriter), and
573: * that is the object that must be flushed and closed.
574: *
575: * <P>
576: * File is assumed to be and ASCII string of digits
577: * representing a value of this data type.
578: */
579: public void exportObject(FileOutputStream outStream, String text)
580: throws IOException {
581:
582: OutputStreamWriter outWriter = new OutputStreamWriter(outStream);
583:
584: // check that the text is a valid representation
585: StringBuffer messageBuffer = new StringBuffer();
586: validateAndConvertInPopup(text, null, messageBuffer);
587: if (messageBuffer.length() > 0) {
588: // there was an error in the conversion
589: throw new IOException(new String(messageBuffer));
590: }
591:
592: // just send the text to the output file
593: outWriter.write(text);
594: outWriter.flush();
595: outWriter.close();
596: }
597: }
|