I am using the GWT Activities and Places framework to structure my application and it is turning out nicely. One thing that annoys me though is that the ActivityMapper
Just for future reference for people like me landed here and still have no answer. I´ve ended up with the following solution using GIN and Generators for a PlaceFactory.
This is what my tokens look like now. eg: #EditUser/id:15/type:Agent
I have a AbstractPlace that every Place should extend from.
public abstract class AbstractPlace extends Place {
public abstract Activity getActivity();
}
An example Place:
public class EditUserPlace extends AbstractPlace {
private Long id;
private User.Type type;
//getters and setters
@Override
public Activity getActivity() {
return App.getClientFactory().getEditUserPresenter().withPlace(this);
}
}
PlaceFactory interface for Defered binding:
public interface PlaceFactory {
Place fromToken(String token);
String toToken(Place place);
}
and a annotation for registering place classes
public @interface WithPlaces {
Class<? extends Place>[] value() default {};
}
and the PlaceFactoryGenerator
Setup the generator in you GWT Module
<generate-with class="app.rebind.place.PlaceFactoryGenerator">
<when-type-assignable class="app.client.common.AppPlaceFactory"/>
</generate-with>
public class PlaceFactoryGenerator extends Generator {
private TreeLogger logger;
private TypeOracle typeOracle;
private JClassType interfaceType;
private String packageName;
private String implName;
private Class<? extends Place>[] placeTypes;
@Override
public String generate(TreeLogger logger,
GeneratorContext generatorContext, String interfaceName)
throws UnableToCompleteException {
this.logger = logger;
this.typeOracle = generatorContext.getTypeOracle();
this.interfaceType = typeOracle.findType(interfaceName);
this.packageName = interfaceType.getPackage().getName();
this.implName = interfaceType.getName().replace(".", "_") + "Impl";
// TODO Trocar annotation por scan
WithPlaces places = interfaceType.getAnnotation(WithPlaces.class);
assert (places != null);
Class<? extends Place>[] placeTypes = places.value();
this.placeTypes = placeTypes;
PrintWriter out = generatorContext.tryCreate(logger,
packageName, implName);
if (out != null) {
generateOnce(generatorContext, out);
}
return packageName + "." + implName;
}
private void generateOnce(GeneratorContext generatorContext, PrintWriter out) {
TreeLogger logger = this.logger.branch(
TreeLogger.DEBUG,
String.format("Generating implementation of %s",
interfaceType));
ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
packageName, implName);
factory.addImport(interfaceType.getQualifiedSourceName());
factory.addImplementedInterface(interfaceType.getSimpleSourceName());
factory.addImport(StringBuilder.class.getCanonicalName());
factory.addImport(Map.class.getCanonicalName());
factory.addImport(HashMap.class.getCanonicalName());
factory.addImport(Place.class.getCanonicalName());
for (Class<? extends Place> place : placeTypes)
factory.addImport(place.getCanonicalName());
SourceWriter sw = factory.createSourceWriter(generatorContext, out);
sw.println("public Place fromToken(String token) {");
sw.indent();
sw.println("int barAt = token.indexOf('/');");
sw.println("String placeName = token;");
sw.println("Map<String, String> params = new HashMap<String, String>();");
sw.println("if (barAt > 0) {");
sw.indent();
sw.println("placeName = token.substring(0, barAt);");
sw.println("String[] keyValues = token.substring(barAt + 1).split(\"/\");");
sw.println("for (String item : keyValues) {");
sw.indent();
sw.println("int colonAt = item.indexOf(':');");
sw.println("if (colonAt > 0) {");
sw.indent();
sw.println("String key = item.substring(0, colonAt);");
sw.println("String value = item.substring(colonAt + 1);");
sw.println("params.put(key, value);");
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("}\n");
for (Class<? extends Place> placeType : placeTypes) {
String placeTypeName = placeType.getSimpleName();
int replaceStrPos = placeTypeName.lastIndexOf("Place");
String placeName = placeTypeName.substring(0, replaceStrPos);
sw.println("if (placeName.equals(\"%s\")) {", placeName);
sw.indent();
sw.println("%s place = new %s();", placeTypeName, placeTypeName);
generateSetExpressions(sw, placeType);
sw.println("return place;");
sw.outdent();
sw.println("}\n");
}
sw.println("return null;");
sw.outdent();
sw.println("}\n");
sw.println("public String toToken(Place place) {");
sw.indent();
sw.println("StringBuilder token = new StringBuilder();\n");
for (Class<? extends Place> placeType : placeTypes) {
String placeTypeName = placeType.getSimpleName();
int replaceStrPos = placeTypeName.lastIndexOf("Place");
String placeName = placeTypeName.substring(0, replaceStrPos);
sw.println("if (place instanceof %s) {", placeTypeName);
sw.indent();
sw.println("%s newPlace = (%s)place;", placeTypeName, placeTypeName);
sw.println("token.append(\"%s\");", placeName);
generateTokenExpressions(sw, placeType);
sw.println("return token.toString();");
sw.outdent();
sw.println("}\n");
}
sw.println("return token.toString();");
sw.outdent();
sw.println("}\n");
sw.outdent();
sw.println("}");
generatorContext.commit(logger, out);
}
private void generateTokenExpressions(SourceWriter sw,
Class<? extends Place> placeType) {
for (Field field : placeType.getDeclaredFields()) {
char[] fieldName = field.getName().toCharArray();
fieldName[0] = Character.toUpperCase(fieldName[0]);
String getterName = "get" + new String(fieldName);
sw.println("token.append(\"/%s:\");", field.getName());
sw.println("token.append(newPlace.%s().toString());", getterName);
}
}
private void generateSetExpressions(SourceWriter sw, Class<? extends Place> placeType) {
for (Field field : placeType.getDeclaredFields()) {
char[] fieldName = field.getName().toCharArray();
fieldName[0] = Character.toUpperCase(fieldName[0]);
String setterName = "set" + new String(fieldName);
List<Method> methods = findMethods(placeType, setterName);
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0 || parameterTypes.length > 1)
continue;
Class<?> parameterType = parameterTypes[0];
String exp = "%s";
if (parameterType == Character.class) {
exp = "%s.charAt(0)";
} else if (parameterType == Boolean.class) {
exp = "Boolean.parseBoolean(%s)";
} else if (parameterType == Byte.class) {
exp = "Byte.parseInt(%s)";
} else if (parameterType == Short.class) {
exp = "Short.parseShort(%s)";
} else if (parameterType == Integer.class) {
exp = "Integer.parseInt(%s)";
} else if (parameterType == Long.class) {
exp = "Long.parseLong(%s)";
} else if (parameterType == Float.class) {
exp = "Float.parseFloat(%s)";
} else if (parameterType == Double.class) {
exp = "Double.parseDouble(%s)";
} else if (parameterType.getSuperclass().isAssignableFrom(Enum.class)) {
exp = parameterType.getCanonicalName() + ".valueOf(%s)";
} else if (parameterType != String.class){
continue;
}
String innerExp = String.format("params.get(\"%s\")", field.getName());
String wrapperExp = String.format(exp, innerExp);
sw.println("place.%s(%s);", setterName, wrapperExp);
}
}
}
private List<Method> findMethods(Class<? extends Place> placeType, String name) {
Method[] methods = placeType.getMethods();
List<Method> found = new ArrayList<Method>();
for (Method method : methods) {
if (method.getName().equals(name)) {
found.add(method);
}
}
return found;
}
}
Whats my ActivityMapper look like?
public class AppActivityMapper implements ActivityMapper {
public Activity getActivity(Place place) {
AbstractPlace abstractPlace = (AbstractPlace)place;
return abstractPlace.getActivity();
}
}
We need a custom PlaceHistoryMapper
public class AppPlaceHistoryMapper implements PlaceHistoryMapper {
private AppPlaceFactory placeFactory = GWT.create(AppPlaceFactory.class);
public Place getPlace(String token) {
return placeFactory.fromToken(token);
}
public String getToken(Place place) {
return placeFactory.toToken(place);
}
}
And at last the PlaceFactory, this will be generated for just put your place classes in the annotation and be happy!
@WithPlaces(value = {
HomePlace.class,
EditUserPlace.class
})
public interface AppPlaceFactory extends PlaceFactory {
}
I found a neat approach by Igor Klimer. He uses the visitor pattern to push the decision logic into the Place implementation, that way the ActivityMapper stays quite simple. Check out his blog post for implementation details.
First of all I created an issue on GWT issuesfor this so please star it and or comment on it. Here's how I do it:
public abstract class PlaceWithActivity extends Place {
public Activity getActivity();
}
Then in your ActivityMapper:
Public Activity get Activity(Place newPlace) {
return ((PlaceWithActivity) newPlace).getActivity();
}
All of your places should extend PlaceWithActivity. The only issue is the down-casting which risks a ClassCastException. Place had the getActivity() then you wouldn't have to downcast but it doesn't so you have to downcast it to a class that does.
What I don't like about it is that you have to do casting and make the PlaceWithActivity class. This wouldn't be necessary, if GWT would add support for what I'm doing. If they included a PlaceWithActivity class you wouldn't have to make it and if the ActivityManager would just call the getActivity() method of the PlaceWithActivity class then you would not only not have to down cast, but you wouldn't even need to write the ActivityMapper!
There isn't a great answer yet. I have code generation schemes in mind, but it's all scribbles on white boards at the moment. For Gin users it seems like a Place Scope might be handy.
Re: the if / else cascade, one common approach is to make your Place objects implement the visitor pattern. E.g. let's assume you have AssistedInject set up for your activities (and forgive the sloppy field injection, it's just a sketch).
class BasePlace extends Place {
<T> T acceptFilter(PlaceFilter filter);
}
interface PlaceFilter<T> {
T filter(FooPlace place);
T filter(BarPlace place);
T filter(BazPlace place);
}
public class MainActivities implements ActivityMapper {
@Inject FooFactory fooMaker;
@Inject BarFactory barMaker;
@Inject BazFactory bazMaker;
public Activity getActivity(PlaceChangeEvent e) {
return ((BasePlace)e.getPlace()).acceptFilter(
new PlaceFilter<Activity>() {
Activity filter(FooPlace place) {
return fooMaker.create(place);
}
Activity filter(BarPlace place) {
return barMaker.create(place);
}
Activity filter(BazPlace place) {
return bazMaker.create(place);
}
})
}
}
Actually I am using custom boilerplate code for this task:
public class PuksaActivityMapper implements ActivityMapper {
private HashMap<String, ActivityContainer> mappings;
@Inject
private SearchResultActivityContainer searchResultContainer;
@Inject
private HelloActivityContainer helloContainer;
@Override
public Activity getActivity(Place place) {
ActivityContainer container = getMappings().get(place.getClass().getName());
return container.getActivity(place);
}
public HashMap<String, ActivityContainer> getMappings() {
if (mappings == null) {
mappings = new HashMap<String, ActivityContainer>();
mappings.put(ShowResultsPlace.class.getName(), searchResultContainer);
mappings.put(HelloPlace.class.getName(), helloContainer);
}
return mappings;
}
}
Where ActivityContainer is a simple factory type (from this point classic ioc methods can be used).
Of course now it is only changing 'if block' with a map lookup/population, but combined with Gin multibinding (witch currently does not exist) could do it's job.
Also Gin enhancement - generic GinModule for GWT Activity/Places looks promising.
One possibility is to have the root of your Place class hierarchy define a createActivity() method, and Place subclasses can return a new instance of the Activity they're associated with.
@Override
public Activity getActivity(Place place) {
return ((BaseAppPlace)place).createActivity();
}
This has the advantage of having eliminated that if/else block, and having one less place to modify when you add a new Place/Activity. The downside of this is that it kindof pollutes your Place class with Activity creation behavior, even if you're just delegating to a Ginjector.