All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.continual.http.util.JsonBodyReader Maven / Gradle / Ivy

The newest version!
/*
 *	Copyright 2019, Continual.io
 *
 *	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 io.continual.http.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.continual.http.service.framework.context.CHttpRequest;
import io.continual.http.service.framework.context.CHttpRequestContext;
import io.continual.util.time.Clock;

/**
 * Read a JSON content body from a request.
 */
public class JsonBodyReader
{
	public static long kDefaultTimeoutMs = 1000 * 10;	// 10 seconds
	public static long kMaxBytes = 1024 * 1024 * 32;	// 32MB

	/**
	 * Read the body of the request in the given context into a list of JSON objects.
	 * 
	 * @param context
	 * @return a list of 0 or more JSONObjects
	 * @throws IOException
	 * @throws JSONException
	 */
	public static List readBodyForObjects ( CHttpRequestContext context ) throws IOException, JSONException
	{
		return readBodyForObjects ( context, null );
	}

	/**
	 * read the request in the given context for a list of objects, optionally from the value
	 * named by 'path' in a top-level object.
	 * @param context
	 * @param path
	 * @return a list of 0 or more JSON objects
	 * @throws JSONException
	 */
	public static List readBodyForObjects ( CHttpRequestContext context, String path ) throws JSONException
	{
		try
		{
			byte[] bytes = readBytes ( context.request (), kDefaultTimeoutMs );
			return readBodyForObjects ( bytes, path );
		}
		catch ( IOException e )
		{
			return new LinkedList<>();
		}
	}

	/**
	 * read the bytes for objects. If the bytes contain a single JSON object and the path is
	 * not null, the objects are loaded from the value named by path rather than the top-level
	 * object.
	 *  
	 * @param bytes
	 * @param path
	 * @return a list of 0 or more JSON objects
	 * @throws IOException
	 * @throws JSONException
	 */
	public static List readBodyForObjects ( final byte[] bytes, String path ) throws IOException, JSONException
	{
		final LinkedList result = new LinkedList<>();

		// determine the first token in the stream to decide if we're reading a single
		// object or an array.
		boolean isSingleObject;
		{
			final ByteArrayInputStream s = new ByteArrayInputStream ( bytes );
			final JSONTokener t = new JSONTokener ( s );
			
			char c = t.next ();
			while ( Character.isWhitespace ( c ) ) c = t.next ();

			switch ( c )
			{
				case '{': isSingleObject = true; break;
				case '[': isSingleObject = false; break;
				default: throw new JSONException ( "Expected an object or an array of objects." );
			}
			s.close ();
		}

		if ( isSingleObject )
		{
			final String jsonStream = new String ( bytes, utf8 );
			final JSONObject o = new JSONObject ( jsonStream );

			if ( path != null )
			{
				final Object oo = o.opt ( path );
				if ( oo instanceof JSONObject )
				{
					result.add ( (JSONObject) oo );
				}
				else if ( oo instanceof JSONArray )
				{
					result.addAll ( readArrayForObjects ( (JSONArray) oo ) );
				}
				else
				{
					throw new JSONException ( "Couldn't read object at path [" + path + "]." );
				}
			}
			else
			{
				result.add ( o );
			}
		}
		else
		{
			final String jsonStream = new String ( bytes );
			final JSONArray a = new JSONArray ( jsonStream );
			result.addAll ( readArrayForObjects ( a ) );
		}

		return result;
	}

	/**
	 * Read the request in the given context for a single JSON object, waiting at most the
	 * default timeout in milliseconds.
	 * @param context
	 * @return a JSONObject
	 * @throws IOException
	 * @throws JSONException
	 */
	public static JSONObject readBody ( CHttpRequestContext context ) throws IOException, JSONException
	{
		return readBody ( context, kDefaultTimeoutMs );
	}

	/**
	 * Read the request in the given context for a single JSON object, waiting at most the
	 * given number of milliseconds.
	 * @param context
	 * @param timeoutMs
	 * @return a JSONObject
	 * @throws IOException
	 * @throws JSONException
	 */
	public static JSONObject readBody ( CHttpRequestContext context, long timeoutMs ) throws IOException, JSONException
	{
		return readBody ( context.request (), timeoutMs );
	}

	/**
	 * Read the given request for a single JSON object, waiting at most the default
	 * number of milliseconds.
	 * @param req
	 * @return a JSONObject
	 * @throws IOException
	 * @throws JSONException
	 */
	public static JSONObject readBody ( CHttpRequest req ) throws IOException, JSONException
	{
		return readBody ( req, kDefaultTimeoutMs );
	}

	/**
	 * Read the given request for a single JSON object, waiting at most the
	 * given number of milliseconds.
	 * @param req
	 * @param timeoutMs
	 * @return a JSONObject
	 * @throws IOException
	 * @throws JSONException
	 */
	public static JSONObject readBody ( CHttpRequest req, long timeoutMs ) throws IOException, JSONException
	{
		byte[] bytes = readBytes ( req, timeoutMs );
		final String jsonStream = new String ( bytes );
		return new JSONObject ( jsonStream );
	}

	/**
	 * Read bytes from the given request, waiting at most timeout milliseconds
	 * @param req
	 * @param timeoutMs
	 * @return an array of bytes
	 * @throws IOException
	 */
	public static byte[] readBytes ( CHttpRequest req, long timeoutMs ) throws IOException
	{
		final int clen = req.getContentLength ();
		log.trace ( "Incoming content-length is " + clen );
		if ( clen == -1 )
		{
			throw new IOException ( "The content length header must be provided." );
		}
		else if ( clen > kMaxBytes )
		{
			throw new IOException ( "Input too large." );
		}
	
		final ByteArrayOutputStream baos = new ByteArrayOutputStream ();
	
		final InputStream is = req.getBodyStream ();
		if ( is == null )
		{
			throw new IOException ( "No input stream on request." );
		}
	
		long totalBytesRead = 0;
		try
		{
			byte[] b = new byte [ 4096 ];
			int len ;
			long lastReadMs = Clock.now ();
			boolean complete = false;
	
			do
			{
				len = is.read ( b );
				if ( len > 0 )
				{
					lastReadMs = Clock.now ();
					totalBytesRead += len;
					if ( totalBytesRead > kMaxBytes )
					{
						throw new IOException ( "Input too large." );
					}
					baos.write ( b, 0, len );
					complete = ( totalBytesRead >= clen );
				}
				else if ( len == 0 )
				{
					if ( lastReadMs + timeoutMs < Clock.now () )
					{
						log.info ( "Read timed out. Total " + totalBytesRead + " bytes, content-length was " + clen );
						throw new IOException ( "Timed out waiting for input." );
					}
				}
			}
			while ( len != -1 && !complete );
		}
		finally
		{
			is.close ();
		}
	
		if ( totalBytesRead < clen )
		{
			throw new IOException ( "Expected " + clen + " bytes, received " + totalBytesRead );
		}
	
		// truncate input to content-length
		final byte[] bytes = new byte [ clen ];
		System.arraycopy ( baos.toByteArray(), 0, bytes, 0, clen );
		return bytes;
	}


	private static List readArrayForObjects ( JSONArray a ) throws JSONException
	{
		final LinkedList result = new LinkedList<>();
		final int len = a.length ();
		for ( int i=0; i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy