package org.commcare.android.storage.framework;
import android.support.annotation.Nullable;
import org.commcare.models.framework.Persisting;
import org.commcare.modern.models.MetaField;
import org.javarosa.core.services.storage.IMetaData;
import org.javarosa.core.services.storage.Persistable;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
/**
* Serialization logic for class fields with @Persisting annotations
*
* @author ctsims
*/
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
public class Persisted implements Persistable, IMetaData {
protected int recordId = -1;
private static final Hashtable<Class, ArrayList<Field>> fieldOrderings = new Hashtable<>();
private static final Comparator<Field> orderedComparator = new Comparator<Field>() {
@Override
public int compare(Field f1, Field f2) {
int i1 = f1.getAnnotation(Persisting.class).value();
int i2 = f2.getAnnotation(Persisting.class).value();
return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
}
};
@Override
public void readExternal(DataInputStream in, PrototypeFactory pf)
throws IOException, DeserializationException {
recordId = ExtUtil.readInt(in);
String currentField = null;
try {
for (Field f : getPersistedFieldsInOrder(getClass())) {
currentField = f.getName();
readVal(f, this, in);
}
} catch (IllegalAccessException iae) {
String message = currentField == null ? "" : (" for field " + currentField);
throw new DeserializationException(message, iae);
}
}
private static void readVal(Field f, Object o, DataInputStream in)
throws DeserializationException, IOException, IllegalAccessException {
synchronized (f) {
// 'f' is a cached field: sync access across threads
Persisting p = f.getAnnotation(Persisting.class);
Class type = f.getType();
try {
f.setAccessible(true);
if (type.equals(String.class)) {
String read = ExtUtil.readString(in);
f.set(o, p.nullable() ? ExtUtil.nullIfEmpty(read) : read);
return;
} else if (type.equals(Integer.TYPE)) {
//Primitive Integers
f.setInt(o, ExtUtil.readInt(in));
return;
} else if (type.equals(Date.class)) {
f.set(o, ExtUtil.readDate(in));
return;
} else if (type.isArray()) {
//We only support byte arrays for now
if (type.getComponentType().equals(Byte.TYPE)) {
f.set(o, ExtUtil.readBytes(in));
return;
}
} else if (type.equals(Boolean.TYPE)) {
f.setBoolean(o, ExtUtil.readBool(in));
return;
}
} finally {
f.setAccessible(false);
}
//By Default
throw new DeserializationException("Couldn't read persisted type " + f.getType().toString());
}
}
@Override
public void writeExternal(DataOutputStream out) throws IOException {
ExtUtil.writeNumeric(out, recordId);
try {
for (Field f : getPersistedFieldsInOrder(getClass())) {
writeVal(f, this, out);
}
} catch (IllegalAccessException iae) {
throw new RuntimeException(iae);
}
}
private static void writeVal(Field f, Object o, DataOutputStream out)
throws IOException, IllegalAccessException {
synchronized (f) {
// 'f' is a cached field: sync access across threads
try {
Persisting p = f.getAnnotation(Persisting.class);
Class type = f.getType();
f.setAccessible(true);
if (type.equals(String.class)) {
String s = (String)f.get(o);
ExtUtil.writeString(out, p.nullable() ? ExtUtil.emptyIfNull(s) : s);
return;
} else if (type.equals(Integer.TYPE)) {
ExtUtil.writeNumeric(out, f.getInt(o));
return;
} else if (type.equals(Date.class)) {
ExtUtil.writeDate(out, (Date)f.get(o));
return;
} else if (type.isArray()) {
//We only support byte arrays for now
if (type.getComponentType().equals(Byte.TYPE)) {
ExtUtil.writeBytes(out, (byte[])f.get(o));
return;
}
} else if (type.equals(Boolean.TYPE)) {
ExtUtil.writeBool(out, f.getBoolean(o));
return;
}
} finally {
f.setAccessible(false);
}
//By Default
throw new RuntimeException("Couldn't write persisted type " + f.getType().toString());
}
}
private static ArrayList<Field> getPersistedFieldsInOrder(Class persistedClass) {
ArrayList<Field> orderings;
synchronized (fieldOrderings) {
// Since fields are cached, we must sync changes in their
// accessibility across threads.
orderings = fieldOrderings.get(persistedClass);
if (orderings == null) {
orderings = new ArrayList<>();
fieldOrderings.put(persistedClass, orderings);
}
}
synchronized (orderings) {
if (orderings.size() == 0) {
for (Field f : persistedClass.getDeclaredFields()) {
if (f.isAnnotationPresent(Persisting.class)) {
orderings.add(f);
}
}
Collections.sort(orderings, orderedComparator);
}
return orderings;
}
}
@Override
public String[] getMetaDataFields() {
ArrayList<String> fields = new ArrayList<>();
addClassFieldsToMetas(fields, getClass());
addClassMethodsToMetas(fields, getClass());
return fields.toArray(new String[fields.size()]);
}
private static void addClassFieldsToMetas(List<String> fields, Class persistedClass) {
for (Field f : persistedClass.getDeclaredFields()) {
synchronized (f) {
try {
f.setAccessible(true);
if (f.isAnnotationPresent(MetaField.class)) {
MetaField mf = f.getAnnotation(MetaField.class);
fields.add(mf.value());
}
} finally {
f.setAccessible(false);
}
}
}
}
private static void addClassMethodsToMetas(List<String> fields, Class persistedClass) {
for (Method m : persistedClass.getDeclaredMethods()) {
synchronized (m) {
try {
m.setAccessible(true);
MetaField mf = m.getAnnotation(MetaField.class);
if (mf != null) {
fields.add(mf.value());
}
} finally {
m.setAccessible(false);
}
}
}
}
@Nullable
@Override
public Object getMetaData(String fieldName) {
try {
for (Field f : this.getClass().getDeclaredFields()) {
synchronized (f) {
try {
f.setAccessible(true);
if (f.isAnnotationPresent(MetaField.class)) {
MetaField mf = f.getAnnotation(MetaField.class);
if (mf.value().equals(fieldName)) {
return f.get(this);
}
}
} finally {
f.setAccessible(false);
}
}
}
for (Method m : this.getClass().getDeclaredMethods()) {
synchronized (m) {
try {
m.setAccessible(true);
MetaField mf = m.getAnnotation(MetaField.class);
if (mf != null && mf.value().equals(fieldName)) {
return m.invoke(this, (Object[])null);
}
} finally {
m.setAccessible(false);
}
}
}
} catch (InvocationTargetException | IllegalArgumentException
| IllegalAccessException e) {
throw new RuntimeException(e.getMessage());
}
//If we didn't find the field
throw new IllegalArgumentException("No metadata field " + fieldName + " in the case storage system");
}
@Override
public void setID(int ID) {
recordId = ID;
}
@Override
public int getID() {
return recordId;
}
}