Mercurial Hosting > nabble
view src/nabble/model/NodeImpl.java @ 62:4674ed7d56df default tip
remove n2
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Sat, 30 Sep 2023 20:25:29 -0600 |
parents | 72765b66e2c3 |
children |
line wrap: on
line source
package nabble.model; import fschmidt.db.DbDatabase; import fschmidt.db.DbNull; import fschmidt.db.DbObjectFactory; import fschmidt.db.DbRecord; import fschmidt.db.DbTable; import fschmidt.db.DbUtils; import fschmidt.db.Listener; import fschmidt.db.ListenerList; import fschmidt.db.LongKey; import fschmidt.html.Html; import fschmidt.util.java.Computable; import fschmidt.util.java.Filter; import fschmidt.util.java.HtmlUtils; import fschmidt.util.java.Memoizer; import fschmidt.util.java.ObjectUtils; import fschmidt.util.java.SimpleCache; import nabble.model.export.Export; import nabble.model.export.NodeData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Serializable; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.CollationKey; import java.text.Collator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; final class NodeImpl implements Node { private static final Logger logger = LoggerFactory.getLogger(NodeImpl.class); private static final Message DELETED_MESSAGE = new Message("- deleted -", Message.Format.TEXT) { public boolean isDeleted() { return true; } public boolean isDeactivated() { return true; } }; final SiteKey siteKey; private final DbRecord<LongKey,NodeImpl> record; private String subject; private MyMessage message; private Date whenCreated; private long ownerId; private UserImpl owner; private long parentId = 0L; private NodeImpl parent; private Date whenUpdated; private long lastNodeId; private Date lastNodeDate; private int nodeCount; private Kind kind; private String type; private boolean isPinned; private int childCount; private String cookie; private String anonymousName; private long exportedNodeId; private String exportPermalink = null; private String exportEmail = null; private String embeddingUrl; private NodeImpl(SiteKey siteKey,LongKey key,ResultSet rs) throws SQLException { this.siteKey = siteKey; record = table(siteKey).newRecord(this,key); subject = rs.getString("subject"); Message.Format msgFmt = Message.Format.getMessageFormat( rs.getString("msg_fmt").charAt(0) ); message = new MyMessage(msgFmt); whenCreated = rs.getTimestamp("when_created"); ownerId = rs.getLong("owner_id"); parentId = rs.getLong("parent_id"); whenUpdated = rs.getTimestamp("when_updated"); lastNodeId = rs.getLong("last_node_id"); lastNodeDate = rs.getTimestamp("last_node_date"); nodeCount = rs.getInt("node_count"); kind = rs.getBoolean("is_app") ? Kind.APP : Kind.POST; type = rs.getString("type"); if( type==null ) type = Type.COMMENT; rs.getInt("pin"); isPinned = !rs.wasNull(); childCount = rs.getInt("child_count"); cookie = rs.getString("cookie"); anonymousName = rs.getString("anonymous_name"); embeddingUrl = rs.getString("embedding_url"); exportedNodeId = rs.getLong("exported_node_id"); exportPermalink = rs.getString("export_permalink"); exportEmail = rs.getString("export_email"); for( ExtensionFactory<Node,?> factory : extensionFactories ) { Object obj = factory.construct(this,rs); if( obj != null ) getExtensionMap().put(factory,obj); } } static NodeImpl newRootNode(Kind kind,UserImpl owner,String subject,String messageRaw,Message.Format msgFmt) throws ModelException { SiteImpl site = owner.getSiteImpl(); return new NodeImpl(site.siteKey,kind,owner,subject,messageRaw,msgFmt); } static NodeImpl newRootNode(Kind kind,Person owner,String subject,String messageRaw,Message.Format msgFmt,SiteImpl site,String type) throws ModelException { NodeImpl newRoot = new NodeImpl(site.siteKey,kind,owner,subject,messageRaw,msgFmt); if( type != null ) newRoot.setType(type); newRoot.insert(true); NodeImpl oldRoot = site.getRootNodeImpl(); oldRoot.parentId = newRoot.getId(); oldRoot.record.fields().put("parent_id", oldRoot.parentId); oldRoot.update(); site.setRoot(newRoot); site.getDbRecord().update(); oldRoot.addNode(); return newRoot; } static NodeImpl newChildNode(Kind kind,Person owner,String subject,String messageRaw,Message.Format msgFmt,NodeImpl parent) throws ModelException { SiteImpl site = parent.getSiteImpl(); if( !site.equals(owner.getSite()) ) throw new RuntimeException(); NodeImpl node = new NodeImpl(site.siteKey,kind,owner,subject,messageRaw,msgFmt); node.setParent(parent); return node; } private NodeImpl(SiteKey siteKey,Kind kind,Person owner,String subject,String messageRaw,Message.Format msgFmt) throws ModelException { this.siteKey = siteKey; record = table(siteKey).newRecord(this); if( owner instanceof UserImpl ) { this.owner = (UserImpl)owner; ownerId = this.owner.getId(); record.fields().put("owner_id", ownerId); } else { Anonymous anon = (Anonymous)owner; this.cookie = anon.getCookie(); this.anonymousName = anon.getName(); record.fields().put("cookie",cookie); record.fields().put("anonymous_name", anonymousName); } setKind(kind); setSubject0(subject); message = new MyMessage(msgFmt); setMessage0(messageRaw,msgFmt); setWhenCreated(new Date()); nodeCount = 1; childCount = 0; } private void setParent(NodeImpl parent) throws ModelException { if( isInDb() ) throw new RuntimeException(); this.parentId = parent.getId(); record.fields().put("parent_id", parentId); if( siteKey != parent.siteKey ) throw new RuntimeException(); if( !parent.isInDb() ) { // hack for dummy nodes this.parent = parent; List<NodeImpl> children = dummyChildMap.get(parent); if( children == null ) { children = new ArrayList<NodeImpl>(); dummyChildMap.put(parent,children); } children.add(this); return; } if( db().isInTransaction() ) uncacheAncestors(); } public DbRecord<LongKey,NodeImpl> getDbRecord() { return record; } private DbTable<LongKey,NodeImpl> table() { return record.getDbTable(); } private DbDatabase db() { return table().getDbDatabase(); } public long getId() { LongKey key = record.getPrimaryKey(); if( key==null ) return 0L; return key.value(); } public Kind getKind() { return kind; } public void setKind(Kind kind) { this.kind = kind; record.fields().put( "is_app", DbNull.fix(kind==Kind.APP) ); } long getParentId() { return parentId; } long getOwnerId() { return ownerId; } public String getSubject() { return subject; } public String getSubjectHtml() { return HtmlUtils.htmlEncode(subject); } private synchronized boolean setSubject0(String subject) throws ModelException.RequiredSubject { subject = subject.trim(); if( subject.equals("") ) throw new ModelException.RequiredSubject(); if( subject.equals(this.subject) ) return false; this.subject = subject; record.fields().put("subject",subject); return true; } public void setSubject(String subject) throws ModelException.RequiredSubject { boolean changed = setSubject0(subject); if (changed) setWhenUpdated(); } private static final long TEN_MINUTES = 10 * 60 * 1000; private void setWhenUpdated() { long timeDiff = new Date().getTime() - getWhenCreated().getTime(); boolean acceptChanges = timeDiff > TEN_MINUTES || !getChildren().isEmpty(); if (acceptChanges) { setWhenUpdated( new Date() ); } } private final class MyMessage extends Message { MyMessage(Message.Format format) { super(null,format); } MyMessage(String raw,Message.Format format) { super(raw,format); } private synchronized boolean hasLoaded() { return raw!=null; } public synchronized String getRaw() { if( raw==null && record.isInDb() ) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "select message from node_msg where node_id=?" ); stmt.setLong(1,getId()); ResultSet rs = stmt.executeQuery(); if( rs.next() ) { raw = rs.getString("message"); } else { logger.error("no message for "+NodeImpl.this+" - inserting empty message"); raw = ""; insertMessage(raw); } rs.close(); stmt.close(); con.close(); } catch(SQLException e) { throw new RuntimeException(e); } if( raw==null ) throw new NullPointerException(toString()); } return raw; } public Source getSource() { if( record.isInDb() ) return NodeImpl.this; Person owner = getOwner(); return owner instanceof User ? new Message.TempSource((User)owner) : null; } public boolean isDeleted() { return isDeletedMessage(); } public boolean isDeactivated() { UserImpl user = getOwnerImpl(); return user != null && user.isDeactivated(); } } public Message getMessage() { return message; } private boolean setMessage0(String messageRaw,Message.Format msgFmt) { MyMessage newMessage = new MyMessage(messageRaw,msgFmt); if( message.equals(newMessage) ) return false; message = newMessage; record.fields().put("msg_fmt",Character.toString(msgFmt.getCode())); record.fields().put("message",messageRaw); return true; } public void setMessage(String message,Message.Format msgFmt) { boolean changed = setMessage0(message,msgFmt); if (changed) setWhenUpdated(); } public void deleteMessageOrNode() { if( !db().isInTransaction() ) { db().beginTransaction(); try { NodeImpl node = DbUtils.getGoodCopy(NodeImpl.this); node.deleteMessageOrNode(); db().commitTransaction(); } finally { db().endTransaction(); } return; } if( getChildCount() == 0 ) { deleteRecursively(); } else { setMessage(DELETED_MESSAGE.getRaw(),DELETED_MESSAGE.getFormat()); update(); } } private boolean isDeletedMessage() { return getMessage().getRaw().equals(DELETED_MESSAGE.getRaw()); } public void deleteRecursively() { if( isRoot() ) { getSiteImpl().delete(); return; } if( !db().isInTransaction() ) { db().beginTransaction(); try { NodeImpl node = DbUtils.getGoodCopy(NodeImpl.this); node.deleteRecursively(); db().commitTransaction(); } finally { db().endTransaction(); } return; } NodeImpl parent = getParentImpl(); record.delete(); removedNodeFrom(parent); fireChangeListeners(); } private synchronized void cacheMessage(String message) { this.message.raw = message; } public Date getWhenCreated() { return whenCreated; } public void setWhenCreated(Date whenCreated) { this.whenCreated = whenCreated; record.fields().put("when_created",whenCreated); if( record.isInDb() && childCount==0 ) { setLastNodeDate( whenCreated ); removedNodeFrom( getParentImpl() ); addNode(); } } private static final Object lastNodeLock = new Object(); public Node getLastNode() { Node lastNode = getNode(lastNodeId); if( lastNode == null ) { logger.error("lastNode not found for "+this+" with lastNodeId="+lastNodeId,new NullPointerException()); // hack fix -fschmidt synchronized(lastNodeLock) { DbUtils.uncache(this); NodeImpl node = DbUtils.getGoodCopy(this); if( lastNodeId != node.lastNodeId ) return getNode(node.lastNodeId); populateLastNodeFields(); } DbUtils.uncache(this); NodeImpl node = DbUtils.getGoodCopy(this); if( lastNodeId != node.lastNodeId ) return getNode(node.lastNodeId); logger.error("after populateLastNodeFields, lastNode not found for "+node+" with lastNodeId="+node.lastNodeId); } return lastNode; } long getLastNodeId() { return lastNodeId; } public Date getLastNodeDate() { return lastNodeDate; } public Node getGoodCopy() { return DbUtils.getGoodCopy(this); } public void insert(boolean isDoneByPoster) throws ModelException.TooManyPosts { if( !db().isInTransaction() ) throw new RuntimeException(); if( kind==Kind.APP && type==null ) throw new RuntimeException("type not set for app"); if( isDoneByPoster ) { UserImpl owner = getOwnerImpl(); if (owner != null) owner.updateNewPostLimit(); } String message = (String)record.fields().remove("message"); record.insert(); insertMessage(message); setLastNodeId( getId() ); setLastNodeDate( whenCreated ); record.update(); addNode(); } private void insertMessage(String message) { try { Connection con = db().getConnection(); try { PreparedStatement pstmt = con.prepareStatement( "insert into node_msg (node_id,message) values (?,?)" ); pstmt.setLong(1,getId()); pstmt.setString(2,message); pstmt.executeUpdate(); pstmt.close(); } finally { con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } public void update() { String message = (String)record.fields().remove("message"); if( message!=null && !db().isInTransaction() ) throw new RuntimeException(); record.update(); if( message != null ) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "update node_msg set message=? where node_id=?" ); stmt.setLong(2,getId()); stmt.setString(1,message); stmt.executeUpdate(); stmt.close(); con.close(); } catch(SQLException e) { throw new RuntimeException(e); } } } void checkNewPostLimit() throws ModelException.TooManyPosts { UserImpl owner = getOwnerImpl(); if (owner!=null && owner.hasTooManyPosts()) { logger.warn( "Too many posts by "+owner ); throw new ModelException.TooManyPosts(); } } private void uncacheAncestors() { for( NodeImpl node=this; node!=null; node=node.getParentImpl() ) { DbUtils.uncache(node); } } public void changeParent(Node n) throws ModelException { NodeImpl node = (NodeImpl)n; if( !db().isInTransaction() ) { db().beginTransaction(); try { DbUtils.getGoodCopy(this).changeParent( node.getGoodCopy() ); db().commitTransaction(); return; } finally { db().endTransaction(); } } changeParentImpl(node); } void changeParentImpl(NodeImpl newParent) throws ModelException { if( !isInDb() ) throw new RuntimeException(); if( !db().isInTransaction() ) throw new RuntimeException(); final NodeImpl oldParent = getParentImpl(); if( oldParent.siteKey != newParent.siteKey ) throw new RuntimeException("can't change site"); db().runAfterCommit(new Runnable(){public void run(){ oldParent.fireChildChangeListeners(); }}); record.fields().put("pin", DbNull.INTEGER); if (newParent.getAncestors().contains(this)) throw new ModelException.NodeLoop(this); parentId = newParent.getId(); record.fields().put("parent_id", parentId); parent = null; getDbRecord().update(); removedNodeFrom(oldParent); addNode(); stale(); uncacheAncestors(); } void makeRoot() { record.fields().put("pin", DbNull.INTEGER); parentId = 0L; record.fields().put("parent_id", DbNull.INTEGER); parent = null; getDbRecord().update(); stale(); } private void stale() { Executors.executeAfterCommit(db(),new Runnable(){public void run(){ try { Lucene.staleNode(DbUtils.getGoodCopy(NodeImpl.this)); } catch(IOException e) { logger.error("StaleNode failed",e); } }}); } private void addNode() { NodeImpl parent = getParentImpl(); if( parent == null ) return; // synchronized(siteKey.lastNodeLock) { parent.setChildCount(); for( NodeImpl node = parent; node != null; node = node.getParentImpl() ) { if( node.nodeCount==1 || node.lastNodeDate.before(lastNodeDate) ) { node.setLastNodeId( lastNodeId ); node.setLastNodeDate( lastNodeDate ); } node.setNodeCount(); node.record.update(); } // } } private void removedNodeFrom(NodeImpl parent) { if( parent == null ) return; // synchronized(siteKey.lastNodeLock) { parent.setChildCount(); try { Connection con = db().getConnection(); try { PreparedStatement pstmtGetLastNode = con.prepareStatement( "select * from node" +" where parent_id = ?" +" order by last_node_date desc, node_id desc" +" limit 1" ); for( NodeImpl node = parent; node != null; node = node.getParentImpl() ) { if( node.lastNodeId == lastNodeId ) { pstmtGetLastNode.setLong(1,node.getId()); ResultSet rs = pstmtGetLastNode.executeQuery(); if( rs.next() ) { NodeImpl lastNode = getNode(rs); node.setLastNodeId( lastNode.lastNodeId ); node.setLastNodeDate( lastNode.lastNodeDate ); } else { node.setLastNodeId( node.getId() ); node.setLastNodeDate( node.whenCreated ); } rs.close(); } node.setNodeCount(); node.record.update(); } pstmtGetLastNode.close(); } finally { con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } // } parent.uncacheAncestors(); } private void setLastNodeId(long lastNodeId) { if( this.lastNodeId == lastNodeId ) return; this.lastNodeId = lastNodeId; record.fields().put("last_node_id", lastNodeId); } private void setLastNodeDate(Date lastNodeDate) { if( ObjectUtils.equals(this.lastNodeDate,lastNodeDate) ) return; this.lastNodeDate = lastNodeDate; record.fields().put("last_node_date", lastNodeDate); } private void setNodeCount() { int nodeCount = getCount( "select 1 + coalesce(sum(node_count),0) as n from node where parent_id = ?" ); if( this.nodeCount == nodeCount ) return; this.nodeCount = nodeCount; record.fields().put("node_count", nodeCount); } private void setChildCount() { int childCount = getCount( "select count(*) as n from node where parent_id = ?" ); if( this.childCount == childCount ) return; this.childCount = childCount; record.fields().put("child_count", childCount); } public NodeIterator<? extends Node> getChildren() { return getChildrenImpl(null); } public NodeIterator<? extends Node> getChildren(String cnd) { return getChildrenImpl(cnd); } private static final Map<NodeImpl,List<NodeImpl>> dummyChildMap = Collections.synchronizedMap(new WeakHashMap<NodeImpl,List<NodeImpl>>()); NodeIterator<NodeImpl> getChildrenImpl(String cnd) { if( !isInDb() ) { List<NodeImpl> children = dummyChildMap.get(this); if( children == null ) return NodeIterator.empty(); return NodeIterator.nodeIterator( children.iterator() ); } return new CursorNodeIterator( siteKey, "(select *" +" from node" +" where parent_id = ?" +" and pin is not null " +(cnd==null?"":"and " + cnd) +" order by pin)" +" union all" +" (select *" +" from node" +" where parent_id = ?" +" and pin is null " +(cnd==null?"":"and " + cnd) +" order by last_node_date desc, node_id desc)" , new DbParamSetter() { public void setParams(PreparedStatement stmt) throws SQLException { stmt.setLong( 1, getId() ); stmt.setLong( 2, getId() ); } } ); } private int getCount(String sql) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement(sql); stmt.setLong(1,getId()); ResultSet rs = stmt.executeQuery(); rs.next(); int n = rs.getInt("n"); rs.close(); stmt.close(); con.close(); return n; } catch(SQLException e) { logger.error("sql = " + sql); throw new RuntimeException(e); } } public int getChildCount() { return childCount; } public boolean isPinned() { return isPinned; } public void pin(Node[] children) { if( !db().isInTransaction() ) { db().beginTransaction(); try { pin(children); db().commitTransaction(); } finally { db().endTransaction(); } return; } try { Connection con = db().getConnection(); try { { PreparedStatement stmt = con.prepareStatement( "select node_id" +" from node" +" where parent_id = ?" +" and pin is not null" ); stmt.setLong(1,getId()); ResultSet rs = stmt.executeQuery(); while( rs.next() ) { table().uncache( new LongKey(rs.getLong("node_id")) ); } rs.close(); stmt.close(); } PreparedStatement stmt = con.prepareStatement( "update node set pin=null where parent_id=? and pin is not null" ); stmt.setLong(1,getId()); stmt.executeUpdate(); stmt.close(); stmt = con.prepareStatement( "update node set pin=? where node_id=?" ); for( int i=0; i<children.length; i++ ) { Node child = children[i]; if( !this.equals(child.getParent()) ) throw new RuntimeException(""+child+" not a child of "+this); stmt.setInt(1,i+1); stmt.setLong(2,child.getId()); stmt.executeUpdate(); DbUtils.uncache((NodeImpl)child); } stmt.close(); } finally { con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } fireChildChangeListeners(); } public long getExportedNodeId() { return exportedNodeId; } public void setExportedNodeId(long id) { if( id==0L ) throw new RuntimeException(); this.exportedNodeId = id; record.fields().put( "exported_node_id", Long.valueOf(id) ); record.update(); } public String getEmbeddingUrl() { return embeddingUrl; } public void setEmbeddingUrl(String url) { this.embeddingUrl = url; record.fields().put( "embedding_url", DbNull.fix(embeddingUrl)); record.update(); } @Override public boolean equals(Object obj) { return this==obj || obj instanceof NodeImpl && record.isInDb() && ((NodeImpl)obj).getId()==getId(); } @Override public int hashCode() { return (int)getId(); } @Override public String toString() { return "node-"+getId(); } // private final ListenerList<V> preInsertListeners = new ListenerList<V>(); static final ListenerList<NodeImpl> preUpdateListeners = new ListenerList<NodeImpl>(); // private final ListenerList<V> preDeleteListeners = new ListenerList<V>(); static final ListenerList<NodeImpl> postInsertListeners = new ListenerList<NodeImpl>(); static final ListenerList<NodeImpl> postUpdateListeners = new ListenerList<NodeImpl>(); static final ListenerList<NodeImpl> postDeleteListeners = new ListenerList<NodeImpl>(); private static Computable<SiteKey,DbTable<LongKey,NodeImpl>> tables = new SimpleCache<SiteKey,DbTable<LongKey,NodeImpl>>(new WeakHashMap<SiteKey,DbTable<LongKey,NodeImpl>>(), new Computable<SiteKey,DbTable<LongKey,NodeImpl>>() { public DbTable<LongKey,NodeImpl> get(SiteKey siteKey) { DbDatabase db = siteKey.getDb(); final long siteId = siteKey.getId(); DbTable<LongKey,NodeImpl> table = db.newTable("node",db.newIdentityLongKeySetter("node_id") , new DbObjectFactory<LongKey,NodeImpl>() { public NodeImpl makeDbObject(LongKey key,ResultSet rs,String tableName) throws SQLException { SiteKey siteKey = SiteKey.getInstance(siteId); return new NodeImpl(siteKey,key,rs); } } ); table.getPreUpdateListeners().add(preUpdateListeners); table.getPostInsertListeners().add(postInsertListeners); table.getPostUpdateListeners().add(postUpdateListeners); table.getPostDeleteListeners().add(postDeleteListeners); return table; } }); static DbTable<LongKey,NodeImpl> table(SiteKey siteKey) { return tables.get(siteKey); } static NodeImpl getNode(SiteKey siteKey,long id) { return table(siteKey).findByPrimaryKey(new LongKey(id)); } private NodeImpl getNode(long id) { return getNode(siteKey,id); } static Collection<NodeImpl> getNodes(SiteKey siteKey,Collection<Long> ids) { List<LongKey> list = new ArrayList<LongKey>(); for( long id : ids ) { list.add( new LongKey(id) ); } return table(siteKey).findByPrimaryKey(list).values(); } static NodeImpl getNode(SiteKey siteKey,ResultSet rs) throws SQLException { return table(siteKey).getDbObject(rs); } private NodeImpl getNode(ResultSet rs) throws SQLException { return getNode(siteKey,rs); } private static final Collator collator = Collator.getInstance(); private CollationKey collationKey = null; CollationKey getCollationKey() { if( collationKey==null ) collationKey = collator.getCollationKey(getSubject()); return collationKey; } public Collection<Subscription> getSubscriptions(int i, int n) { return getSubscriptions( "select * from subscription where node_id = ? limit " + n + " offset " + i ); } Collection<Subscription> getSubscriptions(String sql) { List<Subscription> list = new ArrayList<Subscription>(); try { Connection con = db().getConnection(); try { PreparedStatement stmt = con.prepareStatement(sql); stmt.setLong( 1, getId() ); ResultSet rs = stmt.executeQuery(); while( rs.next() ) { SubscriptionImpl subscription = SubscriptionImpl.getSubscription(siteKey,rs); if( subscription != null ) list.add( subscription ); } rs.close(); stmt.close(); } finally { con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } return list; } public int getSubscriptionCount() { db().beginTransaction(); try { return getCount( "select count(*) as n from subscription where node_id=?" ); } finally { db().endTransaction(); } } public Map<User,Subscription> getSubscribersToNotify() { return SubscriptionImpl.getSubscribersToNotify(this); } public NodeIterator<? extends Node> getAncestors() { return getAncestorImpls(); } NodeIterator<NodeImpl> getAncestorImpls() { return new NodeIterator<NodeImpl>() { private NodeImpl next = NodeImpl.this; public boolean hasNext() { return next != null; } public NodeImpl next() { if( !hasNext() ) throw new NoSuchElementException(); try { return next; } finally { next = next.getParentImpl(); } } public void close() { next = null; } }; } synchronized UserImpl getOwnerImpl() { if( ownerId!=0L && DbUtils.isStale(owner) ) { owner = UserImpl.getUser(siteKey,ownerId); } return owner; } public final Person getOwner() { UserImpl user = getOwnerImpl(); if( user != null ) return user; if( cookie != null ) return new Anonymous(getSiteImpl(), cookie, anonymousName); throw new RuntimeException(); } public void setOwner(User owner) { this.owner = null; this.ownerId = owner.getId(); record.fields().put("owner_id", ownerId); // Remove any anonymous information record.fields().put("cookie", DbNull.STRING); record.fields().put("anonymous_name", DbNull.STRING); } NodeImpl getAppImpl() { for( NodeImpl node=this; node!=null; node=node.getParentImpl() ) { if( node.getKind() == Kind.APP ) return node; } return null; } public final Node getApp() { return getAppImpl(); } static ListenerList<NodeImpl> gotParentListeners = new ListenerList<NodeImpl>(); synchronized NodeImpl getParentImpl() { if( parentId == 0L && parent==null ) return null; if( DbUtils.isStale(parent) ) { parent = getNode(parentId); if( parent == null ) logger.error(""+this+" parent="+parentId+" doesn't exist"); else if( parent.siteKey != siteKey ) logger.error(""+this+" parent="+parentId+" siteId="+siteKey+" parent.siteId="+parent.siteKey); gotParentListeners.event(this); } return parent; } public final Node getParent(){ return getParentImpl(); } SiteImpl getSiteImpl() { return siteKey.site(); } public Site getSite() { return getSiteImpl(); } public boolean isRoot() { return parentId == 0L; } NodeImpl getTopicImpl() { if( getKind() != Kind.POST ) return null; NodeImpl node = this; while(true) { NodeImpl parent = node.getParentImpl(); if( parent==null || parent.getKind() != Kind.POST ) break; node = parent; } return node; } public final Node getTopic() { return getTopicImpl(); } static void preloadMessages(List<Node> nodes) { if( nodes.size()==0 ) return; try { Map<Long,NodeImpl> map = new HashMap<Long,NodeImpl>(); StringBuilder query = new StringBuilder(); query.append( "select node_id, message from node_msg where node_id in (" ); for (Node n : nodes) { NodeImpl node = (NodeImpl) n; if (node.message.hasLoaded()) continue; if (!map.isEmpty()) query.append(','); query.append(node.getId()); map.put(node.getId(), node); } if( map.isEmpty() ) return; query.append( ')' ); Connection con = nodes.get(0).getSite().getDb().getConnection(); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query.toString()); while( rs.next() ) { long nodeId = rs.getLong("node_id"); String message = rs.getString("message"); map.get(nodeId).cacheMessage(message); } rs.close(); stmt.close(); con.close(); } catch(SQLException e) { throw new RuntimeException(e); } } int getIntId() { return getIntId(getId()); } static int getIntId(long id) { if( id > Integer.MAX_VALUE ) throw new RuntimeException(); return (int)id; } public Date getWhenUpdated() { return whenUpdated; } void setWhenUpdated(Date whenUpdated) { this.whenUpdated = whenUpdated; record.fields().put("when_updated", whenUpdated); } static ListenerList<NodeImpl> childChangeListeners = new ListenerList<NodeImpl>(); private void fireChildChangeListeners() { childChangeListeners.event(this); } static void addPostInsertListener(final Listener<? super NodeImpl> listener) { postInsertListeners.add(listener); } static void addPostUpdateListener(final Listener<? super NodeImpl> listener) { postUpdateListeners.add(listener); } static void addPostDeleteListener(final Listener<? super NodeImpl> listener) { postDeleteListeners.add(listener); } private static ListenerList<NodeImpl> changeListeners = new ListenerList<NodeImpl>(); private void fireChangeListeners() { changeListeners.event(this); } static void addChangeListener(final Listener<? super NodeImpl> listener) { addPostUpdateListener(listener); addPostDeleteListener(listener); changeListeners.add(listener); } private boolean hasNeighborTopic(boolean next) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "SELECT 1 " + "FROM node " + "WHERE parent_id = ? " + "AND (is_app = 'f' or is_app is null) " + "AND last_node_date " + (next?'<':'>') + " ? " + "LIMIT 1" ); stmt.setLong( 1, getParentId() ); stmt.setTimestamp( 2, new java.sql.Timestamp(getLastNodeDate().getTime())); ResultSet rs = stmt.executeQuery(); try { return rs.next(); } finally { rs.close(); stmt.close(); con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } private Node getNeighborTopic(boolean next) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "SELECT * "+ "FROM node "+ "WHERE parent_id = ? "+ "AND (is_app = 'f' or is_app is null) " + "AND last_node_date " + (next?'<':'>') + " ? " + "ORDER BY last_node_date " + (next?"DESC ":"ASC ") + "LIMIT 1" ); stmt.setLong( 1, getParentId() ); stmt.setTimestamp( 2, new java.sql.Timestamp(getLastNodeDate().getTime())); ResultSet rs = stmt.executeQuery(); try { return rs.next()? getNode(rs) : null; } finally { rs.close(); stmt.close(); con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } public boolean hasPreviousTopic() { return hasNeighborTopic(false); } public Node getPreviousTopic() { return getNeighborTopic(false); } public boolean hasNextTopic() { return hasNeighborTopic(true); } public Node getNextTopic() { return getNeighborTopic(true); } public int getDescendantPostCount() { return getDescendantCount() - getDescendantAppCount(); } public int getDescendantAppCount() { int count = 1; for( Node f : getChildApps() ) { count += f.getDescendantAppCount(); } return count; } private Collection<NodeImpl> getDescendantApps(Filter<Node> filter) { List<NodeImpl> list = new ArrayList<NodeImpl>(); getDescendantApps(filter,list); return list; } private void getDescendantApps(Filter<Node> filter,Collection<NodeImpl> list) { list.add(this); for( Node f : getChildApps() ) { NodeImpl node = (NodeImpl)f; if( filter.ok(node) ) node.getDescendantApps(filter,list); } } private boolean hasChildKind(Node.Kind kind) { String cnd = kind == Node.Kind.APP? "is_app" : "(is_app = 'f' or is_app is null)"; try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "select exists" +" (select * from node" +" where parent_id = ?" +" and " + cnd +") as b" ); stmt.setLong( 1, getId() ); ResultSet rs = stmt.executeQuery(); rs.next(); try { return rs.getBoolean("b"); } finally { rs.close(); stmt.close(); con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } private boolean hasPinnedKind(Node.Kind kind) { String cnd = kind == Node.Kind.APP? "is_app" : "(is_app = 'f' or is_app is null)"; try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "select exists" +" (select 1 from node" +" where parent_id = ?" +" and pin is not null" +" and " + cnd +") as b" ); stmt.setLong( 1, getId() ); ResultSet rs = stmt.executeQuery(); rs.next(); try { return rs.getBoolean("b"); } finally { rs.close(); stmt.close(); con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } public boolean hasChildApps() { return hasChildKind(Node.Kind.APP); } public boolean hasChildTopics() { return hasChildKind(Node.Kind.POST); } public boolean hasPinnedApps() { return hasPinnedKind(Node.Kind.APP); } public boolean hasPinnedTopics() { return hasPinnedKind(Node.Kind.POST); } public NodeIterator<? extends Node> getChildApps() { return getChildApps(null); } public NodeIterator<? extends Node> getChildApps(String cnd) { cnd = cnd==null ? "is_app" : "is_app and (" + cnd + ")"; return getChildrenImpl(cnd); } public String getType() { return type; } public void setType(String type) { this.type = type; record.fields().put( "type", Type.COMMENT.equals(type) ? DbNull.STRING : type ); } public boolean isInDb() { return record.isInDb(); } public int getDescendantCount() { return nodeCount; } public Message.SourceType getMessageSourceType() { return Message.SourceType.NODE; } static void nop() {} private DbParamSetter simpleParamSetter() { return new DbParamSetter() { public void setParams(PreparedStatement stmt) throws SQLException { stmt.setLong( 1, getId() ); } }; } private List<NodeImpl> childAppList() { return new CursorNodeIterator( siteKey, "select * from node where parent_id = ? and is_app" , simpleParamSetter() ).asList(); } private int getTopicCount(Filter<Node> filter) { int n = childCount; for( NodeImpl childApp : childAppList() ) { n--; if( filter.ok(childApp) ) n += childApp.getTopicCount(filter); } return n; } private int getTopicCount2(String cnd,Filter<Node> filter) { int n = getCount( "select count(*) as n from node where parent_id=? and is_app is null and (" + cnd + ")" ); for( NodeImpl childApp : childAppList() ) { if( filter.ok(childApp) ) n += childApp.getTopicCount2(cnd,filter); } return n; } public int getTopicCount(String cnd,Filter<Node> filter) { if( !db().isInTransaction() ) { db().beginTransaction(); try { return getTopicCount(cnd,filter); } finally { db().endTransaction(); } } return getTopicCount0(cnd, filter); } private Map<String, Integer> topicCountCache; private synchronized int getTopicCount0(String cnd, Filter<Node> filter) { if (topicCountCache == null) topicCountCache = new HashMap<String, Integer>(); String cacheKey = (cnd == null? "" : cnd + '|') + filter.getClass().getName(); Integer countValue = topicCountCache.get(cacheKey); if (countValue == null) { countValue = cnd==null ? getTopicCount(filter) : getTopicCount2(cnd,filter); topicCountCache.put(cacheKey, countValue); } return countValue; } private static class MyIter { NodeImpl node; Comparable cmp; final NodeIterator<NodeImpl> iter; MyIter(NodeImpl node,Comparable cmp,NodeIterator<NodeImpl> iter) { this.node = node; this.cmp = cmp; this.iter = iter; } } private class OrderedNodeIterator extends NodeIterator<NodeImpl> { private final List<MyIter> mis = new ArrayList<MyIter>(); NodeImpl next = null; private final boolean isTopics; private final boolean skipPinned; private final Order order; private final String sql; private final Filter<Node> filter; OrderedNodeIterator(boolean isTopics,boolean skipPinned,String cnd,Filter<Node> filter,Order order) { if( !isTopics && skipPinned ) throw new UnsupportedOperationException(); this.isTopics = isTopics; this.skipPinned = skipPinned; this.order = order; this.filter = filter; StringBuilder buf = new StringBuilder(); buf.append( "select * from node where parent_id = ?" ); if( cnd != null ) buf.append( " and (is_app or (" + cnd + ")) " ); buf.append( " order by " ).append( order.sqlOrder() ); this.sql = buf.toString(); NodeIterator<NodeImpl> empty = NodeIterator.empty(); mis.add( new MyIter(NodeImpl.this,order.getComparable(NodeImpl.this),empty) ); } void add(MyIter mi) { int len = mis.size(); for( int i=0; i<len; i++ ) { @SuppressWarnings("unchecked") boolean precedes = mi.cmp.compareTo(mis.get(i).cmp) < 0; if( precedes ) { mis.add(i,mi); return; } } mis.add(mi); } public boolean hasNext() { if( next != null ) return true; while( !mis.isEmpty() ) { MyIter mi = mis.remove(0); if( mi.iter == null ) { next = mi.node; return true; } final NodeImpl node = mi.node; if( mi.iter.hasNext() ) { mi.node = mi.iter.next(); mi.cmp = order.getComparable(mi.node); add(mi); } if( node != NodeImpl.this && filter != null && !filter.ok(node) ) continue; boolean isPost = node.getKind() == Kind.POST; if( isPost && isTopics ) { if( skipPinned && node.isPinned() && node.parentId==getId() ) continue; next = node; return true; } NodeIterator<NodeImpl> children = new CursorNodeIterator( siteKey, sql, new DbParamSetter() { public void setParams(PreparedStatement stmt) throws SQLException { stmt.setLong( 1, node.getId() ); } } ); if( children.hasNext() ) { NodeImpl firstChild = children.next(); add( new MyIter(firstChild,order.getComparable(firstChild),children) ); } if( isPost ) { Comparable postCmp = order.getPostComparable(node); if( postCmp != order.getComparable(node) ) { add( new MyIter(node,postCmp,null) ); } else { next = node; return true; } } } return false; } public NodeImpl next() throws NoSuchElementException { if( !hasNext() ) throw new NoSuchElementException(); try { return next; } finally { next = null; } } public void close() { while( !mis.isEmpty() ) { NodeIterator<NodeImpl> iter = mis.remove(0).iter; if( iter != null ) iter.close(); } } } private static class ConcatNodeIterator extends NodeIterator<NodeImpl> { private final NodeIterator<NodeImpl>[] a; private int i = 0; ConcatNodeIterator(NodeIterator<NodeImpl>... a) { this.a = a; } public boolean hasNext() { while(true) { if( i==a.length ) return false; if( a[i].hasNext() ) return true; a[i++].close(); } } public NodeImpl next() throws NoSuchElementException { return a[i].next(); } public void close() { while( i < a.length ) { a[i++].close(); } } } private String fixCnd(String cnd) { if( cnd==null ) return ""; cnd = cnd.trim(); return cnd.length()==0 ? "" : " and (" + cnd + ") "; } private NodeIterator<NodeImpl> getPinned(String cnd) { cnd = fixCnd(cnd); return new CursorNodeIterator( siteKey, "select * from node where pin is not null and parent_id = ? and is_app is null" + cnd +" order by pin" , simpleParamSetter() ); } public NodeIterator<? extends Node> getTopicsByPinnedAndLastNodeDate(String cnd,Filter<Node> filter) { @SuppressWarnings("unchecked") NodeIterator<NodeImpl> i = new ConcatNodeIterator( getPinned(cnd), new OrderedNodeIterator(true,true,cnd,filter,Order.BY_LAST_NODE_DATE_DESC) ); return i; } public NodeIterator<? extends Node> getPostsByDate(Filter<Node> filter) { return new OrderedNodeIterator(false,false,null,filter,Order.BY_DATE_DESC); } public NodeIterator<? extends Node> getPostsByDateAscending(Filter<Node> filter) { return new OrderedNodeIterator(false,false,null,filter,Order.BY_WHEN_CREATED); } public NodeIterator<? extends Node> getTopicsByLastNodeDate(String cnd,Filter<Node> filter) { return new OrderedNodeIterator(true,false,cnd,filter,Order.BY_LAST_NODE_DATE_DESC); } public NodeIterator<? extends Node> getTopicsBySubject(String cnd,Filter<Node> filter) { return new OrderedNodeIterator(true,false,cnd,filter,Order.BY_SUBJECT); } public NodeIterator<? extends Node> getTopicsByPinnedAndRootNodeDate(String cnd,Filter<Node> filter) { String fixedCnd = fixCnd(cnd); StringBuilder sql = new StringBuilder(); Collection<NodeImpl> apps = getDescendantApps(filter); apps.remove(this); sql.append( "select * from node where parent_id=" ).append( getId() ) .append( " and is_app is null and pin is null" ).append( fixedCnd ); if( !apps.isEmpty() ) { sql.append( " or parent_id in (" ); Iterator<NodeImpl> iter = apps.iterator(); sql.append( iter.next().getId() ); while( iter.hasNext() ) { sql.append( "," ).append( iter.next().getId() ); } sql.append( ") and is_app is null" ).append( fixedCnd ); } sql.append(" order by when_created desc"); @SuppressWarnings("unchecked") NodeIterator<NodeImpl> i = new ConcatNodeIterator( getPinned(cnd), new CursorNodeIterator( siteKey, sql.toString(), DbParamSetter.NONE ) ); return i; } public NodeIterator<? extends Node> getTopicsByPopularity(String cnd,Filter<Node> filter) { String fixedCnd = fixCnd(cnd); StringBuilder sql = new StringBuilder(); Collection<NodeImpl> apps = getDescendantApps(filter); sql.append( "SELECT n.* FROM node n, view_count vc ") .append("WHERE n.node_id = vc.node_id ") .append("AND is_app is null ") .append("AND pin is null " ) .append(fixedCnd) .append("AND parent_id in ("); Iterator<NodeImpl> iter = apps.iterator(); sql.append( iter.next().getId() ); while( iter.hasNext() ) { sql.append( "," ).append( iter.next().getId() ); } sql.append(") ") .append("ORDER BY views desc"); @SuppressWarnings("unchecked") NodeIterator<NodeImpl> i = new ConcatNodeIterator( getPinned(cnd), new CursorNodeIterator( siteKey, sql.toString(), DbParamSetter.NONE ) ); return i; } public NodeIterator<? extends Node> getDescendants() { return getDescendantImpls(); } NodeIterator<NodeImpl> getDescendantImpls() { return getDescendantImpls(null); } public NodeIterator<? extends Node> getDescendantApps() { return getDescendantImpls("is_app"); } private NodeIterator<NodeImpl> getDescendantImpls(String cnd) { List<NodeImpl> list = new ArrayList<NodeImpl>(); list.add(this); int i = 0; while( i < list.size() ) { NodeImpl node = list.get(i++); for( NodeImpl child : node.getChildrenImpl(cnd) ) { list.add(child); } } return NodeIterator.nodeIterator(list); } public long getSourceId() { return getId(); } public void populateLastNodeFields() { if( !db().isInTransaction() ) { db().beginTransaction(); try { DbUtils.getGoodCopy(this).populateLastNodeFields(); db().commitTransaction(); } finally { db().endTransaction(); } return; } // synchronized(siteKey.lastNodeLock) { populateLastNodeFields2(); // } } private void populateLastNodeFields2() { NodeImpl lastChild = null; NodeIterator<NodeImpl> nodes = new CursorNodeIterator( siteKey, "select * from node where parent_id = ?" , simpleParamSetter() ); for( NodeImpl child : nodes ) { child.populateLastNodeFields2(); if( lastChild==null || lastChild.lastNodeDate.before(child.lastNodeDate) ) lastChild = child; } if( lastChild==null ) { setLastNodeId( getId() ); setLastNodeDate( whenCreated ); } else { setLastNodeId( lastChild.lastNodeId ); setLastNodeDate( lastChild.lastNodeDate ); } setNodeCount(); setChildCount(); if( !record.fields().isEmpty() ) record.update(); } // export/import NodeImpl(SiteImpl site,NodeData data) throws ModelException { this( site.siteKey, Kind.valueOf(data.kind), data.ownerAnonymousId == null ? site.getOrCreateUser(data.ownerEmail,data.ownerName) : new Anonymous(site, data.ownerAnonymousId, data.ownerName), data.subject, data.message, Message.Format.getMessageFormat(data.msgFmt) ); if( data.parentId != null ) { setParent( getNode(data.parentId) ); } setWhenCreated( data.whenCreated ); if( data.whenUpdated != null ) setWhenUpdated( data.whenUpdated ); setType( data.type ); if( data.pin != null ) record.fields().put( "pin", data.pin ); insert(false); for( URL url : data.fileUrls ) { try { FileUpload.uploadFile( new FileUpload.UrlFileItem(url), this ); } catch(ModelException e) {} } // Only for backup recovery if (data.files != null && data.files.size() > 0) { Set<Map.Entry<String, byte[]>> entries = data.files.entrySet(); for (Map.Entry<String, byte[]> entry : entries) { FileUpload.saveFile(entry.getValue(), entry.getKey(), this); } } for( ExtensionFactory<Node,?> factory : extensionFactories ) { Serializable obj = data.extensionData.get(factory.getName()); if( obj != null ) factory.saveExportData(this,obj); } } public NodeData getData() { NodeData data = new NodeData(); data.exportId = getId(); data.kind = kind.toString(); Person owner = getOwner(); if (owner instanceof Anonymous) data.ownerAnonymousId = ((Anonymous)owner).getCookie(); else data.ownerEmail = ((User) owner).getEmail(); data.ownerName = owner.getName(); data.subject = subject; data.message = getMessage().getRaw(); data.msgFmt = getMessage().getFormat().getCode(); data.whenCreated = whenCreated; data.whenUpdated = whenUpdated; data.type = type; List<URL> urls = new ArrayList<URL>(); Message.Format fmt = message.getFormat(); if( !(fmt instanceof MailMessageFormat) ) { Html list = message.parse(); Map<String,String> files = FileUpload.getFileInfo(list,this); for( String url : files.values() ) { try { urls.add( new URL(url) ); } catch(MalformedURLException e) { throw new RuntimeException(e); } } } data.fileUrls = urls.toArray(new URL[0]); for( ExtensionFactory<Node,?> factory : extensionFactories ) { Serializable obj = factory.getExportData(this); if( obj != null ) data.extensionData.put(factory.getName(),obj); } return data; } private static final String EXPORT_TASK = "export"; private static class LazyExports { static { try { for( SiteKey siteKey : SiteKey.getSiteKeys(EXPORT_TASK) ) { Connection con = siteKey.getDb().getConnection(); try { Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery( "select * from node" +" where export_permalink is not null" ); while( rs.next() ) { // Put the task back because the export hasn't finished yet and // it should keep trying until everything has moved. Since the node // is deleted at the end of the export, the SQL above will not find any // node to be migrated when the export finishes. So this task loop will // finally come to an end. siteKey.site().addTask(EXPORT_TASK); NodeImpl node = getNode(siteKey,rs); // Logs node/site ids because we may need this in the shell. logger.error("Export restarted for node=" + node.getId() + " [site=" + node.getSite().getId() + ']'); // Continue exporting... node.doExport(); } rs.close(); stmt.close(); } finally { con.close(); } } } catch(SQLException e) { throw new RuntimeException(e); } } static void start() {} } static { Executors.schedule( new Runnable(){public void run(){ LazyExports.start(); }}, 200, TimeUnit.SECONDS ); } public void export(String permalink,String email) { LazyExports.start(); setExport(permalink,email); doExport(); } private void setExport(String permalink,String email) { if( exportPermalink != null ) throw new RuntimeException("already set"); this.exportPermalink = permalink; getDbRecord().fields().put("export_permalink",exportPermalink); this.exportEmail = email; getDbRecord().fields().put("export_email",exportEmail); getDbRecord().update(); getSiteImpl().addTask(EXPORT_TASK); } /* to be called from luan shell */ public void clearExport() { if( exportPermalink == null ) throw new RuntimeException("already cleared"); this.exportPermalink = null; getDbRecord().fields().put("export_permalink",DbNull.STRING); this.exportEmail = null; getDbRecord().fields().put("export_email",DbNull.STRING); getDbRecord().update(); } /* to be called from luan shell */ public static void clearExportedNodeIds(Node node) { if (node.getExportedNodeId() > 0) { node.getDbRecord().fields().put( "exported_node_id", DbNull.INTEGER ); node.getDbRecord().update(); for (Node child : node.getChildren()) { clearExportedNodeIds(child); } } } private void doExport() { final Export export; try { export = new Export(this, exportPermalink, exportEmail); } catch(IOException e) { throw new RuntimeException(e); } Executors.executeNow(new Runnable(){public void run(){ export.run(); //clearExport(); }}); } private Map<ExtensionFactory<Node,?>,Object> extensionMap; public synchronized Map<ExtensionFactory<Node, ?>, Object> getExtensionMap() { if (extensionMap == null) extensionMap = new HashMap<ExtensionFactory<Node, ?>, Object>(); return extensionMap; } public <T> T getExtension(ExtensionFactory<Node,T> factory) { synchronized(getExtensionMap()) { Object obj = extensionMap.get(factory); if( obj == null ) { obj = factory.construct(this); if( obj != null ) extensionMap.put(factory,obj); } return factory.extensionClass().cast(obj); } } private static Collection<ExtensionFactory<Node,?>> extensionFactories = new CopyOnWriteArrayList<ExtensionFactory<Node,?>>(); static <T> void addExtensionFactory(ExtensionFactory<Node,T> factory) { extensionFactories.add(factory); Db.clearCache(); } private final Memoizer<String,String> propertyCache = new Memoizer<String,String>(new Computable<String,String>() { public String get(String key) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "select value from node_property where node_id = ? and key = ?" ); stmt.setLong( 1, getId() ); stmt.setString( 2, key ); ResultSet rs = stmt.executeQuery(); try { return rs.next() ? rs.getString("value") : null; } finally { rs.close(); stmt.close(); con.close(); } } catch(SQLException e) { throw new RuntimeException(e); } } }); public String getProperty(String key) { return propertyCache.get(key); } public void setProperty(String key,String value) { try { Connection con = db().getConnection(); PreparedStatement stmt = con.prepareStatement( "delete from node_property where node_id = ? and key = ?" ); stmt.setLong( 1, getId() ); stmt.setString( 2, key ); stmt.executeUpdate(); stmt.close(); if( value != null ) { stmt = con.prepareStatement( "insert into node_property (node_id,key,value) values (?,?,?)" ); stmt.setLong( 1, getId() ); stmt.setString( 2, key ); stmt.setString( 3, value ); stmt.executeUpdate(); stmt.close(); } con.close(); } catch(SQLException e) { throw new RuntimeException(e); } finally { propertyCache.remove(key); } } final Memoizer<String,Boolean> tagCache = new Memoizer<String,Boolean>(new Computable<String,Boolean>() { public Boolean get(String sqlCondition) { return TagImpl.countTags(siteKey,sqlCondition) > 0; } }); }