/** * 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.util; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.model.IModel; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.flow.RedirectToUrlException; import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.util.convert.ConversionException; import org.apache.wicket.util.lang.PropertyResolver; /** * Applies the DRY principle to Wicket bookmarkable links and page parameters. * ParameterSpec lets you define your properties and parameters once; it * then takes care of link construction and page parameter parsing so you * don't have to do tedious PageParameters.put() and get() calls. * <p> * For pages that require only a single parameter this class is overkill. * However it is appropriate for detail pages or search * pages where there are many parameters that have to be parsed * from the URL in order to construct the page. In short, the ParameterSpec * does two things: * <ol> * <li>Given a model bean, ParameterSpec can construct a bookmarkable link with * parameters taken from property values of that bean. For example, if * your detail page requires an "id" and a "slug", this will pull * <code>getId()</code> and <code>getSlug()</code> from your model bean * on the fly to construct the link.</li> * <li>Going the other direction, ParameterSpec can parse the PageParameters * of a page request and populate a bean with the appropriate properties. * So if the URL contained "1" for the id parameter and "foo" for the * slug parameter, your DetailBean would have its <code>setId()</code> * and <code>setSlug()</code> call with those values. You could then send * that bean to the backend for loading, etc.</li> * </ol> * <p> * For example, let's say you have a {@code PersonDetailPage} mounted at * <code>/people/${id}/${slug}</code>. * <pre class="example"> * public class MyApplication extends WebApplication * { * @Override * protected void init() * { * super.init(); * mountPage("/people/${id}/${slug}", PersonDetailPage.class); * } * }</pre> * ParameterSpec will handle these {@code id} and {@code slug} parameters * for you. Assuming you have a {@code Person} bean with {@code id} and * {@code slug} properties, you would define a {@code SPEC} static instance * on the page like this: * <pre class="example"> * public class PersonDetailPage extends WebPage * { * public static final ParameterSpec<Person> SPEC = new ParameterSpec<Person>( * PersonDetailPage.class, "id", "slug" * ); * ... * }</pre> * <p> * Now when other parts of the application want add a link to this page, it is * as easy as this: * <pre class="example"> * add(PersonDetailPage.SPEC.createLink("link", personModel));</pre> * <p> * To parse the parameters of a request, use this: * <pre class="example"> * Person person = new Person(); * SPEC.parseParameters(getPageParameters(), person); * // person bean has now been populated based on page parameters</pre> * * @author Matt Brictson */ public class ParameterSpec<T> implements Serializable { private Class <? extends WebPage> pageClass; private Map<String,String> mapping = new HashMap(); /** * Construct a ParameterSpec that will build links to the specified WebPage. * If any String arguments are specified, they will be used as property * expressions for extracting parameter values. It is assumed that the * property expression and PageParameters key are identical. * <p> * For example, if your PersonPage is mounted at "/person" and you want * URLs to look like "/person/[id]/[slug]", where id and slug are provided * by <code>PersonBean.getId()</code> and <code>PersonBean.getSlug()</code>, * you would mount the page using * {@link org.apache.wicket.request.mapper.MountedMapper MountedMapper} * and then construct the ParameterSpec like this: * <pre class="example"> * new ParameterSpec(PersonPage.class, "id", "slug");</pre> * <p> * Which is the same as: * <pre class="example"> * ParameterSpec spec = new ParameterSpec(PersonPage.class); * spec.registerParameter("id", "id"); * spec.registerParameter("slug", "slug");</pre> * * @param page The target page of links created by this ParameterSpec * @param expressions Bean property expressions (e.g. "slug", "id") that * will be used to populate URLs to the page */ public ParameterSpec(Class <? extends WebPage> page, String... expressions) { this.pageClass = page; for(int i=0; i<expressions.length; i++) { registerParameter(expressions[i], expressions[i]); } } /** * Register a parameter that is required by bookmarkable links to your * page. For example, if your URL is "/search?q=[terms]" and the * terms come from a <code>getTerms()</code> property of your model bean, * then you would use this code: * <pre class="example"> * registerParameter("q", "terms")</pre> * * @param parameter The page parameter key (this may appear in the URL * depending on the coding strategy) * @param propExpr Bean property that will be used to populate URLs * when building links */ public ParameterSpec registerParameter(String parameter, String propExpr) { this.mapping.put(parameter, propExpr); return this; } /** * Creates a BookmarkablePageLink to the page managed by this ParameterSpec. * The link will have parameters dictated by the ParameterSpec constructor * or <code>registerParameter()</code> calls. The values of those * parameters will be taken from properties of the specified model bean. * For example, the link may require "id" and "slug" values, meaning * <code>getId()</code> and <code>getSlug()</code> will be called on the * model object at render time to populate the parameters of the link. * * @param id The wicket:id of the link in the HTML markup * @param model A model representing the bean that will be used to * populate the parameters of the link */ public BookmarkablePageLink createLink(String id, final IModel<T> model) { BookmarkablePageLink bpl = new BookmarkablePageLink(id, this.pageClass) { @Override protected void onBeforeRender() { PageParameters params = createParameters(model.getObject()); for(PageParameters.NamedPair pair : params.getAllNamed()) { getPageParameters().set(pair.getKey(), pair.getValue()); } super.onBeforeRender(); } @Override protected void onDetach() { model.detach(); super.onDetach(); } }; return bpl; } /** * Immediately halt the request cycle and force a 302 redirect to the * bookmarkable page managed by this ParameterSpec. Page parameters will * be passed to the page based on the specified model. * * @throws RedirectToUrlException to force Wicket to halt the request */ public void redirect(IModel<T> model) { CharSequence url = RequestCycle.get().urlFor( this.pageClass, createParameters(model.getObject()) ); throw new RedirectToUrlException(url.toString(), 302); } /** * Creates a PageParameters map populated with the parameters dictated * in the ParameterSpec constructor or <code>registerParameter()</code> * calls. For each parameter, the property expression will be * evaluated against the given bean to retreive the value. For example, * if "id" is one of the expressions, <code>bean.getId()</code> will be * used to retrieve its value and place it in the PageParameters. */ public PageParameters createParameters(T bean) { PageParameters params = new PageParameters(); for(String key : this.mapping.keySet()) { String expression = this.mapping.get(key); Object value = PropertyResolver.getValue(expression, bean); if(value != null) { params.set(key, value.toString()); } } return params; } /** * Use this method in your page constructor to parse the * PageParameters. The specified bean will be populated by calling the * appropriate setters as defined by this ParameterSpec. For example, if * the ParameterSpec has been created parameters that map "id" and "slug" * properties, the <code>setId()</code> and <code>setSlug()</code> * methods of the bean will be called with values taken from the * PageParameters. * * @param params Values will be taken from these PageParameters * @param beanToPopulate Values will be set using appropriate setters on * this bean * * @throws AbortWithHttpErrorCodeException with a 404 status code if * a parsing exception occurs. For example, this could happen if the bean * property for "id" is of type Long, but the parameter value being parsed * is not numeric. */ public void parseParameters(PageParameters params, T beanToPopulate) { parseParameters(params, beanToPopulate, true); } /** * Use this method in your page constructor to parse the * PageParameters. The specified bean will be populated by calling the * appropriate setters as defined by this ParameterSpec. For example, if * the ParameterSpec has been created parameters that map "id" and "slug" * properties, the <code>setId()</code> and <code>setSlug()</code> * methods of the bean will be called with values taken from the * PageParameters. * * @param params Values will be taken from these PageParameters * @param beanToPopulate Values will be set using appropriate setters on * this bean * * @throws AbortWithHttpErrorCodeException with a 404 status code if * {@code throw404OnParseError} is {@code true} and a parsing exception * occurs. For example, this could happen if the bean property for "id" is * of type Long, but the parameter value being parsed is not numeric. * If {@code throw404OnParseError} is {@code false}, skip past properties * with parsing errors. */ public void parseParameters(PageParameters params, T beanToPopulate, boolean throw404OnParseError) { for(PageParameters.NamedPair pair : params.getAllNamed()) { String key = pair.getKey(); String expr = this.mapping.get(key); String val = pair.getValue(); if(val != null) { try { PropertyResolver.setValue(expr, beanToPopulate, val, null); } catch(ConversionException ce) { if(throw404OnParseError) { throw new AbortWithHttpErrorCodeException( 404, "Not found" ); } } } } } }