|
Java Persistence with Hibernate
with Gavin King November 2006 Manning Publications 841 pages (English), PDF ebook 703 pages (German) |
|
Hibernate in Action
with Gavin King August 2004 Manning Publications 408 pages (English), PDF ebook |
|
Unternehmen im Internet
with Ingo Petzke, Michael Mueller 1998 Oldenbourg 300 pages (German) |
04. Jan 2013, 17:47 CET, by Christian Bauer
After one year of no updates, the first cut of the next major version of Java's top
UPnP/DLNA library, Cling, is now available. You can
download the ZIP
or use 2.0-alpha1 in your Maven build.
What's new:
Dual-licensed under LGPL or CDDL, this should
more... |
07. Nov 2012, 15:21 CET, by Christian Bauer
XHTML has been one of my favorite text file formats for the last five years. This website uses it internally, try
the same HTTP request with an .xhtml extension. I'm currently writing
a book in my IDE in XHTML. All my Java unit tests have XHTML Javadoc, and I
more... |
02. Jun 2012, 19:40 CET, by Christian Bauer
Recently, as I was working on the new edition of Java Persistence with Hibernate,
one of my favorite issues made it again to the top of the list: How components in Java EE can share (or not share)
a JPA persistence context. Other names given to the same problem are
more... |
Archive starts here...
Finally had time to clean up and write down a few knowledge base articles for Seam. Some of these tricks have been very useful for building and running the Seam website. I still have leftovers, hopefully I can post another round of articles next week.
Here is the list for today, there should be something in there for anyone using Seam:
- Extending DBUnitSeamTest shows how you can extend the unit testing feature of Seam for mock data import. Examples are: adding support for PostgreSQL, writing custom DBUnit dataset operations (like calling a stored procedure before a test method runs) and configuration of DBUnit.
- Importing DBUnit datasets for development deployments explains how to use DBUnit during actual development, not only for unit testing. I found it very convenient to have the same mock data as I had available in unit testing imported automatically when deploying an application on my development machine. It helps you keeping interactive and automated testing in sync.
- Using MySQL in production with UTF8 wasn't as straightforward as I hoped it would be, I had to customize the Hibernate dialect, the JBoss AS datasource, and Tomcat. This recipe summarizes all the changes.
- Automatically extending session timeout for authenticated users is a good idea and if you also have anonymous and authenticated users on your Seam application, I recommend you implement the same pattern.
- Removing JSESSIONID from your URLs (and fixing s:cache) was a problem that actually only hit me in staging and I almost rolled it out without noticing. If you use the Seam HTML fragment cache, you need to read this. Unfortunately, there is nothing we can do in Seam to fix this.
- Drop-down boxes with entities and page scope describes my favorite JSF misfeature, or how the good intentions of the specification have been destroyed by a bad reference implementation. It's very hard to have drop-down box in JSF with a list of products or customers. Seam makes it easy out of the box if you can also use a long-running conversation context. My solution is a bit of a hack, but it makes it easy without the conversation context, just the page context.
Post improvements directly on the pages please, they are editable for all Seam community members.
Norman released Seam 2.1.2 yesterday and it comes with much improved support for REST processing, compared to previous 2.1.x versions. We started integrating RESTEasy - an implementation of JAX-RS (JSR 311) - with Seam almost a year ago in a first prototype. We then waited for the JAX-RS spec to be finalized and for RESTEasy to be GA, which happened a few months ago. So based on that stable foundation we were able to finish the integration with Seam.
I'm going to demonstrate some of the unique features of that integration here, how you can create a RESTful Seam application or simply add an HTTP web service interface to an existing one.
Deploying resources and providers
With JAX-RS you write a plain Java class and put @javax.ws.rs.Path("/customer") on it to make it available under the HTTP base URI path /customer. You then map methods of that class to particular sub-paths and HTTP methods with @javax.ws.rs.GET, @POST, @DELETE, and so on. These classes are called Resource classes. The default life cycle of an instance is per-HTTP-request, an instance is created for a request and destroyed when processing completes and the response has been sent.
Converting HTTP entities (the body of an HTTP request) is the job of Provider classes, annotated with @javax.ws.rs.ext.Provider and usually stateless or singleton. They transform content between HTTP and Java types, say my.Customer entity to and from XML with JAXB. Providers also are the extension point in JAX-RS for custom exception converters, etc.
RESTEasy has its own classpath scanning routine that detects all resources and providers by looking for annotations. That requires a servlet context listener configured in web.xml. You'd also have to configure a request dispatcher servlet. Finally, if you'd like to make your resource classes EJBs, for automatic transaction demarcation and persistence context handling, you'd have to list these EJBs in web.xml as well. This last feature is a RESTEasy enhancement and not part of the JAX-RS specification.
If you use Seam with RESTEasy, none of this extra work is necessary. Of course it still needs to be done but you most likely have already configured the basic Seam listener and resource servlet in web.xml - almost all Seam applications have.
You do not have to configure RESTEasy at all. Just drop in the right JAR files (see the reference docs) and your Seam application will automatically find all @Path resources and @Provider's. Your stateless EJBs still need to be listed to be found, but that can be done in Seam's components.xml or programmatically through the usual Seam APIs. All the other RESTEasy configuration options and some useful other configuration features are available as well.
So without changing any code, you get easier deployment and integrated configuration of JAX-RS artifacts in your Seam application.
Utilizing Seam components
Resources and providers can be made Seam components, with bijection, life cycle management, authorization, interception, etc. Just put an @Name on your resource class:
@Name("customerResource")
@Scope(ScopeType.EVENT) // Default
@Path("/customer")
public class MyCustomerResource {
@In
CustomerDAO customerDAO;
@GET
@Path("/{customerId}")
@Produces("text/xml")
@Restrict("#{s:hasRole('admin')}")
public Customer getCustomer(@PathParam("customerId") int id) {
return customerDAO.find(id);
}
}
Naturally REST-oriented architecture assumes that clients are maintaining application state, so your resource components would be EVENT or APPLICATION scoped, or STATELESS. Although SESSION scope is available, by default a session only spans a single HTTP request and it's automatically destroyed after the HTTP request. This behavior and how to configure it if you really want to transmit a session identifier between the REST client and server and utilize server-side SESSION scope across requests is explained in more detail in the reference docs. We already have some ideas for CONVERSATION scope integration, follow this design document fore more info.
Of course your resource Seam component doesn't have to be a POJO, you can also use @Stateless and turn it into an EJB. Another advantage here is that you do not have to list that EJB in components.xml or web.xml anymore as all Seam components are automatically found and registered according to their type.
The @Restrict annotation is just a regular Seam authorization check, currently you can configure Basic or Digest authentication as you'd for any other Seam application.
CRUD framework integration
Seam has a framework for building basic CRUD database applications quickly, you probably already have seen EntityHome and EntityQuery in other Seam examples. Jozef Hartinger built an extension that allows you to create a basic CRUD application with full HTTP/REST support in minutes. You can declare it through components.xml:
<framework:entity-home name="customerHome"
entity-class="my.Customer"
auto-create="true"/>
<framework:entity-query name="customerQuery"
ejbql="select c from Customer c" order="lastname"/>
<resteasy:resource-home path="/customer" name="resourceCustomerHome"
entity-home="#{customerHome}" entity-id-class="java.lang.Long"
media-types="application/xml application/json"/>
<resteasy:resource-query path="/customer" name="resourceCustomerQuery"
entity-query="#{customerQuery}" entity-class="my.Customer"
media-types="application/xml application/json"/>
You only have to create the my.Customer entity and you are ready to read from and write to the database through HTTP.
- A GET request to /customer?start=30&show=10 will execute the resourceCustomerQuery component and return a list of all customers with pagination, starting at row 30 with 10 rows in the result.
- You can GET, PUT, and DELETE a particular customer instance by sending HTTP requests to /customer/<customerId>.
- Sending a POST request to /customer creates a new customer entity instance and persists it.
Note that the <framework:...> mappings are part of the regular Seam CRUD framework with all the usual options such as query customization. The content will be transformed by the built-in RESTEasy providers for XML and JSON, for example. The XML transformation will use any JAXB bindings on your entity class.
You do not have to use XML configuration; as you'd with the Seam CRUD superclasses ResourceHome and ResourceQuery, you can write subclasses instead and configure the mapping with annotations.
There is a reason this CRUD framework feature is not documented in the current release: We are not sure the API will stay as it is. Consider this release as our proposal and we really need feedback on it, what works and what can be improved. Jozef also wrote a full-featured RESTful application with a jQuery based client for regular webbrowsers to demonstrate the CRUD framework. Have a look at the Tasks example in the Seam distribution. You can find more demo code and tests in the Restbay example which we use for general RESTEasy integration testing and demonstration.
Feature shortlist
I've only highlighted three of the main features of Seam and RESTEasy but there is more available and more to come:
Exceptions in JAX-RS applications are mapped to HTTP responses for clients with provider classes called ExceptionMapper. That can be much more work than it should, so you can also map exceptions in Seam's pages.xml declaratively, see docs.
You can write unit tests that pass mock HTTP request and response through Seam and RESTEasy, all with local calls not TCP sockets. We use them in the integration tests and so can you to test your application. See the reference docs.
There is already talk about MVC and REST
. What this all comes down to, at least from my standpoint, is that hypertext should drive the application state through linked resources (HATEOAS, Hypertext as the engine of application state). From a technical perspective, it simply means that we need more control over how the view
is rendered, not just marshaling dumb XML documents from Customer entities with JAXB defaults. We should render XHTML representations - which of course may include JAXB-rendered XML blobs in addition to links and forms - and be able to customize them with templates.
Facelets seems like a natural fit for this and we have a prototype for sending templated XHTML responses:
@GET
@Path("/customer/{id}")
@ProduceMime("application/xhtml+xml")
@FaceletsXhtmlResponse(
template = "/some/path/to/template/#{thisCanEvenBeEL}/foo.xhtml"
)
@Out(value = "currentCustomer", scope = ScopeType.EVENT)
public Customer getCustomer(@PathParam("id") String id) { ... }
This is just pseudo-code, this feature is not available in the release. It wouldn't be very useful as it is, because we don't know how to transform incoming HTTP requests with XHTML payload back into a Facelet view. It's not trivial to implement either and we'll probably wait for JSF2 before we finalize this. But it shows that providing a JSF-based human client interface and a RESTful HTTP web service interface in the same application might be a natural fit with the given technologies.
Next version?
The currently available RESTEasy version is still not GA, although it is a release candidate. There are also a few open issues with the integration code that we'd like to close, and we have to finalize the CRUD framework interface. This is all expected to happen in the Seam 2.2 releases.
More elaborate additional features such as conversation integration, representation templating, or additional authentication schemes are probably reserved for Seam3 as we might want to build on the new JSF2/JCDI standards as much as possible. Follow this wiki page for updates.
P.S. This book is an excellent starting point if you are wondering what this stuff is all about.
Update: I forgot to mention one important feature that some of you might like. You can annotate your Seam component (POJO or EJB) interface and not the bean class. For EJB Seam components, you actually have to annotate the local business interface.
@Path("/customer")
public interface MyCustomerResource {
@GET
@Path("/{customerId}")
@Produces("text/xml")
public Customer getCustomer(@PathParam("customerId") int id);
}
@Name("customerResource")
public class MyCustomerResourceBean implements MyCustomerResource {
@In
CustomerDAO customerDAO;
@Restrict("#{s:hasRole('admin')}")
public Customer getCustomer(int id) {
return customerDAO.find(id);
}
}
The RESTEasy project is an implementation of JAX-RS. I've just committed the docs for the first step of the integration into Seam. You need a nightly build of Seam 2.1 trunk (wait until tomorrow for updated docs in the nightly build) or better a current SVN trunk checkout.
Some of the integration highlights:
- No configuration files necessary, just drop the JARs into your classpath and deploy @Path annotated resources.
- Fully integrated RESTEeasy configuration as regular Seam infrastructure component.
- HTTP requests are served by Seam, no need for an external servlet.
- Resources and providers can be Seam components (JavaBean or EJB), with full Seam injection, lifecycle, interception, and so on.
We have some other items on the TODO list, see this wiki page. If you have any ideas or suggestions you'd like to see for that integration, just edit the wiki page.
A robust web application should not crash and die when the session times out. I guess we can all agree on that, but thanks to the stateless nature of HTTP and the usual hacks attaching session state onto that protocol, this is quite difficult to accomplish. Just search for session timeout
on Google. So here I am with my JSF/Seam/Ajax4JSF/jQuery application, trying to make it more robust.
Session timeout happens on the server
Well, duh. The HTTP session is stored on the server in some fashion. Of course you could be one of the radicals who think that RIA means that the client keeps all the session state, or that you don't need no stinkin' session because you are all REST. Well, show me your app and we'll talk. You just have whitepapers to show? Too bad.
If you keep session state on the server, and naturally it's cleaned up and timing out on the server-side only, your client doesn't know anything about that. So if you want to do anything on the page your user is looking at when the session times out, you need to poll the server.
In my Seam app I need something to poll so I created the following component that runs on the server:
@Name("httpSessionChecker")
@Scope(ScopeType.APPLICATION)
public class HttpSessionChecker {
@WebRemote
public boolean isNewSession() {
return ServletContexts.instance().getRequest().getSession().isNew();
}
}
I know that this is extremely sophisticated. Here is the trick: When you ask the server if the session has timed out, you need to ask it if the current session it has is new. You have no other way to find out if the (previous) session timed out. Because your poll request will create a new session if the previous one timed out. So if you happen to check isNewSession() after the previous session timed out, you get a true result. (By the way, who had the braindead idea that Tomcat should re-use session identifiers? How incompetent can you be?)
Polling the server
At that point I still don't know what I want to do on the client when a new
session happens to be present on the server. Well, first I need to find out if that is the case by polling the server:
<script type="text/javascript"
src="#{wikiPreferences.baseUrl}/seam/resource/remoting/resource/remote.js"></script>
<script type="text/javascript"
src="#{wikiPreferences.baseUrl}/seam/resource/remoting/interface.js?httpSessionChecker"></script>
<script type="text/javascript">
var sessionChecker = Seam.Component.getInstance("httpSessionChecker");
var timeoutURL = '#{wiki:renderURL(wikiStart)}';
var timeoutMillis = '#{sessionTimeoutSeconds}'*1000+3000;
var sessionTimeoutInterval = null;
function startSessionTimeoutCheck() {
sessionTimeoutInterval = setInterval('sessionChecker.isNewSession(alertTimeout)', timeoutMillis);
}
function stopSessionTimeoutCheck() {
if (sessionTimeoutInterval) clearInterval(sessionTimeoutInterval);
}
function resetSessionTimeoutCheck() {
stopSessionTimeoutCheck();
startSessionTimeoutCheck();
}
function alertTimeout(newSession) {
if (newSession) {
clearInterval(sessionTimeoutInterval);
jQuery(".ajaxSupport")
.removeAttr('onblur')
.removeAttr('onchange')
.removeAttr('onkeyup')
.removeAttr('onclick');
jQuery(".sessionEventTrigger").hide();
var answer = confirm("#{messages['lacewiki.msg.SessionTimeout']}");
if (answer) window.location = timeoutURL;
}
}
</script>
First thing, I import the Seam Remoting JavaScript interfaces and the proxy interface of my server-side component, httpSessionChecker. The two main functions I define on the client are startSessionTimeoutCheck() and stopSessionTimeoutCheck(). The start function starts polling the server at an interval. This interval is not just some random millisecond value. I poll the server every sessionTimeoutSeconds plus 3 seconds (3000 milliseconds). So, if the session timeout configured on the server is 30 minutes, I poll it every 30 minutes and 3 seconds, unless somebody resets the interval by calling resetSessionTimeoutCheck(). This basically guarantees that, if no request happened while this browser window is sleeping, the server-side session will be gone when I poll again.
Those three methods (start, stop, reset) will be useful later when I integrate the session timeout checking with the rest of the user interface. Let's look at what I'm doing in the callback of repeated server poll, alertTimeOut(newSession):
- If the server says that there is no new session, I continue polling it with an unchanged interval.
- If the server says there is a new session, I stop polling by clearing the interval. Then I react to the new session by modifying the UI on the client side.
So from that point on, I'm handling a client callback and I can do whatever I want to react to the new session.
Adapting the client
My goal is to not just kick the user out when a session timeout occurs. I don't want to just redirect him to some start page, I want to provide a choice. But I also don't want him to do anything dangerous that could throw an exception after his session is gone.
The user can either click OK
or Cancel
on the confirmation dialog. If OK
is clicked, I redirect to the start page, otherwise he can stay on the current page. That is going to be a problem because the current page might have all kinds of widgets on it that really really require a session with some data when you click them.
So first, I disable anything that could be dangerous, using jQuery. The first jQuery statement removes all JavaScript events from any elements on the page that have CSS class of ajaxSupport. The second statement hides any elements on the page that have a CSS class of sessionEventTrigger. Elements that have class ajaxSupport are typically input fields that have onblur event triggers, or onclick links.
The user can continue copying stuff out of these input fields (to preserve data that would otherwise be lost) but he can not trigger an AJAX request anymore. He can also not click on any buttons or see any elements that might require a session.
So all of it comes really down to these two questions:
- Which pages require a session timeout check? Sometimes I just don't care about the server-side session because all actions on a given page might be safe, no matter if they run in the previous or a new HTTP session.
- Which elements on a page do I want to hide and what actions do I want to disable if the server-side session times out (if the user doesn't accept the redirect to the start page)?
If a page requires a session timeout check, I add the following code to its body:
<script type="text/javascript">startSessionTimeoutCheck();</script>
When the page is loaded, I start polling the server (with the given interval). If a new session is present on the server, all elements that are marked as styleClass="ajaxSupport" are stripped of their event handlers with jQuery. This would typically be some input field on a form on the page:
<s:decorate id="userNameDecorate" template="formFieldDecorate.xhtml">
<ui:define name="label">#{messages['lacewiki.label.commentForm.Name']}</ui:define>
<h:inputText styleClass="ajaxSupport" tabindex="1" size="40" maxlength="100" required="true"
id="userName" value="#{commentHome.instance.fromUserName}">
<a:support status="commentForm:status" event="onblur"
reRender="userNameDecorate" oncomplete="onAjaxRequestComplete()"/>
</h:inputText>
</s:decorate>
If a new session is present on the server, the confirmation dialog appears. If the user clicks Cancel
, the onblur event on that input field will be disabled. The user can still copy and rescue the value he typed in.
All elements of class sessionEventTrigger will be hidden at that point. These are typically buttons such as Save
, Update
, etc. Here is an example:
<a:commandLink id="post"
action="#{commentHome.persist}" tabindex="1"
reRender="commentDisplayForm, messageBoxContainer"
accesskey="#{messages['lacewiki.button.commentForm.Post.accesskey']}"
status="commentForm:status"
eventsQueue="ajaxEventQueue"
oncomplete="onAjaxRequestComplete()"
styleClass="button sessionEventTrigger">
<h:outputText escape="false" styleClass="buttonLabel"
value="#{messages['lacewiki.button.commentForm.Post']}"/>
</a:commandLink>
Just a coincidence, this is a button that doesn't trigger page navigation put a partial page-rendering after completion of the AJAX request. Now, that is an additional complication we need to take into account.
Dealing with AJAX requests
If you request a page and the session poll interval starts running when the page is rendered, everything is fine. The poll interval will be /session timeout/ + 3 seconds (that's just a safety margin). So, while you are focused on that page and working on it, no request is send to the server and both client and server keep counting the seconds. The server times out after /session timeout/ and 3 seconds later the client asks it if there is a new session present. The server should answer /yes/.
AJAX changes that picture completely. A page that contains AJAX actions supports, by definition, partial re-rendering of that page. So while your session poll interval for the page starts running on the client, you make AJAX requests to the server (trigger onblur events, clicking AJAX buttons, etc.). The client poll interval and the server session timeout are now out of sync. So after every AJAX request completes, you have to reset the client poll interval. That is the job of the oncomplete="onAjaxRequestComplete()" callback you have seen on the code snippets above.
Here is what that JavaScript function does:
function onAjaxRequestComplete() {
resetSessionTimeoutCheck();
}
I guess you could call the reset directly but I like the indirection. Sometimes I need other things to be done after an AJAX request completes (like, apply some CSS styles to the re-rendered parts of the page). I actually would really like to have a default callback for all Ajax4JSF events.
Finally, I can use the start/stop functions of the session check polling conditionally. Let's say I have a page that doesn't require any session checking, all actions and links are perfectly safe to use with an old or new session.
Except that this page might also, conditionally, include a form with plenty of AJAX magic. So I need to enable session state polling when the form is included and shown, and disable it when the form is not shown. Basically, I need to start and stop polling conditionally on the same page. Easy enough:
<s:fragment rendered="#{commentHome.showForm}">
<script type="text/javascript">startSessionTimeoutCheck();</script>
</s:fragment>
And the opposite, which would (probably) be the default condition when the page is loaded first time:
<s:fragment rendered="#{not commentHome.showForm}">
<script type="text/javascript">stopSessionTimeoutCheck();</script>
</s:fragment>
It's fine to stop the polling even if it didn't start, the JS function is safe.
Well, that's about it and I hope we can roll some of that back into the Seam and Ajax4JSF codebase to make it easier.
Seam offers some basic infrastructure for CAPTCHA creation and validation, so all you have to do if you want to add CAPTCHA validation to a form is add a single form field and show the picture with <h:graphicImage/>. The only built-in implementation we shipped with Seam 1.x and 2.0 was based on JCaptcha, but you could easily extend it and do your own question/answer thing. This is actually what I did and you can see my simplified math question CAPTCHA if you try to post a comment to this entry.
I'm not the only Seam user who had problems with JCaptcha (search the Seam forum if you want to know more, basically: it's over-engineered, needs seconds to startup, sometimes needs seconds to render an image, the default image generators are hard to read but CAPTCHAs are still easy to break, etc). So Gavin wrote a new Captcha implementation we could ship with Seam 2.0.1. This is in Seam CVS now and not released, so ignore this blog entry if you are not a Seam CVS user and come back to it when we release 2.0.1.
The usage scenario is still the same, add an input field and a picture to your form:
<s:validateAll>
<h:inputText size="6" maxlength="6" required="true" id="verifyCaptcha" value="#{captcha.response}"/>
<h:graphicImage value="/seam/resource/captcha"/>
</s:validateAll>
By default Gavins implementation only renders a simple math question as an image with no obfuscation at all. So I extended the built-in classes. This is how my generated CAPTCHA questions look like:
The trick is to tell the user to ignore any circles
- and to never use any characters that look like circles (zero, o, O). I don't think people will have issues deciphering these characters, I've been trying myself a few dozen times during testing and failed only once or twice. I think that this CAPTCHA is quite difficult to break automatically though, the grey shades used for obfuscation and real text are the same and circles really destroy the original shape of the characters. And if it's broken, we can simply add more aggressive circles in small incremental steps or increase the rotation range of the characters.
Another thing that makes CAPTCHAs less painful is to store the data in the HTTP session, so that if a user entered a captcha once on your site, you don't require it to be entered a second time. But that's all built-in with the new Seam Captcha stuff.
Here is my custom code for the image generation:
package org.jboss.seam.wiki.core.captcha;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.captcha.Captcha;
import org.jboss.seam.captcha.CaptchaResponse;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
@Name("org.jboss.seam.captcha.captcha")
@Scope(ScopeType.SESSION)
@Install(precedence = Install.APPLICATION)
public class WikiCaptcha extends Captcha {
Color backgroundColor = new Color(0xf5,0xf5, 0xf5);
Font textFont = new Font("Arial", Font.PLAIN, 25);
int charsToPrint = 6;
int width = 120;
int height = 25;
int circlesToDraw = 4;
float horizMargin = 20.0f;
double rotationRange = 0.2;
String elegibleChars = "ABDEFGHJKLMRSTUVWXYabdefhjkmnrstuvwxy23456789";
char[] chars = elegibleChars.toCharArray();
@Override
@Create
public void init() {
super.init();
StringBuffer finalString = new StringBuffer();
for (int i = 0; i < charsToPrint; i++) {
double randomValue = Math.random();
int randomIndex = (int) Math.round(randomValue * (chars.length - 1));
char characterToShow = chars[randomIndex];
finalString.append(characterToShow);
}
setChallenge(finalString.toString());
setCorrectResponse(finalString.toString());
}
@Override
public BufferedImage renderChallenge() {
// Background
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) bufferedImage.getGraphics();
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);
// Some obfuscation circles
for (int i = 0; i < circlesToDraw; i++) {
int circleColor = 80 + (int)(Math.random() * 70);
float circleLinewidth = 0.3f + (float)(Math.random());
g.setColor(new Color(circleColor, circleColor, circleColor));
g.setStroke(new BasicStroke(circleLinewidth));
int circleRadius = (int) (Math.random() * height / 2.0);
int circleX = (int) (Math.random() * width - circleRadius);
int circleY = (int) (Math.random() * height - circleRadius);
g.drawOval(circleX, circleY, circleRadius * 2, circleRadius * 2);
}
// Text
g.setFont(textFont);
FontMetrics fontMetrics = g.getFontMetrics();
int maxAdvance = fontMetrics.getMaxAdvance();
int fontHeight = fontMetrics.getHeight();
float spaceForLetters = -horizMargin * 2 + width;
float spacePerChar = spaceForLetters / (charsToPrint - 1.0f);
char[] allChars = getChallenge().toCharArray();
for (int i = 0; i < allChars.length; i++ ) {
char charToPrint = allChars[i];
int charWidth = fontMetrics.charWidth(charToPrint);
int charDim = Math.max(maxAdvance, fontHeight);
int halfCharDim = (charDim / 2);
BufferedImage charImage = new BufferedImage(charDim, charDim, BufferedImage.TYPE_INT_ARGB);
Graphics2D charGraphics = charImage.createGraphics();
charGraphics.translate(halfCharDim, halfCharDim);
double angle = (Math.random() - 0.5) * rotationRange;
charGraphics.transform(AffineTransform.getRotateInstance(angle));
charGraphics.translate(-halfCharDim, -halfCharDim);
int charColor = 60 + (int)(Math.random() * 90);
charGraphics.setColor(new Color(charColor, charColor, charColor));
charGraphics.setFont(textFont);
int charX = (int) (0.5 * charDim - 0.5 * charWidth);
charGraphics.drawString("" + charToPrint, charX, ((charDim - fontMetrics.getAscent())/2 + fontMetrics.getAscent()));
float x = horizMargin + spacePerChar * (i) - charDim / 2.0f;
int y = ((height - charDim) / 2);
g.drawImage(charImage, (int) x, y, charDim, charDim, null, null);
charGraphics.dispose();
}
g.dispose();
return bufferedImage;
}
@CaptchaResponse(message = "#{messages['lacewiki.label.VerificationError']}")
public String getResponse() {
return super.getResponse();
}
}
(If some of the code looks familiar, you have maybe seen it here before.)
I still need to finish a few other things before I can upgrade the software running this site to the new Captcha though. So if you want to try it out, get Seam CVS and put this class into your codebase, no other configuration necessary.
| Showing 1 to 5 of 6 blog entries tagged 'Seam' |
|
|