Mercurial Hosting > nabble
view src/nabble/model/UserImpl.java @ 62:4674ed7d56df default tip
remove n2
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Sat, 30 Sep 2023 20:25:29 -0600 |
parents | 7df8ec497281 |
children |
line wrap: on
line source
/* 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 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); } 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() && noArchive; } private 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() ) { 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) { 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 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 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()); } boolean isAutoUnsubscribe() { return isDeactivated(); } 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); } }