Mercurial Hosting > nabble
diff src/nabble/model/UserImpl.java @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children | cc5b7d515580 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/nabble/model/UserImpl.java Thu Mar 21 19:15:52 2019 -0600 @@ -0,0 +1,1355 @@ +/* + +Copyright (C) 2003 Franklin Schmidt <frank@gustos.com> + +*/ + +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.db.postgres.DbDatabaseImpl; +import fschmidt.util.java.Computable; +import fschmidt.util.java.Memoizer; +import fschmidt.util.java.ObjectUtils; +import fschmidt.util.java.SimpleCache; +import fschmidt.util.java.TimedCacheMap; +import fschmidt.util.mail.MailAddress; +import org.jasypt.digest.PooledStringDigester; +import org.jasypt.salt.FixedByteArraySaltGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + + +final class UserImpl extends PersonImpl implements User { + private static final Logger logger = LoggerFactory.getLogger(UserImpl.class); + + final SiteKey siteKey; + private final DbRecord<LongKey,UserImpl> record; + private String email; + private String passwordDigest; + private String name; + private Date registered; + private boolean noArchive; + private Message signature = null; + private int bounces; + + private UserImpl(SiteKey siteKey,LongKey key,ResultSet rs) + throws SQLException + { + this.siteKey = siteKey; + record = table(siteKey).newRecord(this,key); + email = rs.getString("email"); + passwordDigest = rs.getString("password_digest"); + name = rs.getString("name"); + registered = DbUtils.getDate(rs,"registered"); + noArchive = rs.getBoolean("no_archive"); + String signatureRaw = rs.getString("signature"); + String signatureFormatS = rs.getString("signature_format"); + if( signatureRaw!=null && signatureFormatS!=null ) { + Message.Format signatureFormat = Message.Format.getMessageFormat( signatureFormatS.charAt(0) ); + signature = new Message(signatureRaw,signatureFormat); + } + bounces = rs.getInt("bounces"); + for( ExtensionFactory<User,?> factory : extensionFactories ) { + Object obj = factory.construct(this,rs); + if( obj != null ) + getExtensionMap().put(factory,obj); + } + } + + private UserImpl(SiteImpl site) { + this.siteKey = site.siteKey; + record = table(siteKey).newRecord(this); + } + + + public DbRecord<LongKey,UserImpl> getDbRecord() { + return record; + } + + private DbTable<LongKey,UserImpl> table() { + return record.getDbTable(); + } + + private DbDatabase db() { + return table().getDbDatabase(); + } + + public long getId() { + return record.getPrimaryKey().value(); + } + + SiteImpl getSiteImpl() { + return siteKey.site(); + } + + public Site getSite() { + return getSiteImpl(); + } + + public boolean isDeactivated() { + return !isRegistered() && isNoArchive(); + } + + boolean isNoArchive() { + return noArchive; + } + + public void setNoArchive(boolean noArchive) { + if( this.noArchive == noArchive ) + return; + + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + UserImpl user = DbUtils.getGoodCopy(this); + user.setNoArchive(noArchive); + user.getDbRecord().update(); + db().commitTransaction(); + return; + } finally { + db().endTransaction(); + } + } + this.noArchive = noArchive; + record.fields().put("no_archive",DbNull.fix(noArchive)); + } + + public String getEmail() { + return email; + } + + static void validateEmail(String email) throws ModelException.EmailFormat { + if (!new MailAddress(email).isValid()) { + throw new ModelException.EmailFormat(email); + } + } + + public void setEmail(String email) throws ModelException { + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + UserImpl user = DbUtils.getGoodCopy(this); + user.setEmail(email); + user.getDbRecord().update(); + db().commitTransaction(); + return; + } finally { + db().endTransaction(); + } + } + validateEmail(email); + setEmail2(email); + } + + private void setEmail2(String email) throws ModelException { + if( email.equals(this.email) ) + return; + SiteImpl site = getSiteImpl(); + if( site.getUserImplFromEmail(email) != null ) + throw ModelException.newInstance("email_already_in_user","Email already in use"); + this.email = email; + record.fields().put("email",email); + } + + public String getPasswordDigest() { + return passwordDigest; + } + + public void setPassword(String password) throws ModelException { + if( "".equals(password) ) + throw ModelException.newInstance("empty_password","Password cannot be empty"); + setPasswordDigest(digestPassword(password)); + } + + public void setPasswordDigest(String passwordDigest) { + if( ObjectUtils.equals(passwordDigest,this.passwordDigest) ) + return; + this.passwordDigest = passwordDigest; + record.fields().put("password_digest",DbNull.fix(passwordDigest)); + synchronized (passcookieLock) { + this.passcookie = null; + } + } + + private volatile String passcookie = null; + private Object passcookieLock = new Object(); + + public String getPasscookie() { + String p = passcookie; + if (p==null) { + synchronized (passcookieLock) { + p = passcookie; + if (p==null) { + p = calcPasscookie(); + passcookie = p; + } + } + } + return p; + } + + public String getName() { + return name; + } + + public void setName(String name) throws ModelException { + setName(name,true); + } + + private void setName(String name,boolean replaceUnregistered) throws ModelException { + name = name.trim(); + if( name.equals("") ) + throw ModelException.newInstance("empty_user_name","User name cannot be empty."); + if( name.equals(this.name) ) + return; + if( !name.equalsIgnoreCase(this.name) ) { + UserImpl user = getSiteImpl().getUserImplFromName(name); + if( user != null ) { + if( !replaceUnregistered || user.isRegistered() ) + throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use"); + user.setNameLike2(name); + user.update(); + } + try { + Connection con = db().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select 'x' from registration where email!=? and name=?" + ); + stmt.setString(1,this.email); + stmt.setString(2,name); + try { + if( stmt.executeQuery().next() ) + throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use"); + } finally { + stmt.close(); + con.close(); + } + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + this.name = name; + record.fields().put("name",name); + } + + void setNameLike(String name,boolean replaceUnregistered) { + try { + setName(name,replaceUnregistered); + } catch(ModelException e) { + setNameLike2(name); + } + } + + private void setNameLike2(String name) { + for( int i=2; true; i++ ) { + try { + setName(name+"-"+i,false); + break; + } catch(ModelException e2) {} + } + } + + /* To be called from the shell */ + public void changeNameTo(String newName) { + db().beginTransaction(); + try { + UserImpl u = (UserImpl) getGoodCopy(); + u.setName(newName); + u.update(); + db().commitTransaction(); + DbUtils.uncache(u); + } catch (ModelException e) { + throw new RuntimeException(e); + } finally { + db().endTransaction(); + } + } + + public Date getRegistered() { + return registered; + } + + void setRegistered(Date registered) { + if( ObjectUtils.equals(registered,this.registered) ) + return; + this.registered = registered; + record.fields().put("registered",DbNull.fix(registered)); + } + + public boolean equals(Object obj) { + return obj instanceof User && ((User)obj).getId()==getId(); + } + + public int hashCode() { + return (int)getId(); + } + + public String toString() { + return record.isInDb() ? "user-"+getId() : "user-new"; + } + + public void register() throws ModelException { + register(new Date()); + } + + public void register(Date registerDate) throws ModelException { + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + UserImpl user; + if( record.isInDb() ) { + user = DbUtils.getGoodCopy(this); + user.setEmail(email); + user.setName(name); + user.setPasswordDigest(passwordDigest); + } else { + user = this; + } + user.register(); + db().commitTransaction(); + } finally { + db().endTransaction(); + } + return; + } + if( passwordDigest==null ) + throw new RuntimeException(); + setRegistered( registerDate ); + if( record.isInDb() ) { + if( isNoArchive() ) + setNoArchive(false); + record.update(); + } else { + insert(); + } + } + + public boolean isRegistered() { + return record.isInDb() && registered!=null; + } + + void insert() { + if( email==null || name==null ) + throw new RuntimeException(); + record.insert(); + } + + public void update() { + if( !db().isInTransaction() ) + throw new RuntimeException("this should be done in a transaction"); + Set<String> keys = record.fields().keySet(); + if( keys.contains("name") || keys.contains("signature") ) { + getSiteImpl().update(); // fire change listeners + } + getDbRecord().update(); + } + + public User getGoodCopy() { + return DbUtils.getGoodCopy(this); + } + + + + + public int getExternalHash(String url) { + return (url.toLowerCase() + getId()).hashCode(); + } + + + static final ListenerList<UserImpl> preUpdateListeners = new ListenerList<UserImpl>(); + static final ListenerList<UserImpl> postInsertListeners = new ListenerList<UserImpl>(); + + private static Computable<SiteKey,DbTable<LongKey,UserImpl>> tables = new SimpleCache<SiteKey,DbTable<LongKey,UserImpl>>(new WeakHashMap<SiteKey,DbTable<LongKey,UserImpl>>(), new Computable<SiteKey,DbTable<LongKey,UserImpl>>() { + public DbTable<LongKey,UserImpl> get(SiteKey siteKey) { + DbDatabase db = siteKey.getDb(); + final long siteId = siteKey.getId(); + DbTable<LongKey,UserImpl> table = db.newTable("user_",db.newIdentityLongKeySetter("user_id") + , new DbObjectFactory<LongKey,UserImpl>() { + public UserImpl makeDbObject(LongKey key,ResultSet rs,String tableName) + throws SQLException + { + SiteKey siteKey = SiteKey.getInstance(siteId); + return new UserImpl(siteKey,key,rs); + } + } + ); + table.getPreUpdateListeners().add(preUpdateListeners); + table.getPostInsertListeners().add(postInsertListeners); + return table; + } + }); + + private static DbTable<LongKey,UserImpl> table(SiteKey siteKey) { + return tables.get(siteKey); + } + + static UserImpl getUser(SiteKey siteKey,long id) { + UserImpl user = table(siteKey).findByPrimaryKey(new LongKey(id)); + if( user==null ) + logger.warn("user "+id+" not found"); + return user; + } + + static Collection<UserImpl> getUsers(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 UserImpl getUser(SiteKey siteKey,ResultSet rs) + throws SQLException + { + return table(siteKey).getDbObject(rs); + } + + static void getUsers(SiteKey siteKey,PreparedStatement stmt,List<? super UserImpl> list) + throws SQLException + { + ResultSet rs = stmt.executeQuery(); + while( rs.next() ) { + UserImpl user = getUser(siteKey,rs); + list.add(user); + } + rs.close(); + stmt.close(); + } + + static List<UserImpl> getUsers(SiteKey siteKey,PreparedStatement stmt) + throws SQLException + { + List<UserImpl> list = new ArrayList<UserImpl>(); + getUsers(siteKey,stmt,list); + return list; + } + + private static UserImpl getUser(SiteImpl site,String val,String sql) { + try { + SiteKey siteKey = site.siteKey; + Connection con = siteKey.getDb().getConnection(); + PreparedStatement stmt = con.prepareStatement(sql); + stmt.setString(1,val); + ResultSet rs = stmt.executeQuery(); + UserImpl user = rs.next() ? getUser(siteKey,rs) : null; + rs.close(); + stmt.close(); + con.close(); + return user; + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + static UserImpl getUserFromEmail(SiteImpl site,String email) { + return getUser( site, email.toLowerCase(), + "select * from user_" + +" where lower(email)=?" + ); + } + + static UserImpl getUserFromName(SiteImpl site,String name) { + return getUser( site, name.toLowerCase(), + "select * from user_" + +" where lower(name)=?" + ); + } + + static UserImpl createGhost(SiteImpl site,String email) { + UserImpl user = new UserImpl(site); + try { + user.setEmail2(email); + } catch(ModelException e) { + throw new RuntimeException(e); + } + return user; + } + + // Subscriptions ----------------------------------------------------------- + + public boolean isSubscribed(Node node) { + return SubscriptionImpl.isSubscribed(this, (NodeImpl) node); + } + + public Subscription getSubscription(Node node) { + return SubscriptionImpl.getSubscription( this, (NodeImpl)node ); + } + + public Subscription subscribe(Node node,Subscription.To to,Subscription.Type type) { + clearBounces(); + Subscription subscription = getSubscription(node); + if( subscription != null ) { + subscription.setTo(to); + subscription.setType(type); + return subscription; + } else { + return SubscriptionImpl.insert( this, (NodeImpl)node, to, type ); + } + } + + /*10 posts in 5 minutes */ + private static final RecentPostLimit postLimit1 = new RecentPostLimit(5 * 60 * 1000L, 10); + + /* 30 posts in 15 minutes */ + private static final RecentPostLimit postLimit2 = new RecentPostLimit(15 * 60 * 1000L, 30); + + void updateNewPostLimit() { + String key = siteKey.getId() + "-" + record.getPrimaryKey().value(); + postLimit1.insert(key); + postLimit2.insert(key); + } + + public boolean hasTooManyPosts() { + String key = siteKey.getId() + "-" + record.getPrimaryKey().value(); + return postLimit1.hasTooManyPosts(key) || postLimit2.hasTooManyPosts(key); + } + + private static class RecentPostLimit { + private final long timeLimit; + private final int postLimit; + private final Map<String,long[]> floodMap; + + private RecentPostLimit(long timeLimit, int postLimit) { + this.timeLimit = timeLimit; + this.postLimit = postLimit; + this.floodMap = new TimedCacheMap<String,long[]>(timeLimit); + } + + public void insert(String key) { + long[] recentPostTimes; + synchronized(floodMap) { + recentPostTimes = floodMap.get(key); + if( recentPostTimes==null ) { + recentPostTimes = new long[postLimit]; + floodMap.put(key,recentPostTimes); + } + } + long now = System.currentTimeMillis(); + long recently = now - timeLimit; + synchronized(recentPostTimes) { + for( int i=0; i<recentPostTimes.length; i++ ) { + if( recentPostTimes[i] < recently ) { + recentPostTimes[i] = now; + return; + } + } + } + } + + public boolean hasTooManyPosts(String key) { + long[] recentPostTimes; + synchronized(floodMap) { + recentPostTimes = floodMap.get(key); + if (recentPostTimes==null) + return false; + } + long now = System.currentTimeMillis(); + long recently = now - timeLimit; + synchronized(recentPostTimes) { + for (long time : recentPostTimes) { + if (time < recently) { + return false; + } + } + } + return true; + } + } + + + static UserImpl getOrCreateUnregisteredUser(SiteImpl site,String email,String name) + throws ModelException + { + DbDatabase db = site.getDb(); + if( !db.isInTransaction() ) { + db.beginTransaction(); + try { + UserImpl user = getOrCreateUnregisteredUser(site,email,name); + db.commitTransaction(); + return user; + } finally { + db.endTransaction(); + } + } + UserImpl user = site.getUserImplFromEmail(email); + if( user==null ) { + user = new UserImpl(site); + user.setEmail(email); + } else { + if( user.isRegistered() ) + throw ModelException.newInstance("email_already_registered","This email is already registered"); + validateEmail(user.getEmail()); + } + user.setName(name); + if( !user.record.isInDb() ) { + user.insert(); + } else if( !user.record.fields().isEmpty() ) { + user.update(); + } + return user; + } + + // registration + + static UserImpl createUser(SiteImpl site,String email,String password,String name) throws ModelException { + return createUser2(site, email, digestPassword(password), name); + } + + private static UserImpl createUser2(SiteImpl site,String email,String passwordDigest,String name) throws ModelException { + // transaction used because setName() may update user + DbDatabase db = site.getDb(); + if( !db.isInTransaction() ) { + db.beginTransaction(); + try { + UserImpl user = createUser2(site,email,passwordDigest,name); + db.commitTransaction(); + return user; + } finally { + db.endTransaction(); + } + } + if (!new MailAddress(email).isValid()) { + throw new ModelException.EmailFormat("invalid_email"); + } + UserImpl user = site.getUserImplFromEmail(email); + if( user==null ) { + user = new UserImpl(site); + user.setEmail(email); + } else { + if( user.isRegistered() ) + throw ModelException.newInstance("user_already_registered","User is already registered"); + validateEmail(user.getEmail()); + } + user.setPasswordDigest(passwordDigest); + user.setName(name); + return user; + } + + static UserImpl getOrCreateUser(SiteImpl site,String email) { + UserImpl user = site.getUserImplFromEmail(email); + if (user == null) { + String username = email.substring(0, email.indexOf('@')); + user = createGhost(site,email); + user.setNameLike(username, false); + user.insert(); + } + return user; + } + + private static final Object regLock = new Object(); + + String newRegistration(String nextUrl) { + if( nextUrl.equals("null") ) + throw new RuntimeException("nextUrl is \"null\""); + synchronized(regLock) { + String key; + try { + Connection con = db().getConnection(); + { + PreparedStatement stmt = con.prepareStatement( + "select 'x' from registration where key_=?" + ); + do { + key = Double.toString(Math.random()); + stmt.setString(1,key); + } while( stmt.executeQuery().next() ); + stmt.close(); + } + { + PreparedStatement stmt = con.prepareStatement( + "insert into registration" + +" ( key_, email, password_digest, name, next_url ) values (?,?,?,?,?)" + ); + int i = 0; + stmt.setString(++i,key); + stmt.setString(++i,getEmail()); + stmt.setString(++i,getPasswordDigest()); + stmt.setString(++i,getName()); + stmt.setString(++i,nextUrl); + stmt.executeUpdate(); + stmt.close(); + } + { + Statement stmt = con.createStatement(); + stmt.executeUpdate( + "delete from registration where date_<" + Db.arcana.dateSub("now()",7,"day") + ); + stmt.close(); + } + con.close(); + } catch(SQLException e) { + throw new RuntimeException(e); + } + return key; + } + } + + static User getRegistration(SiteImpl site,String registrationKey) + throws ModelException + { + try { + DbDatabase db = site.getDb(); + Connection con = db.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select * from registration where key_=?" + ); + stmt.setString(1,registrationKey); + ResultSet rs = stmt.executeQuery(); + try { + if( !rs.next() ) + return null; + String email = rs.getString("email"); + String passwordDigest = rs.getString("password_digest"); + String name = rs.getString("name"); + return createUser2(site,email,passwordDigest,name); + } finally { + rs.close(); + stmt.close(); + con.close(); + } + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + static String getNextUrl(SiteKey siteKey,String registrationKey) + { + try { + Connection con = siteKey.getDb().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select next_url from registration where key_=?" + ); + stmt.setString(1,registrationKey); + ResultSet rs = stmt.executeQuery(); + try { + if( !rs.next() ) + return null; + return rs.getString("next_url"); + } finally { + rs.close(); + stmt.close(); + con.close(); + } + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + // Called from beanshell + private static void deletePendingRegistration(Site site,String email, String username) { + try { + Connection con = site.getDb().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "delete from registration where email=? or name = ?" + ); + stmt.setString(1,email); + stmt.setString(2,username); + stmt.executeUpdate(); + stmt.close(); + con.close(); + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + + public void deactivate() { + db().beginTransaction(); + try { + UserImpl user = DbUtils.getGoodCopy(this); + user.setNoArchive(true); + user.setRegistered(null); + user.setPasswordDigest(null); + user.record.update(); + db().commitTransaction(); + logger.info("User removed his/her account: " + getEmail()); + } finally { + db().endTransaction(); + } + } + + private DbParamSetter simpleParamSetter() { + return new DbParamSetter() { + public void setParams(PreparedStatement stmt) throws SQLException { + stmt.setLong( 1, getId() ); + } + }; + } + + public NodeIterator<? extends Node> getPendingPosts() { + return new CursorNodeIterator( siteKey, + "select *" + +" from node" + +" where owner_id = ?" + +" and when_sent is not null" + +" order by when_sent" + , simpleParamSetter() + ); + } + + public Message getSignature() { + return signature; + } + + public User setSignature( String signatureRaw, Message.Format signatureFormat ) + throws ModelException + { + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + UserImpl user = DbUtils.getGoodCopy(this); + user.setSignature(signatureRaw,signatureFormat); + user.getDbRecord().update(); + db().commitTransaction(); + return DbUtils.getGoodCopy(user); + } finally { + db().endTransaction(); + } + } + if( signatureRaw==null || signatureRaw.trim().length()==0 ) { + if( signature != null ) { + signature = null; + record.fields().put("signature",DbNull.STRING); + record.fields().put("signature_format",DbNull.STRING); + } + } else { + Message newSignature = new Message(signatureRaw,signatureFormat); + if( !newSignature.equals(signature) ) { + signature = newSignature; + record.fields().put("signature",signatureRaw); + record.fields().put("signature_format",Character.toString(signatureFormat.getCode())); + } + } + return this; + } + + + public String getDecoratedAddress(Node node) { + return PostByEmail.getMailAddress(this, node); + } + + public void saveAvatar(BufferedImage smallImage,BufferedImage bigImage) throws ModelException { + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + DbUtils.getGoodCopy(this).saveAvatar(smallImage,bigImage); + db().commitTransaction(); + } finally { + db().endTransaction(); + } + return; + } + Message.AvatarSource as = new Message.AvatarSource(this); + FileUpload.saveImage(smallImage,ModelHome.AVATAR_SMALL,as); + FileUpload.saveImage(bigImage,ModelHome.AVATAR_BIG,as); + getSiteImpl().update(); // fire change listeners + DbUtils.uncache(this); + } + + public void deleteAvatar() { + if( !db().isInTransaction() ) { + db().beginTransaction(); + try { + DbUtils.getGoodCopy(this).deleteAvatar(); + db().commitTransaction(); + } finally { + db().endTransaction(); + } + return; + } + final Message.AvatarSource as = new Message.AvatarSource(this); + FileUpload.deleteFile(ModelHome.AVATAR_SMALL,as); + FileUpload.deleteFile(ModelHome.AVATAR_BIG,as); + getSiteImpl().update(); // fire change listeners + db().runAfterCommit(new Runnable(){public void run(){ + FileUpload.fireFileUpdateListeners(as); + }}); + DbUtils.uncache(this); + } + + private boolean hasAvatar; + private boolean checkedAvatar = false; + + public synchronized boolean hasAvatar() { + if( !checkedAvatar ) { + Message.AvatarSource as = new Message.AvatarSource(this); + hasAvatar = FileUpload.hasFile(as,ModelHome.AVATAR_SMALL) && FileUpload.hasFile(as,ModelHome.AVATAR_BIG); + checkedAvatar = true; + } + return hasAvatar; + } + + + + public Node newRootNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Site site,String type) throws ModelException { + return NodeImpl.newRootNode(kind,this,subject,message,msgFmt,(SiteImpl)site,type); + } + + public Node newChildNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Node parent) throws ModelException { + return NodeImpl.newChildNode(kind,this,subject,message,msgFmt,(NodeImpl)parent); + } + + public String getSearchId() { + return Long.toString(getId()); + } + + public String getIdString() { + return Long.toString(getId()); + } + + private void clearBounces() { + if( bounces==0 ) + return; + bounces = 0; + record.fields().put("bounces",DbNull.INTEGER); + record.update(); + } + + void bounced() { + record.fields().put("bounces",++bounces); + record.update(); + } + + int getBounces() { + return bounces; + } + + private static final int bounceLimit = Init.get("bounceLimit",100); + + boolean isAutoUnsubscribe() { + return isDeactivated() || bounces > bounceLimit; + } + + + + + private volatile Map<String, Integer> nodeCount = new HashMap<String, Integer>(); + + public final int getNodeCount(String cnd) { + String key = cnd == null? "none" : cnd; + if (!nodeCount.containsKey(key)) { + try { + Connection con = db().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select count(*) as n from node where owner_id = ?" + + (cnd == null? "" : " and " + cnd) + ); + stmt.setLong(1,getId()); + ResultSet rs = stmt.executeQuery(); + rs.next(); + nodeCount.put(key, rs.getInt("n")); + rs.close(); + stmt.close(); + con.close(); + } catch(SQLException e) { + throw new RuntimeException(e); + } + } + return nodeCount.get(key); + } + + void setNodeCount(int nodeCount) { + this.nodeCount.put("none", nodeCount); + } + + static { + Listener<NodeImpl> listener = new Listener<NodeImpl>() { + public void event(NodeImpl node) { + table(node.siteKey).uncache(new LongKey(node.getOwnerId())); + } + }; + NodeImpl.postInsertListeners.add(listener); + NodeImpl.postDeleteListeners.add(listener); + } + + + public void moveToRegisteredAccount(final String cookie) { + List<NodeImpl> nodes = new CursorNodeIterator( siteKey, + "select * from node where cookie=?" + , + new DbParamSetter() { + public void setParams(PreparedStatement stmt) throws SQLException { + stmt.setString(1,cookie); + } + } + ).asList(); + for( NodeImpl n : nodes ) { + n.setOwner(this); + n.update(); + } + } + + + public NodeIterator<? extends Node> getNodesByDateDesc(String cnd) { + return new CursorNodeIterator( siteKey, + "select * from node where owner_id = ?" + + (cnd == null? "" : " and " + cnd) + + " order by when_created desc" + , + new DbParamSetter() { + public void setParams(PreparedStatement stmt) throws SQLException { + stmt.setLong( 1, getId() ); + } + } + ); + } + + + public int deleteNodes() { + List<NodeImpl> nodes = new CursorNodeIterator( siteKey, + "select *" + +" from node" + +" where owner_id = ?" + , simpleParamSetter() + ).asList(); + int n = 0; + for( NodeImpl node : nodes ) { + db().beginTransaction(); + try { + DbUtils.getGoodCopy(node).deleteMessageOrNode(); + db().commitTransaction(); + n++; + } finally { + db().endTransaction(); + } + } + return n; + } + + public int deleteNodesRecursively() { + List<NodeImpl> nodes = new CursorNodeIterator( siteKey, + "select *" + +" from node" + +" where owner_id = ?" + , simpleParamSetter() + ).asList(); + int n = 0; + for( NodeImpl node : nodes ) { + db().beginTransaction(); + try { + DbUtils.getGoodCopy(node).deleteRecursively(); + db().commitTransaction(); + n++; + } finally { + db().endTransaction(); + } + } + return n; + } + + + private Map<ExtensionFactory<User,?>,Object> extensionMap; + + private synchronized Map<ExtensionFactory<User, ?>, Object> getExtensionMap() { + if (extensionMap == null) + extensionMap = new HashMap<ExtensionFactory<User, ?>, Object>(); + return extensionMap; + } + + public <T> T getExtension(ExtensionFactory<User,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<User,?>> extensionFactories = new CopyOnWriteArrayList<ExtensionFactory<User,?>>(); + + static <T> void addExtensionFactory(ExtensionFactory<User,T> factory) { + extensionFactories.add(factory); + Db.clearCache(); + } + + + + + // visited node + + private final Map<Long,Long> visitedNodeCache = new HashMap<Long,Long>(); + + public Long lastVisitedNodeId(long nodeId) { + synchronized(visitedNodeCache) { + return visitedNodeCache.containsKey(nodeId) ? visitedNodeCache.get(nodeId) : lastVisitedNodeIds(Collections.singletonList(nodeId)).get(nodeId); + } + } + + public Map<Long,Long> lastVisitedNodeIds(Collection<Long> nodeIds) { + synchronized(visitedNodeCache) { + Set<Long> notCached = new HashSet<Long>(); + for( Long nodeId : nodeIds ) { + if( !visitedNodeCache.containsKey(nodeId) ) + notCached.add(nodeId); + } + if( !notCached.isEmpty() ) { + StringBuilder sql = new StringBuilder(); + sql + .append( "select node_id, last_node_id from visited where user_id = " ) + .append( getId() ) + .append( " and node_id in (" ) + ; + Iterator<Long> iter = notCached.iterator(); + sql.append( iter.next() ); + while( iter.hasNext() ) { + sql.append( ',' ).append( iter.next() ); + } + sql.append( ")" ); + try { + Connection con = db().getConnection(); + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery(sql.toString()); + while( rs.next() ) { + Long nodeId = rs.getLong("node_id"); + Long lastNodeId = rs.getLong("last_node_id"); + visitedNodeCache.put(nodeId,lastNodeId); + notCached.remove(nodeId); + } + rs.close(); + stmt.close(); + con.close(); + } catch(SQLException e) { + throw new RuntimeException(e); + } + for( Long nodeId : notCached ) { + visitedNodeCache.put(nodeId,null); + } + } + Map<Long,Long> map = new HashMap<Long,Long>(); + for( Long nodeId : nodeIds ) { + map.put( nodeId, visitedNodeCache.get(nodeId) ); + } + return map; + } + } + + public void markVisited(Node topic, long lastNodeId) { + NodeImpl topicNode = (NodeImpl)topic; + long nodeId = topicNode.getId(); + boolean updated = false; + try { + Connection con = db().getConnection(); + try { + Long persistedLastVisitedNodeId = lastVisitedNodeId(nodeId); + if( persistedLastVisitedNodeId == null ) { + PreparedStatement stmt = con.prepareStatement( + "insert into visited (user_id, node_id, last_node_id)" + +" values (?, ?, ?)" + ); + stmt.setLong( 1, getId() ); + stmt.setLong( 2, nodeId ); + stmt.setLong( 3, lastNodeId ); + DbDatabaseImpl.executeUpdateIgnoringDuplicateKeys(stmt); + stmt.close(); + updated = true; + } else if (lastNodeId > persistedLastVisitedNodeId) { + PreparedStatement stmt = con.prepareStatement( + "update visited set last_node_id = ?" + +" where user_id = ? and node_id = ?" + ); + stmt.setLong( 1, lastNodeId ); + stmt.setLong( 2, getId() ); + stmt.setLong( 3, nodeId ); + stmt.executeUpdate(); + stmt.close(); + updated = true; + } + } finally { + con.close(); + } + } catch(SQLException e) { + if( !e.getMessage().contains("violates foreign key constraint \"visited_last_node_id_fkey\"") ) + throw new RuntimeException(e); + } + if (updated) { + synchronized(visitedNodeCache) { + visitedNodeCache.remove(nodeId); + } + } + } + + public void unmarkVisited(Node node) { + long nodeId = node.getId(); + try { + Connection con = db().getConnection(); + PreparedStatement stmt = con.prepareStatement( + "delete from visited" + +" where user_id = ? and node_id = ?" + ); + stmt.setLong( 1, getId() ); + stmt.setLong( 2, nodeId ); + stmt.executeUpdate(); + stmt.close(); + con.close(); + } catch(SQLException e) { + throw new RuntimeException(e); + } + synchronized(visitedNodeCache) { + visitedNodeCache.remove(nodeId); + } + } + + + static void addPostInsertListener(final Listener<? super UserImpl> listener) { + postInsertListeners.add(listener); + } + + + + 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 user_property where user_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 user_property where user_id = ? and key = ?" + ); + stmt.setLong( 1, getId() ); + stmt.setString( 2, key ); + stmt.executeUpdate(); + stmt.close(); + if( value != null ) { + stmt = con.prepareStatement( + "insert into user_property (user_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; + } + }); + + + + + + private final static PooledStringDigester passwordDigester = new PooledStringDigester(); + + static { + passwordDigester.setAlgorithm(Init.get("passwordDigestAlgorithm","SHA-256")); + passwordDigester.setIterations(Init.get("passwordDigestIterations",100000)); + passwordDigester.setSaltSizeBytes(Init.get("passwordDigestSaltSize",16)); + passwordDigester.setPoolSize(Init.get("passwordDigestPoolSize",4)); + passwordDigester.initialize(); + } + + private final static PooledStringDigester passcookieDigester = new PooledStringDigester(); + + static { + passcookieDigester.setAlgorithm(Init.get("passcookieDigestAlgorithm","SHA-256")); + passcookieDigester.setIterations(Init.get("passcookieDigestIterations",100000)); + FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator(); + // this fixed salt needs to be kept secret + sg.setSalt(Init.get("passcookieSalt", new byte[]{105, 4, 40, 78, 24, 46, 30, 100, 18, -27, 114, -21, -44, -59, 103, 43})); + passcookieDigester.setSaltGenerator(sg); + passcookieDigester.setPoolSize(Init.get("passcookieDigestPoolSize",4)); + passcookieDigester.initialize(); + } + + private final static PooledStringDigester resetcodeDigester = new PooledStringDigester(); + + static { + resetcodeDigester.setAlgorithm(Init.get("resetcodeDigestAlgorithm","SHA-256")); + resetcodeDigester.setIterations(Init.get("resetcodeDigestIterations",100000)); + FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator(); + // this fixed salt needs to be kept secret + sg.setSalt(Init.get("resetcodeSalt", new byte[]{-47, 9, -128, 109, 112, -88, -91, 39, 77, 111, 57, -102, 120, 12, 54, 16})); + resetcodeDigester.setSaltGenerator(sg); + resetcodeDigester.setPoolSize(Init.get("resetcodeDigestPoolSize",4)); + resetcodeDigester.initialize(); + } + + public boolean checkPassword(String password) { + return passwordDigest!=null && passwordDigester.matches(password, passwordDigest); + } + + private String calcPasscookie() { + return passcookieDigester.digest(passwordDigest); + } + + public boolean checkPasscookie(String passcookie) { + return passwordDigest!=null && getPasscookie().equals(passcookie); + } + + public String getResetcode() { + return resetcodeDigester.digest(passwordDigest); + } + + public boolean checkResetcode(String resetcode) { + return passwordDigest!=null && getResetcode().equals(resetcode); + } + + private static String digestPassword(String password) { + return passwordDigester.digest(password); + } + +}