diff --git a/.gitignore b/.gitignore index d00427d..7f1eb72 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ /eclipse/classes /integration-test/__pycache__ *.pyc - +*.iml +*.ipr +*.iws diff --git a/app/controllers/Application.java b/app/controllers/Application.java index 6dbe0a7..da67ddd 100644 --- a/app/controllers/Application.java +++ b/app/controllers/Application.java @@ -1,30 +1,23 @@ package controllers; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import models.ApexList; import models.ApexType; import models.TypeFactory; - import org.codehaus.jackson.map.ObjectMapper; - import play.data.validation.Required; import play.data.validation.Validation; import play.mvc.Controller; -import play.mvc.Http; -import play.mvc.Scope; -import play.mvc.results.RenderTemplate; -import play.mvc.results.Result; import play.templates.Template; import play.templates.TemplateLoader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + public class Application extends Controller { public static void index(String json, String className) { diff --git a/app/models/ApexClass.java b/app/models/ApexClass.java index 7d4c378..6fd495b 100644 --- a/app/models/ApexClass.java +++ b/app/models/ApexClass.java @@ -1,8 +1,9 @@ package models; -import java.io.IOException; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; public class ApexClass extends ApexType { @@ -12,24 +13,24 @@ public class ApexClass extends ApexType { this.className = className; this.members = members; } - + private final String className; private final Map members; - + public Map getMembers() { return members; } - + @Override public String getParserExpr(String parserName) { return String.format("new %s(%s)", className, parserName); } - + @Override public String additionalMethods() { return ""; } - + public boolean shouldGenerateExplictParse() { for (ApexMember m : members.keySet()) { if (m.shouldGenerateExplictParse()) { @@ -38,7 +39,7 @@ public boolean shouldGenerateExplictParse() { } return false; } - + @Override public String toString() { return className; @@ -46,28 +47,77 @@ public String toString() { @Override public int hashCode() { - return Objects.hash(className, members); + return Objects.hash(className); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; - if (getClass() != obj.getClass()) return false; + if(obj.toString().equals("Object")) return true; + if (!(obj instanceof ApexClass)) return false; ApexClass other = (ApexClass) obj; - return className.equals(other.className); + return membersEqual(other.members); } - - /** @return true if this map of members equals our map of members */ - boolean membersEqual(Map other) { - return members.equals(other); + + /** @return true if every member of our type is equal to every member of the given type and if + all members in our type are present in the other type. Otherwise returns false. **/ + boolean membersEqual(Map otherMembers) { + for (Map.Entry entry : this.members.entrySet()) { + ApexMember memberName = entry.getKey(); + ApexType member = entry.getValue(); + + if (otherMembers.get(memberName) == null) { + return false; + } else { + ApexType otherMember = otherMembers.get(memberName); + if (!member.equals(otherMember)) { + return false; + } + } + } + + return true; } - - void mergeFields(ApexClass other) { + + Set mergeFields(ApexClass other) { + Set classesToRemove = new HashSet<>(); + + // If the object being merged has zero members then we should just discard it. + if (other.getMembers().size() == 0) { + classesToRemove.add(other.toString()); + } + for (ApexMember key : other.getMembers().keySet() ) { - if (members.get(key) == null) { + // If our member is an array and the other member is also an array check the item types of + // each array and if they're both ApexClass' then merge them. + if (members.get(key) instanceof ApexList && other.getMembers().get(key) instanceof ApexList) { + ApexList ourList = (ApexList) members.get(key); + ApexList otherList = (ApexList) other.getMembers().get(key); + + if (ourList.itemType instanceof ApexClass && otherList.itemType instanceof ApexClass) { + ApexClass itemType = (ApexClass) ourList.itemType; + ApexClass otherType = (ApexClass) otherList.itemType; + + classesToRemove.addAll(itemType.mergeFields(otherType)); + } + } + + // Cover case where two members of the same name exist, and both are classes, and so we want to + // recursively merge the members of those classes. + if (members.get(key) instanceof ApexClass && other.getMembers().get(key) instanceof ApexClass) { + classesToRemove.addAll(((ApexClass) members.get(key)).mergeFields((ApexClass) other.getMembers().get(key))); + } + + // Merge a member if it's null, or if the existing member is an Object, because + // that Object may have been originally just from a null value, and is not necessarily + // determinant of it's final type. + if (members.get(key) == null || members.get(key) == ApexPrimitive.OBJECT) { members.put(key, other.getMembers().get(key)); + classesToRemove.add(other.toString()); } } + + return classesToRemove; } } diff --git a/app/models/ApexList.java b/app/models/ApexList.java index 814ef69..a228e46 100644 --- a/app/models/ApexList.java +++ b/app/models/ApexList.java @@ -48,8 +48,15 @@ public int hashCode() { public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; + if (obj.toString().equals("Object")) return true; if (getClass() != obj.getClass()) return false; + ApexList other = (ApexList) obj; + + if (itemType instanceof ApexClass && other.itemType instanceof ApexClass) { + return ((ApexClass) itemType).membersEqual(((ApexClass) other.itemType).getMembers()); + } + return itemType.equals(other.itemType); } } \ No newline at end of file diff --git a/app/models/ApexPrimitive.java b/app/models/ApexPrimitive.java index 2b89650..66d4f96 100644 --- a/app/models/ApexPrimitive.java +++ b/app/models/ApexPrimitive.java @@ -28,7 +28,7 @@ public String toString() { @Override public int hashCode() { - return Objects.hash(type, parserMethod); + return Objects.hash(type); } @Override @@ -49,8 +49,10 @@ boolean canBePromotedTo(ApexPrimitive other) { public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; + if (this.type.equals("Object")) return true; if (getClass() != obj.getClass()) return false; + ApexPrimitive other = (ApexPrimitive) obj; - return type.equals(other.type); + return other.type.equals("Object") || type.equals(other.type); } } diff --git a/app/models/TypeFactory.java b/app/models/TypeFactory.java index 4062edd..0f5ec05 100644 --- a/app/models/TypeFactory.java +++ b/app/models/TypeFactory.java @@ -88,51 +88,63 @@ private ApexType typeOfObjectImpl(String propertyName, Object o) { throw new RuntimeException("Unexpected type " + o.getClass() + " in TypeFactory.typeOfObject()"); } - + /** @return a concrete type if all the list items are of the same type, Object, otherwise */ ApexType typeOfCollection(String propertyName, Collection col) { if (col == null || col.size() == 0) { return typeOfObject(propertyName, Collections.EMPTY_MAP); } + ApexType itemType = null; + for (Object o : col) { ApexType thisItemType = typeOfObject(propertyName, o); + if (itemType == null) { itemType = thisItemType; - } else if (!itemType.equals(thisItemType)) { - if (itemType instanceof ApexClass && thisItemType instanceof ApexClass) { + } else if (itemType instanceof ApexClass && thisItemType instanceof ApexClass) { ApexClass apexClass = (ApexClass)itemType; ApexClass thisApexClass = (ApexClass)thisItemType; - - apexClass.mergeFields(thisApexClass); - classes.remove(thisApexClass.toString()); - } else if (itemType instanceof ApexPrimitive && thisItemType instanceof ApexPrimitive) { - ApexPrimitive a = (ApexPrimitive)itemType; - ApexPrimitive b = (ApexPrimitive)thisItemType; - if (a.canBePromotedTo(b)) { - itemType = b; - } else if (b.canBePromotedTo(a)) { - continue; + // Merge in both directions (for object-overwrite) + thisApexClass.mergeFields(apexClass); + Set classesToRemove = apexClass.mergeFields(thisApexClass); + + classesToRemove.remove(itemType.toString()); + for (String className : classesToRemove) { + classes.remove(className); } - } else { - throw new RuntimeException("Can't add an " + o.getClass() + " to a collection of " + itemType.getClass()); - } + + } else if (itemType instanceof ApexPrimitive && thisItemType instanceof ApexPrimitive) { + ApexPrimitive a = (ApexPrimitive)itemType; + ApexPrimitive b = (ApexPrimitive)thisItemType; + if (a.canBePromotedTo(b)) { + itemType = b; + } else if (b.canBePromotedTo(a)) { + continue; + } + } else { + throw new RuntimeException("Can't add an " + o.getClass() + " to a collection of " + itemType.getClass()); } } + return itemType; } /** @return an ApexClass for this map */ ApexType typeOfMap(String propertyName, Map o) { Map members = makeMembers(o); + String newClassName = getClassName(propertyName); + ApexClass newClass = new ApexClass(newClassName, members); + // see if any existing classes have the same member set for (ApexClass cls : classes.values()) { - if (cls.membersEqual(members)) - return cls; + if (newClass.membersEqual(cls.getMembers())) { + cls.mergeFields(newClass); + return cls; + } } - String newClassName = getClassName(propertyName); - ApexClass newClass = new ApexClass(newClassName, members); + classes.put(newClassName, newClass); return newClass; }