Montag, 7. Mai 2012

Using generator for generic class loading in GWT and GAE

This example illustrates how to use com.google.gwt.core.ext.Generator to use reflaction on client-side in GWT/GAE to create new class instances.

German version
Szenario
I created following szenario:

I have controller which handles the navigation of a web application.

Something like that:
@Override
public void onValueChange(ValueChangeEvent<String> event) {
    String token = event.getValue();
    IPresenter presenter = null;
    if (token != null) {
         if (token.equals("ShowEntry")) {
             presenter = new ShowEntryPresenter(new EntryView());
         } else if (token.equals("EditEntry")) {
             presenter = new EditEntryPresenter(new EditView());
         }  

         if (presenter != null) {
             presenter.display();            
         }    
    }
}

Because i develop this application in a team with 100 other developers and the application scales very fast I need a solution, that I don't have to edit the controller every time i've got a new presenter. I want that to by dynamic.

Something like that:

@Override
public void onValueChange(ValueChangeEvent<String> event) {
    String token = event.getValue();
    IPresenter presenter = null;
    if (token != null) {
        if (token.equals("ShowEntry")) {
            presenter = new ShowEntryPresenter(new EntryView());
        } else if (token.equals("EditEntry")) {
            presenter = new EditEntryPresenter(new EditView());
        } else {    
            presenter = Class.forName(token);
        }
        ....
}

The problem: Class.forName() doesn't work on GWT-Client-side.
To solve this problem you can use a generator to generate the java-script code dynamically.

} else {    
    PresenterFactory factory = (PresenterFactory) GWT.create(IReflectiveFactory.class);
    presenter = (IPresenter) factory.newInstance(
        "com.samuelschmid.dynamicpresenters.client." + token);
}

Solution
To make this work, you have to do some stuff:
1. You need a marker interface
2. You need a wrapper interface
3. You need a generator interface
4. You need a factory interface
5. You need a generator class
6. Apply generator to gwt.xml
7. Complie without runtime class casting

1. Marker interface IDynamicPresenter.java
An interface to mark the class which can be loaded dynamically from server to client.

package com.samuelschmid.dynamicpresenters.client

public interface IDynamicPresenter extends IPresenter {
    
}

2. Wrapper interface IFactoryWrapper.java
To mark the class which should be generated with the new generator class

package com.samuelschmid.dynamicpresenters.client

public interface IFactoryWrapper {
    
}

3. Generator interface IReflectiveFactory.java


package com.samuelschmid.dynamicpresenters.client

public interface IReflectiveFactory implements IFactoryWrapper{

}


4. Factory interface IPresenterFactory.java

package com.samuelschmid.dynamicpresenters.client

public interface IPresenterFactory {
 IDynamicPresenter newInstance(String className);
}

5. Generator class PresenterGenerator.java

package com.samuelschmid.dynamicpresenters.server;

import java.io.PrintWriter;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

public class PresenterGenerator extends Generator {

    public String generate(TreeLogger logger, GeneratorContext context, String typeName) 
        throws UnableToCompleteException {

      
        TypeOracle typeOracle = context.getTypeOracle();
        JClassType clazz = typeOracle.findType(typeName);

        if (clazz == null) {
            throw new UnableToCompleteException();
        }

        try {
            
            JClassType reflectableType = typeOracle.getType(
               "com.samuelschmid.dynamicpresenters.client.IDynamicPresenter");

            SourceWriter sourceWriter = getSourceWriter(clazz, context, logger);

            if (sourceWriter != null) {
                sourceWriter.println("public "+
                     reflectableType.getQualifiedSourceName()+ 
                     " newInstance(String className) {");

                JClassType[] types = typeOracle.getTypes();

                int count = 0;
                for (int i = 0; i < types.length; i++) {
                    if (types[i].isInterface() == null   
                        && types[i].isAssignableTo(reflectableType)) {

                        if (count == 0) {
                            sourceWriter.println("   if(\""
                            + types[i].getQualifiedSourceName()
                            + "\".equals(className)) {"
                            + " return new "
                            + types[i].getQualifiedSourceName() + "();"
                            + "}");

                        } else {

                            sourceWriter.println("   else if(\""
                            + types[i].getQualifiedSourceName()
                            + "\".equals(className)) {"
                            + " return new "
                            + types[i].getQualifiedSourceName() + "();"
                            + "}");

                        }

                        count++;

                    }

                }

                sourceWriter.println("return null;");
                sourceWriter.println("}");
                sourceWriter.commit(logger);
                logger.log(TreeLogger.INFO, "Done Generating source for "
                + clazz.getName(), null);

                return clazz.getQualifiedSourceName() + "Wrapper";

            }

        } catch (NotFoundException e) {

            e.printStackTrace();

        }

        return null;

    }

    public SourceWriter getSourceWriter(JClassType classType, 
        GeneratorContext context, TreeLogger logger) {

        String packageName = classType.getPackage().getName();
        String simpleName = classType.getSimpleSourceName() + "Wrapper";
        ClassSourceFileComposerFactory composer =
           new ClassSourceFileComposerFactory(packageName, simpleName);

        composer.addImplementedInterface(
           "com.samuelschmid.dynamicpresenters.client.IPresenterFactory");
        
        PrintWriter printWriter = context.tryCreate(logger, packageName,simpleName);

        if (printWriter == null) {
            return null;
        } else {
            SourceWriter sw = composer.createSourceWriter(context, printWriter);
            return sw;
        }

    }

}


6. *.gwt.xml
Define the generator to be used in your module

...
<generate-with 
  class="com.samuelschmid.dynamicpresenters.server.PresenterGenerator" >
<when-type-assignable 
  class="com.samuelschmid.dynamicpresenters.client.IFactoryWrapper" />
</generate-with>
...

7. No runtime class-casting
Last but not least, that this code runs on all browsers, you must disable runtime class casting!
Add -XdisableCastChecking to the compiler arguments.

Final thoughts
I found the solution for this real problem on the following blog:
 http://programmatica.blogspot.com/2007/10/classfornamefullyqualifiedclassnamenewi.html

To the fact that this post is 5 years old, i thought it might be good to make a new post - it's still state-of-the-art.

3 Kommentare: