Following up on https://jira.springsource.org/browse/ROO-1276, here's the
proposed alternate design, separating the "keep history and places in sync" and
"map places to/from history tokens" concerns into a concrete PlaceHistoryHandler
class and a PlaceHistoryMapper interface whose implementation can be generated
based on annotations.

This patch superseeds http://gwt-code-reviews.appspot.com/824801/show and
integrates the changes from http://gwt-code-reviews.appspot.com/827801/show (if
it weren't the handler -> mapper renaming, they could have been kept separate).

Derived from http://gwt-code-reviews.appspot.com/845802, but without the
ActivityMapperGenerator bits.

Review by: rjrjr@google.com
Patch by: t.broyer@gmail.com


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8817 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/app/App.gwt.xml b/user/src/com/google/gwt/app/App.gwt.xml
index f894ed4..0487388 100644
--- a/user/src/com/google/gwt/app/App.gwt.xml
+++ b/user/src/com/google/gwt/app/App.gwt.xml
@@ -9,10 +9,7 @@
   <generate-with class="com.google.gwt.app.rebind.EditorSupportGenerator">
     <when-type-assignable class='com.google.gwt.app.client.EditorSupport'/>
   </generate-with>
-  <generate-with class="com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator">
-    <when-type-assignable class="com.google.gwt.app.place.PlaceHistoryHandler"/>
-  </generate-with>
-  <generate-with class="com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator">
-    <when-type-assignable class="com.google.gwt.app.place.PlaceHistoryHandlerWithFactory"/>
+  <generate-with class="com.google.gwt.app.rebind.PlaceHistoryMapperGenerator">
+    <when-type-assignable class="com.google.gwt.app.place.PlaceHistoryMapper"/>
   </generate-with>
 </module>
diff --git a/user/src/com/google/gwt/app/place/AbstractPlaceHistoryHandler.java b/user/src/com/google/gwt/app/place/AbstractPlaceHistoryHandler.java
deleted file mode 100644
index 5702467..0000000
--- a/user/src/com/google/gwt/app/place/AbstractPlaceHistoryHandler.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2010 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.app.place;
-
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.History;
-
-import java.util.logging.Logger;
-
-/**
- * Abstract implementation of {@link PlaceHistoryHandler}.
- * 
- * @param <F> the factory type
- */
-public abstract class AbstractPlaceHistoryHandler<F> implements
-    PlaceHistoryHandlerWithFactory<F> {
-  private static final Logger log = Logger.getLogger(AbstractPlaceHistoryHandler.class.getName());
-
-  /**
-   * Return value for
-   * {@link AbstractPlaceHistoryHandler#getPrefixAndToken(Place)}.
-   */
-  public static class PrefixAndToken {
-    public final String prefix;
-    public final String token;
-
-    public PrefixAndToken(String prefix, String token) {
-      super();
-      this.prefix = prefix;
-      this.token = token;
-    }
-
-    @Override
-    public String toString() {
-      return prefix + ":" + token;
-    }
-  }
-
-  /**
-   * Isolates us from History, for testing.
-   */
-  protected interface Historian {
-    HandlerRegistration addValueChangeHandler(
-        ValueChangeHandler<String> valueChangeHandler);
-
-    String getToken();
-
-    void newItem(String token, boolean issueEvent);
-  }
-
-  private final Historian historian;
-
-  protected F factory;
-
-  protected PlaceController placeController;
-
-  private Place defaultPlace = Place.NOWHERE;
-
-  protected AbstractPlaceHistoryHandler() {
-    this(new Historian() {
-      public HandlerRegistration addValueChangeHandler(
-          ValueChangeHandler<String> valueChangeHandler) {
-        return History.addValueChangeHandler(valueChangeHandler);
-      }
-
-      public String getToken() {
-        return History.getToken();
-      }
-
-      public void newItem(String token, boolean issueEvent) {
-        History.newItem(token, issueEvent);
-      }
-    });
-  }
-
-  protected AbstractPlaceHistoryHandler(Historian historian) {
-    this.historian = historian;
-  }
-
-  public void handleCurrentHistory() {
-    handleHistoryToken(historian.getToken());
-  }
-
-  public HandlerRegistration register(PlaceController placeController,
-      EventBus eventBus, Place defaultPlace) {
-    assert factory != null : "No factory was set";
-    this.placeController = placeController;
-    this.defaultPlace = defaultPlace;
-
-    final HandlerRegistration placeReg = eventBus.addHandler(
-        PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
-          public void onPlaceChange(PlaceChangeEvent event) {
-            Place newPlace = event.getNewPlace();
-            historian.newItem(tokenForPlace(newPlace), false);
-          }
-        });
-
-    final HandlerRegistration historyReg = historian.addValueChangeHandler(new ValueChangeHandler<String>() {
-      public void onValueChange(ValueChangeEvent<String> event) {
-        String token = event.getValue();
-        handleHistoryToken(token);
-      }
-    });
-
-    return new HandlerRegistration() {
-      public void removeHandler() {
-        AbstractPlaceHistoryHandler.this.defaultPlace = Place.NOWHERE;
-        AbstractPlaceHistoryHandler.this.placeController = null;
-        AbstractPlaceHistoryHandler.this.factory = null;
-        placeReg.removeHandler();
-        historyReg.removeHandler();
-      }
-    };
-  }
-
-  public void setFactory(F factory) {
-    this.factory = factory;
-  }
-
-  /**
-   * @param newPlace what needs tokenizing
-   * @return the token, or null
-   */
-  protected abstract PrefixAndToken getPrefixAndToken(Place newPlace);
-
-  /**
-   * @param prefix the prefix found on the history token
-   * @return the PlaceTokenizer registered with that token, or null
-   */
-  protected abstract PlaceTokenizer<?> getTokenizer(String prefix);
-
-  /**
-   * Visible for testing.
-   */
-  Logger log() {
-    return log;
-  }
-
-  private void handleHistoryToken(String token) {
-
-    Place newPlace = null;
-
-    if ("".equals(token)) {
-      newPlace = defaultPlace;
-    }
-
-    if (newPlace == null) {
-      int colonAt = token.indexOf(':');
-      if (colonAt > -1) {
-        String initial = token.substring(0, colonAt);
-        String rest = token.substring(colonAt + 1);
-        PlaceTokenizer<?> tokenizer = getTokenizer(initial);
-        if (tokenizer != null) {
-          newPlace = tokenizer.getPlace(rest);
-        }
-      }
-    }
-
-    if (newPlace == null) {
-      log().warning("Unrecognized history token: " + token);
-      newPlace = defaultPlace;
-    }
-
-    placeController.goTo(newPlace);
-  }
-
-  private String tokenForPlace(Place newPlace) {
-    if (defaultPlace.equals(newPlace)) {
-      return "";
-    }
-
-    PrefixAndToken token = getPrefixAndToken(newPlace);
-    if (token != null) {
-      return token.toString();
-    }
-
-    log().warning("Unregistered place type : " + newPlace);
-    return "";
-  }
-
-}
diff --git a/user/src/com/google/gwt/app/place/PlaceHistoryHandler.java b/user/src/com/google/gwt/app/place/PlaceHistoryHandler.java
index e44e8a6..895d496 100644
--- a/user/src/com/google/gwt/app/place/PlaceHistoryHandler.java
+++ b/user/src/com/google/gwt/app/place/PlaceHistoryHandler.java
@@ -15,27 +15,150 @@
  */
 package com.google.gwt.app.place;
 
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.History;
+
+import java.util.logging.Logger;
 
 /**
- * Implemented by objects that monitor {@link PlaceChangeEvent}s and
- * {@link com.google.gwt.user.client.History} events and keep them in sync.
+ * Monitors {@link PlaceChangeEvent}s and {@link com.google.gwt.user.client.History} events and keep them in sync.
  */
-public interface PlaceHistoryHandler {
-  /**
-   * Sets the current place from current history token, e.g. in case of being
-   * launched from a bookmark.
-   */
-  void handleCurrentHistory();
+public class PlaceHistoryHandler {
+  private static final Logger log = Logger.getLogger(PlaceHistoryHandler.class.getName());
 
   /**
-   * Registers this {@link PlaceHistoryHandler} with the event bus, and sets its
-   * default place (where to go when there is no history token).
-   * 
-   * @return registration object to deregister and reset the default place to
-   *         {@link Place#NOWHERE}.
+   * Default implementation of {@link Historian}, based on {@link History}.
    */
-  HandlerRegistration register(PlaceController placeController, EventBus eventBus,
-      Place defaultPlace);
+  public static class DefaultHistorian implements Historian {
+    public HandlerRegistration addValueChangeHandler(
+        ValueChangeHandler<String> valueChangeHandler) {
+      return History.addValueChangeHandler(valueChangeHandler);
+    }
+
+    public String getToken() {
+      return History.getToken();
+    }
+
+    public void newItem(String token, boolean issueEvent) {
+      History.newItem(token, issueEvent);
+    }
+  }
+
+  /**
+   * Optional delegate in charge of History related events. Provides nice
+   * isolation for unit testing, and allows pre- or post-processing of tokens.
+   */
+  public interface Historian {
+    HandlerRegistration addValueChangeHandler(
+        ValueChangeHandler<String> valueChangeHandler);
+
+    String getToken();
+
+    void newItem(String token, boolean issueEvent);
+  }
+
+  private final Historian historian;
+
+  private final PlaceHistoryMapper mapper;
+
+  private PlaceController placeController;
+
+  private Place defaultPlace = Place.NOWHERE;
+
+  /**
+   * Create a new PlaceHistoryHandler with a {@link DefaultHistorian}.
+   * The DefaultHistorian is created via a call to GWT.create(), so an
+   * alternative default implementation can be provided through
+   * &lt;replace-with> rules in a gwt.xml file.
+   */
+  public PlaceHistoryHandler(PlaceHistoryMapper mapper) {
+    this(mapper, (Historian) GWT.create(DefaultHistorian.class));
+  }
+
+  /**
+   * Create a new PlaceHistoryHandler.
+   */
+  public PlaceHistoryHandler(PlaceHistoryMapper mapper, Historian historian) {
+    this.mapper = mapper;
+    this.historian = historian;
+  }
+
+  public void handleCurrentHistory() {
+    handleHistoryToken(historian.getToken());
+  }
+
+  public HandlerRegistration register(PlaceController placeController,
+      EventBus eventBus, Place defaultPlace) {
+    this.placeController = placeController;
+    this.defaultPlace = defaultPlace;
+
+    final HandlerRegistration placeReg = eventBus.addHandler(
+        PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
+          public void onPlaceChange(PlaceChangeEvent event) {
+            Place newPlace = event.getNewPlace();
+            historian.newItem(tokenForPlace(newPlace), false);
+          }
+        });
+
+    final HandlerRegistration historyReg = historian.addValueChangeHandler(new ValueChangeHandler<String>() {
+      public void onValueChange(ValueChangeEvent<String> event) {
+        String token = event.getValue();
+        handleHistoryToken(token);
+      }
+    });
+
+    return new HandlerRegistration() {
+      public void removeHandler() {
+        PlaceHistoryHandler.this.defaultPlace = Place.NOWHERE;
+        PlaceHistoryHandler.this.placeController = null;
+        placeReg.removeHandler();
+        historyReg.removeHandler();
+      }
+    };
+  }
+
+  /**
+   * Visible for testing.
+   */
+  Logger log() {
+    return log;
+  }
+
+  private void handleHistoryToken(String token) {
+
+    Place newPlace = null;
+
+    if ("".equals(token)) {
+      newPlace = defaultPlace;
+    }
+
+    if (newPlace == null) {
+      newPlace = mapper.getPlace(token);
+    }
+
+    if (newPlace == null) {
+      log().warning("Unrecognized history token: " + token);
+      newPlace = defaultPlace;
+    }
+
+    placeController.goTo(newPlace);
+  }
+
+  private String tokenForPlace(Place newPlace) {
+    if (defaultPlace.equals(newPlace)) {
+      return "";
+    }
+
+    String token = mapper.getToken(newPlace);
+    if (token != null) {
+      return token;
+    }
+
+    log().warning("Place not mapped to a token: " + newPlace);
+    return "";
+  }
 }
diff --git a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java b/user/src/com/google/gwt/app/place/PlaceHistoryMapper.java
similarity index 63%
copy from user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
copy to user/src/com/google/gwt/app/place/PlaceHistoryMapper.java
index cdc38ab..59a27a1 100644
--- a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
+++ b/user/src/com/google/gwt/app/place/PlaceHistoryMapper.java
@@ -16,12 +16,17 @@
 package com.google.gwt.app.place;
 
 /**
- * A PlaceHistoryHandler that can get its {@link PlaceTokenizer} instances from
- * a factory.
- * 
- * @param <F> factory type
+ * <p>
+ * <span style="color:red">Experimental API: This class is still under rapid
+ * development, and is very likely to be deleted. Use it at your own risk.
+ * </span>
+ * </p>
+ * Maps {@link Place}s to/from tokens, used to configure a
+ * {@link PlaceHistoryHandler}.
  */
-public interface PlaceHistoryHandlerWithFactory<F> extends PlaceHistoryHandler {
+public interface PlaceHistoryMapper {
 
-  void setFactory(F factory);
+  Place getPlace(String token);
+  
+  String getToken(Place place);
 }
diff --git a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java b/user/src/com/google/gwt/app/place/PlaceHistoryMapperWithFactory.java
similarity index 81%
rename from user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
rename to user/src/com/google/gwt/app/place/PlaceHistoryMapperWithFactory.java
index cdc38ab..22c0b02 100644
--- a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
+++ b/user/src/com/google/gwt/app/place/PlaceHistoryMapperWithFactory.java
@@ -16,12 +16,12 @@
 package com.google.gwt.app.place;
 
 /**
- * A PlaceHistoryHandler that can get its {@link PlaceTokenizer} instances from
+ * A PlaceHistoryMapper that can get its {@link PlaceTokenizer} instances from
  * a factory.
  * 
  * @param <F> factory type
  */
-public interface PlaceHistoryHandlerWithFactory<F> extends PlaceHistoryHandler {
+public interface PlaceHistoryMapperWithFactory<F> extends PlaceHistoryMapper {
 
   void setFactory(F factory);
 }
diff --git a/user/src/com/google/gwt/app/place/Prefix.java b/user/src/com/google/gwt/app/place/Prefix.java
index 4c9808c..ce68c3b 100644
--- a/user/src/com/google/gwt/app/place/Prefix.java
+++ b/user/src/com/google/gwt/app/place/Prefix.java
@@ -24,7 +24,7 @@
  * Indicates the prefix to use when the token written by
  * {@link PlaceTokenizer#getToken(Place)} is written to
  * {@link com.google.gwt.user.client.History#newItem}.
- * {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator} looks
+ * {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator} looks
  * for this annotation on the factory methods that return a tokenizer, and on
  * the tokenizer types themselves.
  */
diff --git a/user/src/com/google/gwt/app/place/WithTokenizers.java b/user/src/com/google/gwt/app/place/WithTokenizers.java
index df64d75..d592055 100644
--- a/user/src/com/google/gwt/app/place/WithTokenizers.java
+++ b/user/src/com/google/gwt/app/place/WithTokenizers.java
@@ -22,8 +22,8 @@
 
 /**
  * Indicates {@link PlaceTokenizer} types used by an implementation of
- * {@link PlaceHistoryHandler} generated by
- * {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * {@link PlaceHistoryMapper} generated by
+ * {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 @Target({ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
diff --git a/user/src/com/google/gwt/app/place/impl/AbstractPlaceHistoryMapper.java b/user/src/com/google/gwt/app/place/impl/AbstractPlaceHistoryMapper.java
new file mode 100644
index 0000000..0c9794a
--- /dev/null
+++ b/user/src/com/google/gwt/app/place/impl/AbstractPlaceHistoryMapper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.place.impl;
+
+import com.google.gwt.app.place.Place;
+import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;
+import com.google.gwt.app.place.PlaceTokenizer;
+
+/**
+ * Abstract implementation of {@link PlaceHistoryMapper}.
+ *
+ * @param <F> factory type
+ */
+public abstract class AbstractPlaceHistoryMapper<F> implements
+    PlaceHistoryMapperWithFactory<F> {
+
+  /**
+   * Return value for
+   * {@link AbstractPlaceHistoryMapper#getPrefixAndToken(Place)}.
+   */
+  public static class PrefixAndToken {
+    public final String prefix;
+    public final String token;
+
+    public PrefixAndToken(String prefix, String token) {
+      assert prefix != null && !prefix.contains(":");
+      this.prefix = prefix;
+      this.token = token;
+    }
+
+    @Override
+    public String toString() {
+      return prefix.isEmpty() ? token : prefix + ":" + token;
+    }
+  }
+
+  protected F factory;
+  
+  public Place getPlace(String token) {
+    int colonAt = token.indexOf(':');
+    if (colonAt > 0) {
+      String initial = token.substring(0, colonAt);
+      String rest = token.substring(colonAt + 1);
+      PlaceTokenizer<?> tokenizer = getTokenizer(initial);
+      if (tokenizer != null) {
+        return tokenizer.getPlace(rest);
+      }
+    }
+    return null;
+  }
+  
+  public String getToken(Place place) {
+    PrefixAndToken token = getPrefixAndToken(place);
+    if (token != null) {
+      return token.toString();
+    }
+    return null;
+  }
+  
+  public void setFactory(F factory) {
+    this.factory = factory;
+  }
+
+  /**
+   * @param newPlace what needs tokenizing
+   * @return the token, or null
+   */
+  protected abstract PrefixAndToken getPrefixAndToken(Place newPlace);
+
+  /**
+   * @param prefix the prefix found on the history token
+   * @return the PlaceTokenizer registered with that token, or null
+   */
+  protected abstract PlaceTokenizer<?> getTokenizer(String prefix);
+}
diff --git a/user/src/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparator.java b/user/src/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparator.java
new file mode 100644
index 0000000..477de65
--- /dev/null
+++ b/user/src/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.rebind;
+
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+import java.util.Comparator;
+
+/**
+ * Sorts types from most derived to least derived, falling back to alphabetical
+ * sorting.
+ */
+public class MostToLeastDerivedPlaceTypeComparator implements
+    Comparator<JClassType> {
+  public int compare(JClassType o1, JClassType o2) {
+    if (o1.equals(o2)) {
+      return 0;
+    }
+    if (o1.isAssignableFrom(o2)) {
+      return 1;
+    }
+    if (o1.isAssignableTo(o2)) {
+      return -1;
+    }
+    return o1.getQualifiedSourceName().compareTo(o2.getQualifiedSourceName());
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/app/rebind/PlaceHistoryGeneratorContext.java b/user/src/com/google/gwt/app/rebind/PlaceHistoryGeneratorContext.java
index 7428fdc..62cdd00 100644
--- a/user/src/com/google/gwt/app/rebind/PlaceHistoryGeneratorContext.java
+++ b/user/src/com/google/gwt/app/rebind/PlaceHistoryGeneratorContext.java
@@ -15,11 +15,10 @@
  */
 package com.google.gwt.app.rebind;
 
-import com.google.gwt.app.place.PlaceHistoryHandlerWithFactory;
+import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;
 import com.google.gwt.app.place.PlaceTokenizer;
 import com.google.gwt.app.place.Prefix;
 import com.google.gwt.app.place.WithTokenizers;
-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;
@@ -33,23 +32,20 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
-import java.util.Map.Entry;
+import java.util.TreeMap;
 
 class PlaceHistoryGeneratorContext {
   static PlaceHistoryGeneratorContext create(TreeLogger logger,
-      GeneratorContext generatorContext, String interfaceName)
+      TypeOracle typeOracle, String interfaceName)
       throws UnableToCompleteException {
-    TypeOracle typeOracle = generatorContext.getTypeOracle();
     JClassType stringType = requireType(typeOracle, String.class);
     JClassType placeTokenizerType = requireType(typeOracle,
         PlaceTokenizer.class);
-    JClassType placeHistoryHandlerWithFactoryType = requireType(typeOracle,
-        PlaceHistoryHandlerWithFactory.class);
+    JClassType placeHistoryMapperWithFactoryType = requireType(typeOracle,
+        PlaceHistoryMapperWithFactory.class);
 
     JClassType factoryType;
 
@@ -66,26 +62,25 @@
       throw new UnableToCompleteException();
     }
 
-    factoryType = findFactoryType(placeHistoryHandlerWithFactoryType,
+    factoryType = findFactoryType(placeHistoryMapperWithFactoryType,
         interfaceType);
 
     String implName = interfaceType.getName().replace(".", "_") + "Impl";
 
-    return new PlaceHistoryGeneratorContext(logger, generatorContext,
-        interfaceType, factoryType, stringType, placeTokenizerType,
-        placeHistoryHandlerWithFactoryType,
+    return new PlaceHistoryGeneratorContext(logger, typeOracle, interfaceType,
+        factoryType, stringType, placeTokenizerType,
         interfaceType.getPackage().getName(), implName);
   }
 
   private static JClassType findFactoryType(
-      JClassType placeHistoryHandlerWithFactoryType, JClassType interfaceType) {
+      JClassType placeHistoryMapperWithFactoryType, JClassType interfaceType) {
     JClassType superInterfaces[] = interfaceType.getImplementedInterfaces();
 
     for (JClassType superInterface : superInterfaces) {
       JParameterizedType parameterizedType = superInterface.isParameterized();
       if (parameterizedType != null
           && parameterizedType.getBaseType().equals(
-              placeHistoryHandlerWithFactoryType)) {
+              placeHistoryMapperWithFactoryType)) {
         return parameterizedType.getTypeArgs()[0];
       }
     }
@@ -104,10 +99,9 @@
   final JClassType stringType;
 
   final JClassType placeTokenizerType;
-  final JClassType placeHistoryHandlerType;
 
   final TreeLogger logger;
-  final GeneratorContext generatorContext;
+  final TypeOracle typeOracle;
   final JClassType interfaceType;
   final JClassType factoryType;
 
@@ -116,79 +110,115 @@
   final String packageName;
 
   /**
-   * All factory getters that can provide tokenizers, by prefix.
+   * All tokenizers, either as a {@link JMethod} for factory getters or as a
+   * {@link JClassType} for types that must be GWT.create()d, by prefix.
    */
-  private LinkedHashMap<String, JMethod> tokenizerGetters;
+  private HashMap<String, Object> tokenizers;
 
   /**
-   * All tokenizer types that must be GWT.create()d, by prefix.
+   * All place types and the prefix of their associated tokenizer, ordered from
+   * most-derived to least-derived type (and falling back to the natural
+   * ordering of their names).
    */
-  private LinkedHashMap<String, JClassType> tokenizersWithNoGetters;
+  private TreeMap<JClassType, String> placeTypes = new TreeMap<JClassType, String>(
+      new MostToLeastDerivedPlaceTypeComparator());
 
-  /**
-   * Cache of all tokenizer types, union of the entries in tokenizerGetters and
-   * tokenizersWithNoGetters.
-   */
-  private LinkedHashMap<String, JClassType> tokenizerTypes;
-
-  PlaceHistoryGeneratorContext(TreeLogger logger,
-      GeneratorContext generatorContext, JClassType interfaceType,
-      JClassType factoryType, JClassType stringType,
-      JClassType placeTokenizerType, JClassType placeHistoryHandlerType,
-      String packageName, String implName) {
+  PlaceHistoryGeneratorContext(TreeLogger logger, TypeOracle typeOracle,
+      JClassType interfaceType, JClassType factoryType, JClassType stringType,
+      JClassType placeTokenizerType, String packageName, String implName) {
     this.logger = logger;
-    this.generatorContext = generatorContext;
+    this.typeOracle = typeOracle;
     this.interfaceType = interfaceType;
     this.factoryType = factoryType;
     this.stringType = stringType;
     this.placeTokenizerType = placeTokenizerType;
-    this.placeHistoryHandlerType = placeHistoryHandlerType;
     this.packageName = packageName;
     this.implName = implName;
   }
 
-  public JClassType getPlaceType(String prefix)
+  public Set<JClassType> getPlaceTypes() throws UnableToCompleteException {
+    ensureInitialized();
+    return placeTypes.keySet();
+  }
+
+  public String getPrefix(JClassType placeType)
       throws UnableToCompleteException {
-    return getPlaceTypeForTokenizerType(getTokenizerType(prefix));
+    ensureInitialized();
+    return placeTypes.get(placeType);
   }
 
   public Set<String> getPrefixes() throws UnableToCompleteException {
-    return getTokenizerTypes().keySet();
+    ensureInitialized();
+    return tokenizers.keySet();
   }
 
   public JMethod getTokenizerGetter(String prefix)
       throws UnableToCompleteException {
-    return getTokenizerGetters().get(prefix);
+    ensureInitialized();
+    Object tokenizerGetter = tokenizers.get(prefix);
+    if (tokenizerGetter instanceof JMethod) {
+      return (JMethod) tokenizerGetter;
+    }
+    return null;
   }
 
   public JClassType getTokenizerType(String prefix)
       throws UnableToCompleteException {
-
-    JMethod getter = getTokenizerGetters().get(prefix);
-    if (getter != null) {
-      return getter.getReturnType().isClassOrInterface();
+    ensureInitialized();
+    Object tokenizerType = tokenizers.get(prefix);
+    if (tokenizerType instanceof JClassType) {
+      return (JClassType) tokenizerType;
     }
-
-    return getTokenizersWihoutGetters().get(prefix);
+    return null;
   }
 
-  public Map<String, JClassType> getTokenizerTypes()
-      throws UnableToCompleteException {
-    if (tokenizerTypes == null) {
-      tokenizerTypes = new LinkedHashMap<String, JClassType>();
-      for (Entry<String, JMethod> entry : getTokenizerGetters().entrySet()) {
-        tokenizerTypes.put(entry.getKey(),
-            entry.getValue().getReturnType().isClassOrInterface());
-      }
-      for (Entry<String, JClassType> entry : getTokenizersWihoutGetters().entrySet()) {
-        tokenizerTypes.put(entry.getKey(), entry.getValue());
-      }
+  void ensureInitialized() throws UnableToCompleteException {
+    if (tokenizers == null) {
+      assert placeTypes.isEmpty();
+      tokenizers = new HashMap<String, Object>();
+      initTokenizerGetters();
+      initTokenizersWithoutGetters();
     }
-    return tokenizerTypes;
   }
 
-  public boolean hasNonFactoryTokenizer() throws UnableToCompleteException {
-    return !getTokenizersWihoutGetters().isEmpty();
+  private void addPlaceTokenizer(Object tokenizerClassOrGetter, String prefix,
+      JClassType tokenizerType) throws UnableToCompleteException {
+    if (prefix.contains(":")) {
+      logger.log(TreeLogger.ERROR, String.format(
+          "Found place prefix \"%s\" containing separator char \":\", on %s",
+          prefix, getLogMessage(tokenizerClassOrGetter)));
+      throw new UnableToCompleteException();
+    }
+    if (tokenizers.containsKey(prefix)) {
+      logger.log(TreeLogger.ERROR, String.format(
+          "Found duplicate place prefix \"%s\" on %s, already seen on %s",
+          prefix, getLogMessage(tokenizerClassOrGetter),
+          getLogMessage(tokenizers.get(prefix))));
+      throw new UnableToCompleteException();
+    }
+    JClassType placeType = getPlaceTypeForTokenizerType(tokenizerType);
+    if (placeTypes.containsKey(placeType)) {
+      logger.log(
+          TreeLogger.ERROR,
+          String.format(
+              "Found duplicate tokenizer's place type \"%s\" on %s, already seen on %s",
+              placeType.getQualifiedSourceName(),
+              getLogMessage(tokenizerClassOrGetter),
+              getLogMessage(tokenizers.get(placeTypes.get(placeType)))));
+      throw new UnableToCompleteException();
+    }
+    tokenizers.put(prefix, tokenizerClassOrGetter);
+    placeTypes.put(placeType, prefix);
+  }
+
+  private String getLogMessage(Object methodOrClass) {
+    if (methodOrClass instanceof JMethod) {
+      JMethod method = (JMethod) methodOrClass;
+      return method.getEnclosingType().getQualifiedSourceName() + "#"
+          + method.getName() + "()";
+    }
+    JClassType classType = (JClassType) methodOrClass;
+    return classType.getQualifiedSourceName();
   }
 
   private JClassType getPlaceTypeForTokenizerType(JClassType tokenizerType)
@@ -235,29 +265,37 @@
     return getPlaceTypeForTokenizerType(returnType).getName();
   }
 
-  private Map<String, JMethod> getTokenizerGetters()
-      throws UnableToCompleteException {
-    if (factoryType == null) {
-      return Collections.emptyMap();
+  private Set<JClassType> getWithTokenizerEntries() {
+    WithTokenizers annotation = interfaceType.getAnnotation(WithTokenizers.class);
+    if (annotation == null) {
+      return Collections.emptySet();
     }
 
-    if (tokenizerGetters == null) {
-      tokenizerGetters = new LinkedHashMap<String, JMethod>();
-
-      /* Gets inherited methods, but not final ones */
-      JMethod[] overridableMethods = factoryType.getOverridableMethods();
-      /* So we pick up the finals here */
-      JMethod[] methods = factoryType.getMethods();
-
-      LinkedHashSet<JMethod> allMethods = new LinkedHashSet<JMethod>();
-      allMethods.addAll(Arrays.asList(overridableMethods));
-      for (JMethod method : methods) {
-        if (method.isPublic()) {
-          allMethods.add(method);
-        }
+    LinkedHashSet<JClassType> rtn = new LinkedHashSet<JClassType>();
+    for (Class<? extends PlaceTokenizer<?>> tokenizerClass : annotation.value()) {
+      JClassType tokenizerType = typeOracle.findType(tokenizerClass.getCanonicalName());
+      if (tokenizerType == null) {
+        logger.log(TreeLogger.ERROR, String.format(
+            "Error processing @%s, cannot find type %s",
+            WithTokenizers.class.getSimpleName(),
+            tokenizerClass.getCanonicalName()));
       }
+      rtn.add(tokenizerType);
+    }
 
-      for (JMethod method : allMethods) {
+    return rtn;
+  }
+
+  private void initTokenizerGetters() throws UnableToCompleteException {
+    if (factoryType != null) {
+
+      // TODO: include non-public methods that are nevertheless accessible
+      // to the interface (package-scoped);
+      // Add a isCallable(JClassType) method to JAbstractMethod?
+      for (JMethod method : factoryType.getInheritableMethods()) {
+        if (!method.isPublic()) {
+          continue;
+        }
         if (method.getParameters().length > 0) {
           continue;
         }
@@ -272,56 +310,17 @@
           continue;
         }
 
-        String prefix = getPrefixForTokenizerGetter(method);
-        if (tokenizerGetters.containsKey(prefix)) {
-          logger.log(TreeLogger.ERROR, String.format(
-              "Found duplicate place prefix \"%s\" in factory type %s, "
-                  + "used by both %s and %s", prefix,
-              factoryType.getQualifiedSourceName(),
-              tokenizerGetters.get(prefix).getName(), method.getName()));
-          throw new UnableToCompleteException();
-        }
-        tokenizerGetters.put(prefix, method);
+        addPlaceTokenizer(method, getPrefixForTokenizerGetter(method),
+            method.getReturnType().isClassOrInterface());
       }
     }
-
-    return tokenizerGetters;
   }
 
-  private HashMap<String, JClassType> getTokenizersWihoutGetters()
-      throws UnableToCompleteException {
-    if (tokenizersWithNoGetters == null) {
-      tokenizersWithNoGetters = new LinkedHashMap<String, JClassType>();
-
-      for (JClassType tokenizerType : getWithTokenizerEntries()) {
-        tokenizersWithNoGetters.put(getPrefixForTokenizerType(tokenizerType),
-            tokenizerType);
-      }
+  private void initTokenizersWithoutGetters() throws UnableToCompleteException {
+    for (JClassType tokenizerType : getWithTokenizerEntries()) {
+      addPlaceTokenizer(tokenizerType,
+          getPrefixForTokenizerType(tokenizerType), tokenizerType);
     }
-
-    return tokenizersWithNoGetters;
-  }
-
-  private Set<JClassType> getWithTokenizerEntries() {
-    WithTokenizers annotation = interfaceType.getAnnotation(WithTokenizers.class);
-    if (annotation == null) {
-      return Collections.emptySet();
-    }
-
-    LinkedHashSet<JClassType> rtn = new LinkedHashSet<JClassType>();
-    for (Class<? extends PlaceTokenizer<?>> tokenizerClass : annotation.value()) {
-      JClassType tokenizerType = generatorContext.getTypeOracle().findType(
-          tokenizerClass.getCanonicalName());
-      if (tokenizerType == null) {
-        logger.log(TreeLogger.ERROR, String.format(
-            "Error processing @%s, cannot find type %s",
-            WithTokenizers.class.getSimpleName(),
-            tokenizerClass.getCanonicalName()));
-      }
-      rtn.add(tokenizerType);
-    }
-
-    return rtn;
   }
 
   private JClassType placeTypeForInterfaces(Collection<JClassType> interfaces) {
diff --git a/user/src/com/google/gwt/app/rebind/PlaceHistoryHandlerGenerator.java b/user/src/com/google/gwt/app/rebind/PlaceHistoryMapperGenerator.java
similarity index 73%
rename from user/src/com/google/gwt/app/rebind/PlaceHistoryHandlerGenerator.java
rename to user/src/com/google/gwt/app/rebind/PlaceHistoryMapperGenerator.java
index 540cc3e..8c04432 100644
--- a/user/src/com/google/gwt/app/rebind/PlaceHistoryHandlerGenerator.java
+++ b/user/src/com/google/gwt/app/rebind/PlaceHistoryMapperGenerator.java
@@ -15,10 +15,10 @@
  */
 package com.google.gwt.app.rebind;
 
-import com.google.gwt.app.place.AbstractPlaceHistoryHandler;
 import com.google.gwt.app.place.Place;
 import com.google.gwt.app.place.PlaceTokenizer;
-import com.google.gwt.app.place.AbstractPlaceHistoryHandler.PrefixAndToken;
+import com.google.gwt.app.place.impl.AbstractPlaceHistoryMapper;
+import com.google.gwt.app.place.impl.AbstractPlaceHistoryMapper.PrefixAndToken;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.GeneratorContext;
@@ -30,7 +30,6 @@
 import com.google.gwt.user.rebind.SourceWriter;
 
 import java.io.PrintWriter;
-import java.util.Map.Entry;
 
 /**
  * <p>
@@ -39,29 +38,29 @@
  * </span>
  * </p>
  * Generates implementations of
- * {@link com.google.gwt.app.place.PlaceHistoryHandler PlaceHistoryHandler}.
+ * {@link com.google.gwt.app.place.PlaceHistoryMapper PlaceHistoryMapper}.
  */
-public class PlaceHistoryHandlerGenerator extends Generator {
+public class PlaceHistoryMapperGenerator extends Generator {
   private PlaceHistoryGeneratorContext context;
 
   @Override
   public String generate(TreeLogger logger, GeneratorContext generatorContext,
       String interfaceName) throws UnableToCompleteException {
 
-    context = PlaceHistoryGeneratorContext.create(logger, generatorContext,
-        interfaceName);
+    context = PlaceHistoryGeneratorContext.create(logger,
+        generatorContext.getTypeOracle(), interfaceName);
 
     PrintWriter out = generatorContext.tryCreate(logger, context.packageName,
         context.implName);
 
     if (out != null) {
-      generateOnce(context, out);
+      generateOnce(generatorContext, context, out);
     }
 
     return context.packageName + "." + context.implName;
   }
 
-  private void generateOnce(PlaceHistoryGeneratorContext context,
+  private void generateOnce(GeneratorContext generatorContext, PlaceHistoryGeneratorContext context,
       PrintWriter out) throws UnableToCompleteException {
 
     TreeLogger logger = context.logger.branch(TreeLogger.DEBUG, String.format(
@@ -70,15 +69,15 @@
         context.packageName, context.implName);
 
     String superClassName = String.format("%s<%s>",
-        AbstractPlaceHistoryHandler.class.getSimpleName(),
+        AbstractPlaceHistoryMapper.class.getSimpleName(),
         context.factoryType == null ? "Void" : context.factoryType.getName());
     f.setSuperclass(superClassName);
     f.addImplementedInterface(context.interfaceType.getName());
-    
-    f.addImport(AbstractPlaceHistoryHandler.class.getName());
+
+    f.addImport(AbstractPlaceHistoryMapper.class.getName());
     f.addImport(context.interfaceType.getQualifiedSourceName());
 
-    f.addImport(AbstractPlaceHistoryHandler.class.getCanonicalName());
+    f.addImport(AbstractPlaceHistoryMapper.class.getCanonicalName());
     if (context.factoryType != null) {
       f.addImport(context.factoryType.getQualifiedSourceName());
     }
@@ -87,16 +86,9 @@
     f.addImport(PlaceTokenizer.class.getCanonicalName());
     f.addImport(PrefixAndToken.class.getCanonicalName());
 
-    for (Entry<String, JClassType> entry : context.getTokenizerTypes().entrySet()) {
-      f.addImport(entry.getValue().getQualifiedSourceName());
-      f.addImport(context.getPlaceType(entry.getKey()).getQualifiedSourceName());
-    }
+    f.addImport(GWT.class.getCanonicalName());
 
-    if (context.hasNonFactoryTokenizer()) {
-      f.addImport(GWT.class.getCanonicalName());
-    }
-
-    SourceWriter sw = f.createSourceWriter(context.generatorContext, out);
+    SourceWriter sw = f.createSourceWriter(generatorContext, out);
     sw.println();
 
     writeGetPrefixAndToken(context, sw);
@@ -107,15 +99,16 @@
 
     sw.outdent();
     sw.println("}");
-    context.generatorContext.commit(logger, out);
+    generatorContext.commit(logger, out);
   }
 
   private void writeGetPrefixAndToken(PlaceHistoryGeneratorContext context,
       SourceWriter sw) throws UnableToCompleteException {
     sw.println("protected PrefixAndToken getPrefixAndToken(Place newPlace) {");
     sw.indent();
-    for (String prefix : context.getPrefixes()) {
-      String placeTypeName = context.getPlaceType(prefix).getName();
+    for (JClassType placeType : context.getPlaceTypes()) {
+      String placeTypeName = placeType.getQualifiedSourceName();
+      String prefix = context.getPrefix(placeType);
 
       sw.println("if (newPlace instanceof " + placeTypeName + ") {");
       sw.indent();
@@ -124,13 +117,14 @@
       JMethod getter = context.getTokenizerGetter(prefix);
       if (getter != null) {
         sw.println(String.format("return new PrefixAndToken(\"%s\", "
-            + "factory.%s().getToken(place));", prefix, getter.getName()));
+            + "factory.%s().getToken(place));", escape(prefix),
+            getter.getName()));
       } else {
         sw.println(String.format(
             "PlaceTokenizer<%s> t = GWT.create(%s.class);", placeTypeName,
-            context.getTokenizerType(prefix).getName()));
+            context.getTokenizerType(prefix).getQualifiedSourceName()));
         sw.println(String.format("return new PrefixAndToken(\"%s\", "
-            + "t.getToken((%s) place));", prefix, placeTypeName));
+            + "t.getToken((%s) place));", escape(prefix), placeTypeName));
       }
 
       sw.outdent();
@@ -150,14 +144,14 @@
     for (String prefix : context.getPrefixes()) {
       JMethod getter = context.getTokenizerGetter(prefix);
 
-      sw.println("if (\"" + prefix + "\".equals(prefix)) {");
+      sw.println("if (\"" + escape(prefix) + "\".equals(prefix)) {");
       sw.indent();
 
       if (getter != null) {
         sw.println("return factory." + getter.getName() + "();");
       } else {
         sw.println(String.format("return GWT.create(%s.class);",
-            context.getTokenizerType(prefix).getName()));
+            context.getTokenizerType(prefix).getQualifiedSourceName()));
       }
 
       sw.outdent();
diff --git a/user/test/com/google/gwt/app/AppJreSuite.java b/user/test/com/google/gwt/app/AppJreSuite.java
index 4c824d6..5708bf5 100644
--- a/user/test/com/google/gwt/app/AppJreSuite.java
+++ b/user/test/com/google/gwt/app/AppJreSuite.java
@@ -16,9 +16,10 @@
 package com.google.gwt.app;
 
 import com.google.gwt.app.place.ActivityManagerTest;
-import com.google.gwt.app.place.AbstractPlaceHistoryHandlerTest;
 import com.google.gwt.app.place.PlaceChangeRequestEventTest;
 import com.google.gwt.app.place.PlaceControllerTest;
+import com.google.gwt.app.place.PlaceHistoryHandlerTest;
+import com.google.gwt.app.rebind.PlaceHistoryGeneratorContextTest;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
@@ -30,9 +31,10 @@
   public static Test suite() {
     TestSuite suite = new TestSuite("app package tests that require the JRE");
     suite.addTestSuite(ActivityManagerTest.class);
-    suite.addTestSuite(AbstractPlaceHistoryHandlerTest.class);
     suite.addTestSuite(PlaceControllerTest.class);
     suite.addTestSuite(PlaceChangeRequestEventTest.class);
+    suite.addTestSuite(PlaceHistoryGeneratorContextTest.class);
+    suite.addTestSuite(PlaceHistoryHandlerTest.class);
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/app/AppSuite.gwt.xml b/user/test/com/google/gwt/app/AppSuite.gwt.xml
deleted file mode 100644
index 0a3af91..0000000
--- a/user/test/com/google/gwt/app/AppSuite.gwt.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.0.1//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.0.1/distro-source/core/src/gwt-module.dtd">
-<!--
-  Copyright 2010 Google Inc.
-
-  Licensed under the Apache License, Version 2.0 (the "License"); you may not
-  use this file except in compliance with the License. You may obtain a copy of
-  the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-  License for the specific language governing permissions and limitations under
-  the License.
--->
-<module>
-  <inherits name='com.google.gwt.app.App'/>
-  <inherits name='com.google.gwt.junit.JUnit'/>
-  <source path='place'/>
-</module>
diff --git a/user/test/com/google/gwt/app/AppSuite.java b/user/test/com/google/gwt/app/AppSuite.java
index 06f1997..af744ed 100644
--- a/user/test/com/google/gwt/app/AppSuite.java
+++ b/user/test/com/google/gwt/app/AppSuite.java
@@ -15,7 +15,8 @@
  */
 package com.google.gwt.app;
 
-import com.google.gwt.app.place.PlaceHistoryHandlerGeneratorTest;
+import com.google.gwt.app.place.impl.PlaceHistoryMapperGeneratorTest;
+import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
@@ -25,8 +26,8 @@
  */
 public class AppSuite {
   public static Test suite() {
-    TestSuite suite = new TestSuite("app package tests that require GWT");
-    suite.addTestSuite(PlaceHistoryHandlerGeneratorTest.class);
+    TestSuite suite = new GWTTestSuite("app package tests that require GWT");
+    suite.addTestSuite(PlaceHistoryMapperGeneratorTest.class);
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/app/place/AbstractPlaceHistoryHandlerTest.java b/user/test/com/google/gwt/app/place/AbstractPlaceHistoryHandlerTest.java
deleted file mode 100644
index 93c6ef4..0000000
--- a/user/test/com/google/gwt/app/place/AbstractPlaceHistoryHandlerTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright 2010 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.app.place;
-
-import com.google.gwt.app.place.testplaces.Place1;
-import com.google.gwt.app.place.testplaces.Place2;
-import com.google.gwt.app.place.testplaces.Tokenizer2;
-import com.google.gwt.app.place.testplaces.TokenizerFactory;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.event.shared.SimpleEventBus;
-
-import junit.framework.TestCase;
-
-import java.util.logging.Logger;
-
-/**
- * Eponymous unit test.
- */
-public class AbstractPlaceHistoryHandlerTest extends TestCase {
-  private static class MockHistorian implements
-      AbstractPlaceHistoryHandler.Historian {
-    final HandlerRegistration registration = new Registration();
-
-    ValueChangeHandler<String> handler;
-    String token = "";
-
-    public HandlerRegistration addValueChangeHandler(
-        ValueChangeHandler<String> valueChangeHandler) {
-      this.handler = valueChangeHandler;
-      return registration;
-    }
-
-    public String getToken() {
-      return token;
-    }
-
-    public void newItem(String token, boolean issueEvent) {
-      assertFalse(issueEvent);
-      this.token = token;
-    }
-
-    public void postToken(String string) {
-      handler.onValueChange(new ValueChangeEvent<String>(string) {
-      });
-    }
-  }
-
-  private static class Registration implements HandlerRegistration {
-    public void removeHandler() {
-      throw new UnsupportedOperationException("Auto-generated method stub");
-    }
-  }
-
-  private class Subject extends AbstractPlaceHistoryHandler<TokenizerFactory> {
-
-    Subject(Historian historian) {
-      super(historian);
-    }
-
-    @Override
-    protected PrefixAndToken getPrefixAndToken(Place newPlace) {
-      if (newPlace instanceof Place1) {
-        return new PrefixAndToken(PREFIX1, factory.getTokenizer1().getToken(
-            (Place1) newPlace));
-      }
-      if (newPlace instanceof Place2) {
-        return new PrefixAndToken(PREFIX2,
-            new Tokenizer2().getToken((Place2) newPlace));
-      }
-
-      return null;
-    }
-
-    @Override
-    protected PlaceTokenizer<?> getTokenizer(String prefix) {
-      if (PREFIX1.equals(prefix)) {
-        return factory.getTokenizer1();
-      }
-      if (PREFIX2.equals(prefix)) {
-        return new Tokenizer2();
-      }
-
-      return null;
-    }
-
-    TokenizerFactory getFactory() {
-      return factory;
-    }
-
-    @Override
-    Logger log() {
-      return deadLogger;
-    }
-  }
-
-  private static final String PREFIX1 = "t1";
-
-  private static final String PREFIX2 = "token2";
-
-  Logger deadLogger = new Logger("shut up", null) {
-  };
-
-  EventBus eventBus = new SimpleEventBus();
-
-  PlaceController placeController = new PlaceController(eventBus,
-      new MockPlaceControllerDelegate()) {
-    @Override
-    Logger log() {
-      return deadLogger;
-    }
-  };
-
-  MockHistorian historian = new MockHistorian();
-
-  Subject subject = new Subject(historian);
-  Place1 place1 = new Place1("able");
-  Place2 place2 = new Place2("baker");
-
-  final Place defaultPlace = new Place() {
-  };
-
-  public void testEmptyToken() {
-    historian.postToken("");
-    assertEquals(defaultPlace, placeController.getWhere());
-  }
-
-  public void testGoToDefaultPlace() {
-    placeController.goTo(defaultPlace);
-    assertEquals("", historian.token);
-  }
-
-  public void testPlaceChange() {
-    placeController.goTo(place1);
-    assertEquals(subject.getPrefixAndToken(place1).toString(), historian.token);
-    placeController.goTo(place2);
-    assertEquals(subject.getPrefixAndToken(place2).toString(), historian.token);
-  }
-
-  public void testProperToken() {
-    historian.postToken(subject.getPrefixAndToken(place1).toString());
-    assertEquals(place1.content, ((Place1) placeController.getWhere()).content);
-
-    historian.postToken(subject.getPrefixAndToken(place2).toString());
-    assertEquals(place2.content, ((Place2) placeController.getWhere()).content);
-  }
-
-  public void testTheTestSubjectAndPrefixAndTokenToString() {
-    String history1 = subject.getPrefixAndToken(place1).toString();
-    assertEquals(PREFIX1 + ":" + place1.content, history1);
-
-    String history2 = subject.getPrefixAndToken(place2).toString();
-    assertEquals(PREFIX2 + ":" + place2.content, history2);
-
-    assertEquals(subject.getFactory().tokenizer, subject.getTokenizer(PREFIX1));
-    assertTrue(subject.getTokenizer(PREFIX2) instanceof Tokenizer2);
-
-    Place place = new Place() {
-    };
-    assertNull(subject.getPrefixAndToken(place));
-    assertNull(subject.getTokenizer("snot"));
-  }
-
-  public void testUnknownToken() {
-    historian.postToken("abcdefghijklmnop");
-    assertEquals(defaultPlace, placeController.getWhere());
-  }
-
-  @Override
-  protected void setUp() {
-    subject.setFactory(new TokenizerFactory());
-    subject.register(placeController, eventBus, defaultPlace);
-  };
-}
diff --git a/user/test/com/google/gwt/app/place/PlaceHistoryHandlerTest.java b/user/test/com/google/gwt/app/place/PlaceHistoryHandlerTest.java
new file mode 100644
index 0000000..5306086
--- /dev/null
+++ b/user/test/com/google/gwt/app/place/PlaceHistoryHandlerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.place;
+
+import com.google.gwt.app.place.testplaces.Place1;
+import com.google.gwt.app.place.testplaces.Place2;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.SimpleEventBus;
+
+import junit.framework.TestCase;
+
+import java.util.logging.Logger;
+
+/**
+ * Eponymous unit test.
+ */
+public class PlaceHistoryHandlerTest extends TestCase {
+  private static class MockHistorian implements
+      PlaceHistoryHandler.Historian {
+    final HandlerRegistration registration = new Registration();
+
+    ValueChangeHandler<String> handler;
+    String token = "";
+
+    public HandlerRegistration addValueChangeHandler(
+        ValueChangeHandler<String> valueChangeHandler) {
+      this.handler = valueChangeHandler;
+      return registration;
+    }
+
+    public String getToken() {
+      return token;
+    }
+
+    public void newItem(String token, boolean issueEvent) {
+      assertFalse(issueEvent);
+      this.token = token;
+    }
+
+    public void postToken(String string) {
+      handler.onValueChange(new ValueChangeEvent<String>(string) {
+      });
+    }
+  }
+  
+  private static class MockPlaceHistoryMapper implements PlaceHistoryMapper {
+
+    public Place getPlace(String token) {
+      if (TOKEN1.equals(token)) {
+        return PLACE1;
+      }
+      if (TOKEN2.equals(token)) {
+        return PLACE2;
+      }
+
+      return null;
+    }
+
+    public String getToken(Place place) {
+      if (place == PLACE1) {
+        return TOKEN1;
+      }
+      if (place == PLACE2) {
+        return TOKEN2;
+      }
+
+      return null;
+    }
+  }
+
+  private static class Registration implements HandlerRegistration {
+    public void removeHandler() {
+      throw new UnsupportedOperationException("Auto-generated method stub");
+    }
+  }
+
+  private class Subject extends PlaceHistoryHandler {
+
+    Subject(PlaceHistoryMapper mapper, Historian historian) {
+      super(mapper, historian);
+    }
+
+    @Override
+    Logger log() {
+      return deadLogger;
+    }
+  }
+
+  private static final String TOKEN1 = "t1";
+
+  private static final String TOKEN2 = "token2";
+
+  private static final Place1 PLACE1 = new Place1("able");
+
+  private static final Place2 PLACE2 = new Place2("baker");
+
+  Logger deadLogger = new Logger("shut up", null) {
+  };
+
+  PlaceController placeController;
+
+  MockHistorian historian;
+
+  Subject subject;
+
+  final Place defaultPlace = new Place() {
+  };
+
+  public void testEmptyToken() {
+    historian.postToken("");
+    assertEquals(defaultPlace, placeController.getWhere());
+  }
+
+  public void testGoToDefaultPlace() {
+    placeController.goTo(defaultPlace);
+    assertEquals("", historian.token);
+  }
+
+  public void testPlaceChange() {
+    placeController.goTo(PLACE1);
+    assertEquals(TOKEN1, historian.token);
+    placeController.goTo(PLACE2);
+    assertEquals(TOKEN2, historian.token);
+  }
+
+  public void testProperToken() {
+    historian.postToken(TOKEN1);
+    assertEquals(PLACE1, placeController.getWhere());
+
+    historian.postToken(TOKEN2);
+    assertEquals(PLACE2, placeController.getWhere());
+  }
+
+  public void testUnknownToken() {
+    historian.postToken("abcdefghijklmnop");
+    assertEquals(defaultPlace, placeController.getWhere());
+  }
+
+  @Override
+  protected void setUp() {
+    EventBus eventBus = new SimpleEventBus();
+    historian = new MockHistorian();
+    placeController = new PlaceController(eventBus,
+        new MockPlaceControllerDelegate()) {
+      @Override
+      Logger log() {
+        return deadLogger;
+      }
+    };
+    subject = new Subject(new MockPlaceHistoryMapper(), historian);
+    subject.register(placeController, eventBus, defaultPlace);
+  };
+}
diff --git a/user/test/com/google/gwt/app/place/PlaceHistoryHandlerGeneratorTest.java b/user/test/com/google/gwt/app/place/impl/PlaceHistoryMapperGeneratorTest.java
similarity index 72%
rename from user/test/com/google/gwt/app/place/PlaceHistoryHandlerGeneratorTest.java
rename to user/test/com/google/gwt/app/place/impl/PlaceHistoryMapperGeneratorTest.java
index 16bd4b9..51ba94e 100644
--- a/user/test/com/google/gwt/app/place/PlaceHistoryHandlerGeneratorTest.java
+++ b/user/test/com/google/gwt/app/place/impl/PlaceHistoryMapperGeneratorTest.java
@@ -13,14 +13,19 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.app.place;
+package com.google.gwt.app.place.impl;
 
-import com.google.gwt.app.place.testplacehandler.NoFactory;
-import com.google.gwt.app.place.testplacehandler.WithFactory;
+import com.google.gwt.app.place.Place;
+import com.google.gwt.app.place.PlaceHistoryMapper;
+import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;
+import com.google.gwt.app.place.WithTokenizers;
+import com.google.gwt.app.place.testplacemappers.NoFactory;
+import com.google.gwt.app.place.testplacemappers.WithFactory;
 import com.google.gwt.app.place.testplaces.Place1;
 import com.google.gwt.app.place.testplaces.Place2;
 import com.google.gwt.app.place.testplaces.Place3;
 import com.google.gwt.app.place.testplaces.Place4;
+import com.google.gwt.app.place.testplaces.Place5;
 import com.google.gwt.app.place.testplaces.Tokenizer2;
 import com.google.gwt.app.place.testplaces.Tokenizer3;
 import com.google.gwt.app.place.testplaces.Tokenizer4;
@@ -29,38 +34,39 @@
 import com.google.gwt.junit.client.GWTTestCase;
 
 /**
- * Functional test of PlaceHistoryHandlerGenerator.
+ * Functional test of PlaceHistoryMapperGenerator.
  */
-public class PlaceHistoryHandlerGeneratorTest extends GWTTestCase {
+public class PlaceHistoryMapperGeneratorTest extends GWTTestCase {
   @WithTokenizers( {
       Place1.Tokenizer.class, Tokenizer2.class, Tokenizer3.class,
       Tokenizer4.class})
-  interface LocalNoFactory extends PlaceHistoryHandler {
+  interface LocalNoFactory extends PlaceHistoryMapper {
   };
 
   @WithTokenizers(Tokenizer4.class)
   interface LocalWithFactory extends
-      PlaceHistoryHandlerWithFactory<TokenizerFactory> {
+      PlaceHistoryMapperWithFactory<TokenizerFactory> {
   };
 
   @Override
   public String getModuleName() {
-    return "com.google.gwt.app.AppSuite";
+    return "com.google.gwt.app.App";
   }
 
   Place1 place1 = new Place1("able");
   Place2 place2 = new Place2("baker");
   Place3 place3 = new Place3("charlie");
   Place4 place4 = new Place4("delta");
+  Place5 place5 = new Place5("echo");
 
   public void testTopLevelWithoutFactory() {
-    AbstractPlaceHistoryHandler<?> subject = GWT.create(NoFactory.class);
+    AbstractPlaceHistoryMapper<?> subject = GWT.create(NoFactory.class);
 
     doTest(subject, null);
   }
 
   public void testTopLevelWithFactory() {
-    AbstractPlaceHistoryHandler<TokenizerFactory> subject = GWT.create(WithFactory.class);
+    AbstractPlaceHistoryMapper<TokenizerFactory> subject = GWT.create(WithFactory.class);
     TokenizerFactory factory = new TokenizerFactory();
     subject.setFactory(factory);
 
@@ -68,21 +74,20 @@
   }
 
   public void testNestedWithoutFactory() {
-    AbstractPlaceHistoryHandler<?> subject = GWT.create(LocalNoFactory.class);
+    AbstractPlaceHistoryMapper<?> subject = GWT.create(LocalNoFactory.class);
 
     doTest(subject, null);
   }
 
   public void testNestedWithFactory() {
-    AbstractPlaceHistoryHandler<TokenizerFactory> subject = GWT.create(LocalWithFactory.class);
+    AbstractPlaceHistoryMapper<TokenizerFactory> subject = GWT.create(LocalWithFactory.class);
     TokenizerFactory factory = new TokenizerFactory();
     subject.setFactory(factory);
 
     doTest(subject, factory);
   }
 
-  // CHECKSTYLE_OFF
-  private void doTest(AbstractPlaceHistoryHandler<?> subject,
+  private void doTest(AbstractPlaceHistoryMapper<?> subject,
       TokenizerFactory factory) {
     String history1 = subject.getPrefixAndToken(place1).toString();
     assertEquals(Place1.Tokenizer.PREFIX + ":" + place1.content, history1);
@@ -92,20 +97,18 @@
       assertEquals(TokenizerFactory.PLACE2_PREFIX + ":" + place2.content,
           history2);
     } else {
-      // CHECKSTYLE_OFF
       assertEquals("Place2:" + place2.content, history2);
-      // CHECKSTYLE_ON
     }
 
     String history3 = subject.getPrefixAndToken(place3).toString();
-    // CHECKSTYLE_OFF
     assertEquals("Place3:" + place3.content, history3);
-    // CHECKSTYLE_ON
 
     String history4 = subject.getPrefixAndToken(place4).toString();
-    // CHECKSTYLE_OFF
     assertEquals("Place4:" + place4.content, history4);
-    // CHECKSTYLE_ON
+
+    // Place 5 extends Place3 and does not have its own PlaceTokenizer
+    String history5 = subject.getPrefixAndToken(place5).toString();
+    assertEquals("Place3:" + place5.content, history5);
 
     if (factory != null) {
       assertEquals(factory.tokenizer,
diff --git a/user/test/com/google/gwt/app/place/testplacehandler/NoFactory.java b/user/test/com/google/gwt/app/place/testplacemappers/NoFactory.java
similarity index 84%
rename from user/test/com/google/gwt/app/place/testplacehandler/NoFactory.java
rename to user/test/com/google/gwt/app/place/testplacemappers/NoFactory.java
index a3bdac4..9a7f803 100644
--- a/user/test/com/google/gwt/app/place/testplacehandler/NoFactory.java
+++ b/user/test/com/google/gwt/app/place/testplacemappers/NoFactory.java
@@ -13,9 +13,9 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.app.place.testplacehandler;
+package com.google.gwt.app.place.testplacemappers;
 
-import com.google.gwt.app.place.PlaceHistoryHandler;
+import com.google.gwt.app.place.PlaceHistoryMapper;
 import com.google.gwt.app.place.WithTokenizers;
 import com.google.gwt.app.place.testplaces.Place1;
 import com.google.gwt.app.place.testplaces.Tokenizer2;
@@ -23,10 +23,10 @@
 import com.google.gwt.app.place.testplaces.Tokenizer4;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 @WithTokenizers( {
   Place1.Tokenizer.class, Tokenizer2.class, Tokenizer3.class,
   Tokenizer4.class})
-public interface NoFactory extends PlaceHistoryHandler {
+public interface NoFactory extends PlaceHistoryMapper {
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/app/place/testplacehandler/WithFactory.java b/user/test/com/google/gwt/app/place/testplacemappers/WithFactory.java
similarity index 82%
rename from user/test/com/google/gwt/app/place/testplacehandler/WithFactory.java
rename to user/test/com/google/gwt/app/place/testplacemappers/WithFactory.java
index 644fe04..bdcb6f1 100644
--- a/user/test/com/google/gwt/app/place/testplacehandler/WithFactory.java
+++ b/user/test/com/google/gwt/app/place/testplacemappers/WithFactory.java
@@ -13,17 +13,17 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.app.place.testplacehandler;
+package com.google.gwt.app.place.testplacemappers;
 
-import com.google.gwt.app.place.PlaceHistoryHandlerWithFactory;
+import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;
 import com.google.gwt.app.place.WithTokenizers;
 import com.google.gwt.app.place.testplaces.Tokenizer4;
 import com.google.gwt.app.place.testplaces.TokenizerFactory;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 @WithTokenizers(Tokenizer4.class)
 public interface WithFactory extends
-  PlaceHistoryHandlerWithFactory<TokenizerFactory> {
+  PlaceHistoryMapperWithFactory<TokenizerFactory> {
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/app/place/testplaces/Place1.java b/user/test/com/google/gwt/app/place/testplaces/Place1.java
index 52ecc3b..0e4dd55 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Place1.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Place1.java
@@ -20,7 +20,7 @@
 import com.google.gwt.app.place.Prefix;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class Place1 extends Place {
   public final String content;
diff --git a/user/test/com/google/gwt/app/place/testplaces/Place2.java b/user/test/com/google/gwt/app/place/testplaces/Place2.java
index 04dac76..425a5bb 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Place2.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Place2.java
@@ -18,7 +18,7 @@
 import com.google.gwt.app.place.Place;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class Place2 extends Place {
   public final String content;
diff --git a/user/test/com/google/gwt/app/place/testplaces/Place3.java b/user/test/com/google/gwt/app/place/testplaces/Place3.java
index ebb6faa..2ec0c64 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Place3.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Place3.java
@@ -15,15 +15,12 @@
  */
 package com.google.gwt.app.place.testplaces;
 
-import com.google.gwt.app.place.Place;
-
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
-public class Place3 extends Place {
-  public final String content;
+public class Place3 extends Place1 {
 
   public Place3(String token) {
-    this.content = token;
+    super(token);
   }
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/app/place/testplaces/Place4.java b/user/test/com/google/gwt/app/place/testplaces/Place4.java
index 2aaa999..3c98fee 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Place4.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Place4.java
@@ -15,15 +15,12 @@
  */
 package com.google.gwt.app.place.testplaces;
 
-import com.google.gwt.app.place.Place;
-
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
-public class Place4 extends Place {
-  public final String content;
+public class Place4 extends Place1 {
 
   public Place4(String token) {
-    this.content = token;
+    super(token);
   }
 }
diff --git a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java b/user/test/com/google/gwt/app/place/testplaces/Place5.java
similarity index 68%
copy from user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
copy to user/test/com/google/gwt/app/place/testplaces/Place5.java
index cdc38ab..47bfb1a 100644
--- a/user/src/com/google/gwt/app/place/PlaceHistoryHandlerWithFactory.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Place5.java
@@ -13,15 +13,14 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.app.place;
+package com.google.gwt.app.place.testplaces;
 
 /**
- * A PlaceHistoryHandler that can get its {@link PlaceTokenizer} instances from
- * a factory.
- * 
- * @param <F> factory type
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
-public interface PlaceHistoryHandlerWithFactory<F> extends PlaceHistoryHandler {
+public class Place5 extends Place3 {
 
-  void setFactory(F factory);
+  public Place5(String token) {
+    super(token);
+  }
 }
diff --git a/user/test/com/google/gwt/app/place/testplaces/Tokenizer2.java b/user/test/com/google/gwt/app/place/testplaces/Tokenizer2.java
index 87c76f3..dc3d772 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Tokenizer2.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Tokenizer2.java
@@ -18,7 +18,7 @@
 import com.google.gwt.app.place.PlaceTokenizer;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class Tokenizer2 implements PlaceTokenizer<Place2> {
   public Place2 getPlace(String token) {
diff --git a/user/test/com/google/gwt/app/place/testplaces/Tokenizer3.java b/user/test/com/google/gwt/app/place/testplaces/Tokenizer3.java
index 664f17e..f8a129a 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Tokenizer3.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Tokenizer3.java
@@ -18,7 +18,7 @@
 import com.google.gwt.app.place.PlaceTokenizer;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class Tokenizer3 implements PlaceTokenizer<Place3> {
 
diff --git a/user/test/com/google/gwt/app/place/testplaces/Tokenizer4.java b/user/test/com/google/gwt/app/place/testplaces/Tokenizer4.java
index 141b278..089ca8c 100644
--- a/user/test/com/google/gwt/app/place/testplaces/Tokenizer4.java
+++ b/user/test/com/google/gwt/app/place/testplaces/Tokenizer4.java
@@ -18,7 +18,7 @@
 import com.google.gwt.app.place.PlaceTokenizer;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class Tokenizer4 implements PlaceTokenizer<Place4> {
   public Place4 getPlace(String token) {
diff --git a/user/test/com/google/gwt/app/place/testplaces/TokenizerFactory.java b/user/test/com/google/gwt/app/place/testplaces/TokenizerFactory.java
index d6a291c..60046b4 100644
--- a/user/test/com/google/gwt/app/place/testplaces/TokenizerFactory.java
+++ b/user/test/com/google/gwt/app/place/testplaces/TokenizerFactory.java
@@ -19,7 +19,7 @@
 import com.google.gwt.app.place.Prefix;
 
 /**
- * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryHandlerGenerator}.
+ * Used by tests of {@link com.google.gwt.app.rebind.PlaceHistoryMapperGenerator}.
  */
 public class TokenizerFactory {
   public static final String PLACE2_PREFIX = "p2";
diff --git a/user/test/com/google/gwt/app/rebind/EmptyMockJavaResource.java b/user/test/com/google/gwt/app/rebind/EmptyMockJavaResource.java
new file mode 100644
index 0000000..eb3c5d3
--- /dev/null
+++ b/user/test/com/google/gwt/app/rebind/EmptyMockJavaResource.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.rebind;
+
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+
+/**
+ * Constructs an empty interface representation of a type.
+ * <p>
+ * Copied from {@link com.google.gwt.editor.rebind.model.EditorModelTest}
+ * pending a public API.
+ */
+class EmptyMockJavaResource extends MockJavaResource {
+  private final StringBuilder code = new StringBuilder();
+
+  public EmptyMockJavaResource(Class<?> clazz) {
+    super(clazz.getName());
+
+    code.append("package ").append(clazz.getPackage().getName()).append(";\n");
+    code.append("public interface ").append(clazz.getSimpleName());
+
+    int numParams = clazz.getTypeParameters().length;
+    if (numParams != 0) {
+      code.append("<");
+      for (int i = 0; i < numParams; i++) {
+        if (i != 0) {
+          code.append(",");
+        }
+        code.append("T").append(i);
+      }
+      code.append(">");
+    }
+
+    code.append("{}\n");
+  }
+
+  @Override
+  protected CharSequence getContent() {
+    return code;
+  }
+}
diff --git a/user/test/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparatorTest.java b/user/test/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparatorTest.java
new file mode 100644
index 0000000..8f5dcbe
--- /dev/null
+++ b/user/test/com/google/gwt/app/rebind/MostToLeastDerivedPlaceTypeComparatorTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.rebind;
+
+import com.google.gwt.app.place.Place;
+import com.google.gwt.app.place.PlaceTokenizer;
+import com.google.gwt.app.place.Prefix;
+import com.google.gwt.app.place.testplaces.Place1;
+import com.google.gwt.app.place.testplaces.Place2;
+import com.google.gwt.app.place.testplaces.Place3;
+import com.google.gwt.app.place.testplaces.Place4;
+import com.google.gwt.app.place.testplaces.Place5;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.javac.CompilationState;
+import com.google.gwt.dev.javac.CompilationStateBuilder;
+import com.google.gwt.dev.javac.impl.JavaResourceBase;
+import com.google.gwt.dev.resource.Resource;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Test case for {@link MostToLeastDerivedPlaceTypeComparator} that uses mock
+ * CompilationStates.
+ */
+public class MostToLeastDerivedPlaceTypeComparatorTest extends TestCase {
+
+  private static TreeLogger createCompileLogger() {
+    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
+        System.err, true));
+    logger.setMaxDetail(TreeLogger.ERROR);
+    return logger;
+  }
+
+  private TypeOracle typeOracle;
+
+  private Comparator<JClassType> comparator;
+
+  private JClassType place;
+  private JClassType place1;
+  private JClassType place2;
+  private JClassType place3;
+  private JClassType place4;
+  private JClassType place5;
+
+  @Override
+  protected void setUp() throws Exception {
+    comparator = new MostToLeastDerivedPlaceTypeComparator();
+
+    TreeLogger logger = createCompileLogger();
+    CompilationState state = CompilationStateBuilder.buildFrom(logger,
+        getJavaResources());
+    typeOracle = state.getTypeOracle();
+
+    place = typeOracle.getType("com.google.gwt.app.place.Place");
+    assertNotNull(place);
+    place1 = typeOracle.getType("com.google.gwt.app.place.testplaces.Place1");
+    assertNotNull(place1);
+    place2 = typeOracle.getType("com.google.gwt.app.place.testplaces.Place2");
+    assertNotNull(place2);
+    place3 = typeOracle.getType("com.google.gwt.app.place.testplaces.Place3");
+    assertNotNull(place3);
+    place4 = typeOracle.getType("com.google.gwt.app.place.testplaces.Place4");
+    assertNotNull(place4);
+    place5 = typeOracle.getType("com.google.gwt.app.place.testplaces.Place5");
+    assertNotNull(place5);
+  }
+
+  private Set<Resource> getJavaResources() {
+    Set<Resource> rtn = new HashSet<Resource>(
+        Arrays.asList(JavaResourceBase.getStandardResources()));
+    rtn.add(new RealJavaResource(Place.class));
+    // referenced by Place1
+    rtn.add(new RealJavaResource(PlaceTokenizer.class));
+    // referenced by Place1.Tokenizer
+    rtn.add(new RealJavaResource(Prefix.class));
+    rtn.add(new RealJavaResource(Place1.class));
+    rtn.add(new RealJavaResource(Place2.class));
+    rtn.add(new RealJavaResource(Place3.class));
+    rtn.add(new RealJavaResource(Place4.class));
+    rtn.add(new RealJavaResource(Place5.class));
+    return rtn;
+  }
+
+  public void testEquality() {
+    for (JClassType p : new JClassType[] {
+        place, place1, place2, place3, place4, place5}) {
+      assertEquals(0, comparator.compare(p, p));
+    }
+  }
+
+  public void testPlaceComparesGreaterThanAnyDerivedClass() {
+    for (JClassType p : new JClassType[] {
+        place1, place2, place3, place4, place5}) {
+      assertEquals(1, (int) Math.signum(comparator.compare(place, p)));
+      assertEquals(-1, (int) Math.signum(comparator.compare(p, place)));
+    }
+  }
+
+  public void testPlaceInheritanceOrder() {
+    // Place3 extends Place1
+    assertEquals(1, (int) Math.signum(comparator.compare(place1, place3)));
+    assertEquals(-1, (int) Math.signum(comparator.compare(place3, place1)));
+
+    // Place5 extends Place3 extends Place1
+    assertEquals(1, (int) Math.signum(comparator.compare(place1, place5)));
+    assertEquals(-1, (int) Math.signum(comparator.compare(place5, place1)));
+
+    // Place4 extends Place1
+    assertEquals(1, (int) Math.signum(comparator.compare(place1, place4)));
+    assertEquals(-1, (int) Math.signum(comparator.compare(place4, place1)));
+
+    // Place5 extends Place3
+    assertEquals(1, (int) Math.signum(comparator.compare(place3, place5)));
+    assertEquals(-1, (int) Math.signum(comparator.compare(place5, place3)));
+  }
+
+  public void testFallbackToClassName() {
+    // Array sorted from least derived to most derived. In each pair of adjacent
+    // values, neither place extends the other.
+    JClassType[] places = {place1, place2, place3, place4, place5};
+    for (int i = 0; i < places.length - 1; i++) {
+      assertEquals(-1, (int) Math.signum(comparator.compare(places[i],
+          places[i + 1])));
+      assertEquals(1, (int) Math.signum(comparator.compare(places[i + 1],
+          places[i])));
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/app/rebind/PlaceHistoryGeneratorContextTest.java b/user/test/com/google/gwt/app/rebind/PlaceHistoryGeneratorContextTest.java
new file mode 100644
index 0000000..c188e13
--- /dev/null
+++ b/user/test/com/google/gwt/app/rebind/PlaceHistoryGeneratorContextTest.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.rebind;
+
+import com.google.gwt.app.place.Place;
+import com.google.gwt.app.place.PlaceHistoryMapper;
+import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;
+import com.google.gwt.app.place.PlaceTokenizer;
+import com.google.gwt.app.place.Prefix;
+import com.google.gwt.app.place.WithTokenizers;
+import com.google.gwt.app.place.testplacemappers.NoFactory;
+import com.google.gwt.app.place.testplacemappers.WithFactory;
+import com.google.gwt.app.place.testplaces.Place1;
+import com.google.gwt.app.place.testplaces.Place2;
+import com.google.gwt.app.place.testplaces.Place3;
+import com.google.gwt.app.place.testplaces.Place4;
+import com.google.gwt.app.place.testplaces.Tokenizer2;
+import com.google.gwt.app.place.testplaces.Tokenizer3;
+import com.google.gwt.app.place.testplaces.Tokenizer4;
+import com.google.gwt.app.place.testplaces.TokenizerFactory;
+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.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.javac.CompilationStateBuilder;
+import com.google.gwt.dev.javac.impl.JavaResourceBase;
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+import com.google.gwt.dev.resource.Resource;
+import com.google.gwt.dev.util.UnitTestTreeLogger;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Test case for {@link PlaceHistoryGeneratorContext} that uses mock
+ * CompilationStates.
+ */
+public class PlaceHistoryGeneratorContextTest extends TestCase {
+
+  private static final JType[] EMPTY_JTYPE_ARRAY = new JType[0];
+
+  private static TreeLogger createCompileLogger() {
+    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
+        System.err, true));
+    logger.setMaxDetail(TreeLogger.ERROR);
+    return logger;
+  }
+
+  private static TypeOracle createTypeOracle(Resource... resources) {
+    Set<Resource> rtn = new HashSet<Resource>(
+        Arrays.asList(JavaResourceBase.getStandardResources()));
+    rtn.add(new RealJavaResource(Place.class));
+    rtn.add(new RealJavaResource(PlaceTokenizer.class));
+    rtn.add(new RealJavaResource(PlaceHistoryMapper.class));
+    rtn.add(new RealJavaResource(PlaceHistoryMapperWithFactory.class));
+    rtn.add(new RealJavaResource(WithTokenizers.class));
+    rtn.add(new RealJavaResource(Prefix.class));
+    rtn.add(new RealJavaResource(NoFactory.class));
+    rtn.add(new RealJavaResource(WithFactory.class));
+    rtn.add(new RealJavaResource(TokenizerFactory.class));
+    rtn.add(new RealJavaResource(Place1.class));
+    rtn.add(new RealJavaResource(Place2.class));
+    rtn.add(new RealJavaResource(Place3.class));
+    rtn.add(new RealJavaResource(Place4.class));
+    rtn.add(new RealJavaResource(Tokenizer2.class));
+    rtn.add(new RealJavaResource(Tokenizer3.class));
+    rtn.add(new RealJavaResource(Tokenizer4.class));
+    rtn.addAll(Arrays.asList(resources));
+    return CompilationStateBuilder.buildFrom(createCompileLogger(), rtn).getTypeOracle();
+  }
+
+  public void testCreateNoFactory() throws UnableToCompleteException,
+      NotFoundException {
+    doTestCreate(NoFactory.class, null);
+  }
+
+  public void testCreateWithFactory() throws UnableToCompleteException,
+      NotFoundException {
+    doTestCreate(WithFactory.class, TokenizerFactory.class);
+  }
+
+  public void testNoFactory() throws UnableToCompleteException,
+      NotFoundException {
+
+    TypeOracle typeOracle = createTypeOracle();
+    JClassType place1 = typeOracle.getType(Place1.class.getName());
+    JClassType place2 = typeOracle.getType(Place2.class.getName());
+    JClassType place3 = typeOracle.getType(Place3.class.getName());
+    JClassType place4 = typeOracle.getType(Place4.class.getName());
+
+    PlaceHistoryGeneratorContext context = createContext(TreeLogger.NULL,
+        typeOracle, NoFactory.class.getName(), null);
+
+    // Found all place prefixes?
+    assertEquals(new HashSet<String>(Arrays.asList(Place1.Tokenizer.PREFIX,
+        "Place2", "Place3", "Place4")), context.getPrefixes());
+
+    // Found all place types and correctly sorted them?
+    assertEquals(Arrays.asList(place3, place4, place1, place2),
+        new ArrayList<JClassType>(context.getPlaceTypes()));
+
+    // correctly maps place types to their prefixes?
+    assertEquals(Place1.Tokenizer.PREFIX, context.getPrefix(place1));
+    assertEquals("Place2", context.getPrefix(place2));
+    assertEquals("Place3", context.getPrefix(place3));
+    assertEquals("Place4", context.getPrefix(place4));
+
+    // there obviously shouldn't be factory methods
+    assertNull(context.getTokenizerGetter(Place1.Tokenizer.PREFIX));
+    assertNull(context.getTokenizerGetter("Place2"));
+    assertNull(context.getTokenizerGetter("Place3"));
+    assertNull(context.getTokenizerGetter("Place4"));
+
+    // correctly maps prefixes to their tokenizer type?
+    assertEquals(typeOracle.getType(Place1.Tokenizer.class.getCanonicalName()),
+        context.getTokenizerType(Place1.Tokenizer.PREFIX));
+    assertEquals(typeOracle.getType(Tokenizer2.class.getName()),
+        context.getTokenizerType("Place2"));
+    assertEquals(typeOracle.getType(Tokenizer3.class.getName()),
+        context.getTokenizerType("Place3"));
+    assertEquals(typeOracle.getType(Tokenizer4.class.getName()),
+        context.getTokenizerType("Place4"));
+  }
+
+  public void testWithFactory() throws UnableToCompleteException,
+      NotFoundException {
+
+    TypeOracle typeOracle = createTypeOracle();
+
+    JClassType place1 = typeOracle.getType(Place1.class.getName());
+    JClassType place2 = typeOracle.getType(Place2.class.getName());
+    JClassType place3 = typeOracle.getType(Place3.class.getName());
+    JClassType place4 = typeOracle.getType(Place4.class.getName());
+    JClassType factory = typeOracle.getType(TokenizerFactory.class.getName());
+
+    PlaceHistoryGeneratorContext context = createContext(TreeLogger.NULL,
+        typeOracle, WithFactory.class.getName(),
+        TokenizerFactory.class.getName());
+
+    // Found all place prefixes?
+    assertEquals(new HashSet<String>(Arrays.asList(Place1.Tokenizer.PREFIX,
+        TokenizerFactory.PLACE2_PREFIX, "Place3", "Place4")),
+        context.getPrefixes());
+
+    // Found all place types and correctly sorted them?
+    assertEquals(Arrays.asList(place3, place4, place1, place2),
+        new ArrayList<JClassType>(context.getPlaceTypes()));
+
+    // correctly maps place types to their prefixes?
+    assertEquals(Place1.Tokenizer.PREFIX, context.getPrefix(place1));
+    assertEquals(TokenizerFactory.PLACE2_PREFIX, context.getPrefix(place2));
+    assertEquals("Place3", context.getPrefix(place3));
+    assertEquals("Place4", context.getPrefix(place4));
+
+    // correctly map prefixes to their factory method (or null)?
+    assertEquals(factory.getMethod("getTokenizer1", EMPTY_JTYPE_ARRAY),
+        context.getTokenizerGetter(Place1.Tokenizer.PREFIX));
+    assertEquals(factory.getMethod("getTokenizer2", EMPTY_JTYPE_ARRAY),
+        context.getTokenizerGetter(TokenizerFactory.PLACE2_PREFIX));
+    assertEquals(factory.getMethod("getTokenizer3", EMPTY_JTYPE_ARRAY),
+        context.getTokenizerGetter("Place3"));
+    assertNull(context.getTokenizerGetter("Place4"));
+
+    // correctly maps prefixes to their tokenizer type (or null)?
+    assertNull(context.getTokenizerType(Place1.Tokenizer.PREFIX));
+    assertNull(context.getTokenizerType(TokenizerFactory.PLACE2_PREFIX));
+    assertNull(context.getTokenizerType("Place3"));
+    assertEquals(typeOracle.getType(Tokenizer4.class.getName()),
+        context.getTokenizerType("Place4"));
+  }
+
+  public void testDuplicatePrefix() {
+    MockJavaResource intf = new MockJavaResource("my.MyPlaceHistoryMapper") {
+
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package my;\n");
+        code.append("import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;\n");
+        code.append("import com.google.gwt.app.place.WithTokenizers;\n");
+        code.append("import com.google.gwt.app.place.Prefix;\n");
+        code.append("import com.google.gwt.app.place.testplaces.Place1;\n");
+        code.append("import com.google.gwt.app.place.testplaces.Tokenizer2;\n");
+
+        code.append("@WithTokenizers(Place1.Tokenizer.class)\n");
+        code.append("public interface MyPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<MyPlaceHistoryMapper.Factory> {\n");
+        code.append("  interface Factory {\n");
+        code.append("    @Prefix(Place1.Tokenizer.PREFIX) Tokenizer2 tokenizer2();\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    };
+
+    TypeOracle typeOracle = createTypeOracle(intf);
+
+    UnitTestTreeLogger.Builder loggerBuilder = new UnitTestTreeLogger.Builder();
+    loggerBuilder.expectError(String.format(
+        "Found duplicate place prefix \"%s\" on %s, already seen on %s",
+        Place1.Tokenizer.PREFIX, Place1.Tokenizer.class.getCanonicalName(),
+        intf.getTypeName() + ".Factory#tokenizer2()"), null);
+    UnitTestTreeLogger logger = loggerBuilder.createLogger();
+
+    PlaceHistoryGeneratorContext context = createContext(logger, typeOracle,
+        intf.getTypeName(), intf.getTypeName() + ".Factory");
+
+    try {
+      context.ensureInitialized();
+      fail();
+    } catch (UnableToCompleteException e) {
+      // expected exception
+    }
+
+    logger.assertCorrectLogEntries();
+  }
+
+  public void testDuplicatePlaceType() {
+    MockJavaResource intf = new MockJavaResource("my.MyPlaceHistoryMapper") {
+
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package my;\n");
+        code.append("import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;\n");
+        code.append("import com.google.gwt.app.place.PlaceTokenizer;\n");
+        code.append("import com.google.gwt.app.place.WithTokenizers;\n");
+        code.append("import com.google.gwt.app.place.Prefix;\n");
+        code.append("import com.google.gwt.app.place.testplaces.Place1;\n");
+
+        code.append("@WithTokenizers(Place1.Tokenizer.class)\n");
+        code.append("public interface MyPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<MyPlaceHistoryMapper.Factory> {\n");
+        code.append("  interface Factory {\n");
+        code.append("    @Prefix(\"anotherPrefix\") PlaceTokenizer<Place1> bar();\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    };
+
+    TypeOracle typeOracle = createTypeOracle(intf);
+
+    UnitTestTreeLogger.Builder loggerBuilder = new UnitTestTreeLogger.Builder();
+    loggerBuilder.expectError(
+        String.format(
+            "Found duplicate tokenizer's place type \"%s\" on %s, already seen on %s",
+            Place1.class.getName(), Place1.Tokenizer.class.getCanonicalName(),
+            intf.getTypeName() + ".Factory#bar()"), null);
+    UnitTestTreeLogger logger = loggerBuilder.createLogger();
+
+    PlaceHistoryGeneratorContext context = createContext(logger, typeOracle,
+        intf.getTypeName(), intf.getTypeName() + ".Factory");
+
+    try {
+      context.ensureInitialized();
+      fail();
+    } catch (UnableToCompleteException e) {
+      // expected exception
+    }
+
+    logger.assertCorrectLogEntries();
+  }
+
+  public void testPrefixContainingColon() {
+    MockJavaResource intf = new MockJavaResource("my.MyPlaceHistoryMapper") {
+
+      @Override
+      protected CharSequence getContent() {
+        StringBuilder code = new StringBuilder();
+        code.append("package my;\n");
+        code.append("import com.google.gwt.app.place.Place;\n");
+        code.append("import com.google.gwt.app.place.PlaceHistoryMapperWithFactory;\n");
+        code.append("import com.google.gwt.app.place.PlaceTokenizer;\n");
+        code.append("import com.google.gwt.app.place.Prefix;\n");
+
+        code.append("public interface MyPlaceHistoryMapper extends PlaceHistoryMapperWithFactory<MyPlaceHistoryMapper.Factory> {\n");
+        code.append("  interface Factory {\n");
+        code.append("    @Prefix(\"foo:bar\") PlaceTokenizer<Place> foo_bar();\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    };
+
+    TypeOracle typeOracle = createTypeOracle(intf);
+
+    UnitTestTreeLogger.Builder loggerBuilder = new UnitTestTreeLogger.Builder();
+    loggerBuilder.expectError(
+        "Found place prefix \"foo:bar\" containing separator char \":\", on "
+            + intf.getTypeName() + ".Factory#foo_bar()", null);
+    UnitTestTreeLogger logger = loggerBuilder.createLogger();
+
+    PlaceHistoryGeneratorContext context = createContext(logger, typeOracle,
+        intf.getTypeName(), intf.getTypeName() + ".Factory");
+
+    try {
+      context.ensureInitialized();
+      fail();
+    } catch (UnableToCompleteException e) {
+      // expected exception
+    }
+
+    logger.assertCorrectLogEntries();
+  }
+
+  private void doTestCreate(Class<? extends PlaceHistoryMapper> intf,
+      Class<?> factory) throws UnableToCompleteException, NotFoundException {
+    UnitTestTreeLogger logger = new UnitTestTreeLogger.Builder().createLogger();
+
+    TypeOracle typeOracle = createTypeOracle();
+
+    PlaceHistoryGeneratorContext context = PlaceHistoryGeneratorContext.create(
+        logger, typeOracle, intf.getName());
+
+    assertEquals(typeOracle.getType(String.class.getName()), context.stringType);
+    assertEquals(typeOracle.getType(PlaceTokenizer.class.getName()),
+        context.placeTokenizerType);
+    assertSame(logger, context.logger);
+    assertSame(typeOracle, context.typeOracle);
+
+    assertEquals(typeOracle.getType(intf.getName()), context.interfaceType);
+
+    if (factory == null) {
+      assertNull(context.factoryType);
+    } else {
+      assertEquals(typeOracle.getType(factory.getName()), context.factoryType);
+    }
+
+    assertEquals(intf.getSimpleName() + "Impl", context.implName);
+    assertEquals(intf.getPackage().getName(), context.packageName);
+
+    logger.assertCorrectLogEntries();
+  }
+
+  private PlaceHistoryGeneratorContext createContext(TreeLogger logger,
+      TypeOracle typeOracle, String interfaceName, String factoryName) {
+    return new PlaceHistoryGeneratorContext(logger, typeOracle,
+        typeOracle.findType(interfaceName), //
+        typeOracle.findType(factoryName), //
+        typeOracle.findType(String.class.getName()), //
+        typeOracle.findType(PlaceTokenizer.class.getName()), //
+        null, null);
+  }
+}
diff --git a/user/test/com/google/gwt/app/rebind/RealJavaResource.java b/user/test/com/google/gwt/app/rebind/RealJavaResource.java
new file mode 100644
index 0000000..a18e161
--- /dev/null
+++ b/user/test/com/google/gwt/app/rebind/RealJavaResource.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.app.rebind;
+
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+import com.google.gwt.dev.util.Util;
+
+import java.io.InputStream;
+
+/**
+ * Loads the actual source of a type. This should be used only for types
+ * directly tested by this package's tests. Note that use of this class
+ * requires your source files to be on your classpath.
+ * <p>
+ * Copied from {@link com.google.gwt.editor.rebind.model.EditorModelTest}
+ * pending a public API.
+ */
+class RealJavaResource extends MockJavaResource {
+  public RealJavaResource(Class<?> clazz) {
+    super(clazz.getName());
+  }
+
+  @Override
+  protected CharSequence getContent() {
+    String resourceName = getTypeName().replace('.', '/') + ".java";
+    InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+        resourceName);
+    return Util.readStreamAsString(stream);
+  }
+}
diff --git a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
index 1fc7f51..bdb1139 100644
--- a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
+++ b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
@@ -94,7 +94,8 @@
 
   /**
    * Loads the actual source of a type. This should be used only for types
-   * directly tested by this test.
+   * directly tested by this test. Note that use of this class requires your
+   * source files to be on your classpath.
    */
   private static class RealJavaResource extends MockJavaResource {
     public RealJavaResource(Class<?> clazz) {