Montag, 7. Mai 2012

Verwenden eines Generators zum generischen Klassen laden mit GWT und GAE


Ein Beispiel wie man die Klasse com.google.gwt.core.ext.Generator verwenden kann, um auf Client-Seite von GWT und GAE Java-Reflection zu verwenden kann.

English version
Szenario
Ich habe ein kleine Szenario erstellt:

Ich habe eine Controller, welche die Navigation der Web-Application behandelt.

Etwa so:
@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();            
         }    
    }
}

Weil unsere Web-Applikation von einem grösseren Entwicklungsteam erweitert wird, brauche ich eine lösung, damit ich nicht immer diesen Controller anpassen muss, wenn ich einen neuen Presenter publizieren will.

Am liebsten würde ich Java-Reflection verwenden, etwa so:

@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);
        }
        ....
}

Das Problem: Class.forName() funktioniert beim GWT-Client nicht.
Deswegen müssen wir einen Generator schreiben, welcher uns diese Operation ermöglicht.

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

Solution
Folgendes muss gemacht werden:
1. Wir brauchen ein Markierungs-Interface
2. Wir brauchen ein Wrapper-Interface
3. Wir brauchen ein Generator-Interface
4. Wir brauchen ein Fabrik-Interface
5. Wir brauchen eine Generator Klasse
6. Wir müssen den Generator im gwt.xml anwenden.
7. Wir müssen die Applikation ohne Runtime Class Casting kompilieren

1. IDynamicPresenter.java
Ein Interface um die dynamisch ladbaren Klassen zu markieren.

package com.samuelschmid.dynamicpresenters.client

public interface IDynamicPresenter extends IPresenter {
    
}

2. Wrapper interface IFactoryWrapper.java

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
Definier den Generator im module.gwt.xml

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

7. Kein Runtime ClassCasting
Zu guter letzt muss noch das Runtime Class Casting deaktiviert werden.
Dazu muss -XdisableCastChecking als Compiler-Argument verwendet werden.

Fazit
Ich habe die Lösung anhand folgendes Blog-Eintrags umgesetzt.
 http://programmatica.blogspot.com/2007/10/classfornamefullyqualifiedclassnamenewi.html

Der Fakt, dass dieser Eintrag schon 5 Jahre alt ist, hat mich dazu bewogen eine Neuauflage zu schrieben, welche auch mit allen gängigen Browsern funktioniert.

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.