Re: Note to Cayenne/Tapestry users ...

From: Bryan Lewis (brya..aine.rr.com)
Date: Fri Jan 06 2006 - 12:56:41 EST

  • Next message: chad smith: "Q: Spring, DWR & Cayenne"

    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