
io.continual.http.app.htmlForms.CHttpFormPostWrapper Maven / Gradle / Ivy
/*
* 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.app.htmlForms;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import org.slf4j.LoggerFactory;
import io.continual.http.app.htmlForms.mime.CHttpMimePart;
import io.continual.http.app.htmlForms.mime.CHttpMimePartFactory;
import io.continual.http.app.htmlForms.mime.CHttpMimePartsReader;
import io.continual.http.service.framework.context.CHttpRequest;
import io.continual.util.collections.MultiMap;
import io.continual.util.data.TypeConvertor;
import io.continual.util.data.TypeConvertor.conversionError;
import io.continual.util.standards.HttpMethods;
/**
* A form post wrapper provides form related methods over a CHttpRequest.
*/
public class CHttpFormPostWrapper
{
public static class ParseException extends Exception
{
public ParseException ( Throwable t ) { super ( t ); }
private static final long serialVersionUID = 1L;
}
/**
* Construct a form post wrapper from a request.
* @param req
*/
public CHttpFormPostWrapper ( CHttpRequest req )
{
this ( req, null );
}
/**
* Construct a form post wrapper from a request, and use the given MIME reader
* part factory. The MIME reader is only invoked if the request's content type
* is multipart/form-data.
*
* @param req
* @param mimePartFactory If null, use the built-in part factory.
*/
public CHttpFormPostWrapper ( CHttpRequest req, CHttpMimePartFactory mimePartFactory )
{
fRequest = req;
final String ct = req.getContentType ();
fIsMultipartFormData = ct != null && ct.startsWith ( "multipart/form-data" );
fPartFactory = mimePartFactory == null ? new simpleStorage () : mimePartFactory;
fParsedValues = new HashMap<>();
fParseComplete = false;
}
/**
* this must be called to cleanup mime part resources (e.g. tmp files)
*/
public void close ()
{
for ( CHttpMimePart vi : fParsedValues.values () )
{
vi.discard ();
}
}
@Override
public String toString ()
{
final StringBuilder sb = new StringBuilder ();
sb.append ( fRequest.getMethod ().toUpperCase () ).append ( " {" );
if ( fIsMultipartFormData )
{
if ( fParseComplete )
{
for ( Entry e : fParsedValues.entrySet () )
{
sb.append ( e.getKey () ).append ( ":" );
final CHttpMimePart mp = e.getValue ();
if ( mp.getAsString () != null )
{
sb.append ( "'" ).append(mp.getAsString()).append ( "' " );
}
else
{
sb.append ( "(data) " );
}
}
}
else
{
sb.append ( "not parsed yet" );
}
}
else
{
for ( Entry e : fRequest.getParameterMap().entrySet () )
{
final StringBuilder sb2 = new StringBuilder ();
for ( String val : e.getValue () )
{
if ( sb2.length () > 0 ) sb2.append ( "," );
sb2.append ( val );
}
sb.append ( e.getKey() ).append ( ": [" ).append ( sb2.toString () ).append ( "], " );
}
}
sb.append ( " }" );
return sb.toString ();
}
/**
* Is the underlying request a POST? (Not a PUT, not anything else. Just POST.)
* @return true if the underlying request is a POST.
*/
public boolean isPost ()
{
return fRequest.getMethod ().toLowerCase ().equals ( HttpMethods.POST );
}
/**
* Does the form have a given parameter (aka field)
* @param name
* @return true if the named parameter/field exists in the form post
* @throws ParseException
*/
public boolean hasParameter ( String name ) throws ParseException
{
parseIfNeeded ();
return fIsMultipartFormData ?
fParsedValues.containsKey ( name ) :
fRequest.getParameterMap ().containsKey ( name );
}
/**
* Get the form post parameters in a map from name to string value.
* @return a map of post parameters
* @throws ParseException
*/
public Set getKeys () throws ParseException
{
final TreeSet set = new TreeSet<>();
parseIfNeeded ();
if ( fIsMultipartFormData )
{
set.addAll ( fParsedValues.keySet () );
}
else
{
set.addAll ( fRequest.getParameterMap().keySet () );
}
return set;
}
/**
* Get the form post parameters in a map from name to string value.
* @return a map of post parameters
* @throws ParseException
*/
public Map getValues () throws ParseException
{
final HashMap map = new HashMap<>();
parseIfNeeded ();
if ( fIsMultipartFormData )
{
for ( Map.Entry e : fParsedValues.entrySet () )
{
final String val = e.getValue ().getAsString ();
if ( val != null )
{
map.put ( e.getKey(), val );
}
}
}
else
{
for ( Map.Entry,?> e : fRequest.getParameterMap ().entrySet() )
{
final String key = e.getKey ().toString ();
final String[] vals = (String[]) e.getValue ();
String valToUse = "";
if ( vals.length > 0 )
{
valToUse = vals[0];
}
map.put ( key, valToUse );
}
}
return map;
}
/**
* Does the form contain the given field? This goes beyond hasParameter() to check
* on a multipart MIME post whether the value provided is a string.
*
* @param name
* @return true if the named field exists
* @throws ParseException
*/
public boolean isFormField ( String name ) throws ParseException
{
boolean result = false;
if ( hasParameter ( name ) )
{
if ( fIsMultipartFormData )
{
final CHttpMimePart val = fParsedValues.get ( name );
result = ( val != null && val.getAsString () != null );
}
else
{
result = true;
}
}
return result;
}
/**
* Get the value of a field as a string. This returns null for MIME parts like
* file uploads -- the value has to be available as a string rather than a stream.
*
* @param name
* @return a string for the named field, or null if it doesn't exist (or is a MIME part)
* @throws ParseException
*/
public String getValue ( String name ) throws ParseException
{
parseIfNeeded ();
String result;
if ( fIsMultipartFormData )
{
final CHttpMimePart val = fParsedValues.get ( name );
result = null;
if ( val != null && val.getAsString () != null )
{
result = val.getAsString ().trim ();
}
}
else
{
result = fRequest.getParameter ( name );
if ( result != null )
{
result = result.trim ();
}
}
return result;
}
/**
* A convenience version of getValue(String). Useful for passing enums. The argument
* is converted to a string.
* @param o
* @return the string value for the given field
* @throws ParseException
*/
public String getValue ( Object o ) throws ParseException
{
return getValue ( o.toString () );
}
/**
* Get the named value, or return defVal if it does not exist on the form.
* @param key
* @param defVal
* @return the value from the form, or the default value
* @throws ParseException
*/
public String getValue ( String key, String defVal ) throws ParseException
{
String result = getValue ( key );
if ( result == null )
{
result = defVal;
}
return result;
}
/**
* A convenience version for use with Enums. The field name argument is converted to a string.
* @param fieldName
* @param defVal
* @return
* @throws ParseException
*/
public String getValue ( Object fieldName, String defVal ) throws ParseException
{
return getValue ( fieldName.toString (), defVal );
}
/**
* Get the named value as a boolean, or return valIfMissing if no such field exists.
* @param name
* @param valIfMissing
* @return true/false
* @throws ParseException
*/
public boolean getValueBoolean ( String name, boolean valIfMissing ) throws ParseException
{
boolean result = valIfMissing;
final String val = getValue ( name );
if ( val != null )
{
result = TypeConvertor.convertToBooleanBroad ( val );
}
return result;
}
/**
* A convenience version for use with Enums. The field name argument is converted to a string.
* @param fieldName
* @param valIfMissing
* @return true/false
* @throws ParseException
*/
public boolean getValueBoolean ( Object fieldName, boolean valIfMissing ) throws ParseException
{
return getValueBoolean ( fieldName.toString() , valIfMissing );
}
/**
* Get the named value as an integer, or null if no such value exists
* @param name
* @return the integer value or null
* @throws ParseException
*/
public Integer getValueInt ( String name ) throws ParseException
{
return getValueInt ( name, null );
}
/**
* Get the named value as an integer, or return valIfMissing if no such field exists.
* @param name
* @param valIfMissing
* @return the integer value
* @throws ParseException
*/
public Integer getValueInt ( String name, Integer valIfMissing ) throws ParseException
{
Integer result = valIfMissing;
final String val = getValue ( name );
if ( val != null )
{
try
{
result = TypeConvertor.convertToInt ( val );
}
catch ( conversionError e )
{
result = valIfMissing;
}
}
return result;
}
/**
* A convenience version for use with Enums. The field name argument is converted to a string.
* @param fieldName
* @param valIfMissing
* @throws ParseException
*/
public Integer getValueInt ( Object fieldName, Integer valIfMissing ) throws ParseException
{
return getValueInt ( fieldName.toString() , valIfMissing );
}
/**
* Get the named value as an double.
* @param name
* @return the value, or null
* @throws ParseException
*/
public Double getValueDouble ( String name ) throws ParseException
{
return getValueDouble ( name, null );
}
/**
* Get the named value as an double, or return valIfMissing if no such field exists.
* @param name
* @param valIfMissing
* @return the value
* @throws ParseException
*/
public Double getValueDouble ( String name, Double valIfMissing ) throws ParseException
{
Double result = valIfMissing;
final String val = getValue ( name );
if ( val != null )
{
try
{
result = TypeConvertor.convertToDouble ( val );
}
catch ( conversionError e )
{
result = valIfMissing;
}
}
return result;
}
/**
* A convenience version for use with Enums. The field name argument is converted to a string.
* @param fieldName
* @param valIfMissing
* @throws ParseException
*/
public Double getValueDouble ( Object fieldName, Double valIfMissing ) throws ParseException
{
return getValueDouble ( fieldName.toString() , valIfMissing );
}
/**
* Change the value for a given field.
* @param fieldName
* @param newVal
* @throws ParseException
*/
public void changeValue ( String fieldName, String newVal ) throws ParseException
{
parseIfNeeded ();
if ( fIsMultipartFormData )
{
if ( fParsedValues.containsKey ( fieldName ) )
{
fParsedValues.get ( fieldName ).discard ();
}
final inMemoryFormDataPart part = new inMemoryFormDataPart ( "", "form-data; name=\"" + fieldName + "\"" );
final byte[] array = newVal.getBytes ();
part.write ( array, 0, array.length );
part.close ();
fParsedValues.put ( fieldName, part );
}
else
{
fRequest.changeParameter ( fieldName, newVal );
}
}
/**
* Get the MIME part for a given field name.
* @param name
* @return a MIME part
* @throws ParseException
*/
public CHttpMimePart getStream ( String name ) throws ParseException
{
parseIfNeeded ();
if ( fIsMultipartFormData )
{
final CHttpMimePart val = fParsedValues.get ( name );
if ( val != null && val.getAsString () == null )
{
return val;
}
}
return null;
}
private final CHttpRequest fRequest;
private final boolean fIsMultipartFormData;
private boolean fParseComplete;
private final HashMap fParsedValues;
private final CHttpMimePartFactory fPartFactory;
private void parseIfNeeded () throws ParseException
{
if ( fIsMultipartFormData && !fParseComplete )
{
try
{
final String ct = fRequest.getContentType ();
int boundaryStartIndex = ct.indexOf ( kBoundaryTag );
if ( boundaryStartIndex != -1 )
{
boundaryStartIndex = boundaryStartIndex + kBoundaryTag.length ();
final int semi = ct.indexOf ( ";", boundaryStartIndex );
int boundaryEndIndex = semi == -1 ? ct.length () : semi;
final String boundary = ct.substring ( boundaryStartIndex, boundaryEndIndex ).trim ();
final CHttpMimePartsReader mmr = new CHttpMimePartsReader ( boundary, fPartFactory );
final InputStream is = fRequest.getBodyStream ();
mmr.read ( is );
is.close ();
for ( CHttpMimePart mp : mmr.getParts () )
{
fParsedValues.put ( mp.getName(), mp );
}
}
}
catch ( IOException e )
{
log.warn ( "There was a problem reading a multipart/form-data POST: " + e.getMessage () );
throw new ParseException ( e );
}
fParseComplete = true;
}
}
private static final String kBoundaryTag = "boundary=";
static final org.slf4j.Logger log = LoggerFactory.getLogger ( CHttpFormPostWrapper.class );
public static abstract class basePart implements CHttpMimePart
{
public basePart ( String contentType, String contentDisp )
{
fType = contentType;
fDisp = contentDisp;
fDispMap = new HashMap<>();
parseDisposition ( contentDisp );
final int nameSpot = fDisp.indexOf ( "name=\"" );
String namePart = fDisp.substring ( nameSpot + "name=\"".length () );
final int closeQuote = namePart.indexOf ( "\"" );
namePart = namePart.substring ( 0, closeQuote );
fName = namePart;
}
@Override
public String getContentType ()
{
return fType;
}
@Override
public String getContentDisposition ()
{
return fDisp;
}
@Override
public String getContentDispositionValue ( String key )
{
return fDispMap.get ( key );
}
@Override
public String getName ()
{
return fName;
}
@Override
public void discard ()
{
}
private final String fType;
private final String fDisp;
private final String fName;
private final HashMap fDispMap;
// form-data; name="file"; filename="IMG_21022013_122919.png"
private void parseDisposition ( String contentDisp )
{
final String[] parts = contentDisp.split ( ";");
for ( String part : parts )
{
String key = part.trim ();
String val = "";
final int eq = key.indexOf ( '=' );
if ( eq > -1 )
{
val = key.substring ( eq+1 );
key = key.substring ( 0, eq );
// if val is in quotes, remove them
if ( val.startsWith ( "\"" ) && val.endsWith ( "\"" ) )
{
val = val.substring ( 1, val.length () - 1 );
}
}
fDispMap.put ( key, val );
}
}
}
public static class inMemoryFormDataPart extends basePart
{
public inMemoryFormDataPart ( String ct, String cd )
{
super ( ct, cd );
fValue = "";
}
@Override
public void write ( byte[] line, int offset, int length )
{
fValue = new String ( line, offset, length );
}
@Override
public void close ()
{
}
@Override
public InputStream openStream () throws IOException
{
throw new IOException ( "Opening stream on in-memory form data." );
}
@Override
public String getAsString ()
{
return fValue;
}
private String fValue;
}
private static class tmpFilePart extends basePart
{
public tmpFilePart ( String ct, String cd ) throws IOException
{
super ( ct, cd );
fFile = File.createTempFile ( "chttp.", ".part" );
fStream = new FileOutputStream ( fFile );
}
@Override
public void write ( byte[] line, int offset, int length ) throws IOException
{
if ( fStream != null )
{
fStream.write ( line, offset, length );
}
}
@Override
public void close () throws IOException
{
if ( fStream != null )
{
fStream.close ();
fStream = null;
}
}
@Override
public InputStream openStream () throws IOException
{
if ( fStream != null )
{
log.warn ( "Opening input stream on tmp file before it's fully written." );
}
return new FileInputStream ( fFile );
}
@Override
public String getAsString ()
{
return null;
}
@Override
public void discard ()
{
//noinspection ResultOfMethodCallIgnored
fFile.delete ();
fFile = null;
fStream = null;
}
private File fFile;
private FileOutputStream fStream;
}
static class simpleStorage implements CHttpMimePartFactory
{
@Override
public CHttpMimePart createPart ( MultiMap partHeaders ) throws IOException
{
final String contentDisp = partHeaders.getFirst ( "content-disposition" );
if ( contentDisp != null && contentDisp.contains ( "filename=\"" ) )
{
return new tmpFilePart ( partHeaders.getFirst ( "content-type" ), contentDisp );
}
else
{
return new inMemoryFormDataPart ( partHeaders.getFirst ( "content-type" ), contentDisp );
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy