blob: 9a75e188578bbd40eda9665942239e610848aa58 [file] [log] [blame]
/**
* Derby - Class org.apache.derbyTesting.functionTests.tests.lang.SimpleJsonTest
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.derbyTesting.functionTests.tests.lang;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import junit.framework.Test;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.SupportFilesSetup;
import org.apache.derbyTesting.junit.TestConfiguration;
import org.apache.derbyTesting.functionTests.util.PrivilegedFileOpsForTests;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
/**
* <p>
* Basic test of the optional tool which provides JSON support functions
* which use the simple json library from https://code.google.com/p/json-simple/.
* </p>
*/
public class SimpleJsonTest extends BaseJDBCTestCase
{
///////////////////////////////////////////////////////////////////////////////////
//
// CONSTANTS
//
///////////////////////////////////////////////////////////////////////////////////
private static final String TAB = " ";
private static final String USER_ERROR = "38000";
private static final String OUT_OF_RANGE = "22003";
private static final String THERMOSTAT_READINGS =
"[\n" +
" {\n" +
" \"id\": 1,\n" +
" \"temperature\": 70.3,\n" +
" \"fanOn\": true\n" +
" },\n" +
" {\n" +
" \"id\": 2,\n" +
" \"temperature\": 65.5,\n" +
" \"fanOn\": false\n" +
" }\n" +
"]";
///////////////////////////////////////////////////////////////////////////////////
//
// CONSTRUCTORS
//
///////////////////////////////////////////////////////////////////////////////////
public SimpleJsonTest(String name) {
super(name);
}
///////////////////////////////////////////////////////////////////////////////////
//
// JUnit MACHINERY
//
///////////////////////////////////////////////////////////////////////////////////
public static Test suite()
{
BaseTestSuite suite = new BaseTestSuite("SimpleJsonTest");
suite.addTest( TestConfiguration.defaultSuite( SimpleJsonTest.class ) );
return new SupportFilesSetup
(
suite,
new String[]
{
"functionTests/tests/lang/thermostatReadings.dat",
"functionTests/tests/lang/json.dat",
}
);
}
///////////////////////////////////////////////////////////////////////////////////
//
// JUnit TESTS
//
///////////////////////////////////////////////////////////////////////////////////
public void testBasic_001() throws Exception
{
Connection conn = getConnection();
// create the json support types and functions
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
createSchema_001( conn );
assertResults
(
conn,
"values prettyPrint( toJSON( " +
"'select * from thermostatReadings order by \"id\", \"sampleTime\"' ) )",
new String[][]
{
{
"[\n" +
" {\n" +
" \"fanOn\" : false, \n" +
" \"id\" : 1, \n" +
" \"sampleTime\" : \"2015-07-08 04:03:20.0\", \n" +
" \"temperature\" : 65.5\n" +
" }, \n" +
" {\n" +
" \"fanOn\" : true, \n" +
" \"id\" : 1, \n" +
" \"sampleTime\" : \"2015-07-08 13:03:20.0\", \n" +
" \"temperature\" : 70.1\n" +
" }, \n" +
" {\n" +
" \"fanOn\" : false, \n" +
" \"id\" : 2, \n" +
" \"sampleTime\" : \"2015-07-08 03:03:20.0\", \n" +
" \"temperature\" : 64.5\n" +
" }, \n" +
" {\n" +
" \"fanOn\" : true, \n" +
" \"id\" : 2, \n" +
" \"sampleTime\" : \"2015-07-08 16:03:20.0\", \n" +
" \"temperature\" : 72.1\n" +
" }\n" +
"]"
}
},
true
);
assertResults
(
conn,
"values prettyPrint( toJSON( 'select \"id\", max( \"temperature\" ) \"maxTemp\" from thermostatReadings group by \"id\"' ) )",
new String[][]
{
{
"[\n" +
" {\n" +
" \"id\" : 1, \n" +
" \"maxTemp\" : 70.1\n" +
" }, \n" +
" {\n" +
" \"id\" : 2, \n" +
" \"maxTemp\" : 72.1\n" +
" }\n" +
"]"
}
},
true
);
assertResults
(
conn,
"values prettyPrint( toJSON( 'select \"id\", max( \"temperature\" ) \"maxTemp\" from thermostatReadings where \"id\" = ? group by \"id\"', '2' ) )",
new String[][]
{
{
"[\n" +
" {\n" +
" \"id\" : 2, \n" +
" \"maxTemp\" : 72.1\n" +
" }\n" +
"]"
}
},
true
);
dropSchema_001( conn );
// drop the json support types and functions
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
private void createSchema_001( Connection conn ) throws Exception
{
goodStatement
(
conn,
"create table thermostatReadings\n" +
"(\n" +
"\"id\" int,\n" +
"\"temperature\" double,\n" +
"\"fanOn\" boolean,\n" +
"\"sampleTime\" timestamp,\n" +
"primary key( \"id\", \"sampleTime\" )\n" +
")\n"
);
goodStatement
(
conn,
"insert into thermostatReadings values\n" +
"( 1, 65.5, false, timestamp( '2015-07-08 04:03:20') ),\n" +
"( 1, 70.1, true, timestamp( '2015-07-08 13:03:20') ),\n" +
"( 2, 64.5, false, timestamp( '2015-07-08 03:03:20') ),\n" +
"( 2, 72.1, true, timestamp( '2015-07-08 16:03:20') )\n"
);
goodStatement
(
conn,
"create function prettyPrint( doc JSONArray ) returns varchar( 32672 )\n" +
"language java parameter style java no sql\n" +
"external name '" + getClass().getName() + ".prettyPrint'\n"
);
}
private void dropSchema_001( Connection conn ) throws Exception
{
goodStatement( conn, "drop function prettyPrint" );
goodStatement( conn, "drop table thermostatReadings" );
}
///////////////////////////////////////////////////////////////////////////////////
/**
* <p>
* Test the jsonification of Derby's datatypes.
* </p>
*/
public void testDatatypes_002() throws Exception
{
Connection conn = getConnection();
// create the json support types and functions
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
createSchema_002( conn );
assertResults
(
conn,
"values( prettyPrint( toJSON( 'select * from all_types order by key_col' ) ) )",
new String[][]
{
{
"[\n" +
" {\n" +
" \"BIGINT_COL\" : 0, \n" +
" \"BLOB_COL\" : \"01\", \n" +
" \"CHAR_COL\" : \"0 \", \n" +
" \"CHAR_FOR_BIT_DATA_COL\" : \"de202020202020202020\", \n" +
" \"CLOB_COL\" : \"0\", \n" +
" \"DATE_COL\" : \"1994-02-23\", \n" +
" \"DECIMAL_COL\" : 0.0, \n" +
" \"DOUBLE_COL\" : 0.0, \n" +
" \"INT_COL\" : 0, \n" +
" \"JSON_ARRAY_COL\" : [\n" +
" {\n" +
" \"A\" : 1, \n" +
" \"B\" : true\n" +
" }, \n" +
" {\n" +
" \"A\" : 2, \n" +
" \"B\" : false\n" +
" }\n" +
" ], \n" +
" \"KEY_COL\" : 0, \n" +
" \"LONG_VARCHAR_COL\" : \"0\", \n" +
" \"LONG_VARCHAR_FOR_BIT_DATA_COL\" : \"de\", \n" +
" \"NUMERIC_COL\" : 0.0, \n" +
" \"PRICE_COL\" : \"Price( USD, 9.99000, 2009-10-16 14:24:43.0 )\", \n" +
" \"REAL_COL\" : 0.0, \n" +
" \"SMALLINT_COL\" : 0, \n" +
" \"TIMESTAMP_COL\" : \"1962-09-23 03:23:34.234\", \n" +
" \"TIME_COL\" : \"15:09:02\", \n" +
" \"VARCHAR_COL\" : \"0\", \n" +
" \"VARCHAR_FOR_BIT_DATA_COL\" : \"de\"\n" +
" }, \n" +
" {\n" +
" \"BIGINT_COL\" : null, \n" +
" \"BLOB_COL\" : null, \n" +
" \"CHAR_COL\" : null, \n" +
" \"CHAR_FOR_BIT_DATA_COL\" : null, \n" +
" \"CLOB_COL\" : null, \n" +
" \"DATE_COL\" : null, \n" +
" \"DECIMAL_COL\" : null, \n" +
" \"DOUBLE_COL\" : null, \n" +
" \"INT_COL\" : null, \n" +
" \"JSON_ARRAY_COL\" : null, \n" +
" \"KEY_COL\" : 1, \n" +
" \"LONG_VARCHAR_COL\" : null, \n" +
" \"LONG_VARCHAR_FOR_BIT_DATA_COL\" : null, \n" +
" \"NUMERIC_COL\" : null, \n" +
" \"PRICE_COL\" : null, \n" +
" \"REAL_COL\" : null, \n" +
" \"SMALLINT_COL\" : null, \n" +
" \"TIMESTAMP_COL\" : null, \n" +
" \"TIME_COL\" : null, \n" +
" \"VARCHAR_COL\" : null, \n" +
" \"VARCHAR_FOR_BIT_DATA_COL\" : null\n" +
" }\n" +
"]" }
},
true
);
dropSchema_002( conn );
// drop the json support types and functions
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
private void createSchema_002( Connection conn ) throws Exception
{
goodStatement
(
conn,
"create type Price external name 'org.apache.derbyTesting.functionTests.tests.lang.Price' language java"
);
goodStatement
(
conn,
"create function makePrice( currencyCode char( 3 ), amount decimal( 31, 5 ), timeInstant Timestamp )\n" +
"returns Price language java parameter style java no sql\n" +
"external name 'org.apache.derbyTesting.functionTests.tests.lang.Price.makePrice'\n"
);
goodStatement
(
conn,
"create function makeSimpleBlob( ) returns blob\n" +
"language java parameter style java no sql deterministic\n" +
"external name 'org.apache.derbyTesting.functionTests.tests.lang.BooleanValuesTest.makeSimpleBlob'\n"
);
goodStatement
(
conn,
"create table dummy( a int, b boolean )"
);
goodStatement
(
conn,
"insert into dummy values ( 1, true ), ( 2, false )"
);
goodStatement
(
conn,
"create table all_types\n" +
"(\n" +
" key_col int,\n" +
" bigint_col BIGINT,\n" +
" blob_col BLOB(2147483647),\n" +
" char_col CHAR(10),\n" +
" char_for_bit_data_col CHAR (10) FOR BIT DATA,\n" +
" clob_col CLOB(2147483647),\n" +
" date_col DATE,\n" +
" decimal_col DECIMAL(5,2),\n" +
" real_col REAL,\n" +
" double_col DOUBLE,\n" +
" int_col INTEGER,\n" +
" long_varchar_col LONG VARCHAR,\n" +
" long_varchar_for_bit_data_col LONG VARCHAR FOR BIT DATA,\n" +
" numeric_col NUMERIC(5,2),\n" +
" smallint_col SMALLINT,\n" +
" time_col TIME,\n" +
" timestamp_col TIMESTAMP,\n" +
" varchar_col VARCHAR(10),\n" +
" varchar_for_bit_data_col VARCHAR (10) FOR BIT DATA,\n" +
" price_col price,\n" +
" json_array_col jsonArray\n" +
")\n"
);
goodStatement
(
conn,
"insert into all_types( key_col ) values ( 1 )"
);
goodStatement
(
conn,
"insert into all_types\n" +
"(\n" +
" key_col,\n" +
" bigint_col,\n" +
" blob_col,\n" +
" char_col,\n" +
" char_for_bit_data_col,\n" +
" clob_col,\n" +
" date_col,\n" +
" decimal_col,\n" +
" real_col,\n" +
" double_col,\n" +
" int_col,\n" +
" long_varchar_col,\n" +
" long_varchar_for_bit_data_col,\n" +
" numeric_col,\n" +
" smallint_col,\n" +
" time_col,\n" +
" timestamp_col,\n" +
" varchar_col,\n" +
" varchar_for_bit_data_col,\n" +
" price_col,\n" +
" json_array_col\n" +
")\n" +
"values\n" +
"(\n" +
" 0,\n" +
" 0,\n" +
" makeSimpleBlob(),\n" +
" '0',\n" +
" X'DE',\n" +
" '0',\n" +
" date('1994-02-23'),\n" +
" 0.00,\n" +
" 0.0,\n" +
" 0.0,\n" +
" 0,\n" +
" '0',\n" +
" X'DE',\n" +
" 0.00,\n" +
" 0,\n" +
" time('15:09:02'),\n" +
" timestamp('1962-09-23 03:23:34.234'),\n" +
" '0',\n" +
" X'DE',\n" +
" makePrice( 'USD', cast( 9.99 as decimal( 31, 5 ) ), timestamp('2009-10-16 14:24:43') ),\n" +
" toJSON( 'select * from dummy order by a' ) \n" +
")\n"
);
goodStatement
(
conn,
"create function prettyPrint( doc JSONArray ) returns varchar( 32672 )\n" +
"language java parameter style java no sql\n" +
"external name 'org.apache.derbyTesting.functionTests.tests.lang.SimpleJsonTest.prettyPrint'\n"
);
}
private void dropSchema_002( Connection conn ) throws Exception
{
goodStatement
(
conn,
"drop function makePrice"
);
goodStatement
(
conn,
"drop function makeSimpleBlob"
);
goodStatement
(
conn,
"drop table all_types"
);
goodStatement
(
conn,
"drop type price restrict"
);
goodStatement
(
conn,
"drop function prettyPrint"
);
}
///////////////////////////////////////////////////////////////////////////////////
/**
* <p>
* Test errors.
* </p>
*/
public void testNegative_003() throws Exception
{
Connection conn = getConnection();
// can't load the tool redundantly
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
expectExecutionError
(
conn,
USER_ERROR,
"call syscs_util.syscs_register_tool( 'simpleJson', true )"
);
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
// can't unload the tool redundantly
expectExecutionError
(
conn,
USER_ERROR,
"call syscs_util.syscs_register_tool( 'simpleJson', false )"
);
// need to specify all ? parameters
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
expectExecutionError
(
conn,
USER_ERROR,
"values( toJSON( 'select * from sys.systables where tablename = ?' ) )"
);
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
///////////////////////////////////////////////////////////////////////////////////
/**
* <p>
* Test the SimpleJsonVTI.
* </p>
*/
public void testVTI_004() throws Exception
{
Connection conn = getConnection();
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
// declare a table function for reading a string
goodStatement
(
conn,
"create function thermostatReadings( jsonDocument JSONArray )\n" +
"returns table\n" +
"(\n" +
"\"id\" int,\n" +
"\"temperature\" float,\n" +
"\"fanOn\" boolean\n" +
")\n" +
"language java parameter style derby_jdbc_result_set contains sql\n" +
"external name 'org.apache.derby.optional.api.SimpleJsonVTI.readArray'\n"
);
PreparedStatement ps;
ResultSet rs;
// turn a JSON document string into a ResultSet
ps = conn.prepareStatement
(
"select * from table\n" +
"( thermostatReadings( readArrayFromString(?) ) ) t order by \"id\""
);
ps.setString( 1, THERMOSTAT_READINGS );
rs = ps.executeQuery();
assertResults
(
rs,
new String[][]
{
{ "1", "70.3", "true" },
{ "2", "65.5", "false" }
},
true
);
rs.close();
ps.close();
// make a ResultSet out of a file containing JSON text
File inputFile = SupportFilesSetup.getReadOnly( "thermostatReadings.dat" );
String[][] fileReadings = new String[][]
{
{ "1", "70.3", "true" },
{ "2", "65.5", "false" },
{ "3", "60.5", "false" },
};
ps = conn.prepareStatement
(
"select * from table\n" +
"( thermostatReadings( readArrayFromFile( ?, 'UTF-8' ) ) ) t order by \"id\""
);
ps.setString( 1, PrivilegedFileOpsForTests.getAbsolutePath( inputFile ) );
rs = ps.executeQuery();
assertResults(rs, fileReadings, true );
rs.close();
ps.close();
// make a ResultSet out of an URL which points to a file containing JSON text
ps = conn.prepareStatement
(
"select * from table\n" +
"( thermostatReadings( readArrayFromURL( ?, 'UTF-8' ) ) ) t order by \"id\""
);
String inputFileURL = PrivilegedFileOpsForTests.toURI(inputFile ).toURL().toString();
ps.setString( 1, inputFileURL);
rs = ps.executeQuery();
assertResults(rs, fileReadings, true );
rs.close();
ps.close();
goodStatement( conn, "drop function thermostatReadings" );
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
/**
* <p>
* Test the datatypes understood by SimpleJsonVTI.
* </p>
*/
public void testVTIdatatypes005() throws Exception
{
Connection conn = getConnection();
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
vetDatatype_005
(
conn,
"smallint",
new String[][]
{
{ "abc","true", "127" },
{ "def", "false", "1" },
{ "ghi", null, "345" },
{ "lmn", "true", "-1" },
}
);
vetDatatype_005
(
conn,
"int",
new String[][]
{
{ "abc","true", "127" },
{ "def", "false", "1" },
{ "ghi", null, "345" },
{ "lmn", "true", "-1" },
}
);
vetDatatype_005
(
conn,
"bigint",
new String[][]
{
{ "abc","true", "127" },
{ "def", "false", "1" },
{ "ghi", null, "345" },
{ "lmn", "true", "9223372036854775807" },
}
);
vetDatatype_005
(
conn,
"float",
new String[][]
{
{ "abc","true", "127.0" },
{ "def", "false", "1.2" },
{ "ghi", null, "345.67" },
{ "lmn", "true", "9.223372036854776E18" },
}
);
vetDatatype_005
(
conn,
"double",
new String[][]
{
{ "abc","true", "127.0" },
{ "def", "false", "1.2" },
{ "ghi", null, "345.67" },
{ "lmn", "true", "9.223372036854776E18" },
}
);
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
private void vetDatatype_005
(
Connection conn,
String datatype,
String[][] expectedResults
)
throws Exception
{
createFunction_005( conn, datatype );
PreparedStatement ps = conn.prepareStatement
(
"select * from table\n" +
"( f_" + datatype + "( readArrayFromFile( ?, 'UTF-8' ) )) t\n"
);
File inputFile = SupportFilesSetup.getReadOnly( "json.dat" );
ps.setString( 1, PrivilegedFileOpsForTests.getAbsolutePath( inputFile ) );
ResultSet rs;
rs = ps.executeQuery();
assertResults(rs, expectedResults, true );
rs.close();
// the first two rows have numeric values which won't raise
// truncation exceptions when fetched into tinyint
rs = ps.executeQuery();
rs.next();
assertEquals( (byte) 127, rs.getByte( "NUM_COL" ) );
rs.next();
assertEquals( (byte) 1, rs.getByte( "NUM_COL" ) );
rs.close();
ps.close();
dropFunction_005( conn, datatype );
}
private void createFunction_005( Connection conn, String datatype )
throws Exception
{
goodStatement
(
conn,
"create function f_" + datatype + "( jsonArray JSONArray )\n" +
"returns table\n" +
"(\n" +
" str_col varchar( 10 ),\n" +
" bool_col boolean,\n" +
" num_col " + datatype + "\n" +
")\n" +
"language java parameter style derby_jdbc_result_set contains sql\n" +
"external name 'org.apache.derby.optional.api.SimpleJsonVTI.readArray'\n"
);
}
private void dropFunction_005( Connection conn, String datatype )
throws Exception
{
goodStatement( conn, "drop function f_" + datatype );
}
/**
* <p>
* Test the arrayToClob() function.
* </p>
*/
public void testArrayToClob_006() throws Exception
{
Connection conn = getConnection();
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', true )" );
goodStatement( conn, "create table employee( fullName varchar( 100 ) )" );
goodStatement( conn, "create table docs( stringDoc varchar( 32672 ) )" );
goodStatement( conn, "insert into employee values ( 'Fred Flintstone' ), ( 'Barney Rubble' )" );
goodStatement( conn, "insert into docs values( arrayToClob( toJSON( 'select * from employee' ) ) )" );
assertResults
(
conn,
"select * from docs",
new String[][]
{
{
"[{\"FULLNAME\":\"Fred Flintstone\"},{\"FULLNAME\":\"Barney Rubble\"}]"
}
},
true
);
goodStatement( conn, "drop table docs" );
goodStatement( conn, "drop table employee" );
goodStatement( conn, "call syscs_util.syscs_register_tool( 'simpleJson', false )" );
}
///////////////////////////////////////////////////////////////////////////////////
//
// FUNCTIONS
//
///////////////////////////////////////////////////////////////////////////////////
/**
* <p>
* Pretty-print a JSONArray.
* </p>
*/
public static String prettyPrint( JSONArray array )
{
StringBuilder buffer = new StringBuilder();
prettyPrintArray( buffer, 0, array );
return buffer.toString();
}
private static void prettyPrintArray
(
StringBuilder buffer,
int indentLevel,
JSONArray array
)
{
buffer.append( "[" );
int cellCount = array.size();
if ( cellCount > 0 )
{
for ( int i = 0; i < cellCount; i++ )
{
if ( i > 0 ) { buffer.append( ", " ); }
indent( buffer, indentLevel + 1 );
prettyPrint( buffer, indentLevel + 1, array.get( i ) );
}
indent( buffer, indentLevel );
}
buffer.append( "]" );
}
private static void prettyPrintObject
(
StringBuilder buffer,
int indentLevel,
JSONObject obj
)
{
buffer.append( "{" );
int keyCount = obj.size();
if ( keyCount > 0 )
{
// alphabetize the keys
Object[] keys = obj.keySet().toArray();
Arrays.sort( keys );
for ( int i = 0; i < keyCount; i++ )
{
Object key = keys[ i ];
if ( i > 0 ) { buffer.append( ", " ); }
indent( buffer, indentLevel + 1 );
buffer.append( doubleQuote( (String) key ) );
buffer.append( " : " );
prettyPrint( buffer, indentLevel + 1, obj.get( key ) );
}
indent( buffer, indentLevel );
}
buffer.append( "}" );
}
private static void prettyPrint
(
StringBuilder buffer,
int indentLevel,
Object obj
)
{
if ( obj == null ) { buffer.append( "null" ); }
else if ( obj instanceof JSONArray )
{ prettyPrintArray( buffer, indentLevel, (JSONArray) obj ); }
else if ( obj instanceof JSONObject )
{ prettyPrintObject( buffer, indentLevel, (JSONObject) obj ); }
else if ( (obj instanceof Number) || (obj instanceof Boolean) )
{ buffer.append( obj.toString() ); }
else { buffer.append( doubleQuote( obj.toString() ) ); }
}
private static void indent( StringBuilder buffer, int indentLevel )
{
buffer.append( "\n" );
for ( int i = 0; i < indentLevel; i++ ) { buffer.append( TAB ); }
}
private static String doubleQuote( String raw )
{
return "\"" + raw + "\"";
}
}