/*
* #!
* 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.topicmaps.impl.rdbms;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.persistence.proxy.AbstractTransaction;
import net.ontopia.persistence.proxy.ConnectionFactoryIF;
import net.ontopia.persistence.proxy.IdentityIF;
import net.ontopia.persistence.proxy.PersistentIF;
import net.ontopia.persistence.proxy.RDBMSAccess;
import net.ontopia.persistence.proxy.RDBMSStorage;
import net.ontopia.persistence.proxy.RWTransaction;
import net.ontopia.persistence.proxy.StorageCacheIF;
import net.ontopia.persistence.proxy.StorageIF;
import net.ontopia.persistence.proxy.TransactionIF;
import net.ontopia.topicmaps.core.NotRemovableException;
import net.ontopia.topicmaps.core.ReadOnlyException;
import net.ontopia.topicmaps.core.StoreNotOpenException;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TopicMapStoreIF;
import net.ontopia.topicmaps.entry.TopicMapSourceIF;
import net.ontopia.topicmaps.impl.utils.AbstractTopicMapStore;
import net.ontopia.topicmaps.impl.utils.AbstractTopicMapTransaction;
import net.ontopia.topicmaps.impl.utils.EventManagerIF;
import net.ontopia.topicmaps.impl.utils.TopicMapTransactionIF;
import net.ontopia.utils.OntopiaRuntimeException;
/**
* PUBLIC: The rdbms topic map store implementation.<p>
*/
public class RDBMSTopicMapStore extends AbstractTopicMapStore {
long topicmap_id;
protected RDBMSStorage storage;
protected String propfile;
protected Map<String, String> properties;
protected RDBMSTopicMapTransaction transaction;
protected boolean storage_local = false;
/**
* PUBLIC: Creates a new topic map store without a specified
* database property file. A new topic map is created in the
* repository with this constructor at the time the topic map is
* accessed.
*/
public RDBMSTopicMapStore() throws IOException {
this(-1);
}
/**
* PUBLIC: Creates a new topic map store without a specified
* database property file. The store references an existing topic
* map with the specified id.
*/
public RDBMSTopicMapStore(long topicmap_id) throws IOException {
this.storage = new RDBMSStorage();
this.storage_local = true;
this.topicmap_id = topicmap_id;
}
/**
* PUBLIC: Creates a new topic map store with the database property
* file set. A new topic map is created in the repository with this
* constructor at the time the topic map is accessed.
* @param propfile Path reference to a Java properties file.
*/
public RDBMSTopicMapStore(String propfile) throws IOException {
this(propfile, -1);
}
/**
* PUBLIC: Creates a new topic map store with the database property
* file set. The store references an existing topic map with the
* specified id.
* @param propfile Path reference to a Java properties file.
* @param topicmap_id The ID of the topic map in the database.
*/
public RDBMSTopicMapStore(String propfile, long topicmap_id) throws IOException {
this.propfile = propfile;
this.storage = new RDBMSStorage(propfile);
this.storage_local = true;
this.topicmap_id = topicmap_id;
}
/**
* PUBLIC: Creates a new topic map store with the specified database
* properties. A new topic map is created in the repository with
* this constructor at the time the topic map is accessed.
*
* @since 1.2.4
*/
public RDBMSTopicMapStore(Map<String, String> properties) throws IOException {
this(properties, -1);
}
/**
* PUBLIC: Creates a new topic map store with the specified database
* properties. The store references an existing topic map with the
* specified id.
*
* @since 1.2.4
*/
public RDBMSTopicMapStore(Map<String, String> properties, long topicmap_id) throws IOException {
this.properties = properties;
this.storage = new RDBMSStorage(properties);
this.storage_local = true;
this.topicmap_id = topicmap_id;
}
/**
* INTERNAL:
*/
public RDBMSTopicMapStore(StorageIF storage) {
this(storage, -1);
}
/**
* INTERNAL:
*/
public RDBMSTopicMapStore(StorageIF storage, long topicmap_id) {
this.storage = (RDBMSStorage)storage;
this.topicmap_id = topicmap_id;
}
/**
* INTERNAL: Returns the proxy storage implementation used by the
* topic map store.
*/
public RDBMSStorage getStorage() {
if (storage_local && storage == null) {
try {
if (propfile != null)
this.storage = new RDBMSStorage(propfile);
else if (properties != null)
this.storage = new RDBMSStorage(properties);
else
this.storage = new RDBMSStorage();
} catch (IOException e) {
throw new OntopiaRuntimeException(e);
}
}
return storage;
}
public int getImplementation() {
return TopicMapStoreIF.RDBMS_IMPLEMENTATION;
}
public boolean isTransactional() {
return true;
}
@Override
public LocatorIF getBaseAddress() {
if (base_address != null)
return base_address;
else if (readonly)
return ((ReadOnlyTopicMap)getTopicMap()).getBaseAddress();
else
return ((TopicMap)getTopicMap()).getBaseAddress();
}
public void setBaseAddress(LocatorIF base_address) {
this.base_address = null;
// update persistent field
if (readonly)
((ReadOnlyTopicMap)getTopicMap()).setBaseAddress(base_address);
else
((TopicMap)getTopicMap()).setBaseAddress(base_address);
}
/**
* INTERNAL: Sets the apparent base address of the store. The value
* of this field is not considered persistent and may for that
* reason be transaction specific.
*/
public void setBaseAddressOverride(LocatorIF base_address) {
this.base_address = base_address;
}
public TransactionIF getTransactionIF() {
RDBMSTopicMapTransaction txn = (RDBMSTopicMapTransaction)getTransaction();
return txn.getTransaction();
}
public TopicMapTransactionIF getTransaction() {
// Open store automagically if store is not open at this point.
if (!isOpen()) open();
// Create a new transaction if it doesn't exist or it has been
// deactivated.
if (transaction == null || !transaction.isActive()) {
transaction = new RDBMSTopicMapTransaction(this, topicmap_id, readonly);
}
return transaction;
}
@Override
public TopicMapIF getTopicMap() {
return getTransaction().getTopicMap();
}
@Override
public void commit() {
if (transaction != null) {
transaction.commit();
this.topicmap_id = transaction.getActualId();
}
}
@Override
public void abort() {
if (transaction != null) transaction.abort();
}
public void clear() {
if (readonly) throw new ReadOnlyException();
// Do direct table row deletion here since it is much more efficient.
try {
Utils.clearTopicMap(getTopicMap());
} catch (java.sql.SQLException e) {
throw new OntopiaRuntimeException("Could not clear topicmap. Transaction has been rolled back.", e);
}
}
boolean delete(RDBMSTopicMapReference ref) {
delete(true);
return deleted;
}
@Override
public void delete(boolean force) throws NotRemovableException {
if (readonly) throw new ReadOnlyException();
if (deleted) return;
// check that parent source allows deleting
if (reference != null) {
TopicMapSourceIF source = reference.getSource();
if (source != null && !source.supportsDelete())
throw new NotRemovableException("Cannot delete topic map as the parent topic map source does not allow deleting.");
}
// Get topic map
TopicMapIF tm = getTopicMap();
if (!force) {
// If we're not forcing, complain if the topic map contains any data.
if (!tm.getTopics().isEmpty())
throw new NotRemovableException("Cannot delete topic map when it contains topics.");
if (!tm.getAssociations().isEmpty())
throw new NotRemovableException("Cannot delete topic map when it contains associations.");
}
flush();
// Do direct table row deletion here since it is much more efficient.
try {
Utils.deleteTopicMap(tm);
// Commit and close transaction
commit();
deleted = true;
} catch (java.sql.SQLException e) {
throw new OntopiaRuntimeException("Could not delete topicmap. Transaction has been rolled back.", e);
} finally {
close();
}
// TODO: drop store from pool and delete from repository
}
/**
* INTERNAL: Gets the value of the specified store property.
*/
public String getProperty(String name) {
return getStorage().getProperty(name);
}
@Override
protected void finalize() {
// Close store when garbage collected.
if (isOpen()) close();
}
/* -- store pool -- */
public void close() {
// return to reference or close
close((reference != null));
}
public void close(boolean returnStore) {
if (returnStore) {
// allow access to release connection, preventing loads of idle connections
if ((transaction != null) && transaction.isActive()) {
TransactionIF tnx = transaction.getTransaction();
if (tnx.isActive()) {
((RDBMSAccess)tnx.getStorageAccess()).releaseConnection();
}
}
// return store
if (reference != null) {
// rollback, but not invalidate, open transaction
if (transaction != null)
((AbstractTopicMapTransaction)transaction).abort(false);
// notify topic map reference that store has been closed.
reference.storeClosed(this);
} else {
throw new OntopiaRuntimeException("Cannot return store when not attached to topic map reference.");
}
} else {
// physically close store
if (!isOpen()) throw new StoreNotOpenException("Store is not open.");
// reset reference
reference = null;
// close transaction
if (transaction != null) transaction.abort(true);
// if storage is not shared close it as well
if (storage_local && storage != null) {
storage.close();
storage = null;
}
// set open flag to false and closed to true
open = false;
closed = true;
}
}
@Override
public boolean validate() {
// if we're closed then not valid
if (closed) return false;
// delegate to transaction
if (transaction != null)
return ((AbstractTopicMapTransaction)transaction).validate();
else
return true;
}
// -- cache statistics
/**
* INTERNAL: Evicts the given object from the shared RDBMS caches.
*
* @since 3.3.0
*/
public void evictObject(String object_id) {
if (transaction != null) {
if (storage != null) {
StorageCacheIF scache = storage.getStorageCache();
if (scache != null) {
IdentityIF identity = getIdentityForObjectId(transaction.getTransaction(), object_id);
scache.evictIdentity(identity, true);
}
}
}
}
/**
* INTERNAL: Empties the shared RDBMS caches.
*
* @since 2.2.1
*/
public void clearCache() {
if (transaction != null) {
//! RDBMSStorage storage = (RDBMSStorage)transaction.getTransaction().getStorageAccess().getStorage();
// clear shared caches
if (storage != null) storage.clearCache();
}
}
/**
* EXPERIMENTAL: Writes a cache statistics report to the given
* file.
* @param filename the name of the file to write the report to
* @param dumpCaches whether to include detailed cache dumps
*
* @since 2.2.1
*/
public void writeReport(String filename, boolean dumpCaches) throws IOException {
if (transaction != null && transaction.isActive()) {
java.io.FileWriter out = new java.io.FileWriter(filename);
try {
writeReport(out, dumpCaches);
} finally {
out.close();
}
}
}
/**
* EXPERIMENTAL: Writes a cache statistics report to the given writer.
* @param out the writer to write the report to
* @param dumpCaches whether to include detailed cache dumps
*
* @since 2.2.1
*/
public void writeReport(java.io.Writer out, boolean dumpCaches) throws IOException {
if (transaction != null && transaction.isActive()) {
//! RDBMSStorage storage = (RDBMSStorage)transaction.getTransaction().getStorageAccess().getStorage();
if (storage != null) {
IdentityIF namespace = ((PersistentIF)getTopicMap())._p_getIdentity();
storage.writeReport(out, reference, namespace, dumpCaches);
}
}
}
/**
* EXPERIMENTAL: Dumps the identity map to the given writer.
* @param out the writer to write the report to
*
* @since 3.0
*/
public void writeIdentityMap(java.io.Writer out, boolean dump)
throws IOException {
if (transaction != null && transaction.isActive()) {
AbstractTransaction txn = (AbstractTransaction)transaction.getTransaction();
txn.writeIdentityMap(out, dump);
}
}
// ---------------------------------------------------------------------------
// Prefetch objects by object id
// ---------------------------------------------------------------------------
public boolean prefetchObjectsById(Collection<String> object_ids) {
TransactionIF txn = transaction.getTransaction();
// create a map per object type
Map<Class<?>, Collection<IdentityIF>> idmap = new HashMap<Class<?>, Collection<IdentityIF>>();
for (String object_id : object_ids) {
IdentityIF identity = getIdentityForObjectId(txn, object_id);
if (identity == null) continue;
if (txn.isObjectLoaded(identity)) continue;
Collection<IdentityIF> ids = idmap.get(identity.getType());
if (ids == null) {
ids = new ArrayList<IdentityIF>();
idmap.put(identity.getType(), ids);
}
ids.add(identity);
}
Iterator<Class<?>> keys = idmap.keySet().iterator();
while (keys.hasNext()) {
// prefetch TMObjectIF_topicmap (always field number 1)
Class<?> type = keys.next();
Collection<IdentityIF> identities = idmap.get(type);
txn.prefetch(type, 1, false, identities);
}
return true;
}
public boolean prefetchFieldsById(Collection<String> object_ids, int field) {
TransactionIF txn = transaction.getTransaction();
// create a map per object type
Map<Class<?>, Collection<IdentityIF>> idmap = new HashMap<Class<?>, Collection<IdentityIF>>();
for (String object_id : object_ids) {
IdentityIF identity = getIdentityForObjectId(txn, object_id);
if (identity == null) continue;
Collection<IdentityIF> ids = idmap.get(identity.getType());
if (ids == null) {
ids = new ArrayList<IdentityIF>();
idmap.put(identity.getType(), ids);
}
ids.add(identity);
}
Iterator<Class<?>> keys = idmap.keySet().iterator();
while (keys.hasNext()) {
// prefetch TMObjectIF_topicmap (always field number 1)
Class<?> type = keys.next();
Collection<IdentityIF> identities = idmap.get(type);
txn.prefetch(type, field, false, identities);
}
return true;
}
protected IdentityIF getIdentityForObjectId(TransactionIF txn, String object_id) {
long numid;
try {
numid = Long.parseLong(object_id.substring(1), 10);
} catch (NumberFormatException e) {
return null; // if not a valid ID no object will have it... :)
}
switch ( object_id.charAt(0) ) {
case 'T':
return txn.getAccessRegistrar().createIdentity(Topic.class, numid);
case 'A':
return txn.getAccessRegistrar().createIdentity(Association.class, numid);
case 'O':
return txn.getAccessRegistrar().createIdentity(Occurrence.class, numid);
case 'B':
return txn.getAccessRegistrar().createIdentity(TopicName.class, numid);
case 'N':
return txn.getAccessRegistrar().createIdentity(VariantName.class, numid);
case 'R':
return txn.getAccessRegistrar().createIdentity(AssociationRole.class, numid);
//! case 'M':
//! return txn.getAccessRegistrar().createIdentity(TopicMap.class, numid);
default:
return null;
}
}
// ---------------------------------------------------------------------------
// Prefetching
// ---------------------------------------------------------------------------
protected static final Class[] types = new Class[] { Association.class, AssociationRole.class, TopicName.class, Occurrence.class, Topic.class, TopicMap.class, VariantName.class };
public boolean prefetch(int type, int field, boolean traverse, Collection objects) {
TransactionIF txn = transaction.getTransaction();
int size = objects.size();
if (size == 0) return false;
//! if (size <= 3) return false;
Collection identities = new ArrayList(size);
Iterator iter = objects.iterator();
for (int i=0; i < size; i++) {
PersistentIF o = (PersistentIF)iter.next();
if (o != null) {
// filter out objects by getClass() == types[type]
IdentityIF identity = o._p_getIdentity();
if (types[type].equals(identity.getType()))
identities.add(identity);
//! else
//! new RuntimeException("X: " + o + " not of expected type " + types[type]).printStackTrace();
}
}
txn.prefetch(types[type], field, traverse, identities);
return true;
}
public boolean prefetch(int type, int[] fields, boolean[] traverse, Collection objects) {
TransactionIF txn = transaction.getTransaction();
int size = objects.size();
if (size == 0) return false;
//! if (size <= 3 && fields.length < 2) return false;
Collection identities = new ArrayList(size);
Iterator iter = objects.iterator();
for (int i=0; i < size; i++) {
// TODO: filter out objects by getClass() == types[type]
PersistentIF o = (PersistentIF)iter.next();
if (o != null) {
IdentityIF identity = o._p_getIdentity();
if (types[type].equals(identity.getType()))
identities.add(identity);
//! else
//! new RuntimeException("Y: " + o + " not of expected type " + types[type]).printStackTrace();
}
}
txn.prefetch(types[type], fields, traverse, identities);
return true;
}
// ---------------------------------------------------------------------------
// Prefetch: roles by type and association type
// ---------------------------------------------------------------------------
public void prefetchRolesByType(Collection players,
TopicIF rtype, TopicIF atype) {
transaction.prefetchRolesByType(players, rtype, atype);
}
// ---------------------------------------------------------------------------
// Misc. methods
// ---------------------------------------------------------------------------
public long getLongId() {
return transaction.getActualId();
}
public long getLongId(TMObjectIF o) {
IdentityIF identity = ((PersistentIF)o)._p_getIdentity();
return ((Long)identity.getKey(0)).longValue();
}
public void flush() {
TransactionIF txn = transaction.getTransaction();
txn.flush();
}
public java.sql.Connection getConnection() {
return ((RDBMSAccess)transaction.getTransaction().getStorageAccess()).getConnection();
}
public ConnectionFactoryIF getConnectionFactory(boolean readonly) {
return getStorage().getConnectionFactory(readonly);
}
public String getQueryString(String name) {
return getStorage().getQueryString(name);
}
/**
* INTERNAL: Called by MergeUtils to notify transaction of a performed merge.
* @param source
* @param target
*/
public void merged(TMObjectIF source, TMObjectIF target) {
TransactionIF tnx = getTransactionIF();
if (tnx instanceof RWTransaction) {
((RWTransaction)tnx).registerMerge((TMObject) source, (TMObject) target);
}
}
// ---------------------------------------------------------------------------
// EventManagerIF: for testing purposes only
// ---------------------------------------------------------------------------
@Override
public EventManagerIF getEventManager() {
return (EventManagerIF)transaction;
}
}