Tapestry 3 pages were defined as an extension of one of it's built in page classes, usually BasePage. These page classes are defined as "abstract" and Tapestry takes the abstract class you define and dynamically creates a subclass all fleshed out with standard properties and functionality at runtime.
Thus a typical page might be defined as follows:
public abstract class Checkout extends BasePage implements IExternalPage, PageRenderListener {
Now in tapestry 5 it would just be a simple class:
public class Checkout {
Note that there might be interface implementations or even class extensions used, but would be for special features required by the developer, and nothing directly to do with basic page functionality.
Data Models defined in Tapestry 3 page classes would also be abstract:
public abstract CustomerData getCustomerData();
public abstract void setCustomerData(CustomerData customerData);
public abstract OrderData getOrderData();
public abstract void setOrderData(OrderData orderData);
This would allow Tapestry to create the appropriate declarations in the subclass of your abstract page.
In Tapestry 5 adding the @Property annotation would cause the appropriate getter and setter to be generated:
@Persist(PersistenceConstants.FLASH)
@Property
private ShiptoData _shiptoData;
@Persist(PersistenceConstants.FLASH)
@Property
private DetailData _detailData;
For some reason I felt like these properties needed to be retained during the redirect that happens after every form submit (hence the FLASH). I still can't seem to get my head around this, when this is required and when it is taken care of automatically.
To be clear, this FLASH persistence is sometimes required, because when Tapestry 5 processes a form submit it always causes the browser to talk to the server twice. First it posts the submit, and the page class processes the data that is submitted from the form. The only reply that is made back to the browser is a "redirect" with the URL the browser should go to. The browser responds by making a request of the server to display the page at the URL it was redirected to.
In case of an error this redirected request will often be the same page with the form so the users can fix the problem with the information and resubmit the form. But that will be blank page like any other request might be starting with. Somehow tapestry, through the session data, must know which user's form data to display because that isn't necessarily contained in the redirect url.
Thursday, May 6, 2010
Friday, April 23, 2010
Primary Keys on Forms and IActionListener and no more Rewinds
In Tapestry 3 you could specify a hidden field that would trigger an
event when the form was submitted:
<span jwcid="@Hidden" value="ognl:itemFormKey" listener="ognl:listeners.formKeyListener"/>
This gets fired at the beginning of the form rewind cycle so that any updating or validating that requires the primary key of the object(s) being added or updated by the form.
In Tapestry 5 the "Hidden" component that won't automatically serialize the the object like in T3 and it doesn't fire a listener. Instead you can specify a "ValueEncoder" which can be an interface added to your page or component class. This can be used to encode (toClient) and decode (toValue) objects so long as you declare the object Serializable and use some sort of method to convert it to a string (e.g. toString() or convert use base64 if it is a complex object. You can also use the toValue(String) function to instantiate any objects or lookup any data related to the primary key before the rest of the form submit is processed. The hidden field ValueEncoders are processed before the form submit processing starts copying the form fields into your page or component class. Note that I am not sure, but this order of processing might require that the Hidden component is placed at the top of the form.
-OR-
You don't have to use hidden components to accomplish the same thing.
A more direct route, which is the one I used, is to define a context for the form in the template for the page or component.
<form t:type="form" t:id="itemForm" t:context="itemFormKey" action="POST">
This automatically creates a hidden field for the itemFormKey. You will need to define an "onPrepareForSubmit" function in your page class that uses "itemFormKey" as a parameter. Note that the object _itemFormKey will need to be instantiated at least the first time the page containing the form is requested.
void onPrepareForSubmit(String itemFormKey) {
try {
_itemFormKey = (ItemFormKey) Base64.decodeToObject(itemFormKey);
}
catch(Throwable e) {
throw new RuntimeException(e);
}
}
The Base64 class is one from Robert Harder at http://iharder.net/base64
Add a getter for it so that you can encode the object (must be serializable) to a base64 string for the hidden field during rendering of the template containing the form:
public String getItemFormKey() {
try {
return Base64.encodeObject(_itemFormKey);
}
catch(Throwable e) {
return "";
}
}
You know how Tapestry 5 does no rewind cycle? What it does when a submit takes place is the page object processes a series of methods related to handling the form data. See this page for a list of the methods called in what order:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
After all this processing of the Submit is done, that's it. The tapestry page finishes by sending a URL to the browser as a page redirect. So every form submit is followed by a separate request automatically from the client (not counting Ajax). Generally you have the flexibility to redirect anywhere you want after or during processing the form submit, but you needn't do much of anything for it to redirect to the current page which is what you probably want if there is an error found during form validation in onValidateForm().
Now think about this. Since this is a redirect, you start over with a brand new instance (or a clean instance from the pool) of the page object. The "key" field (in this case itemFormKey) will be null.
In order to fix this you will need to utilize a special kind of data "persistance" available in Tapestry5 for just this purpose. It is called "FLASH" and it has nothing to do with macromedia but if you apply the annotation to the declaration in your java class, the object will be saved in the session for the duration of one more rendering. So during the "redirected" request that always happens after every form submission, that object will be available. When that one single rendering is done, the object is deleted from the session. To accomplish this all you need to do is this:
@Persist(PersistenceConstants.FLASH)
private ItemFormKey _itemFormKey;
Now the ItemFormKey will be available for as long as the current form is being used, and Submitted. If the user navigates away from the form the value will no longer be persisted, unless of course the navigation is done as an "Open in New Window".
event when the form was submitted:
<span jwcid="@Hidden" value="ognl:itemFormKey" listener="ognl:listeners.formKeyListener"/>
This gets fired at the beginning of the form rewind cycle so that any updating or validating that requires the primary key of the object(s) being added or updated by the form.
In Tapestry 5 the "Hidden" component that won't automatically serialize the the object like in T3 and it doesn't fire a listener. Instead you can specify a "ValueEncoder" which can be an interface added to your page or component class. This can be used to encode (toClient) and decode (toValue) objects so long as you declare the object Serializable and use some sort of method to convert it to a string (e.g. toString() or convert use base64 if it is a complex object. You can also use the toValue(String) function to instantiate any objects or lookup any data related to the primary key before the rest of the form submit is processed. The hidden field ValueEncoders are processed before the form submit processing starts copying the form fields into your page or component class. Note that I am not sure, but this order of processing might require that the Hidden component is placed at the top of the form.
-OR-
You don't have to use hidden components to accomplish the same thing.
A more direct route, which is the one I used, is to define a context for the form in the template for the page or component.
<form t:type="form" t:id="itemForm" t:context="itemFormKey" action="POST">
This automatically creates a hidden field for the itemFormKey. You will need to define an "onPrepareForSubmit" function in your page class that uses "itemFormKey" as a parameter. Note that the object _itemFormKey will need to be instantiated at least the first time the page containing the form is requested.
void onPrepareForSubmit(String itemFormKey) {
try {
_itemFormKey = (ItemFormKey) Base64.decodeToObject(itemFormKey);
}
catch(Throwable e) {
throw new RuntimeException(e);
}
}
The Base64 class is one from Robert Harder at http://iharder.net/base64
Add a getter for it so that you can encode the object (must be serializable) to a base64 string for the hidden field during rendering of the template containing the form:
public String getItemFormKey() {
try {
return Base64.encodeObject(_itemFormKey);
}
catch(Throwable e) {
return "";
}
}
You know how Tapestry 5 does no rewind cycle? What it does when a submit takes place is the page object processes a series of methods related to handling the form data. See this page for a list of the methods called in what order:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
After all this processing of the Submit is done, that's it. The tapestry page finishes by sending a URL to the browser as a page redirect. So every form submit is followed by a separate request automatically from the client (not counting Ajax). Generally you have the flexibility to redirect anywhere you want after or during processing the form submit, but you needn't do much of anything for it to redirect to the current page which is what you probably want if there is an error found during form validation in onValidateForm().
Now think about this. Since this is a redirect, you start over with a brand new instance (or a clean instance from the pool) of the page object. The "key" field (in this case itemFormKey) will be null.
In order to fix this you will need to utilize a special kind of data "persistance" available in Tapestry5 for just this purpose. It is called "FLASH" and it has nothing to do with macromedia but if you apply the annotation to the declaration in your java class, the object will be saved in the session for the duration of one more rendering. So during the "redirected" request that always happens after every form submission, that object will be available. When that one single rendering is done, the object is deleted from the session. To accomplish this all you need to do is this:
@Persist(PersistenceConstants.FLASH)
private ItemFormKey _itemFormKey;
Now the ItemFormKey will be available for as long as the current form is being used, and Submitted. If the user navigates away from the form the value will no longer be persisted, unless of course the navigation is done as an "Open in New Window".
Persist Sessions and Clients
Tapestry 5 provides the @Persist annotation. I think something like this actually showed up in T4. In T5 there are three modes:
@Persist(PersistenceConstants.SESSION)
@Persist(PersistenceConstants.CLIENTS)
@Persist(PersistenceConstants.FLASH)
SESSION is the default, so you can just specify it as:
@Persist
private String username;
As with the "Visit" in tapestry 3 the Session Persistence is supported with a cookie that acts as a key to the actual session data.
FLASH persistence is useful because of the way form submits are handled. The Page object processes a set of methods specifically for handling form submissions, then at the end of every form submission a client redirect is performed. This means that following every submit, the browser is told to request a new page (it can be the same page if there were errors on the form).
That redirected request will like involve a completely different instance of a page, even if it is the "same" page. FLASH persistence saves data for exactly one render cylce. Error messages are passed to the redirected page using FLASH persistence. Also if you want to redisplay the values that were entered into the form so that the user can make changes to clear the error instead of retyping everything, then you will have to either FLASH persist all of fields or FLASH persist a primary key which will allow you to retrieve the data.
Fortunately it looks like error messages stored using "recordError" and properties linked to Form templates (i.e. input tags) are all automatically FLASH persisted. Exactly what else is FLASH persisted is still a mystery to me.
What I do know is if you create a private property to use as a form "context" for the purpose of maintaining a "primary key" for the life of the form, and that property has a custom getter because it is a complex class that requires serialization, then you will have to define it using:
@Persist(PersistenceConstants.FLASH)
private Key _key;
If you have other one time messages or text besides the ones saved using the "record.Error" function, like maybe something that says "Changes saved" then you will need to apply the @Persist(PersistenceConstants.FLASH) annotation to those as well.
CLIENT persistance is almost useless. I guess if there was no way to keep track of a session it might be helpful. The problem with it is any data placed in CLIENT persistence must be serialized into every link and every form POST on the page. If the data is a little bit big then your page will become huge and the URLs too long. So long in fact that data might get truncated by some browsers.
@Persist(PersistenceConstants.SESSION)
@Persist(PersistenceConstants.CLIENTS)
@Persist(PersistenceConstants.FLASH)
SESSION is the default, so you can just specify it as:
@Persist
private String username;
As with the "Visit" in tapestry 3 the Session Persistence is supported with a cookie that acts as a key to the actual session data.
FLASH persistence is useful because of the way form submits are handled. The Page object processes a set of methods specifically for handling form submissions, then at the end of every form submission a client redirect is performed. This means that following every submit, the browser is told to request a new page (it can be the same page if there were errors on the form).
That redirected request will like involve a completely different instance of a page, even if it is the "same" page. FLASH persistence saves data for exactly one render cylce. Error messages are passed to the redirected page using FLASH persistence. Also if you want to redisplay the values that were entered into the form so that the user can make changes to clear the error instead of retyping everything, then you will have to either FLASH persist all of fields or FLASH persist a primary key which will allow you to retrieve the data.
Fortunately it looks like error messages stored using "recordError" and properties linked to Form templates (i.e. input tags) are all automatically FLASH persisted. Exactly what else is FLASH persisted is still a mystery to me.
What I do know is if you create a private property to use as a form "context" for the purpose of maintaining a "primary key" for the life of the form, and that property has a custom getter because it is a complex class that requires serialization, then you will have to define it using:
@Persist(PersistenceConstants.FLASH)
private Key _key;
If you have other one time messages or text besides the ones saved using the "record.Error" function, like maybe something that says "Changes saved" then you will need to apply the @Persist(PersistenceConstants.FLASH) annotation to those as well.
CLIENT persistance is almost useless. I guess if there was no way to keep track of a session it might be helpful. The problem with it is any data placed in CLIENT persistence must be serialized into every link and every form POST on the page. If the data is a little bit big then your page will become huge and the URLs too long. So long in fact that data might get truncated by some browsers.
Server State what replaces Visit object?
This one is easy. T5 includes an annotation called @SessionState.
Just declare:
@SessionState
private Visit visit;
And you can use your old visit class from Tapestry 3 without modification. You won't have to do anything like Visit visit = (Visit) getVisit(); to instantiate it. It'll just be there.
Just declare:
@SessionState
private Visit visit;
And you can use your old visit class from Tapestry 3 without modification. You won't have to do anything like Visit visit = (Visit) getVisit(); to instantiate it. It'll just be there.
Monday, April 19, 2010
Select Drop-down List. Where is PropertySelection?
Functionally SELECTS work pretty much the same, except you now you need to build the whole list for Tapestry to retrieve all at once. Under Tapestry 3 I was already building a List of Vectors in my Model for drop downs that gets used by both the Swing (desktop gui) application and the Tapestry3 web app. That meant that I was making a list, the Tapestry3 PropertySelector object was reading that list one option at a time and building it's own separate copy of the list.
This was done through an IPropertySelectionModel interface. Now instead the list goes all at once directly to Tapestry and it keeps track of items and the current selection for you.
For drop-down selects my model which previously implemented IPropertySelectionModel was converted to extend AbstractSelectModel.
So previously in our template (html) file under Tapestry 3 we would have something like this:
<select jwcid="@PropertySelection"
value="ognl:shiptoData.state"
model="ognl:stateSelectModel"/>
It only looks a little simpler for Tapestry 5:
<select t:type="Select"
value="shiptoData.state"
model="StateSelectModel"/>
The class declaration changes from implementing an interface to extending an abstract class definition:
Tapestry3:
public class StateSelectModel implements IPropertySelectionModel {
Tapestry5:
public class StateSelectModel extends AbstractSelectModel {
Additional code for Tapestry 5:
Add a private property for the entire list if you don't already have one (make sure you include the OptionModel interface in your delcaration):
private List optionModelList;
Then add the following two functions to your class:
public List<OptionModel> getOptions() {
optionModelList = new ArrayList<OptionModel>();
for (int i=1; i <= getOptionCount(); i++) {
optionModelList.add(new OptionModelImpl(getLabel(i), getValue(i)));
}
return optionModelList;
}
public List<OptionGroupModel> getOptionGroups() {
return null;
}
That's all there is to it. Your SELECT drop-down will work. Note that the get functions used in that getOptions() function are ones already implemented for the IPropertySelectionModel interface in Tapestry 3.
But even if you are doing this from scratch you should be able to easily provide the return values for the "label" and "value" which are both String type.
The getOptionGroups() function is just returning a null. This is a simple drop down. To do groups is slightly more complicated. You would have to have a loop like the one in getOptions, but then another loop inside of that which built an OptionModel list.
Now if you look closely at this simple example you'll notice that the servlet ends up doing the same extra step as in Tapestry 3. I'm making a list (you can assume that anyway), then the function is reading it one option at a time and making a copy of that list which is then offerered to Tapestry Select component through the getOptions() function.
The reason for this is there is another class that I am getting the values from and that class is the same one that is being used for the JComboBox in the swing application.
This was done through an IPropertySelectionModel interface. Now instead the list goes all at once directly to Tapestry and it keeps track of items and the current selection for you.
For drop-down selects my model which previously implemented IPropertySelectionModel was converted to extend AbstractSelectModel.
So previously in our template (html) file under Tapestry 3 we would have something like this:
<select jwcid="@PropertySelection"
value="ognl:shiptoData.state"
model="ognl:stateSelectModel"/>
It only looks a little simpler for Tapestry 5:
<select t:type="Select"
value="shiptoData.state"
model="StateSelectModel"/>
The class declaration changes from implementing an interface to extending an abstract class definition:
Tapestry3:
public class StateSelectModel implements IPropertySelectionModel {
Tapestry5:
public class StateSelectModel extends AbstractSelectModel {
Additional code for Tapestry 5:
Add a private property for the entire list if you don't already have one (make sure you include the OptionModel interface in your delcaration):
private List
Then add the following two functions to your class:
public List<OptionModel> getOptions() {
optionModelList = new ArrayList<OptionModel>();
for (int i=1; i <= getOptionCount(); i++) {
optionModelList.add(new OptionModelImpl(getLabel(i), getValue(i)));
}
return optionModelList;
}
public List<OptionGroupModel> getOptionGroups() {
return null;
}
That's all there is to it. Your SELECT drop-down will work. Note that the get functions used in that getOptions() function are ones already implemented for the IPropertySelectionModel interface in Tapestry 3.
But even if you are doing this from scratch you should be able to easily provide the return values for the "label" and "value" which are both String type.
The getOptionGroups() function is just returning a null. This is a simple drop down. To do groups is slightly more complicated. You would have to have a loop like the one in getOptions, but then another loop inside of that which built an OptionModel list.
Now if you look closely at this simple example you'll notice that the servlet ends up doing the same extra step as in Tapestry 3. I'm making a list (you can assume that anyway), then the function is reading it one option at a time and making a copy of that list which is then offerered to Tapestry Select component through the getOptions() function.
The reason for this is there is another class that I am getting the values from and that class is the same one that is being used for the JComboBox in the swing application.
Thursday, April 8, 2010
Passing paramters: activateExternalPage
Tapestry 5 doesn't seem to distinguish between a regular page and an external page. There is no BasePage class anymore and there are no special page interfaces like IExternal page.
Makes sense given the new architecture. It is simpler. Everything is easier than it used to be, it is just taking me longer than I expected to figure that out. I have a few more entries to make, but this one just came up and it's a quick one.
Every page has an "onActivate" function that you can override. Actually there are a lot of functions you can override, during each step in the process. Knowing exactly when these functions are called is essential to making your application interface work.
In Tapestry 3 you would have to declare your page something like this:
public abstract class Item extends BasePage implements IExternalPage, PageRenderListener {
Also in Tapestry 3 you'd have to implement "activateExtrenalPage" in order to
access the parsed query string:
public void activateExternalPage(Object[] params, IRequestCycle cycle) {
Then inside this function you can store values from the params array into class properties for later use. The query strings are pretty cryptic.
Now in Tapestry 5 it is just a straight up class:
public class Item {
You also need to inject your request identifier (giving you an interface to the usual HttpServletRequest data:
@Inject private Request _request;
Then add an onActivate function like this:
void onActivate() {
if (_request.getParameter("itemn") != null) {
_itemn = _request.getParameter("itemn");
}
}
Note that you can access parameters by readable string name. The query string parameters can then be in any order. Note that if the parameter is null that usually means it is not in the query string. You can do other things in this function, like plug in a default value, but keep in mind that onActivate gets called every time a page is either requested or a form submitted.
For more on these functions you can override take a look at the output on this page:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
These you implement by simply declaring a function named the same as these steps.
Also the Render phase can be broken down even more using annotations that represent the flow described in the chart on this page:
http://tapestry.apache.org/tapestry5/guide/rendering.html
The annotations available at this time are:
@SetupRender
@BeginRender
@BeforeRenderTemplate
@RenderTemplate
@BeforeRenderBody
@RenderBody
@AfterRenderBody
@AfterRenderTemplate
@AfterRender
@CleanupRender
Usage for example would look something like this:
@SetupRender void JimsToDoB4Render() {
ItemData itemData = new ItemData(_itemn);
_itemDescription = itemData.getDescription();
_itemPrice = itemData.getPrice();
}
Makes sense given the new architecture. It is simpler. Everything is easier than it used to be, it is just taking me longer than I expected to figure that out. I have a few more entries to make, but this one just came up and it's a quick one.
Every page has an "onActivate" function that you can override. Actually there are a lot of functions you can override, during each step in the process. Knowing exactly when these functions are called is essential to making your application interface work.
In Tapestry 3 you would have to declare your page something like this:
public abstract class Item extends BasePage implements IExternalPage, PageRenderListener {
Also in Tapestry 3 you'd have to implement "activateExtrenalPage" in order to
access the parsed query string:
public void activateExternalPage(Object[] params, IRequestCycle cycle) {
Then inside this function you can store values from the params array into class properties for later use. The query strings are pretty cryptic.
Now in Tapestry 5 it is just a straight up class:
public class Item {
You also need to inject your request identifier (giving you an interface to the usual HttpServletRequest data:
@Inject private Request _request;
Then add an onActivate function like this:
void onActivate() {
if (_request.getParameter("itemn") != null) {
_itemn = _request.getParameter("itemn");
}
}
Note that you can access parameters by readable string name. The query string parameters can then be in any order. Note that if the parameter is null that usually means it is not in the query string. You can do other things in this function, like plug in a default value, but keep in mind that onActivate gets called every time a page is either requested or a form submitted.
For more on these functions you can override take a look at the output on this page:
http://jumpstart.doublenegative.com.au:8080/jumpstart/examples/navigation/whatiscalledandwhen
These you implement by simply declaring a function named the same as these steps.
Also the Render phase can be broken down even more using annotations that represent the flow described in the chart on this page:
http://tapestry.apache.org/tapestry5/guide/rendering.html
The annotations available at this time are:
@SetupRender
@BeginRender
@BeforeRenderTemplate
@RenderTemplate
@BeforeRenderBody
@RenderBody
@AfterRenderBody
@AfterRenderTemplate
@AfterRender
@CleanupRender
Usage for example would look something like this:
@SetupRender void JimsToDoB4Render() {
ItemData itemData = new ItemData(_itemn);
_itemDescription = itemData.getDescription();
_itemPrice = itemData.getPrice();
}
Monday, April 5, 2010
Tapestry 5 won't find or use my AppModule.java class
This one was dumb. It took me a more time than I'll ever admit to find that I mistyped the source package name for "services". You must place AppModule.java in the services package and the package name must be the main "tapestry.app-package" name (see web.xml) plus the word "services" (e.g. test.services).
I suppose if I had used Maven to set this up, it'd been done right already.
In case you didn't find the documentation, the class "AppModule" is just the filter name (capitalized) plus the word "Module". If you setup something else for your filter in web.xml then you need to use a different name for that class.
I suppose if I had used Maven to set this up, it'd been done right already.
In case you didn't find the documentation, the class "AppModule" is just the filter name (capitalized) plus the word "Module". If you setup something else for your filter in web.xml then you need to use a different name for that class.
What happend to @Insert? and Previewability
One of the benefits to the new architecture in Tapestry 5 is that inserting standard or custom components into a page has become a lot simpler.
For example to insert a field called "title" that is stored in an object called webItemData used to require the following code:
<span jwcid="@Insert" value="ognl:webItemData.title">Item Summary</span>
The text "Item Summary" would get replaced by the contents of the "title" property. You could also define it using xml in your .jwc or .page file and do without the "ognl" entry, but that was even more typing. Essentially you would be defining the property two times, in the class, and in xml.
In T5 all you need to insert this same property is this:
${webItemData.title}
The property is only defined in the java class, and you only need to make sure the class is instantiated in your page or component class. I'm finding that most of the work I need to do in conversion involves deleting excess code.
There is a downside. I can see that it is harder to get useful previewable templates. Go to JumpStart (or search for Tapestry Jumpstart and click on Demo if this link breaks). Here you will find a discussion of invisible instrumentation (can we just call it "aye-aye" or something?). It helps with previewability, a little bit.
Nobody comes right out and says you should always use this invisible instrumentation, but I think it'd be foolish not too. Even if in some cases it doesn't help previews, it will always make the template easier to read for someone who knows html.
For example to insert a field called "title" that is stored in an object called webItemData used to require the following code:
<span jwcid="@Insert" value="ognl:webItemData.title">Item Summary</span>
The text "Item Summary" would get replaced by the contents of the "title" property. You could also define it using xml in your .jwc or .page file and do without the "ognl" entry, but that was even more typing. Essentially you would be defining the property two times, in the class, and in xml.
In T5 all you need to insert this same property is this:
${webItemData.title}
The property is only defined in the java class, and you only need to make sure the class is instantiated in your page or component class. I'm finding that most of the work I need to do in conversion involves deleting excess code.
There is a downside. I can see that it is harder to get useful previewable templates. Go to JumpStart (or search for Tapestry Jumpstart and click on Demo if this link breaks). Here you will find a discussion of invisible instrumentation (can we just call it "aye-aye" or something?). It helps with previewability, a little bit.
Nobody comes right out and says you should always use this invisible instrumentation, but I think it'd be foolish not too. Even if in some cases it doesn't help previews, it will always make the template easier to read for someone who knows html.
Thursday, March 25, 2010
Embedded Javascript
At the time this entry was posted I was using Tapestry 5.1.0.5
The java script PopUp function wasn't working like it did under T3 so I started Googling for an answer. What I found was references to a "delegate" class. You are supposed to create a "block" () for the javascript text and then "delegate" it (with ).
I couldn't seem to figure this one out. You know I think the lack of Documentation effort in Tapestry was less of an issue when it used the interface model. But I'm keeping an open mind. Some things do seem easier (better?).
It seemed to me that the problem had to do with whitespace characters getting removed from the javascript. Line endings matter because...well it's not xml with close tags...it's javascript.
So I added the following property to my html tag: xml:space="preserve". No go. I messed with this for a good hour trying to determine why Tapestry was deleting the line ending right after the <!-- tag on the line above the first line of code. The browsers didn't like this:
<!-- function PopUp(page, size) {
In the end it became apparent that no matter how many line endings I put between the comment and the function lines, Tapestry was going to delete them and put them on one line. Even with that xml:space="preserve" thing T5 was still deleting those particular whitespace characters.
In the end I did get it working. All that was required was giving something for Tapestry to place after the <!-- characters. I added the characters // right after the <!--. This gave Tapestry something to screw with and since // marks the beginning of a comment limited to the remainder of the current line in javascript, it didn't have any affect on the script.
Here is what ended up working (Note the extra // slashes at the beginning):
<script type="text/javascript">
<!-- //
function PopUp(page, size) {
Npop = window_handle = window.open(page,'popupWindowName', size, 'status=no,toolbar=no,scrollbars=no');
}
//-->
</script>
As it turns out the xml:space="preserve" is not required. The script text doesn't get messed with by Tapestry except for that issue with the first line. I'm still going to leave the spaces in there for now so that I can more easily see what is being generated by T5.
The java script PopUp function wasn't working like it did under T3 so I started Googling for an answer. What I found was references to a "delegate" class. You are supposed to create a "block" (
I couldn't seem to figure this one out. You know I think the lack of Documentation effort in Tapestry was less of an issue when it used the interface model. But I'm keeping an open mind. Some things do seem easier (better?).
It seemed to me that the problem had to do with whitespace characters getting removed from the javascript. Line endings matter because...well it's not xml with close tags...it's javascript.
So I added the following property to my html tag: xml:space="preserve". No go. I messed with this for a good hour trying to determine why Tapestry was deleting the line ending right after the <!-- tag on the line above the first line of code. The browsers didn't like this:
<!-- function PopUp(page, size) {
In the end it became apparent that no matter how many line endings I put between the comment and the function lines, Tapestry was going to delete them and put them on one line. Even with that xml:space="preserve" thing T5 was still deleting those particular whitespace characters.
In the end I did get it working. All that was required was giving something for Tapestry to place after the <!-- characters. I added the characters // right after the <!--. This gave Tapestry something to screw with and since // marks the beginning of a comment limited to the remainder of the current line in javascript, it didn't have any affect on the script.
Here is what ended up working (Note the extra // slashes at the beginning):
<script type="text/javascript">
<!-- //
function PopUp(page, size) {
Npop = window_handle = window.open(page,'popupWindowName', size, 'status=no,toolbar=no,scrollbars=no');
}
//-->
</script>
As it turns out the xml:space="preserve" is not required. The script text doesn't get messed with by Tapestry except for that issue with the first line. I'm still going to leave the spaces in there for now so that I can more easily see what is being generated by T5.
Monday, March 22, 2010
Exception Page Not Showing Detail
After doing the basic setup for the project as described earlier, I noticed that the default exception page lacked the detail normally included in tapestry. I could still get it from the Netbeans log window, but that's pretty cumbersome because it just isn't laid out as nice.
It turns out that Tapestry 5 defaults to "production-mode" in order to hide all the excess information from users. Only a summary message is shown.
The easiest way to ensure that production-mode is off when testing in Netbeans and when deployed to your server is to specify a flag in the VM settings of your server. Note that putting this into the VM settings of the project doesn't work reliably.
In Netbeans, under Services, Servers right click on Apache Tomcat 6.0.xx and select the Platform tab and in the VM Options box enter: -Dtapestry.production-mode=false
Restart the sevice and you should be golden.
To make the setting at the application level or so the production mode is off while deployed on a server (while beta testing for example), edit the web.xml and add the following lines:
<context-param>
<param-name>tapestry.production-mode</param-name>
<param-value>false</param-value>
</context-param>
Just make sure to go back later and edit the web.xml and remove or change this to true (the default is true) if you want to deploy on a live server without the exception detail.
You can also define tapestry.exception-report-page (in web.xml) and program your own exception page in which you might be able to do something like Cold Fusion does, and specify that error detail only gets displayed to certain client IP addresses, which is handy if you want to diagnose a problem in something that's already running on a live server. Primarily this custom exception page function is intended to be used for displaying a user friendly error page, but you can do pretty much anything you want.
It turns out that Tapestry 5 defaults to "production-mode" in order to hide all the excess information from users. Only a summary message is shown.
The easiest way to ensure that production-mode is off when testing in Netbeans and when deployed to your server is to specify a flag in the VM settings of your server. Note that putting this into the VM settings of the project doesn't work reliably.
In Netbeans, under Services, Servers right click on Apache Tomcat 6.0.xx and select the Platform tab and in the VM Options box enter: -Dtapestry.production-mode=false
Restart the sevice and you should be golden.
To make the setting at the application level or so the production mode is off while deployed on a server (while beta testing for example), edit the web.xml and add the following lines:
<context-param>
<param-name>tapestry.production-mode</param-name>
<param-value>false</param-value>
</context-param>
Just make sure to go back later and edit the web.xml and remove or change this to true (the default is true) if you want to deploy on a live server without the exception detail.
You can also define tapestry.exception-report-page (in web.xml) and program your own exception page in which you might be able to do something like Cold Fusion does, and specify that error detail only gets displayed to certain client IP addresses, which is handy if you want to diagnose a problem in something that's already running on a live server. Primarily this custom exception page function is intended to be used for displaying a user friendly error page, but you can do pretty much anything you want.
Thursday, March 18, 2010
Undeclared namespace prefix "p"
First time I used the if component as documented here like in this example:
<t:if test="loggedIn">
Welcome back, ${user.firstName}
<p:else>
<t:pagelink name="login">Login</t:pagelink> /
<t:pagelink name="register">Register</t:pagelink>
</p:else>
</t:if>
I got the followng exception:
Failure parsing template classpath... Undeclared namespace prefix "p" at....
on the tag I was using.
The problem? I didn't add xmlns:p="tapestry:parameter" to the html tag in the template file (look at the example).
<t:if test="loggedIn">
Welcome back, ${user.firstName}
<p:else>
<t:pagelink name="login">Login</t:pagelink> /
<t:pagelink name="register">Register</t:pagelink>
</p:else>
</t:if>
I got the followng exception:
Failure parsing template classpath... Undeclared namespace prefix "p" at....
on the
The problem? I didn't add xmlns:p="tapestry:parameter" to the html tag in the template file (look at the example).
Shell, CommonBorder or Border Components in T5
There is no Shell or Body component in Tapestry 5, but the newer method actually makes a lot more sense. Doing common borders was kind of screwy in Tapestry 3.
In t5 the "Layout" component "pattern" is used. See the basic idea here at http://tapestry.apache.org/tapestry5/guide/layout.html.
Various methods can be used to add images and style sheets including just coding simple "link" and "img" XHtml tags for static assets. Relative tags are not recommended for maximum component flexibility.
You will need to put BOTH the "tml" file and "java" file for components in the folder for the "appname.components" package. Otherwise you'll get nothing.
So far this "improved" approach looks good.
In t5 the "Layout" component "pattern" is used. See the basic idea here at http://tapestry.apache.org/tapestry5/guide/layout.html.
Various methods can be used to add images and style sheets including just coding simple "link" and "img" XHtml tags for static assets. Relative tags are not recommended for maximum component flexibility.
You will need to put BOTH the "tml" file and "java" file for components in the folder for the "appname.components" package. Otherwise you'll get nothing.
So far this "improved" approach looks good.
Tapestry 5 and Netbeans 6.8
There are a few pages on the internet about setting up Tapestry 5 on Netbeans 6.8.
I did manage to get it work with the Netbeans Maven plugin (as opposed to installing the maven package). I don't know much about Maven, but the whole repository thing sounds a bit too slick for me. At the scale of development I'm doing, there's no question it is overkill.
In order to set the new application I just did created a regular java web app. This is based on the "Tapestry 5, Netbeans 6.5" page at old.nabble.com. Using the regular Tapestry 5.0.5 download I created a library with the following jars in it:
antlr-runtime-3.1.1.jar
commons-codec-1.3.jar
javassist-3.9.0.GA.jar
log4j-1.2.14.jar
slf4j-api-1.5.2.jar
slf4j-log4j12-1.5.2.jar
stax2-api-3.0.1.jar
stax-api-1.0.1.jar
tapestry5-annotations-5.1.0.5.jar
tapestry-core-5.1.0.5.jar
tapestry-ioc-5.1.0.5.jar
woodstox-core-asi-4.0.3.jar
The application is called "test" in the following examples.
Setup your web.xml to look something like this (set the applicaton name):
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Test Application</display-name>
<context-param>
<param-name>tapestry.app-package</param-name>
<param-value>test</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
You might as well delete the index.jsp file.
In Web Pages folder, create an empty file called Start.tml:
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
<head>
<title>Start Page</title>
</head>
<body>
<h1>Start Page (Start.tml/Start.java)</h1>
<p> The current time is: ${currentTime}. </p>
<p>
[<t:pagelink t:page="Start">Refresh</t:pagelink>]
</p>
</body>
</html>
In the Source Packages folder create project called "test.pages". If you are going to do anything serious here, you might as well create a "test.components" package because you will need that to build any sort of application. Other locations can be used for other classes.
In "test.pages" create java class "Start.java":
package test.pages;
import java.util.Date;
public class Start {
public Date getCurrentTime() {
return new Date();
}
}
By the way these examples above would look better with indenting. I'm afraid I don't see a way to do this with blogspot. To bad you couldn't just wrap stuff in a pre tag.
Make sure you add the library with those jars listed above in it, then build and then run. If you didn't remove the index.jsp you will get a "Hello World". If not you'll get a demo screen showing the current time (so you know it is real :-)).
I did manage to get it work with the Netbeans Maven plugin (as opposed to installing the maven package). I don't know much about Maven, but the whole repository thing sounds a bit too slick for me. At the scale of development I'm doing, there's no question it is overkill.
In order to set the new application I just did created a regular java web app. This is based on the "Tapestry 5, Netbeans 6.5" page at old.nabble.com. Using the regular Tapestry 5.0.5 download I created a library with the following jars in it:
antlr-runtime-3.1.1.jar
commons-codec-1.3.jar
javassist-3.9.0.GA.jar
log4j-1.2.14.jar
slf4j-api-1.5.2.jar
slf4j-log4j12-1.5.2.jar
stax2-api-3.0.1.jar
stax-api-1.0.1.jar
tapestry5-annotations-5.1.0.5.jar
tapestry-core-5.1.0.5.jar
tapestry-ioc-5.1.0.5.jar
woodstox-core-asi-4.0.3.jar
The application is called "test" in the following examples.
Setup your web.xml to look something like this (set the applicaton name):
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Test Application</display-name>
<context-param>
<param-name>tapestry.app-package</param-name>
<param-value>test</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
You might as well delete the index.jsp file.
In Web Pages folder, create an empty file called Start.tml:
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
<head>
<title>Start Page</title>
</head>
<body>
<h1>Start Page (Start.tml/Start.java)</h1>
<p> The current time is: ${currentTime}. </p>
<p>
[<t:pagelink t:page="Start">Refresh</t:pagelink>]
</p>
</body>
</html>
In the Source Packages folder create project called "test.pages". If you are going to do anything serious here, you might as well create a "test.components" package because you will need that to build any sort of application. Other locations can be used for other classes.
In "test.pages" create java class "Start.java":
package test.pages;
import java.util.Date;
public class Start {
public Date getCurrentTime() {
return new Date();
}
}
By the way these examples above would look better with indenting. I'm afraid I don't see a way to do this with blogspot. To bad you couldn't just wrap stuff in a pre tag.
Make sure you add the library with those jars listed above in it, then build and then run. If you didn't remove the index.jsp you will get a "Hello World". If not you'll get a demo screen showing the current time (so you know it is real :-)).
Why Bother migrating to Tapestry 5?
I have one web application that's integrated with our internal customer service system and it came time to and upgrade to current supported releases of java, jasper, and tapestry. It didn't take long to discover that moving to Tapestry 5.1 from Tapestry 3.0 required a "whole rewrite" of the application.
Apparently T3 to T4 also required extensive code changes, although the scope is much smaller. So I decided that I wasn't going to go down this road (of rewriting the app every 3 years) and started looking into the myriad of java web frameworks available out there.
To make a long story short, I ended up back at Tapestry. I even considered going way back to Cold Fusion, but that didn't sound like it'd be much fun.
Howard was promising that this would be the last time a rewrite would be required. Backward compatibility was going to be there from version 5 on out. (Of course his use of "t:comp" in his #2 screencast is already obsolete!).
Thinking the whole development process through, it seemed that the way this particular application was put together, very little of the code was actually Tapestry specific. It shouldn't be all that difficult to take this small website, move the html files to templates and flesh out the classes behind them with a lot of the same code. Deciding to take a chance, I've begun the process of migrating.
Apparently T3 to T4 also required extensive code changes, although the scope is much smaller. So I decided that I wasn't going to go down this road (of rewriting the app every 3 years) and started looking into the myriad of java web frameworks available out there.
To make a long story short, I ended up back at Tapestry. I even considered going way back to Cold Fusion, but that didn't sound like it'd be much fun.
Howard was promising that this would be the last time a rewrite would be required. Backward compatibility was going to be there from version 5 on out. (Of course his use of "t:comp" in his #2 screencast is already obsolete!).
Thinking the whole development process through, it seemed that the way this particular application was put together, very little of the code was actually Tapestry specific. It shouldn't be all that difficult to take this small website, move the html files to templates and flesh out the classes behind them with a lot of the same code. Deciding to take a chance, I've begun the process of migrating.
Subscribe to:
Posts (Atom)