/*
* Copyright (C) 2009 JavaRosa
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.openrosa.client.jr.core.model.instance.utils;
import java.io.IOException;
import java.util.Date;
import java.util.Vector;
import org.openrosa.client.java.io.DataInputStream;
import org.openrosa.client.java.io.DataOutputStream;
import org.openrosa.client.jr.core.model.Constants;
import org.openrosa.client.jr.core.model.FormDef;
import org.openrosa.client.jr.core.model.data.BooleanData;
import org.openrosa.client.jr.core.model.data.DateData;
import org.openrosa.client.jr.core.model.data.DateTimeData;
import org.openrosa.client.jr.core.model.data.DecimalData;
import org.openrosa.client.jr.core.model.data.GeoPointData;
import org.openrosa.client.jr.core.model.data.IAnswerData;
import org.openrosa.client.jr.core.model.data.IntegerData;
import org.openrosa.client.jr.core.model.data.SelectMultiData;
import org.openrosa.client.jr.core.model.data.SelectOneData;
import org.openrosa.client.jr.core.model.data.StringData;
import org.openrosa.client.jr.core.model.data.TimeData;
import org.openrosa.client.jr.core.model.data.helper.Selection;
import org.openrosa.client.jr.core.model.instance.FormInstance;
import org.openrosa.client.jr.core.model.instance.InvalidReferenceException;
import org.openrosa.client.jr.core.model.instance.TreeElement;
import org.openrosa.client.jr.core.model.instance.TreeReference;
import org.openrosa.client.jr.core.services.storage.IStorageUtility;
import org.openrosa.client.jr.core.services.storage.StorageManager;
import org.openrosa.client.jr.core.services.storage.WrappingStorageUtility;
import org.openrosa.client.jr.core.util.externalizable.DeserializationException;
import org.openrosa.client.jr.core.util.externalizable.ExtUtil;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapBase;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapList;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapNullable;
import org.openrosa.client.jr.core.util.externalizable.ExtWrapTagged;
import org.openrosa.client.jr.core.util.externalizable.Externalizable;
import org.openrosa.client.jr.core.util.externalizable.ExternalizableWrapper;
import org.openrosa.client.jr.core.util.externalizable.PrototypeFactory;
/**
* An alternate serialization format for FormInstances (saved form instances) that drastically reduces the
* resultant record size by cutting out redundant information. Size savings are typically 90-95%. The trade-off is
* that in order to deserialize, a template FormInstance (typically from the original FormDef) must be provided.
*
* In general, the format is thus:
* 1) write the fields from the FormInstance object (e.g., date saved), excluding those that never change for a given
* form type (e.g., schema).
* 2) walk the tree depth-first. for each node: if repeatable, write the number of repetitions at the current level; if
* not, write a boolean indicating if the node is relevant. non-relevant nodes are not descended into. repeated nodes
* (i.e., several nodes with the same name at the current level) are handled in order
* 3) for each leaf (data) node, write a boolean whether the node is empty or has data
* 4) if the node has data, serialize the data. do not specify the data type -- it can be determined from the template.
* multiple choice questions use a more compact format than normal.
* 4a) in certain situations where the data differs from its prescribed data type (can happen as the result of 'calculate'
* expressions), flag the actual data type by hijacking the 'empty' flag above
*
* @author Drew Roos
*
*/
public class CompactInstanceWrapper implements WrappingStorageUtility.SerializationWrapper {
public static final int CHOICE_VALUE = 0; /* serialize multiple-select choices by writing out the <value> */
public static final int CHOICE_INDEX = 1; /* serialize multiple-select choices by writing out only the index of the
* choice; much more compact than CHOICE_VALUE, but the deserialized
* instance must be explicitly re-attached to the parent FormDef (not just
* the template data instance) before the instance can be serialized to xml
* (otherwise the actual xml <value>s are still unknown)
*/
public static final int CHOICE_MODE = CHOICE_INDEX;
private InstanceTemplateManager templateMgr; /* instance template provider; provides templates needed for deserialization. */
private FormInstance instance; /* underlying FormInstance to serialize/deserialize */
public CompactInstanceWrapper () {
this(null);
}
/**
*
* @param templateMgr template provider; if null, template is always fetched on-demand from RMS (slow!)
*/
public CompactInstanceWrapper (InstanceTemplateManager templateMgr) {
this.templateMgr = templateMgr;
}
public Class baseType () {
return FormInstance.class;
}
public void setData (Externalizable e) {
this.instance = (FormInstance)e;
}
public Externalizable getData () {
return instance;
}
/**
* deserialize a compact instance. note the retrieval of the template data instance
*/
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
int formID = ExtUtil.readInt(in);
instance = getTemplateInstance(formID).clone();
instance.setID(ExtUtil.readInt(in));
instance.setDateSaved((Date)ExtUtil.read(in, new ExtWrapNullable(Date.class)));
//formID, name, schema, versions, and namespaces are all invariants of the template instance
TreeElement root = instance.getRoot();
readTreeElement(root, in, pf);
}
/**
* serialize a compact instance
*/
public void writeExternal(DataOutputStream out) throws IOException {
if (instance == null) {
throw new RuntimeException("instance has not yet been set via setData()");
}
ExtUtil.writeNumeric(out, instance.getFormId());
ExtUtil.writeNumeric(out, instance.getID());
ExtUtil.write(out, new ExtWrapNullable(instance.getDateSaved()));
writeTreeElement(out, instance.getRoot());
}
private FormInstance getTemplateInstance (int formID) {
if (templateMgr != null) {
return templateMgr.getTemplateInstance(formID);
} else {
FormInstance template = loadTemplateInstance(formID);
if (template == null) {
throw new RuntimeException("no formdef found for form id [" + formID + "]");
}
return template;
}
}
/**
* load a template instance fresh from the original FormDef, retrieved from RMS
* @param formID
* @return
*/
public static FormInstance loadTemplateInstance (int formID) {
IStorageUtility forms = StorageManager.getStorage(FormDef.STORAGE_KEY);
FormDef f = (FormDef)forms.read(formID);
return (f != null ? f.getInstance() : null);
}
/**
* recursively read in a node of the instance, by filling out the template instance
* @param e
* @param ref
* @param in
* @param pf
* @throws IOException
* @throws DeserializationException
*/
private void readTreeElement (TreeElement e, DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
TreeElement templ = instance.getTemplatePath(e.getRef());
boolean isGroup = !templ.isLeaf();
if (isGroup) {
Vector childTypes = new Vector();
for (int i = 0; i < templ.getNumChildren(); i++) {
String childName = templ.getChildAt(i).getName();
if (!childTypes.contains(childName)) {
childTypes.addElement(childName);
}
}
for (int i = 0; i < childTypes.size(); i++) {
String childName = (String)childTypes.elementAt(i);
TreeReference childTemplRef = e.getRef().extendRef(childName, 0);
TreeElement childTempl = instance.getTemplatePath(childTemplRef);
boolean repeatable = childTempl.repeatable;
int n = ExtUtil.readInt(in);
boolean relevant = (n > 0);
if (!repeatable && n > 1) {
throw new DeserializationException("Detected repeated instances of a non-repeatable node");
}
if (repeatable) {
int mult = e.getChildMultiplicity(childName);
for (int j = mult - 1; j >= 0; j--) {
e.removeChild(childName, j);
}
for (int j = 0; j < n; j++) {
TreeReference dstRef = e.getRef().extendRef(childName, j);
try {
instance.copyNode(childTempl, dstRef);
} catch(InvalidReferenceException ire) {
//If there is an invalid reference, this is a malformed instance,
//so we'll throw a Deserialization exception.
TreeReference r = ire.getInvalidReference();
if(r == null) {
throw new DeserializationException("Null Reference while attempting to deserialize! " + ire.getMessage());
} else{
throw new DeserializationException("Invalid Reference while attemtping to deserialize! Reference: " + r.toString(true) + " | "+ ire.getMessage());
}
}
TreeElement child = e.getChild(childName, j);
child.setRelevant(true);
readTreeElement(child, in, pf);
}
} else {
TreeElement child = e.getChild(childName, 0);
child.setRelevant(relevant);
if (relevant) {
readTreeElement(child, in, pf);
}
}
}
} else {
e.setValue((IAnswerData)ExtUtil.read(in, new ExtWrapAnswerData(e.dataType)));
}
}
/**
* recursively write out a node of the instance
* @param out
* @param e
* @param ref
* @throws IOException
*/
private void writeTreeElement (DataOutputStream out, TreeElement e) throws IOException {
TreeElement templ = instance.getTemplatePath(e.getRef());
boolean isGroup = !templ.isLeaf();
if (isGroup) {
Vector childTypesHandled = new Vector();
for (int i = 0; i < templ.getNumChildren(); i++) {
String childName = templ.getChildAt(i).getName();
if (!childTypesHandled.contains(childName)) {
childTypesHandled.addElement(childName);
int mult = e.getChildMultiplicity(childName);
if (mult > 0 && !e.getChild(childName, 0).isRelevant()) {
mult = 0;
}
ExtUtil.writeNumeric(out, mult);
for (int j = 0; j < mult; j++) {
writeTreeElement(out, e.getChild(childName, j));
}
}
}
} else {
ExtUtil.write(out, new ExtWrapAnswerData(e.dataType, e.getValue()));
}
}
/**
* ExternalizableWrapper to handle writing out a node's data. In particular, handles:
* * empty nodes
* * ultra-compact serialization of multiple-choice answers
* * tagging with extra type information when the template alone will not contain sufficient information
*
* @author Drew Roos
*
*/
private class ExtWrapAnswerData extends ExternalizableWrapper {
int dataType;
public ExtWrapAnswerData (int dataType, IAnswerData val) {
this.val = val;
this.dataType = dataType;
}
public ExtWrapAnswerData (int dataType) {
this.dataType = dataType;
}
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
byte flag = in.readByte();
if (flag == 0x00) {
val = null;
} else {
Class answerType = classForDataType(dataType);
if (answerType == null) {
//custom data types
val = ExtUtil.read(in, new ExtWrapTagged(), pf);
} else if (answerType == SelectOneData.class) {
val = getSelectOne(ExtUtil.read(in, CHOICE_MODE == CHOICE_VALUE ? String.class : Integer.class));
} else if (answerType == SelectMultiData.class) {
val = getSelectMulti((Vector)ExtUtil.read(in, new ExtWrapList(CHOICE_MODE == CHOICE_VALUE ? String.class : Integer.class)));
} else {
switch (flag) {
case 0x40: answerType = StringData.class; break;
case 0x41: answerType = IntegerData.class; break;
case 0x42: answerType = DecimalData.class; break;
case 0x43: answerType = DateData.class; break;
case 0x44: answerType = BooleanData.class; break;
}
val = (IAnswerData)ExtUtil.read(in, answerType);
}
}
}
public void writeExternal(DataOutputStream out) throws IOException {
if (val == null) {
out.writeByte(0x00);
} else {
byte prefix = 0x01;
Externalizable serEntity;
if (dataType < 0 || dataType >= 100) {
//custom data types
serEntity = new ExtWrapTagged(val);
} else if (val instanceof SelectOneData) {
serEntity = new ExtWrapBase(compactSelectOne((SelectOneData)val));
} else if (val instanceof SelectMultiData) {
serEntity = new ExtWrapList(compactSelectMulti((SelectMultiData)val));
} else {
serEntity = (IAnswerData)val;
//flag when data type differs from the default data type in the <bind> (can happen with 'calculate's)
if (val.getClass() != classForDataType(dataType)) {
if (val instanceof StringData) {
prefix = 0x40;
} else if (val instanceof IntegerData) {
prefix = 0x41;
} else if (val instanceof DecimalData) {
prefix = 0x42;
} else if (val instanceof DateData) {
prefix = 0x43;
} else if (val instanceof BooleanData) {
prefix = 0x44;
} else {
throw new RuntimeException("divergent data type not allowed");
}
}
}
out.writeByte(prefix);
ExtUtil.write(out, serEntity);
}
}
public ExternalizableWrapper clone(Object val) {
throw new RuntimeException("not supported");
}
public void metaReadExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
throw new RuntimeException("not supported");
}
public void metaWriteExternal(DataOutputStream out) throws IOException {
throw new RuntimeException("not supported");
}
}
/**
* reduce a SelectOneData to an integer (index mode) or string (value mode)
* @param data
* @return Integer or String
*/
private Object compactSelectOne (SelectOneData data) {
Selection val = (Selection)data.getValue();
return extractSelection(val);
}
/**
* reduce a SelectMultiData to a vector of integers (index mode) or strings (value mode)
* @param data
* @return
*/
private Vector compactSelectMulti (SelectMultiData data) {
Vector val = (Vector)data.getValue();
Vector choices = new Vector();
for (int i = 0; i < val.size(); i++) {
choices.addElement(extractSelection((Selection)val.elementAt(i)));
}
return choices;
}
/**
* create a SelectOneData from an integer (index mode) or string (value mode)
*/
private SelectOneData getSelectOne (Object o) {
return new SelectOneData(makeSelection(o));
}
/**
* create a SelectMultiData from a vector of integers (index mode) or strings (value mode)
*/
private SelectMultiData getSelectMulti (Vector v) {
Vector choices = new Vector();
for (int i = 0; i < v.size(); i++) {
choices.addElement(makeSelection(v.elementAt(i)));
}
return new SelectMultiData(choices);
}
/**
* extract the value out of a Selection according to the current CHOICE_MODE
* @param s
* @return Integer or String
*/
private Object extractSelection (Selection s) {
switch (CHOICE_MODE) {
case CHOICE_VALUE:
return s.getValue();
case CHOICE_INDEX:
if (s.index == -1) {
throw new RuntimeException("trying to serialize in choice-index mode but selections do not have indexes set!");
}
return new Integer(s.index);
default: throw new IllegalArgumentException();
}
}
/**
* build a Selection from an integer or string, according to the current CHOICE_MODE
* @param o
* @return
*/
private Selection makeSelection (Object o) {
if (o instanceof String) {
return new Selection((String)o);
} else if (o instanceof Integer) {
return new Selection(((Integer)o).intValue());
} else {
throw new RuntimeException();
}
}
/**
* map xforms data types to the Class that represents that data in a FormInstance
* @param dataType
* @return
*/
public static Class classForDataType (int dataType) {
switch (dataType) {
case Constants.DATATYPE_NULL: return StringData.class;
case Constants.DATATYPE_TEXT: return StringData.class;
case Constants.DATATYPE_INTEGER: return IntegerData.class;
case Constants.DATATYPE_DECIMAL: return DecimalData.class;
case Constants.DATATYPE_BOOLEAN: return BooleanData.class;
case Constants.DATATYPE_DATE: return DateData.class;
case Constants.DATATYPE_TIME: return TimeData.class;
case Constants.DATATYPE_DATE_TIME: return DateTimeData.class;
case Constants.DATATYPE_CHOICE: return SelectOneData.class;
case Constants.DATATYPE_CHOICE_LIST: return SelectMultiData.class;
case Constants.DATATYPE_GEOPOINT: return GeoPointData.class;
default: return null;
}
}
}