"""Bobo handler module
For use with medusa & python object publisher

copyright 1997 amos latteier
(code based on script_handler.py)

Use:

here is a sample fragment from a script to start medusa:

...
hs = http_server.http_server (IP_ADDRESS, HTTP_PORT, rs, lg)
...
sys.path.insert(0,'c:\\windows\\desktop\\medusa2\\www\\bobo')
import bobotest
bh=bobo_handler.bobo_handler(bobotest, debug=1)	#create bobo handler	
hs.install_handler(bh)	#install handler in http server
...

This will install the bobo handler on the http server and give you 
access to the bobotest module via urls like this:

http://myserver.com/bobotest/blah/blah

bobo_handler initalization options: 
* debug: If the debug flag is set then the published module will be reloaded
whenever its source is changed on disk. This is very handy for developement.
* uri_base: If the uri_base isn't specified it defaults to /<module_name>
"""

__version__="1.03"

import cgi_module_publisher
import sys
import regex
import string
import os

try:
	from cStringIO import StringIO
except:
	from StringIO import StringIO
try:
	import thread
  	mutex = thread.allocate_lock()
	THREADS = 1
except:
	THREADS = 0

import counter
import default_handler
import producers

split_path = default_handler.split_path
unquote    = default_handler.unquote
get_header = default_handler.get_header

CONTENT_LENGTH = regex.compile ('Content-Length: \([0-9]+\)', regex.casefold)


# maps request headers to environment variables
#

header2env={'Content-Length'	: 'CONTENT_LENGTH',
			'Content-Type'		: 'CONTENT_TYPE',
			'Referer'			: 'HTTP_REFERER',
			'User-Agent'		: 'HTTP_USER_AGENT',
			'Accept'			: 'HTTP_ACCEPT',
			'Accept-Charset'	: 'HTTP_ACCEPT_CHARSET',
			'Accept-Language'	: 'HTTP_ACCEPT_LANGUAGE',
			'Host'				: None,
			'Connection'		: 'CONNECTION_TYPE',
			'Pragma'			: None,
			'Authorization'		: 'HTTP_AUTHORIZATION',
			'Cookie'			: 'HTTP_COOKIE',
			}
# convert keys to lower case for case-insensitive matching
#
for (key,value) in header2env.items():
	del header2env[key]
	key=string.lower(key)
	header2env[key]=value


class bobo_handler:
	"publishes a module via bobo"
	
	def __init__ (self, module, uri_base=None, debug=None):
		self.module = module
		self.debug = debug
		if self.debug:
			self.last_reload=self.module_mtime()
		self.hits = counter.counter()
		
		# if uri_base is unspecified, assume it
		# starts with the published module name
		#
		if not uri_base:	
			uri_base="/%s" % module.__name__
		elif uri_base[-1]=="/":	# kill possible trailing /
			uri_base=uri_base[:-1]
		self.uri_base=uri_base
		
		uri_regex='%s.*' % self.uri_base
		self.uri_regex = regex.compile(uri_regex)
		

	def match (self, request):
		uri = request.uri
		if self.uri_regex.match (uri) == len(uri):
			return 1
		else:
			return 0

	def handle_request (self, request):
		[path, params, query, fragment] = split_path (request.uri)
		while path and path[0] == '/':
			path = path[1:]

		if '%' in path:
			path = unquote (path)

		self.hits.increment()

		if query:
			# cgi_publisher_module doesn't want the leading '?'
			query = query[1:]
		
		self.env = {}
		self.env['REQUEST_METHOD']	= string.upper(request.command)
		self.env['SERVER_PORT']		= '%s' % request.channel.server.port
		self.env['SERVER_NAME']		= request.channel.server.server_name
		self.env['SERVER_SOFTWARE']	= request['Server']
		self.env['SCRIPT_NAME']		= self.uri_base  # are script_name and path_info ok?
		self.env['QUERY_STRING']	= query
		try:
			path_info=string.split(path,self.uri_base[1:],1)[1]
		except:
			path_info=''
		self.env['PATH_INFO']		= path_info
		self.env['GATEWAY_INTERFACE']='CGI/1.1'			# what should this really be?
		self.env['REMOTE_ADDR']=request.channel.addr[0]
		self.env['REMOTE_HOST']=request.channel.addr[0]	#what should this be?

		for header in request.header:
			[key,value]=string.split(header,": ",1)
			key=string.lower(key)
			if header2env.has_key(key):
				if header2env[key]:
					self.env[header2env[key]]=value
			else:
				key='HTTP_'+string.upper(string.join(string.split(key,"-"),"_"))
				self.env[key]=value

		# remove empty environment variables
		#
		for key in self.env.keys():
			if self.env[key]=="" or self.env[key]==None:
				del self.env[key]

		if request.command in ["post","put"]:
			request.collector=input_collector(self,request)
			request.channel.set_terminator (None)
		else:
			sin=StringIO('')
			self.continue_request(sin,request)
		

	def continue_request(self,sin,request):
		"continue handling request now that we have the stdin"
		
		# if we have threads spawn a new one to publish the module
		# so we dont freeze the server while publishing.
		if THREADS:
			thread.start_new_thread(self._continue_request,(sin,request))
		else:
			self._continue_request(sin,request)
			

	def _continue_request(self,sin,request):
		"continue handling request now that we have the stdin"
		sout = StringIO()
		serr = StringIO()

		if self.debug:
			m_time=self.module_mtime()
			if m_time> self.last_reload:
				reload(self.module)
				self.last_reload=m_time
		if THREADS:
			mutex.acquire()
		cgi_module_publisher.publish_module(
			self.module.__name__,
			stdin=sin,
			stdout=sout,
			stderr=serr,
			environ=self.env,
			#debug=1
			)
		if THREADS:
			mutex.release()

		if serr.tell():
			request.log(serr.getvalue())
		
		response=sout
		response=response.getvalue()

		# set response headers
		[headers,html]=string.split(response,"\n\n",1)
		headers=string.split(headers,"\n")

		for line in headers:
			[header, header_value]=string.split(line,": ",1)
			if header=="Status":
				[code,message]=string.split(header_value," ",1)
				request.reply_code=string.atoi(code)
			else:
				request[header]=header_value

		request.push(html)
		request.done()


	def module_mtime(self):
		"returns the last modified date for a given module's source file"
		return os.stat(self.module.__file__)[8]

	def status (self):
		return producers.simple_producer (
			'<li>Bobo Handler'
			+ '<ul>'
			+ '  <li><b>Hits:</b> %d' % int(self.hits)
			+ '</ul>'
			)


class input_collector:
	"gathers input for put and post requests"

	def __init__ (self, handler, request):
		self.handler	= handler
		self.request	= request
		self.data = StringIO()
		
		# make sure there's a content-length header
		self.cl = get_header (CONTENT_LENGTH, request.header)
		
		if not self.cl:
			request.error(411)
			return
		else:
			self.cl = string.atoi(self.cl)

	def collect_incoming_data (self, data):
		self.data.write(data)
		if self.data.tell() >= self.cl:
			self.data.seek(0)

			h=self.handler
			r=self.request

			# set the terminator back to the default
			self.request.channel.set_terminator ('\r\n\r\n')
			del self.handler
			del self.request

			h.continue_request(self.data,r)