/** * 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.data; import fiftyfive.util.ReflectUtils; import java.io.Serializable; import java.util.Iterator; import org.apache.wicket.markup.repeater.AbstractPageableView; import org.apache.wicket.markup.repeater.data.IDataProvider; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.lang.Checks; /** * An IDataProvider that implements the DTO pattern. Suitable for full-text * search results and other result sets where size and data are returned in a * single DTO. * <p> * This is a drop-in replacement for {@link IDataProvider}, allowing you to use * Wicket's existing components like * {@link org.apache.wicket.extensions.markup.html.repeater.data.grid.DataGridView DataGridView}, * {@link org.apache.wicket.markup.repeater.data.DataView DataView}, * {@link org.apache.wicket.markup.html.navigation.paging.PagingNavigator PagingNavigator}, * etc. in a DTO-style efficient manner without any customization. * (However see the cautionary note below.) * <p> * The main advantage of using this class is that it implements * {@link IDataProvider#size IDataProvider.size()} and * {@link IDataProvider#iterator IDataProvider.iterator()} * with a single backend query. This is accomplished by maintaining * a reference to a pageable view, so that page size and offset can be * determined when {@code size()} is called. * The size is also cached to prevent * extra backend calls when paging links are clicked. * <p> * In other words, rather than having to issue two calls to the backend, once * to determine the size of the result, and then again to determine the actual * rows of data in the result, you can instead * {@link #load(int,int) implement a single load() method} * that returns a DTO containing both the size and the data. Often * times this is much more efficient, especially when dealing with web service * and full-text search implementations. * <p> * <b>Be sure to call {@link #flushSizeCache() flushSizeCache()} or construct * a new DtoDataProvider when you know your result size will * change, for example if the user changes her search criteria.</b> * <p> * Generic types: * <ul> * <li>{@code R} is a <b>R</b>esult DTO: a container class that holds the * elements of actual data of the current page, plus a total size of the * result. * </li> * <li>{@code E} represents each <b>E</b>lement of data * in the result container.</li> * </ul> * <p> * Note that since DtoDataProvider needs a reference back to the pageable view * that is displaying its data, the object construction process takes a few * steps: * <pre class="example"> * // Let's say this is our concrete implementation. * public class UserResultProvider extends DtoDataProvider<UserSearchResult,User> * { * // implement iterator(UserSearchResult), size(UserSearchResult) and load(int,int) * } * * // To use our provider to drive a DataView, first we construct our provider. * UserResultProvider provider = new UserResultProvider(); * * // Then construct the DataView, passing in our provider. * DataView<User> dataView = new DataView<User>("users", provider) { * // implement populateItem() * }; * * // Finally, wire up our provider back to the view * provider.setPageableView(dataView);</pre> * <p> * <b>Caution: This class should be considered experimental.</b> * By implementing {@code size()} and {@code iterator()} with a single backend * query, this class goes against the Wicket developers' original intentions * for the IDataProvider interface. We accomplish this feat by * using the Java reflection API to access private data within * {@link AbstractPageableView}. * * @since 2.0 */ public abstract class DtoDataProvider<R,E> implements IDataProvider<E> { private transient R transientResult; private transient Integer transientOffset; private transient Integer transientAmount; private Integer cachedDataSize; private AbstractPageableView pageableView; /** * Constructs an empty provider. You must call * {@link #setPageableView setPageableView()} before * the provider can be used. */ public DtoDataProvider() { super(); } /** * Constructs a provider that will use size and offset information from * the specified {@code AbstractPageableView} when loading data. */ public DtoDataProvider(AbstractPageableView pageableView) { super(); this.pageableView = pageableView; } /** * Flush the cached size information that is normally held between * requests. This method should be called for example when your search * criteria changes, meaning that the result data could completely change. * <p> * You shouldn't need to use this method, since new search * criteria would normally mean constructing a completely new * DtoDataProvider. */ public void flushSizeCache() { this.cachedDataSize = null; } /** * Returns the pageable view associated with this provider. */ public AbstractPageableView getPageableView() { return this.pageableView; } /** * Sets the {@code AbstractPageableView} for which this object will be used * as data provider. The pageable view is consulted whenever the result * object is loaded from the backend, in order to get the current page * offset and page size. This property must not be {@code null}. */ public void setPageableView(AbstractPageableView pageableView) { this.pageableView = pageableView; } /** * Loads the result object from the backend. The object will be cached * for the remainder of the current request, or until * {@link #detach() detach()} is called. * * @param offset A zero-based offset of the first result desired, based on * the current page number and page size. * @param amount The number of results desired (i.e. the page size). */ protected abstract R load(int offset, int amount); /** * Returns an iterator of the items contained in the given result object. */ protected abstract Iterator<? extends E> iterator(R result); /** * Returns the total number of items in the entire result, as represented * by the given result object. */ protected abstract int size(R result); // IDataProvider support /** * Loads the result DTO from the backend if necessary, then delegates * to the implementation of {@link #iterator(Object) iterator(R)}. */ public Iterator<? extends E> iterator(int offset, int amount) { return iterator(getCachedResultOrLoad(offset, amount)); } /** * This implementation assumes the object is Serializable and simply * calls Model.of(). You may wish to override with a custom model. */ public IModel<E> model(E object) { return (IModel<E>) Model.of((Serializable)object); } /** * Loads the result DTO from the back-end based on the current state * of the page. A cached version of the DTO will be used if possible * to reduce extra back-end calls. * <p> * Delegates to the implementation of * {@link #size(Object) size(R)}, which subclasses must implement. * <p> * This result will be cached, and the cache used if possible. */ public int size() { if(null == this.cachedDataSize) { this.cachedDataSize = size(getCachedResultOrLoad()); } return this.cachedDataSize; } // loadable detachable support /** * Loads and returns the result DTO from the backend, or returns the * cached copy if it has already been loaded. The cache is discarded * automatically if the cache is out of date (i.e. the offset and * amount to load have changed). The cache is also discarded when * {@link #detach() detach()} is called. * <p> * This no-argument version infers the offset and amount to load based * on the pageable view that is being used with this data provider. * * @since 4.0 */ public R getCachedResultOrLoad() { return getCachedResultOrLoad(getPageableViewOffset(), getPageableRowsPerPage()); } /** * Loads and returns the result DTO from the backend, or returns the * cached copy if it has already been loaded. The cache is discarded * automatically if the cache is out of date (i.e. the offset and * amount to load have changed). The cache is also discarded when * {@link #detach() detach()} is called. * <p> * The explicit offset and amount parameters indicate the items to be * loaded. If the cache was for a different set of parameters, it will * be discarded. * * @since 4.0 */ public R getCachedResultOrLoad(int offset, int amount) { if(isCacheStale(offset, amount)) { // Reset cached values by loading from the back-end this.transientOffset = offset; this.transientAmount = amount; this.transientResult = load(offset, amount); } // Return the cached result return this.transientResult; } /** * Discards the cached view offset, rows per page, and result DTO objects. * Note that the result size remains cached. */ public void detach() { this.transientResult = null; this.transientOffset = null; this.transientAmount = null; } // Pageable reflection "magic" /** * Obtains the current view offset using the Java reflection API to * get the {@code currentPage} private field from the pageable view and * multiplying it by the rows per page. */ protected int getPageableViewOffset() { assertPageableView(); int page = (Integer) ReflectUtils.readField( this.pageableView, "currentPage" ); return page * getPageableRowsPerPage(); } /** * Obtains the maximum rows per page needed by the pageable view by * calling the * {@link AbstractPageableView#getItemsPerPage getItemsPerPage()} * method. */ protected int getPageableRowsPerPage() { assertPageableView(); return this.pageableView.getItemsPerPage(); } /** * Returns {@code true} if the desired {@code offset} and {@code amount} * are different than the previously cached values. */ private boolean isCacheStale(int offset, int amount) { boolean stale = false; if(null == this.transientOffset || null == this.transientAmount) { stale = true; } else if(this.transientOffset != offset || this.transientAmount < amount) { stale = true; } return stale; } /** * Asserts that {@code pageableView} is not {@code null}. */ private void assertPageableView() { Checks.notNull( this.pageableView, "setPageableView() must be called before provider can load" ); } }