package org.commcare.models;
import org.commcare.android.javarosa.AndroidXFormExtensions;
import org.commcare.android.javarosa.IntentCallout;
import org.commcare.android.javarosa.PollSensorAction;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
/**
* This class overrides the core PrototypeFactory class primarily because we
* can store our Android hashes as an int and thus store them in a map for faster lookups. Most
* other functionality is the same, except we override how we store and retrieve hashes
* so that we can use the Map.
*
* @author ctsims
* @author wspride
*/
public class AndroidPrototypeFactory extends PrototypeFactory {
private Hashtable<Integer, Class> prototypes;
private static final HashMap<String, Class> migratedClasses = new HashMap<>();
static {
// These class names were changed in CommCare 2.28; this migration
// mapping can be removed when we are sure no pre-2.28 device with
// saved forms is upgrading to 2.28 or higher
migratedClasses.put("org.odk.collect.android.jr.extensions.AndroidXFormExtensions",
AndroidXFormExtensions.class);
migratedClasses.put("org.odk.collect.android.jr.extensions.IntentCallout",
IntentCallout.class);
migratedClasses.put("org.odk.collect.android.jr.extensions.PollSensorAction",
PollSensorAction.class);
}
public AndroidPrototypeFactory(HashSet<String> classNames) {
super(AndroidClassHasher.getInstance(), classNames);
}
@Override
protected void lazyInit() {
initialized = false;
prototypes = new Hashtable<>();
super.lazyInit();
}
private Integer hashAsInteger(byte[] hash) {
return (hash[3]) + (hash[2] << 8) + (hash[1] << 16) + (hash[0] << 24);
}
@Override
public Class getClass(byte[] hash) {
if (!initialized) {
lazyInit();
}
return prototypes.get(hashAsInteger(hash));
}
@Override
protected void storeHash(Class c, byte[] hash) {
prototypes.put(hashAsInteger(hash), c);
}
@Override
protected void addMigratedClasses() {
// map old classname to new class. Needed to load data serialized with
// old classname. Subsequent writes should use the class's new name
for (Map.Entry<String, Class> c : migratedClasses.entrySet()) {
addMigratedClass(c.getKey(), c.getValue());
}
}
/**
* @param oldClassName Assumes the class name was stored in the prototype
* factory on earlier versions of CommCare. Hence
* don't check for hash collisions.
*/
private void addMigratedClass(String oldClassName, Class newClass) {
if (!initialized) {
lazyInit();
}
byte[] hashForOldClass = AndroidClassHasher.getInstance().getClassnameHash(oldClassName);
prototypes.put(hashAsInteger(hashForOldClass), newClass);
}
/**
* For testing purposes
*/
public static Set<String> getMigratedClassNames() {
return migratedClasses.keySet();
}
}