diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index da64e19c..ee3f3c8b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -573,6 +573,15 @@ public class MinecraftReflection { } } + /** + * Retrieve the EntityHuman class. + * @return The entity human class. + */ + public static Class getEntityHumanClass() { + // Assume its the direct superclass + return getEntityPlayerClass().getSuperclass(); + } + /** * Retrieve the GameProfile class in 1.7.2 and later. * @return The game profile class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/GuavaWrappers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/GuavaWrappers.java new file mode 100644 index 00000000..29df7a35 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/GuavaWrappers.java @@ -0,0 +1,251 @@ +package com.comphenix.protocol.wrappers; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.comphenix.protocol.wrappers.collection.ConvertedSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +/** + * Represents wrappers for Minecraft's own version of Guava. + * @author Kristian + */ +class GuavaWrappers { + /** + * Wrap a Bukkit multimap around Minecraft's internal multimap. + * @param multimap - the multimap to wrap. + * @return The Bukkit multimap. + */ + public static Multimap getBukkitMultimap( + final net.minecraft.util.com.google.common.collect.Multimap multimap) { + return new Multimap() { + public Map> asMap() { + return multimap.asMap(); + } + + public void clear() { + multimap.clear(); + } + + public boolean containsEntry(Object arg0, Object arg1) { + return multimap.containsEntry(arg0, arg1); + } + + public boolean containsKey(Object arg0) { + return multimap.containsKey(arg0); + } + + public boolean containsValue(Object arg0) { + return multimap.containsValue(arg0); + } + + public Collection> entries() { + return multimap.entries(); + } + + public boolean equals(Object arg0) { + return multimap.equals(arg0); + } + + public Collection get(TKey arg0) { + return multimap.get(arg0); + } + + public int hashCode() { + return multimap.hashCode(); + } + + public boolean isEmpty() { + return multimap.isEmpty(); + } + + public Set keySet() { + return multimap.keySet(); + } + + public Multiset keys() { + return getBukkitMultiset(multimap.keys()); + } + + public boolean put(TKey arg0, TValue arg1) { + return multimap.put(arg0, arg1); + } + + public boolean putAll(com.google.common.collect.Multimap arg0) { + boolean result = false; + + // Add each entry + for (Entry entry : arg0.entries()) { + result |= multimap.put(entry.getKey(), entry.getValue()); + } + return result; + } + + public boolean putAll(TKey arg0, Iterable arg1) { + return multimap.putAll(arg0, arg1); + } + + public boolean remove(Object arg0, Object arg1) { + return multimap.remove(arg0, arg1); + } + + public Collection removeAll(Object arg0) { + return multimap.removeAll(arg0); + } + + public Collection replaceValues(TKey arg0, Iterable arg1) { + return multimap.replaceValues(arg0, arg1); + } + + public int size() { + return multimap.size(); + } + + public Collection values() { + return multimap.values(); + } + }; + } + + public static Multiset getBukkitMultiset(net.minecraft.util.com.google.common.collect.Multiset multiset2) { + return new Multiset() { + private net.minecraft.util.com.google.common.collect.Multiset multiset; + + public int add(TValue arg0, int arg1) { + return multiset.add(arg0, arg1); + } + + public boolean add(TValue arg0) { + return multiset.add(arg0); + } + + public boolean addAll(Collection c) { + return multiset.addAll(c); + } + + public void clear() { + multiset.clear(); + } + + public boolean contains(Object arg0) { + return multiset.contains(arg0); + } + + public boolean containsAll(Collection arg0) { + return multiset.containsAll(arg0); + } + + public int count(Object arg0) { + return multiset.count(arg0); + } + + public Set elementSet() { + return multiset.elementSet(); + } + + public Set> entrySet() { + return new ConvertedSet< + net.minecraft.util.com.google.common.collect.Multiset.Entry, + Multiset.Entry> + (multiset.entrySet()) { + + @Override + protected com.google.common.collect.Multiset.Entry toOuter( + net.minecraft.util.com.google.common.collect.Multiset.Entry inner) { + return getBukkitEntry(inner); + } + + @Override + protected net.minecraft.util.com.google.common.collect.Multiset.Entry toInner( + com.google.common.collect.Multiset.Entry outer) { + throw new UnsupportedOperationException("Cannot convert " + outer); + } + }; + } + + public boolean equals(Object arg0) { + return multiset.equals(arg0); + } + + public int hashCode() { + return multiset.hashCode(); + } + + public boolean isEmpty() { + return multiset.isEmpty(); + } + + public Iterator iterator() { + return multiset.iterator(); + } + + public int remove(Object arg0, int arg1) { + return multiset.remove(arg0, arg1); + } + + public boolean remove(Object arg0) { + return multiset.remove(arg0); + } + + public boolean removeAll(Collection arg0) { + return multiset.removeAll(arg0); + } + + public boolean retainAll(Collection arg0) { + return multiset.retainAll(arg0); + } + + public boolean setCount(TValue arg0, int arg1, int arg2) { + return multiset.setCount(arg0, arg1, arg2); + } + + public int setCount(TValue arg0, int arg1) { + return multiset.setCount(arg0, arg1); + } + + public int size() { + return multiset.size(); + } + + public Object[] toArray() { + return multiset.toArray(); + } + + public T[] toArray(T[] a) { + return multiset.toArray(a); + } + + public String toString() { + return multiset.toString(); + } + }; + } + + private static Multiset.Entry getBukkitEntry(final net.minecraft.util.com.google.common.collect.Multiset.Entry entry) { + return new Multiset.Entry() { + public boolean equals(Object arg0) { + return entry.equals(arg0); + } + + public int getCount() { + return entry.getCount(); + } + + public TValue getElement() { + return entry.getElement(); + } + + public int hashCode() { + return entry.hashCode(); + } + + public String toString() { + return entry.toString(); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java index b1add0a0..3115364a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedGameProfile.java @@ -3,13 +3,19 @@ package com.comphenix.protocol.wrappers; import java.util.UUID; import org.apache.commons.lang.StringUtils; +import org.bukkit.entity.Player; +import com.comphenix.protocol.injector.BukkitUnwrapper; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.collection.ConvertedMultimap; import com.google.common.base.Objects; +import com.google.common.collect.Multimap; import net.minecraft.util.com.mojang.authlib.GameProfile; +import net.minecraft.util.com.mojang.authlib.properties.Property; /** * Represents a wrapper for a game profile. @@ -20,12 +26,34 @@ public class WrappedGameProfile extends AbstractWrapper { private static final ConstructorAccessor CREATE_STRING_STRING = Accessors.getConstructorAccessorOrNull(GameProfile.class, String.class, String.class); private static final FieldAccessor GET_UUID_STRING = Accessors.getFieldAcccessorOrNull(GameProfile.class, "id", String.class); + // Fetching game profile + private static FieldAccessor GET_PROFILE; + + // Property map + private Multimap propertyMap; + // Profile from a handle private WrappedGameProfile(Object profile) { super(GameProfile.class); setHandle(profile); } + /** + * Retrieve the associated game profile of a player. + * @param player - the player. + * @return The game profile. + */ + public static WrappedGameProfile fromPlayer(Player player) { + FieldAccessor accessor = GET_PROFILE; + Object nmsPlayer = BukkitUnwrapper.getInstance().unwrapItem(player); + + if (accessor == null) { + accessor = Accessors.getFieldAccessor(MinecraftReflection.getEntityHumanClass(), GameProfile.class, true); + GET_PROFILE = accessor; + } + return WrappedGameProfile.fromHandle(GET_PROFILE.get(nmsPlayer)); + } + /** * Construct a new game profile with the given properties. *

@@ -110,6 +138,39 @@ public class WrappedGameProfile extends AbstractWrapper { return getProfile().getName(); } + /** + * Retrieve the property map of signed values. + * @return Property map. + */ + public Multimap getProperties() { + Multimap result = propertyMap; + + if (result == null) { + result = new ConvertedMultimap( + GuavaWrappers.getBukkitMultimap(getProfile().getProperties())) { + @Override + protected Property toInner(WrappedSignedProperty outer) { + return (Property) outer.handle; + } + + @Override + protected Object toInnerObject(Object outer) { + if (outer instanceof WrappedSignedProperty) { + return toInner((WrappedSignedProperty) outer); + } + return outer; + } + + @Override + protected WrappedSignedProperty toOuter(Property inner) { + return WrappedSignedProperty.fromHandle(inner); + } + }; + propertyMap = result; + } + return result; + } + /** * Retrieve the underlying GameProfile. * @return The GameProfile. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedSignedProperty.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedSignedProperty.java new file mode 100644 index 00000000..9c8d40f7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedSignedProperty.java @@ -0,0 +1,106 @@ +package com.comphenix.protocol.wrappers; + +import java.security.PublicKey; + +import net.minecraft.util.com.mojang.authlib.properties.Property; +import com.google.common.base.Objects; + +/** + * Represents a wrapper over a signed property. + * @author Kristian + */ +public class WrappedSignedProperty extends AbstractWrapper { + /** + * Construct a new wrapped signed property from a given handle. + * @param handle - the handle. + */ + private WrappedSignedProperty(Object handle) { + super(Property.class); + setHandle(handle); + } + + /** + * Construct a new signed property from a given NMS property. + * @param handle - the property. + * @return The wrapped signed property. + */ + public static WrappedSignedProperty fromHandle(Object handle) { + return new WrappedSignedProperty(handle); + } + + /** + * Retrieve the underlying signed property. + * @return The GameProfile. + */ + private Property getProfile() { + return (Property) handle; + } + + /** + * Retrieve the name of the underlying property, such as "textures". + * @return Name of the property. + */ + public String getName() { + return getProfile().getName(); + } + + /** + * Retrieve the signature of the property (base64) as returned by the session server's /hasJoined. + * @return The signature of the property. + */ + public String getSignature() { + return getProfile().getSignature(); + } + + /** + * Retrieve the value of the property (base64) as return by the session server's /hasJoined + * @return The value of the property. + */ + public String getValue() { + return getProfile().getValue(); + } + + /** + * Determine if this property has a signature. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean hasSignature() { + return getProfile().hasSignature(); + } + + /** + * Determine if the signature of this property is valid and signed by the corresponding private key. + * @param key - the public key. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean isSignatureValid(PublicKey key) { + return getProfile().isSignatureValid(key); + } + + @Override + public int hashCode() { + return Objects.hashCode(getName(), getValue(), getSignature()); + } + + @Override + public boolean equals(Object object){ + if (object instanceof WrappedSignedProperty) { + if (!super.equals(object)) + return false; + WrappedSignedProperty that = (WrappedSignedProperty) object; + return Objects.equal(this.getName(), that.getName()) + && Objects.equal(this.getValue(), that.getValue()) + && Objects.equal(this.getSignature(), that.getSignature()); + } + return false; + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("name", getName()) + .add("value", getValue()) + .add("signature", getSignature()) + .toString(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/AbstractConverted.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/AbstractConverted.java index 7491a5a7..6e27931a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/AbstractConverted.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/AbstractConverted.java @@ -30,6 +30,22 @@ import com.google.common.base.Function; * @param - the second type. */ public abstract class AbstractConverted { + // Inner conversion + private Function innerConverter = new Function() { + @Override + public VInner apply(@Nullable VOuter param) { + return toInner(param); + } + }; + + // Outer conversion + private Function outerConverter = new Function() { + @Override + public VOuter apply(@Nullable VInner param) { + return toOuter(param); + } + }; + /** * Convert a value from the inner map to the outer visible map. * @param inner - the inner value. @@ -44,16 +60,19 @@ public abstract class AbstractConverted { */ protected abstract VInner toInner(VOuter outer); + /** + * Retrieve a function delegate that converts outer objects to inner objects. + * @return A function delegate. + */ + protected Function getInnerConverter() { + return innerConverter; + } + /** * Retrieve a function delegate that converts inner objects to outer objects. * @return A function delegate. */ protected Function getOuterConverter() { - return new Function() { - @Override - public VOuter apply(@Nullable VInner param) { - return toOuter(param); - } - }; + return outerConverter; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/BiFunction.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/BiFunction.java new file mode 100644 index 00000000..77fd51c5 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/BiFunction.java @@ -0,0 +1,12 @@ +package com.comphenix.protocol.wrappers.collection; + +/** + * Represents a function that accepts two parameters. + * @author Kristian + * @param - type of the first parameter. + * @param - type of the second parameter. + * @param - type of the return value. + */ +public interface BiFunction { + public TResult apply(T1 arg1, T2 arg2); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMap.java index 1262c31b..53a1db8f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMap.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMap.java @@ -39,6 +39,22 @@ public abstract class ConvertedMap extends AbstractConverte // Inner map private Map inner; + // Inner conversion + private BiFunction innerConverter = new BiFunction() { + @Override + public VInner apply(Key key, VOuter outer) { + return toInner(key, outer); + } + }; + + // Outer conversion + private BiFunction outerConverter = new BiFunction() { + @Override + public VOuter apply(Key key, VInner inner) { + return toOuter(key, inner); + } + }; + public ConvertedMap(Map inner) { if (inner == null) throw new IllegalArgumentException("Inner map cannot be NULL."); @@ -63,58 +79,7 @@ public abstract class ConvertedMap extends AbstractConverte @Override public Set> entrySet() { - return new ConvertedSet, Entry>(inner.entrySet()) { - @Override - protected Entry toInner(final Entry outer) { - return new Entry() { - @Override - public Key getKey() { - return outer.getKey(); - } - - @Override - public VInner getValue() { - return ConvertedMap.this.toInner(outer.getValue()); - } - - @Override - public VInner setValue(VInner value) { - return ConvertedMap.this.toInner(outer.setValue(ConvertedMap.this.toOuter(value))); - } - - @Override - public String toString() { - return String.format("\"%s\": %s", getKey(), getValue()); - } - }; - } - - @Override - protected Entry toOuter(final Entry inner) { - return new Entry() { - @Override - public Key getKey() { - return inner.getKey(); - } - - @Override - public VOuter getValue() { - return ConvertedMap.this.toOuter(inner.getKey(), inner.getValue()); - } - - @Override - public VOuter setValue(VOuter value) { - final VInner converted = ConvertedMap.this.toInner(getKey(), value); - return ConvertedMap.this.toOuter(getKey(), inner.setValue(converted)); - } - - @Override - public String toString() { - return String.format("\"%s\": %s", getKey(), getValue()); - } - }; - } - }; + return convertedEntrySet(inner.entrySet(), innerConverter, outerConverter); } /** @@ -215,4 +180,70 @@ public abstract class ConvertedMap extends AbstractConverte sb.append(", "); } } + + /** + * Convert a collection of entries. + * @param entries - the collection of entries. + * @param innerFunction - the inner entry converter. + * @param outerFunction - the outer entry converter. + * @return The converted set of entries. + */ + static Set> convertedEntrySet( + final Collection> entries, + final BiFunction innerFunction, + final BiFunction outerFunction) { + + return new ConvertedSet, Entry>(entries) { + @Override + protected Entry toInner(final Entry outer) { + return new Entry() { + @Override + public Key getKey() { + return outer.getKey(); + } + + @Override + public VInner getValue() { + return innerFunction.apply(getKey(), outer.getValue()); + } + + @Override + public VInner setValue(VInner value) { + return innerFunction.apply(getKey(), outer.setValue(outerFunction.apply(getKey(), value))); + } + + @Override + public String toString() { + return String.format("\"%s\": %s", getKey(), getValue()); + } + }; + } + + @Override + protected Entry toOuter(final Entry inner) { + return new Entry() { + @Override + public Key getKey() { + return inner.getKey(); + } + + @Override + public VOuter getValue() { + return outerFunction.apply(getKey(), inner.getValue()); + } + + @Override + public VOuter setValue(VOuter value) { + final VInner converted = innerFunction.apply(getKey(), value); + return outerFunction.apply(getKey(), inner.setValue(converted)); + } + + @Override + public String toString() { + return String.format("\"%s\": %s", getKey(), getValue()); + } + }; + } + }; + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMultimap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMultimap.java new file mode 100644 index 00000000..8a616761 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/collection/ConvertedMultimap.java @@ -0,0 +1,246 @@ +package com.comphenix.protocol.wrappers.collection; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +/** + * Represents a multimap that wraps another multimap by transforming the entries that are going in and out. + * @author Kristian + * + * @param - the key. + * @param - the inner value type. + * @param - the outer value type. + */ +public abstract class ConvertedMultimap extends AbstractConverted implements Multimap { + // Inner multimap + private Multimap inner; + + public ConvertedMultimap(Multimap inner) { + this.inner = Preconditions.checkNotNull(inner, "inner map cannot be NULL."); + } + + /** + * Wrap a given collection. + * @param inner - the inner collection. + * @return The outer collection. + */ + protected Collection toOuterCollection(Collection inner) { + return new ConvertedCollection(inner) { + @Override + protected VInner toInner(VOuter outer) { + return ConvertedMultimap.this.toInner(outer); + } + + @Override + protected VOuter toOuter(VInner inner) { + return ConvertedMultimap.this.toOuter(inner); + } + + @Override + public String toString() { + return "[" + Joiner.on(", ").join(this) + "]"; + } + }; + } + + /** + * Wrap a given collection. + * @param outer - the outer collection. + * @return The inner collection. + */ + protected Collection toInnerCollection(Collection outer) { + return new ConvertedCollection(outer) { + @Override + protected VOuter toInner(VInner outer) { + return ConvertedMultimap.this.toOuter(outer); + } + + @Override + protected VInner toOuter(VOuter inner) { + return ConvertedMultimap.this.toInner(inner); + } + + @Override + public String toString() { + return "[" + Joiner.on(", ").join(this) + "]"; + } + }; + } + + /** + * Convert to an inner object if its of the correct type, otherwise leave it. + * @param outer - the outer object. + * @return The inner object, or the same object. + */ + @SuppressWarnings("unchecked") + protected Object toInnerObject(Object outer) { + return toInner((VOuter) outer); + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object key) { + return inner.containsKey(key); + } + + @Override + public boolean containsValue(@Nullable Object value) { + return inner.containsValue(toInnerObject(value)); + } + + @Override + public boolean containsEntry(@Nullable Object key, @Nullable Object value) { + return inner.containsEntry(key, toInnerObject(value)); + } + + @Override + public boolean put(@Nullable Key key, @Nullable VOuter value) { + return inner.put(key, toInner(value)); + } + + @Override + public boolean remove(@Nullable Object key, @Nullable Object value) { + return inner.remove(key, toInnerObject(value)); + } + + @Override + public boolean putAll(@Nullable Key key, Iterable values) { + return inner.putAll(key, Iterables.transform(values, getInnerConverter())); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public boolean putAll(Multimap multimap) { + return inner.putAll(new ConvertedMultimap((Multimap) multimap) { + @Override + protected VOuter toInner(VInner outer) { + return ConvertedMultimap.this.toOuter(outer); + } + + @Override + protected VInner toOuter(VOuter inner) { + return ConvertedMultimap.this.toInner(inner); + } + }); + } + + @Override + public Collection replaceValues(@Nullable Key key, Iterable values) { + return toOuterCollection( + inner.replaceValues(key, Iterables.transform(values, getInnerConverter())) + ); + } + + @Override + public Collection removeAll(@Nullable Object key) { + return toOuterCollection(inner.removeAll(key)); + } + + @Override + public void clear() { + inner.clear(); + } + + @Override + public Collection get(@Nullable Key key) { + return toOuterCollection(inner.get(key)); + } + + @Override + public Set keySet() { + return inner.keySet(); + } + + @Override + public Multiset keys() { + return inner.keys(); + } + + @Override + public Collection values() { + return toOuterCollection(inner.values()); + } + + @Override + public Collection> entries() { + return ConvertedMap.convertedEntrySet(inner.entries(), + new BiFunction() { + public VInner apply(Key key, VOuter outer) { + return toInner(outer); + } + }, + new BiFunction() { + public VOuter apply(Key key, VInner inner) { + return toOuter(inner); + } + } + ); + } + + @Override + public Map> asMap() { + return new ConvertedMap, Collection>(inner.asMap()) { + @Override + protected Collection toInner(Collection outer) { + return toInnerCollection(outer); + } + + @Override + protected Collection toOuter(Collection inner) { + return toOuterCollection(inner); + } + }; + } + + /** + * Returns a string representation of this map. The string representation + * consists of a list of key-value mappings in the order returned by the + * map's entrySet view's iterator, enclosed in braces + * ("{}"). Adjacent mappings are separated by the characters + * ", " (comma and space). Each key-value mapping is rendered as + * the key followed by an equals sign ("=") followed by the + * associated value. Keys and values are converted to strings as by + * {@link String#valueOf(Object)}. + * + * @return a string representation of this map + */ + public String toString() { + Iterator> i = entries().iterator(); + if (!i.hasNext()) + return "{}"; + + StringBuilder sb = new StringBuilder(); + sb.append('{'); + for (;;) { + Entry e = i.next(); + Key key = e.getKey(); + VOuter value = e.getValue(); + sb.append(key == this ? "(this Map)" : key); + sb.append('='); + sb.append(value == this ? "(this Map)" : value); + if (! i.hasNext()) + return sb.append('}').toString(); + sb.append(", "); + } + } +}