org.opendatakit.aggregate.odktables.InstanceFileManager.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.aggregate.odktables.InstanceFileManager.java

Source

/*
 * Copyright (C) 2016 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 org.opendatakit.aggregate.odktables;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.ws.rs.core.MultivaluedMap;

import org.apache.http.HeaderElement;
import org.apache.http.message.BasicHeaderValueParser;
import org.apache.http.message.HeaderValueParser;
import org.apache.wink.common.model.multipart.InMultiPart;
import org.apache.wink.common.model.multipart.InPart;
import org.opendatakit.aggregate.odktables.api.InstanceFileService;
import org.opendatakit.aggregate.odktables.exception.InstanceFileModificationException;
import org.opendatakit.aggregate.odktables.exception.ODKTablesException;
import org.opendatakit.aggregate.odktables.exception.PermissionDeniedException;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceFiles;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceManifestETags;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceManifestETags.DbTableInstanceManifestETagEntity;
import org.opendatakit.aggregate.odktables.rest.entity.TableRole.TablePermission;
import org.opendatakit.aggregate.odktables.security.TablesUserPermissions;
import org.opendatakit.common.datamodel.BinaryContentManipulator.BlobSubmissionOutcome;
import org.opendatakit.common.ermodel.BlobEntitySet;
import org.opendatakit.common.persistence.PersistenceUtils;
import org.opendatakit.common.persistence.exception.ODKDatastoreException;
import org.opendatakit.common.persistence.exception.ODKEntityNotFoundException;
import org.opendatakit.common.persistence.exception.ODKTaskLockException;
import org.opendatakit.common.web.CallingContext;

/**
 * Implementation of file management APIs for row-level attachments.
 * 
 * 
 * @author mitchellsundt@gmail.com
 *
 */
public class InstanceFileManager {

    public interface FetchBlobHandler {
        byte[] getBlob() throws ODKDatastoreException;
    }

    public interface FileContentHandler {
        void processFileContent(FileContentInfo content, FetchBlobHandler fetcher);
    };

    private static final String ERROR_FILE_VERSION_DIFFERS = "File on server does not match file being uploaded. Aborting upload. ";

    private String appId;

    private CallingContext cc;

    public InstanceFileManager(String appId, CallingContext cc) {
        this.appId = appId;
        this.cc = cc;
    }

    /**
     * Retrieve the content info for a given file. Access to the blob entity set
     * needs to be guarded by a task lock.
     * 
     * @param tableId
     * @param rowId
     * @param partialPath
     * @param userPermissions
     * @return the file content if found; otherwise returns null
     * @throws ODKDatastoreException
     * @throws ODKTaskLockException
     * @throws PermissionDeniedException
     */
    public FileContentInfo getFile(String tableId, String rowId, String partialPath,
            TablesUserPermissions userPermissions)
            throws ODKDatastoreException, ODKTaskLockException, PermissionDeniedException {

        try {
            if (tableId == null) {
                throw new IllegalArgumentException("tableId cannot be null!");
            }

            if (rowId == null) {
                throw new IllegalArgumentException("rowId cannot be null!");
            }

            if (partialPath == null) {
                throw new IllegalArgumentException("partialPath cannot be null!");
            }

            userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW);

            OdkTablesLockTemplate propsLock = new OdkTablesLockTemplate(tableId, rowId,
                    ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, OdkTablesLockTemplate.DelayStrategy.LONG,
                    cc);
            try {
                propsLock.acquire();

                DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
                BlobEntitySet instance = blobStore.getBlobEntitySet(rowId, cc);

                int count = instance.getAttachmentCount(cc);
                for (int i = 1; i <= count; ++i) {
                    String path = instance.getUnrootedFilename(i, cc);
                    if (path != null && path.equals(partialPath)) {
                        byte[] fileBlob = instance.getBlob(i, cc);
                        String contentType = instance.getContentType(i, cc);
                        String contentHash = instance.getContentHash(i, cc);
                        Long contentLength = instance.getContentLength(i, cc);

                        // And now prepare everything to be returned to the caller.
                        if (fileBlob != null && contentType != null && contentLength != null
                                && contentLength != 0L) {

                            FileContentInfo fo = new FileContentInfo(path, contentType, contentLength, contentHash,
                                    fileBlob);
                            return fo;
                        } else {
                            return null;
                        }
                    }
                }
                return null;

            } finally {
                propsLock.release();
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw e;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw e;
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKTaskLockException e) {
            e.printStackTrace();
            throw e;
        } catch (PermissionDeniedException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Save a given file content under this tableId and rowId. Manipulations of
     * the blob entity set (which is being updated) needs to be guarded by a task
     * lock.
     * 
     * @param tableId
     * @param rowId
     * @param fi
     * @param userPermissions
     * @return
     * @throws ODKDatastoreException
     * @throws ODKTaskLockException
     * @throws PermissionDeniedException
     */
    public InstanceFileChangeDetail putFile(String tableId, String rowId, FileContentInfo fi,
            TablesUserPermissions userPermissions)
            throws ODKDatastoreException, ODKTaskLockException, PermissionDeniedException {

        try {
            if (tableId == null) {
                throw new IllegalArgumentException("tableId cannot be null!");
            }

            if (rowId == null) {
                throw new IllegalArgumentException("rowId cannot be null!");
            }

            if (fi.partialPath == null) {
                throw new IllegalArgumentException("partialPath cannot be null!");
            }

            userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_ROW);

            OdkTablesLockTemplate propsLock = new OdkTablesLockTemplate(tableId, rowId,
                    ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, OdkTablesLockTemplate.DelayStrategy.LONG,
                    cc);
            try {
                propsLock.acquire();

                // we are adding a file -- delete any cached ETag value for this row's
                // attachments manifest
                try {
                    DbTableInstanceManifestETagEntity entity = DbTableInstanceManifestETags.getRowIdEntry(tableId,
                            rowId, cc);
                    entity.delete(cc);
                } catch (ODKEntityNotFoundException e) {
                    // ignore...
                }

                DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
                BlobEntitySet instance = blobStore.newBlobEntitySet(rowId, cc);
                int count = instance.getAttachmentCount(cc);
                for (int i = 1; i <= count; ++i) {
                    String path = instance.getUnrootedFilename(i, cc);
                    if (path != null && path.equals(fi.partialPath)) {
                        // we already have this in our store -- check that it is identical.
                        // if not, we have a problem!!!
                        if (fi.contentHash.equals(instance.getContentHash(i, cc))) {
                            return InstanceFileChangeDetail.FILE_PRESENT;
                        } else {
                            return InstanceFileChangeDetail.FILE_INCOMPATIBLE;
                        }
                    }
                }
                BlobSubmissionOutcome outcome = instance.addBlob(fi.fileBlob, fi.contentType, fi.partialPath, false,
                        cc);

                switch (outcome) {
                case FILE_UNCHANGED:
                    return InstanceFileChangeDetail.FILE_PRESENT;
                case NEW_FILE_VERSION:
                    return InstanceFileChangeDetail.FILE_INCOMPATIBLE;
                case COMPLETELY_NEW_FILE:
                    return InstanceFileChangeDetail.FILE_PRESENT;
                default:
                    throw new IllegalStateException("Unexpected extra status for BlobSubmissionOutcome");
                }

            } finally {
                propsLock.release();
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw e;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw e;
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKTaskLockException e) {
            e.printStackTrace();
            throw e;
        } catch (PermissionDeniedException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            throw e;
        }
    }

    /**
     * Retrieve the BlobEntitySet for a given tableId and rowId. This should be
     * treated as read-only by the caller. Updates should be done through the
     * putFile or postFiles APIs. Manipulations of the blob entity set (which is
     * being updated) needs to be guarded by a task lock.
     * 
     * It is safe to fetch the Blob from this set because the set is write-only.
     * 
     * @param tableId
     * @param rowId
     * @param cb
     *          -- callback to process each file manifest entry
     * @param userPermissions
     * @return map of partial path of file to the FileContentInfo for that file.
     * @throws IOException
     * @throws ODKTaskLockException
     * @throws PermissionDeniedException
     * @throws ODKDatastoreException
     */
    public void getInstanceAttachments(String tableId, String rowId, FileContentHandler cb,
            TablesUserPermissions userPermissions)
            throws IOException, ODKTaskLockException, PermissionDeniedException, ODKDatastoreException {

        try {
            if (tableId == null) {
                throw new IllegalArgumentException("tableId cannot be null!");
            }

            if (rowId == null) {
                throw new IllegalArgumentException("rowId cannot be null!");
            }

            userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW);

            OdkTablesLockTemplate propsLock = new OdkTablesLockTemplate(tableId, rowId,
                    ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, OdkTablesLockTemplate.DelayStrategy.LONG,
                    cc);

            try {
                propsLock.acquire();

                DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
                final BlobEntitySet instance = blobStore.getBlobEntitySet(rowId, cc);

                int count = instance.getAttachmentCount(cc);
                for (int i = 1; i <= count; ++i) {
                    final int iSafe = i;
                    FileContentInfo info = new FileContentInfo(instance.getUnrootedFilename(i, cc),
                            instance.getContentType(i, cc), instance.getContentLength(i, cc),
                            instance.getContentHash(i, cc), null);

                    cb.processFileContent(info, new FetchBlobHandler() {
                        @Override
                        public byte[] getBlob() throws ODKDatastoreException {
                            return instance.getBlob(iSafe, cc);
                        }
                    });
                }
            } finally {
                propsLock.release();
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw e;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw e;
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKTaskLockException e) {
            e.printStackTrace();
            throw e;
        } catch (PermissionDeniedException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            throw e;
        }
    }

    public void postFiles(String tableId, String rowId, InMultiPart inMP, TablesUserPermissions userPermissions)
            throws IOException, ODKTaskLockException, ODKTablesException, ODKDatastoreException {

        try {
            if (tableId == null) {
                throw new IllegalArgumentException("tableId cannot be null!");
            }

            if (rowId == null) {
                throw new IllegalArgumentException("rowId cannot be null!");
            }

            userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_ROW);

            OdkTablesLockTemplate propsLock = new OdkTablesLockTemplate(tableId, rowId,
                    ODKTablesTaskLockType.TABLES_NON_PERMISSIONS_CHANGES, OdkTablesLockTemplate.DelayStrategy.LONG,
                    cc);

            try {
                propsLock.acquire();

                // fetch these once and then continue to re-use them.
                DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
                BlobEntitySet instance = blobStore.newBlobEntitySet(rowId, cc);

                ODKTablesException e = null;
                // Parse the request
                while (inMP.hasNext()) {
                    InPart part = inMP.next();
                    MultivaluedMap<String, String> headers = part.getHeaders();
                    String disposition = (headers != null) ? headers.getFirst("Content-Disposition") : null;
                    if (disposition == null) {
                        e = new ODKTablesException(InstanceFileService.ERROR_MSG_MULTIPART_FILES_ONLY_EXPECTED);
                        continue;
                    }
                    String partialPath = null;
                    {
                        HeaderValueParser parser = new BasicHeaderValueParser();
                        HeaderElement[] values = BasicHeaderValueParser.parseElements(disposition, parser);
                        for (HeaderElement v : values) {
                            if (v.getName().equalsIgnoreCase("file")) {
                                partialPath = v.getParameterByName("filename").getValue();
                                break;
                            }
                        }
                    }
                    if (partialPath == null) {
                        e = new ODKTablesException(
                                InstanceFileService.ERROR_MSG_MULTIPART_CONTENT_FILENAME_EXPECTED);
                        continue;
                    }

                    String contentType = (headers != null) ? headers.getFirst("Content-Type") : null;

                    ByteArrayOutputStream bo = new ByteArrayOutputStream();
                    InputStream bi = null;
                    try {
                        bi = new BufferedInputStream(part.getInputStream());
                        int length = 1024;
                        // Transfer bytes from in to out
                        byte[] data = new byte[length];
                        int len;
                        while ((len = bi.read(data, 0, length)) >= 0) {
                            if (len != 0) {
                                bo.write(data, 0, len);
                            }
                        }
                    } finally {
                        bi.close();
                    }
                    byte[] content = bo.toByteArray();
                    String md5Hash = PersistenceUtils.newMD5HashUri(content);

                    // we are adding one or more files -- delete any cached ETag value for
                    // this row's attachments manifest
                    try {
                        DbTableInstanceManifestETagEntity entity = DbTableInstanceManifestETags
                                .getRowIdEntry(tableId, rowId, cc);
                        entity.delete(cc);
                    } catch (ODKEntityNotFoundException ex) {
                        // ignore... it might already be deleted or have never existed
                    }

                    int count = instance.getAttachmentCount(cc);
                    boolean found = false;
                    for (int i = 1; i <= count; ++i) {
                        String path = instance.getUnrootedFilename(i, cc);
                        if (path != null && path.equals(partialPath)) {
                            // we already have this in our store -- check that it is
                            // identical.
                            // if not, we have a problem!!!
                            if (md5Hash.equals(instance.getContentHash(i, cc))) {
                                // no-op
                                found = true;
                            } else {
                                // this is an error case; indicated by setting exception
                                found = true;
                                e = new InstanceFileModificationException(
                                        ERROR_FILE_VERSION_DIFFERS + "\n" + partialPath);
                                break;
                            }
                        }
                    }
                    if (!found) {
                        BlobSubmissionOutcome outcome = instance.addBlob(content, contentType, partialPath, false,
                                cc);
                        if (outcome == BlobSubmissionOutcome.NEW_FILE_VERSION) {
                            e = new InstanceFileModificationException(
                                    ERROR_FILE_VERSION_DIFFERS + "\n" + partialPath);
                        }
                    }
                }
                if (e != null) {
                    throw e;
                }
            } finally {
                propsLock.release();
            }
        } catch (NullPointerException e) {
            e.printStackTrace();
            throw e;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw e;
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKTaskLockException e) {
            e.printStackTrace();
            throw e;
        } catch (PermissionDeniedException e) {
            e.printStackTrace();
            throw e;
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            throw e;
        }
    }
}