JsonRequestsParser.java
/*
** Module : JsonRequestsParser.java
** Abstract : APIs to read the INPUT arguments from the HTTP servlet request, as JSON.
**
** Copyright (c) 2019-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------------------------Description--------------------------------------
** 001 CA 20190614 First version.
** 002 CA 20190628 Fixed parsing request JSON arguments (from the HTTP body).
** 003 CA 20200427 Fixed TABLE and added DATASET support; misc improvements.
** CA 20200514 Refactoring to allow common code for SOAP services support.
** CA 20220110 Refactored the temp-table's row-meta state to use constants for the indexes in the array
** used for serialization.
** CA 20220329 Added support for REST services written directly in Java.
** OM 20221103 Fixed parsing of tables from JSON string in loadTable().
** OM 20221205 Boolean values are not parsed but the original string value returned.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.rest;
import java.io.*;
import java.util.*;
import javax.servlet.http.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.util.*;
/**
* Helper class to parse a parameter's value, JSON-style.
*/
class JsonRequestsParser
extends RequestArguments
{
/** Used to parse the HTTP body as a JSON object. */
private final ThreadLocal<ObjectMapper> mapper = ThreadLocal.withInitial(ObjectMapper::new);
/**
* Parse the argument, by interpreting the request.
*
* @param body
* The request body.
* @param src
* The parameter's encoded source.
* @param request
* The request payload.
*
* @return The resolved argument.
*/
@Override
protected String parseArgumentInt(String body, String src, HttpServletRequest request)
throws IOException
{
String sval = super.parseArgumentInt(body, src, request);
if (sval != null)
{
return sval;
}
if (src.startsWith("${"))
{
src = src.substring(2);
}
if (src.endsWith("}"))
{
src = src.substring(0, src.length() - 1);
}
if (src.startsWith("json.object['request']."))
{
String jsonMode = src.substring("json.object['request'].".length());
String attr = RestHandler.extractVal(jsonMode);
jsonMode = jsonMode.substring(0, jsonMode.indexOf('['));
JsonNode rootNode = mapper.get().readTree(body);
JsonNode jsonReq = rootNode.path("request");
if (!jsonReq.isMissingNode())
{
jsonReq = jsonReq.path(attr);
if (!jsonReq.isMissingNode())
{
if (jsonReq.isNull())
{
return null;
}
if ("".equals(jsonMode))
{
switch (jsonReq.getNodeType())
{
case BOOLEAN:
jsonMode = "boolean";
break;
case STRING:
case NUMBER:
case BINARY:
jsonMode = "string";
break;
case ARRAY:
case NULL:
case OBJECT:
case POJO:
jsonMode = "object";
break;
}
}
// the values seem to be simply returned as its original string representation, not sure if
// there is any validation performed on the node's content
switch (jsonMode.toLowerCase())
{
case "boolean":
return jsonReq.asText();
case "integervalue":
return Integer.toString(jsonReq.asInt());
case "longvalue":
return Long.toString(jsonReq.asLong());
case "decimalvalue":
return Double.toString(jsonReq.asDouble());
case "string":
return jsonReq.asText();
case "object":
default:
return jsonReq.toString();
}
}
else
{
return null;
}
}
else
{
return null;
}
}
return null;
}
/**
* Load the specified dataset.
*
* @param content
* The dataset definition.
*
* @return A {@link DatasetWrapper} instance to be passed as argument to the remote call.
*/
@Override
protected DatasetWrapper loadDataSet(String content)
throws IOException
{
DataSetContainer dataset = new DataSetContainer(true, false, false);
ObjectNode root = (ObjectNode) mapper.get().readTree(content);
String dsName = root.fieldNames().next();
dataset.setStructureName(dsName);
List<DsTableDefinition> tableDefinitions = new ArrayList<>();
root = (ObjectNode) root.get(dsName);
Iterator<String> tables = root.fieldNames();
for (int i = 0; i < root.size(); i++)
{
String table = tables.next();
List<PropertyDefinition> props = new ArrayList<>();
List<Object[]> rows = new ArrayList<>();
JsonNode tableRoot = root.get(table);
if (tableRoot.isArray())
{
for (int j = 0; j < tableRoot.size(); j++)
{
JsonNode jrow = tableRoot.get(j);
rows.add(readRow(props, jrow));
}
}
else
{
rows.add(readRow(props, tableRoot));
}
Iterator<Object[]> iterRows = rows.iterator();
Iterator<PropertyDefinition> iterProps = props.iterator();
DsTableDefinition dsTable = new DsTableDefinition(new TableResultSet(true, false, false)
{
@Override
public Object[] nextRow()
{
return iterRows.next();
}
@Override
public PropertyDefinition nextProperty()
{
return iterProps.next();
}
@Override
public boolean hasMoreRows()
{
return iterRows.hasNext();
}
@Override
public boolean hasMoreProperties()
{
return iterProps.hasNext();
}
@Override
public Object[] nextRowMeta()
{
// nothing is kept here...
return new Object[TempRecord._META_PROPERTIES_LENGTH];
}
});
dsTable.setName(table);
tableDefinitions.add(dsTable);
}
dataset.setTableDefinitions(tableDefinitions);
DatasetWrapper ds = new DatasetWrapper(true, dataset);
ds.setDatasetName(dsName);
return ds;
}
/**
* Load the specified table.
*
* @param content
* The table definition.
*
* @return A {@link TableWrapper} instance to be passed as argument to the remote call.
*/
@Override
protected TableWrapper loadTable(String content)
throws IOException
{
List<PropertyDefinition> props = new ArrayList<>();
List<Object[]> rows = new ArrayList<>();
String tableName = "";
JsonNode root = mapper.get().readTree(content);
if (root.isArray())
{
// the table is directly serialized with multiple arrays rows
// (4) table name is not specified, request.tInfieldvalue is an array:
// "tInfieldvalue": [ { "t-infieldvalue": "some-value" } ]
for (int i = 0; i < root.size(); i++)
{
rows.add(readRow(props, root.get(i)));
}
}
else if (root.size() == 1)
{
// should be only one child
Iterator<String> it = root.fieldNames();
tableName = it.next(); // legacy name ?
JsonNode firstAndOnlyChild = root.get(tableName);
if (firstAndOnlyChild.isArray())
{
// (1) table name is specified, rows as array:
// "tInfieldvalue": { "t-infieldvalue": [] }
for (int i = 0; i < firstAndOnlyChild.size(); i++)
{
rows.add(readRow(props, firstAndOnlyChild.get(i)));
}
}
else if (firstAndOnlyChild.size() != 0)
{
// (2) table name is specified, single row as object
// "tInfieldvalue": { "t-infieldvalue": { field assignments }}
rows.add(readRow(props, firstAndOnlyChild)); // firstAndOnlyChild is a normal object
}
else
{
// (3) table name is not specified, request.tInfieldvalue is an object and there is a field with
// the same name as the table (the child is not an object or an array):
// "tInfieldvalue": { "t-infieldvalue": "some-value" }
props.add(new PropertyDefinition(tableName, unknown.class, tableName));
rows.add( new Object[] { firstAndOnlyChild.textValue() });
}
}
else
{
// (5) table name is not specified, rows are an object with more than one field:
// "tInfieldvalue": { "t-infieldvalue": "some-value", "t-outfieldvalue": "other-value" }
rows.add(readRow(props, root));
}
Iterator<Object[]> iterRows = rows.iterator();
Iterator<PropertyDefinition> iterProps = props.iterator();
TableWrapper tw = new TableWrapper(true, new TableResultSet(true, false, false)
{
@Override
public Object[] nextRow()
{
return iterRows.next();
}
@Override
public PropertyDefinition nextProperty()
{
return iterProps.next();
}
@Override
public boolean hasMoreRows()
{
return iterRows.hasNext();
}
@Override
public boolean hasMoreProperties()
{
return iterProps.hasNext();
}
@Override
public Object[] nextRowMeta()
{
// nothing is kept here...
return new Object[TempRecord._META_PROPERTIES_LENGTH];
}
});
tw.setTableName(tableName);
return tw;
}
/**
* Read a row from the specified JSON node.
*
* @param props
* The table properties.
* @param root
* The JSON row.
*
* @return The de-serialized row, based on each property type.
*/
protected Object[] readRow(List<PropertyDefinition> props, JsonNode root)
{
Object[] vals = new Object[root.size()];
ObjectNode node = (ObjectNode) root;
Iterator<String> itrFields = node.fieldNames();
boolean addProps = props.isEmpty();
for (int i = 0; i < root.size(); i++)
{
String field = itrFields.next();
vals[i] = node.get(field).asText(null);
if (addProps)
{
props.add(new PropertyDefinition(field, unknown.class, field));
}
}
return vals;
}
}