view src/nabble/model/SiteImpl.java @ 35:5ea557eece1f

remove site.isNew()
author Franklin Schmidt <fschmidt@gmail.com>
date Tue, 07 Jul 2020 09:57:53 -0600
parents b0e75dfe1853
children b5d56f522ea3
line wrap: on
line source

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 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 = Init.get("salt","zDf3s");
	private static final File schemaDir = new File((String)Init.get("home_dir")+"local/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")

}