Upload di un file in multipart/form-data in Android

Nello precedente articolo abbiamo visto come ottenere il nome del file che dobbiamo scaricare dal campo Content-disposition degli header di richiesta http.  Sempre rimanendo in tema di utilizzo del protocollo http in Android, vedremo adesso un altro caso particolare, ovvero l’upload di un file codificato in multipart/form-data: pensiamo ad esempio alla tipica form di un portale web, adibita al caricamento di un’immagine, di un video o quant’altro. Anche in questo caso, un webservice ad hoc ci avrebbe fatto comodo, ma, grazie una classe helper studiata allo scopo, il problema risulta di facile soluzione.

Creazione della classe helper

Come già anticipato, astrarremo la logica di upload in una classe di utilità, che potremo riutilizzare anche altrove. Dichiariamo quindi la classe MultipartHelper, importiamo i package che di cui faremo uso successivamente e aggiungiamo subito i campi privati che andremo a utilizzare nei vari metodi:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

public class MultipartHelper {

    private static final String NEWLINE = "\r\n";

    private final String boundaryValue;
    
    private HttpURLConnection cn;

    private String charset;

    private PrintWriter printWriter;

    private OutputStream outStream;    


}

Vediamo nel dettaglio il significato dei campi privati di classe:

    • La costante NEWLINE è di comodo, per inserire rapidamente dove necessario i caratteri di carriage return e newline
    • La stringa boundaryValue valorizzerà il parametro “boundary” del campo Content-type, necessario per i contenuti multipart/
    • La variabile cn di tipo HttpURLConnection verrà utilizzata per aprire un canale di comunicazione http con il server
    • La stringa charset ci servirà per indicare il set di caratteri nell’intestazione (che di default è US-ASCII; maggiori informazioni si trovano nel documenti di specifiche rfc2046
    • infine, printWriter e outStream ci serviranno per gestire gli stream di output delle nostre richieste

Il costruttore della classe MultipartHelper

Il costruttore della classe MultipartHelper si occuperà di inizializzare una nuova richiesta HTTP POST, impostando il content-type in multipart/form-data e assegnando il parametro boundary in base al timestamp corrente (in modo che sia univoco); il set di caratteri e e l’url della richiesta verranno passati come argomenti del costruttore:

public MultipartHelper(String requestURL, String charset) throws IOException {    

    //crea un delimitatore univoco basato sul timestamp corrente
    boundaryValue = "===" + System.currentTimeMillis() + "==="; 

    this.charset = charset; 

    URL url = new URL(requestURL);
    
    cn = (HttpURLConnection) url.openConnection();
    cn.setUseCaches(false);
    //indichiamo il metodo POST
    cn.setDoOutput(true); 
    cn.setDoInput(true);
    cn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundaryValue);    
    
    outStream = cn.getOutputStream();
    printWriter = new PrintWriter(new OutputStreamWriter(outStream, charset), true);
}

Il metodo setHeaderField

Qualora lato server fossero stati previsti particolari header, li gestiremo con il metodo setHeaderField che prende in input il nome dell’header e il valore da assegnargli e accodandoli allo stream di output:

public void setHeaderField(String name, String value) {
    printWriter.append(name + ": " + value).append(NEWLINE);
    printWriter.flush();
}

Il metodo setFilePart

Veniamo al nodo cruciale: inserire il nostro file nella richiesta POST: utilizzeremo il metodo setFilePart, che prende in input sia il nome del file, che il file stesso. Dopo aver impostato i campi content-disposition, content-type e content-transfer-encoding, leggeremo il file a buffer di 4096 byte, che scriveremo contemporaneamente nello stream di output:

public void setFilePart(String fieldName, File uploadFile) throws IOException {
    String fileName = uploadFile.getName();
    printWriter.append("--" + boundary).append(NEWLINE);
    printWriter.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"").append(NEWLINE);
    printWriter.append("Content-Type: " + URLConnection.guessContentTypeFromName(fileName)).append(NEWLINE);
    printWriter.append("Content-Transfer-Encoding: binary").append(NEWLINE);
    printWriter.append(NEWLINE);
    printWriter.flush();

    FileInputStream inputStream = new FileInputStream(uploadFile);
    byte[] buffer = new byte[4096];
    int bytesRead = -1;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        outStream.write(buffer, 0, bytesRead);
    }
    outStream.flush();
    inputStream.close();

    printWriter.append(NEWLINE);
    printWriter.flush();
}

Il metodo setFormField

Lato server potrebbe essere richiesto qualche campo (pensiamo ai tipici campi di una form), per questo creeremo il metodo setFormField anche in questo caso nome e valore del campo:

public void setFormField(String name, String value) {
    printWriter.append("--" + boundary).append(NEWLINE);
    printWriter.append("Content-Disposition: form-data; name=\"" + name + "\"").append(LINE_FEED);
    printWriter.append("Content-Type: text/plain; charset=" + charset).append(NEWLINE);
    printWriter.append(NEWLINE);
    printWriter.append(value).append(NEWLINE);
    printWriter.flush();
}

Il metodo getResponse

Infine, tramite il metodo getResponse, gestiremo la risposta del server: nel caso in cui ci venga restituito codice 200, indicante HTTP Status ok, leggeremo lo stream di risposta e lo restituiremo al chiamante sotto forma di stringa, altrimenti scateneremo un eccezione indicante il codice di errore http, chiudendo successivamente la connessione:

public String getResponse() throws IOException {
    StringBuffer response = new StringBuffer();

    printWriter.append(NEWLINE).flush();
    printWriter.append("--" + boundary + "--").append(NEWLINE);
    printWriter.close();

    // checks server's status code first
    int status = cn.getResponseCode();
    if (status == HttpURLConnection.HTTP_OK) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(cn.getInputStream()));
        String line = null;
        while ((line = reader.readLine()) != null) {
            response.append(line);
        }
        reader.close();
        cn.disconnect();
    } else {
        throw new IOException("Server status: " + status);
    }

    return response.toString();
}

Utilizzo della classe MultipartHelper ed esempio di upload di un file in multipart/form-data in Android

Vediamo adesso come utilizzare la classe appena creata: noti il file, l’url http, e il charset, istanzieremo un oggetto di tipo MultipartHelper, gli passeremo l’url di download e il set di caratetteri e, tramite i metodi setHeaderField, setFormField, setFilePart, comporremo la richiesta POST, ottenendo la risposta dal server con il metodo getResponse

String charset = "UTF-8";
String requestURL = "www.example.com/upload?myId=12312";

string filePath = "/MyStorage/MySubdir/myfilename.jpg";
string fileName = filepath.substring(filepath.lastIndexOf("/")+1);

MultipartHelper multipart = new MultipartHelper(requestURL, charset);
multipart.setFormField("myFormFieldName1", "foo");
multipart.setFormField("myFormFieldName2", "bar");
multipart.setFormField("file", fileName);
multipart.setFilePart("file", new File(filePath));
String response = multipart.getResponse();

La classe è reperibile presso questo repository github.