While most development still goes on in java 1.5, the push for java 1.6 is starting to gain steam. Apple had a java 1.6 developer preview for Tiger, but it has mysteriously disappeared for Leopard, leaving the only option an OpenJDK port.
To push for an official JDK 1.6, check out the voting page.
And here is the magic number:
13949712720901ForOSX
J
Saturday, December 15, 2007
Wednesday, June 27, 2007
Event Listener Oddities
I have a pretty straight forward paradigm:
- two groups of 3 radio buttons followed by a property selection.
- an async form with no listener whatsoever
- a submit button with an action listener (called saveTime)
The idea is that choosing a radio button narrows down the list in the property selection.
I chose to add eventlisteners to the onclicks of the radiobuttons, and update the property selection in response. The eventlisteners submit the above form, which is why there is no listener set in the form binding. That is the gist of the problem.
Here is my impl:
<html jwcid="@Shell" title="EventListener">
<body jwcid="@Body">
<div id="main">
<span jwcid="response@Any">
<span jwcid="message"/>
</span>
<form jwcid="addEventForm">
<div>
<label>Category</label>
<div jwcid="category" id="categoryRadioGroup">
<input type="radio" jwcid="@Radio" value="ognl:@...Category@VIDEO" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Video
<input type="radio" jwcid="@Radio" value="ognl:@...Category@PROJECT" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Project
<input type="radio" jwcid="@Radio" value="ognl:@...Category@OTHER" onclick="document.getElementById('categoryRadioGroup').clickRadio(this);"/> Other
</div>
</div>
<div>
<label>Stage</label>
<div jwcid="stage" id="stageRadioGroup">
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@PreProduction" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Pre-
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@Production" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Production
<input type="radio" jwcid="@Radio" value="ognl:@...Stage@PostProduction" onclick="document.getElementById('stageRadioGroup').clickRadio(this);"/> Post-
</div>
</div>
<div>
<label>Phase</label>
<select jwcid="phase">
<option>A-Roll</option>
<option>Programming</option>
</select>
</div>
<input jwcid="addEventButton" type="submit" value="Save Event"/>
</form>
</div>
</body>
</html>
<page-specification class="ca.ucalgary.tlc.eventlistener.presentation.Home">
<component id="addEventForm" type="Form">
<binding name="clientValidationEnabled" value="true"/>
<binding name="async" value="ognl:true"/>
</component>
<component id="addEventButton" type="Submit">
<binding name="action" value="listener:addEvent"/>
<binding name="async" value="ognl:true"/>
</component>
<component id="category" type="RadioGroup">
<binding name="displayName" value="literal:Category"/>
<binding name="selected" value="ognl:category"/>
<binding name="validators" value="validators:required"/>
</component>
<component id="stage" type="RadioGroup">
<binding name="displayName" value="literal:Stage"/>
<binding name="selected" value="ognl:stage"/>
<binding name="validators" value="validators:required"/>
</component>
<component id="phase" type="PropertySelection">
<binding name="displayName" value="literal:Phase"/>
<binding name="value" value="ognl:phase"/>
<binding name="model" value="ognl:phaseSelectionModel"/>
<binding name="validators" value="validators:required"/>
</component>
<component id="message" type="Insert">
<binding name="value" value="ognl:message"/>
</component>
</page-specification>
public abstract class Home extends BasePage {
private Log log = LogFactory.getLog(Home.class);
@InitialValue("ognl:null")
public abstract void setCategory(Category category);
public abstract Category getCategory();
@InitialValue("ognl:null")
public abstract void setStage(Stage stage);
public abstract Stage getStage();
@InitialValue("ognl:null")
public abstract void setPhase(Phase phase);
public abstract Phase getPhase();
@InitialValue("ognl:null")
public abstract String getMessage();
public abstract void setMessage(String message);
public IPropertySelectionModel getPhaseSelectionModel() {
Listphases = PhaseRule.getRelevantPhases(PhaseRule.getAllPhases(), getCategory(), getStage());
return new PhaseSelectionModel(phases);
}
@EventListener(events = "clickRadio", elements = "categoryRadioGroup", async = true, submitForm = "addEventForm", validateForm = false)
public void categorySelected(IRequestCycle cycle) {
log.info("Chose a category: " + getCategory());
cycle.getResponseBuilder().updateComponent("phase");
}
@EventListener(events = "clickRadio", elements = "stageRadioGroup", async = true, submitForm = "addEventForm", validateForm = false)
public void stageSelected(IRequestCycle cycle) {
log.info("Chose a stage: " + getStage());
cycle.getResponseBuilder().updateComponent("phase");
}
public void addEvent(IRequestCycle cycle)
{
log.info("Adding event.");
setMessage("Added event with phase = " + getPhase().shortString());
cycle.getResponseBuilder().updateComponent("response");
}
}
Now, this all works perfectly the first time through - click a radio button, watch the property selection values change, submit the form. Of particular interest, note that saveEvent is not called when you click a radio button - only categorySelected.
The second time around, however, the eventlisteners behave slightly differently. This time, when you click a radio button, the form is submitted, categorySelected is invoked _and_ saveEvent is called (which is a problem).
The primary difference between the two posts (at the request level) is the inclusion of the name of said button, which I believe is how Tapestry determines what listener to call (ie if &addEventButton=Save%20Event is included, then saveEvent is called - correct me if I'm wrong).
Note also the trick used to get the radio buttons to send an event, by adding the onclick handler.
You can download a sample application here: eventlistener.zip. It's mavenized, and antized, so it's pretty easy to get a working, editable app and running in a few minutes.
Wednesday, May 16, 2007
Creating a CRUD interface using widgets in tapestry
One thing that has now become possible is to create, update and delete items in your app model, all while never leaving the same page. A create can invoke a css-styled div, made to look like a dialog, which asks for a few field values, saves it and invokes an ajax response to update the master page. Same for edit, and for delete. The old way would have been to take you to a new page to do the create, or to pop up a full window and do the job with the aid of a bit of javascript.
I've been experimenting with the dojo dialog provided in tapestry 4.1.2 to do just this. It works very well. Now I want to add a little bit more intelligence to the form (in the dialog - let's call it the crud form). So for instance, if you choose an item from a selection (lets call it billableItem), other fields can react. In tapestry, the simplest way to do this is with the EventListener annotation. Rather than dreaming up some custom javascript to do it, you can do a little ajax request/response to update your form for you. Very cool.
There are a few challenges though. Since the crud form is itself asynchronous, and is told to update some components of the master page, we have a few problems when we try to get the information in our crud form to our app, so that the secondary fields can react. Obviously the billableItem selection needs to get to our app so that we can react, and this is typically done by submitting the form in question, while turning validation off and making sure it is asynchronous.
The problem is that when our EventListener is invoked, telling that crud form to submit itself so that we can react, it goes and updates the master page and validates on the server side (the crud form is configured to validate on the client side). So then we have validation errors on required fields and such, which is not what we want. So we can't really have our EventListener submit this form, because that form needs to be configured the way it is.
So what can we do to add our intelligence, short of developing some custom javascript as per usual?
I've been experimenting with the dojo dialog provided in tapestry 4.1.2 to do just this. It works very well. Now I want to add a little bit more intelligence to the form (in the dialog - let's call it the crud form). So for instance, if you choose an item from a selection (lets call it billableItem), other fields can react. In tapestry, the simplest way to do this is with the EventListener annotation. Rather than dreaming up some custom javascript to do it, you can do a little ajax request/response to update your form for you. Very cool.
There are a few challenges though. Since the crud form is itself asynchronous, and is told to update some components of the master page, we have a few problems when we try to get the information in our crud form to our app, so that the secondary fields can react. Obviously the billableItem selection needs to get to our app so that we can react, and this is typically done by submitting the form in question, while turning validation off and making sure it is asynchronous.
<component id="addBillableItemEventForm" type="Form">
<binding name="delegate" value="bean:delegate"/>
<binding name="clientValidationEnabled" value="true"/>
<binding name="success"
value="listener:addBillableItemEvent"/>
<binding name="async" value="ognl:true"/>
<binding name="updateComponents"
value="{'weekScheduler_billable_row'}"/>
</component>
<component id="billableItem" type="PropertySelection">
<binding name="displayName" value="literal:Billable Item"/>
<binding name="model"
value="ognl:billableItemSelectionModel"/>
<binding name="value" value="ognl:billableItem"/>
<binding name="validators" value="validators:required"/>
</component>
--
@EventListener(elements = "billableItem", events = "onchange",
submitForm = "addBillableItemEventForm",
async=true, validateForm = false)
The problem is that when our EventListener is invoked, telling that crud form to submit itself so that we can react, it goes and updates the master page and validates on the server side (the crud form is configured to validate on the client side). So then we have validation errors on required fields and such, which is not what we want. So we can't really have our EventListener submit this form, because that form needs to be configured the way it is.
@EventListener(elements = "billableItem", events = "onchange")
So what can we do to add our intelligence, short of developing some custom javascript as per usual?
Thursday, May 3, 2007
Hibernate and link tables
I'm struggling with hibernate and link tables. What I want is a fairly standard link table, to enable a unidirectional many-to-many relationship. While it's fairly trivial to set up in hibernate, there appear to be a few gotchas.
So here's what I have:
AccessLevel
Privilege
AccessLevel_Privilege
An AccessLevel has a collection of Privilege, which can be shared among AccessLevel (and other classes, ultimately). In other words, a Privilege also has a collection of AccessLevel. A typical many-to-many relationship. It doesn't need to be bi-directional, though if that's what it takes to make it work, then okay.
This is the same as what they describe in the hibernate docs, section 7.3.4.
So now if we create (in code) an AccessLevel, and add 3 Privileges to that AccessLevel, and then save, all is good. We get some sql like this:
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into AccessLevel (name, description, creationDate, modificationDate) values (?, ?, ?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Nice. Now if we delete that AccessLevel, regardless of any cascade specified, we get this:
Hibernate: delete from AccessLevel_Privilege where accessLevelId=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from AccessLevel where id=?
That's because the join table is made using foreign key constraints. I'm sure you can see the upcoming problem now.
So now make two AccessLevels, which share some of the same Privileges, persist them (all good up to here), and now delete one of them. This will fail with a foreign key constraint violation, because when you delete the AccessLevel, the corresponding relationships in AccessLevel_Privilege are deleted, and then the corresponding Privileges are deleted. The latter operation fails because those Privileges are foreign keys for other relationships!
Now this doesn't seem to me to be an unreasonable design, and is something I've used many times prior to using hibernate. You can partially fix the problem by just removing the foreign key constraints from the link table. Unfortunately you still get hibernate deleting all the privileges, even though you set cascade="none" on the privileges set in AccessLevel. So all we've done is allow the database to get out of sync (since now there are no Privileges for the other AccessLevel). It seems to me if you set cascade to none, it should only delete the AccessLevel. So continuing with this idea, I added an on-delete attribute to the set:
but alas, hibernate happily goes and deletes the privileges anyway, screwing up the database.
So I'm thinking this is not possible with hibernate. My next avenue of exploration is to use a mapped relationship with two bidirectional one-to-many associations.
So here's what I have:
AccessLevel
Privilege
AccessLevel_Privilege
An AccessLevel has a collection of Privilege, which can be shared among AccessLevel (and other classes, ultimately). In other words, a Privilege also has a collection of AccessLevel. A typical many-to-many relationship. It doesn't need to be bi-directional, though if that's what it takes to make it work, then okay.
<class name="AccessLevel" table="AccessLevel">
<id name="id" column="id">
<generator class="native"/>
</id>
<set name="privileges" table="AccessLevel_Privilege">
<key column="accessLevelId"/>
<many-to-many class="Privilege" column="privilegeId"/>
</set>
</class>
<class name="Privilege" table="Privilege">
<id name="id" column="id">
<generator class="native"/>
</id>
<property name="privilegeType"/>
<property name="permissions"/>
</class>
This is the same as what they describe in the hibernate docs, section 7.3.4.
So now if we create (in code) an AccessLevel, and add 3 Privileges to that AccessLevel, and then save, all is good. We get some sql like this:
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into Privilege (privilegeType, permissions) values (?, ?)
Hibernate: insert into AccessLevel (name, description, creationDate, modificationDate) values (?, ?, ?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Hibernate: insert into AccessLevel_Privilege (accessLevelId, privilegeId) values (?, ?)
Nice. Now if we delete that AccessLevel, regardless of any cascade specified, we get this:
Hibernate: delete from AccessLevel_Privilege where accessLevelId=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from Privilege where id=?
Hibernate: delete from AccessLevel where id=?
That's because the join table is made using foreign key constraints. I'm sure you can see the upcoming problem now.
So now make two AccessLevels, which share some of the same Privileges, persist them (all good up to here), and now delete one of them. This will fail with a foreign key constraint violation, because when you delete the AccessLevel, the corresponding relationships in AccessLevel_Privilege are deleted, and then the corresponding Privileges are deleted. The latter operation fails because those Privileges are foreign keys for other relationships!
Now this doesn't seem to me to be an unreasonable design, and is something I've used many times prior to using hibernate. You can partially fix the problem by just removing the foreign key constraints from the link table. Unfortunately you still get hibernate deleting all the privileges, even though you set cascade="none" on the privileges set in AccessLevel. So all we've done is allow the database to get out of sync (since now there are no Privileges for the other AccessLevel). It seems to me if you set cascade to none, it should only delete the AccessLevel. So continuing with this idea, I added an on-delete attribute to the set:
<set name="privileges" table="AccessLevel_Privilege" cascade="none">
<key column="accessLevelId" on-delete="noaction"/>
<many-to-many class="Privilege" column="privilegeId"/>
</set>
but alas, hibernate happily goes and deletes the privileges anyway, screwing up the database.
So I'm thinking this is not possible with hibernate. My next avenue of exploration is to use a mapped relationship with two bidirectional one-to-many associations.
Friday, March 2, 2007
A recipe for logging in your java webap
Logging in your web application is one of those things that should take 2 seconds to set up before you can just forget about it. You don't really want to invest a lot of time figuring out how it works. In many cases, it does work just like this, until it doesn't.
Recently, my log4j.properties has started to be ignored on my developer machine. Why? It could be many things - tomcat upgrades, log4j upgrades, commons-logging changes, library conflicts, classloader problems, etc. Because I work on many applications simultaneously, there are small differences between them, often with the newest apps using the newest dependencies, so logging is not necessarily identical across them. And of course, you know it's probably something simple, so often the best remedy is to keep working, and the solution will present itself at some point. But it hasn't, and it has become irksome to keep using the container configuration. So it was time to dig in and figure out what was going on.
That has finally led me to this excellent article:
http://minaret.biz/tips/tomcatLogging.html
All I've wanted for some time is an article telling me the preferred way to set up logging in your web app. Of course, it cannot be so simple.
The main thing to realize is that while your app is the producer of log information at various detail levels, it is not within its responsibility to decide what to do with that information. The target container should decide this.
Imagine you're in charge of the deployment server, and you have several apps running, all of which produce log information. All of a sudden you get this one app which insists on using its own log impl, writes it to some strange location, and writes a ton of stuff all over everywhere. Would you want to dig into the app and figure out how to configure it yourself, or just use your own "master" config to dictate its logging?
So you need to ask yourself, am I deploying my app to a common container, where there are multiple apps running? Furthermore, am I in control of the logging configuration?
We'll deal with the case where the answers to both these questions are yes. This assumes you can do whatever you want to your tomcat config, both on deployment and on developer machines. We will end up with a completely controlled logging environment in both developer and deployed scenarios.
So first, you need to follow the instructions in Geoff Mottram's article. I'll reiterate:
Now you should just have two rotating files in your logs directory. You can control all your apps' logging from that single log4j config, either in a mode suitable for development (ie log4j.logger.ca.ucalgary.commons.mt3=DEBUG) or deployment (ie log4j.logger.ca.ucalgary.commons.mt3=INFO). And because you're a responsible app developer, you have no sys.out's or sys.err's, but have logical messages sent to TRACE < DEBUG < INFO < WARN < ERROR < FATAL, so that they can be logged as needed.
If you need to send log messages to separate files, then you need to do what Geoff terms WEB-INF logging. Add a log4j.properties to your webapp/WEB-INF/classes and commons-logging-1.1.jar, log4j-1.2.14.jar to webapp/WEB-INF/lib. I haven't quite figured out this log4j.properties though.
This works fine, but log4j might complain about other appenders:
I'm not sure why the addition of an app-specific logger suddenly means that sax cannot find it's appender anymore.
Recently, my log4j.properties has started to be ignored on my developer machine. Why? It could be many things - tomcat upgrades, log4j upgrades, commons-logging changes, library conflicts, classloader problems, etc. Because I work on many applications simultaneously, there are small differences between them, often with the newest apps using the newest dependencies, so logging is not necessarily identical across them. And of course, you know it's probably something simple, so often the best remedy is to keep working, and the solution will present itself at some point. But it hasn't, and it has become irksome to keep using the container configuration. So it was time to dig in and figure out what was going on.
That has finally led me to this excellent article:
http://minaret.biz/tips/tomcatLogging.html
All I've wanted for some time is an article telling me the preferred way to set up logging in your web app. Of course, it cannot be so simple.
The main thing to realize is that while your app is the producer of log information at various detail levels, it is not within its responsibility to decide what to do with that information. The target container should decide this.
Imagine you're in charge of the deployment server, and you have several apps running, all of which produce log information. All of a sudden you get this one app which insists on using its own log impl, writes it to some strange location, and writes a ton of stuff all over everywhere. Would you want to dig into the app and figure out how to configure it yourself, or just use your own "master" config to dictate its logging?
So you need to ask yourself, am I deploying my app to a common container, where there are multiple apps running? Furthermore, am I in control of the logging configuration?
We'll deal with the case where the answers to both these questions are yes. This assumes you can do whatever you want to your tomcat config, both on deployment and on developer machines. We will end up with a completely controlled logging environment in both developer and deployed scenarios.
So first, you need to follow the instructions in Geoff Mottram's article. I'll reiterate:
- Install Tomcat (5.5.17), make sure it is not running before next step
- Obtain commons-logging.jar (1.1)
- Copy the commons-logging.jar file from the distribution into your Tomcat common/lib directory.
- Obtain log4j.jar (1.2.14)
- Copy the log4j.jar file from the distribution into your Tomcat common/lib directory.
- Create a log4j.properties file in your Tomcat common/classes directory (see Geoff's article).
- Remove the existing log4j.xml if it is there (or use that file if you're comfortable with the xml format for configuration).
- Remove logging.properties from conf (this produces catalina.out, and all the rotating admin, manager,host-manager, etc log files)
- For now, make sure there is no log4j config in your webapp, nor a log4j.jar, nor any commons-logging.jar - these jars are fine for compiling during dev, but make sure they don't get into the final webapp (war) distribution
- In your app, LogFactory.getLog(Hello.class).info("Hello!!!!!");
- Restart Tomcat.
Now you should just have two rotating files in your logs directory. You can control all your apps' logging from that single log4j config, either in a mode suitable for development (ie log4j.logger.ca.ucalgary.commons.mt3=DEBUG) or deployment (ie log4j.logger.ca.ucalgary.commons.mt3=INFO). And because you're a responsible app developer, you have no sys.out's or sys.err's, but have logical messages sent to TRACE < DEBUG < INFO < WARN < ERROR < FATAL, so that they can be logged as needed.
If you need to send log messages to separate files, then you need to do what Geoff terms WEB-INF logging. Add a log4j.properties to your webapp/WEB-INF/classes and commons-logging-1.1.jar, log4j-1.2.14.jar to webapp/WEB-INF/lib. I haven't quite figured out this log4j.properties though.
log4j.logger.ca.ucalgary.commons.mt3=DEBUG, R1
log4j.appender.R1=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R1.DatePattern='.'yyyy-MM-dd
log4j.appender.R1.File=/usr/local/tomcat/logs/mt3.log
log4j.appender.R1.layout=org.apache.log4j.PatternLayout
log4j.appender.R1.layout.ConversionPattern=%d %p %c - %m%n
This works fine, but log4j might complain about other appenders:
log4j:WARN No appenders could be found for logger (org.apache.commons.digester.Digester.sax).
log4j:WARN Please initialize the log4j system properly.
I'm not sure why the addition of an app-specific logger suddenly means that sax cannot find it's appender anymore.
Labels:
commons-logging,
java,
log4j,
logging,
tomcat,
web applications
Tapestry Case Study
I came across this interesting writeup today on a Fortune 100 company deciding to use Tapestry over the tried and true struts approach. It's a decision facing many developers, and it can be tough to justify your choice of a new and emerging technology. Read on for their experiences.
[flinksystems.com]
[flinksystems.com]
Monday, February 5, 2007
Application State Objects in Tapestry - changed?
I've noticed in 4.1.2-SNAPSHOT, that ASO handling has changed somewhat. From what I've read, the ASO should be created upon first request. That used to include OGNL accessors as well (<= 4.1.1), but now with the latest snapshot, you get an IllegalStateException if you don't access it prior to the OGNL accessor. One solution, which seems a hack, is to simply access the ASO from a PageBeginRenderListener.
Here's a test app which shows the problem. Uncomment the pageBeginRender hack in Home.java to see it work.
Update: Bug reported: http://issues.apache.org/jira/browse/TAPESTRY-1256
Here's a test app which shows the problem. Uncomment the pageBeginRender hack in Home.java to see it work.
Update: Bug reported: http://issues.apache.org/jira/browse/TAPESTRY-1256
Subscribe to:
Posts (Atom)