# HG changeset patch # User casties # Date 1297433123 -3600 # Node ID 09361041be515287bbb138e1c78f6cc1bc3cfd0c first checkin diff -r 000000000000 -r 09361041be51 RestDbInterface.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RestDbInterface.py Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,393 @@ +''' +Created on 19.5.2010 + +@author: casties +''' + +from OFS.Folder import Folder +from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from AccessControl import getSecurityManager, Unauthorized +from Products.ZSQLExtend import ZSQLExtend +import logging +import re +import json +import time +import psycopg2 +# make psycopg use unicode objects +import psycopg2.extensions +psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) +psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) + +from zope.interface import implements +from zope.publisher.interfaces import IPublishTraverse +from ZPublisher.BaseRequest import DefaultPublishTraverse + + +def unicodify(s,alternate='latin-1'): + """decode str (utf-8 or latin-1 representation) into unicode object""" + if not s: + return u"" + if isinstance(s, str): + try: + return s.decode('utf-8') + except: + return s.decode(alternate) + else: + return s + +def utf8ify(s): + """encode unicode object or string into byte string in utf-8 representation. + assumes string objects to be utf-8""" + if not s: + return "" + if isinstance(s, str): + return s + else: + return s.encode('utf-8') + +def getTextFromNode(node): + """get the cdata content of a XML node""" + if node is None: + return "" + + if isinstance(node, list): + nodelist = node + else: + nodelist=node.childNodes + + rc = "" + for node in nodelist: + if node.nodeType == node.TEXT_NODE: + rc = rc + node.data + return rc + +def sqlName(s,lc=True): + """returns restricted ASCII-only version of string""" + if s is None: + return "" + + # all else -> "_" + s = re.sub(r'[^A-Za-z0-9_]','_',s) + if lc: + return s.lower() + + return s + + +class RestDbInterface(Folder): + """Object for RESTful database queries + path schema: /db/{schema}/{table}/ + omitting table gives a list of schemas + omitting table and schema gives a list of schemas + """ + implements(IPublishTraverse) + + meta_type="RESTdb" + manage_options=Folder.manage_options+( + {'label':'Config','action':'manage_editRestDbInterfaceForm'}, + ) + + # management templates + manage_editRestDbInterfaceForm=PageTemplateFile('zpt/editRestDbInterface',globals()) + + # data templates + XML_index = PageTemplateFile('zpt/XML_index', globals()) + XML_schema = PageTemplateFile('zpt/XML_schema', globals()) + XML_schema_table = PageTemplateFile('zpt/XML_schema_table', globals()) + HTML_index = PageTemplateFile('zpt/HTML_index', globals()) + HTML_schema = PageTemplateFile('zpt/HTML_schema', globals()) + HTML_schema_table = PageTemplateFile('zpt/HTML_schema_table', globals()) + JSONHTML_index = PageTemplateFile('zpt/JSONHTML_index', globals()) + JSONHTML_schema = PageTemplateFile('zpt/JSONHTML_schema', globals()) + JSONHTML_schema_table = PageTemplateFile('zpt/JSONHTML_schema_table', globals()) + # JSON_* templates are scripts + def JSON_index(self): + """JSON index function""" + self.REQUEST.RESPONSE.setHeader("Content-Type", "application/json") + json.dump(self.getListOfSchemas(), self.REQUEST.RESPONSE) + + def JSON_schema(self,schema): + """JSON index function""" + self.REQUEST.RESPONSE.setHeader("Content-Type", "application/json") + json.dump(self.getListOfTables(schema), self.REQUEST.RESPONSE) + + def JSON_schema_table(self,schema,table): + """JSON index function""" + self.REQUEST.RESPONSE.setHeader("Content-Type", "application/json") + json.dump(self.getTable(schema, table), self.REQUEST.RESPONSE) + + + def __init__(self, id, title, connection_id=None): + """init""" + self.id = id + self.title = title + # database connection id + self.connection_id = connection_id + # create template folder + self.manage_addFolder('template') + + + def getRestDbUrl(self): + """returns url to the RestDb instance""" + return self.absolute_url() + + def getJsonString(self,object): + """returns a JSON formatted string from object""" + return json.dumps(object) + + def getCursor(self,autocommit=True): + """returns fresh DB cursor""" + conn = getattr(self,"_v_database_connection",None) + if conn is None: + # create a new connection object + try: + if self.connection_id is None: + # try to take the first existing ID + connids = SQLConnectionIDs(self) + if len(connids) > 0: + connection_id = connids[0][0] + self.connection_id = connection_id + logging.debug("connection_id: %s"%repr(connection_id)) + + da = getattr(self, self.connection_id) + da.connect('') + # we copy the DAs database connection + conn = da._v_database_connection + #conn._register() # register with the Zope transaction system + self._v_database_connection = conn + except Exception, e: + raise IOError("No database connection! (%s)"%str(e)) + + cursor = conn.getcursor() + if autocommit: + # is there a better version to get to the connection? + cursor.connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + + return cursor + + def getFieldNameMap(self,fields): + """returns a dict mapping field names to row indexes""" + map = {} + i = 0 + for f in fields: + map[f[0]] = i + i += 1 + + return map + + def executeSQL(self, query, args=None, hasResult=True, autocommit=True): + """execute query with args on database and return all results. + result format: {"fields":fields, "rows":data}""" + logging.debug("executeSQL query=%s args=%s"%(query,args)) + cur = self.getCursor(autocommit=autocommit) + if args is not None: + # make sure args is a list + if isinstance(args,basestring): + args = (args,) + + cur.execute(query, args) + # description of returned fields + fields = cur.description + if hasResult: + # get all data in an array + data = cur.fetchall() + cur.close() + #logging.debug("fields: %s"%repr(fields)) + #logging.debug("rows: %s"%repr(data)) + return {"fields":fields, "rows":data} + else: + cur.close() + return None + + def isAllowed(self,action,schema,table,user=None): + """returns if the requested action on the table is allowed""" + if user is None: + user = self.REQUEST.get('AUTHENTICATED_USER',None) + logging.debug("isAllowed action=%s schema=%s table=%s user=%s"%(action,schema,table,user)) + # no default policy! + return True + + + def publishTraverse(self,request,name): + """change the traversal""" + # get stored path + path = request.get('restdb_path', []) + logging.debug("publishtraverse: name=%s restdb_path=%s"%(name,path)) + + if name in ("index_html", "PUT"): + # end of traversal + if request.get("method") == "POST" and request.get("action",None) == "PUT": + # fake PUT by POST with action=PUT + name = "PUT" + + return getattr(self, name) + #TODO: should we check more? + else: + # traverse + if len(path) == 0: + # first segment + if name == 'db': + # virtual path -- continue traversing + path = [name] + request['restdb_path'] = path + else: + # try real path + tr = DefaultPublishTraverse(self, request) + ob = tr.publishTraverse(request, name) + return ob + else: + path.append(name) + + # continue traversing + return self + + + def index_html(self,REQUEST,RESPONSE): + """index method""" + # ReST path was stored in request + path = REQUEST.get('restdb_path',[]) + + # type and format are real parameter + resultFormat = REQUEST.get('format','HTML').upper() + queryType = REQUEST.get('type',None) + + logging.debug("index_html path=%s resultFormat=%s queryType=%s"%(path,resultFormat,queryType)) + + if queryType is not None: + # non-empty queryType -- look for template + pt = getattr(self.template, "%s_%s"%(resultFormat,queryType), None) + if pt is not None: + return pt(format=resultFormat,type=queryType,path=path) + + if len(path) == 1: + # list of schemas + return self.showListOfSchemas(format=resultFormat) + elif len(path) == 2: + # list of tables + return self.showListOfTables(format=resultFormat,schema=path[1]) + elif len(path) == 3: + # table + if REQUEST.get("method") == "POST" and REQUEST.get("create_table_file",None) is not None: + # POST to table to check + return self.checkTable(format=resultFormat,schema=path[1],table=path[2]) + # else show table + return self.showTable(format=resultFormat,schema=path[1],table=path[2]) + + # don't know what to do + return str(REQUEST) + + def showTable(self,format='XML',schema='public',table=None,REQUEST=None,RESPONSE=None): + """returns PageTemplate with tables""" + logging.debug("showtable") + if REQUEST is None: + REQUEST = self.REQUEST + + # should be cross-site accessible + if RESPONSE is None: + RESPONSE = self.REQUEST.RESPONSE + + RESPONSE.setHeader('Access-Control-Allow-Origin', '*') + + # everything else has its own template + pt = getattr(self.template, '%s_schema_table'%format, None) + if pt is None: + return "ERROR!! template %s_schema_table not found"%format + + #data = self.getTable(schema,table) + return pt(schema=schema,table=table) + + def getTable(self,schema='public',table=None,sortBy=1,username='guest'): + """return table data""" + logging.debug("gettable") + if sortBy: + data = self.executeSQL('select * from "%s"."%s" order by %s'%(schema,table,sortBy)) + else: + data = self.executeSQL('select * from "%s"."%s"'%(schema,table)) + return data + + def hasTable(self,schema='public',table=None,username='guest'): + """return if table exists""" + logging.debug("hastable") + data = self.executeSQL('select 1 from information_schema.tables where table_schema=%s and table_name=%s',(schema,table)) + ret = bool(data['rows']) + return ret + + def showListOfTables(self,format='XML',schema='public',REQUEST=None,RESPONSE=None): + """returns PageTemplate with list of tables""" + logging.debug("showlistoftables") + # should be cross-site accessible + if RESPONSE is None: + RESPONSE = self.REQUEST.RESPONSE + RESPONSE.setHeader('Access-Control-Allow-Origin', '*') + + pt = getattr(self.template, '%s_schema'%format, None) + if pt is None: + return "ERROR!! template %s_schema not found"%format + + #data = self.getListOfTables(schema) + return pt(schema=schema) + + def getListOfTables(self,schema='public',username='guest'): + """return list of tables""" + logging.debug("getlistoftables") + # get list of fields and types of db table + #qstr="""SELECT c.relname AS tablename FROM pg_catalog.pg_class c + # LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + # WHERE c.relkind IN ('r','') AND n.nspname NOT IN ('pg_catalog', 'pg_toast') + # AND pg_catalog.pg_table_is_visible(c.oid) + # AND c.relname ORDER BY 1""" + qstr = """SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' + AND table_schema = %s ORDER BY 1""" + data=self.executeSQL(qstr,(schema,)) + return data + + def showListOfSchemas(self,format='XML',REQUEST=None,RESPONSE=None): + """returns PageTemplate with list of schemas""" + logging.debug("showlistofschemas") + # should be cross-site accessible + if RESPONSE is None: + RESPONSE = self.REQUEST.RESPONSE + RESPONSE.setHeader('Access-Control-Allow-Origin', '*') + + pt = getattr(self.template, '%s_index'%format, None) + if pt is None: + return "ERROR!! template %s_index not found"%format + + #data = self.getListOfSchemas() + return pt() + + def getListOfSchemas(self,username='guest'): + """return list of schemas""" + logging.debug("getlistofschemas") + # TODO: really look up schemas + data={'fields': (('schemas',),), 'rows': [('public',),]} + return data + + def manage_editRestDbInterface(self, title=None, connection_id=None, + REQUEST=None): + """Change the object""" + if title is not None: + self.title = title + + if connection_id is not None: + self.connection_id = connection_id + + #checkPermission=getSecurityManager().checkPermission + REQUEST.RESPONSE.redirect('manage_main') + + +manage_addRestDbInterfaceForm=PageTemplateFile('zpt/addRestDbInterface',globals()) + +def manage_addRestDbInterface(self, id, title='', label='', description='', + createPublic=0, + createUserF=0, + REQUEST=None): + """Add a new object with id *id*.""" + + ob=RestDbInterface(str(id),title) + self._setObject(id, ob) + + #checkPermission=getSecurityManager().checkPermission + REQUEST.RESPONSE.redirect('manage_main') + + diff -r 000000000000 -r 09361041be51 WritableRestDbInterface.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WritableRestDbInterface.py Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,275 @@ +''' +Created on 11.2.2011 + +@author: casties, fknauft +''' + +import logging +import re +import time +import datetime +import urllib + +from RestDbInterface import * + + + +class WritableRestDbInterface(RestDbInterface): + """Object for RESTful database queries + path schema: /db/{schema}/{table}/ + omitting table gives a list of schemas + omitting table and schema gives a list of schemas + """ + + meta_type="rwRESTdb" + + + def PUT(self, REQUEST, RESPONSE): + """ + Implement WebDAV/HTTP PUT/FTP put method for this object. + """ + logging.debug("RestDbInterface PUT") + #logging.debug("req=%s"%REQUEST) + #self.dav__init(REQUEST, RESPONSE) + #self.dav__simpleifhandler(REQUEST, RESPONSE) + # ReST path was stored in request + path = REQUEST.get('restdb_path',[]) + if len(path) == 3: + schema = path[1] + tablename = path[2] + file = REQUEST.get("create_table_file",None) + if file is None: + RESPONSE.setStatus(400) + return + + fields = None + fieldsStr = REQUEST.get("create_table_fields",None) + logging.debug("put with schema=%s table=%s file=%s fields=%s"%(schema,tablename,file,repr(fieldsStr))) + if fieldsStr is not None: + # unpack fields + fields = [{"name":n, "type": t} for (n,t) in [f.split(":") for f in fieldsStr.split(",")]] + + ret = self.createTableFromXML(schema, tablename, file, fields) + # return the result as JSON + format = REQUEST.get("format","JSON") + if format == "JSON": + RESPONSE.setHeader("Content-Type", "application/json") + json.dump(ret, RESPONSE) + + elif format == "JSONHTML": + RESPONSE.setHeader("Content-Type", "text/html") + RESPONSE.write("\n\n
")
+                json.dump(ret, RESPONSE)
+                RESPONSE.write("
\n\n") + + else: + # 400 Bad Request + RESPONSE.setStatus(400) + return + + def checkTable(self,format,schema,table,REQUEST=None,RESPONSE=None): + """check the table. + returns valid data fields and table name.""" + if REQUEST is None: + REQUEST = self.REQUEST + RESPONSE = REQUEST.RESPONSE + + file = REQUEST.get("create_table_file",None) + res = self.checkTableFromXML(schema, table, file) + logging.debug("checkTable result=%s"%repr(res)) + # return the result as JSON + if format == "JSON": + RESPONSE.setHeader("Content-Type", "application/json") + json.dump(res, RESPONSE) + + elif format == "JSONHTML": + RESPONSE.setHeader("Content-Type", "text/html") + RESPONSE.write("\n\n
")
+            json.dump(res, RESPONSE)
+            RESPONSE.write("
\n\n") + + else: + return "ERROR: invalid format" + + def checkTableFromXML(self,schema,table,data,REQUEST=None,RESPONSE=None): + """check the table with the given XML data. + returns valid data fields and table name.""" + logging.debug("checkTableFromXML schema=%s table=%s"%(schema,table)) + # clean table name + tablename = sqlName(table) + tableExists = self.hasTable(schema, table) + if data is None: + fieldNames = [] + else: + # get list of field names from upload file + fields = self.importExcelXML(schema,tablename,data,fieldsOnly=True) + + res = {'tablename': tablename, 'table_exists': tableExists} + res['fields'] = fields + return res + + def createEmptyTable(self,schema,table,fields): + """create a table with the given fields + returns list of created fields""" + logging.debug("createEmptyTable") + + sqlFields = [] + for f in fields: + if isinstance(f,dict): + # {name: XX, type: YY} + name = sqlName(f['name']) + type = f['type'] + if hasattr(self, 'toSqlTypeMap'): + sqltype = self.toSqlTypeMap[type] + else: + sqltype = 'text' + + else: + # name only + name = sqlName(f) + type = 'text' + sqltype = 'text' + + sqlFields.append({'name':name, 'type':type, 'sqltype':sqltype}) + + if self.hasTable(schema,table): + # TODO: find owner + if not self.isAllowed("update", schema, table): + raise Unauthorized + self.executeSQL('drop table "%s"."%s"'%(schema,table),hasResult=False) + else: + if not self.isAllowed("create", schema, table): + raise Unauthorized + + fieldString = ", ".join(['"%s" %s'%(f['name'],f['sqltype']) for f in sqlFields]) + sqlString = 'create table "%s"."%s" (%s)'%(schema,table,fieldString) + logging.debug("createemptytable: SQL=%s"%sqlString) + self.executeSQL(sqlString,hasResult=False) + self.setTableMetaTypes(schema,table,sqlFields) + return sqlFields + + def createTableFromXML(self,schema,table,data, fields=None): + """create or replace a table with the given XML data""" + logging.debug("createTableFromXML schema=%s table=%s data=%s fields=%s"%(schema,table,data,fields)) + tablename = sqlName(table) + self.importExcelXML(schema, tablename, data, fields) + return {"tablename": tablename} + + def importExcelXML(self,schema,table,xmldata,fields=None,fieldsOnly=False): + ''' + Import XML file in Excel format into the table + @param table: name of the table the xml shall be imported into + ''' + from xml.dom.pulldom import parseString,parse + + if not (fieldsOnly or self.isAllowed("create", schema, table)): + raise Unauthorized + + namespace = "urn:schemas-microsoft-com:office:spreadsheet" + containerTagName = "Table" + rowTagName = "Row" + colTagName = "Cell" + dataTagName = "Data" + xmlFields = [] + sqlFields = [] + numFields = 0 + sqlInsert = None + + logging.debug("import excel xml") + + ret="" + if isinstance(xmldata, str): + logging.debug("importXML reading string data") + doc=parseString(xmldata) + else: + logging.debug("importXML reading file data") + doc=parse(xmldata) + + cnt = 0 + while True: + node=doc.getEvent() + + if node is None: + break + + else: + #logging.debug("tag=%s"%node[1].localName) + if node[1].localName is not None: + tagName = node[1].localName.lower() + else: + # ignore non-tag nodes + continue + + if tagName == rowTagName.lower(): + # start of row + doc.expandNode(node[1]) + cnt += 1 + if cnt == 1: + # first row -- field names + names=node[1].getElementsByTagNameNS(namespace, dataTagName) + for name in names: + fn = getTextFromNode(name) + xmlFields.append({'name':sqlName(fn),'type':'text'}) + + if fieldsOnly: + # return just field names + return xmlFields + + # create table + if fields is None: + fields = xmlFields + + sqlFields = self.createEmptyTable(schema, table, fields) + numFields = len(sqlFields) + fieldString = ", ".join(['"%s"'%f['name'] for f in sqlFields]) + valString = ", ".join(["%s" for f in sqlFields]) + sqlInsert = 'insert into "%s"."%s" (%s) values (%s)'%(schema,table,fieldString,valString) + #logging.debug("importexcelsql: sqlInsert=%s"%sqlInsert) + + else: + # following rows are data + colNodes=node[1].getElementsByTagNameNS(namespace, colTagName) + data = [] + hasData = False + for colNode in colNodes: + dataNodes=colNode.getElementsByTagNameNS(namespace, dataTagName) + if len(dataNodes) > 0: + val = getTextFromNode(dataNodes[0]) + hasData = True + else: + val = None + + data.append(val) + + if not hasData: + # ignore empty rows + continue + + # fix number of data fields + if len(data) > numFields: + del data[numFields:] + elif len(data) < numFields: + missFields = numFields - len(data) + data.extend(missFields * [None,]) + + logging.debug("importexcel sqlinsert=%s data=%s"%(sqlInsert,data)) + self.executeSQL(sqlInsert, data, hasResult=False) + + return cnt + + +manage_addWritableRestDbInterfaceForm=PageTemplateFile('zpt/addWritableRestDbInterface',globals()) + +def manage_addWritableRestDbInterface(self, id, title='', label='', description='', + createPublic=0, + createUserF=0, + REQUEST=None): + """Add a new object with id *id*.""" + + ob=WritableRestDbInterface(str(id),title) + self._setObject(id, ob) + + #checkPermission=getSecurityManager().checkPermission + REQUEST.RESPONSE.redirect('manage_main') + + diff -r 000000000000 -r 09361041be51 __init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/__init__.py Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,21 @@ + +import RestDbInterface +import WritableRestDbInterface + +def initialize(context): + + context.registerClass( + RestDbInterface.RestDbInterface, + constructors = ( + RestDbInterface.manage_addRestDbInterfaceForm, + RestDbInterface.manage_addRestDbInterface + ) + ) + + context.registerClass( + WritableRestDbInterface.WritableRestDbInterface, + constructors = ( + WritableRestDbInterface.manage_addWritableRestDbInterfaceForm, + WritableRestDbInterface.manage_addWritableRestDbInterface + ) + ) diff -r 000000000000 -r 09361041be51 zpt/HTML_index.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/HTML_index.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,25 @@ + + + + + The title + + +

List of schemas

+ + + + + + + + +
+
+ + \ No newline at end of file diff -r 000000000000 -r 09361041be51 zpt/HTML_schema.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/HTML_schema.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,25 @@ + + + + + The title + + +

List of tables for schema

+ + + + + + + + +
+
+ + \ No newline at end of file diff -r 000000000000 -r 09361041be51 zpt/HTML_schema_table.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/HTML_schema_table.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,28 @@ + + + + + The title + + +

table

+ + + + + + + + + + +
+
+
+
+ + \ No newline at end of file diff -r 000000000000 -r 09361041be51 zpt/HTML_schema_usertables.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/HTML_schema_usertables.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,25 @@ + + + + + The title + + +

List of user tables for schema

+ + + + + + + + +
+
+ + \ No newline at end of file diff -r 000000000000 -r 09361041be51 zpt/JSONHTML_index.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/JSONHTML_index.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,13 @@ + + + + + The title + + +

+  
+
\ No newline at end of file
diff -r 000000000000 -r 09361041be51 zpt/JSONHTML_schema.zpt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zpt/JSONHTML_schema.zpt	Fri Feb 11 15:05:23 2011 +0100
@@ -0,0 +1,13 @@
+
+
+  
+    
+    The title
+  
+  
+  

+  
+
\ No newline at end of file
diff -r 000000000000 -r 09361041be51 zpt/JSONHTML_schema_table.zpt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zpt/JSONHTML_schema_table.zpt	Fri Feb 11 15:05:23 2011 +0100
@@ -0,0 +1,13 @@
+
+
+  
+    
+    The title
+  
+  
+  

+  
+
\ No newline at end of file
diff -r 000000000000 -r 09361041be51 zpt/XML_index.zpt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zpt/XML_index.zpt	Fri Feb 11 15:05:23 2011 +0100
@@ -0,0 +1,7 @@
+
+
+  
+	
+  
+
\ No newline at end of file
diff -r 000000000000 -r 09361041be51 zpt/XML_schema.zpt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/zpt/XML_schema.zpt	Fri Feb 11 15:05:23 2011 +0100
@@ -0,0 +1,7 @@
+
+
+  
+	
+  
+
\ No newline at end of file diff -r 000000000000 -r 09361041be51 zpt/XML_schema_table.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/XML_schema_table.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,8 @@ + + + + + +
diff -r 000000000000 -r 09361041be51 zpt/addRestDbInterface.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/addRestDbInterface.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,9 @@ +
Header
+

Add a RESTdb interface

+
+

ID

+

+

Title

+

+

+
diff -r 000000000000 -r 09361041be51 zpt/addWritableRestDbInterface.zpt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/zpt/addWritableRestDbInterface.zpt Fri Feb 11 15:05:23 2011 +0100 @@ -0,0 +1,9 @@ +
Header
+

Add a RESTdb interface

+
+

ID

+

+

Title

+

+

+