com.radicaldynamic.groupinform.tasks.DownloadFormsTask.java Source code

Java tutorial

Introduction

Here is the source code for com.radicaldynamic.groupinform.tasks.DownloadFormsTask.java

Source

/*
 * Copyright (C) 2009 University of Washington
 * 
 * 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 com.radicaldynamic.groupinform.tasks;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.protocol.HttpContext;
import org.ektorp.AttachmentInputStream;
import org.javarosa.xform.parse.XFormParser;
import org.kxml2.kdom.Element;
import org.odk.collect.android.listeners.FormDownloaderListener;
import org.odk.collect.android.logic.FormDetails;
import org.odk.collect.android.utilities.DocumentFetchResult;
import org.odk.collect.android.utilities.FileUtils;
import org.odk.collect.android.utilities.WebUtils;

import android.os.AsyncTask;
import android.util.Log;
import android.webkit.MimeTypeMap;

import com.radicaldynamic.groupinform.R;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.documents.FormDefinition;
import com.radicaldynamic.groupinform.repositories.FormDefinitionRepo;
import com.radicaldynamic.groupinform.utilities.FileUtilsExtended;

/**
 * Background task for downloading a given list of forms. We assume right now that the forms are
 * coming from the same server that presented the form list, but theoretically that won't always be
 * true.
 * 
 * @author msundt
 * @author carlhartung
 */
public class DownloadFormsTask extends AsyncTask<ArrayList<FormDetails>, String, HashMap<String, String>> {

    private static final String t = "DownloadFormsTask";

    private static final String MD5_COLON_PREFIX = "md5:";

    private FormDownloaderListener mStateListener;

    private static final String NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST = "http://openrosa.org/xforms/xformsManifest";

    private String mAuth = "";

    public void setAuth(String auth) {
        this.mAuth = auth;
    }

    private boolean isXformsManifestNamespacedElement(Element e) {
        return e.getNamespace().equalsIgnoreCase(NAMESPACE_OPENROSA_ORG_XFORMS_XFORMS_MANIFEST);
    }

    @Override
    protected HashMap<String, String> doInBackground(ArrayList<FormDetails>... values) {
        ArrayList<FormDetails> toDownload = values[0];

        int total = toDownload.size();
        int count = 1;

        HashMap<String, String> result = new HashMap<String, String>();

        for (int i = 0; i < total; i++) {
            FormDetails fd = toDownload.get(i);
            publishProgress(fd.formName, Integer.valueOf(count).toString(), Integer.valueOf(total).toString());

            String message = "";

            try {
                // get the xml file
                // if we've downloaded a duplicate, this gives us the file
                File dl = downloadXform(fd.formName, fd.downloadUrl);

                // BEGIN custom
                //                String[] projection = {
                //                        FormsColumns._ID, FormsColumns.FORM_FILE_PATH
                //                };
                //                String[] selectionArgs = {
                //                    dl.getAbsolutePath()
                //                };
                //                String selection = FormsColumns.FORM_FILE_PATH + "=?";
                //                Cursor alreadyExists =
                //                    Collect.getInstance()
                //                            .getContentResolver()
                //                            .query(FormsColumns.CONTENT_URI, projection, selection, selectionArgs,
                //                                null);
                //
                //                Uri uri = null;
                //                if (alreadyExists.getCount() <= 0) {
                //                    // doesn't exist, so insert it
                //                    ContentValues v = new ContentValues();
                //                    v.put(FormsColumns.FORM_FILE_PATH, dl.getAbsolutePath());
                //
                //                    HashMap<String, String> formInfo = FileUtils.parseXML(dl);
                //                    v.put(FormsColumns.DISPLAY_NAME, formInfo.get(FileUtils.TITLE));
                //                    v.put(FormsColumns.MODEL_VERSION, formInfo.get(FileUtils.MODEL));
                //                    v.put(FormsColumns.UI_VERSION, formInfo.get(FileUtils.UI));
                //                    v.put(FormsColumns.JR_FORM_ID, formInfo.get(FileUtils.FORMID));
                //                    v.put(FormsColumns.SUBMISSION_URI, formInfo.get(FileUtils.SUBMISSIONURI));
                //                    uri =
                //                        Collect.getInstance().getContentResolver()
                //                                .insert(FormsColumns.CONTENT_URI, v);
                //                } else {
                //                    alreadyExists.moveToFirst();
                //                    uri =
                //                        Uri.withAppendedPath(FormsColumns.CONTENT_URI,
                //                            alreadyExists.getString(alreadyExists.getColumnIndex(FormsColumns._ID)));
                //                }
                // END custom

                if (fd.manifestUrl != null) {
                    // BEGIN custom
                    //                    Cursor c =
                    //                        Collect.getInstance().getContentResolver()
                    //                                .query(uri, null, null, null, null);
                    //                    if (c.getCount() > 0) {
                    //                        // should be exactly 1
                    //                        c.moveToFirst();
                    //
                    //                        String error =
                    //                            downloadManifestAndMediaFiles(
                    //                                c.getString(c.getColumnIndex(FormsColumns.FORM_MEDIA_PATH)), fd,
                    //                                count, total);
                    //                        if (error != null) {
                    //                            message += error;
                    //                        }
                    //                    }
                    String error = downloadManifestAndMediaFiles(dl.getParent(), fd, count, total);

                    if (error != null) {
                        message += error;
                    }
                    // END custom
                } else {
                    // TODO: manifest was null?
                    Log.e(t, "Manifest was null.  PANIC");
                }

                // BEGIN custom
                HashMap<String, String> formInfo = FileUtils.parseXML(dl);

                // Create form document and add attachments; commit to database
                FormDefinition fDoc = new FormDefinition();
                fDoc.setName(formInfo.get(FileUtils.TITLE));
                fDoc.setModelVersion(formInfo.get(FileUtils.MODEL));
                fDoc.setUiVersion(formInfo.get(FileUtils.UI));
                fDoc.setJavaRosaId(formInfo.get(FileUtils.FORMID));
                fDoc.setSubmissionUri(formInfo.get(FileUtils.SUBMISSIONURI));
                fDoc.setStatus(FormDefinition.Status.active);
                fDoc.setXmlHash(FileUtils.getMd5Hash(dl));
                Collect.getInstance().getDbService().getDb().create(fDoc);

                String revision = fDoc.getRevision();

                for (File f : dl.getParentFile().listFiles()) {
                    String fileName = f.getName();
                    String attachmentName = fileName;

                    if (Collect.Log.VERBOSE)
                        Log.v(Collect.LOGTAG, t + ": attaching " + fileName + " to " + fDoc.getId());

                    if (fileName.equals(dl.getName()))
                        attachmentName = "xml";

                    String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
                    String contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);

                    FileInputStream fis = new FileInputStream(f);
                    AttachmentInputStream ais = new AttachmentInputStream(attachmentName, fis, contentType,
                            f.length());
                    revision = Collect.getInstance().getDbService().getDb().createAttachment(fDoc.getId(), revision,
                            ais);
                    fis.close();
                }

                // Remove temporary download directory
                FileUtilsExtended.deleteFolder(dl.getParent());
                // END custom
            } catch (SocketTimeoutException se) {
                se.printStackTrace();
                message += se.getMessage();
                // BEGIN custom
            } catch (DuplicateXFormFile e) {
                message = " SKIPPED (duplicate)";
                // END custom
            } catch (Exception e) {
                e.printStackTrace();
                if (e.getCause() != null) {
                    message += e.getCause().getMessage();
                } else {
                    message += e.getMessage();
                }
            }
            count++;
            if (message.equalsIgnoreCase("")) {
                message = Collect.getInstance().getString(R.string.success);
            }
            result.put(fd.formName, message);
        }

        return result;
    }

    /**
     * Takes the formName and the URL and attempts to download the specified file. Returns a file
     * object representing the downloaded file.
     * 
     * @param formName
     * @param url
     * @return
     * @throws Exception
     */
    private File downloadXform(String formName, String url) throws Exception {
        File f = null;

        // clean up friendly form name...
        String rootName = formName.replaceAll("[^\\p{L}\\p{Digit}]", " ");
        rootName = rootName.replaceAll("\\p{javaWhitespace}+", " ");
        rootName = rootName.trim();

        // proposed name of xml file...
        // BEGIN custom
        //        String path = Collect.FORMS_PATH + "/" + rootName + ".xml";
        //        int i = 2;
        //        f = new File(path);
        //        while (f.exists()) {
        //            path = Collect.FORMS_PATH + "/" + rootName + "_" + i + ".xml";
        //            f = new File(path);
        //            i++;
        //        }
        String downloadFolder = FileUtilsExtended.ODK_IMPORT_PATH + File.separator + UUID.randomUUID();
        FileUtils.createFolder(downloadFolder);
        f = new File(downloadFolder + File.separator + rootName + ".xml");
        // END custom

        downloadFile(f, url);

        // we've downloaded the file, and we may have renamed it
        // make sure it's not the same as a file we already have
        // BEGIN custom
        //        String[] projection = {
        //            FormsColumns.FORM_FILE_PATH
        //        };
        //        String[] selectionArgs = {
        //            FileUtils.getMd5Hash(f)
        //        };
        //        String selection = FormsColumns.MD5_HASH + "=?";
        //
        //        Cursor c =
        //            Collect.getInstance().getContentResolver()
        //                    .query(FormsColumns.CONTENT_URI, projection, selection, selectionArgs, null);
        //        if (c.getCount() > 0) {
        //            // Should be at most, 1
        //            c.moveToFirst();
        //
        //            // delete the file we just downloaded, because it's a duplicate
        //            f.delete();
        //
        //            // set the file returned to the file we already had
        //            f = new File(c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH)));
        //        }
        //        c.close();

        // Don't duplicate existing (identical) XForms
        String md5Hash = FileUtils.getMd5Hash(f);
        FormDefinitionRepo fdr = new FormDefinitionRepo(Collect.getInstance().getDbService().getDb());
        List<FormDefinition> fdl = fdr.findByXmlHash(md5Hash);

        if (!fdl.isEmpty()) {
            FileUtilsExtended.deleteFolder(downloadFolder);
            throw new DuplicateXFormFile();
        }
        // END custom

        return f;
    }

    /**
     * Common routine to download a document from the downloadUrl and save the contents in the file
     * 'f'. Shared by media file download and form file download.
     * 
     * @param f
     * @param downloadUrl
     * @throws Exception
     */
    private void downloadFile(File f, String downloadUrl) throws Exception {
        URI uri = null;
        try {
            // assume the downloadUrl is escaped properly
            URL url = new URL(downloadUrl);
            uri = url.toURI();
        } catch (MalformedURLException e) {
            e.printStackTrace();
            throw e;
        } catch (URISyntaxException e) {
            e.printStackTrace();
            throw e;
        }

        // get shared HttpContext so that authentication and cookies are retained.
        HttpContext localContext = Collect.getInstance().getHttpContext();

        HttpClient httpclient = WebUtils.createHttpClient(WebUtils.CONNECTION_TIMEOUT);

        // set up request...
        HttpGet req = WebUtils.createOpenRosaHttpGet(uri, mAuth);

        HttpResponse response = null;
        try {
            response = httpclient.execute(req, localContext);
            int statusCode = response.getStatusLine().getStatusCode();

            if (statusCode != 200) {
                String errMsg = Collect.getInstance().getString(R.string.file_fetch_failed, downloadUrl,
                        response.getStatusLine().getReasonPhrase(), statusCode);
                Log.e(t, errMsg);
                throw new Exception(errMsg);
            }

            // write connection to file
            InputStream is = null;
            OutputStream os = null;
            try {
                is = response.getEntity().getContent();
                os = new FileOutputStream(f);
                byte buf[] = new byte[1024];
                int len;
                while ((len = is.read(buf)) > 0) {
                    os.write(buf, 0, len);
                }
                os.flush();
            } finally {
                if (os != null) {
                    try {
                        os.close();
                    } catch (Exception e) {
                    }
                }
                if (is != null) {
                    try {
                        is.close();
                    } catch (Exception e) {
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    private static class MediaFile {
        final String filename;
        final String hash;
        final String downloadUrl;

        MediaFile(String filename, String hash, String downloadUrl) {
            this.filename = filename;
            this.hash = hash;
            this.downloadUrl = downloadUrl;
        }
    }

    private String downloadManifestAndMediaFiles(String mediaPath, FormDetails fd, int count, int total) {
        if (fd.manifestUrl == null)
            return null;

        publishProgress(Collect.getInstance().getString(R.string.fetching_manifest, fd.formName),
                Integer.valueOf(count).toString(), Integer.valueOf(total).toString());

        List<MediaFile> files = new ArrayList<MediaFile>();
        // get shared HttpContext so that authentication and cookies are retained.
        HttpContext localContext = Collect.getInstance().getHttpContext();

        HttpClient httpclient = WebUtils.createHttpClient(WebUtils.CONNECTION_TIMEOUT);

        DocumentFetchResult result = WebUtils.getXmlDocument(fd.manifestUrl, localContext, httpclient, mAuth);

        if (result.errorMessage != null) {
            return result.errorMessage;
        }

        String errMessage = Collect.getInstance().getString(R.string.access_error, fd.manifestUrl);

        if (!result.isOpenRosaResponse) {
            errMessage += Collect.getInstance().getString(R.string.manifest_server_error);
            Log.e(t, errMessage);
            return errMessage;
        }

        // Attempt OpenRosa 1.0 parsing
        Element manifestElement = result.doc.getRootElement();
        if (!manifestElement.getName().equals("manifest")) {
            errMessage += Collect.getInstance().getString(R.string.root_element_error, manifestElement.getName());
            Log.e(t, errMessage);
            return errMessage;
        }
        String namespace = manifestElement.getNamespace();
        if (!isXformsManifestNamespacedElement(manifestElement)) {
            errMessage += Collect.getInstance().getString(R.string.root_namespace_error, namespace);
            Log.e(t, errMessage);
            return errMessage;
        }
        int nElements = manifestElement.getChildCount();
        for (int i = 0; i < nElements; ++i) {
            if (manifestElement.getType(i) != Element.ELEMENT) {
                // e.g., whitespace (text)
                continue;
            }
            Element mediaFileElement = (Element) manifestElement.getElement(i);
            if (!isXformsManifestNamespacedElement(mediaFileElement)) {
                // someone else's extension?
                continue;
            }
            String name = mediaFileElement.getName();
            if (name.equalsIgnoreCase("mediaFile")) {
                String filename = null;
                String hash = null;
                String downloadUrl = null;
                // don't process descriptionUrl
                int childCount = mediaFileElement.getChildCount();
                for (int j = 0; j < childCount; ++j) {
                    if (mediaFileElement.getType(j) != Element.ELEMENT) {
                        // e.g., whitespace (text)
                        continue;
                    }
                    Element child = mediaFileElement.getElement(j);
                    if (!isXformsManifestNamespacedElement(child)) {
                        // someone else's extension?
                        continue;
                    }
                    String tag = child.getName();
                    if (tag.equals("filename")) {
                        filename = XFormParser.getXMLText(child, true);
                        if (filename != null && filename.length() == 0) {
                            filename = null;
                        }
                    } else if (tag.equals("hash")) {
                        hash = XFormParser.getXMLText(child, true);
                        if (hash != null && hash.length() == 0) {
                            hash = null;
                        }
                    } else if (tag.equals("downloadUrl")) {
                        downloadUrl = XFormParser.getXMLText(child, true);
                        if (downloadUrl != null && downloadUrl.length() == 0) {
                            downloadUrl = null;
                        }
                    }
                }
                if (filename == null || downloadUrl == null || hash == null) {
                    errMessage += Collect.getInstance().getString(R.string.manifest_tag_error, Integer.toString(i));
                    Log.e(t, errMessage);
                    return errMessage;
                }
                files.add(new MediaFile(filename, hash, downloadUrl));
            }
        }

        // OK we now have the full set of files to download...
        Log.i(t, "Downloading " + files.size() + " media files.");
        int mediaCount = 0;
        if (files.size() > 0) {
            FileUtils.createFolder(mediaPath);
            File mediaDir = new File(mediaPath);
            for (MediaFile toDownload : files) {
                if (isCancelled()) {
                    return "cancelled";
                }
                ++mediaCount;
                publishProgress(
                        Collect.getInstance().getString(R.string.form_download_progress, fd.formName, mediaCount,
                                files.size()),
                        Integer.valueOf(count).toString(), Integer.valueOf(total).toString());
                try {
                    File mediaFile = new File(mediaDir, toDownload.filename);

                    String currentFileHash = FileUtils.getMd5Hash(mediaFile);
                    String downloadFileHash = toDownload.hash.substring(MD5_COLON_PREFIX.length());

                    if (!mediaFile.exists()) {
                        downloadFile(mediaFile, toDownload.downloadUrl);
                    } else {
                        if (!currentFileHash.contentEquals(downloadFileHash)) {
                            // if the hashes match, it's the same file
                            // otherwise delete our current one and replace it with the new one
                            mediaFile.delete();
                            downloadFile(mediaFile, toDownload.downloadUrl);
                        } else {
                            // exists, and the hash is the same
                            // no need to download it again
                        }
                    }
                } catch (Exception e) {
                    return e.getLocalizedMessage();
                }
            }
        }
        return null;
    }

    @Override
    protected void onPostExecute(HashMap<String, String> value) {
        synchronized (this) {
            if (mStateListener != null) {
                mStateListener.formsDownloadingComplete(value);
            }
        }
    }

    @Override
    protected void onProgressUpdate(String... values) {
        synchronized (this) {
            if (mStateListener != null) {
                // update progress and total
                mStateListener.progressUpdate(values[0], new Integer(values[1]).intValue(),
                        new Integer(values[2]).intValue());
            }
        }

    }

    public void setDownloaderListener(FormDownloaderListener sl) {
        synchronized (this) {
            mStateListener = sl;
        }
    }

    @SuppressWarnings("serial")
    public class DuplicateXFormFile extends Exception {
        DuplicateXFormFile() {
            super();
        }
    }
}