PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/java-src/com/streambase/sbunit/ext/matcher/builder/JSONMatcherBuilder.java

https://github.com/streambase/SBUnit-Extensions
Java | 492 lines | 387 code | 39 blank | 66 comment | 68 complexity | ba7b6f471ba8263c3de12ca703321acb MD5 | raw file
  1. package com.streambase.sbunit.ext.matcher.builder;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.ArrayList;
  5. import java.util.EnumSet;
  6. import java.util.HashSet;
  7. import java.util.List;
  8. import java.util.TreeSet;
  9. import com.streambase.sb.AbstractFunction;
  10. import com.streambase.sb.BasicFunction;
  11. import com.streambase.sb.CompleteDataType.FunctionType;
  12. import com.streambase.sb.Function;
  13. import com.streambase.sb.Schema;
  14. import com.streambase.sb.Schema.Field;
  15. import com.streambase.sb.SchemaUtil;
  16. import com.streambase.sb.Timestamp;
  17. import com.streambase.sb.TupleJSONUtil.Options;
  18. import com.streambase.sb.ByteArrayView;
  19. import com.streambase.sb.CompleteDataType;
  20. import com.streambase.sb.DataType;
  21. import com.streambase.sb.StreamBaseException;
  22. import com.streambase.sb.Tuple;
  23. import com.streambase.sbunit.ext.Matchers;
  24. import com.streambase.sbunit.ext.matchers.FieldBasedTupleMatcher;
  25. import com.streambase.sb.internal.CoercedFunction;
  26. import com.streambase.sb.internal.SchemaJSONUtil;
  27. import com.streambase.sb.util.Msg;
  28. import com.streambase.sb.util.Util;
  29. import com.streambase.sb.util.Xml;
  30. import com.streambase.sb.TupleJSONUtil;
  31. import com.alibaba.fastjson.JSON;
  32. import com.alibaba.fastjson.JSONArray;
  33. import com.alibaba.fastjson.JSONException;
  34. import com.alibaba.fastjson.JSONObject;
  35. /**
  36. * Build matchers to match a subset of a tuple's fields from JSON strings. Any fields that aren't mentioned
  37. * in the constructor are ignored.
  38. *
  39. * Some of this code is "borrowed" from TupleJSONUtil, where there are some private classes "JSONFunctions" and setTuple() (molded to include matcher.*() methods).
  40. *
  41. * IMPORTANT NOTE: this code is single threaded (synchronized makeMatcher()) as handling of lists is non-reentrant (class global variable "handlingAlist").
  42. * This is OK as very high performance is not critical in Unit testing
  43. *
  44. * This matcher builder builds its matcher on a FieldBasedTupleMatcher, so that fields can be ignored, etc
  45. *
  46. */
  47. public class JSONMatcherBuilder {
  48. private final Schema completeSchema;
  49. private FieldBasedTupleMatcher matcher;
  50. private boolean ignoreMissingFields = false;
  51. SimpleDateFormat sdft = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSSZ");
  52. ArrayList<String> subFieldHandled = new ArrayList<String>(); // list of field that are of type "tuple"; don't want to "match.require" their parent, in case matching is "sparse"
  53. private boolean handlingAlist = false; // when handling lists, do not add a child node to matcher, only the top, parent node. this is also true of lists of tuples
  54. /**
  55. * create a Matcher that has a companion partial JSON string that mentions each node that will be of interest in the match
  56. * @param inSchema
  57. */
  58. public JSONMatcherBuilder( Schema inSchema ) {
  59. this.completeSchema = inSchema;
  60. }
  61. /**
  62. * Build a matcher from string values that matches only the fields given to the constructor.
  63. *
  64. * Note that either a null column or an empty string matches a null field
  65. *
  66. * similar to TupleJSONUtil.getTuplesFromJSON()
  67. *
  68. * @param columns String value for each column matching the field names in the constructor
  69. * @return The Matcher for this JSON row.
  70. * @throws StreamBaseException
  71. */
  72. public synchronized FieldBasedTupleMatcher makeMatcher(String columns) throws StreamBaseException {
  73. matcher = Matchers.emptyFieldMatcher(); // create an empty field matcher
  74. Tuple tuple = completeSchema.createTuple(); // parallel tuple for sub-tuple processing
  75. Object jsonObject = parseJSONString(columns); // create JSON object from string
  76. setTupleAndMatcher( tuple, jsonObject, null ); // fill in values, throw away the scratch tuple
  77. return matcher;
  78. }
  79. /**
  80. * create JSON object from string
  81. *
  82. * TODO: The method parseJSONString(String) from the type TupleJSONUtil is not visible, so is recopied here; could remove this if public
  83. *
  84. * @param JSONString
  85. * @return The object into which the JSON string was parsed
  86. * @throws StreamBaseException
  87. */
  88. public static Object parseJSONString(String JSONString)
  89. throws StreamBaseException {
  90. Object jsonObject = null;
  91. try {
  92. jsonObject = JSON.parse(JSONString);
  93. } catch (JSONException e) {
  94. // Error seems kinda nasty for something that could just be bad input
  95. // remap to SBException
  96. String msg = e.getMessage();
  97. if (msg == null || !(
  98. msg.contains("syntax error") ||
  99. msg.contains("error parse") ||
  100. msg.contains("TODO"))) {
  101. throw e;
  102. }
  103. }
  104. if (jsonObject == null) {
  105. throw new StreamBaseException("Invalid JSON string: " + JSONString);
  106. }
  107. return jsonObject;
  108. }
  109. /**
  110. * Set the fields of a tuple based on the contents of a JSON object
  111. * -- similar to TupleJSONUtil.setTuple(), but handles depth of matching in subtuples
  112. *
  113. * TODO: is there a way to do similar for lists? This is not evident in FieldBasedTupleMatcher, so a bit tricky i think
  114. *
  115. * @param tuple the tuple to set
  116. * @param jsonObject the JSON object; must have at least the fields of the tuple's schema
  117. * @throws JSONException
  118. * @throws StreamBaseException
  119. */
  120. public Tuple setTupleAndMatcher( Tuple tuple, Object jsonObject, String fieldParent ) throws StreamBaseException {
  121. TreeSet<String> missingInJSONString = new TreeSet<String>();
  122. Schema subSchema = tuple.getSchema();
  123. if (jsonObject instanceof JSONObject) {
  124. JSONObject jsonTuple = (JSONObject) jsonObject;
  125. for (Field field: subSchema.getFields()) {
  126. String fieldName = field.getName();
  127. Object jsonField = jsonTuple.get(fieldName);
  128. String fullFieldName = ((fieldParent == null) || (handlingAlist) )? fieldName : (fieldParent + "." + fieldName);
  129. if (jsonField != null) {
  130. try {
  131. Object o = jsonFunctions.get(field).convertJsonToTuple(fieldName, field.getCompleteDataType(), jsonField, fullFieldName );
  132. // adjust for long field type (JSON only handles int), or for string that is not really a string (Timestamp)
  133. if ( field.getDataType().equals(DataType.LONG)) {
  134. o = new Long( ((Integer)o).longValue() );
  135. } else if ( field.getDataType().equals(DataType.TIMESTAMP)) {
  136. o = new Timestamp(sdft.parse((String)o));
  137. }
  138. if(o == null) {
  139. if ((! handlingAlist) && (! subFieldHandled.contains(fullFieldName)) ){
  140. matcher.requireNull(fullFieldName); // do not place into matcher if this is part of a list array, that is handled later
  141. }
  142. tuple.setNull(fieldName);
  143. } else {
  144. if ((! handlingAlist) && (! subFieldHandled.contains(fullFieldName)) ) {
  145. matcher = matcher.require( fullFieldName, o );
  146. }
  147. tuple.setField( fieldName, o );
  148. }
  149. } catch (ClassCastException ex) {
  150. throw new StreamBaseException(ex);
  151. } catch (ParseException e) {
  152. throw new StreamBaseException(e);
  153. }
  154. } else
  155. if ( ignoreMissingFields ) {
  156. // TODO: fix "bug" in FieldBasedTupleMatcher: ignore will only work if field is first defined (.requireNull() is one way)
  157. matcher = matcher.requireNull(fullFieldName); // this fixes the bug where ".ignore()" expects the matcher field to already be defined
  158. matcher = matcher.ignore(fullFieldName);
  159. tuple.setNull( fieldName );
  160. } else {
  161. missingInJSONString.add( fullFieldName );
  162. }
  163. }
  164. if (!ignoreMissingFields && !missingInJSONString.isEmpty()) { // print error listing missing, mandatory fields
  165. StringBuilder err = new StringBuilder();
  166. err.append("Error setting tuple with schema ");
  167. err.append(subSchema.toHumanString());
  168. err.append(" from JSON string ");
  169. err.append(jsonTuple.toString());
  170. err.append(". The following ").append(missingInJSONString.size() > 1 ? "fields do" : "field does");
  171. err.append(" not exist in the JSON string ('use .ignoreMissingFields(true)) if appropriate: ");
  172. err.append(Util.join(", ", missingInJSONString));
  173. throw new StreamBaseException(err.toString());
  174. }
  175. // might as well give them a sorted error message
  176. TreeSet<String> unknownFields = new TreeSet<String>();
  177. for (Object jsonKey : jsonTuple.keySet()) {
  178. if ( subSchema.getFieldIndex(jsonKey.toString()) == Schema.NO_SUCH_FIELD) {
  179. unknownFields.add(jsonKey.toString());
  180. }
  181. }
  182. if (!unknownFields.isEmpty()) {
  183. StringBuilder err = new StringBuilder();
  184. err.append("Error setting tuple with schema ");
  185. err.append(subSchema.toHumanString());
  186. err.append(" from JSON string ");
  187. err.append(jsonTuple.toString());
  188. err.append(". The following ").append(unknownFields.size() > 1 ? "fields do" : "field does");
  189. err.append(" not exist in the schema: ");
  190. err.append(Util.join(", ", unknownFields));
  191. throw new StreamBaseException(err.toString());
  192. }
  193. } else if (jsonObject instanceof JSONArray) {
  194. JSONArray jsonArray = (JSONArray) jsonObject;
  195. if (jsonArray.size() != subSchema.getFieldCount()) {
  196. throw new StreamBaseException(Msg.format(
  197. "Error setting tuple with schema {0} from JSON string {1}. " +
  198. "Tuple has {2} fields, JSON has {3} fields",
  199. subSchema.toHumanString(), jsonArray.toString(),
  200. subSchema.getFieldCount(), jsonArray.size()));
  201. }
  202. for (int i = 0; i < jsonArray.size(); ++i) {
  203. Object jsonField = jsonArray.get(i);
  204. Schema.Field field = subSchema.getField(i);
  205. if (jsonField == null) {
  206. matcher = matcher.require(field.getName(), matcher);
  207. } else {
  208. matcher = matcher.ignore(field.getName());
  209. }
  210. }
  211. } else {
  212. throw new StreamBaseException("Unexpected type for jsonObject: " + jsonObject.getClass().getName());
  213. }
  214. return tuple;
  215. }
  216. /**
  217. * DataType.Registry machinery in support of TupleUtil.toJSONObject
  218. * >> similar to TupleJSONUtil, but handles depth of matching in subtuples
  219. */
  220. private class JSONFunctions implements DataType.Registry.Functor {
  221. /**
  222. * Convert a Tuple object to a JSON object.
  223. */
  224. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) throws StreamBaseException {
  225. return tupleObject;
  226. }
  227. /**
  228. * Convert a JSON object to a tuple. If the item could come from a list or a map, handle both.
  229. */
  230. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  231. return jsonObject;
  232. }
  233. }
  234. private DataType.Registry<JSONFunctions> jsonFunctions =
  235. new DataType.Registry<JSONFunctions>( new JSONFunctions(), DataType.BOOL, DataType.LONG, DataType.STRING);
  236. {
  237. jsonFunctions.register(DataType.DOUBLE, new JSONFunctions() {
  238. @Override
  239. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) throws StreamBaseException {
  240. if (Double.POSITIVE_INFINITY == (Double)tupleObject) {
  241. return "Infinity";
  242. }
  243. if (Double.NEGATIVE_INFINITY == (Double)tupleObject) {
  244. return "-Infinity";
  245. }
  246. if (Double.isNaN((Double)tupleObject)) {
  247. return "NaN";
  248. }
  249. return super.convertTupleToJson(type, tupleObject, options);
  250. }
  251. @Override
  252. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent )
  253. throws StreamBaseException {
  254. if ("Infinity".equals(jsonObject)) {
  255. return Double.POSITIVE_INFINITY;
  256. }
  257. if ("-Infinity".equals(jsonObject)) {
  258. return Double.NEGATIVE_INFINITY;
  259. }
  260. if ("NaN".equals(jsonObject)) {
  261. return Double.NaN;
  262. }
  263. if (jsonObject instanceof Number) {
  264. return ((Number)jsonObject).doubleValue();
  265. }
  266. return super.convertJsonToTuple( (fieldParent+fieldName), type, jsonObject, fieldParent);
  267. }
  268. });
  269. jsonFunctions.register(DataType.BLOB, new JSONFunctions() {
  270. @Override
  271. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) {
  272. return ((ByteArrayView) tupleObject).asString();
  273. }
  274. @Override
  275. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  276. return ByteArrayView.makeView(((String) jsonObject).getBytes());
  277. }
  278. });
  279. jsonFunctions.register(DataType.INT, new JSONFunctions() {
  280. @Override
  281. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) {
  282. return ((Integer)tupleObject).longValue();
  283. }
  284. @Override
  285. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  286. return ((Number)jsonObject).intValue();
  287. }
  288. });
  289. jsonFunctions.register(DataType.TIMESTAMP, new JSONFunctions() {
  290. @Override
  291. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) {
  292. return tupleObject.toString();
  293. }
  294. });
  295. jsonFunctions.register(DataType.TUPLE, new JSONFunctions() {
  296. @Override
  297. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) throws StreamBaseException {
  298. return TupleJSONUtil.toJSONObject((Tuple)tupleObject, options);
  299. }
  300. @Override
  301. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  302. Tuple tupleObject = type.getSchema().createTuple();
  303. /*if (fieldParent != null)*/ setTupleAndMatcher( tupleObject, jsonObject, fieldParent );
  304. subFieldHandled.add( fieldParent ); // don't want to redo "match.require(parent)", in case matching fields are "sparse"
  305. return tupleObject;
  306. }
  307. });
  308. jsonFunctions.register(DataType.LIST, new JSONFunctions() {
  309. @Override
  310. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) throws StreamBaseException {
  311. List<?> tList = (List<?>) tupleObject;
  312. JSONArray jList = new JSONArray();
  313. JSONFunctions elemConvert = jsonFunctions.get(type.getElementType());
  314. for (Object item: tList) {
  315. if (item == null) {
  316. jList.add(null);
  317. } else {
  318. jList.add(elemConvert.convertTupleToJson(type.getElementType(), item, options));
  319. }
  320. }
  321. return jList;
  322. }
  323. @Override
  324. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  325. List<?> jsonList = (List<?>)jsonObject;
  326. List<Object> tupleList = new ArrayList<Object>(jsonList.size());
  327. JSONFunctions elemConvert = jsonFunctions.get(type.getElementType());
  328. handlingAlist = true;
  329. for (Object item: jsonList) {
  330. if (item == null) {
  331. tupleList.add(null);
  332. } else {
  333. tupleList.add(elemConvert.convertJsonToTuple(fieldName, type.getElementType(), item, null));
  334. }
  335. }
  336. handlingAlist = false;
  337. // matcher.require( fieldName, tupleList );
  338. return tupleList;
  339. }
  340. });
  341. jsonFunctions.register(DataType.CAPTURE, new JSONFunctions() {
  342. @Override
  343. Object convertTupleToJson(CompleteDataType type, Object tupleObject, EnumSet<Options> options) throws StreamBaseException {
  344. Tuple capt = (Tuple) tupleObject;
  345. JSONObject jsonCapt = new JSONObject(true);
  346. jsonCapt.put("schema", Xml.serialize(capt.getSchema().as_xml()));
  347. jsonCapt.put("value", TupleJSONUtil.toJSONObject(capt, options));
  348. return jsonCapt;
  349. }
  350. @Override
  351. Object convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  352. JSONObject obj = (JSONObject) jsonObject;
  353. Schema s = new Schema(obj.getString("schema"));
  354. Tuple t = s.createTuple();
  355. setTupleAndMatcher( t, obj.get("value"), fieldParent );
  356. return t;
  357. }
  358. });
  359. jsonFunctions.register(DataType.FUNCTION, new JSONFunctions() {
  360. @Override
  361. Object convertTupleToJson(CompleteDataType type, Object functionObject, EnumSet<Options> options) throws StreamBaseException {
  362. // All our function implementations extend AbstractFunction
  363. AbstractFunction f = (AbstractFunction) functionObject;
  364. return f.getJSON();
  365. }
  366. @Override
  367. Function convertJsonToTuple(String fieldName, CompleteDataType type, Object jsonObject, String fieldParent) throws StreamBaseException {
  368. return readFunctionFromJSON((FunctionType) type, jsonObject, false);
  369. }
  370. });
  371. } // end of: private class JSONFunctions
  372. public static AbstractFunction readFunctionFromJSON(CompleteDataType cdt, Object jsonObject, boolean strict) throws StreamBaseException {
  373. return readFunctionFromJSON(cdt, jsonObject, strict, "");
  374. }
  375. public static AbstractFunction readFunctionFromJSON(CompleteDataType cdt, Object jsonObject, boolean strict, String timestampFormat) throws StreamBaseException {
  376. FunctionType type = (FunctionType) cdt;
  377. // do some validation
  378. if (! (jsonObject instanceof JSONObject)) {
  379. throw new StreamBaseException("The format for functions is JSON object");
  380. }
  381. JSONObject obj = (JSONObject) jsonObject;
  382. HashSet<String> keys = new HashSet<String>(obj.keySet());
  383. if (keys.contains("inner")) {
  384. // this is a coerced function.
  385. return readCoercedFunctionFromJSON(type, obj);
  386. } else {
  387. if (!keys.contains("body") && !keys.contains("function_definition")) {
  388. throw new StreamBaseException("JSON functions must have a body or definition");
  389. }
  390. keys.remove("body");
  391. keys.remove("function_definition");
  392. keys.remove("environment");
  393. keys.remove("name");
  394. keys.remove("environmentSchema");
  395. if (!keys.isEmpty()) {
  396. throw new StreamBaseException("Unknown JSON attributes: "+keys.toString());
  397. }
  398. return readBasicFunctionFromJSON(type, obj, strict, timestampFormat);
  399. }
  400. }
  401. private static AbstractFunction readCoercedFunctionFromJSON(FunctionType functionType, JSONObject obj) {
  402. try {
  403. Object inTransformer = obj.get("inTransformer");
  404. Object outTransformer = obj.get("outTransformer");
  405. FunctionType innerType = (FunctionType) SchemaJSONUtil.typeFromJSON(obj.get("innerType"));
  406. AbstractFunction inner = readFunctionFromJSON(innerType, (JSONObject) obj.get("inner"), true);
  407. return new CoercedFunction(functionType, inner, inTransformer, outTransformer);
  408. } catch (StreamBaseException ex) {
  409. throw new RuntimeException(ex);
  410. }
  411. }
  412. private static AbstractFunction readBasicFunctionFromJSON(FunctionType functionType, JSONObject obj, boolean strict, String timestampFormat) {
  413. try {
  414. Schema envSchema = SchemaUtil.EMPTY_SCHEMA;
  415. Tuple environment;
  416. String name;
  417. Tuple env = null;
  418. String stringRep;
  419. String body;
  420. if (obj.containsKey("environment")) {
  421. envSchema = SchemaJSONUtil.schemaFromJSON((JSONObject)obj.get("environmentSchema"));
  422. env = envSchema.createTuple();
  423. TupleJSONUtil.setTuple(env, obj.get("environment"), strict);
  424. environment = env.createReadOnlyTuple();
  425. } else {
  426. environment = SchemaUtil.EMPTY_SCHEMA.createTuple();
  427. }
  428. if (obj.containsKey("name")) {
  429. name = obj.getString("name");
  430. } else {
  431. name = null;
  432. }
  433. stringRep = obj.getString("function_definition");
  434. body = obj.getString("body");
  435. return new BasicFunction(functionType, environment, stringRep, body, name);
  436. } catch (StreamBaseException ex) {
  437. throw new RuntimeException("Malformed JSON string for function", ex);
  438. }
  439. }
  440. public FieldBasedTupleMatcher getMatcher() {
  441. return matcher;
  442. }
  443. public boolean isIgnoreMissingFields() {
  444. return ignoreMissingFields;
  445. }
  446. public void setIgnoreMissingFields(boolean ignoreMissingFields) {
  447. this.ignoreMissingFields = ignoreMissingFields;
  448. }
  449. public JSONMatcherBuilder ignoreMissingFields(boolean ignoreMissingFields) {
  450. this.ignoreMissingFields = ignoreMissingFields;
  451. return this;
  452. }
  453. }