Mercurial Hosting > nabble
diff src/nabble/model/NodeImpl.java @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children | 72765b66e2c3 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/nabble/model/NodeImpl.java Thu Mar 21 19:15:52 2019 -0600 @@ -0,0 +1,2347 @@ +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, Node.MailFromList { + 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 String messageID; + private Date whenUpdated; + private Boolean isGuessedParent = false; + private Date whenSent; + 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"); + messageID = rs.getString("message_id"); + whenUpdated = rs.getTimestamp("when_updated"); + isGuessedParent = rs.getBoolean("guessed_parent"); + if (rs.wasNull()) isGuessedParent = null; + whenSent = rs.getTimestamp("when_sent"); + 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() || isMailToList(); + 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; + } + MailingListImpl mailingList = getMailingListImpl(); + if (mailingList != null) { + mailingList.unsubscribe(); + } + if( getChildCount() == 0 ) { + deleteRecursively(); + } else { + setMessage(DELETED_MESSAGE.getRaw(),DELETED_MESSAGE.getFormat()); + clearPending(); + 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(); + } + if( isMailToList() && isDoneByPoster ) + throw new RuntimeException("no more pending"); + 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); + Listener<MailingListImpl> mlListener = new Listener<MailingListImpl>() { + public void event(MailingListImpl ml) { + listener.event(ml.getForumImpl()); + } + }; + MailingListImpl.postInsertListeners.add(mlListener); + MailingListImpl.postUpdateListeners.add(mlListener); + MailingListImpl.postDeleteListeners.add(mlListener); + } + + 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(); + } + + + + // mailing list related + + static final String messageIDEnding; + static { + try { + messageIDEnding = ".post@" + Init.get("mailDomain",InetAddress.getLocalHost().getHostName()); + } catch(UnknownHostException e) { + logger.error("",e); + System.exit(-1); + throw new RuntimeException(); // for compiler + } + } + + public String getMessageID() { + return messageID; + } + + public MailFromList getParentMailFromList() { + Node node = getParent(); + return (node == null) ? null : node.getMailFromList(); + } + + public String getOrGenerateMessageID() { + if (messageID == null) { + String id = "" + System.currentTimeMillis() + "-" + getId() + messageIDEnding; + setMessageID(id); + if( isInDb() ) + this.record.update(); + } + return messageID; + } + + void setMessageID(String messageID) { + this.messageID = messageID; + record.fields().put("message_id",messageID); + } + + void setGuessedParent(NodeImpl newParent) + throws ModelException + { + setGuessedParent(true); + changeParentImpl(newParent); + } + + boolean isGuessedParent() { + return (isGuessedParent != null) && isGuessedParent; + } + + boolean isNotGuessedParent() { + return (isGuessedParent != null) && !isGuessedParent; + } + + public boolean hasGuessedParent() { + return isGuessedParent(); + } + + Boolean rawGuessedParent() { + return isGuessedParent; + } + + /** + * Set 'guessed_parent' flag of the post. + * + * @param guessed flag value + */ + void setGuessedParent(Boolean guessed) { + this.isGuessedParent = guessed; + record.fields().put("guessed_parent", DbNull.fix(this.isGuessedParent) ); + } + + void setGuessedParent(String parentID) { + setGuessedParent(Boolean.TRUE); + setRawParentMessageId(parentID); + } + + // only returns MailingList if not isUIHidden + public MailingList getAssociatedMailingList() { + return getAssociatedMailingListImpl(); + } + + MailingListImpl getAssociatedMailingListImpl() { + for( NodeImpl node=this; node!=null; node=node.getParentImpl() ) { + if( node.getKind()==Kind.APP ) { + MailingListImpl ml = node.getMailingListImpl(); + if( ml != null ) + return ml; + } + } + return null; + } + + boolean isFromMailingList() { + return message.getFormat() == Message.Format.MAILING_LIST; + } + + void clearPending() { + whenSent = null; + record.fields().put("when_sent", DbNull.TIMESTAMP); + if( !db().isInTransaction() ) { + record.update(); + DbUtils.uncache(getOwnerImpl()); + } + } + + private boolean isMailToList() { + return getKind() == Kind.POST && getAssociatedMailingListImpl() != null && !isFromMailingList(); + } + + public MailToList getMailToList() { + if( !isMailToList() ) + return null; + return new MailToList() { + + public Node getNode() { + return NodeImpl.this; + } + + public boolean isPending() { + return whenSent != null; + } + + public void clearPending() { + NodeImpl.this.clearPending(); + } + + public Date getWhenSent() { + return whenSent; + } + + public String getOrGenerateMessageID() { + return NodeImpl.this.getOrGenerateMessageID(); + } + + }; + } + + + public MailFromList getMailFromList() { + // MailFromList is used to work with all messages from mailing lists, both local and external + return (getKind() == Kind.POST && getAssociatedMailingListImpl() != null) ? this : null; + } + + public MailingList newMailingList(ListServer listServer,String listAddress,String url) throws ModelException { + if( getKind() != Kind.APP ) + throw new UnsupportedOperationException(); + if( !ModelHome.insideImportProcedure.get() ) + DailyNumber.forumsStarted.dec(); + setMailingList(new MailingListImpl(this, listServer, listAddress, url)); + DbUtils.uncache(this); + return mailingList; + } + + private void setRawParentMessageId(String parentMessageId) { + record.fields().put("parent_message_id", DbNull.fix(parentMessageId)); + } + + static NodeImpl[] getFromParentID(String parentID, MailingListImpl mailingList) { + try { + SiteKey siteKey = mailingList.siteKey; + Connection con = siteKey.getDb().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select *" + +" from node" + +" where lower(parent_message_id)=?" + ); + stmt.setString(1,parentID.toLowerCase()); + ResultSet rs = stmt.executeQuery(); + try { + List<NodeImpl> list = new ArrayList<NodeImpl>(); + while( rs.next() ) { + NodeImpl post = getNode(siteKey,rs); + if (mailingList.equals(post.getAssociatedMailingListImpl())) + list.add( post ); + } + return list.toArray(new NodeImpl[0]); + } finally { + rs.close(); + stmt.close(); + con.close(); + } + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + NodeImpl getNodeImplFromMessageID(String messageID) { + if( messageID == null ) + return null; + try { + Connection con = db().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select * from node" + +" where lower(message_id)=? and message_id is not null" + ); + stmt.setString(1,messageID.toLowerCase()); + ResultSet rs = stmt.executeQuery(); + try { + while (rs.next()) { + NodeImpl post = getNode(rs); + if( post.getAncestors().contains(this) ) + return post; + } + return null; + } finally { + rs.close(); + stmt.close(); + con.close(); + } + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + + + + + private MailingListImpl mailingList; + private boolean mailingListSet = false; + + private void setMailingList(MailingListImpl mailingList) { + this.mailingList = mailingList; + mailingListSet = true; + } + + MailingListImpl getMailingListImpl() { + if( getKind() != Kind.APP ) + return null; + if (!mailingListSet || (mailingList != null && DbUtils.isStale(mailingList))) { + setMailingList(MailingListImpl.getMailingListForForum(this)); + } + return mailingList; + } + + public MailingList getMailingList() { + return getMailingListImpl(); + } + + public void deleteMailingList() { + MailingList mailingList = getMailingList(); + if (mailingList != null) { + mailingList.delete(); + this.mailingList = null; + this.mailingListSet = false; + DbUtils.uncache(this); + } + } + + + + + // 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 ); + if( data.messageID != null ) + setMessageID( data.messageID ); + setGuessedParent( data.isGuessedParent ); + insert(false); + + if( data.mlAddress != null ) { + ListServer listServer = ListServer.getServer( data.mlServer ); + MailingList mailingList = newMailingList( listServer, data.mlAddress, data.mlUrl ); + mailingList.setPlainTextOnly( data.mlPlainTextOnly ); + mailingList.setIgnoreNoArchive( data.mlIgnoreNoArchive ); + mailingList.setListName( data.mlListName ); + mailingList.update(); + update(); + } + + 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; + data.messageID = messageID; + data.isGuessedParent = isGuessedParent; + + MailingListImpl mailingList = getMailingListImpl(); + if( mailingList != null ) { + data.mlAddress = mailingList.getListAddress(); + data.mlUrl = mailingList.getUrl(); + if( data.mlUrl==null ) + data.mlUrl = "http://localhost"; + data.mlPlainTextOnly = mailingList.plainTextOnly(); + data.mlIgnoreNoArchive = mailingList.ignoreNoArchive(); + data.mlServer = mailingList.getListServer().getType(); + data.mlListName = mailingList.getListName(); + } + + 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; + } + }); + +}