/**
* 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"
);
}
}