/* * 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.openjpa.jdbc.sql; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.Locale; import org.apache.openjpa.jdbc.kernel.JDBCStore; import org.apache.openjpa.jdbc.meta.ClassMapping; import org.apache.openjpa.jdbc.meta.FieldMapping; import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.meta.Joinable; import org.apache.openjpa.jdbc.meta.RelationId; import org.apache.openjpa.jdbc.schema.Column; import org.apache.openjpa.jdbc.schema.ColumnIO; import org.apache.openjpa.jdbc.schema.ForeignKey; import org.apache.openjpa.jdbc.schema.Table; import org.apache.openjpa.kernel.OpenJPAStateManager; import org.apache.openjpa.meta.ClassMetaData; import org.apache.openjpa.meta.FieldMetaData; import org.apache.openjpa.meta.JavaTypes; import org.apache.openjpa.util.InternalException; /** * Basic {@link Row} implementation. * * @author Abe White */ public class RowImpl implements Row, Cloneable { public static final Object NULL = new Object(); protected static final int VALID = 2 << 0; public static final int RAW = Integer.MIN_VALUE; protected byte flags = 0; private final Column[] _cols; private final int _action; private final Object[] _vals; private final int[] _types; private String _sql = null; private boolean _isFlushed = false; /** * Constructor. * * @param table the table the row is a part of * @param action the action on the row */ public RowImpl(Table table, int action) { this(table.getColumns(), action); } protected RowImpl(Column[] cols, int action) { _cols = cols; _action = action; // we need room for values and types for all columns; if an update or // delete, then we need to double that for where column conditions int len = _cols.length; if (action != ACTION_INSERT) len *= 2; _vals = new Object[len]; _types = new int[len]; } public Table getTable() { return _cols[0].getTable(); } public Column[] getColumns() { return _cols; } public int getAction() { return _action; } public boolean isValid() { return (flags & VALID) != 0; } public void setValid(boolean valid) { if (valid) flags |= VALID; else flags &= ~VALID; } /** * This implementation does not track primary keys. */ public OpenJPAStateManager getPrimaryKey() { return null; } /** * This implementation does not track failed objects. */ public Object getFailedObject() { return null; } /** * This implementation does not track failed objects. */ public void setFailedObject(Object failed) { throw new InternalException(); } /** * Secondary rows cannot be dependent. */ public boolean isDependent() { return false; } /** * Return the value set for update on the given column. */ public Object getSet(Column col) { return _vals[getSetIndex(col)]; } /** * Return the value set for where on the given column. */ public Object getWhere(Column col) { return _vals[getWhereIndex(col)]; } public void setPrimaryKey(OpenJPAStateManager sm) throws SQLException { setPrimaryKey(null, sm); } public void setPrimaryKey(ColumnIO io, OpenJPAStateManager sm) throws SQLException { flushPrimaryKey(sm, io, true); } public void wherePrimaryKey(OpenJPAStateManager sm) throws SQLException { flushPrimaryKey(sm, null, false); } /** * Flush the primary key values. */ private void flushPrimaryKey(OpenJPAStateManager sm, ColumnIO io, boolean set) throws SQLException { ClassMapping mapping = (ClassMapping) sm.getMetaData(); while (mapping.getTable() != getTable()) mapping = mapping.getPCSuperclassMapping(); Column[] cols = mapping.getPrimaryKeyColumns(); Object oid = mapping.useIdClassFromParent() ? sm.getObjectId() : null; flushJoinValues(sm, oid, cols, cols, io, set); } public void setForeignKey(ForeignKey fk, OpenJPAStateManager sm) throws SQLException { setForeignKey(fk, null, sm); } public void setForeignKey(ForeignKey fk, ColumnIO io, OpenJPAStateManager sm) throws SQLException { flushForeignKey(fk, io, sm, true); } public void whereForeignKey(ForeignKey fk, OpenJPAStateManager sm) throws SQLException { flushForeignKey(fk, null, sm, false); } /** * Clear a circular foreign key. */ public void clearForeignKey(ForeignKey fk) throws SQLException { _sql = null; Column[] cols = fk.getColumns(); for (int i = 0; i < cols.length; i++) _vals[getSetIndex(cols[i])] = null; } /** * Flush the foreign key values. */ private void flushForeignKey(ForeignKey fk, ColumnIO io, OpenJPAStateManager sm, boolean set) throws SQLException { flushJoinValues(sm, null, fk.getPrimaryKeyColumns(), fk.getColumns(), io, set); if (sm != null) { Column[] cols = fk.getConstantColumns(); int len = fk.getColumns().length; Object obj; int type; for (int i = 0; i < cols.length; i++) { obj = fk.getConstant(cols[i]); type = cols[i].getJavaType(); if (set && canSet(io, i + len, obj == null)) setObject(cols[i], obj, type, false); else if (!set) whereObject(cols[i], obj, type); } } } /** * Flush the given instance value to the given columns. Note that * foreign keys may include columns also mapped by simple values. We * use a priority mechanism to ensure that we do not let the nulling * of a foreign key null columns also owned by simple values. * * @param to the instance being joined to * @param toCols the columns being joined to * @param fromCols the columns being joined from * @param io information about I/O capabilities in this context * @param set whether this should be flushed as an update or * as a where condition */ private void flushJoinValues(OpenJPAStateManager to, Object oid, Column[] toCols, Column[] fromCols, ColumnIO io, boolean set) throws SQLException { if (to == null) { for (int i = 0; i < fromCols.length; i++) { if (set && canSet(io, i, true)) setNull(fromCols[i]); else if (!set) whereNull(fromCols[i]); } return; } if (set && !canSetAny(io, fromCols.length, false)) return; ClassMapping toMapping = (ClassMapping) to.getMetaData(); Joinable join; Object val; for (int i = 0; i < toCols.length; i++) { // don't even translate join value if unsettable if (set) { if (_action == ACTION_INSERT && fromCols[i].isAutoAssigned()) continue; if (!canSet(io, i, false)) continue; } join = toMapping.assertJoinable(toCols[i]); if (oid != null) val = join.getJoinValue(oid, toCols[i], (JDBCStore) to. getContext().getStoreManager().getInnermostDelegate()); else val = join.getJoinValue(to, toCols[i], (JDBCStore) to. getContext().getStoreManager().getInnermostDelegate()); if (set && val == null) { if (canSet(io, i, true)) setNull(fromCols[i]); } else if (set && val instanceof Raw) setRaw(fromCols[i], val.toString()); else if (set) { setObject(fromCols[i], val, toCols[i].getJavaType(), false); setJoinRefColumn(to, fromCols, toCols[i], val); } else if (val == null) whereNull(fromCols[i]); else if (val instanceof Raw) whereRaw(fromCols[i], val.toString()); else whereObject(fromCols[i], val, toCols[i].getJavaType()); } } private void setJoinRefColumn(OpenJPAStateManager inverseSm, Column ownerCols[], Column inverseCol, Object val) { OpenJPAStateManager ownerSm = getPrimaryKey(); if (ownerSm != null) { ClassMetaData ownerMeta = ownerSm.getMetaData(); // loop through all the fields in the owner entity for (FieldMetaData ownerFM : ownerMeta.getFields()) { // look for any single column in this field references the // same column as the foreign key target column Column cols[] = ((FieldMapping) ownerFM).getColumns(); if (cols.length == 1 // only support attribute of non-compound foreign key && cols != ownerCols // not @Id field && cols[0].getIdentifier().equals(ownerCols[0].getIdentifier())) { // copy the foreign key value to the current field. FieldMetaData inverseFM = inverseSm.getMetaData().getField( inverseCol.getIdentifier().getName()); if (inverseFM != null) { int inverseValIndex = inverseFM.getIndex(); Class<?> inverseType = inverseSm.getMetaData().getField(inverseValIndex).getType(); int ownerIndex = ownerFM.getIndex(); Class<?> ownerType = ownerSm.getMetaData().getField(ownerIndex).getType(); if (inverseType == ownerType) { Object inverseVal = inverseSm.fetch(inverseValIndex); ownerSm.storeField(ownerIndex, inverseVal); } } } } } } /** * Return true if any of the given column indexes are settable. */ protected boolean canSetAny(ColumnIO io, int i, boolean nullValue) { if (io == null) return true; if (_action == ACTION_INSERT) return io.isAnyInsertable(i, nullValue); if (_action == ACTION_UPDATE) return io.isAnyUpdatable(i, nullValue); return true; } /** * Return true if the given column index is settable. */ protected boolean canSet(ColumnIO io, int i, boolean nullValue) { if (io == null) return true; if (_action == ACTION_INSERT) return io.isInsertable(i, nullValue); if (_action == ACTION_UPDATE) return io.isUpdatable(i, nullValue); return true; } public void setRelationId(Column col, OpenJPAStateManager sm, RelationId rel) throws SQLException { setObject(col, rel.toRelationDataStoreValue(sm, col), col.getJavaType(), false); } /** * Clear a circular relation id. */ public void clearRelationId(Column col) throws SQLException { _sql = null; _vals[getSetIndex(col)] = null; } public void setArray(Column col, Array val) throws SQLException { setObject(col, val, JavaSQLTypes.ARRAY, false); } public void setAsciiStream(Column col, InputStream val, int length) throws SQLException { setObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.ASCII_STREAM, false); } public void setBigDecimal(Column col, BigDecimal val) throws SQLException { setObject(col, val, JavaTypes.BIGDECIMAL, false); } public void setBigInteger(Column col, BigInteger val) throws SQLException { setObject(col, val, JavaTypes.BIGINTEGER, false); } public void setBinaryStream(Column col, InputStream val, int length) throws SQLException { setObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.BINARY_STREAM, false); } public void setBlob(Column col, Blob val) throws SQLException { setObject(col, val, JavaSQLTypes.BLOB, false); } public void setBoolean(Column col, boolean val) throws SQLException { setObject(col, ((val) ? Boolean.TRUE : Boolean.FALSE), JavaTypes.BOOLEAN, false); } public void setByte(Column col, byte val) throws SQLException { setObject(col, new Byte(val), JavaTypes.BYTE, false); } public void setBytes(Column col, byte[] val) throws SQLException { setObject(col, val, JavaSQLTypes.BYTES, false); } public void setCalendar(Column col, Calendar val) throws SQLException { setObject(col, val, JavaTypes.CALENDAR, false); } public void setChar(Column col, char val) throws SQLException { setObject(col, new Character(val), JavaTypes.CHAR, false); } public void setCharacterStream(Column col, Reader val, int length) throws SQLException { setObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.CHAR_STREAM, false); } public void setClob(Column col, Clob val) throws SQLException { setObject(col, val, JavaSQLTypes.CLOB, false); } public void setDate(Column col, Date val) throws SQLException { setObject(col, val, JavaTypes.DATE, false); } public void setDate(Column col, java.sql.Date val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); setObject(col, obj, JavaSQLTypes.SQL_DATE, false); } public void setDouble(Column col, double val) throws SQLException { setObject(col, new Double(val), JavaTypes.DOUBLE, false); } public void setFloat(Column col, float val) throws SQLException { setObject(col, new Float(val), JavaTypes.FLOAT, false); } public void setInt(Column col, int val) throws SQLException { setObject(col, val, JavaTypes.INT, false); } public void setLong(Column col, long val) throws SQLException { setObject(col, val, JavaTypes.LONG, false); } public void setLocale(Column col, Locale val) throws SQLException { setObject(col, val, JavaTypes.LOCALE, false); } public void setNull(Column col) throws SQLException { setNull(col, false); } public void setNull(Column col, boolean overrideDefault) throws SQLException { setObject(col, null, col.getJavaType(), overrideDefault); } public void setNumber(Column col, Number val) throws SQLException { setObject(col, val, JavaTypes.NUMBER, false); } public void setRaw(Column col, String val) throws SQLException { setObject(col, val, RAW, false); } public void setShort(Column col, short val) throws SQLException { setObject(col, new Short(val), JavaTypes.SHORT, false); } public void setString(Column col, String val) throws SQLException { setObject(col, val, JavaTypes.STRING, false); } public void setTime(Column col, Time val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); setObject(col, obj, JavaSQLTypes.TIME, false); } public void setTimestamp(Column col, Timestamp val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); setObject(col, obj, JavaSQLTypes.TIMESTAMP, false); } public void setObject(Column col, Object val) throws SQLException { if (val instanceof Raw) setObject(col, val, RAW, false); else setObject(col, val, col.getJavaType(), false); } public void whereArray(Column col, Array val) throws SQLException { whereObject(col, val, JavaSQLTypes.SQL_ARRAY); } public void whereAsciiStream(Column col, InputStream val, int length) throws SQLException { whereObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.ASCII_STREAM); } public void whereBigDecimal(Column col, BigDecimal val) throws SQLException { whereObject(col, val, JavaTypes.BIGDECIMAL); } public void whereBigInteger(Column col, BigInteger val) throws SQLException { whereObject(col, val, JavaTypes.BIGINTEGER); } public void whereBinaryStream(Column col, InputStream val, int length) throws SQLException { whereObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.BINARY_STREAM); } public void whereBlob(Column col, Blob val) throws SQLException { whereObject(col, val, JavaSQLTypes.BLOB); } public void whereBoolean(Column col, boolean val) throws SQLException { whereObject(col, ((val) ? Boolean.TRUE : Boolean.FALSE), JavaTypes.BOOLEAN); } public void whereByte(Column col, byte val) throws SQLException { whereObject(col, new Byte(val), JavaTypes.BYTE); } public void whereBytes(Column col, byte[] val) throws SQLException { whereObject(col, val, JavaSQLTypes.BYTES); } public void whereCalendar(Column col, Calendar val) throws SQLException { whereObject(col, val, JavaTypes.CALENDAR); } public void whereChar(Column col, char val) throws SQLException { whereObject(col, new Character(val), JavaTypes.CHAR); } public void whereCharacterStream(Column col, Reader val, int length) throws SQLException { whereObject(col, (val == null) ? null : new Sized(val, length), JavaSQLTypes.CHAR_STREAM); } public void whereClob(Column col, Clob val) throws SQLException { whereObject(col, val, JavaSQLTypes.CLOB); } public void whereDate(Column col, Date val) throws SQLException { whereObject(col, val, JavaTypes.DATE); } public void whereDate(Column col, java.sql.Date val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); whereObject(col, obj, JavaSQLTypes.SQL_DATE); } public void whereDouble(Column col, double val) throws SQLException { whereObject(col, new Double(val), JavaTypes.DOUBLE); } public void whereFloat(Column col, float val) throws SQLException { whereObject(col, new Float(val), JavaTypes.FLOAT); } public void whereInt(Column col, int val) throws SQLException { whereObject(col, val, JavaTypes.INT); } public void whereLong(Column col, long val) throws SQLException { whereObject(col, val, JavaTypes.LONG); } public void whereLocale(Column col, Locale val) throws SQLException { whereObject(col, val, JavaTypes.LOCALE); } public void whereNull(Column col) throws SQLException { whereObject(col, null, col.getJavaType()); } public void whereNumber(Column col, Number val) throws SQLException { whereObject(col, val, JavaTypes.NUMBER); } public void whereRaw(Column col, String val) throws SQLException { whereObject(col, val, RAW); } public void whereShort(Column col, short val) throws SQLException { whereObject(col, new Short(val), JavaTypes.SHORT); } public void whereString(Column col, String val) throws SQLException { whereObject(col, val, JavaTypes.STRING); } public void whereTime(Column col, Time val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); whereObject(col, obj, JavaSQLTypes.TIME); } public void whereTimestamp(Column col, Timestamp val, Calendar cal) throws SQLException { Object obj; if (val == null || cal == null) obj = val; else obj = new Calendard(val, cal); whereObject(col, obj, JavaSQLTypes.TIMESTAMP); } public void whereObject(Column col, Object val) throws SQLException { if (val instanceof Raw) whereObject(col, val, RAW); else whereObject(col, val, col.getJavaType()); } /** * All set column methods delegate to this one. Set the given object * unless this is an insert and the given column is auto-assigned. */ protected void setObject(Column col, Object val, int metaType, boolean overrideDefault) throws SQLException { // never set auto increment columns and honor column defaults if (_action == ACTION_INSERT) { if (col.isAutoAssigned()) { // OPENJPA-349: validate because this can be the only column setValid(true); return; } if (!overrideDefault && val == null && col.getDefaultString() != null) return; } flush(col, val, metaType, true); } /** * All where column methods delegate to this one. */ protected void whereObject(Column col, Object val, int metaType) throws SQLException { flush(col, val, metaType, false); } /** * Flush the given value as a set or where condition. */ private void flush(Column col, Object val, int metaType, boolean set) { int idx = (set) ? getSetIndex(col) : getWhereIndex(col); _types[idx] = metaType; if (val == null) _vals[idx] = NULL; else _vals[idx] = val; if (set || _action == ACTION_DELETE) setValid(true); } /** * Return the SQL for the operation on this row. */ public String getSQL(DBDictionary dict) { if (!isValid()) return ""; if (_sql == null) _sql = generateSQL(dict); return _sql; } /** * Generate the SQL for this row; the result of this method is cached. */ protected String generateSQL(DBDictionary dict) { switch (getAction()) { case ACTION_UPDATE: return getUpdateSQL(dict); case ACTION_INSERT: return getInsertSQL(dict); default: return getDeleteSQL(dict); } } /** * Return the SQL for a prepared statement update on this row. */ private String getUpdateSQL(DBDictionary dict) { StringBuilder buf = new StringBuilder(); buf.append("UPDATE ").append(dict.getFullName(getTable(), false)). append(" SET "); boolean hasVal = false; for (int i = 0; i < _cols.length; i++) { if (_vals[i] == null) continue; if (hasVal) buf.append(", "); buf.append(dict.getColumnDBName(_cols[i])); if (_types[i] == RAW) buf.append(" = ").append(_vals[i]); else { buf.append(" = "); buf.append(dict.getMarkerForInsertUpdate(_cols[i], _vals[i])); } hasVal = true; } appendWhere(buf, dict); return buf.toString(); } /** * Return the SQL for a prepared statement insert on this row. */ private String getInsertSQL(DBDictionary dict) { StringBuilder buf = new StringBuilder(); StringBuilder vals = new StringBuilder(); buf.append("INSERT INTO "). append(dict.getFullName(getTable(), false)).append(" ("); boolean hasVal = false; for (int i = 0; i < _cols.length; i++) { if (_vals[i] == null) continue; if (hasVal) { buf.append(", "); vals.append(", "); } buf.append(dict.getColumnDBName(_cols[i])); if (_types[i] == RAW) vals.append(_vals[i]); else vals.append(dict.getMarkerForInsertUpdate(_cols[i], _vals[i])); hasVal = true; } buf.append(") VALUES (").append(vals.toString()).append(")"); return buf.toString(); } /** * Return the SQL for a prepared statement delete on this row. */ private String getDeleteSQL(DBDictionary dict) { StringBuilder buf = new StringBuilder(); buf.append("DELETE FROM "). append(dict.getFullName(getTable(), false)); appendWhere(buf, dict); return buf.toString(); } /** * Appends the where clause onto the given sql buffer. */ private void appendWhere(StringBuilder buf, DBDictionary dict) { boolean hasWhere = false; for (int i = 0; i < _cols.length; i++) { if (_vals[getWhereIndex(_cols[i])] == null) continue; if (!hasWhere) buf.append(" WHERE "); else buf.append(" AND "); // Get platform specific version column name if (_cols[i].getVersionStrategy() != null) buf.append(dict.toDBName(dict.getVersionColumn(_cols[i], _cols[i] .getTableIdentifier()))).append(" = ?"); // sqlserver seems to have problems using null parameters in the // where clause else if (_vals[getWhereIndex(_cols[i])] == NULL) buf.append(dict.getColumnDBName(_cols[i])).append(" IS NULL"); else if (_types[i] == RAW) buf.append(dict.getColumnDBName(_cols[i])).append(" = ").append(_vals[i]); else buf.append(dict.getColumnDBName(_cols[i])).append(" = ?"); hasWhere = true; } } /** * The number of parameters that will be set for this row. */ public int getParameterCount() { return _vals.length; } /** * Flush the row's values to the given prepared statement. */ public void flush(PreparedStatement stmnt, DBDictionary dict, JDBCStore store) throws SQLException { flush(stmnt, 1, dict, store); } /** * Flush the row's values to the given prepared statement. */ public void flush(PreparedStatement stmnt, int idx, DBDictionary dict, JDBCStore store) throws SQLException { // this simple method works because the SQL is always prepared // based on the indexing of the columns in the table object -- the // same ordering we use when storing values and meta types. skip // updates when setting params for DELETEs; the updates are just there // to let us eval fk constraints int i = (getAction() == ACTION_DELETE) ? _cols.length: 0; Column col; Object val; int half = _vals.length / 2; for (; i < _vals.length; i++) { if (_vals[i] == null) continue; // we don't set null params in the WHERE clause; we use the NULL // keyword instead to satisfy sqlserver if (_vals[i] == NULL && getAction() != ACTION_INSERT && i >= half) continue; // if this is an update the vals array will be 2 x the cols // array length; it repeats for where values if (i < _cols.length) col = _cols[i]; else col = _cols[i - _cols.length]; val = _vals[i]; if (val == NULL) val = null; if (val == null || _types[i] != RAW) { dict.setTyped(stmnt, idx, val, col, _types[i], store); idx++; } } setFlushed(true); } /** * The array value array index for the given column's value. */ private int getSetIndex(Column col) { return col.getIndex(); } /** * The array value array index for the given column's value. */ private int getWhereIndex(Column col) { return col.getIndex() + _cols.length; } /** * Performs a proper deep clone. */ public Object clone() { RowImpl clone = newInstance(getColumns(), getAction()); copyInto(clone, false); return clone; } /** * Return a new row. */ protected RowImpl newInstance(Column[] cols, int action) { return new RowImpl(cols, action); } /** * Copy all values from this row into the given one. * * @param whereOnly if true, only copy where conditions */ public void copyInto(RowImpl row, boolean whereOnly) { int action = getAction(); int rowAction = row.getAction(); int start; int len; if (whereOnly) { if (action == ACTION_INSERT || rowAction == ACTION_INSERT) start = len = 0; else start = len = _vals.length / 2; } else { start = 0; if (rowAction == ACTION_INSERT && action != ACTION_INSERT) len = _vals.length / 2; else len = _vals.length; } System.arraycopy(_vals, start, row._vals, start, len); System.arraycopy(_types, start, row._types, start, len); if (isValid()) row.setValid(true); } public Object[] getVals() { return _vals; } public int[] getTypes() { return _types; } public boolean isFlushed() { return _isFlushed; } public void setFlushed(boolean isFlushed) { _isFlushed = isFlushed; } }