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