Rewrite to the Edge - Getting the Most out of it! On GlassFish!
Join the DZone community and get the full member experience.
Join For FreeA great topic for modern application development is rewriting. Since the
introduction of Java Server Faces and the new lightweight programming
model in Java EE 6 you are struggling with pretty and simple,
bookmarkable URLs. PrettyFaces was out there a while and even if it could be called mature at the 3.3.3 version, I wasn't convinced.
Getting Started
Nothing is easy as getting started with stuff coming from one of the RedHat guys. Fire up NetBeans, create a new Maven based Webapp, add JSF and Primefaces to the mix and run it on GlassFish.
First step for adding rewriting magic to your application is to add the rewrite dependencies to your project.
<dependency> <groupId>org.ocpsoft.rewrite</groupId> <artifactId>rewrite-servlet</artifactId> <version>1.1.0.Final</version> </dependency>
That isn't enough since I am going to use it together with JSF, you also need the jsf-integration.
<dependency> <groupId>org.ocpsoft.rewrite</groupId> <artifactId>rewrite-integration-faces</artifactId> <version>1.1.0.Final</version> </dependency>
Next implement your own ConfigurationProvider. This is the central piece where most of the magic happens.Let's call it TricksProvider for now and we also extend the abstract HttpConfigurationProvider. A simple first version looks like this:
public class TricksProvider extends HttpConfigurationProvider { @Override public int priority() { return 10; } @Override public Configuration getConfiguration(final ServletContext context) { return ConfigurationBuilder.begin() .addRule(Join.path("/").to("/welcomePrimefaces.xhtml")); } }
Now you have to register your ConfigurationProvider. You do this by
adding a simple textfile named
org.ocpsoft.rewrite.config.ConfigurationProvider to your applications
/META-INF/services/ folder. Add the fully qualified name of your
ConfigurationProvider implementation to it and you are done. If you fire
up your application.
The Rewriting Basics
While copying the above provider you implicitly added your first
rewriting rule. By requesting http://host:8080/yourapp/ you get directly
forwarded to the Primefaces welcome page generated by NetBeans. All
rules are based on the same principle. Every single rule consists of a
condition and an operation. Something like "If X happens, do Y". Rewrite
knows two different kinds of Rules. Some preconfigured ones (Join)
starting with "addRule()" and a fluent interface starting with
defineRule(). This is a bit confusing because the next major release
will deprecate the defineRule() and rename it to addRule(). So most the
examples you find (especially the test cases in the latest trunk) are
not working with the 1.1.0.Final.
Rewrite knows about two different Directions. Inbound and Outbound.
Inbound is most likely working like every rewriting engine you know
(e.g. mod_rewrite). A request arrives and is forwarded or redirected to
the resources defined in your rules. The Outbound direction is little
less. It basically has a hook in the encodeURL() method of the
HttpServletRequest and rewrites the links you have in your pages (if
they get rendered with the help of encodeURL at all). JSF is doing this
out of the box. If you are thinking to use it with JSPs you have to make
sure to call it yourself.
Forwarding .html to .xhtml with some magic
Let's look at some stuff you could do with rewrite. First we add the following to the TricksProvider:
.defineRule() .when(Direction.isInbound() .and(Path.matches("{name}.html").where("name").matches("[a-zA-Z/]+"))) .perform(Forward.to("{name}.xhtml"));
This is a rule which is looking at inbound requests and checks for all
Patch matches {name}.html which confirm to the regular expression
pattern [a-zA-Z/]+ and Forwards those to {name}.xhtml files.
If this rule is in place all requests
to http://host:8080/yourapp/something.html will end up being forwarded
to something.xhtml. Now your users will no longer know that you are
using fancy JSF stuff underneath and believe you are working with html
:) If a url which isn't matching the regular expression is requested,
for example something like http://host:8080/yourapp/something123.html
this simply isn't forwarded and if the something123.html isn't present
in your application you will end up receiving a 404 error.
Rewriting Outbound Links
The other way round you could also add the following rule:
.defineRule() .when(Path.matches("test.xhtml") .and(Direction.isOutbound())) .perform(Substitute.with("test.html"))
You imagine what this is doing, right? If you have a facelet which contains something like this:
<h:outputLink value="test.xhtml">Normal Test</h:outputLink>
The link that is rendered to the user will be rewritten to test.html.
This is the most basic action for outbound links you will ever need.
Most of the magic happens with inbound links. Not a big surprise looking
at the very limited reach of the encodeURL() hook.
The OutputBuffer
The most astonishing stuff in rewrite is called OutputBuffer. At least
until the release we are working with at the moment. It is going to be
renamed in 2.0 but for now let's simply look at what you could do. The
OutputBuffer is your hook to the response. Whatever you would like to do
with the response before it actually arrives at your client's browser
could be done here. Thinking about transforming the markup? Converting
css? Or even GZIP compression? Great, that is exactly what you could do.
Let's implement a simple ZipOutputBuffer
public class ZipOutputBuffer implements OutputBuffer { private final static Logger LOGGER = Logger.getLogger(ZipOutputBuffer.class.getName()); @Override public InputStream execute(InputStream input) { String contents = Streams.toString(input); LOGGER.log(Level.FINER, "Content {0} Length {1}", new Object[]{contents, contents.getBytes().length}); byte[] compressed = compress(contents); LOGGER.log(Level.FINER, "Length: {0}", compressed.length); return new ByteArrayInputStream(compressed); } public static byte[] compress(String string) { ByteArrayOutputStream os = new ByteArrayOutputStream(string.length()); byte[] compressed = null; try { try (GZIPOutputStream gos = new GZIPOutputStream(os)) { gos.write(string.getBytes()); } compressed = os.toByteArray(); os.close(); } catch (IOException iox) { LOGGER.log(Level.SEVERE, "Compression Failed: ", iox); } return compressed; } }
As you can see, I am messing around with some streams and use the java.util.zip.GZIPOutputStream to shrink the stream received in this method. Next we have to add the relevant rule to the TricksProvider:
.defineRule() .when(Path.matches("/gziptest").and(Direction.isInbound())) .perform(Forward.to("test.xhtml") .and(Response.withOutputBufferedBy(new ZipOutputBuffer()) .and(Response.addHeader("Content-Encoding", "gzip")) .and(Response.addHeader("Content-Type", "text/html"))))
An inbound rule (we are not willing to rewrite links in pages here .. so
it has to be inbound) which adds the ZipOutputBuffer to the Response.
Also take care for the additional response header (both) unless you want
to see your browser complaining about the content I have mixed up :)
That is it. The request http://host:8080/yourapp/gziptest now delivers
the test.xhtml with GZIP compression. That is 2,6KB vs. 1,23 KB!! Less
than half of the size !! It's not very convenient to work with streams
and byte[]. And I am not sure if this will work with larger page sizes
in terms of memory fragmentation, but it is an easy way out if you don't
have a compression filter in place or only need to compress single
parts of your application.
Enhance Security with Rewrite
But that is not all you could do: You could also enhance the security with rewrite. Lincoln has a great post up about securing your application with rewrite.
There are plenty of possible examples around how to use this. I Came up
with a single use-case where didn't want to use the welcome-file
features and prefer to dispatch users individually. While doing this I
would also inspect their paths and check if the stuff they are entering
is malicious or not. You could either do it with the .matches()
condition or with a custom constraint. Add the following to the
TricksProvider:
Constraint<String> selectedCharacters = new Constraint<String>() { @Override public boolean isSatisfiedBy(Rewrite event, EvaluationContext context, String value) { return value.matches("[a-zA-Z/]+"); } };
And define the following rule:
.defineRule() .when(Direction.isInbound() .and(Path.matches("{path}").where("path").matches("^(.+)/$") .and(Path.captureIn("checkChar").where("checkChar").constrainedBy(selectedCharacters)))) .perform(Redirect.permanent(context.getContextPath() + "{path}index.html"))
Another inbound modification. Checking the path if it is has a folder
pattern and capturing it in a variable which is checked against the
custom constraints. Great! Now you have a save and easy forwarding
mechanism in place. All http://host:8080/yourapp/folder/ request are now
rewritten to http://host:8080/yourapp/index.html. If you look at the
other rules from above you see, that the .html is forwarded to .xhtml
... and you are done!
Bottom Line
I like working with rewrite a lot. It feels easier than configuring the
xml files of prettyfaces and I truly enjoyed the support of Lincoln and Christian
during my first steps with it. I am curious to see what the 2.0 is
coming up with and I hope that I get some more debug output for the
rules configuration just to see what is happening. The default is
nothing and it could be very tricky to find the right combination of
conditions to have a working rule.
Looking for the complete sources? Find them on github. Happy to read about your experiences.
Where is the GlassFish Part?
Oh, yeah. I mentioned it in the headline, right? That should be more like a default. I was running everything with latest GlassFish 3.1.2.2 so you can be sure that this is working. And NetBeans is at 7.2
at the moment and you should give it a try if you haven't. I didn't
came across a single issue related to GlassFish and I am very pleased to
stress this here. Great work! One last remark: Before you are going to
implement the OutputBuffer like crazy take a look at what your favorite
appserver has in stock already. GlassFish knows about GZIP compression already and it simply can be switched on! Might be a good idea to think twice before implementing here.
Published at DZone with permission of Markus Eisele, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments