diff src/nabble/model/SiteImpl.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children abe0694e9849
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/nabble/model/SiteImpl.java	Thu Mar 21 19:15:52 2019 -0600
@@ -0,0 +1,1103 @@
+package nabble.model;
+
+import fschmidt.db.DbDatabase;
+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.NoKey;
+import fschmidt.db.NoKeySetter;
+import fschmidt.util.java.CollectionUtils;
+import fschmidt.util.java.Computable;
+import fschmidt.util.java.FutureValue;
+import fschmidt.util.java.Memoizer;
+import fschmidt.util.java.SimpleCache;
+import jdbcpgbackup.DataFilter;
+import jdbcpgbackup.ZipBackup;
+import nabble.model.export.NodeData;
+import nabble.modules.ModuleManager;
+import nabble.naml.compiler.CompileException;
+import nabble.naml.compiler.Module;
+import nabble.naml.compiler.Program;
+import nabble.naml.compiler.StackTraceElement;
+import nabble.naml.compiler.Template;
+import nabble.naml.compiler.TemplatePrintWriter;
+import nabble.naml.namespaces.BasicNamespace;
+import nabble.view.web.template.NabbleNamespace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.StringWriter;
+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.Calendar;
+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 SiteImpl implements Site {
+	private static final Logger logger = LoggerFactory.getLogger(SiteImpl.class);
+
+	final SiteKey siteKey;
+	private final DbRecord<NoKey,SiteImpl> record;
+	private long rootNodeId;
+	private NodeImpl rootNode = null;
+
+	private final Object tweakLock = new Object();
+	private CompileException tweakException = null;
+	private Program program = null;
+	private final Date whenCreated;
+
+	SiteImpl(SiteKey siteKey) {
+		this.siteKey = siteKey;
+		record = table(siteKey).newRecord(this);
+		record.fields().put( "root_node_id", 0L );
+		whenCreated = new Date();
+	}
+
+	void setRoot(NodeImpl node) {
+		if( !node.isInDb() )
+			throw new RuntimeException("node must be in db");
+		this.rootNodeId = node.getId();
+		record.fields().put( "root_node_id", rootNodeId );
+		this.rootNode = node;
+		record.update();
+	}
+
+	private SiteImpl(SiteKey siteKey,NoKey key,ResultSet rs)
+		throws SQLException
+	{
+		this.siteKey = siteKey;
+		record = table(siteKey).newRecord(this,key);
+		rootNodeId = rs.getLong("root_node_id");
+		whenCreated = rs.getTimestamp("when_created");
+		for( ExtensionFactory<Site,?> factory : extensionFactories ) {
+			Object obj = factory.construct(this,rs);
+			if( obj != null )
+				getExtensionMap().put(factory,obj);
+		}
+	}
+
+	public DbRecord<NoKey,SiteImpl> getDbRecord() {
+		return record;
+	}
+
+	private DbTable<NoKey,SiteImpl> table() {
+		return record.getDbTable();
+	}
+
+	private DbDatabase db() {
+		return table().getDbDatabase();
+	}
+
+	public DbDatabase getDb() {
+		return siteKey.getDb();
+	}
+
+	public long getId() {
+		return siteKey.getId();
+	}
+/*
+	private void calcBaseUrl() {
+		siteGlobal().calcBaseUrl();
+	}
+*/
+	long getRootNodeId() {
+		return rootNodeId;
+	}
+
+	NodeImpl getRootNodeImpl() {
+		if( DbUtils.isStale(rootNode) ) {
+			rootNode = NodeImpl.getNode(siteKey,rootNodeId);
+		}
+		return rootNode;
+	}
+
+	public Node getRootNode() {
+		return getRootNodeImpl();
+	}
+
+	public Date getWhenCreated() {
+		return whenCreated;
+	}
+
+	/** To be called from the shell */
+	public void setWhenCreated(int day, int month, int year) {
+		Calendar cal = Calendar.getInstance();
+		cal.set(Calendar.DAY_OF_MONTH, day);
+		cal.set(Calendar.MONTH, month);
+		cal.set(Calendar.YEAR, year);
+		DbRecord<NoKey,?> record = getDbRecord();
+		record.fields().put("when_created", cal.getTime());
+		record.update();
+	}
+
+	SiteGlobal siteGlobal() {
+		return siteKey.siteGlobal();
+	}
+
+	@Override public boolean equals(Object obj) {
+		return this==obj || obj instanceof SiteImpl && record.isInDb() && ((SiteImpl)obj).getId()==getId();
+	}
+
+	@Override public int hashCode() {
+		return (int)getId();
+	}
+
+	public Program getProgram() {
+		synchronized(tweakLock) {
+			if( program == null ) {
+				if(trace()) logger.error("getting Program for "+this+" "+System.identityHashCode(this)+" tweakException="+tweakException);
+				List<Module> modules = ModuleManager.getModules(SiteImpl.this);
+				program = Program.getInstance(modules);
+			}
+			return program;
+		}
+	}
+
+	public Template getTemplate(String templateName,Class... base) {
+		synchronized(tweakLock) {
+			try {
+				return getProgram().getTemplate(templateName,base);
+			} catch(CompileException e) {
+				if( setTweakException(e) ) {
+					return getTemplate(templateName,base);
+				}
+				throw new RuntimeException(""+this+" "+System.identityHashCode(this),e);
+			}
+		}
+	}
+
+	public void setCustomDomain(String customDomain) {
+		siteGlobal().setCustomDomain(customDomain);
+	}
+
+	public String getCustomDomain() {
+		return siteGlobal().getCustomDomain();
+	}
+
+
+	public String getBaseUrl() {
+		return siteGlobal().getBaseUrl();
+	}
+
+	List<UserImpl> getPosters() {
+		try {
+			Connection con = db().getConnection();
+			PreparedStatement stmt = con.prepareStatement(
+				"select * from user_ where user_id in ("
+					+"select distinct owner_id from node where redirect is null"
+				+")"
+			);
+			try {
+				return UserImpl.getUsers(siteKey,stmt);
+			} finally {
+				stmt.close();
+				con.close();
+			}
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public List<User> getUsers(String cnd) {
+		List<User> list = new ArrayList<User>();
+		getUserImpls(list, cnd);
+		return list;
+	}
+
+	List<UserImpl> getUserImpls(String cnd) {
+		List<UserImpl> list = new ArrayList<UserImpl>();
+		getUserImpls(list, cnd);
+		return list;
+	}
+
+	void getUserImpls(List<? super UserImpl> list, String cnd) {
+		try {
+			Connection con = db().getConnection();
+			PreparedStatement stmt = con.prepareStatement(
+				"select * from user_" +
+				(cnd == null? "" :  " where " + cnd)
+			);
+			try {
+				UserImpl.getUsers(siteKey,stmt,list);
+			} finally {
+				stmt.close();
+				con.close();
+			}
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public int getActivity() {
+		return siteGlobal().getActivity();
+	}
+
+	public void setActivity(int activity) {
+		siteGlobal().setActivity(activity);
+	}
+
+	public boolean isEmbarrassing() {
+		return siteGlobal().isEmbarrassing();
+	}
+
+	public void setEmbarrassing(boolean isEmbarrassing) {
+		siteGlobal().setEmbarrassing(isEmbarrassing);
+	}
+
+
+	public String toString() {
+		return "site-"+getId();
+	}
+
+	static final ListenerList<SiteImpl> postUpdateListeners = new ListenerList<SiteImpl>();
+	private static final ListenerList<SiteImpl> postDeleteListeners = new ListenerList<SiteImpl>();
+	private static final ListenerList<SiteImpl> preInsertListeners = new ListenerList<SiteImpl>();
+	private static final ListenerList<SiteImpl> preUpdateListeners = new ListenerList<SiteImpl>();
+
+	private static Computable<SiteKey,DbTable<NoKey,SiteImpl>> tables = new SimpleCache<SiteKey,DbTable<NoKey,SiteImpl>>(new WeakHashMap<SiteKey,DbTable<NoKey,SiteImpl>>(), new Computable<SiteKey,DbTable<NoKey,SiteImpl>>() {
+		public DbTable<NoKey,SiteImpl> get(SiteKey siteKey) {
+			DbDatabase db = siteKey.getDb();
+			final long siteId = siteKey.getId();
+			DbTable<NoKey,SiteImpl> table = db.newTable("site",NoKeySetter.INSTANCE
+				, new DbObjectFactory<NoKey,SiteImpl>() {
+					public SiteImpl makeDbObject(NoKey key,ResultSet rs,String tableName)
+						throws SQLException
+					{
+						SiteKey siteKey = SiteKey.getInstance(siteId);
+						return new SiteImpl(siteKey,key,rs);
+					}
+				}
+			);
+			table.getPreInsertListeners().add(preInsertListeners);
+			table.getPreUpdateListeners().add(preUpdateListeners);
+			table.getPostUpdateListeners().add(postUpdateListeners);
+			table.getPostDeleteListeners().add(postDeleteListeners);
+			return table;
+		}
+	});
+
+	private static DbTable<NoKey,SiteImpl> table(SiteKey siteKey) {
+		return tables.get(siteKey);
+	}
+
+	static SiteImpl getSite(SiteKey siteKey,long siteId) {
+		return table(siteKey).findByPrimaryKey(NoKey.INSTANCE);
+	}
+
+	static SiteImpl getSite(SiteKey siteKey,ResultSet rs)
+		throws SQLException
+	{
+		return table(siteKey).getDbObject(rs);
+	}
+
+	public Date getDeleteDate() {
+		return siteGlobal().getDeleteDate();
+	}
+
+	public void clearDeleteDate() {
+		siteGlobal().clearDeleteDate();
+	}
+
+
+
+
+
+
+
+	static void addChangeListener(final Listener<? super SiteImpl> listener) {
+		postUpdateListeners.add(listener);
+		postDeleteListeners.add(listener);
+	}
+
+	static void addPreChangeListener(final Listener<? super SiteImpl> listener) {
+		preInsertListeners.add(listener);
+		preUpdateListeners.add(listener);
+	}
+
+
+	public User getUser(long id) {
+		return getUserImpl(id);
+	}
+
+	UserImpl getUserImpl(long id) {
+		return UserImpl.getUser(siteKey,id);
+	}
+
+	public User getUserFromEmail(String email) {
+		return getUserImplFromEmail(email);
+	}
+
+	UserImpl getUserImplFromEmail(String email) {
+		return UserImpl.getUserFromEmail(this,email);
+	}
+
+	public User getUserFromName(String name) {
+		return getUserImplFromName(name);
+	}
+
+	UserImpl getUserImplFromName(String name) {
+		return UserImpl.getUserFromName(this,name);
+	}
+
+	public User getOrCreateUnregisteredUser(String email,String name)
+		throws ModelException
+	{
+		return UserImpl.getOrCreateUnregisteredUser(this,email,name);
+	}
+
+	public User getOrCreateUser(String email) {
+		return UserImpl.getOrCreateUser(this,email);
+	}
+
+	public User getOrCreateUser(String email,String name) {
+		UserImpl user = getUserImplFromEmail(email);
+		if( user==null ) {
+			user = UserImpl.createGhost(this,email);
+			user.setNameLike(name,false);
+			user.insert();
+		}
+		return user;
+	}
+
+	public String newRegistration(String email,String password,String name,String nextUrl)
+		throws ModelException
+	{
+		return UserImpl.createUser(this,email,password,name).newRegistration(nextUrl);
+	}
+
+	public User getRegistration(String registrationKey)
+		throws ModelException
+	{
+		return UserImpl.getRegistration(this,registrationKey);
+	}
+
+	public List<User> getUsersByNodeCount(int i, int n, String cnd) {
+		try {
+			List<User> list = new ArrayList<User>();
+			Connection con = db().getConnection();
+			PreparedStatement stmt1 = con.prepareStatement(
+				"select *, (select count(*) from node where user_.user_id=node.owner_id) as n"
+				+" from user_"
+				+ (cnd == null? "" : " where " + cnd)
+				+" order by n desc"
+				+" limit ? offset ?"
+			);
+			stmt1.setInt(1,n);
+			stmt1.setInt(2,i);
+			ResultSet rs1 = stmt1.executeQuery();
+			while( rs1.next() ) {
+				UserImpl user = UserImpl.getUser(siteKey,rs1);
+				user.setNodeCount( rs1.getInt("n") );
+				list.add(user);
+			}
+			rs1.close();
+			stmt1.close();
+			con.close();
+			return list;
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public int getUserCount(String cnd) {
+		try {
+			Connection con = db().getConnection();
+			Statement stmt = con.createStatement();
+			ResultSet rs = stmt.executeQuery(
+				"select count(*) as n from user_" +
+				(cnd == null? "" : " where " + cnd)
+			);
+			rs.next();
+			int n = rs.getInt("n");
+			rs.close();
+			stmt.close();
+			con.close();
+			return n;
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+
+	public void deleteRootNode() throws ModelException {
+		if( !db().isInTransaction() ) {
+			db().beginTransaction();
+			try {
+				SiteImpl site = DbUtils.getGoodCopy(this);
+				site.deleteRootNode();
+				db().commitTransaction();
+			} finally {
+				db().endTransaction();
+			}
+			return;
+		}
+		NodeImpl oldRoot = getRootNodeImpl();
+		List<NodeImpl> children = oldRoot.getChildrenImpl(null).asList();
+		if( children.size() != 1 )
+			throw ModelException.newInstance("cant_delete_root","Root node must have exactly one child");
+		NodeImpl child = children.get(0);
+		child.makeRoot();
+		setRoot(child);
+		oldRoot.getDbRecord().delete();
+	}
+
+	public Person getAnonymous(String cookie, String name) {
+		return new Anonymous(this,cookie,name);
+	}
+
+	public String newAnonymousCookie() {
+		return Anonymous.newCookie(this);
+	}
+
+	public Person getPerson(String id) {
+		int i = id.indexOf(Anonymous.SEPERATOR);
+		if( i == -1 )
+			return getUser(Long.parseLong(id));
+		String cookie = id.substring(0,i);
+		String name = id.substring(i+1);
+		if( name.length() == 0 )
+			name = null;
+		return getAnonymous(cookie,name);
+	}
+
+
+	public void addTag(Node node,User user,String label) {
+		TagImpl.addTag(this,node,user,label);
+		uncacheTags(node,user);
+	}
+
+	public void deleteTags(String sqlCondition) {
+		TagImpl.deleteTags( siteKey, sqlCondition );
+	}
+
+	private static String tagSql(Node node,User user,String sqlCondition) {
+		StringBuilder sb = new StringBuilder();
+		if( node == null )
+			sb.append( "node_id is null" );
+		else
+			sb.append( "node_id=" ).append( node.getId() );
+		sb.append( " and " );
+		if( user == null )
+			sb.append( "user_id is null" );
+		else
+			sb.append( "user_id=" ).append( user.getId() );
+		sb.append( " and " ).append( sqlCondition );
+		return sb.toString();
+	}
+
+	public void deleteTags(Node node,User user,String sqlCondition) {
+		deleteTags( tagSql(node,user,sqlCondition) );
+		uncacheTags(node,user);
+	}
+
+	private 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 Memoizer<String,Boolean> tagCache(Node node,User user) {
+		if( user != null )
+			return ((UserImpl)user).tagCache;
+		else if( node != null )
+			return ((NodeImpl)node).tagCache;
+		else
+			return tagCache;
+	}
+
+	private void uncacheTags(Node node,User user) {
+		if( user != null )
+			DbUtils.uncache((UserImpl)user);
+		else if( node != null )
+			DbUtils.uncache((NodeImpl)node);
+		else
+			DbUtils.uncache(this);
+	}
+
+	public boolean hasTags(Node node,User user,String sqlCondition) {
+		return tagCache(node,user).get( tagSql(node,user,sqlCondition) );
+	}
+
+	public int countTags(String sqlCondition) {
+		return TagImpl.countTags(siteKey,sqlCondition);
+	}
+
+	public List<String> findTagLabels(String sqlCondition) {
+		return TagImpl.findTagLabels(this,sqlCondition);
+	}
+
+	public List<User> findTagUsers(String sqlCondition) {
+		return new ArrayList<User>( UserImpl.getUsers( siteKey, TagImpl.findTagUserIds(this,sqlCondition) ) );
+	}
+
+	public List<Long> findTagUserIds(String sqlCondition) {
+		return TagImpl.findTagUserIds(this,sqlCondition);
+	}
+
+	public List<Node> findTagNodes(String sqlCondition) {
+		return new ArrayList<Node>( NodeImpl.getNodes( siteKey, TagImpl.findTagNodeIds(this,sqlCondition) ) );
+	}
+
+	public List<Long> findTagNodeIds(String sqlCondition) {
+		return TagImpl.findTagNodeIds(this,sqlCondition);
+	}
+
+
+	private FutureValue<Map<String,Boolean>> modulesEnabled = new FutureValue<Map<String,Boolean>>() {
+		protected Map<String,Boolean> compute() {
+			Map<String,Boolean> map = new HashMap<String,Boolean>();
+			try {
+				Connection con = db().getConnection();
+				Statement stmt = con.createStatement();
+				ResultSet rs = stmt.executeQuery(
+					"select module_name, is_enabled from module"
+				);
+				while( rs.next() ) {
+					String moduleName = rs.getString("module_name");
+					boolean isEnabled = rs.getBoolean("is_enabled");
+					map.put(moduleName,isEnabled);
+				}
+				rs.close();
+				stmt.close();
+				con.close();
+			} catch(SQLException e) {
+				throw new RuntimeException(e);
+			}
+			if( map.isEmpty() )
+				map = Collections.emptyMap();
+			return map;
+		}
+	};
+
+	public boolean isModuleEnabled(String moduleName) {
+		Boolean b = modulesEnabled.get().get(moduleName);
+		return b != null ? b : ModuleManager.isEnabledByDefault(moduleName);
+	}
+
+	public void setModuleEnabled(String moduleName,boolean isEnabled) {
+		try {
+			Connection con = db().getConnection();
+			if( isEnabled == ModuleManager.isEnabledByDefault(moduleName) ) {
+				PreparedStatement stmt = con.prepareStatement(
+					"delete from module where module_name = ?"
+				);
+				stmt.setString( 1, moduleName );
+				stmt.executeUpdate();
+				stmt.close();
+			} else if( modulesEnabled.get().get(moduleName) == null ) {
+				PreparedStatement stmt = con.prepareStatement(
+					"insert into module (module_name,is_enabled) values (?,?)"
+				);
+				stmt.setString( 1, moduleName );
+				stmt.setBoolean( 2, isEnabled );
+				stmt.executeUpdate();
+				stmt.close();
+			} else {
+				PreparedStatement stmt = con.prepareStatement(
+					"update module set is_enabled = ? where module_name = ?"
+				);
+				stmt.setBoolean( 1, isEnabled );
+				stmt.setString( 2, moduleName );
+				stmt.executeUpdate();
+				stmt.close();
+			}
+			con.close();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+		record.update();  // uncache and fire update listeners
+	}
+
+	private FutureValue<String> config = new FutureValue<String>() {
+		protected String compute() {
+			StringBuilder tweak = new StringBuilder();
+			final Set<String> names = new HashSet<String>();
+			try {
+				Connection con = db().getConnection();
+				Statement stmt = con.createStatement();
+				ResultSet rs = stmt.executeQuery(
+					"select name, naml from configuration"
+				);
+				while( rs.next() ) {
+					names.add( rs.getString("name") );
+					tweak.append(rs.getString("naml"));
+					tweak.append("\n\n");
+				}
+				rs.close();
+				stmt.close();
+				con.close();
+			} catch(SQLException e) {
+				throw new RuntimeException(e);
+			}
+			if( !names.isEmpty() ) {
+				Executors.executeSometime(new Runnable(){
+					public void run() {
+						boolean didDelete = false;
+						for( String name : names ) {
+							if( !isValidConfiguration(name) ) {
+								deleteConfiguration(name);
+								didDelete = true;
+								logger.error("deleted invalid config: "+name);
+							}
+						}
+						if( didDelete )
+							update();
+					}
+				});
+			}
+			return tweak.toString();
+		}
+	};
+
+	public String getConfigurationTweak() {
+		return config.get();
+	}
+
+	private volatile FutureValue<Map<String,String>> tweaks = newTweaks();
+
+	private FutureValue<Map<String,String>> newTweaks() {
+		return new FutureValue<Map<String,String>>() {
+			protected Map<String,String> compute() {
+				Map<String,String> map = new HashMap<String,String>();
+				try {
+					Connection con = db().getConnection();
+					Statement stmt = con.createStatement();
+					ResultSet rs = stmt.executeQuery(
+						"select tweak_name, content from tweak"
+					);
+					while( rs.next() ) {
+						String tweakName = rs.getString("tweak_name");
+						String content = rs.getString("content");
+						map.put(tweakName,content);
+					}
+					rs.close();
+					stmt.close();
+					con.close();
+				} catch(SQLException e) {
+					throw new RuntimeException(e);
+				}
+				return CollectionUtils.optimizeMap(map);
+			}
+		};
+	}
+
+	public Map<String,String> getCustomTweaks() {
+		return tweaks.get();
+	}
+
+	public void setCustomTweaks(Map<String,String> tweaks) {
+		try {
+			Connection con = db().getConnection();
+			try {
+				{
+					Statement stmt = con.createStatement();
+					stmt.executeUpdate(
+						"delete from tweak"
+					);
+					stmt.close();
+				}
+				{
+					PreparedStatement stmt = con.prepareStatement(
+						"insert into tweak (tweak_name,content) values (?,?)"
+					);
+					for( Map.Entry<String,String> entry : tweaks.entrySet() ) {
+						String tweakName = entry.getKey();
+						String content = entry.getValue();
+						stmt.setString( 1, tweakName );
+						stmt.setString( 2, content );
+						stmt.executeUpdate();
+					}
+					stmt.close();
+				}
+			} finally {
+				con.close();
+			}
+			DailyNumber.tweaks.inc();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+		this.tweaks = newTweaks();
+		synchronized(tweakLock) {
+			this.tweakException = null;
+			this.program = null;
+		}
+		record.update();  // uncache and fire update listeners
+	}
+
+	public void resetCustomTweaks() {
+		setCustomTweaks(Collections.<String,String>emptyMap());
+	}
+
+	private static long traceSiteId = Init.get("traceSiteId",0L);
+
+	private boolean trace() {
+		return getId() == traceSiteId;
+	}
+
+	public boolean setTweakException(CompileException tweakException) {
+		synchronized(tweakLock) {
+			if( this.tweakException != null ) {
+				if(trace()) logger.error("this.tweakException already set in "+this+" "+System.identityHashCode(this),new Exception(this.tweakException));
+				return false;
+			}
+			for( StackTraceElement ste : tweakException.stackTrace ) {
+				if( ModuleManager.isConfigurationTweak(ste.source) || ModuleManager.isCustomTweak(ste.source) ) {
+					logger.debug("tweak exception in "+this,tweakException);
+					if(trace()) logger.error("tweak exception in "+this+" "+System.identityHashCode(this),tweakException);
+					this.tweakException = tweakException;
+					this.program = null;
+					return true;
+				}
+			}
+			if(trace()) logger.error("no tweak in stack trace");
+			return false;
+		}
+	}
+
+	public CompileException getTweakException() {
+		synchronized(tweakLock) {
+			return tweakException;
+		}
+	}
+
+	public void update() {
+		record.update();
+	}
+
+	public Site getGoodCopy() {
+		return DbUtils.getGoodCopy(this);
+	}
+
+	NodeImpl getNodeImpl(long id) {
+		return NodeImpl.getNode(siteKey,id);
+	}
+
+	public Node getNode(long id) {
+		return getNodeImpl(id);
+	}
+
+	public Node getNode(ResultSet rs) throws SQLException {
+		return NodeImpl.getNode(siteKey,rs);
+	}
+
+	private void check(Collection<? extends Node> nodes) {
+		for( Iterator<? extends Node> i = nodes.iterator(); i.hasNext(); ) {
+			if( !i.next().getSite().equals(this) )
+				throw new RuntimeException("node from wrong site");
+		}
+	}
+
+	public Collection<? extends Node> getNodes(Collection<Long> ids) {
+		Collection<NodeImpl> nodes = NodeImpl.getNodes(siteKey,ids);
+		check(nodes);
+		return nodes;
+	}
+
+	public NodeIterator<? extends Node> getNodeIterator(String sql,DbParamSetter paramSetter) {
+		return new CursorNodeIterator( siteKey, sql, paramSetter );
+	}
+
+
+	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 site_property where key = ?"
+				);
+				stmt.setString( 1, 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 site_property where key = ?"
+			);
+			stmt.setString( 1, key );
+			stmt.executeUpdate();
+			stmt.close();
+			if( value != null ) {
+				stmt = con.prepareStatement(
+					"insert into site_property (key,value) values (?,?)"
+				);
+				stmt.setString( 1, key );
+				stmt.setString( 2, value );
+				stmt.executeUpdate();
+				stmt.close();
+			}
+			con.close();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		} finally {
+			propertyCache.remove(key);
+		}
+	}
+
+
+	public boolean isValidConfiguration(String name) {
+		Template template = getTemplate( "is_valid_configuration",
+			BasicNamespace.class, NabbleNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		template.run( new TemplatePrintWriter(sw), Collections.<String,Object>singletonMap("config",name),
+			new BasicNamespace(template), new NabbleNamespace(this)
+		);
+		return Template.booleanValue(sw.toString().trim());
+	}
+
+	private void deleteConfiguration(Connection con,String name) throws SQLException {
+		PreparedStatement stmt = con.prepareStatement(
+			"delete from configuration where name = ?"
+		);
+		stmt.setString( 1, name );
+		stmt.executeUpdate();
+		stmt.close();
+	}
+
+	public void deleteConfiguration(String name) {
+		try {
+			Connection con = db().getConnection();
+			deleteConfiguration(con,name);
+			con.close();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public void saveConfiguration(String name,String value,String naml) {
+		if( !isValidConfiguration(name) )
+			throw new RuntimeException("invalid configuration: "+name);
+		try {
+			Connection con = db().getConnection();
+			deleteConfiguration(con,name);
+			PreparedStatement stmt = con.prepareStatement(
+				"insert into configuration (name,value,naml) values (?,?,?)"
+			);
+			stmt.setString( 1, name );
+			stmt.setString( 2, value );
+			stmt.setString( 3, naml );
+			stmt.executeUpdate();
+			stmt.close();
+			con.close();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public String getConfigurationValue(String name) {
+		if( !isValidConfiguration(name) )
+			throw new RuntimeException("invalid configuration: "+name);
+		try {
+			Connection con = db().getConnection();
+			PreparedStatement stmt = con.prepareStatement(
+				"select value from configuration where name = ?"
+			);
+			stmt.setString( 1, name );
+			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 getNextUrl(String registrationKey) {
+		return UserImpl.getNextUrl(siteKey,registrationKey);
+	}
+
+	public Node newNode(NodeData data)
+		throws ModelException
+	{
+		return new NodeImpl(this,data);
+	}
+
+	public Collection<Node> cacheLastNodes(Collection<Node> nodes) {
+		Collection<LongKey> keys = new ArrayList<LongKey>();
+		for( Node n : nodes ) {
+			NodeImpl node = (NodeImpl)n;
+			keys.add( new LongKey(node.getLastNodeId()) );
+		}
+		Map<LongKey,NodeImpl> objs = NodeImpl.table(siteKey).findByPrimaryKey(keys);
+		return new ArrayList<Node>(objs.values());
+	}
+
+
+	public void addTask(String task) {
+		siteKey.addTask(task);
+	}
+
+
+
+	private Map<ExtensionFactory<Site,?>,Object> extensionMap;
+
+	private synchronized Map<ExtensionFactory<Site, ?>, Object> getExtensionMap() {
+		if (extensionMap == null)
+			extensionMap = new HashMap<ExtensionFactory<Site, ?>, Object>();
+		return extensionMap;
+	}
+
+	public <T> T getExtension(ExtensionFactory<Site,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<Site,?>> extensionFactories = new CopyOnWriteArrayList<ExtensionFactory<Site,?>>();
+
+	static <T> void addExtensionFactory(ExtensionFactory<Site,T> factory) {
+		extensionFactories.add(factory);
+		Db.clearCache();
+	}
+
+
+
+	public Site getSite() {
+		return this;
+	}
+
+	public long getSourceId() {
+		throw new UnsupportedOperationException();
+	}
+
+	public Message.SourceType getMessageSourceType() {
+		return Message.SourceType.SITE;
+	}
+
+
+	public void delete() {
+		if( getRootNode().getOwner() instanceof User ) {
+			File file = backup();
+			// Don't sent the deletion email if the site has only one node
+			if (getRootNode().getDescendantCount() > 1) {
+				Template template = getTemplate( "site deletion email",
+					BasicNamespace.class, NabbleNamespace.class
+				);
+				Map<String,Object> params = new HashMap<String,Object>();
+				params.put("file",file.getName());
+				template.run( TemplatePrintWriter.NULL, params,
+					new BasicNamespace(template),
+					new NabbleNamespace(this)
+				);
+			}
+		}
+		kill();
+	}
+
+	public void kill() {
+		if( !db().isInTransaction() ) {
+			db().beginTransaction();
+			try {
+				SiteImpl site = DbUtils.getGoodCopy(SiteImpl.this);
+				site.kill();
+				db().commitTransaction();
+			} finally {
+				db().endTransaction();
+			}
+			return;
+		}
+		SiteGlobal siteGlobal = siteGlobal();
+		if( siteGlobal == null )
+			throw new NullPointerException("siteGlobal not found for "+siteKey);
+		try {
+			Connection con = Db.dbPostgres().getConnection();
+			Statement stmt = con.createStatement();
+			stmt.executeUpdate(
+				"drop schema " + siteKey.schema() + " cascade"
+			);
+			DbUtils.uncache(this);
+			stmt.close();
+			con.close();
+		} catch(SQLException e) {
+			throw new RuntimeException(e);
+		}
+		siteGlobal.getDbRecord().delete();
+	}
+
+	private static final String SALT = "zDf3s";
+	private static final File schemaDir = new File((String)Init.get("local_dir")+"schemas/");
+
+	private static File getBackupFile(long siteId) {
+		int hash = Math.abs(( SALT + siteId ).hashCode());
+		String filename = "site_"+siteId+"_"+hash+".zip";
+		return new File(schemaDir,filename);
+	}
+
+	public File backup() {
+		File file = getBackupFile(getId());
+		backup(file);
+		return file;
+	}
+
+	public void backup(String filename) {
+		backup( new File(filename) );
+	}
+
+	private void backup(File file) {
+		file.delete();
+		ZipBackup backup = new ZipBackup( file, Db.completeUrl );
+		backup.dump( Collections.singleton(siteKey.schema()), DataFilter.ALL_DATA );
+	}
+
+	static final DataFilter SCHEMA_DATA = new DataFilter() {
+		public boolean dumpData(String schema,String tableName) {
+			return tableName.equals("version");
+		}
+	};
+
+	public void backupSchema(String filename) {
+		ZipBackup backup = new ZipBackup( new File(filename), Db.completeUrl );
+		backup.dump( Collections.singleton(siteKey.schema()), SCHEMA_DATA );
+	}
+//	in beanshell, I do:
+//	s = ModelHome.getSite(2)
+//	s.backupSchema("/Users/Franklin/hg/nabble/src/nabble/data/site.schema")
+
+}