Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.empire.db; import java.lang.reflect.InvocationTargetException; import java.sql.Connection; import java.util.Collection; import java.util.List; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.PropertyUtilsBean; import org.apache.empire.commons.ObjectUtils; import org.apache.empire.commons.Options; import org.apache.empire.data.Column; import org.apache.empire.data.ColumnExpr; import org.apache.empire.data.Record; import org.apache.empire.db.exceptions.FieldIsReadOnlyException; import org.apache.empire.exceptions.BeanPropertyGetException; import org.apache.empire.exceptions.InvalidArgumentException; import org.apache.empire.exceptions.ObjectNotValidException; import org.apache.empire.xml.XMLUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * * This class handles one record from a database table. * */ public class DBRecord extends DBRecordData implements Record, Cloneable { private final static long serialVersionUID = 1L; /* Record state enum */ public enum State { Invalid, /* Empty, not used! */ Valid, Modified, New; /* Accessors */ boolean isLess(State other) { return this.ordinal() < other.ordinal(); } boolean isEqualOrMore(State other) { return this.ordinal() >= other.ordinal(); } } protected static final Logger log = LoggerFactory.getLogger(DBRecord.class); // This is the record data private State state; private DBRowSet rowset; private Object[] fields; private boolean[] modified; private boolean validateFieldValues; // Special Rowset Data (usually null) private Object rowsetData; /** * Create a new DBRecord object.<BR> * The record is not attached to a RowSet and the record's state is initially set to REC_INVALID. * * Please derive your own Objects from this class. */ public DBRecord() { state = State.Invalid; rowset = null; fields = null; modified = null; rowsetData = null; validateFieldValues = true; } public DBRecord(DBRowSet initialRowset) { this(); // allow initial rowset rowset = initialRowset; } /** * This method is used internally by the RowSet to initialize the record's properties * @param rowset the rowset to which to attach this record * @param rowSetData any further RowSet specific data * @param newRecord */ protected void initData(DBRowSet rowset, Object rowSetData, boolean newRecord) { // Init rowset boolean rowsetChanged = (this.rowset != rowset); if (rowsetChanged) fields = null; this.rowset = rowset; // Init fields if (rowset != null) { // Set Rowset int colCount = rowset.getColumns().size(); if (fields == null || fields.length != colCount) fields = new Object[colCount]; else { // clear fields for (int i = 0; i < fields.length; i++) fields[i] = null; // ObjectUtils.NO_VALUE -> works too (difference?); } } // Set State this.rowsetData = rowSetData; this.modified = null; changeState((rowset == null ? State.Invalid : (newRecord ? State.New : State.Valid))); // notify if (rowsetChanged) onRowSetChanged(); } /** * changes the state of the record * @param newState */ protected void changeState(State newState) { this.state = newState; } /** * This function provides direct access to the record fields.<BR> * This method is used internally be the RowSet to fill the data.<BR> * @return an array of field values */ protected Object[] getFields() { return fields; } /** * Closes the record by releasing all resources and resetting the record's state to invalid. */ @Override public void close() { // rowset = null; -- do not change this -- fields = null; modified = null; rowsetData = null; // change state if (state != State.Invalid) changeState(State.Invalid); } /** {@inheritDoc} */ @Override public DBRecord clone() { try { DBRecord rec = (DBRecord) super.clone(); rec.rowset = this.rowset; rec.state = this.state; if (rec.fields == fields && fields != null) rec.fields = fields.clone(); if (rec.modified == modified && modified != null) rec.modified = modified.clone(); rec.rowsetData = this.rowsetData; rec.validateFieldValues = this.validateFieldValues; return rec; } catch (CloneNotSupportedException e) { log.error("Unable to clone record.", e); return null; } } /** * Returns the current DBDatabase object. * * @return the current DBDatabase object */ @Override public DBDatabase getDatabase() { return (rowset != null) ? rowset.db : null; } /** * Returns the DBRowSet object. * * @return the DBRowSet object */ public DBRowSet getRowSet() { return rowset; } /** * Returns the DBRowSet object. * * @return the DBRowSet object */ public Object getRowSetData() { return rowsetData; } /** * Returns the record state. * * @return the record state */ public State getState() { return state; } /** * Returns true if the record is valid. * * @return true if the record is valid */ public boolean isValid() { return (state != State.Invalid); } /** * Returns true if the record is valid. * * @return true if the record is valid */ public boolean isReadOnly() { if (!isValid()) return true; DBRowSet rowset = getRowSet(); return (rowset == null || !rowset.isUpdateable()); } /** * Returns true if the record is modified. * * @return true if the record is modified */ public boolean isModified() { return (state.isEqualOrMore(State.Modified)); } /** * Returns true if this record is a new record. * * @return true if this record is a new record */ public boolean isNew() { return (state == State.New); } /** * Returns the number of the columns. * * @return the number of the columns */ @Override public int getFieldCount() { return (fields != null) ? fields.length : 0; } /** * Returns the index value by a specified DBColumnExpr object. * * @return the index value */ @Override public int getFieldIndex(ColumnExpr column) { DBColumnExpr expr = (DBColumnExpr) column; return (rowset != null) ? rowset.getColumnIndex(expr.getUpdateColumn()) : -1; } /** * Returns the index value by a specified column name. * * @return the index value */ @Override public int getFieldIndex(String column) { if (rowset != null) { List<DBColumn> columns = rowset.getColumns(); for (int i = 0; i < columns.size(); i++) { DBColumn col = columns.get(i); if (col.getName().equalsIgnoreCase(column)) return i; } } // not found return -1; } /** * Returns the DBColumn for the field at the given index. * * @param index the field index * * @return the index value */ public DBColumn getDBColumn(int index) { return (rowset != null ? rowset.getColumn(index) : null); } /** * Implements the Record Interface getColumn method.<BR> * Internally calls getDBColumn() * @return the Column at the specified index */ public final Column getColumn(int index) { return getDBColumn(index); } /** * Returns a DBColumnExpr object by a specified index value. * @return the index value */ @Override public final ColumnExpr getColumnExpr(int index) { return getDBColumn(index); } /** * Returns true if the field was modified. * * @param index the field index * * @return true if the field was modified */ public boolean wasModified(int index) { if (!isValid()) throw new ObjectNotValidException(this); if (index < 0 || index >= fields.length) throw new InvalidArgumentException("index", index); // Check modified if (modified == null) return false; return modified[index]; } /** * Returns true if the field was modified. * * @return true if the field was modified */ public final boolean wasModified(Column column) { return wasModified(getFieldIndex(column)); } /** * Sets the modified state of a column.<BR> * This will force the field to be updated in the database, if set to TRUE. * * @param column the column * @param isModified modified or not */ public void setModified(DBColumn column, boolean isModified) { // Check valid if (state == State.Invalid) throw new ObjectNotValidException(this); // Check modified if (modified == null) { // Init array modified = new boolean[fields.length]; for (int j = 0; j < fields.length; j++) modified[j] = false; } int index = getFieldIndex(column); if (index >= 0) modified[index] = isModified; // Set State to modified, if not already at least modified and isModified is set to true if (state.isLess(State.Modified) && isModified) changeState(State.Modified); // Reset state to unmodified, if currently modified and not modified anymore after the change if (state == State.Modified && !isModified) { boolean recordNotModified = true; for (int j = 0; j < fields.length; j++) { if (modified[j] == true) { recordNotModified = false; } } if (recordNotModified) { changeState(State.Valid); } } } /** * returns an array of key columns which uniquely identify the record. * @return the array of key columns if any */ public Column[] getKeyColumns() { return rowset.getKeyColumns(); } /** * Returns the array of primary key columns. * @return the array of primary key columns */ public Object[] getKeyValues() { return ((rowset != null) ? rowset.getRecordKey(this) : null); } /** * Returns the value for the given column or null if either the index is out of range or the value is not valid (see {@link DBRecord#isValueValid(int)}) * @return the index value */ @Override public Object getValue(int index) { // Check state if (fields == null) throw new ObjectNotValidException(this); // Check index if (index < 0 || index >= fields.length) throw new InvalidArgumentException("index", index); // Special check for NO_VALUE if (fields[index] == ObjectUtils.NO_VALUE) return null; // Return field value return fields[index]; } /** * Returns whether a field value is provided i.e. the value is not DBRowSet.NO_VALUE<BR> * This function is only useful in cases where records are partially loaded.<BR> * * @param index the filed index * * @return true if a valid value is supplied for the given field or false if value is {@link ObjectUtils#NO_VALUE} */ public boolean isValueValid(int index) { // Check state if (fields == null) throw new ObjectNotValidException(this); // Check index if (index < 0 || index >= fields.length) { // Index out of range throw new InvalidArgumentException("index", index); } // Special check for NO_VALUE return (fields[index] != ObjectUtils.NO_VALUE); } /** * Gets the possbile Options for a field in the context of the current record. * * @param column the database field column * * @return the field options */ public Options getFieldOptions(DBColumn column) { // DBColumn col = ((colexpr instanceof DBColumn) ? ((DBColumn) colexpr) : colexpr.getUpdateColumn()); return column.getOptions(); } /** * Gets the possbile Options for a field in the context of the current record.<BR> * Same as getFieldOptions(DBColumn) * @return the Option */ public final Options getFieldOptions(Column column) { return getFieldOptions((DBColumn) column); } /** * Modifies a column value bypassing all checks made by setValue. * Use this to explicitly set invalid values i.e. for temporary storage. * * @param i index of the column * @param value the column value */ protected void modifyValue(int i, Object value, boolean fireChangeEvent) { // Check valid if (state == State.Invalid) throw new ObjectNotValidException(this); // Init original values if (modified == null) { // Save all original values modified = new boolean[fields.length]; for (int j = 0; j < fields.length; j++) modified[j] = false; } // Set Modified if (fields[i] != ObjectUtils.NO_VALUE || value != null) modified[i] = true; // Set Value fields[i] = value; // Set State if (state.isLess(State.Modified)) changeState(State.Modified); // field changed if (fireChangeEvent) onFieldChanged(i); } /** * Sets the value of the column in the record. * The functions checks if the column and the value are valid and whether the * value has changed. * * @param index the index of the column * @param value the value */ public void setValue(int index, Object value) { if (state == State.Invalid) throw new ObjectNotValidException(this); if (index < 0 || index >= fields.length) throw new InvalidArgumentException("index", index); // Strings special if ((value instanceof String) && ((String) value).length() == 0) value = null; // Has Value changed? if (ObjectUtils.compareEqual(fields[index], value)) return; // no change // Field has changed DBColumn column = rowset.getColumn(index); // Check whether we can change this field if (!allowFieldChange(column)) { // Read Only column may be set throw new FieldIsReadOnlyException(column); } // Is Value valid if (validateFieldValues) value = validateValue(column, value); // Init original values modifyValue(index, value, true); } /** * Sets the value of the column in the record. * The functions checks if the column and the value are valid and whether the * value has changed. * * @param column a DBColumn object * @param value the value */ public final void setValue(Column column, Object value) { if (!isValid()) throw new ObjectNotValidException(this); // Get Column Index setValue(getFieldIndex(column), value); } /** * Checks whether or not this field can be changed at all. * Note: This is not equivalent to isFieldReadOnly() * @param column the column that needs to be changed * @return true if it is possible to change this field for this record context */ protected boolean allowFieldChange(DBColumn column) { // Check auto generated if (column.isAutoGenerated() && (!isNew() || !isNull(column))) return false; // Check key Column if (!isNew() && rowset.isKeyColumn(column)) return false; // done return true; } /** * Validates a value before it is set in the record. * By default, this method simply calls column.validate() * @param column the column that needs to be changed * @param value the new value */ public Object validateValue(Column column, Object value) { return column.validate(value); } /** * Returns whether or not values are checked for validity when calling setValue(). * If set to true validateValue() is called to check validity * @return true if the validity of values is checked or false otherwise */ public boolean isValidateFieldValues() { return validateFieldValues; } /** * Set whether or not values are checked for validity when calling setValue(). * If set to true validateValue() is called to check validity, otherwise not. * @param validateFieldValues flag whether to check validity */ public void setValidateFieldValues(boolean validateFieldValues) { this.validateFieldValues = validateFieldValues; } /** * returns whether a field is visible to the client or not * <P> * May be overridden to implement context specific logic. * @param column the column which to check for visibility * @return true if the column is visible or false if not */ public boolean isFieldVisible(Column column) { if (rowset == null) return false; // Check value int index = rowset.getColumnIndex(column); return (index >= 0 && isValueValid(index)); } /** * returns whether a field is read only or not * * @param column the database column * * @return true if the field is read only */ public boolean isFieldReadOnly(Column column) { if (rowset == null) throw new ObjectNotValidException(this); if (getFieldIndex(column) < 0) throw new InvalidArgumentException("column", column); // Check key column if (isValid() && !isNew() && rowset.isKeyColumn((DBColumn) column)) return true; // Ask RowSet return (rowset.isColumnReadOnly((DBColumn) column)); } /** * returns whether a field is required or not * * @param column the database column * * @return true if the field is required */ public boolean isFieldRequired(Column column) { if (rowset == null) throw new ObjectNotValidException(this); if (rowset.getColumnIndex(column) < 0) throw new InvalidArgumentException("column", column); // from column definition return (column.isRequired()); } /** * Initializes this record object by attaching it to a rowset, * setting its primary key values and setting the record state.<BR> * This function is useful for updating a record without prior reading. * <P> * @param table the rowset * @param keyValues a Object array, the primary key(s) * @param insert if true change the state of this object to REC_NEW */ public void init(DBRowSet table, Object[] keyValues, boolean insert) { // Init with keys if (table != null) table.initRecord(this, keyValues, insert); else initData(null, null, false); } /** * Creates a new record for the given table.<BR> * All record fields will be filled with their default values. * The record's state is set to NEW * <P> * If a connection is supplied sequence generated values will be obtained<BR> * Otherwise the sequence will be generated later. * <P> * @param table the table for which to create a record * @param conn a valid JDBC connection */ public void create(DBRowSet table, Connection conn) { if (table == null) throw new InvalidArgumentException("table", table); // create table.createRecord(this, conn); } /** * Creates a new record for the given table.<BR> * All record fields will be filled with their default values.<BR> * The record's state is set to NEW * <P> * @param table the table for which to create a record */ public final void create(DBRowSet table) { create(table, null); } /** * Loads a record from the database identified by it's primary key. * After successful reading the record will be valid and all values will be accessible. * @see org.apache.empire.db.DBTable#readRecord(DBRecord, Object[], Connection) * * @param table the rowset from which to read the record * @param keys an array of the primary key values * @param conn a valid connection to the database. */ public void read(DBRowSet table, Object[] keys, Connection conn) { if (table == null) throw new InvalidArgumentException("table", table); // read table.readRecord(this, keys, conn); } /** * Loads a record from the database identified by it's primary key. * After successful reading the record will be valid and all values will be accessible. * @see org.apache.empire.db.DBTable#readRecord(DBRecord, Object[], Connection) * * @param table the rowset from which to read the record * @param id the primary key of the record to load. * @param conn a valid connection to the database. */ public final void read(DBRowSet table, Object id, Connection conn) { if (id instanceof Collection<?>) { // If it's a collection then convert it to an array read(table, ((Collection<?>) id).toArray(), conn); } // Simple One-Column key read(table, new Object[] { id }, conn); } /** * Updates the record and saves all changes in the database. * * @see org.apache.empire.db.DBTable#updateRecord(DBRecord, Connection) * @param conn a valid connection to the database. */ public void update(Connection conn) { if (!isValid()) throw new ObjectNotValidException(this); if (!isModified()) return; /* Not modified. Nothing to do! */ // update rowset.updateRecord(this, conn); } /** * This method is used internally to indicate that the record update has completed<BR> * This will set change the record's state to Valid * @param rowSetData additional data held by the rowset for this record (optional) */ protected void updateComplete(Object rowSetData) { this.rowsetData = rowSetData; this.modified = null; changeState(State.Valid); } /** * This helper function calls the DBRowset.deleteRecord method * to delete the record. * * WARING: There is no guarantee that it ist called * Implement delete logic in the table's deleteRecord method if possible * * @see org.apache.empire.db.DBTable#deleteRecord(Object[], Connection) * @param conn a valid connection to the database. */ public void delete(Connection conn) { if (isValid() == false) throw new ObjectNotValidException(this); // Delete only if record is not new if (!isNew()) { Object[] keys = rowset.getRecordKey(this); rowset.deleteRecord(keys, conn); } close(); } /** * This function set the field descriptions to the the XML tag. * * @return the number of column descriptions added to the element */ @Override public int addColumnDesc(Element parent) { if (!isValid()) throw new ObjectNotValidException(this); // Add Field Description int count = 0; List<DBColumn> columns = rowset.getColumns(); for (int i = 0; i < columns.size(); i++) { // Add Field DBColumn column = columns.get(i); if (isFieldVisible(column) == false) continue; column.addXml(parent, 0); count++; } return count; } /** * Add the values of this record to the specified XML Element object. * * @param parent the XML Element object * @return the number of row values added to the element */ @Override public int addRowValues(Element parent) { if (!isValid()) throw new ObjectNotValidException(this); // set row key DBColumn[] keyColumns = rowset.getKeyColumns(); if (keyColumns != null && keyColumns.length > 0) { // key exits if (keyColumns.length > 1) { // multi-Column-id StringBuilder buf = new StringBuilder(); for (int i = 0; i < keyColumns.length; i++) { // add if (i > 0) buf.append("/"); buf.append(getString(keyColumns[i])); } parent.setAttribute("id", buf.toString()); } else parent.setAttribute("id", getString(keyColumns[0])); } // row attributes if (isNew()) parent.setAttribute("new", "1"); // Add all children int count = 0; List<DBColumn> columns = rowset.getColumns(); for (int i = 0; i < fields.length; i++) { // Read all DBColumn column = columns.get(i); if (isFieldVisible(column) == false) continue; // Add Field Value String name = column.getName(); if (fields[i] != null) XMLUtil.addElement(parent, name, getString(i)); else XMLUtil.addElement(parent, name).setAttribute("null", "yes"); // Null-Value // increase count count++; } return count; } /** * returns the DBXmlDictionary that should used to generate XMLDocuments<BR> * @return the DBXmlDictionary */ protected DBXmlDictionary getXmlDictionary() { return DBXmlDictionary.getInstance(); } /** * Returns a XML document with the field description an values of this record. * * @return the new XML Document object */ @Override public Document getXmlDocument() { if (!isValid()) throw new ObjectNotValidException(this); // Create Document DBXmlDictionary xmlDic = getXmlDictionary(); Element root = XMLUtil.createDocument(xmlDic.getRowSetElementName()); if (rowset.getName() != null) root.setAttribute("name", rowset.getName()); // Add Field Description if (addColumnDesc(root) > 0) { // Add row Values addRowValues(XMLUtil.addElement(root, xmlDic.getRowElementName())); } // return Document return root.getOwnerDocument(); } /** * Set the record default value for the fields with * the value {@link ObjectUtils#NO_VALUE} * * @param conn the sql connection * * @return the number of fields set to default */ public int fillMissingDefaults(Connection conn) { int count = 0; for (int i = 0; i < fields.length; i++) { if (fields[i] == ObjectUtils.NO_VALUE) { DBTableColumn col = (DBTableColumn) rowset.getColumn(i); Object value = col.getRecordDefaultValue(conn); if (value == null) continue; // Modify value modifyValue(i, value, true); count++; } } return count; } /** * set a record value from a particular bean property. * <P> * For a property called FOO this is equivalent of calling<BR> * setValue(column, bean.getFOO()) * <P> * @param bean the Java Bean from which to read the value from * @param property the name of the property * @param column the column for which to set the record value */ protected void setBeanValue(Object bean, String property, Column column) { try { /* if (log.isTraceEnabled()) log.trace(bean.getClass().getName() + ": getting property '" + property + "' for column " + column.getName()); */ // Get Property Value PropertyUtilsBean pub = BeanUtilsBean.getInstance().getPropertyUtils(); Object value = pub.getSimpleProperty(bean, property); // Now, set the record value setValue(column, value); } catch (IllegalAccessException e) { log.error(bean.getClass().getName() + ": unable to get property '" + property + "'"); throw new BeanPropertyGetException(bean, property, e); } catch (InvocationTargetException e) { log.error(bean.getClass().getName() + ": unable to get property '" + property + "'"); throw new BeanPropertyGetException(bean, property, e); } catch (NoSuchMethodException e) { log.warn(bean.getClass().getName() + ": no getter available for property '" + property + "'"); throw new BeanPropertyGetException(bean, property, e); } } /** * Sets record values from the supplied java bean. * * @return true if at least one value has been set successfully */ public int setBeanValues(Object bean, Collection<Column> ignoreList) { // Add all Columns int count = 0; for (int i = 0; i < getFieldCount(); i++) { // Check Property DBColumn column = getDBColumn(i); if (column.isReadOnly()) continue; if (ignoreList != null && ignoreList.contains(column)) continue; // ignore this property // Get Property Name String property = column.getBeanPropertyName(); setBeanValue(bean, property, column); count++; } return count; } /** * Sets record values from the suppied java bean. * @return true if at least one value has been set sucessfully */ public final int setBeanValues(Object bean) { return setBeanValues(bean, null); } /** * Override this to do extra handling when the rowset for this record changes */ protected void onRowSetChanged() { if (log.isTraceEnabled() && rowset != null) log.trace("Record has been attached to rowset " + rowset.getName()); } /** * Override this to do extra handling when the record changes */ protected void onRecordChanged() { if (log.isTraceEnabled() && isValid()) log.trace("Record has been changed"); } /** * Override this to get notified when a field value changes */ protected void onFieldChanged(int i) { if (log.isDebugEnabled()) log.debug("Record field " + rowset.getColumn(i).getName() + " changed to " + String.valueOf(fields[i])); } }