Skip to content

Issue 32 (do not merge, full integration/regression tests pending) #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
/eclipse/classes
/integration-test/__pycache__
*.pyc

*.iml
*.ipr
*.iws
25 changes: 9 additions & 16 deletions app/controllers/Application.java
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
84 changes: 67 additions & 17 deletions app/models/ApexClass.java
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -12,24 +13,24 @@ public class ApexClass extends ApexType {
this.className = className;
this.members = members;
}

private final String className;
private final Map<ApexMember, ApexType> members;

public Map<ApexMember, ApexType> 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()) {
Expand All @@ -38,36 +39,85 @@ public boolean shouldGenerateExplictParse() {
}
return false;
}

@Override
public String toString() {
return className;
}

@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<ApexMember, ApexType> 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<ApexMember, ApexType> otherMembers) {
for (Map.Entry<ApexMember, ApexType> 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<String> mergeFields(ApexClass other) {
Set<String> 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;
}
}
7 changes: 7 additions & 0 deletions app/models/ApexList.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
6 changes: 4 additions & 2 deletions app/models/ApexPrimitive.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public String toString() {

@Override
public int hashCode() {
return Objects.hash(type, parserMethod);
return Objects.hash(type);
}

@Override
Expand All @@ -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);
}
}
52 changes: 32 additions & 20 deletions app/models/TypeFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<ApexMember, ApexType> 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;
}
Expand Down