/** * Copyright 2014 55 Minutes (http://www.55minutes.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package fiftyfive.wicket.resource; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestMapper; import org.apache.wicket.request.Request; import org.apache.wicket.request.Url; import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; import org.apache.wicket.util.string.Strings; /** * Enables a Wicket application to have its static resources proxied by a CDN, for example * by Amazon Cloudfront. This works by intercepting Wicket's default behavior for rendering * URLs of resource references, and then rewriting those URLs by prepending a CDN hostname * (or any arbitrary URL fragment). The web browser will therefore make requests to the * CDN host instead of the Wicket app. * <p> * Here's an example. Normally a CSS resource reference is rendered by Wicket like this: * <pre class="example"> * /wicket/resource/com.mycompany.WicketApplication/test.css</pre> * <p> * With {@code SimpleCDN} installed, that resource reference URL is transformed into this: * <pre class="example"> * http://age39p8hg23.cloudfront.net/wicket/resource/com.mycompany.WicketApplication/test.css</pre> * <p> * <b>Please note: {@code SimpleCDN} will not rewrite resource reference URLs that * include query string parameters.</b> Our reasoning is that parameterized URLs usually * indicate that the resource is dynamic, and therefore not appropriate for serving via * CDN. Furthermore it should be noted that Amazon CloudFront will refuse to proxy * URLs that contain query string parameters (it strips the parameters off). * <p> * When configuring the CDN host, the easiest setup is a reverse-proxy. For example, with * Amazon CloudFront, you would specify your Wicket app as the <em>custom origin</em>, and specify * the CloudFront host when constructing this SimpleCDN. It's that easy. * <pre class="example"> * public class MyApplication extends WebApplication * { * @Override * protected void init() * { * super.init(); * // Enable CDN when in deployment mode * if(usesDeploymentConfig()) * { * new SimpleCDN("http://age39p8hg23.cloudfront.net").install(this); * } * } * }</pre> * <em>For those familiar with Ruby on Rails, {@code SimpleCDN} is inspired by the Rails * {@code action_controller.asset_host} configuration setting.</em> * * @since 3.2 */ public class SimpleCDN implements IRequestMapper { private String baseUrl; private IRequestMapper delegate; private boolean delegated = false; /** * Construct a {@code SimpleCDN} that will rewrite resource reference URLs by prepending * the given {@code baseUrl}. * * @param baseUrl For example, "//age39p8hg23.cloudfront.net" */ public SimpleCDN(String baseUrl) { this.baseUrl = baseUrl; } /** * Install this {@code SimpleCDN} into the given application. The {@code SimpleCDN} instance * will not have any effect unless it is installed. */ public void install(WebApplication app) { this.delegate = app.getRootRequestMapperAsCompound(); app.mount(this); } /** * If the {@code requestHandler} is a {@link ResourceReferenceRequestHandler}, delegate to * Wicket's default mapper for creating an appropriate URL, and then prepend the CDN * {@code baseUrl} that was provided to the {@code SimpleCDN} constructor. * * @return a rewritten Url to the resource, or {@code null} if {@code requestHandler} is * not for a resource reference */ public Url mapHandler(IRequestHandler requestHandler) { // CDN doesn't apply to non-resources if(!(requestHandler instanceof ResourceReferenceRequestHandler)) return null; // Prevent infinite recursion in case this SimpleCDN is also contained within the delegate if(this.delegated) return null; Url url = null; try { this.delegated = true; url = this.delegate.mapHandler(requestHandler); if(url != null && url.getQueryParameters().isEmpty()) { url = Url.parse(Strings.join("/", this.baseUrl, url.toString())); } } finally { this.delegated = false; } return url; } /** * Always return {@code null}, since {@code SimpleCDN} does not play any part in handling requests * (they will be handled by Wicket's default mechanism). */ public IRequestHandler mapRequest(Request request) { return null; } /** * Always return {@code 0}, since {@code SimpleCDN} does not play any part in handling requests * (they will be handled by Wicket's default mechanism). */ public int getCompatibilityScore(Request request) { return 0; } }