Skip to content

Instantly share code, notes, and snippets.

@mraible
Created March 7, 2011 23:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mraible/859534 to your computer and use it in GitHub Desktop.
Save mraible/859534 to your computer and use it in GitHub Desktop.
What it took to implement extensionless URL with Wicket.
Index: wicket/src/main/webapp/decorators/default.jsp
===================================================================
--- wicket/src/main/webapp/decorators/default.jsp (revision 242)
+++ wicket/src/main/webapp/decorators/default.jsp (revision )
@@ -69,7 +69,7 @@
<h2 class="accessibility">Navigation</h2>
<ul class="clearfix">
<li><a href="${ctx}/" title="Home"><span>Home</span></a></li>
- <li><a href="${ctx}/app/users" title="View Users"><span>Users</span></a></li>
+ <li><a href="${ctx}/users" title="View Users"><span>Users</span></a></li>
</ul>
</div>
</div><!-- end nav -->
Index: wicket/src/main/java/org/appfuse/web/rootmount/RawPathUtil.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/rootmount/RawPathUtil.java (revision )
+++ wicket/src/main/java/org/appfuse/web/rootmount/RawPathUtil.java (revision )
@@ -0,0 +1,51 @@
+package org.appfuse.web.rootmount;
+
+import org.apache.wicket.protocol.http.request.InvalidUrlException;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+/**
+ * Utilities to work with raw paths as given to {@link RootMountedUrlCodingStrategy#accepts(String)}.
+ * <p>
+ * Assumes that the servlet container is configured to work with UTF-8 URLs.
+ *
+ * @author Erik van Oosten
+ */
+public class RawPathUtil {
+
+ /**
+ * Decode and split the path into its path parts.
+ *
+ * @param rawPath the raw request path (can be empty or null)
+ * @return the path split on "/", or null when rawPath is empty or null
+ * @throws org.apache.wicket.protocol.http.request.InvalidUrlException when the URL could not be decoded
+ */
+ public static String[] splitToPathParts(String rawPath) {
+ if (rawPath == null || rawPath.length() == 0) {
+ return null;
+ }
+ try {
+ return URLDecoder.decode(rawPath, "UTF-8").split("/");
+ } catch (UnsupportedEncodingException e) {
+ throw new InvalidUrlException("Could not decode URL");
+ }
+ }
+
+ /**
+ * Decode and give the first path part.
+ *
+ * @param rawPath the raw request path (can be empty or null)
+ * @return the first path part or null when there is none
+ * @throws org.apache.wicket.protocol.http.request.InvalidUrlException when the URL could not be decoded
+ */
+ public static String firstPathPath(String rawPath) {
+ String[] pathParts = splitToPathParts(rawPath);
+ if (pathParts == null || pathParts.length == 0) {
+ return null;
+ }
+ String firstPathPart = pathParts[0];
+ return firstPathPart == null || firstPathPart.length() == 0 ? null : firstPathPart;
+ }
+
+}
Index: wicket/src/main/java/org/appfuse/web/Application.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/Application.java (revision 190)
+++ wicket/src/main/java/org/appfuse/web/Application.java (revision )
@@ -1,5 +1,7 @@
package org.appfuse.web;
+import org.apache.wicket.request.IRequestCycleProcessor;
+import org.appfuse.web.pages.DefaultUrlCodingStrategy;
import org.appfuse.web.pages.Index;
import org.appfuse.web.pages.UserForm;
import org.appfuse.web.pages.UserList;
@@ -7,8 +9,24 @@
import org.apache.wicket.settings.IRequestCycleSettings;
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
import org.apache.wicket.protocol.http.WebApplication;
+import org.appfuse.web.rootmount.RootMountedUrlCodingStrategy;
+import org.appfuse.web.rootmount.RootWebRequestProcessor;
+import java.util.ArrayList;
+import java.util.List;
+
public class Application extends WebApplication {
+
+ private List<RootMountedUrlCodingStrategy> rootMounts = new ArrayList<RootMountedUrlCodingStrategy>();
+
+ /**
+ * Constructor
+ */
+ public Application() {
+ // NOTE: there is no need to synchronize this list as it is written to from the init() method.
+ rootMounts = new ArrayList<RootMountedUrlCodingStrategy>(10);
+ }
+
@Override
public void init() {
super.init();
@@ -22,11 +40,35 @@
getMarkupSettings().setStripWicketTags(true);
// make bookmarkable pages for easy linking from Menu/SiteMesh
- mountBookmarkablePage("/users", UserList.class);
- mountBookmarkablePage("/userform", UserForm.class);
+ //mountBookmarkablePage("/index", Index.class);
+ mountOnRoot(new DefaultUrlCodingStrategy("/users", UserList.class));
+ mountOnRoot(new DefaultUrlCodingStrategy("/userform", UserForm.class));
}
public Class<Index> getHomePage() {
return Index.class;
}
+
+ /**
+ * Mount a target url strategy no the root URL.
+ *
+ * @param strategy the strategy (not null)
+ */
+ private void mountOnRoot(RootMountedUrlCodingStrategy strategy) {
+ rootMounts.add(strategy);
+
+ // Also add to the wicket mounts so that it can be found when searched by target page class.
+ mount(strategy);
-}
\ No newline at end of file
+ }
+
+ @Override
+ protected IRequestCycleProcessor newRequestCycleProcessor() {
+
+ return new RootWebRequestProcessor(rootMounts);
+
+ // Alternative that redirects all unknown requests to the not found page.
+ // Please read {@link RootWebRequestProcessor class comment} before you enable this.
+ //
+ // return new RootWebRequestProcessor(rootMounts, NotFoundPage.class);
+ }
+}
\ No newline at end of file
Index: wicket/src/main/java/org/appfuse/web/rootmount/RootMountedUrlCodingStrategy.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/rootmount/RootMountedUrlCodingStrategy.java (revision )
+++ wicket/src/main/java/org/appfuse/web/rootmount/RootMountedUrlCodingStrategy.java (revision )
@@ -0,0 +1,34 @@
+package org.appfuse.web.rootmount;
+
+import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
+
+/**
+ * A TargetUrlCodingStrategy that can be mounted on the root.
+ *
+ * <p>Every implementation of RootMountedUrlCodingStrategy MUST
+ * remove the mount path from the URL upon an encode.</p>
+ *
+ * @author Erik van Oosten
+ */
+public interface RootMountedUrlCodingStrategy extends IRequestTargetUrlCodingStrategy {
+
+ /**
+ * Determine whether the path is accepted by this URL coding strategy.
+ * <p>
+ * Note: for each request that is not handled by the regular wicket mounts, this method is
+ * called twice. Result caching is left to the implementation.
+ * <p>
+ * Note 2: there is no guarantee that this method is only invoked twice per request, there might be more.
+ * <p>
+ * Note 3: during the first invocation in a request,
+ * {@link org.apache.wicket.Application#get() Application.get()},
+ * {@link org.apache.wicket.Session#get() Session.get()} and
+ * {@link org.apache.wicket.RequestCycle#get() RequestCycle.get()} return null.
+ *
+ * @param rawPath the complete raw path (could be null or empty)
+ * (See also {@link RawPathUtil}.)
+ * @return true when the path is accepted by this URL coding strategy, false otherwise
+ */
+ boolean accepts(String rawPath);
+
+}
Index: wicket/src/main/java/org/appfuse/web/pages/DefaultUrlCodingStrategy.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/pages/DefaultUrlCodingStrategy.java (revision )
+++ wicket/src/main/java/org/appfuse/web/pages/DefaultUrlCodingStrategy.java (revision )
@@ -0,0 +1,26 @@
+package org.appfuse.web.pages;
+
+import org.apache.wicket.Page;
+import org.appfuse.web.rootmount.RootMountedBookmarkablePageRequestTargetUrlCodingStrategy;
+
+/**
+ * Url Strategy for allowing any page to be mounted as at the root context.
+ */
+public class DefaultUrlCodingStrategy extends RootMountedBookmarkablePageRequestTargetUrlCodingStrategy {
+
+ /**
+ * Constructor.
+ *
+ * @param mountPath the internal 'fake' mount path, its exact value does not matter as long as
+ * it is a unique mount path in the entire application (not null)
+ * @param bookmarkablePageClass type of target page (not null) See constructor of base class.
+ * @param <P> type of target page
+ */
+ public <P extends Page> DefaultUrlCodingStrategy(String mountPath, Class<P> bookmarkablePageClass) {
+ super(mountPath, bookmarkablePageClass, null);
+ }
+
+ public boolean accepts(String rawPath) {
+ return true;
+ }
+}
Index: wicket/src/main/java/org/appfuse/web/rootmount/RootWebRequestProcessor.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/rootmount/RootWebRequestProcessor.java (revision )
+++ wicket/src/main/java/org/appfuse/web/rootmount/RootWebRequestProcessor.java (revision )
@@ -0,0 +1,201 @@
+package org.appfuse.web.rootmount;
+
+import org.apache.wicket.Page;
+import org.apache.wicket.Request;
+import org.apache.wicket.protocol.http.request.urlcompressing.UrlCompressingWebCodingStrategy;
+import org.apache.wicket.protocol.http.request.urlcompressing.UrlCompressingWebRequestProcessor;
+import org.apache.wicket.request.IRequestCodingStrategy;
+import org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy;
+import org.apache.wicket.request.target.coding.QueryStringUrlCodingStrategy;
+import org.apache.wicket.util.string.Strings;
+
+import java.util.List;
+
+/**
+ * Url coding web request processor to catch parameters directly on the root mount path.
+ * For example used to support URLs of the form http://www.example.com/member.
+ *
+ * <p>To use root mounts, make sure your application overrides
+ * {@link org.apache.wicket.protocol.http.WebApplication#newRequestCycleProcessor}
+ * and returns an instance of this. Secondly, these root mounts <strong>MUST</strong> be mounted directly via
+ * {@link org.apache.wicket.protocol.http.WebApplication#mount(org.apache.wicket.request.target.coding.IRequestTargetUrlCodingStrategy)}.
+ * For example use {@link org.appfuse.web.Application#mountOnRoot(RootMountedUrlCodingStrategy)}.
+ *
+ * <p>In addition you may want to configure ignorePaths as described below.
+ *
+ * <p>The following request handlers are considered in order:
+ * <ol>
+ * <li>As in Wicket's normal request handling: all normal page mounts (created with the
+ * WebApplication#mount* methods), mounted resources, call backs to Link's, ajax requests, etc.
+ * <li>Pages mounted as given to the constructor of this. They are tried in the order as given.
+ * <li>The not-found page (when not null).
+ * <li>Fall back to the servlet container to handle non-Wicket content (this includes servlets configured
+ * in web.xml and all static content in the WEB-INF folder).
+ * </ol>
+ *
+ * <p>To speed up URL processing, it is <strong>RECOMMENDED</strong> to move all non-Wicket content (i.e. servlets
+ * and static content in the WEB-INF directory) behind a clear URL path and let the Wicket filter ignore these URLs.
+ * You <strong>MUST</strong> do this if you want to combine non-Wicket content with the optional not-found page.
+ *
+ * <p>Here is an example fragment of the <code>ignorePath</code> parameter that makes the Wicket filter ignore all
+ * request for <code>/favicon.ico</code>, <code>/robots.txt</code> and requests of which the path starts with
+ * <code>/static/</code> and <code>/servletpath/</code>:
+ * <pre>
+ * &lt;!-- The Wicket application filter. --&gt;
+ * &lt;filter&gt;
+ * &lt;filter-name&gt;wicket.filter&lt;/filter-name&gt;
+ * &lt;filter-class&gt;org.apache.wicket.protocol.http.WicketFilter&lt;/filter-class&gt;
+ * &lt;init-param&gt;
+ * &lt;param-name&gt;applicationFactoryClassName&lt;/param-name&gt;
+ * &lt;param-value&gt;org.apache.wicket.spring.SpringWebApplicationFactory&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * &lt;init-param&gt;
+ * &lt;param-name&gt;ignorePaths&lt;/param-name&gt;
+ * &lt;param-value&gt;favicon.ico,robots.txt,static/,servletpath/&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * &lt;/filter&gt;
+ * </pre>
+ * You can find this XML in your WEB.xml. (The example XML above uses Spring to instantiate the application class.)
+ *
+ * <p>See class comment of {@link org.apache.wicket.protocol.http.request.urlcompressing.UrlCompressingWebRequestProcessor} for more information on the
+ * purpose of the base class. Alternatively, this could extend
+ * {@link org.apache.wicket.protocol.http.WebRequestCycleProcessor}.
+ *
+ * @author Erik van Oosten
+ */
+public class RootWebRequestProcessor extends UrlCompressingWebRequestProcessor {
+
+ private List<RootMountedUrlCodingStrategy> rootMounts;
+ private Class<? extends Page> notFoundPage;
+
+ /**
+ * Constructor.
+ *
+ * @param rootMounts a list of root mounts in the order in which they are tried (not null)
+ * The list must be immutable or an implementation of something like
+ * {@link java.util.concurrent.CopyOnWriteArrayList}.
+ */
+ public <P extends Page> RootWebRequestProcessor(List<RootMountedUrlCodingStrategy> rootMounts) {
+ this(rootMounts, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param rootMounts a list of root mounts in the order in which they are tried (not null)
+ * The list must be immutable or an implementation of something like
+ * {@link java.util.concurrent.CopyOnWriteArrayList}.
+ * @param notFoundPage the page to forward to when none of the root mounts accepts the current request, or null
+ * to fall through to the servlet container. When this value is non-null, and you have non-wicket content,
+ * you <strong>MUST</strong> configure ignorePaths in the web.xml as described in the
+ * {@link org.appfuse.web.rootmount.RootWebRequestProcessor class comment} (for performance this is recommended anyway).
+ */
+ public <P extends Page> RootWebRequestProcessor(List<RootMountedUrlCodingStrategy> rootMounts, Class<P> notFoundPage) {
+ this.rootMounts = rootMounts;
+ this.notFoundPage = notFoundPage;
+ }
+
+ @Override
+ protected IRequestCodingStrategy newRequestCodingStrategy() {
+ return new FallbackUrlRequestCodingStrategy();
+ }
+
+ /**
+ * Url coding web strategy to catch parameters directly on the root mount path.
+ * Forwards all unmounted requests to the FallbackUrlPage.
+ */
+ private class FallbackUrlRequestCodingStrategy extends UrlCompressingWebCodingStrategy {
+
+ /**
+ * During a simple request cycle this method is invoked twice:
+ * 1. by Wicket's filter to determine whether we have a request that needs
+ * to be handled by Wicket at all (returning a non-null value is sufficient)
+ * (This step is skipped for resources and the home page.)
+ * 2. by Wicket's request handling (from WebRequestCycle) to actually start
+ * processing the request. In this second invocation the 'fake' mount path
+ * is present in the path argument, therefore Wicket is able to get the
+ * correct url coding strategy (in the call to the super class), and there
+ * is no need to call {@link #findRootMount(String)} again.
+ * <p>
+ * This is also invoked during form processing and from mock requests (WicketTester).
+ *
+ * @param path the relative path (the requested uri, minus query string,
+ * context path, and filterPath. Relative, no leading '/'.
+ * @return the url coding strategy to use or null when this is not a Wicket request
+ */
+ @Override
+ public IRequestTargetUrlCodingStrategy urlCodingStrategyForPath(String path) {
+ IRequestTargetUrlCodingStrategy strategy = super.urlCodingStrategyForPath(path);
+
+ // Wicket could not find a (mounted) target for the given path, lets see if it is
+ // mounted on the root path.
+ if (strategy == null && !Strings.isEmpty(path)) {
+
+ // Determine the target and return the URL coding strategy (if any)
+ RootMountedUrlCodingStrategy rootStrategy = findRootMount(path);
+
+ if (rootStrategy == null) {
+ if (notFoundPage != null) {
+ // Forward to the not found page
+ strategy = new QueryStringUrlCodingStrategy("", notFoundPage);
+ }
+ } else {
+ strategy = rootStrategy;
+ }
+ }
+
+ return strategy;
+ }
+
+ /**
+ * Note that method may be invoked by Wicket for many different cases. Often however, the
+ * number of invocations is limited to once per request.
+ *
+ * @param request the request (not null)
+ * @return the path for this request (not null)
+ */
+ @Override
+ protected String getRequestPath(Request request) {
+ // Get the real path of the request.
+ String path = request.getPath();
+
+ // Empty path is reserved for home page;
+ // no need to see if it is a root mounted path.
+ if (!Strings.isEmpty(path)) {
+ // Recompute the regular wicket url coding strategy
+ IRequestTargetUrlCodingStrategy wicketStrategy =
+ super.urlCodingStrategyForPath(path);
+ // Is this a regular Wicket path? If so, just leave it alone.
+ if (wicketStrategy == null) {
+
+ // If not, it might be a root path.
+ RootMountedUrlCodingStrategy strategy = findRootMount(path);
+ if (strategy != null) {
+ // It is a root path; add the internal 'fake' mount path
+ // so that Wicket doesn't get confused.
+ path = strategy.getMountPath() + "/" + path;
+ }
+ }
+ }
+
+ return path;
+ }
+ }
+
+ /**
+ * Iterate over the root mounted url coding strategies to see if any accepts this path.
+ *
+ * @param path the relative path (the requested uri, minus query string,
+ * context path, and filterPath. Relative, no leading '/'.
+ * @return the first root mounted url coding strategies that accepts this path, or null when there are none
+ */
+ private RootMountedUrlCodingStrategy findRootMount(String path) {
+ for (RootMountedUrlCodingStrategy rootMount : rootMounts) {
+ if (rootMount.accepts(path)) {
+ return rootMount;
+ }
+ }
+ return null;
+ }
+
+}
Index: wicket/src/main/java/org/appfuse/web/rootmount/RootMountedBookmarkablePageRequestTargetUrlCodingStrategy.java
===================================================================
--- wicket/src/main/java/org/appfuse/web/rootmount/RootMountedBookmarkablePageRequestTargetUrlCodingStrategy.java (revision )
+++ wicket/src/main/java/org/appfuse/web/rootmount/RootMountedBookmarkablePageRequestTargetUrlCodingStrategy.java (revision )
@@ -0,0 +1,47 @@
+package org.appfuse.web.rootmount;
+
+import org.apache.wicket.IRequestTarget;
+import org.apache.wicket.Page;
+import org.apache.wicket.request.target.coding.BookmarkablePageRequestTargetUrlCodingStrategy;
+
+/**
+ * BookmarkablePageRequestTargetUrlCodingStrategy that mounts on the root.
+ *
+ * <p>An internal 'fake' mount path is used to do URL routing within Wicket. The internal mount path
+ * is removed when the URL is encoded. It is therefore never visible outside the Wicket application.
+ *
+ * <p>Note that the original
+ * {@link org.apache.wicket.request.target.coding.BookmarkablePageRequestTargetUrlCodingStrategy#encode(org.apache.wicket.IRequestTarget)}
+ * is final and can therefore not be overridden here. Therefore, a patched version is needed.
+ *
+ * @author Erik van Oosten
+ */
+public abstract class RootMountedBookmarkablePageRequestTargetUrlCodingStrategy
+ extends BookmarkablePageRequestTargetUrlCodingStrategy
+ implements RootMountedUrlCodingStrategy {
+
+ /**
+ * Constructor.
+ *
+ * @param mountPath the internal 'fake' mount path, its exact value does not matter as long as
+ * it is a unique mount path in the entire application (not null)
+ * @param bookmarkablePageClass type of target page (not null) See constructor of base class.
+ * @param pageMapName the page map name or null for none
+ * @param <P> type of target page
+ */
+ protected <P extends Page> RootMountedBookmarkablePageRequestTargetUrlCodingStrategy(
+ String mountPath, Class<P> bookmarkablePageClass, String pageMapName) {
+
+ super(mountPath, bookmarkablePageClass, pageMapName);
+ }
+
+ @Override
+ public CharSequence encode(IRequestTarget requestTarget) {
+ // Get the original URL (includes the internal mount path).
+ String url = super.encode(requestTarget).toString();
+ // Remove the internal mount path to directly mount on "/".
+ // Note the +1 to also remove the '/'
+ return url.substring(getMountPath().length() + 1);
+ }
+
+}
\ No newline at end of file
Index: wicket/src/main/webapp/WEB-INF/web.xml
===================================================================
--- wicket/src/main/webapp/WEB-INF/web.xml (revision 163)
+++ wicket/src/main/webapp/WEB-INF/web.xml (revision )
@@ -51,6 +51,10 @@
<param-name>configuration</param-name>
<param-value>development</param-value>
</init-param>
+ <init-param>
+ <param-name>ignorePaths</param-name>
+ <param-value>images/,scripts/,styles/,dwr/,crossdomain.xml,favicon.ico</param-value>
+ </init-param>
</filter>
<context-param>
@@ -67,7 +71,6 @@
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-
<filter-mapping>
<filter-name>messageFilter</filter-name>
<url-pattern>/*</url-pattern>
@@ -79,15 +82,13 @@
<filter-name>lazyLoadingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>-->
-
<filter-mapping>
<filter-name>sitemesh</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-
<filter-mapping>
<filter-name>wicket</filter-name>
- <url-pattern>/app/*</url-pattern>
+ <url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
@@ -108,10 +109,6 @@
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
- <welcome-file-list>
- <welcome-file>index.jsp</welcome-file>
- </welcome-file-list>
-
<error-page>
<error-code>404</error-code>
<location>/404.jsp</location>
Index: wicket/src/main/java/org/appfuse/web/pages/UserForm.html
===================================================================
--- wicket/src/main/java/org/appfuse/web/pages/UserForm.html (revision 181)
+++ wicket/src/main/java/org/appfuse/web/pages/UserForm.html (revision )
@@ -53,5 +53,5 @@
</form>
<script type="text/javascript">
- Form.focusFirstElement($('userForm'));
+ Form.focusFirstElement(document.forms[0]);
</script>
\ No newline at end of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment