Skip navigation
1 3 4 5 6 7 Previous Next

Ondrej Zizka's Blog

102 posts

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'].

This happens when you're trying to deploy an app to root context (either using the name ROOT.war or "/" context in jboss-web.xml),

but the AS welcome page is already occupying that.

 

The AS CLI command to disable default root context is:

 

[standalone@localhost:9999 /] /subsystem=web/virtual-server=default-host:write-attribute(name=enable-welcome-root,value=false)   
{
    "outcome" => "success",
    "response-headers" => {
        "operation-requires-reload" => true,
        "process-state" => "reload-required"
    }
}

 

Then, restart the AS (or at least web subsystem).

 

 

Or, shut down the server and edit the standalone.xml:

 

<subsystem xmlns="urn:jboss:domain:web:1.2" default-virtual-server="default-host" native="false">
    <virtual-server name="default-host" enable-welcome-root="false">
    ...
</subsystem>

Module.xml is a file defining how JBoss Modules, part of the AS 7 core, will treat the content of the module.

 

JBoss Modules is mostly about classloading, so almost whole content of module.xml is for classloaders.

Here is the official info:

However the first is not complete, the comments in XSD are a bit uninformative, and the last is about classloading in general, so I 'll put a brief summary here.

 

Full module.xml example with comments

<?xml version="1.0" encoding="UTF-8"?>

<module xmlns="urn:jboss:module:1.1" name="org.jboss.msc">

    <!-- Main class to use when launched as a standalone jar. -->
    <main-class name="org.jboss.msc.Version"/>

    <!-- Properties readable through Modules API. Not to be confused with Java system properties. -->
    <properties>
        <!-- jboss.api=private means that the module is not part of the JBoss AS 7 public API - basically saying, "Don't use it's packages in your apps." -->
        <property name="jboss.api" value="private"/>
    </properties>

    <resources>
        <!-- Basically, list of jars to load classes and resources from. -->
        <resource-root path="jboss-msc-1.0.1.GA.jar"/>
        ...
    </resources>

    <dependencies>

        <!--  Export paths and packages from the class loader which loaded JBoss Modules (usually the system's application CL). -->
        <system export="true">
            <!-- List of exported paths. Mandatory. -->
            <paths>
               <path name="org/jboss/modules"/>
               <path name="org/jboss/modules/log"/>
            </paths>
            <!-- Optional limitation of what's exported. -->
            <exports>
                 <include path="META-INF/"/>
            </exports>
        </system>

        <!-- Dependencies on other modules. Classloader of this module will have their classes visible. -->
        <module name="javax.api"/>
        <module name="org.jboss.logging"/>

        <!-- services="import/export/none" controls whether services defined in META-INF/services are also visible to/from this module.
               I.e. services="export" will make the services of this module visible to the dependency. Import will do the other way around.
               Defaults to "none". -->
        <!-- export="true" controls whether own exports of this dependency are visible. -->
        <module name="org.jboss.ws.native.jbossws-native-core" services="export" export="true">
            <!-- You can limit what packages in dependency modules are allowed 
                   to be seen from this module's point of view (import), or vice versa (export).
                   By default, all are imported/exported. When you specify <imports> or <exports>, only those listed are. -->
            <imports>
               <include path="META-INF"/>
               <include path="dtd"/>
               <include path="schema"/>
               <exclude-set>
                   <path name="org.jboss.example.tests"/>
               </exclude-set>
            </imports>
            <exports>
                <include path="META-INF"/>
                <include path="dtd"/>
                <include path="schema"/>
            </exports>
        </module>

        <!-- Optional deps -->
        <module name="javax.inject.api" optional="true"/>
    </dependencies>
</module>

It's ok to change standalone.xml in OpenShift, if you take care not to remove things like MysqlDS provided by OpenShift's infrastructure.

 

After checkout, the file is located in /.openshift/config/standalone.xml .

It's hidden both on Windows and Linux.

It's also ignored, so changes to it can't be added to git's stage.

To unignore this and only this file, change the relevant part in /.gitignore from

/.openshift/

to

/.openshift/*
!/.openshift/config/

/.openshift/config/*
!/.openshift/config/standalone.xml

After that, you can `git add`.

 

Upon pushing, the AS is restarted with the new file. Check if your app responds.

 

If you see HTTP 503 - Temporarily unavailable, SSH your app's shell (see the "NEED TO LOGIN TO YOUR APP?" at app's OpenShift page) and check:

tail -200f /jbossas-7/logs/boot.log

 

If you see HTTP 500, then something's wrong in your app;

If you see a blank page, something went south during the deployment phase.

In both cases, see

tail -200f /jbossas-7/logs/server.log

Those who tried to run Wicket on JBoss AS 7 hit this:

 

java.lang.ClassNotFoundException: org.jboss.msc.service.ServiceName from [Module "deployment.ROOT.war:main" from Service Module Loader]
at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:190)
...
at org.apache.wicket.application.AbstractClassResolver.resolveClass(AbstractClassResolver.java:108)
at org.apache.wicket.serialize.java.JavaSerializer$ClassResolverObjectInputStream.resolveClass(JavaSerializer.java:216)

 

Well, the proper solution would be a special serializer, but who has time to do it.

 

So the quick workaround for that is to add org.jboss.msc to app's dependencies.

 

Add the org.jboss.msc module to dependencies. E.g. put this to META-INF/MANIFEST.MF :

Dependencies: org.jboss.msc

 

 

E.g. using this in pom.xml: 

 

    <build>
       ...
       <plugins>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-war-plugin</artifactId>
           <configuration>
              <archive>
                 <manifestEntries>
                    <Dependencies>org.jboss.msc</Dependencies>
                 </manifestEntries>  
              </archive>
           </configuration>
         </plugin>   
       </plugins>
    </build>
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.IExceptionMapper;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.handler.PageProvider;
import org.apache.wicket.request.handler.RenderPageRequestHandler;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.IProvider;

public class MyApplication extends WebApplication {

    /**
     *  Maps exceptions to pages.
     */
    @Override public IProvider<IExceptionMapper> getExceptionMapperProvider() {
        return new IProvider<IExceptionMapper>() {
            @Override public IExceptionMapper get() {
                return new IExceptionMapper() {
                    final DefaultExceptionMapper def = new DefaultExceptionMapper();

                    @Override public IRequestHandler map( Exception ex ) {
                        PageParameters par = new PageParameters().add("ex", ex);

                        // Possible workaround for AS7-4554, WICKET-4785 - only for stateful pages.
                        // java.lang.ClassNotFoundException: org.jboss.msc.service.ServiceName
                        // at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:190)
                        if( ClassNotFoundException.class.isInstance(ex) ){
                            if("org.jboss.msc.service.ServiceName".equals( ((ClassNotFoundException)ex).getMessage() ) )
                                 return new RenderPageRequestHandler(new PageProvider(HomePage.class, par) );
                        }
                        return def.map( ex );                    }
                };
            }
        };
    }

}

 

I plan to create some convenience MapExceptionMapper which would work like

 

new MapExceptionMapper()
   //                     Type, Destination
   .add( Exception.class, ExceptionPage.class )
   .add( ... )

 

Sources:

https://cwiki.apache.org/WICKET/migration-to-wicket-15.html#MigrationtoWicket1.5-Exceptionhandling

 

Exception handling

In Wicket 1.4 it was needed to extend org.apache.wicket.RequestCycle.onRuntimeException(Page, RuntimeException).
Wicket 1.5 gives even better control:

  • add your own org.apache.wicket.request.cycle.IRequestCycleListener (AbstractRequestCycleListener) with org.apache.wicket.Application.getRequestCycleListeners().add() and implement its #onException(RequestCycle, Exception)
  • or override org.apache.wicket.Application.getExceptionMapperProvider() - the IExceptionMapper is used if none of the configured IRequestCycleListeners doesn't know how to handle the exception.
    For information on how the request cycle handles exceptions see RequestCycle in Wicket 1.5 for more information

Filter Blog

By date:
By tag: