Philip Miller wrote:
>Unfortunately I can only find documentation for v3.0. If anyone has
>successfully implemented squeezers in Tapestry 4 and can explain to a
>newbie, I'd be delighted to hear.
>
I've been upgrading our apps from Tap3 to 4, including a DataSqueezer.
I think I fgiured it out from the source code for the built-in
DataSqueezers.
I created a new class ModelObjectAdaptor, so named because all my
DataObjects derive from a ModelObject "wedge" class. Could've called it
CayenneDataObjectAdaptor, I guess.
import org.apache.tapestry.services.DataSqueezer;
import org.apache.tapestry.util.io.SqueezeAdaptor;
import model.Model;
import model.ModelObject;
import model.UserContext;
/**
* Squeezes a ModelObject, our version of a CayenneDataObject.
* Allows our page code to deal only with objects, not oids.
* It uses the prefix 'k', resulting in a squeezed parameter like
* "kCompany-5885".
*/
public class ModelObjectAdaptor implements SqueezeAdaptor
{
public String getPrefix()
{
return Model.DATASQUEEZER_PREFIX;
}
public Class getDataClass()
{
return ModelObject.class;
}
public String squeeze(DataSqueezer squeezer, Object object)
{
return Model.getDataSqueezerKeyFromObject(object);
}
public Object unsqueeze(DataSqueezer squeezer, String key)
{
// Magically get the current request's DataContext.
return
Model.getObjectFromDataSqueezerKey(UserContext.getDataContext(), key);
}
}
I have some static methods in a Model utility class for historical
reasosn. Come to think of it, I could've included these methods in the
ModelObjectAdaptor code... they're not needed anywhere else. They are:
public static final String DATASQUEEZER_PREFIX = "k";
private static final String DATASQUEEZER_SEPARATOR = "-";
/** Returns the key String needed by our custom ModelObject
DataSqueezer;
* see Engine.createDataSqueezer(). The string will start with a "k"
* prefix, then the entityName, a hyphen and the oid, e.g.,
* "kCompany-5885".
*/
public static String getDataSqueezerKeyFromObject(Object object)
{
ModelObject modelObject = (ModelObject) object;
// This works only if the object has a single-column primary
key. It
// could probably be generalized to any ObjectId -- see
// compoundPKForObject() -- but this should satisfy our needs.
Object pkObject = null;
try {
pkObject = DataObjectUtils.pkForObject(modelObject);
}
catch (Exception ex) {
log.error("getDataSqueezerKeyFromObject(): object that
doesn't have a single-column pk?! " + ex);
return null;
}
// There might be a simpler way to do this. Get the class name and
// strip off the "model." prefix.
String className = modelObject.getClass().getName();
int index = className.lastIndexOf('.');
String entityName = className.substring(index + 1);
StringBuffer sb = new StringBuffer();
sb.append(DATASQUEEZER_PREFIX).append(entityName);
sb.append(DATASQUEEZER_SEPARATOR).append(pkObject.toString());
return sb.toString();
}
/** Returns a ModelObject corresponding to the given key. */
public static ModelObject getObjectFromDataSqueezerKey(DataContext
dc, String key)
{
// A minimum key would be 4 characters long, "kX-1".
assert key != null && key.length() >= 4;
assert dc != null;
int index = key.indexOf(DATASQUEEZER_SEPARATOR);
assert index > 1;
// We drop the initial 'k' prefix too.
String entityName = key.substring(1, index);
// So far all our oids are Integers. Some day we might need to deal
// with another type, either by creating another DataSqueezer or by
// asking Cayenne for the entity's pk type and doing the right
thing.
Integer oid = new Integer(key.substring(index + 1));
// Call the nice utility method that gets the object from the
cache.
return (ModelObject) DataObjectUtils.objectForPK(dc, entityName,
oid);
}
The UserContext class is the same trick from Tapestry 3, a ThreadLocal
to hold the DataContext where anyone can get it.
public final class UserContext
{
private static final ThreadLocal dataContextLocal = new ThreadLocal();
// Disable construction.
private UserContext() {}
/** Engine.requestDestroyed() calls this. It prevents lingering object
* references by nullifying all threadLocals.
*/
public static void reset()
{
setDataContext(null);
setAppName(null);
}
public static DataContext getDataContext()
{
return (DataContext) dataContextLocal.get();
}
public static void setDataContext(DataContext dc)
{
dataContextLocal.set(dc);
}
}
I tell hivemind to add my DataSqueezer to the list with this snippet of
hivemodule.xml in the same package as the ModelObjectAdapter code:.
<?xml version="1.0"?>
<module id="cview" version="1.0.0">
<contribution configuration-id="tapestry.data.SqueezeAdaptors">
<adaptor object="instance:cview.base.ModelObjectAdaptor"/>
</contribution>
</module>
The final trick came from Cayenne 1.2's WebApplicationContextProvider.
You can use that directly if you don't have a custom configuration. I
have this code in my Engine class:
public static final String DATA_CONTEXT_KEY = "cayenne.datacontext";
/** This method is called by the servlet container when a session is
* created. It creates a DataContext and stores it in the session for
* later retrieval by requestInitialized(). This is the same approach
* used by Cayenne 1.2's WebApplicationContextProvider, except calling
* our custom getConfiguration().
*/
public void sessionCreated(HttpSessionEvent se)
{
log.info("sessionCreated, no login yet; creating DataContext");
Configuration config = Model.getConfiguration();
DataContext dataContext = config.getDomain().createDataContext();
// Store the DC in the session.
se.getSession().setAttribute(DATA_CONTEXT_KEY, dataContext);
}
/** This is called at the beginning of each request; we use it as we did
* the old Tapestry 3 setupForRequest(). Primarily it retrieves the
* DataContext stored earlier in the HttpSession and binds it to the
* current thread.
*/
public void requestInitialized(ServletRequestEvent sre)
{
// Get the DataContext from the session and bind to the thread.
DataContext dc = null;
ServletRequest req = sre.getServletRequest();
HttpSession session = null;
if (req instanceof HttpServletRequest) {
session = ((HttpServletRequest) req).getSession();
dc = (DataContext) session.getAttribute(DATA_CONTEXT_KEY);
}
UserContext.setDataContext(dc);
}
In order to get notified about the session and request initialization,
the Engine implements HttpSessionListener and ServletRequestListener.
Each web-app's web.xml specifies Engine as a listener:
<listener>
<listener-class>cview.base.Engine</listener-class>
</listener>
I think that's it. It seems like a bit of plumbing, eh? I wouldn't be
surprised (or offended :-) if someone pointed out how it could be more
elegant. It works.
This archive was generated by hypermail 2.0.0 : Fri Jan 06 2006 - 12:56:48 EST