net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent.DataTypeBigDecimal.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent.DataTypeBigDecimal.java

Source

package net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent;

/*
 * Copyright (C) 2001-2003 Colin Bell
 * colbell@users.sourceforge.net
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.text.NumberFormat;

import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;

import org.apache.commons.lang.StringUtils;

import net.sourceforge.squirrel_sql.fw.datasetviewer.CellDataPopup;
import net.sourceforge.squirrel_sql.fw.datasetviewer.ColumnDisplayDefinition;
import net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent.whereClause.IWhereClausePart;
import net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent.whereClause.IsNullWhereClausePart;
import net.sourceforge.squirrel_sql.fw.datasetviewer.cellcomponent.whereClause.ParameterWhereClausePart;
import net.sourceforge.squirrel_sql.fw.sql.ISQLDatabaseMetaData;
import net.sourceforge.squirrel_sql.fw.util.StringManager;
import net.sourceforge.squirrel_sql.fw.util.StringManagerFactory;
import net.sourceforge.squirrel_sql.fw.util.log.ILogger;
import net.sourceforge.squirrel_sql.fw.util.log.LoggerController;

/**
 * @author gwg
 *
 * This class provides the display components for handling BigDecimal data types,
 * specifically SQL types DECIMAL and NUMERIC.
 * The display components are for:
 * <UL>
 * <LI> read-only display within a table cell
 * <LI> editing within a table cell
 * <LI> read-only or editing display within a separate window
 * </UL>
 * The class also contains 
 * <UL>
 * <LI> a function to compare two display values
 * to see if they are equal.  This is needed because the display format
 * may not be the same as the internal format, and all internal object
 * types may not provide an appropriate equals() function.
 * <LI> a function to return a printable text form of the cell contents,
 * which is used in the text version of the table.
 * </UL>
 * <P>
 * The components returned from this class extend RestorableJTextField
 * and RestorableJTextArea for use in editing table cells that
 * contain BigDecimalr values.  It provides the special behavior for null
 * handling and resetting the cell to the original value.
 */

public class DataTypeBigDecimal extends FloatingPointBase implements IDataTypeComponent {
    private static final StringManager s_stringMgr = StringManagerFactory
            .getStringManager(DataTypeBigDecimal.class);

    /** Logger for this class. */
    private static ILogger s_log = LoggerController.createLogger(DataTypeBigDecimal.class);

    /* whether nulls are allowed or not */
    private boolean _isNullable;

    /* The total number of decimal digits allowed in the number */
    private int _precision;

    /* the number of decimal digits allowed to the right of the decimal point
        * in the number
        */
    private int _scale;

    /* table of which we are part (needed for creating popup dialog) */
    private JTable _table;

    /* The JTextComponent that is being used for editing */
    private IRestorableTextComponent _textComponent;

    /* The CellRenderer used for this data type */
    //??? For now, use the same renderer as everyone else.
    //??
    //?? IN FUTURE: change this to use a new instance of renederer
    //?? for this data type.
    private DefaultColumnRenderer _renderer = DefaultColumnRenderer.getInstance();

    // The NumberFormat object to use for all locale-dependent formatting.
    private NumberFormat _numberFormat;
    private boolean _renderExceptionHasBeenLogged;

    /**
     * Constructor - save the data needed by this data type.
     */
    public DataTypeBigDecimal(JTable table, ColumnDisplayDefinition colDef) {
        _table = table;
        _colDef = colDef;
        _isNullable = colDef.isNullable();
        _precision = colDef.getPrecision();
        _scale = colDef.getScale();

        _numberFormat = NumberFormat.getInstance();

        // If we use _scale here some number displays go crazy.
        //_numberFormat.setMaximumFractionDigits(_scale);
        _numberFormat.setMaximumFractionDigits(maximumFractionDigits);

        _numberFormat.setMinimumFractionDigits(0);

    }

    /**
     * Return the name of the java class used to hold this data type.
     */
    public String getClassName() {
        return "java.math.BigDecimal";
    }

    /*
     * First we have the methods for in-cell and Text-table operations
     */

    /**
     * Render a value into text for this DataType.
     */
    public String renderObject(Object value) {

        //return (String)_renderer.renderObject(value);

        if (value == null || useJavaDefaultFormat) {
            return (String) _renderer.renderObject(value);
        } else {

            try {
                return (String) _renderer.renderObject(_numberFormat.format(value));
            } catch (Exception e) {
                if (false == _renderExceptionHasBeenLogged) {
                    _renderExceptionHasBeenLogged = true;
                    s_log.error("Could not format \"" + value + "\" as number type", e);
                }
                return (String) _renderer.renderObject(value);
            }

        }

    }

    /**
     * This Data Type can be edited in a table cell.
     */
    public boolean isEditableInCell(Object originalValue) {
        return true;
    }

    /**
     * See if a value in a column has been limited in some way and
     * needs to be re-read before being used for editing.
     * For read-only tables this may actually return true since we want
     * to be able to view the entire contents of the cell even if it was not
     * completely loaded during the initial table setup.
     */
    public boolean needToReRead(Object originalValue) {
        // this DataType does not limit the data read during the initial load of the table,
        // so there is no need to re-read the complete data later
        return false;
    }

    /**
     * Return a JTextField usable in a CellEditor.
     */
    public JTextField getJTextField() {
        _textComponent = new RestorableJTextField();

        // special handling of operations while editing this data type
        ((RestorableJTextField) _textComponent).addKeyListener(new KeyTextHandler());

        //
        // handle mouse events for double-click creation of popup dialog.
        // This happens only in the JTextField, not the JTextArea, so we can
        // make this an inner class within this method rather than a separate
        // inner class as is done with the KeyTextHandler class.
        //
        ((RestorableJTextField) _textComponent).addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent evt) {
                if (evt.getClickCount() == 2) {
                    MouseEvent tableEvt = SwingUtilities.convertMouseEvent(
                            (RestorableJTextField) DataTypeBigDecimal.this._textComponent, evt,
                            DataTypeBigDecimal.this._table);
                    CellDataPopup.showDialog(DataTypeBigDecimal.this._table, DataTypeBigDecimal.this._colDef,
                            tableEvt, true);
                }
            }
        }); // end of mouse listener

        return (JTextField) _textComponent;
    }

    /**
     * Implement the interface for validating and converting to internal object.
     * Null is a valid successful return, so errors are indicated only by
     * existance or not of a message in the messageBuffer.
     */
    public Object validateAndConvert(String value, Object originalValue, StringBuffer messageBuffer) {
        // handle null, which is shown as the special string "<null>"
        if (value.equals("<null>") || value.equals(""))
            return null;

        // Do the conversion into the object in a safe manner
        try {
            BigDecimal obj;

            if (useJavaDefaultFormat) {
                obj = new BigDecimal(value);
            } else {
                obj = new BigDecimal("" + _numberFormat.parse(value));
            }

            // Some DBs give a negative number when they do not have a value for
            // the scale.  Assume that if the _scale is 0 or positive that the DB really
            // means for that to be the scale, but if it is negative then we do not check.
            if (_scale >= 0 && obj.scale() > _scale) {
                Object[] args = new Object[] { Integer.valueOf(obj.scale()), Integer.valueOf(_scale) };
                // i18n[dataTypeBigDecimal.scaleEceeded=Scale Exceeded: Number 
                //of digits to right of decimal place ({0})\nis greater than 
                //allowed in column ({1}).]
                String msg = s_stringMgr.getString("dataTypeBigDecimal.scaleEceeded", args);

                messageBuffer.append(msg);
                return null;
            }

            // check the total number of digits in the number.
            // Since the string version of the number is therepresentation of
            // the digits in that number and including possibly a plus or minus
            // and a decimal, start by counting the number of digits in the string.
            int objPrecision = value.length();
            // now remove the non-digit chars, if any
            if (value.indexOf("+") > -1 || value.indexOf("-") > -1)
                objPrecision--;
            if (value.indexOf(".") > -1)
                objPrecision--;

            // Some drivers (e.g. Oracle) give precision as 0 in some cases.
            // When precision is 0, we cannot check the length, so do not try.
            if (_precision > 0 && objPrecision > _precision) {
                Object[] args = new Object[] { Integer.valueOf(objPrecision), Integer.valueOf(_precision) };
                // i18n[dataTypeBigDecimal.precisionEceeded=Precision Exceeded: 
                //Number of digits in number ({0})\nis greater than allowed in 
                //column ({1})]
                String msg = s_stringMgr.getString("dataTypeBigDecimal.precisionEceeded", args);

                messageBuffer.append(msg);
                return null;
            }
            return obj;
        } catch (Exception e) {
            messageBuffer.append(e.toString() + "\n");
            //?? do we need the message also, or is it automatically part of the toString()?
            //messageBuffer.append(e.getMessage());
            return null;
        }
    }

    /**
     * If true, this tells the PopupEditableIOPanel to use the
     * binary editing panel rather than a pure text panel.
     * The binary editing panel assumes the data is an array of bytes,
     * converts it into text form, allows the user to change how that
     * data is displayed (e.g. Hex, Decimal, etc.), and converts
     * the data back from text to bytes when the user editing is completed.
     * If this returns false, this DataType class must
     * convert the internal data into a text string that
     * can be displayed (and edited, if allowed) in a TextField
     * or TextArea, and must handle all
     * user key strokes related to editing of that data.
     */
    public boolean useBinaryEditingPanel() {
        return false;
    }

    /*
        * Now the functions for the Popup-related operations.
        */

    /**
     * Returns true if data type may be edited in the popup,
     * false if not.
     */
    public boolean isEditableInPopup(Object originalValue) {
        return true;
    }

    /*
     * Return a JTextArea usable in the CellPopupDialog
     * and fill in the value.
     */
    public JTextArea getJTextArea(Object value) {
        _textComponent = new RestorableJTextArea();

        // value is a simple string representation of the data,
        // the same one used in Text and in-cell operations.
        ((RestorableJTextArea) _textComponent).setText(renderObject(value));

        // special handling of operations while editing this data type
        ((RestorableJTextArea) _textComponent).addKeyListener(new KeyTextHandler());

        return (RestorableJTextArea) _textComponent;
    }

    /**
     * Validating and converting in Popup is identical to cell-related operation.
     */
    public Object validateAndConvertInPopup(String value, Object originalValue, StringBuffer messageBuffer) {
        return validateAndConvert(value, originalValue, messageBuffer);
    }

    /*
     * The following is used in both cell and popup operations.
     */

    /*
        * Internal class for handling key events during editing
        * of both JTextField and JTextArea.
        */
    private class KeyTextHandler extends BaseKeyTextHandler {

        public void keyTyped(KeyEvent e) {
            char c = e.getKeyChar();

            // as a coding convenience, create a reference to the text component
            // that is typecast to JTextComponent.  this is not essential, as we
            // could typecast every reference, but this makes the code cleaner
            JTextComponent _theComponent = (JTextComponent) DataTypeBigDecimal.this._textComponent;
            String text = _theComponent.getText();

            // tabs and newlines get put into the text before this check,
            // so remove them
            // This only applies to Popup editing since these chars are
            // not passed to this level by the in-cell editor.
            if (c == KeyEvent.VK_TAB || c == KeyEvent.VK_ENTER) {
                // remove all instances of the offending char
                int index = text.indexOf(c);

                if (-1 != index) {
                    if (index == text.length() - 1) {
                        text = text.substring(0, text.length() - 1); // truncate string
                    } else {
                        text = text.substring(0, index) + text.substring(index + 1);
                    }
                    ((IRestorableTextComponent) _theComponent).updateText(text);
                    _beepHelper.beep(_theComponent);
                    e.consume();
                }
            }

            checkSignCharacter(e, _theComponent, _colDef, _beepHelper);

            if (!(Character.isDigit(c) || (isSignCharacter(c)) || (c == '.') || (c == ',') || // several number formats use '.' as decimal separator, others use ','
                    (c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
                _beepHelper.beep(_theComponent);
                e.consume();
            }

            // handle cases of null
            // The processing is different when nulls are allowed and when they are not.
            //

            if (DataTypeBigDecimal.this._isNullable) {

                // user enters something when field is null
                if (text.equals("<null>")) {
                    if ((c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE)) {
                        // delete when null => original value
                        DataTypeBigDecimal.this._textComponent.restoreText();
                        e.consume();
                    } else {
                        // non-delete when null => clear field and add text
                        DataTypeBigDecimal.this._textComponent.updateText("");
                        // fall through to normal processing of this key stroke
                    }
                } else {
                    // check for user deletes last thing in field
                    if ((c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE)) {
                        if (text.length() <= 1) {
                            // about to delete last thing in field, so replace with null
                            DataTypeBigDecimal.this._textComponent.updateText("<null>");
                            e.consume();
                        }
                    }
                }
            } else {
                // field is not nullable
                //
                handleNotNullableField(text, c, e, _textComponent);
            }
        }

    }

    /*
        * DataBase-related functions
        */

    /**
     * On input from the DB, read the data from the ResultSet into the appropriate
     * type of object to be stored in the table cell.
     */
    public Object readResultSet(ResultSet rs, int index, boolean limitDataRead) throws java.sql.SQLException {

        BigDecimal data = rs.getBigDecimal(index);
        if (rs.wasNull())
            return null;
        else
            return data;
    }

    /**
     * When updating the database, generate a string form of this object value
     * that can be used in the WHERE clause to match the value in the database.
     * A return value of null means that this column cannot be used in the WHERE
     * clause, while a return of "null" (or "is null", etc) means that the column
     * can be used in the WHERE clause and the value is actually a null value.
     * This function must also include the column label so that its output
     * is of the form:
     *    "columnName = value"
     * or
     *    "columnName is null"
     * or whatever is appropriate for this column in the database.
     */
    public IWhereClausePart getWhereClauseValue(Object value, ISQLDatabaseMetaData md) {
        if (value == null || value.toString() == null || value.toString().length() == 0)
            return new IsNullWhereClausePart(_colDef);
        else
            return new ParameterWhereClausePart(_colDef, value, this);
    }

    /**
     * When updating the database, insert the appropriate datatype into the
     * prepared statment at the given variable position.
     */
    public void setPreparedStatementValue(PreparedStatement pstmt, Object value, int position)
            throws java.sql.SQLException {
        if (value == null) {
            pstmt.setNull(position, _colDef.getSqlType());
        } else {
            pstmt.setBigDecimal(position, (BigDecimal) value);
        }
    }

    /**
     * Get a default value for the table used to input data for a new row
     * to be inserted into the DB.
     */
    public Object getDefaultValue(String dbDefaultValue) {
        if (dbDefaultValue != null) {
            // try to use the DB default value
            StringBuffer mbuf = new StringBuffer();
            Object newObject = validateAndConvert(dbDefaultValue, null, mbuf);

            // if there was a problem with converting, then just fall through
            // and continue as if there was no default given in the DB.
            // Otherwise, use the converted object
            if (mbuf.length() == 0)
                return newObject;
        }

        // no default in DB.  If nullable, use null.
        if (_isNullable)
            return null;

        // field is not nullable, so create a reasonable default value
        return BigDecimal.ZERO;
    }

    /*
        * File IO related functions
        */

    /**
     * Say whether or not object can be exported to and imported from
     * a file.  We put both export and import together in one test
     * on the assumption that all conversions can be done both ways.
     */
    public boolean canDoFileIO() {
        return true;
    }

    /**
     * Read a file and construct a valid object from its contents.
     * Errors are returned by throwing an IOException containing the
     * cause of the problem as its message.
     * <P>
     * DataType is responsible for validating that the imported
     * data can be converted to an object, and then must return
     * a text string that can be used in the Popup window text area.
     * This object-to-text conversion is the same as is done by
     * the DataType object internally in the getJTextArea() method.
     * 
     * <P>
     * File is assumed to be and ASCII string of digits
     * representing a value of this data type.
     */
    public String importObject(FileInputStream inStream) throws IOException {

        InputStreamReader inReader = new InputStreamReader(inStream);

        int fileSize = inStream.available();

        char charBuf[] = new char[fileSize];

        int count = inReader.read(charBuf, 0, fileSize);

        if (count != fileSize)
            throw new IOException("Could read only " + count + " chars from a total file size of " + fileSize
                    + ". Import failed.");

        // convert file text into a string
        // Special case: some systems tack a newline at the end of
        // the text read.  Assume that if last char is a newline that
        // we want everything else in the line.
        String fileText;
        if (charBuf[count - 1] == KeyEvent.VK_ENTER)
            fileText = new String(charBuf, 0, count - 1);
        else
            fileText = new String(charBuf);

        // test that the string is valid by converting it into an
        // object of this data type
        StringBuffer messageBuffer = new StringBuffer();
        validateAndConvertInPopup(fileText, null, messageBuffer);
        if (messageBuffer.length() > 0) {
            ;
            // convert number conversion issue into IO issue for consistancy
            throw new IOException(
                    "Text does not represent data of type " + getClassName() + ".  Text was:\n" + fileText);
        }

        // return the text from the file since it does
        // represent a valid value of this data type
        return fileText;
    }

    /**
     * Construct an appropriate external representation of the object
     * and write it to a file.
     * Errors are returned by throwing an IOException containing the
     * cause of the problem as its message.
     * <P>
     * DataType is responsible for validating that the given text
     * text from a Popup JTextArea can be converted to an object.
     * This text-to-object conversion is the same as validateAndConvertInPopup,
     * which may be used internally by the object to do the validation.
     * <P>
     * The DataType object must flush and close the output stream before returning.
     * Typically it will create another object (e.g. an OutputWriter), and
     * that is the object that must be flushed and closed.
     * 
     * <P>
     * File is assumed to be and ASCII string of digits
     * representing a value of this data type.
     */
    public void exportObject(FileOutputStream outStream, String text) throws IOException {

        OutputStreamWriter outWriter = new OutputStreamWriter(outStream);

        // check that the text is a valid representation
        StringBuffer messageBuffer = new StringBuffer();
        validateAndConvertInPopup(text, null, messageBuffer);
        if (messageBuffer.length() > 0) {
            // there was an error in the conversion
            throw new IOException(new String(messageBuffer));
        }

        // just send the text to the output file
        outWriter.write(text);
        outWriter.flush();
        outWriter.close();
    }
}