package er.ajax.json.serializer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
import org.jabsorb.JSONSerializer;
import org.jabsorb.serializer.AbstractSerializer;
import org.jabsorb.serializer.MarshallException;
import org.jabsorb.serializer.ObjectMatch;
import org.jabsorb.serializer.SerializerState;
import org.jabsorb.serializer.UnmarshallException;
import org.json.JSONException;
import org.json.JSONObject;
import com.webobjects.appserver.WOSession;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOClassDescription;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOGlobalID;
import com.webobjects.eocontrol.EOTemporaryGlobalID;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableDictionary;
import er.extensions.appserver.ERXSession;
import er.extensions.eof.ERXEC;
import er.extensions.eof.ERXEOControlUtilities;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
/**
* La classe EOEnterpriseObjectSerializer s'occupe de la conversion des objets paramĂȘtres de type
* <code>EOEnterpriseObject</code> entre le monde Javascript et le monde Java.
*
* @property er.ajax.json.EOEditingContextFactory
* @property er.ajax.json.[entityName].canInsert
* @property er.ajax.json.[currentEntity.name].attributes
* @property er.ajax.json.[currentEntity.name].writableAttributes
* @property er.ajax.json.[currentEntity.name]relationships
*
* @author john
* @author <a href="mailto:jfveillette@os.ca">Jean-François Veillette</a>
*/
public class EOEnterpriseObjectSerializer extends AbstractSerializer {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
protected static final NSMutableDictionary<String, NSArray<String>> readableAttributeNames = new NSMutableDictionary<String, NSArray<String>>();
protected static final NSMutableDictionary<String, NSArray<String>> writableAttributeNames = new NSMutableDictionary<String, NSArray<String>>();
protected static final NSMutableDictionary<String, NSArray<String>> includedRelationshipNames = new NSMutableDictionary<String, NSArray<String>>();
private static Class[] _serializableClasses = new Class[] { EOEnterpriseObject.class };
private static Class[] _JSONClasses = new Class[] { JSONObject.class };
private EOEditingContextFactory _editingContextFactory;
public EOEnterpriseObjectSerializer() {
String editingContextFactory = ERXProperties.stringForKey("er.ajax.json.EOEditingContextFactory");
if (editingContextFactory == null) {
_editingContextFactory = new ERXECEditingContextFactory();
}
else {
try {
_editingContextFactory = (EOEditingContextFactory) Class.forName(editingContextFactory).newInstance();
}
catch (Exception e) {
throw new RuntimeException("Failed to initialize EOEnterpriseObjectSerializer.", e);
}
}
}
public Class[] getSerializableClasses() {
return _serializableClasses;
}
public Class[] getJSONClasses() {
return _JSONClasses;
}
protected boolean _canSerialize(Class clazz, Class jsonClazz) {
return super.canSerialize(clazz, jsonClazz);
}
@Override
public boolean canSerialize(Class clazz, Class jsonClazz) {
return (super.canSerialize(clazz, jsonClazz) || ((jsonClazz == null || jsonClazz == JSONObject.class) && EOEnterpriseObject.class.isAssignableFrom(clazz)));
}
public ObjectMatch tryUnmarshall(SerializerState state, Class clazz, Object jso) {
return null;
}
public Object unmarshall(SerializerState state, Class clazz, Object o) throws UnmarshallException {
try {
if (o == null) {
throw new UnmarshallException("eo missing");
}
JSONObject eoDict = (JSONObject) o;
if(eoDict.has("eo")) {
eoDict.getJSONObject("eo");
}
String gidString = eoDict.getString("gid");
if (gidString == null) {
throw new UnmarshallException("gid missing");
}
String parts[] = gidString.split("/");
String ecid = parts[0];
String entityName = parts[1];
EOEditingContext ec = null;
if(ecid != null) {
ec = editingContextForKey(ecid);
}
if(ec == null) {
ec = _editingContextFactory.newEditingContext();
registerEditingContext(ec);
}
ec.lock();
try {
String type = null;
String pk = null;
if (parts.length > 2) {
type = parts[2];
pk = parts[3];
}
EOEnterpriseObject eo;
EOGlobalID gid;
if(pk != null && pk.length() > 0) {
if ("T".equals(type)) {
byte[] bytes = ERXStringUtilities.hexStringToByteArray(pk);
gid = EOTemporaryGlobalID._gidForRawBytes(bytes);
eo = ec.objectForGlobalID(gid);
}
else {
pk = ERXStringUtilities.urlDecode(pk);
gid = ERXEOControlUtilities.globalIDForString(ec, entityName, pk);
eo = ec.faultForGlobalID(gid, ec);
}
}
else if (_canInsert(entityName)) {
eo = ERXEOControlUtilities.createAndInsertObject(ec, entityName);
}
else {
eo = null;
}
if (eo != null) {
NSArray<String> attributeNames = _writableAttributeNames(eo);
NSArray<String> relationshipNames = _includedRelationshipNames(eo);
for (Iterator iterator = eoDict.keys(); iterator.hasNext();) {
String key = (String) iterator.next();
if(!("javaClass".equals(key) || "gid".equals(key))) {
Object value = eoDict.get(key);
Object obj = ser.unmarshall(state, null, value);
if (attributeNames.containsObject(key)) {
if (obj == null && !relationshipNames.containsObject(key) && (eo.toOneRelationshipKeys().containsObject(key) || eo.toManyRelationshipKeys().containsObject(key))) {
// ignore nulls for non-included relationships
}
else {
eo.takeValueForKey(obj, key);
}
}
}
}
}
state.setSerialized(o, eo);
return eo;
}
finally {
ec.unlock();
}
}
catch (JSONException e) {
throw new UnmarshallException("Failed to unmarshall EO.", e);
}
}
public Object marshall(SerializerState state, Object p, Object o) throws MarshallException {
try {
EOEnterpriseObject eo = (EOEnterpriseObject) o;
JSONObject obj = new JSONObject();
obj.put("javaClass", o.getClass().getName());
EOEditingContext ec = eo.editingContext();
String ecid = registerEditingContext(ec);
String type;
String pkStr;
EOGlobalID gid = ec.globalIDForObject(eo);
if (gid instanceof EOTemporaryGlobalID) {
type = "T";
byte[] bytes = ((EOTemporaryGlobalID)gid)._rawBytes();
pkStr = ERXStringUtilities.byteArrayToHexString(bytes);
}
else {
type = "K";
pkStr = ERXEOControlUtilities.primaryKeyStringForObject(eo);
pkStr = ERXStringUtilities.urlEncode(pkStr);
}
obj.put("gid", ecid + "/" + eo.entityName() + "/" + type + "/" + pkStr);
addAttributes(state, eo, obj);
return obj;
}
catch (JSONException e) {
throw new MarshallException("Failed to marshall EO.", e);
}
}
/**
* This copies the attributes from the source EOEnterpriseObject to the destination. Only attributes which are class
* properties are copied. However if an attribute is a class property and also used in a relationship it is assumed
* to be an exposed primary or foreign key and not copied. Such attributes are set to null. See
* exposedKeyAttributeNames for details on how this is determined. It can be used when creating custom
* implementations of the duplicate() method in EOCopyable.
* @param state
* object that holds the sate of the serialization
* @param source
* the EOEnterpriseObject to copy attribute values from
* @param destination
* the EOEnterpriseObject to copy attribute values to
* @throws MarshallException if conversion failed
*/
public void addAttributes(SerializerState state, EOEnterpriseObject source, JSONObject destination) throws MarshallException {
boolean useEO = false;
try {
JSONObject eoData = destination;
if(useEO) {
destination = new JSONObject();
destination.put("eo", eoData);
state.push(source, eoData, "eo");
}
EOClassDescription cd = source.classDescription();
NSArray<String> attributeNames = _readableAttributeNames(source);
NSArray<String> relationshipNames = _includedRelationshipNames(source);
for (Enumeration e = attributeNames.objectEnumerator(); e.hasMoreElements();) {
String key = (String) e.nextElement();
Object jsonValue;
if(cd.toManyRelationshipKeys().containsObject(key)) {
if (relationshipNames.containsObject(key)) {
Object value = source.valueForKey(key);
jsonValue = ser.marshall(state, source, value, key);
}
else {
// JSONObject rel = new JSONObject();
// rel.put("javaClass", "com.webobjects.eocontrol.EOArrayFault");
// rel.put("sourceGlobalID", destination.get("gid"));
// rel.put("relationshipName", key);
// jsonValue = rel;
jsonValue = null;
}
} else if (cd.toOneRelationshipKeys().containsObject(key)) {
if (relationshipNames.containsObject(key)) {
Object value = source.valueForKey(key);
jsonValue = ser.marshall(state, source, value, key);
}
else {
// JSONObject rel = new JSONObject();
// rel.put("javaClass", "com.webobjects.eocontrol.EOFault");
// rel.put("sourceGlobalID", destination.get("gid"));
// rel.put("relationshipName", key);
// jsonValue = rel;
jsonValue = null;
}
} else {
Object value = source.valueForKey(key);
jsonValue = ser.marshall(state, source, value, key);
}
if (JSONSerializer.CIRC_REF_OR_DUPLICATE == jsonValue) {
destination.put(key, JSONObject.NULL);
}
else {
destination.put(key, jsonValue);
}
}
_addCustomAttributes(state, source, destination);
}
catch (JSONException e) {
throw new MarshallException("Failed to marshall EO.", e);
}
finally {
if(useEO) {
state.pop();
}
}
}
protected void _addCustomAttributes(SerializerState state, EOEnterpriseObject source, JSONObject destination) throws MarshallException {
// DO NOTHING
}
/**
* Override to return whether or not a new entity can be inserted.
* @param entityName name of an entity
*
* @return <code>true</code> if entity is insertable
*/
protected boolean _canInsert(String entityName) {
return ERXProperties.booleanForKeyWithDefault("er.ajax.json." + entityName + ".canInsert", false);
}
/**
* Override to return the appropriate attribute names.
* @param eo enterprise object
*
* @return array of attribute names
*/
protected NSArray<String> _readableAttributeNames(EOEnterpriseObject eo) {
return EOEnterpriseObjectSerializer.readableAttributeNames(eo);
}
/**
* Override to return the appropriate attribute names.
* @param eo enterprise object
*
* @return array of attribute names
*/
protected NSArray<String> _writableAttributeNames(EOEnterpriseObject eo) {
return EOEnterpriseObjectSerializer.writableAttributeNames(eo);
}
/**
* Override to return the appropriate relationship names.
* @param eo enterprise object
*
* @return array of relationship names
*/
protected NSArray<String> _includedRelationshipNames(EOEnterpriseObject eo) {
return EOEnterpriseObjectSerializer.includedRelationshipNames(eo);
}
/**
* Returns an array of attribute names from the EOEntity of source that should be marshalled to the client.
*
* @param source
* the EOEnterpriseObject to copy attribute values from
* @return an array of attribute names from the EOEntity of source that should be marshalled
*/
@SuppressWarnings({ "unchecked", "cast" })
public static NSArray<String> readableAttributeNames(EOEnterpriseObject source) {
// These are cached on EOEntity name as an optimization.
EOEntity entity = EOUtilities.entityForObject(source.editingContext(), source);
NSArray<String> attributeNames = EOEnterpriseObjectSerializer.readableAttributeNames.objectForKey(entity.name());
//AK: should use clientProperties from EM
if (attributeNames == null) {
EOEntity currentEntity = entity;
while (attributeNames == null && currentEntity != null) {
attributeNames = (NSArray<String>)ERXProperties.arrayForKey("er.ajax.json." + currentEntity.name() + ".attributes");
currentEntity = currentEntity.parentEntity();
}
if (attributeNames == null) {
//publicAttributes = source.attributeKeys();
//publicAttributeSet.addObjectsFromArray(publicAttributes);
//NSArray classProperties = entity.classPropertyNames();
//publicAttributeNames = publicAttributeSet.setByIntersectingSet(new NSSet(classProperties)).allObjects();
attributeNames = entity.clientClassPropertyNames();
}
EOEnterpriseObjectSerializer.readableAttributeNames.setObjectForKey(attributeNames, entity.name());
}
return attributeNames;
}
/**
* Returns an array of attribute names from the EOEntity of source that should be marshalled from the client.
*
* @param source
* the EOEnterpriseObject
* @return an array of attribute names from the EOEntity of source that should be unmarshalled
*/
@SuppressWarnings({ "unchecked", "cast" })
public static NSArray<String> writableAttributeNames(EOEnterpriseObject source) {
// These are cached on EOEntity name as an optimization.
EOEntity entity = EOUtilities.entityForObject(source.editingContext(), source);
NSArray<String> writableNames = EOEnterpriseObjectSerializer.writableAttributeNames.objectForKey(entity.name());
//AK: should use clientProperties from EM
if (writableNames == null) {
EOEntity currentEntity = entity;
while (writableNames == null && currentEntity != null) {
writableNames = (NSArray<String>)ERXProperties.arrayForKey("er.ajax.json." + currentEntity.name() + ".writableAttributes");
currentEntity = currentEntity.parentEntity();
}
if (writableNames == null) {
//publicAttributes = source.attributeKeys();
//publicAttributeSet.addObjectsFromArray(publicAttributes);
//NSArray classProperties = entity.classPropertyNames();
//publicAttributeNames = publicAttributeSet.setByIntersectingSet(new NSSet(classProperties)).allObjects();
writableNames = entity.clientClassPropertyNames();
}
EOEnterpriseObjectSerializer.writableAttributeNames.setObjectForKey(writableNames, entity.name());
}
return writableNames;
}
/**
* Returns an array of relationships on this EO that should be included in its marshalled output as
* the actual destination objects rather than just faults.
*
* @param source
* the EOEnterpriseObject being marhsalled
* @return an array of relationships that should be included in the marshalling
*/
@SuppressWarnings({ "unchecked", "cast" })
public static NSArray<String> includedRelationshipNames(EOEnterpriseObject source) {
// These are cached on EOEntity name as an optimization.
EOEntity entity = EOUtilities.entityForObject(source.editingContext(), source);
NSArray<String> relationshipNames = EOEnterpriseObjectSerializer.includedRelationshipNames.objectForKey(entity.name());
if (relationshipNames == null) {
EOEntity currentEntity = entity;
while (relationshipNames == null && currentEntity != null) {
relationshipNames = (NSArray<String>)ERXProperties.arrayForKey("er.ajax.json." + currentEntity.name() + ".relationships");
currentEntity = currentEntity.parentEntity();
}
if (relationshipNames == null) {
relationshipNames = entity.classDescriptionForInstances().toOneRelationshipKeys();
}
EOEnterpriseObjectSerializer.includedRelationshipNames.setObjectForKey(relationshipNames, entity.name());
}
return relationshipNames;
}
public static interface EOEditingContextFactory {
public EOEditingContext newEditingContext();
}
public static class ERXECEditingContextFactory implements EOEnterpriseObjectSerializer.EOEditingContextFactory {
public EOEditingContext newEditingContext() {
return ERXEC.newEditingContext();
}
}
public static class SadEditingContextFactory implements EOEnterpriseObjectSerializer.EOEditingContextFactory {
public EOEditingContext newEditingContext() {
return new EOEditingContext();
}
}
private static Map<EOEditingContext, String> _contexts = new WeakHashMap<>();
@SuppressWarnings("unchecked")
public static Map<EOEditingContext, String> contexts() {
Map<EOEditingContext, String> contexts;
WOSession session = ERXSession.anySession();
if (session == null) {
contexts = _contexts;
}
else {
contexts = (Map<EOEditingContext, String>) session.objectForKey("_jsonContexts");
if (contexts == null) {
contexts = new HashMap<>();
session.setObjectForKey(contexts, "_jsonContexts");
}
}
return contexts;
}
public static String registerEditingContext(EOEditingContext ec) {
Map<EOEditingContext, String> contexts = contexts();
synchronized (contexts) {
String id = contexts.get(ec);
if (id != null) {
return id;
}
id = UUID.randomUUID().toString();
contexts.put(ec, id);
return id;
}
}
@SuppressWarnings("unchecked")
public static EOEditingContext editingContextForKey(String key) {
Map<EOEditingContext, String> contexts = contexts();
synchronized (contexts) {
for (Iterator iterator = contexts.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<EOEditingContext, String> entry = (Map.Entry<EOEditingContext, String>) iterator.next();
if(entry.getValue().equals(key)) {
return entry.getKey();
}
}
return null;
}
}
}