package winterwell.utils.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import winterwell.utils.Key;
import winterwell.utils.Utils;
import winterwell.utils.containers.ArrayMap;
import winterwell.utils.containers.ArraySet;
import winterwell.utils.io.FileUtils;
import winterwell.utils.io.XStreamBinaryConverter;
import winterwell.utils.io.XStreamBinaryConverter.BinaryXML;
import winterwell.utils.reporting.Log;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import com.thoughtworks.xstream.io.xml.CompactWriter;
/**
* Special case serialiser for {@link ConcurrentHashMap}.
* <p>
* NB: ConcurrentHashMap also has a special case in Java serialisation.
*
* @author daniel
*/
final class ConcurrentMapConverter implements SingleValueConverter {
@Override
public boolean canConvert(Class type) {
return type == ConcurrentHashMap.class;
}
@Override
public Object fromString(String str) {
HashMap<?, ?> amap = XStreamUtils.serialiseFromXml(str);
ConcurrentHashMap map = new ConcurrentHashMap(amap);
return map;
}
@Override
public String toString(Object obj) {
Map<?, ?> map = (Map) obj;
HashMap<?, ?> amap = new HashMap(map);
return XStreamUtils.serialiseToXml(amap);
}
}
/**
* Special case serialiser for {@link Timestamp}, due to bugs in transmission of
* Timestamps between different servers.
* <p>
* This loses some accuracy (nano vs milli-seconds) -- does anyone care?
*
* @author daniel
*/
final class TimestampConverter implements SingleValueConverter {
@Override
public boolean canConvert(Class type) {
return type == Timestamp.class;
}
@Override
public Object fromString(String str) {
// TODO should we also handle the xml from an un-modified XStream here?
Long utc = Long.valueOf(str);
return new Timestamp(utc);
}
@Override
public String toString(Object obj) {
Timestamp ts = (Timestamp) obj;
return Long.toString(ts.getTime());
}
}
/**
* Separated out to isolate the XStream dependency.
*
* @author daniel
* @testedby {@link XStreamUtilsTest}
*/
public class XStreamUtils {
private static XStream _xstream;
// static final XStream JSON_XSTREAM = new XStream(new
// JsonHierarchicalStreamDriver());
/**
* Setup aliases to give prettier shorter xml. E.g. <S>hello</S>
* instead of <java.lang.String>hello</java.lang.String>
*/
static void initShorterXml(XStream xstream) {
// prettier shorter xml
xstream.alias("key", Key.class);
xstream.alias("map", HashMap.class);
xstream.alias("amap", ArrayMap.class);
xstream.alias("cmap", ConcurrentHashMap.class);
xstream.alias("list", ArrayList.class);
xstream.alias("set", HashSet.class);
xstream.alias("aset", ArraySet.class);
// primitives (this is ugly)
xstream.alias("I", Integer.class);
xstream.alias("i", int.class);
xstream.alias("L", Long.class);
xstream.alias("l", long.class);
xstream.alias("D", Double.class);
xstream.alias("d", double.class);
xstream.alias("S", String.class);
xstream.alias("B", Boolean.class);
xstream.alias("b", boolean.class);
}
@SuppressWarnings("unchecked")
public static <X> X serialiseFromXml(InputStream xml) {
X x = (X) xstream().fromXML(xml);
FileUtils.close(xml);
return x;
}
@SuppressWarnings("unchecked")
public static <X> X serialiseFromXml(Reader xml) {
X x = (X) xstream().fromXML(xml);
FileUtils.close(xml);
return x;
}
@SuppressWarnings("unchecked")
public static <X> X serialiseFromXml(String xml) {
return (X) xstream().fromXML(xml);
}
/**
* Uses XStream
* <p>
* <h3>Thread Safety</h3>
* XStream serialisation is not thread safe :( If your object is edited by
* another thread during a save, it can lead to a corrupted version getting
* saved.
* <p>
* This method synchronises on the top-level object to provide a bit of
* safety. However there are cases where this is not enough: e.g. if child
* objects have synchronised blocks but the top level object doesn't.
*
* @param object
* @return an xml representation for object
* @see #serialiseFromXml(String)
*/
public static String serialiseToXml(Object object) {
// neither can nor need to sychronise on null - this is what xstream
// would return
if (object == null)
return "<null/>";
synchronized (object) {
StringWriter sw = new StringWriter();
CompactWriter compact = new CompactWriter(sw);
xstream().marshal(object, compact);
return sw.toString();
}
}
/**
* Uses XStream
* <p>
* <h3>Thread Safety</h3>
* XStream serialisation is not thread safe :( If your object is edited by
* another thread during a save, it can lead to a corrupted version getting
* saved.
* <p>
* This method synchronises on the top-level object to provide a bit of
* safety. However there are cases where this is not enough: e.g. if child
* objects have synchronised blocks but the top level object doesn't.
*
* @param writer
* Serialise out to here. This will NOT be closed here.
* @param object
* @see #serialiseFromXml(String)
*/
public static void serialiseToXml(Writer writer, Object object) {
// We neither can nor need to sychronise on null - this is what xstream
// would return
if (object == null) {
try {
writer.write("<null/>");
} catch (IOException e) {
throw Utils.runtime(e);
}
}
synchronized (object) {
CompactWriter compact = new CompactWriter(writer);
xstream().marshal(object, compact);
}
}
static XStream setupXStream() {
try {
XStream xstream = new XStream();
// prettier shorter xml
XStreamUtils.initShorterXml(xstream);
// Binary fields
xstream.registerConverter(new XStreamBinaryConverter());
// ConcurrentHashMaps
xstream.registerConverter(new ConcurrentMapConverter());
// Hack: SQL Timestamps
xstream.registerConverter(new TimestampConverter());
return xstream;
} catch (Throwable e) {
Log.report(e);
return null;
}
}
/**
* @return default XStream instance with: - shorter xml c.f.
* {@link #initShorterXml(XStream)} - {@link BinaryXML} via
* {@link XStreamBinaryConverter} - better ConcurrentHashMap output
* via {@link ConcurrentMapConverter}
*/
public static XStream xstream() {
if (XStreamUtils._xstream == null) {
XStreamUtils._xstream = setupXStream();
}
return XStreamUtils._xstream;
}
}