blob: 47e11bfb28fb383da7740abdce18fc7311a20b99 [file] [log] [blame]
/*
* 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.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Connection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.empire.commons.Attributes;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.OptionEntry;
import org.apache.empire.commons.Options;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.Column;
import org.apache.empire.data.DataMode;
import org.apache.empire.data.DataType;
import org.apache.empire.db.exceptions.FieldIllegalValueException;
import org.apache.empire.db.exceptions.FieldNotNullException;
import org.apache.empire.db.exceptions.FieldValueOutOfRangeException;
import org.apache.empire.db.exceptions.FieldValueTooLongException;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.InvalidPropertyException;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.PropertyReadOnlyException;
import org.apache.empire.xml.XMLUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
/**
* This class represent one column of a table.
* It contains all properties of this columns (e.g. the column width).
*
*
*/
public class DBTableColumn extends DBColumn
{
private final static long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DBTableColumn.class);
// Column Information
protected DataType type;
protected double size;
protected boolean required;
protected boolean autoGenerated;
protected boolean readOnly;
protected Object defaultValue;
protected int decimalScale = 0;
/**
* Constructs a DBTableColumn object set the specified parameters to this object.
*
* @param table the table object to add the column to, set to null if you don't want it added to a table
* @param type the type of the column e.g. integer, text, date
* @param name the column name
* @param size the column width
* @param dataMode determines whether this column is optional, required or auto-generated
* @param defValue the object value
*/
public DBTableColumn(DBTable table, DataType type, String name, double size, boolean required, boolean autoGenerated, Object defValue)
{
super(table, name);
// check properties
// set column properties
this.type = type;
this.required = required;
this.autoGenerated = autoGenerated;
this.readOnly = autoGenerated;
this.defaultValue = defValue;
// xml
this.attributes = new Attributes();
this.options = null;
// size (after attributes!)
setSize(size);
}
/**
* Old constructor which will be removed in a further release.
* @deprecated use {@link #DBTableColumn(DBTable table, DataType type, String name, double size, boolean required, boolean autoGenerated, Object defValue)} instead.
*/
@Deprecated
public DBTableColumn(DBTable table, DataType type, String name, double size, DataMode dataMode, Object defValue)
{
this(table, type, name, size, (dataMode!=DataMode.Nullable), (dataMode==DataMode.AutoGenerated), defValue);
}
/**
* Clone Constructor - use clone()
*/
protected DBTableColumn(DBTable newTable, DBTableColumn other)
{
super(newTable, other.name);
// Copy
this.type = other.type;
this.size = other.size;
this.required = other.required;
this.autoGenerated = other.autoGenerated;
this.readOnly = other.readOnly;
this.defaultValue = other.defaultValue;
this.attributes = new Attributes();
this.attributes.addAll(other.attributes);
this.options = other.options;
if (newTable != null)
{
newTable.addColumn(this);
}
}
/**
* Returns the default column value.
* For columns of type DBDataType.AUTOINC this is assumed to be the name of a sequence
*
* @return the default column value
*/
public Object getDefaultValue()
{
return defaultValue;
}
/**
* Sets the default column value.
*
* @param defValue the default column value
*/
public void setDefaultValue(Object defValue)
{
this.defaultValue = defValue;
}
/**
* Returns the default column value.
* Unlike getDefaultValue this function is used when creating or adding records.
* If the column value is DBDataType AUTOIN this function will return a new sequence value for this record
*
* @param conn a valid database connection
* @return the default column value
*/
public Object getRecordDefaultValue(Connection conn)
{ // Check params
if (rowset==null)
return defaultValue;
// Detect default value
DBDatabase db = rowset.getDatabase();
if (isAutoGenerated())
{ // If no connection is supplied defer till later
if (conn==null)
return null; // Create Later
// Other auto-generated values
DBDatabaseDriver driver = db.getDriver();
return driver.getColumnAutoValue(db, this, conn);
}
// Normal value
return defaultValue;
}
/**
* Returns the data type.
*
* @return the data type
*/
@Override
public DataType getDataType()
{
return type;
}
/**
* Gets the the column width.
*
* @return the column width
*/
@Override
public double getSize()
{
return size;
}
/**
* Changes the size of the table column<BR>
* Use for dynamic data model changes only.<BR>
* @param size the new column size
*/
public void setSize(double size)
{
// Negative size?
if (size<0)
{ // For Text-Columns set attribute "SINGLEBYTECHARS"
if (getDataType().isText())
{
setAttribute(DBCOLATTR_SINGLEBYTECHARS, Boolean.TRUE);
}
else
throw new InvalidArgumentException("size", size);
// Remove sign
size = Math.abs(size);
}
else if (attributes!=null && attributes.contains(DBCOLATTR_SINGLEBYTECHARS))
{ // Remove single by chars attribute
attributes.remove(DBCOLATTR_SINGLEBYTECHARS);
}
// set now
this.size = size;
// set scale
if (getDataType()==DataType.DECIMAL)
{ // set scale from size
int reqPrec = (int)size;
this.decimalScale = ((int)(size*10)-(reqPrec*10));
}
}
/**
* Returns the scale of the Decimal or 0 if the DataType is not DataType.DECIMAL.
* @return the decimal scale
*/
public int getDecimalScale()
{
return this.decimalScale;
}
/**
* Sets the scale of a decimal. The DataType must be set to DataType.DECIMAL otherwise an exception is thrown.
*/
public void setDecimalScale(int scale)
{
if (getDataType()!=DataType.DECIMAL)
throw new NotSupportedException(this, "setDecimalScale");
// return scale
this.decimalScale = scale;
}
/**
* Returns true if column is mandatory. Only for the graphic presentation.
*
* @return true if column is mandatory
*/
@Override
public boolean isRequired()
{
return this.required;
}
/**
* Returns true if column is a numeric sequence or otherwise generated value
*
* @return true if column is auto increment
*/
@Override
public boolean isAutoGenerated()
{
return this.autoGenerated;
}
/**
* Returns true if column the column is a single byte text or character column or false otherwise
*
* @return true if column is a single byte character based column
*/
public boolean isSingleByteChars()
{
if (attributes==null || !attributes.contains(DBCOLATTR_SINGLEBYTECHARS))
return false;
// Check Attribute
return ObjectUtils.getBoolean(attributes.get(DBCOLATTR_SINGLEBYTECHARS));
}
/**
* sets whether this column is a single byte character or text column
*/
public void setSingleByteChars(boolean singleByteChars)
{
if (!getDataType().isText())
throw new NotSupportedException(this, "setSingleByteChars");
// set single byte
setAttribute(DBCOLATTR_SINGLEBYTECHARS, singleByteChars);
}
/**
* Changes the required property of the table column<BR>
* Use for dynamic data model changes only.<BR>
* @param required true if the column is required or false otherwise
*/
public void setRequired(boolean required)
{
if (isAutoGenerated())
{ // cannot change auto-generated columns
throw new PropertyReadOnlyException("required");
}
else
{ // Set DataMode
this.required = required;
}
}
/**
* Checks whether the column is read only.
*
* @return true if the column is read only
*/
@Override
public boolean isReadOnly()
{
return this.readOnly;
}
/**
* Sets the read only attribute of the column.
*
* @param readOnly true if the column should be read only or false otherwise
*/
public void setReadOnly(boolean readOnly)
{
this.readOnly = readOnly;
}
/**
* sets the options from an enum class
*/
public void setEnumOptions(Class<?> enumType)
{
// Enum special treatment
log.debug("Adding enum options of type {} for column {}.", enumType.getName(), getName());
this.options = new Options(enumType);
// set enumType
setAttribute(Column.COLATTR_ENUMTYPE, enumType);
// check length
if (getDataType().isNumeric())
return; // no check required
int maxLength = (int)size;
for (OptionEntry oe : options)
{ // check length
String val = oe.getValueString();
if (val!=null && val.length()>maxLength)
throw new InvalidPropertyException(enumType.getName(), val);
}
}
/**
* Checks whether the supplied value is valid for this column.
* If the type of the value supplied does not match the columns
* data type the value will be checked for compatibility.
*
* @param value the checked to check for validity
* @return true if the value is valid or false otherwise.
*/
@Override
public Object validate(Object value)
{
// Check for NULL
if (ObjectUtils.isEmpty(value))
{ // Null value
if (isRequired())
throw new FieldNotNullException(this);
// Null is allowed
return null;
}
// Check for Column expression
if (value instanceof DBColumnExpr)
{ DataType funcType = ((DBColumnExpr)value).getDataType();
if (!type.isCompatible(funcType))
{ // Incompatible data types
log.info("Incompatible data types in expression for column {} using function {}!", getName(), value.toString());
throw new FieldIllegalValueException(this, String.valueOf(value));
}
// allowed
return value;
}
// Check for Command expression
if (value instanceof DBCommandExpr)
{ DBColumnExpr[] exprList = ((DBCommandExpr)value).getSelectExprList();
if (exprList.length!=1)
{ // Incompatible data types
log.info("Invalid command expression for column {} using command {}!", getName(), ((DBCommandExpr)value).getSelect());
throw new FieldIllegalValueException(this, ((DBCommandExpr)value).getSelect());
}
// Compare types
if (!type.isCompatible(exprList[0].getDataType()))
{ // Incompatible data types
log.info("Incompatible data types in expression for column {} using function {}!", getName(), value.toString());
throw new FieldIllegalValueException(this, String.valueOf(value));
}
// allowed
return value;
}
// Is value valid
switch (type)
{
case DATE:
case DATETIME:
case TIMESTAMP:
// Check whether value is a valid date/time value!
if (!(value instanceof Date) && !DBDatabase.SYSDATE.equals(value))
{ // Parse String
String dateValue = value.toString();
if (dateValue.length()==0)
return null;
// Convert through SimpleDateFormat
String datePattern = StringUtils.coalesce(StringUtils.toString(getAttribute(Column.COLATTR_DATETIMEPATTERN)), "yyyy-MM-dd HH:mm:ss");
if ((type==DataType.DATE || dateValue.length()<=12) && datePattern.indexOf(' ')>0)
datePattern = datePattern.substring(0, datePattern.indexOf(' ')); // Strip off time
try
{ // Parse date time value
SimpleDateFormat sdFormat = new SimpleDateFormat(datePattern);
sdFormat.setLenient(true);
value = sdFormat.parse(dateValue);
// OK
} catch (ParseException e)
{ // Error
log.info("Parsing '{}' to Date ("+datePattern+") failed for column {}. Message is "+e.toString(), value, getName());
throw new FieldIllegalValueException(this, String.valueOf(value), e);
}
}
break;
case DECIMAL:
// check enum
if (value instanceof Enum<?>)
{ // convert enum
value = ((Enum<?>)value).ordinal();
}
// check number
if (!(value instanceof java.lang.Number))
{ try
{ // Convert to String and check
value = ObjectUtils.toDecimal(value);
// throws NumberFormatException if not a number!
} catch (NumberFormatException e)
{
log.info("Parsing '{}' to Decimal failed for column {}. Message is "+e.toString(), value, getName());
throw new FieldIllegalValueException(this, String.valueOf(value), e);
}
}
// validate Number
value = validateNumber(type, (Number)value);
break;
case FLOAT:
if (!(value instanceof java.lang.Number))
{ try
{ // Convert to String and check
value = ObjectUtils.toDouble(value);
// throws NumberFormatException if not a number!
} catch (NumberFormatException e)
{
log.info("Parsing '{}' to Double failed for column {}. Message is "+e.toString(), value, getName());
throw new FieldIllegalValueException(this, String.valueOf(value), e);
}
}
// validate Number
value = validateNumber(type, (Number)value);
break;
case INTEGER:
// check enum
if (value instanceof Enum<?>)
{ // convert enum
value = ((Enum<?>)value).ordinal();
}
// check number
if (!(value instanceof java.lang.Number))
{ try
{ // Convert to String and check
value = ObjectUtils.toLong(value);
// throws NumberFormatException if not an integer!
} catch (NumberFormatException e)
{
log.info("Parsing '{}' to Integer failed for column {}. Message is "+e.toString(), value, getName());
throw new FieldIllegalValueException(this, String.valueOf(value), e);
}
}
// validate Number
value = validateNumber(type, (Number)value);
break;
case TEXT:
case VARCHAR:
case CHAR:
// check enum
if (value instanceof Enum<?>)
{ // convert enum
value = ObjectUtils.getString((Enum<?>)value);
}
// check length
if (value.toString().length() > size)
{
throw new FieldValueTooLongException(this);
}
break;
default:
if (log.isDebugEnabled())
log.debug("No column validation has been implemented for data type " + type);
break;
}
return value;
}
protected Number validateNumber(DataType type, Number n)
{
// Check Range
Object min = getAttribute(Column.COLATTR_MINVALUE);
Object max = getAttribute(Column.COLATTR_MAXVALUE);
if (min!=null && max!=null)
{ // Check Range
long minVal = ObjectUtils.getLong(min);
long maxVal = ObjectUtils.getLong(max);
if (n.longValue()<minVal || n.longValue()>maxVal)
{ // Out of Range
throw new FieldValueOutOfRangeException(this, minVal, maxVal);
}
}
else if (min!=null)
{ // Check Min Value
long minVal = ObjectUtils.getLong(min);
if (n.longValue()<minVal)
{ // Out of Range
throw new FieldValueOutOfRangeException(this, minVal, false);
}
}
else if (max!=null)
{ // Check Max Value
long maxVal = ObjectUtils.getLong(max);
if (n.longValue()>maxVal)
{ // Out of Range
throw new FieldValueOutOfRangeException(this, maxVal, true);
}
}
// Check overall
if (type==DataType.DECIMAL)
{ // Convert to Decimal
BigDecimal dv = ObjectUtils.toDecimal(n);
int prec = dv.precision();
int scale = dv.scale();
// check precision and scale
double size = getSize();
int reqPrec = (int)size;
int reqScale = getDecimalScale();
if (scale>reqScale)
{ // Round if scale is exceeded
dv = dv.setScale(reqScale, RoundingMode.HALF_UP);
prec = dv.precision();
scale = dv.scale();
n = dv;
}
if ((prec-scale)>(reqPrec-reqScale))
{
throw new FieldValueOutOfRangeException(this);
}
}
return n;
}
/**
* Creates a foreign key relation for this column.
*
* @param target the referenced primary key column
* @return the reference object
*/
public DBRelation.DBReference referenceOn(DBTableColumn target)
{
return new DBRelation.DBReference(this, target);
}
/**
* Sets field elements, default attributes and all options to
* the specified Element object (XML tag).
*
* @param parent the parent object
* @param flags a long value
* @return the work up Element object
*/
@Override
public Element addXml(Element parent, long flags)
{ // Add Field element
Element elem = XMLUtil.addElement(parent, "column");
elem.setAttribute("name", name);
// set default attributes
DBIndex primaryKey = ((DBTable) rowset).getPrimaryKey();
if (primaryKey != null)
{
int keyIndex;
if ((keyIndex = ((DBTable) rowset).getPrimaryKey().getColumnPos(this)) >= 0)
elem.setAttribute("key", String.valueOf(keyIndex + 1));
}
if (size > 0)
{
elem.setAttribute("size", String.valueOf((int)size));
if (getDataType()==DataType.DECIMAL)
elem.setAttribute("decimals", String.valueOf((int)(size*10)%10));
}
if (isRequired())
elem.setAttribute("mandatory", String.valueOf(Boolean.TRUE));
// add All Attributes
if (attributes!=null)
attributes.addXml(elem, flags);
// add All Options
if (options!=null)
options.addXml(elem, flags);
// done
return elem;
}
/**
* Gets the sequence name for this table's sequence (if it has one)
* This is derived form the default value or auto generated if no default value is set
* @return the sequence name
*/
public String getSequenceName()
{
String seqName;
Object defValue = getDefaultValue();
if(defValue != null)
{
seqName = defValue.toString();
}
else
{
if (rowset != null)
{
seqName = rowset.getName() + "." + name;
}
else
{
seqName = name;
}
}
return seqName;
}
}