Skip navigation
2013

Summarization of different ways to redirect or render another response in Apache Wicket.

 

Some of the sources:

http://stackoverflow.com/questions/3334827/wicket-how-to-redirect-to-another-page

http://stackoverflow.com/questions/5800974/redirect-to-external-non-wicket-page-in-wicket-1-5

http://www.developer.com/java/other/article.php/10936_3673576_6/Wicket-The-First-Steps.htm

 

Redirection in wicket basically revolves around IRequestHandler implementations.

Martin Grigorov wrote:

IRequestHandler (IRH) is the improved IRequestTarget from 1.4. Read

RequestCycle's javadoc for more information about the scheduling of

IRequestHandlers.

Basically the idea is that the request processing starts with a IRH

that is resolved by the request url, then later during processing a

new IRH can be scheduled to be executed after the current one (e.g.

using setResponsePage() will do that). At the end only the last

scheduled IRH actually writes to the http response.

 

There are 2 kinds of redirects:

 

A) In event handlers - links etc.

 

setResponsePage()

add( new Link("link"){
     @Override void onClick(){
         setResponsePage( new MyPage( new PageParameters().add("product", product).add("release", release) );
     }
}

 

DownloadLink

add( new DownloadLink("download"){

     ... (TBD)

});

 

redirectToInterceptPage & continueToOriginalDestination()

First, redirectToInterceptPage() "stores" the current page and redirects to e.g. a sing-in page.

Then, continueToOriginalDestination() redirects back to this stored page, e.g. when user is done with signing-in.

 

B) During page initialization.

 

RestartResponseAtInterceptPageException

Renders another page.

Useful if navigated to a page with invalid params (nonexistent id etc.)

 

ImmediateRedirectException

If actual HTTP redirection is needed.

 

RequestCycle.scheduleRequestHandlerAfterCurrent(IRequestHandler)

 

org.apache.wicket.markup.html.pages.RedirectPage

http://wicket.apache.org/apidocs/1.4/org/apache/wicket/markup/html/pages/RedirectPage.html

 

RedirectToUrlException

import org.apache.wicket.request.flow.RedirectToUrlException;
...
throw new RedirectToUrlException(
    "http://www.facebook.com/login.php?api_key="+ _apiKey + "&v=1.0");

 

Using HTTP 301 ("Moved Permanently", SEO friendly):

import org.apache.wicket.request.flow.RedirectToUrlException;
import javax.servlet.http.HttpServletResponse;
...
throw new RedirectToUrlException(
   
"http://www.facebook.com/login.php?api_key="+ _apiKey + "&v=1.0",
   
HttpServletResponse.SC_MOVED_PERMANENTLY);

Yep, it's being worked on.

 

It will take what you had in AS 5 and either put it in standalone[-*].xml, or generate, and optionally execute, CLI commands, while dealing with collisions, and giving you possibility to decide in cases when the tool can't decide for you.

Looking forward to see it being useful.

 

Stay tuned.

I've created a very simple page which executes your JPQL query and shows the results as JSON, using GSON.

It has no features besides a textarea and the formatted output.

Here it is.

 

Screenshot from 2013-01-24 02:49:50.png

 

Before the code, let me warn you that GSON can't deal with circular references on it's own.

You need to cut them at appropriate places, by either using @XmlTransient to prevent that particular member to be serialized,

or by serializing it differently, e.g. using toString(). I think GSON follows most, if not all, JAXB annotations, so that's the way.

 

First, add GSON to classpath.

 

<!-- GSON - for JpaQueryPage -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.2.2</version>
</dependency>

 

And here is the Wicket page - HTML and Java.

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>JPA Query Page</title>
    </head>
    <body>
        <form wicket:id="form">
            <textarea wicket:id="query" style="min-width: 60em;" rows="5">SELECT p FROM Product p</textarea>
            <br/> Example: <code>SELECT p FROM Product p LEFT JOIN FETCH p.customFields</code>
            <br/>
            <input type="submit" value="Perform"/>

            <table style="font-family: monospace; font-size: 8pt;">
                <tr wicket:id="result">
                    <td wicket:id="item" style="white-space: pre">Some query result...</td>
                </tr>
            </table>
        </form>

    </body>
</html>

 

package org.jboss.essc.web.pages.test;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.CompoundPropertyModel;
import org.jboss.essc.web.model.ProductCustomField;
import org.jboss.essc.web.model.ReleaseCustomField;

/**
 *
 *  @author Ondrej Zizka
 */
public class JpaQueryPage extends WebPage {

    @PersistenceContext EntityManager em;

    private String query = "";

    private List<Object> result = Collections.EMPTY_LIST;

    public JpaQueryPage() {

        Form form = new Form("form", new CompoundPropertyModel(this) ){
            @Override protected void onSubmit() {
                try {
                    result = em.createQuery( query ).getResultList();
                }
                catch (Exception ex){
                    result = new ArrayList(2);
                    result.add( ex );
                    result.add( query );
                }
            }
        };
        add( form );

        form.add( new TextArea("query") );
        this.query = 
                "SELECT rel, rel.product FROM Release rel\n"
                + " LEFT JOIN FETCH rel.product.customFields\n"
                + " LEFT OUTER JOIN FETCH rel.customFields\n"
                + " WHERE rel.product.name = 'EAP' AND rel.version = '6.0.1.GA'";


        form.add( new ListView("result") {
            @Override
            protected void populateItem( ListItem item ) {
                String content;
                try {
                    content = toJSON( item.getModelObject() );
                } catch (Throwable ex){
                    try {
                        content = item.getModelObject().toString();
                    } catch (Exception ex2) {
                        content = item.getModelObject().getClass() + " but toString() threw: " + ex2.toString();
                    }
                }
                item.add( new Label("item", content) );
            }

        } );

    }// const


    /**
     *  Convert any object to JSON.
     */
    private static String toJSON( Object object ) {
        //Gson gson = new Gson();
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .addDeserializationExclusionStrategy( new ExclusionStrategy() {
                    @Override public boolean shouldSkipField( FieldAttributes f ) {
                        return false;
                    }
                    @Override public boolean shouldSkipClass( Class<?> clazz ) {
                        if( ReleaseCustomField.class.equals( clazz ) )
                                return true;
                        if( ProductCustomField.class.equals( clazz ) )
                                return true;
                        return false;
                    }
                } )
                .create();
        return gson.toJson(object);
    }

}// class

 

HTH.

Wicket is cool and and gives you great power to create AJAXified pages, but may get tricky.

Most of my mistakes in Wicket come from the Models.

What works for pages generated per request, may fail for AJAX, since for AJAX requests, pages are not re-created. (Also, going to the same page doesn't re-create, just uses the same instance.)

 

Here's how I dealt with a ListView which displays data from a Map<String, SomeEntity>.

 

Fist, I create an IModel<List<ProductCustomField>> which loads it's content from the actual value of the component's model. (That's quite important as that might change if you code it to.) The implementation is just the basic Model, not LoadableDetachableModel; LDM would load the data at the start of the request, and then use these stalled data even after the map was changed.

Then, ListView is given this model. So, when rendering during an AJAX request, it always uses the latest data.

 

When creating the item's component, I pass item.getModel() as it's model.

 

The JPA Entity, actually mapped in @OneToMany Map, must have the map key as it's property. In my case, it's "name".

Hence, to refer to given object in AJAX action handler, I can use item.getModelObject().getName().

 

The component structure is:

 

  • ProductPage
    • FeedbackPanel
    • CustomFieldsPanel
      • ListView
        • CustomFieldRowPanel[]

@Entity ProductCustomField

 

public CustomFieldsPanel( String id, final IModel<Map<String,ProductCustomField>> fieldMapModel, final FeedbackPanel feedbackPanel ) {

    super( id, fieldMapModel );
    this.feedbackPanel = feedbackPanel;

    this.setOutputMarkupId( true ); // AJAX JavaScript code needs to have some id="...".

    IModel<List<ProductCustomField>> listModel = new Model() {
        @Override protected List<ProductCustomField> getObject() {
            Map<String,ProductCustomField> map = (Map) CustomFieldsPanel.this.getDefaultModelObject();
            return new ArrayList(map.values());
        }
    };

    ListView<ProductCustomField> listView;
    add( listView = new ListView<ProductCustomField>("fieldsRows", listModel){
        @Override
        protected void populateItem( final ListItem<ProductCustomField> item ) {
            item.add( new CustomFieldRowPanel("fieldRow", item.getModel()){
                // Delete icon was clicked.
                @Override
                protected void onDelete( AjaxRequestTarget target ) {
                    Map<String,ProductCustomField> fieldsMap = (Map) CustomFieldsPanel.this.getDefaultModelObject();
                    fieldsMap.remove( item.getModelObject().getName() );
                    target.add( CustomFieldsPanel.this ); // Update UI.
                    try {
                        CustomFieldsPanel.this.onChange( target ); // Persists.
                    } catch (Exception ex){
                        feedbackPanel.error( ex.toString() );
                    }
                }
            });
        }
    });
    ...
}

 

The entity (simplified):

 

@Entity
public class ProductCustomField implements Serializable {

    @Id  @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Basic(optional = false) private String name;

    @Basic(optional = false)  private String label;
}

This is my way of decomposing an @Entity editing Page in Apache Wicket framework (applies for 1.4, 1.5, 6.x+).

 

Basic principle

Use just one page for all CRUD (create, read, update, delete). It may have drawbacks, but it's the fastest way.

     Create:   create a new entity object (not persistent yet) with default values and use it as Page's model.

     Read:     load the entity from DB

     Update:  use ajax-based editable labels to edit values in place.

     Delete:   Button with ajax-based confirmation somewhere on the page. User can see what he actually deletes. Redirect to some listing of objects of that kind.

 

Tips & Rules

1) Use fully loaded entity as a model. Use LEFT JOIN FETCH to load collections and other LAZY_LOADed parts.

     Do not resort to Open Session in View.

 

SELECT p FROM Product p LEFT JOIN FETCH p.customFields

 

Use PropertyModel. That will keep page object's property and the model the same.

 

this.setDefaultModel( new PropertyModel(this, "product") );

 

Instead of Page's default model, you can use your own, to avoid type casting.

 

 

2) Use PropertyModel for subcomponents.

     Maybe CompoundPropertyModel could be used? (Not sure if limited to Form component or usable for any.)

 

3) Use EditableLabel. Having a "show" page and "edit" page for everything sucks.

    I have created an EditableLabel and EditableLink components which appear as labels, but are editable when clicked or activated by a JavaScript call.

 

4) Use AJAX to update the page. Reloading or clickink a save button (often) sucks.

 

5) If practical, have only one call to dao.update() - in a special method in the Page object.

 

/**
 *  Called when some of sub-components were updated.
 *
 *  @param target  Ajax target, or null if not a result of AJAX.
 */
private void onProductUpdate( AjaxRequestTarget target ) {
    if( target != null )  target.add( this.feedbackPanel );
    try {
        product = productDao.update( product );
        modelChanged();
        this.feedbackPanel.info("Product saved.");
        if( target != null )
            target.appendJavaScript("window.notifyFlash('Product saved.')");
    } catch( Exception ex ){
        this.feedbackPanel.info("Saving product failed: " + ex.toString());
    }
}

 

     Call to this method from other methods in Page.

     Such methods may be overriden onChange-like methods of your components, used as a mechanism to propagate changes to parent components up to the Page.

     Don't forget to call modelChanged() wherever you change Page's model or it's object.

RequestCycle#scheduleRequestHandlerAfterCurrent() is used to hand over the request to different handler.

For example, if you figure out in some ajax-handling code that you want to serve the user a file.

 

Here's a link to provide an excel file, spotted at http://apache-wicket.1842946.n4.nabble.com/Export-a-file-with-wicket-1-5-3-td4110367.html :

 

            final Link lnkExport = new Link("export") { 

                        public void onClick() { 
                                        
                                        getRequestCycle().scheduleRequestHandlerAfterCurrent(new IRequestHandler() { 
                        
                                                @Override 
                                                public void detach(IRequestCycle requestCycle) { 
                                                        
                                                } 

                                                @Override 
                                                public void respond(IRequestCycle requestCycle) { 
                                                        try { 
                                                                JxlExportOrderList jxlExportOrderList = new JxlExportOrderList(dataProvider.getOrderIds(), coOrderService); 
                                                                String fileName = String.format("118114life_%s.xls", new Object[]{DateUtils.format(Calendar.getInstance().getTime(), "yyMMddHHmm")}); 
                                                                
                                                                HttpServletResponse httpResponse = (HttpServletResponse)requestCycle.getResponse().getContainerResponse(); 
                                                                httpResponse.setContentType( "application/vnd.ms-excel" ); 
                                                                httpResponse.setHeader("Content-disposition", "attachment; filename=" + fileName ); 
                                                                jxlExportOrderList.exportToExcel(httpResponse.getOutputStream()); 
                                                        } catch (Exception e) { 
                                                                throw new RuntimeException(e); 
                                                        } 
                                                } 
                                        }); 
                                } 
                        }; 
                        add(lnkExport);
          }

OpenShift applications code is uploaded using Git.

Any changes in the repository directory is recreated after push.

Therefore, storing uploaded files in there won't work.

 

The only persistent directory you can use is ../data. It's full path is stored in environment variable $OPENSHIFT_DATA_DIR.

However, this dir is not public - so no URL leads there.

 

The solution is quite easy - just create a symlink. Here's an example for PHP.

 

Login to your machine via SSH, and run:

 

cd app-root/repo/php    #  php/ is the only publicly accessible directory (by default, not sure if not changeable in .htaccess).
ln -s ../../data/photos photos

 

This makes the content in ../data/photos publicly accessible at http://myapp-myaccount.rhcloud.com/photos/ .

The directory to manage the files in can be referred to using $_ENV['OPENSHIFT_DATA_DIR'].