/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed 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 net.ontopia.persistence.proxy;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Collection;
import java.util.Iterator;
import net.ontopia.utils.CollectionUtils;
import net.ontopia.utils.OntopiaRuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* INTERNAL: Class that performs the task of accessing and
* manipulating "identifiable object type" instances in the database.
*/
public class SQLObjectAccess implements ClassAccessIF {
// Define a logging category.
static Logger log = LoggerFactory.getLogger(SQLObjectAccess.class.getName());
protected boolean debug = log.isDebugEnabled();
protected RDBMSAccess access;
protected ClassInfoIF cinfo;
protected String sql_load;
protected String sql_create;
protected String sql_delete;
protected FieldInfoIF identity_field;
protected FieldInfoIF[] value_fields;
protected FieldInfoIF[] o2o_fields;
protected FieldInfoIF[] o2a_fields;
protected FieldInfoIF[] m2m_fields;
protected FieldAccessIF[] faccesses;
public SQLObjectAccess(RDBMSAccess access, ClassInfoIF cinfo) {
// TODO: The load method is identical to the one in
// SQLOneToOne.java. Should make sure that they are being shared.
this.access = access;
this.cinfo = cinfo;
// Get 1:M aggregate and M:M reference field infos
this.o2a_fields = FieldUtils.filterAggregate(cinfo.getOne2ManyFieldInfos());
this.m2m_fields = cinfo.getMany2ManyFieldInfos();
// Prepare fields
identity_field = cinfo.getIdentityFieldInfo();
value_fields = cinfo.getValueFieldInfos();
o2o_fields = cinfo.getOne2OneFieldInfos();
// -----------------------------------------------------------------------------
// Load
// -----------------------------------------------------------------------------
// Generate SQL statement
FieldInfoIF[] fields = FieldUtils.joinFieldInfos(new FieldInfoIF[] { identity_field }, o2o_fields);
sql_load = SQLGenerator.getSelectStatement(cinfo.getMasterTable(),
fields, new FieldInfoIF[] { identity_field }, 0);
if (debug)
// log.debug("Load SQL (1:1) " + field.getName() + ": " + sql_load);
log.debug("Load SQL (1:1) : " + sql_load);
// Selecting all 1:1 fields
// SELECT id, topicmap_id, subject_notation, subject_address FROM TM_TOPIC WHERE id = ?
// -----------------------------------------------------------------------------
// Create
// -----------------------------------------------------------------------------
// Generate SQL statement
//! FieldInfoIF[] fields = FieldUtils.joinFieldInfos(new FieldInfoIF[] { identity_field }, o2o_fields);
sql_create = SQLGenerator.getInsertStatement(cinfo.getMasterTable(), fields);
if (debug)
log.debug("Create SQL (" + cinfo.getDescriptorClass().getName() + "): " + sql_create);
// INSERT INTO TM_TOPIC (id, topicmap_id, subject_notation, subject_address) VALUES (?, ?, ?, ?)
// -----------------------------------------------------------------------------
// Delete
// -----------------------------------------------------------------------------
// Generate SQL statement
sql_delete = SQLGenerator.getDeleteStatement(cinfo.getMasterTable(), new FieldInfoIF[] { identity_field });
if (debug)
log.debug("Delete SQL (" + cinfo.getDescriptorClass().getName() + "): " + sql_delete);
// DELETE FROM TM_TOPIC WHERE id = ?
// -----------------------------------------------------------------------------
// Field accesses handlers
// -----------------------------------------------------------------------------
faccesses = new FieldAccessIF[value_fields.length];
// Loop over the value fields and create field access handlers
for (int i=0; i < value_fields.length; i++) {
faccesses[i] = getFieldAccess(i);
}
}
protected FieldAccessIF getFieldAccess(int field) {
// Create a field access instance for the given field
FieldInfoIF finfo = value_fields[field];
switch (finfo.getCardinality()) {
case FieldInfoIF.ONE_TO_ONE:
return new SQLOneToOne(access, finfo);
case FieldInfoIF.ONE_TO_MANY:
if (finfo.isAggregateField())
return new SQLOneToManyAggregate(access, finfo);
else
return new SQLOneToManyReference(access, finfo);
case FieldInfoIF.MANY_TO_MANY:
return new SQLManyToManyReference(access, finfo);
default:
throw new OntopiaRuntimeException("Unknown field cardinality: " + finfo.getCardinality());
}
}
// -----------------------------------------------------------------------------
// Load
public boolean load(AccessRegistrarIF registrar, IdentityIF identity) throws Exception {
// Get ticket
TicketIF ticket = registrar.getTicket();
// Prepare statement
PreparedStatement stm = access.prepareStatement(sql_load);
try {
// Bind identity columns
identity_field.bind(identity, stm, 1);
// Execute statement
if (debug)
log.debug("Executing: " + sql_load);
ResultSet rs = stm.executeQuery();
// Exactly one row expected
if (rs.next()) {
// Register object identity with registrar
registrar.registerIdentity(ticket, identity);
// Load row data (skip identity column)
int rsindex = 1 + identity_field.getColumnCount();
for (int i=0; i < o2o_fields.length; i++) {
// Let field info traverse the result set row
FieldInfoIF finfo = o2o_fields[i];
// Load field value
Object value = finfo.load(registrar, ticket, rs, rsindex, false);
if (value instanceof OnDemandValue) {
OnDemandValue odv = (OnDemandValue)value;
odv.setContext(identity, finfo);
}
// Update cache
registrar.registerField(ticket, identity, finfo.getIndex(), value);
// Increment column index
rsindex += finfo.getColumnCount();
}
// Close result set
rs.close();
return true;
} else {
// Close result set
rs.close();
// No rows were found.
return false;
}
} finally {
if (stm != null) stm.close();
}
}
// -----------------------------------------------------------------------------
// Load field
public Object loadField(AccessRegistrarIF registrar, IdentityIF identity, int field) {
try {
//! System.out.println("LFF:" + field + " " + identity);
return faccesses[field].load(registrar, identity);
} catch (IdentityNotFoundException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
public static int batchSize = 50;
public Object loadFieldMultiple(AccessRegistrarIF registrar, Collection<IdentityIF> identities,
IdentityIF current, int field) {
try {
if (identities.size() > batchSize) {
// split identities into smaller batches that can be handled
// by underlying data store
Object result = null;
Iterator<IdentityIF> iter = identities.iterator();
while (iter.hasNext()) {
Collection<IdentityIF> batch = CollectionUtils.nextBatch(iter, batchSize);
result = faccesses[field].loadMultiple(registrar, batch, current);
}
return result;
} else {
return faccesses[field].loadMultiple(registrar, identities, current);
}
} catch (IdentityNotFoundException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new OntopiaRuntimeException(e);
}
}
// -----------------------------------------------------------------------------
// Create
public void create(ObjectAccessIF oaccess, Object object) throws Exception {
// Prepare statement
PreparedStatement stm = access.prepareStatement(sql_create);
try {
// Bind parameters
bindParametersCreate(stm, oaccess, object);
// Execute statement
if (debug) log.debug("Executing: " + sql_create);
stm.executeUpdate();
} finally {
if (stm != null) stm.close();
}
}
protected void bindParametersCreate(PreparedStatement stm, ObjectAccessIF oaccess, Object object) throws Exception {
// Note: The fields map is modified by removing keys of fields
// that are being inserted at this point. This hack and might be
// optimized in the future.
// Bind identity columns
Object identity = oaccess.getIdentity(object);
if (debug) log.debug("Binding object identity: " + identity);
identity_field.bind(identity, stm, 1);
// Bind value columns
int stmindex = 1 + identity_field.getColumnCount();
for (int i=0; i < o2o_fields.length; i++) {
// Let field infos bind themselves
FieldInfoIF finfo = o2o_fields[i];
// Get field value
Object value = oaccess.getValue(object, finfo);
// Bind field value
finfo.bind(value, stm, stmindex);
// Mark dirty field as flushed
oaccess.setDirtyFlushed(object, finfo.getIndex());
// Increment column index
stmindex += finfo.getColumnCount();
}
// TODO: Reset all dirty flags in one go: ObjectAccessIF.setDirtyFlushed(false).
}
// -----------------------------------------------------------------------------
// Delete
public void delete(ObjectAccessIF oaccess, Object object) throws Exception {
IdentityIF identity = oaccess.getIdentity(object);
// Clear 1:M and M:M fields
clearFields(identity);
// Prepare statement
PreparedStatement stm = access.prepareStatement(sql_delete);
try {
// Bind parameters
bindParametersDelete(stm, identity);
// Execute statement
if (debug) log.debug("Executing: " + sql_delete);
stm.executeUpdate();
} finally {
if (stm != null) stm.close();
}
}
protected void bindParametersDelete(PreparedStatement stm, IdentityIF identity) throws Exception {
// Bind identity columns
if (debug) log.debug("Binding object identity: " + identity);
identity_field.bind(identity, stm, 1);
}
protected void clearFields(IdentityIF identity) throws Exception {
// Loop over all 1:M aggregate fields and remove their entries (or all in one go)
for (int i=0; i < o2a_fields.length; i++) {
log.debug("Deleting 1:M (aggregate) " + o2a_fields[i].getName());
// FIXME: not neccessary if collection is empty
faccesses[o2a_fields[i].getIndex()].clear(identity);
}
// Loop over all M:M fields and remove their entries (or all in one go)
for (int i=0; i < m2m_fields.length; i++) {
log.debug("Deleting M:M " + m2m_fields[i].getName());
// FIXME: not neccessary if collection is empty
faccesses[m2m_fields[i].getIndex()].clear(identity);
}
}
// -----------------------------------------------------------------------------
// Store dirty
public void storeDirty(ObjectAccessIF oaccess, Object object) throws Exception {
// Loop over dirty fields
int fcount = value_fields.length;
int i = 0; // Index of next dirty field
while (i < fcount) {
i = oaccess.nextDirty(object, i, fcount);
if (i == -1) break;
//! System.out.println(">- " + i + " " + oaccess.getIdentity(object));
if (value_fields[i].isReadOnly()) {
// Mark dirty field as flushed
oaccess.setDirtyFlushed(object, i);
} else {
// Update field
faccesses[i].storeDirty(oaccess, object);
}
// Prepare for next index
i++;
}
// WARNING: If the FieldAccessIF instances does not reset the
// dirty flag we may end up looping forever.
}
}