| /* |
| * 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; |
| } |
| } |