/* * #! * 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.util.ArrayList; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import net.ontopia.persistence.proxy.AccessRegistrarIF; import net.ontopia.persistence.proxy.CachesIF; import net.ontopia.persistence.proxy.FieldInfoIF; import net.ontopia.persistence.proxy.IdentityIF; import net.ontopia.persistence.proxy.ObjectRelationalMappingIF; import net.ontopia.persistence.proxy.PersistentIF; import net.ontopia.persistence.proxy.RDBMSAccess; import net.ontopia.persistence.proxy.RDBMSStorage; import net.ontopia.persistence.proxy.SQLGenerator; import net.ontopia.persistence.proxy.SQLObjectAccess; import net.ontopia.persistence.proxy.StorageCacheIF; import net.ontopia.persistence.proxy.TicketIF; import net.ontopia.persistence.proxy.TransactionIF; import net.ontopia.persistence.proxy.TransactionalLookupIndexIF; import net.ontopia.persistence.proxy.TransactionalSoftHashMapIndex; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.impl.utils.TopicMapTransactionIF; import net.ontopia.topicmaps.impl.utils.EventListenerIF; import net.ontopia.topicmaps.impl.utils.EventManagerIF; import net.ontopia.topicmaps.query.impl.utils.Prefetcher; import net.ontopia.utils.OntopiaRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: */ public class RoleTypeAssocTypeCache { // Define a logging category. static Logger log = LoggerFactory.getLogger(RoleTypeAssocTypeCache.class.getName()); protected RDBMSAccess access; protected AccessRegistrarIF registrar; protected FieldInfoIF TopicIF_idfield; protected FieldInfoIF TopicMapIF_idfield; protected FieldInfoIF AssociationIF_idfield; protected FieldInfoIF AssociationRoleIF_idfield; protected String sql; protected String sql_individual; protected int batchSize = SQLObjectAccess.batchSize; protected TopicMapIF tm; protected TopicMapTransactionIF txn; protected TransactionIF ptxn; protected TransactionalLookupIndexIF rolesByType; protected boolean qlshared; protected Map radd = new HashMap(); protected Map rrem = new HashMap(); public RoleTypeAssocTypeCache(TopicMapTransactionIF txn, EventManagerIF emanager, EventManagerIF otree) { this.txn = txn; this.ptxn = ((RDBMSTopicMapTransaction)txn).getTransaction(); RDBMSStorage storage = (RDBMSStorage)ptxn.getStorageAccess().getStorage(); StorageCacheIF scache = storage.getStorageCache(); this.access = (RDBMSAccess)ptxn.getStorageAccess(); this.registrar = (scache == null ? ptxn.getAccessRegistrar() : scache.getRegistrar()); // lookup query caches this.tm = txn.getTopicMap(); IdentityIF tmid = ((PersistentIF)txn.getTopicMap())._p_getIdentity(); if (storage.isSharedCache()) { this.rolesByType = (TransactionalLookupIndexIF)storage.getHelperObject(CachesIF.QUERY_CACHE_RT2, tmid); this.qlshared = true; } else { // ISSUE: need lru? this.rolesByType = new TransactionalSoftHashMapIndex(); this.qlshared = false; } // register event handlers otree.addListener(new AssociationRoleAddedHandler(), AssociationRoleIF.EVENT_ADDED); otree.addListener(new AssociationRoleRemovedHandler(), AssociationRoleIF.EVENT_REMOVED); emanager.addListener(new EH01(), AssociationRoleIF.EVENT_SET_TYPE); emanager.addListener(new EH02(), AssociationRoleIF.EVENT_SET_PLAYER); emanager.addListener(new EH03(), AssociationIF.EVENT_SET_TYPE); // get mapping data ObjectRelationalMappingIF mapping = storage.getMapping(); // get identity fields this.TopicIF_idfield = mapping.getClassInfo(Topic.class).getIdentityFieldInfo(); this.TopicMapIF_idfield = mapping.getClassInfo(TopicMap.class).getIdentityFieldInfo(); this.AssociationIF_idfield = mapping.getClassInfo(Association.class).getIdentityFieldInfo(); this.AssociationRoleIF_idfield = mapping.getClassInfo(AssociationRole.class).getIdentityFieldInfo(); // build query strings StringBuilder sb = new StringBuilder(); sb.append("select r.player_id, r.id, r.assoc_id from TM_ASSOCIATION_ROLE r, TM_ASSOCIATION a where r.topicmap_id = ? and r.type_id = ? and r.assoc_id = a.id and a.topicmap_id = ? and a.type_id = ? and r.player_id in ("); for (int i=0; i < batchSize; i++) { if (i > 0) sb.append(", ?"); else sb.append('?'); } sb.append(")"); this.sql = sb.toString(); this.sql_individual = "select r.id, a.id from TM_ASSOCIATION_ROLE r, TM_ASSOCIATION a where r.topicmap_id = ? and r.type_id = ? and r.assoc_id = a.id and a.topicmap_id = ? and a.type_id = ? and r.player_id = ?"; } // ----------------------------------------------------------------------------- // transaction callbacks // ----------------------------------------------------------------------------- public void commit() { if (qlshared) { // invalidate shared query cache entries if (!radd.isEmpty()) { try { rolesByType.removeAll(new ArrayList(radd.keySet())); } finally { radd = new HashMap(); } } if (!rrem.isEmpty()) { try { rolesByType.removeAll(new ArrayList(rrem.keySet())); } finally { rrem = new HashMap(); } } } rolesByType.commit(); } public void abort() { if (qlshared) { radd.clear(); rrem.clear(); } rolesByType.abort(); } // ----------------------------------------------------------------------------- // prefetch and lookup methods // ----------------------------------------------------------------------------- public void prefetchRolesByType(Collection players, TopicIF rtype, TopicIF atype) { // TODO: // // - foreach player check query cache for key: new Object[] { player, rtype, atype }} // - for those players that don't have an entry in the query cache: // - execute individual query // - update shared cache // - update query cache // ISSUES: // // - execute query through shared access // - notify shared cache registrar try { if (!ptxn.isClean()) return; // flush transaction if not shared if (!qlshared) ptxn.flush(); // get identities IdentityIF rtypeid = i(rtype); IdentityIF atypeid = i(atype); IdentityIF tmid = i(tm); // check cache and prepare result collection Object[] key = new Object[] { null, rtypeid, atypeid }; ParameterArray params = new ParameterArray(key); Map rbt = new HashMap(players.size()); Iterator iter = players.iterator(); while (iter.hasNext()) { key[0] = i(iter.next()); // filter out parameters that are already in the cache if (key[0] != null && rolesByType.get(params) == null) rbt.put(key[0], new HashSet()); } if (rbt.size() < 1) return; // collection associations for prefetching Collection assocs = new HashSet(); // Get ticket TicketIF ticket = registrar.getTicket(); // run batch query Connection conn = access.getConnection(); PreparedStatement stm = conn.prepareStatement(sql); stm.setFetchSize(1000); Collection filteredPlayerIds = rbt.keySet(); iter = filteredPlayerIds.iterator(); try { while (iter.hasNext()) { int offset = 1; // bind: r.topicmap_id offset = bind(TopicMapIF_idfield, tmid, stm, offset); // bind: r.type_id offset = bind(TopicIF_idfield, rtypeid, stm, offset); // bind: a.topicmap_id offset = bind(TopicMapIF_idfield, tmid, stm, offset); // bind: a.type_id offset = bind(TopicIF_idfield, atypeid, stm, offset); // bind: r.player_id* SQLGenerator.bindMultipleParameters(iter, TopicIF_idfield, stm, offset, batchSize); // execute statement if (log.isDebugEnabled()) log.debug("Executing: " + sql); ResultSet rs = stm.executeQuery(); // zero or more rows expected while (rs.next()) { offset = 1; // load player identity IdentityIF pid = (IdentityIF)TopicIF_idfield.load(registrar, ticket, rs, offset, false); offset += TopicIF_idfield.getColumnCount(); // load association role identity IdentityIF rid = (IdentityIF)AssociationRoleIF_idfield.load(registrar, ticket, rs, offset, false); offset += AssociationRoleIF_idfield.getColumnCount(); // load association identity IdentityIF aid = (IdentityIF)AssociationIF_idfield.load(registrar, ticket, rs, offset, false); offset += AssociationIF_idfield.getColumnCount(); // update roles list Collection roles = (Collection)rbt.get(pid); roles.add(rid); // collect association assocs.add(aid); // update storage cache registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_topicmap, tmid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_type, rtypeid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_player, pid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_association, aid); registrar.registerField(ticket, aid, Prefetcher.AssociationIF_topicmap, tmid); registrar.registerField(ticket, aid, Prefetcher.AssociationIF_type, atypeid); } // close result set rs.close(); } } finally { if (stm != null) stm.close(); } // update query cache iter = filteredPlayerIds.iterator(); while (iter.hasNext()) { Object playerid = iter.next(); Collection r = (Collection)rbt.get(playerid); ParameterArray k = new ParameterArray(new Object[] { playerid, rtypeid, atypeid }); rolesByType.put(k, r); } // prefetch A.roles.player //! System.out.println("PF: " + rtypeid + " " + atypeid + " " + assocs.size()); ptxn.prefetch(Association.class, Prefetcher_RBT_fields, Prefetcher_RBT_traverse, assocs); } catch (SQLException e) { throw new OntopiaRuntimeException(e); } } private final static int[] Prefetcher_RBT_fields = new int[] { Prefetcher.AssociationIF_roles, Prefetcher.AssociationRoleIF_player }; private final static boolean[] Prefetcher_RBT_traverse = new boolean[] { false, false }; public Collection getRolesByType(TopicIF player, TopicIF rtype, TopicIF atype) { // TODO: // // - check query cache for key: new Object[] { player, rtype, atype }} // - if match return to caller // - if player does not have a match execute the individual query // - update shared cache // - update query cache // - sync with transaction (remove matches that no longer match) // - no match if: R.player != player & R.type != rtype % R.assoc.type != atype // - return to caller // ON COMMIT: // // - invalidate query entries where try { // flush transaction if not shared if (!qlshared) ptxn.flush(); // get identities IdentityIF playerid = i(player); IdentityIF rtypeid = i(rtype); IdentityIF atypeid = i(atype); IdentityIF tmid = i(tm); // check query cache ParameterArray params = new ParameterArray(new Object[] { playerid, rtypeid, atypeid }); Object result = rolesByType.get(params); if (result != null) return syncWithTransaction((Collection)result, params, playerid, rtypeid, atypeid, tmid); //! System.out.println("CM: " + params); // Get ticket TicketIF ticket = registrar.getTicket(); // run individual query Connection conn = access.getConnection(); PreparedStatement stm = conn.prepareStatement(sql_individual); stm.setFetchSize(500); int offset = 1; // bind: r.topicmap_id offset = bind(TopicMapIF_idfield, tmid, stm, offset); // bind: r.type_id offset = bind(TopicIF_idfield, rtypeid, stm, offset); // bind: a.topicmap_id offset = bind(TopicMapIF_idfield, tmid, stm, offset); // bind: a.type_id offset = bind(TopicIF_idfield, atypeid, stm, offset); // bind: r.player_id* offset = bind(TopicIF_idfield, playerid, stm, offset); Collection roles = new HashSet(); try { // execute statement if (log.isDebugEnabled()) log.debug("Executing: " + sql_individual); ResultSet rs = stm.executeQuery(); // zero or more rows expected while (rs.next()) { offset = 1; // load association role identity IdentityIF rid = (IdentityIF)AssociationRoleIF_idfield.load(registrar, ticket, rs, offset, false); offset += AssociationRoleIF_idfield.getColumnCount(); // load association identity IdentityIF aid = (IdentityIF)AssociationIF_idfield.load(registrar, ticket, rs, offset, false); offset += AssociationIF_idfield.getColumnCount(); // update roles list roles.add(rid); // update storage cache registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_topicmap, tmid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_type, rtypeid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_player, playerid); registrar.registerField(ticket, rid, Prefetcher.AssociationRoleIF_association, aid); registrar.registerField(ticket, aid, Prefetcher.AssociationIF_topicmap, tmid); registrar.registerField(ticket, aid, Prefetcher.AssociationIF_type, atypeid); } // close result set rs.close(); } finally { if (stm != null) stm.close(); } // update query cache rolesByType.put(params, roles); // sync changes with transaction return syncWithTransaction(roles, params, playerid, rtypeid, atypeid, tmid); } catch (SQLException e) { throw new OntopiaRuntimeException(e); } } protected Collection syncWithTransaction(Collection roles, ParameterArray params, IdentityIF playerid, IdentityIF rtypeid, IdentityIF atypeid, IdentityIF tmid) { // filter out roles where appropriate Collection result = new HashSet(roles.size()); // look up roles objects in transaction Iterator iter = roles.iterator(); while (iter.hasNext()) { IdentityIF rid = (IdentityIF)iter.next(); AssociationRoleIF r; try { r = (AssociationRoleIF)ptxn.getObject(rid); } catch (Throwable t) { r = null; // identity not found } // passed through filter so add to result if (r != null) result.add(r); } // add roles that have been introduced in this transaction Collection ra = (Collection)radd.get(params); if (ra != null) { Iterator i = ra.iterator(); while (i.hasNext()) { result.add(i.next()); } } // remove roles that are no longer applicable Collection rr = (Collection)rrem.get(params); if (rr != null) { Iterator i = rr.iterator(); while (i.hasNext()) { result.remove(i.next()); } } //! System.out.println("B: " + roles + " A: " + result); //! System.out.println("BA: " + radd.get(params)); //! System.out.println("BR: " + rrem.get(params)); return result; } protected int bind(FieldInfoIF finfo, Object value, PreparedStatement stm, int offset) throws SQLException { finfo.bind(value, stm, offset); return offset + finfo.getColumnCount(); } // ----------------------------------------------------------------------------- // Event handlers // ----------------------------------------------------------------------------- protected IdentityIF i(Object o) { return (o == null ? null : ((PersistentIF)o)._p_getIdentity()); } protected void addEntry(ParameterArray key, AssociationRoleIF added) { // + added Collection avals = (Collection)radd.get(key); if (avals == null) { avals = new HashSet(); radd.put(key, avals); } avals.add(added); // - removed Collection rvals = (Collection)rrem.get(key); if (rvals != null) rvals.remove(added); } protected void removeEntry(ParameterArray key, AssociationRoleIF removed) { // + removed Collection rvals = (Collection)rrem.get(key); if (rvals == null) { rvals = new HashSet(); rrem.put(key, rvals); } rvals.add(removed); // - added Collection avals = (Collection)radd.get(key); if (avals != null) avals.remove(removed); } /** * EventHandler: AssociationRoleIF.added */ class AssociationRoleAddedHandler implements EventListenerIF { public void processEvent(Object object, String event, Object new_value, Object old_value) { AssociationRoleIF added = (AssociationRoleIF)new_value; // ignore event if player is null TopicIF player = added.getPlayer(); if (player == null) return; // get association type AssociationIF assoc = added.getAssociation(); TopicIF atype = (assoc == null ? null : assoc.getType()); // register entry addEntry(new ParameterArray(new Object[] { i(player), i(added.getType()), i(atype) }), added); } } /** * EventHandler: AssociationRoleIF.removed */ class AssociationRoleRemovedHandler implements EventListenerIF { public void processEvent(Object object, String event, Object new_value, Object old_value) { AssociationRoleIF removed = (AssociationRoleIF)old_value; // ignore event if player is null TopicIF player = removed.getPlayer(); if (player == null) return; // get association type AssociationIF assoc = removed.getAssociation(); TopicIF atype = (assoc == null ? null : assoc.getType()); // unregister entry removeEntry(new ParameterArray(new Object[] { i(player), i(removed.getType()), i(atype) }), removed); } } /** * EventHandler: AssociationRoleIF.setType */ class EH01 implements EventListenerIF { public void processEvent(Object object, String event, Object new_value, Object old_value) { AssociationRoleIF arole = (AssociationRoleIF)object; // ignore event if player is null TopicIF player = arole.getPlayer(); if (player == null) return; // get association type AssociationIF assoc = arole.getAssociation(); TopicIF atype = (assoc == null ? null : assoc.getType()); // unregister old entry removeEntry(new ParameterArray(new Object[] { i(player), i(old_value), i(atype) }), arole); // register new entry addEntry(new ParameterArray(new Object[] { i(player), i(new_value), i(atype) }), arole); } } /** * EventHandler: AssociationRoleIF.setPlayer */ class EH02 implements EventListenerIF { public void processEvent(Object object, String event, Object new_value, Object old_value) { AssociationRoleIF arole = (AssociationRoleIF)object; // get association type AssociationIF assoc = arole.getAssociation(); TopicIF atype = (assoc == null ? null : assoc.getType()); // unregister old entry if (old_value != null) removeEntry(new ParameterArray(new Object[] { i(old_value), i(arole.getType()), i(atype) }), arole); // register new entry if (new_value != null) addEntry(new ParameterArray(new Object[] { i(new_value), i(arole.getType()), i(atype) }), arole); } } /** * EventHandler: AssociationIF.setType */ class EH03 implements EventListenerIF { public void processEvent(Object object, String event, Object new_value, Object old_value) { AssociationIF assoc = (Association)object; // loop over roles Iterator iter = assoc.getRoles().iterator(); while (iter.hasNext()) { AssociationRoleIF arole = (AssociationRoleIF)iter.next(); // ignore event if player is null TopicIF player = arole.getPlayer(); if (player == null) continue; // unregister old entry removeEntry(new ParameterArray(new Object[] { i(player), i(arole.getType()), i(old_value) }), arole); // register new entry addEntry(new ParameterArray(new Object[] { i(player), i(arole.getType()), i(new_value) }), arole); } } } }