Thursday, December 29, 2011

Bookmarkable PagingNavigator

The code for SEO is written in https://cwiki.apache.org/WICKET/seo-search-engine-optimization.html.

But the implementation to make paging stateless is not presented.

I wrote the following simple implementation.

public class BookmarkableNavigator extends PagingNavigator {
    private static final long serialVersionUID = 1117219289688557460L;
    private static final String DEFAULT_KEYNAME = "page";
    private final String keyName;

    public BookmarkableNavigator(String id, IPageable pageable) {
        this(id, pageable, DEFAULT_KEYNAME);
    }
    public BookmarkableNavigator(String id, IPageable pageable, String keyName) {
        super(id, pageable);
        this.keyName = keyName;
    }

    @Override
    protected void onInitialize() {
        super.onInitialize();
        int currentPage;
        try {
            currentPage = getPage().getPageParameters().get(keyName).toInt();
        }catch (StringValueConversionException e) {
            currentPage = 0;
        }
        getPageable().setCurrentPage(currentPage);
    }

    private PageParameters getParams(int page) {
        PageParameters params = new PageParameters(getPage().getPageParameters()); // copy
        params.set(keyName, page);
        return params;
    }

    @Override
    protected AbstractLink newPagingNavigationIncrementLink(String id, final IPageable pageable, final int increment) {
        int idx = pageable.getCurrentPage() + increment;
        int page = Math.max(0, Math.min(pageable.getPageCount() - 1, idx));
        return new BookmarkablePageLink<void>(id, getPage().getClass(), getParams(page)) {
            private static final long serialVersionUID = 4583210927959068500L;
            private boolean isFirst() {
                return pageable.getCurrentPage() <= 0;
            }
            public boolean isLast() {
                return pageable.getCurrentPage() >= (pageable.getPageCount() - 1);
            }
            @Override
            public boolean linksTo(final Page page)
            {
                pageable.getCurrentPage();
                return ((increment < 0) && isFirst()) || ((increment > 0) && isLast());
            }
        }.setAutoEnable(true);
    }

    // copy from PagingNavigationLink#cullPageNumber
    private int cullPageNumber(int pageNumber)
    {
        int idx = pageNumber;
        IPageable pageable = getPageable();
        if (idx < 0)
            idx = pageable.getPageCount() + idx;
        if (idx > (pageable.getPageCount() - 1))
            idx = pageable.getPageCount() - 1;
        if (idx < 0)
            idx = 0;
        return idx;
    }
    private class BookmarkableNavigationLink extends BookmarkablePageLink<void< {
        private static final long serialVersionUID = 7905210383154082192L;
        private final int pageNumber;
        private final IPageable pageable;
        public <c extends page> BookmarkableNavigationLink(
            String id, Class<c> pageClass, IPageable pageable, int pageNumber) {
            super(id, pageClass, getParams(pageNumber));
            this.pageNumber = pageNumber;
            this.pageable = pageable;
            setAutoEnable(true);
        }
        @Override
        public final boolean linksTo(final Page page) {
            return cullPageNumber(pageNumber) == pageable.getCurrentPage();
        }
    }
    @Override
    protected AbstractLink newPagingNavigationLink(String id, final IPageable pageable, final int pageNumber) {
        return new BookmarkableNavigationLink(id, getPage().getClass(), pageable, cullPageNumber(pageNumber));
    }

    @Override
    protected PagingNavigation newNavigation(String id, IPageable pageable, IPagingLabelProvider labelProvider) {
        return new PagingNavigation(id, pageable, labelProvider) {
            private static final long serialVersionUID = -4044262294210880251L;

            @Override
            protected AbstractLink newPagingNavigationLink(String id, IPageable pageable, int pageIndex) {
                return new BookmarkableNavigationLink(id, getPage().getClass(), pageable, pageIndex);
            }
        };
    }
}

Thursday, December 8, 2011

A way of preventing state of persistent object from becoming "Hollow" when using Wicket + Guice + Apache Cayenne

I'm using wicket-guice to inject ObjectContext to Page by Guice.
In this case, persistent object become "hollow" state when I click the back button of browser.

Persistent object don't serialize value to the session. DataContext object sets values in persistent object when it is deserialized.
But the processing of DataContext deserialization doesn't work because the serialized object is a proxy object when wicket-guice is used.
The proxy object always create a new object. Therefore DataContext object cannot deserialize persistent object, and the state of persistent object become "hollow".


A simple method of solving is written here.
http://cayenne.195.n3.nabble.com/Conditions-when-PersistenceState-gets-quot-hollow-quot-td691334.html
But, this method needs to add the code of checking persistent state to all models.


Another method is to add the following code to Application.java.
        getComponentInstantiationListeners().add(
            new GuiceComponentInjector(
                this, Guice.createInjector(
                    usesDeploymentConfig() ? Stage.PRODUCTION : Stage.DEVELOPMENT, new GuiceModule()), false));
This method needs to modify all injected objects to serializable. You will need a lot of corrections if you are already using service class of not serializable.



If you want to change deserialization processing, you need copying and changing source code of wicket-guice.
I changed the source code of wicket-guice as shown as follows.

GuiceCayenneComponentInjector.java
    public GuiceCayenneComponentInjector(final Application app, final Injector injector,
        final boolean wrapInProxies)
    {
        app.setMetaData(GuiceInjectorHolder.INJECTOR_KEY, new GuiceInjectorHolder(injector));
        fieldValueFactory = new GuiceCayenneFieldValueFactory(wrapInProxies);
        app.getBehaviorInstantiationListeners().add(this);
        bind(app);
    }

GuiceCayenneFieldValueFactory.java
 public class GuiceCayenneFieldValueFactory implements IFieldValueFactory
{
    private final boolean wrapInProxies;

    private boolean wrapClass(Class<?> type) {
        if (!type.getPackage().getName().startsWith("org.apache.cayenne"))
            return true;
        if (type.getName().endsWith("ObjectContext") ||
                type.getName().endsWith("DataContext"))
            return false;
        return true;
    }

    ... (snip)

    public Object getFieldValue(final Field field, final Object fieldOwner)

        ... (snip)

                    if (wrapInProxies && wrapClass(field.getType()))
                    {
                        target = LazyInitProxyFactory.createProxy(field.getType(), locator);
                    }
                    else
                    {
                        target = locator.locateProxyTarget();
                    }

A object is serialized and deserialized without using proxy by this code when injected object is DataContext.





Monday, December 6, 2010

Switching Login Page in Activity

In GWT with MVP, how to switch member pages to default login page when session was invalid?
A simple way is to use the goTo method of PlaceController.


public class MemberActivity extends AbstractActivity implements MemberView.Presenter {
    private final ClientFactory clientFactory;
    private MemberView view;
    private static String sid;
    public MemberActivity(ClientFactory factory){
        clientFactory = factory;
    }
    @Override
    public void start(final AcceptsOneWidget panel, EventBus eventBus) {
        sid = Cookies.getCookie("sid");
        if (clientFactory.getMemberService().validSession(sid, new AsyncCallback<Boolean>(){
            @Override
            public void onFailure(Throwable caught) {
                clientFactory.getPlaceController().goTo(new LoginPlace());
            }
            @Override
            public void onSuccess(CmmsAccountDto result) {
                if (!result){
                    clientFactory.getPlaceController().goTo(new LoginPlace());
                    return;
                }

                view = clientFactory.getMemberView();
                view.setPresenter(this);
                panel.setWidget(view);
            }
        });
    }
}

But, this way have a problem that browser history is increased.
The browser cannot return to the history before MemberPlace with the back button because the browser redirects to LoginPlace when it visits MemberPlace.

Then, I used the approach that called another Activity from a Activity.


public class MemberActivity extends AbstractActivity implements MemberView.Presenter {
    private final ClientFactory clientFactory;
    private MemberView view;
    private static String sid;
    public MemberActivity(ClientFactory factory){
        clientFactory = factory;
    }
    @Override
    public void start(final AcceptsOneWidget panel, final EventBus eventBus) {
        sid = Cookies.getCookie("sid");
        if (clientFactory.getMemberService().validSession(sid, new AsyncCallback<Boolean>(){
            @Override
            public void onFailure(Throwable caught) {
                loginView(panel, eventBus);
            }
            @Override
            public void onSuccess(CmmsAccountDto result) {
                if (!result){
                    loginView(panel, eventBus);
                    return;
                }

                view = clientFactory.getMemberView();
                view.setPresenter(this);
                panel.setWidget(view);
            }
        });
    }

    private void loginView(AcceptsOneWidget panel, EventBus eventBus){
        LoginActivity loginActivity = new LoginActivity(clientFactory);
        loginActivity.start(panel, eventBus);
    }
}

In above code, The view can be switched without the history changing.