Python With ZK Part3

From Documentation
Revision as of 10:01, 28 November 2011 by Southerncrossie (talk | contribs)
Python With ZK Part3

Author
BobZK
Date
November 28, 2011
Version
ZK 3.6+

Python With ZK Part 3

In previous small-talks [1][2] I showed how it was possible to use Python with ZK in a MVC (Model View Controller) style and not have to rely on ZSCRIPT. It made it possible to create a web application without any JAVA code, without ZSCRIPT and yet still have an efficient application and without mixing application code with the view code.

Internally we created some applications based on my previous Small_Talks but there were two problems I left unanswered - security and performance. For us, security was not an issue as the applications were used only internally. Performance was also not really a problem as the number of users was small.

Now I want to show you how we overcame both problems and show you a framework that will allow you to develop real applications that are quick to develop, require no Java skill, perform very well and are secure. Maybe I should have called this Small_Talk "ZK without JAVA"?

For those not familiar with the previous small-talks, I use the term "Python" everywhere in this document but in reality it is realy "Jython".

A Simple ZUL - showairport.zul

<?page title="Find Airports (Model/View/Contoller)" contentType="text/html;charset=UTF-8"?>
<zk>
    <window id="mywin" title="Find Airports" border="normal" height="200px" width="200px" closable="true" sizable="true"
    apply="pfactory.ComposerFactory"> 
    <custom-attributes factory-class="HelloController" />
           Enter an Airport Code:
           <textbox id="txMessage"/>
           <button id="okButton" label="Go Find It"/>
           <separator></separator>
           <label id="myLabel" visible="false">We have found</label>
           <label id="lMessage"/>
    </window>
</zk>

This simple ZUL has changed from the previous small-talks. You will notice on line 4 (a continuation of line 3) the statement apply="pfactory.ComposerFactory" - this is a standard bit of code and is required on all windows where you want to have a Python program handle the model and controller functions. The code for ComposerFactory is the only Java you will need and should not require changing. You can ignore ComposerFactory for now - the code is shown later.

On line 5 you will see <custom-attributes factory-class="HelloController" /> - this is where you give the name of the actual Python class that does all the work.

The format of the custom attribute is factory-class="package.class" where "package" is the python package directory and defaults to "src" if omitted and "class" is the name of the class to be instantiated. For example if you specified "mypython.myclass" then there needs to be a python package called "mypython" under the "src" directory and there needs to be a class called "myclass" somewhere in it. In this simple small_talk we put the source directly in the "src" directory but bigger projects may well need to create a package under "src".

On lines 7,8,10 and 11 you will see I have given the components an ID (txMessage, okButton, myLabel, lMessage) - these IDs are referenced in the controller code.

The screen should look like :-

Screenshot-4.png

HelloController.py

Now we need some controller code :-

from org.zkoss.zk.ui.event import Events
from HelloModel import HelloModel
 
class HelloController:
     
    def __init__(self):
        self.model = HelloModel()
 
    def onClick_okButton(self, event):
        message = self.txMessage.getValue().upper()
        self.lMessage.setValue(self.model.getAirport(message))
        self.myLabel.setVisible(True)
                   
    def onClose_mywin(self, event):
        Events.getRealOrigin(event).stopPropagation()
        return False

The class HelloController is involked by ZK (via the ComposerFactory) because of the "apply" and "custom-attributes" parameters in the ZUL. As you can see there is a standard python "__init__" method invoked so we can do initialization - here we simply instantiate the model (more later). There are also methods to handle "onClick" and "onClose" events. Notice that unlike java we use an underscore and not a dollar to separate the event name from the component id. In this case "onClick_okButton" handles the onClick event for the component with the id "okButton".

You will see in the onClick method how we can access components from the ZUL. For example the code "message = self.txMessage.getValue().upper()" gets the value of the textbox with the id "txMessage", converts it to uppercase and stores it in "message". One important thing to notice is that you have to use "self" before the name. In this example we use self.txMessage. If you don't use "self", Python will not find the variable.

You will also see how in HelloController we can use txMessage and lMessage and myLabel (defined in the ZUL with exactly those names) directly and get and set them as required

Ok, we have seen how a simple python can automatically get at ZUL variables and how events from a ZUL component will invoke a method in HelloController.

HelloModel.py

Now for some model code. This is the same as the previous small-talk but is included here for completeness :-

from com.ziclix.python.sql import zxJDBC
 
class Select:
    __STATIC  = 0
    __DYNAMIC = 1
    __jndiName = "java:comp/env/jdbc/mydatabase"
    __factory = "com.sun.jndi.fscontext.RefFSContextFactory"
     
    def __init__(self, select):
        self.select = select
         
    def runSelect(self, variables):
        try:   
            conn = None
            cur = None  
            result = []                     # a list
            result_dict = {}                # a dict
             
            conn = zxJDBC.lookup(Select.__jndiName, INITIAL_CONTEXT_FACTORY=Select.__factory)
            cur = conn.cursor(Select.__DYNAMIC)
            cur.execute(self.select, variables)
             
            rows = cur.fetchall()
            if cur.rowcount == 0:
                return None
 
            for row in rows:
                result_dict = {}
                for i, column in enumerate(row):
                    result_dict[cur.description[i][0]] = column
                result.append(result_dict)
            return tuple(result)
         
        finally:
            if cur is not None:
                cur.close()
                cur = None
            if conn is not None:
                conn.close()
                conn = None
     
class HelloModel:
    
    def getAirport(self, airport):
        statement = Select("select a_name from f_airports where a_code = ?;")
        rowset = statement.runSelect([airport])
        if rowset is None:
            return "Not Found"
        else:
            return rowset[0]['a_name']

You can see in class HelloModel in method getAirport that we first instantiate a select object ("statement") and then run that select with the airport code as the variable.

I am not going to say much more about this code, I'm sure other people would code it differently, but it does work, It uses Postgresql (but can be changed easily to any other DB) and zxJDBC with tomcat connection pooling. By coding it in such a manner, we ensure that we never hold a connection longer than absolutely necessary and that we always release the connection.

The appropriate context code needs entering into context.xml file in the META-INF directory (WebContent/META-INF/context.xml) -

context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context path="/helloworld" reloadable="true" crossContext="true">
<Resource name="jdbc/mydatabase" username="myuser" password="mypassword"
   url="jdbc:postgresql://localhost/mydatabase"
   auth="Container" defaultAutoCommit="false"
   driverClassName="org.postgresql.Driver" maxActive="20"
   type="javax.sql.DataSource" />
</Context>

The code in context.xml is the only code you need to change to use the same zul, controller and model code with a different database server (eg. mysql).

Of course you will need to set up your own database (mydatabase) and f_airports table with an a_code column and an a_name column, if you really want to test the code.

Now for the magic :-

pfactory.ComposerFactory.java

This is the only bit of Java you need and you should not need to change it:-

package pfactory;


import org.python.core.Py;
import org.python.core.PyObject;
import org.python.core.PySystemState;
import org.python.core.PyTuple;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.util.GenericForwardComposer;

public class ComposerFactory extends GenericForwardComposer {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	protected PySystemState state;
	protected PyObject pyComp;
	public ComposerFactory() {
		super('_');
	}
	public void doAfterCompose(Component component) {
		try {
			super.doAfterCompose(component);
		} catch (Exception e) {
			e.printStackTrace();
		}
		String name = (String) self.getAttribute("factory-class");
		int last_dot = name.lastIndexOf(".");
		state = new PySystemState();
		PyObject importer = state.getBuiltins().__getitem__(Py.newString("__import__"));
		PyObject module;
		if (last_dot == -1) {
			module = importer.__call__(Py.newString(name));
			pyComp =  module.__getattr__(name).__call__();
		}
		else {
			String base_name = name.substring(0, last_dot);
			name = name.substring(last_dot + 1);
			PyObject fromlist = new PyTuple(Py.newString(name));
			module = importer.__call__(Py.newString(base_name), null, null, fromlist);
			pyComp =  module.__getattr__(name).__getattr__(name).__call__();
		}
		PyObject dir = pyComp.__dir__();
		for (int i = 0; dir.__finditem__(i) != null; i++) {
			String methodName = dir.__finditem__(i).toString();
			if (methodName.startsWith("on") & methodName.contains("_")) {
                int idx = methodName.indexOf("_");
                Component tcomp = component.getFellowIfAny(methodName.substring(idx + 1));
                if (tcomp != null) { 
                	tcomp.addForward(methodName.substring(0, idx), component, methodName);
                	component.addEventListener(methodName, this);
                }
			}
		}
		for (Object tcomp : component.getFellows()) {
			String fieldName = ((Component) tcomp).getId();
			PyObject ob = Py.java2py(tcomp);
			pyComp.__setattr__(fieldName, ob);
		}
		PyObject ob = Py.java2py(component);
		if (pyComp.__findattr__("doAfterCompose") != null) {
			pyComp.invoke("doAfterCompose", ob);
		}
	}
		
	public void onEvent(Event evt) {
		try {
			pyComp.__getattr__(evt.getName()).__call__(Py.java2py(evt));
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

This bit of magic extends the standard ZK GenericForwardComposer. Importantly it changes the separator from being a dollar to an underscore. It loads python/jython. It then wires in all of the "events" and on receiving one of those events it fires off the appropriate method in the appropriate controller (in this case HelloController).

That's it.

Where does all this code go?

Well assuming you have a standard Eclipse project set up, then the directory structure is like :-

Project dir

   src dir
       pfactory dir
           ComposerFactory.java
       HelloController.py
       HelloModel.py
  WebContent dir
      META-INF dir
          context.xml
      showairport.zul

Conclusion

It is possible to use ZK with Python in a Model-View-Controller fashion and keep the code separate from each other. You can easily set this up in Eclipse and use all the highlighting and code completion assistance of Eclipse, ZK and Pydev.

The code runs efficiently and securely.

One of the web sites we developed has thousands of visitors and 10s of thousands of hits per day with processor usage averaging less than 5% with very quick response times and it is written entirely in Python.

Comments



Copyright © BobZK. This article is licensed under GNU Free Documentation License.