org.opendatakit.aggregate.externalservice.FormServiceCursor.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.aggregate.externalservice.FormServiceCursor.java

Source

/**
 * Copyright (C) 2010 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.externalservice;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opendatakit.aggregate.constants.TaskLockType;
import org.opendatakit.aggregate.constants.common.ExternalServicePublicationOption;
import org.opendatakit.aggregate.constants.common.ExternalServiceType;
import org.opendatakit.aggregate.constants.common.OperationalStatus;
import org.opendatakit.aggregate.exception.ODKFormNotFoundException;
import org.opendatakit.aggregate.form.FormFactory;
import org.opendatakit.aggregate.form.IForm;
import org.opendatakit.common.persistence.CommonFieldsBase;
import org.opendatakit.common.persistence.DataField;
import org.opendatakit.common.persistence.DataField.IndexType;
import org.opendatakit.common.persistence.Datastore;
import org.opendatakit.common.persistence.PersistConsts;
import org.opendatakit.common.persistence.Query;
import org.opendatakit.common.persistence.Query.Direction;
import org.opendatakit.common.persistence.Query.FilterOperation;
import org.opendatakit.common.persistence.TaskLock;
import org.opendatakit.common.persistence.exception.ODKDatastoreException;
import org.opendatakit.common.persistence.exception.ODKEntityNotFoundException;
import org.opendatakit.common.persistence.exception.ODKOverQuotaException;
import org.opendatakit.common.persistence.exception.ODKTaskLockException;
import org.opendatakit.common.security.User;
import org.opendatakit.common.web.CallingContext;

/**
 *
 * @author wbrunette@gmail.com
 * @author mitchellsundt@gmail.com
 *
 */
public final class FormServiceCursor extends CommonFieldsBase {

    private static final String TABLE_NAME = "_form_service_cursor_2";

    private static final DataField URI_MD5_FORM_ID_PROPERTY = new DataField("URI_MD5_FORM_ID",
            DataField.DataType.URI, false, PersistConsts.URI_STRING_LEN).setIndexable(IndexType.HASH);
    private static final DataField AURI_SERVICE_PROPERTY = new DataField("AURI_SERVICE", DataField.DataType.URI,
            false, PersistConsts.URI_STRING_LEN).setIndexable(IndexType.HASH);

    private static final DataField EXT_SERVICE_TYPE_PROPERTY = new DataField("EXT_SERVICE_TYPE",
            DataField.DataType.STRING, false, 200L);
    private static final DataField EXTERNAL_SERVICE_OPTION = new DataField("EXTERNAL_SERVICE_OPTION",
            DataField.DataType.STRING, false, 80L);
    // some external services need to be prepared before they can receive data...
    private static final DataField IS_EXTERNAL_SERVICE_PREPARED = new DataField("IS_EXTERNAL_SERVICE_PREPARED",
            DataField.DataType.BOOLEAN, true);
    private static final DataField OPERATIONAL_STATUS = new DataField("OPERATIONAL_STATUS",
            DataField.DataType.STRING, true, 80L);
    private static final DataField RETRY_STATUS = new DataField("RETRY_STATUS", DataField.DataType.STRING, true,
            80L);
    private static final DataField ESTABLISHMENT_DATETIME = new DataField("ESTABLISHMENT_DATETIME",
            DataField.DataType.DATETIME, false);
    private static final DataField UPLOAD_COMPLETED_PROPERTY = new DataField("UPLOAD_COMPLETED",
            DataField.DataType.BOOLEAN, true);
    private static final DataField LAST_UPLOAD_CURSOR_DATE_PROPERTY = new DataField(
            "LAST_UPLOAD_PERSISTENCE_CURSOR", DataField.DataType.DATETIME, true);
    private static final DataField LAST_UPLOAD_KEY_PROPERTY = new DataField("LAST_UPLOAD_KEY",
            DataField.DataType.STRING, true, 4096L);
    private static final DataField LAST_STREAMING_CURSOR_DATE_PROPERTY = new DataField(
            "LAST_STREAMING_PERSISTENCE_CURSOR", DataField.DataType.DATETIME, true);
    private static final DataField LAST_STREAMING_KEY_PROPERTY = new DataField("LAST_STREAMING_KEY",
            DataField.DataType.STRING, true, 4096L);
    private static final DataField FORM_ID_PROPERTY = new DataField("FORM_ID", DataField.DataType.STRING, true,
            4096L);

    /**
     * Construct a relation prototype.  Only called via {@link #assertRelation(CallingContext)}
     *
     * @param databaseSchema
     * @param tableName
     */
    private FormServiceCursor(String schemaName) {
        super(schemaName, TABLE_NAME);
        fieldList.add(URI_MD5_FORM_ID_PROPERTY);
        fieldList.add(AURI_SERVICE_PROPERTY);
        fieldList.add(EXT_SERVICE_TYPE_PROPERTY);
        fieldList.add(EXTERNAL_SERVICE_OPTION);
        fieldList.add(IS_EXTERNAL_SERVICE_PREPARED);
        fieldList.add(OPERATIONAL_STATUS);
        fieldList.add(RETRY_STATUS);
        fieldList.add(ESTABLISHMENT_DATETIME);
        fieldList.add(UPLOAD_COMPLETED_PROPERTY);
        fieldList.add(LAST_UPLOAD_CURSOR_DATE_PROPERTY);
        fieldList.add(LAST_UPLOAD_KEY_PROPERTY);
        fieldList.add(LAST_STREAMING_CURSOR_DATE_PROPERTY);
        fieldList.add(LAST_STREAMING_KEY_PROPERTY);
        fieldList.add(FORM_ID_PROPERTY);
    }

    /**
     * Construct an empty entity.  Only called via {@link #getEmptyRow(User)}
     *
     * @param ref
     * @param user
     */
    private FormServiceCursor(FormServiceCursor ref, User user) {
        super(ref, user);
    }

    // Only called from within the persistence layer.
    @Override
    public FormServiceCursor getEmptyRow(User user) {
        return new FormServiceCursor(this, user);
    }

    public ExternalServiceType getExternalServiceType() {
        String type = getStringField(EXT_SERVICE_TYPE_PROPERTY);
        return ExternalServiceType.valueOf(type);
    }

    public void setServiceClassname(ExternalServiceType value) {
        if (!setStringField(EXT_SERVICE_TYPE_PROPERTY, value.name())) {
            throw new IllegalArgumentException("overflow externalServiceType");
        }
    }

    public ExternalServicePublicationOption getExternalServicePublicationOption() {
        return ExternalServicePublicationOption.valueOf(getStringField(EXTERNAL_SERVICE_OPTION));
    }

    public void setExternalServiceOption(ExternalServicePublicationOption value) {
        if (!setStringField(EXTERNAL_SERVICE_OPTION, value.name())) {
            throw new IllegalArgumentException("overflow externalServiceOption");
        }
    }

    public Boolean isExternalServicePrepared() {
        return getBooleanField(IS_EXTERNAL_SERVICE_PREPARED);
    }

    public void setIsExternalServicePrepared(Boolean value) {
        setBooleanField(IS_EXTERNAL_SERVICE_PREPARED, value);
    }

    public OperationalStatus getOperationalStatus() {
        String value = getStringField(OPERATIONAL_STATUS);
        if (value == null)
            return null;
        return OperationalStatus.valueOf(value);
    }

    public void setOperationalStatus(OperationalStatus value) {
        if (!setStringField(OPERATIONAL_STATUS, value.name())) {
            throw new IllegalArgumentException("overflow operationalStatus");
        }
    }
    // TODO: implement failure after N retries (TBD)
    // RetryStatus is added to support that determination.
    //
    //  public String getRetryStatus() {
    //     String value = getStringField(RETRY_STATUS);
    //     if ( value == null ) return null;
    //     return value;
    //  }
    //
    //  public void setRetryStatus(String value) {
    //    if (!setStringField(RETRY_STATUS, value)) {
    //      throw new IllegalArgumentException("overflow retryStatus");
    //    }
    //  }

    public Date getEstablishmentDateTime() {
        return getDateField(ESTABLISHMENT_DATETIME);
    }

    public void setEstablishmentDateTime(Date value) {
        setDateField(ESTABLISHMENT_DATETIME, value);
    }

    public Boolean getUploadCompleted() {
        return getBooleanField(UPLOAD_COMPLETED_PROPERTY);
    }

    public void setUploadCompleted(Boolean value) {
        setBooleanField(UPLOAD_COMPLETED_PROPERTY, value);
    }

    public Date getLastUploadCursorDate() {
        return getDateField(LAST_UPLOAD_CURSOR_DATE_PROPERTY);
    }

    public void setLastUploadCursorDate(Date value) {
        setDateField(LAST_UPLOAD_CURSOR_DATE_PROPERTY, value);
    }

    public String getLastUploadKey() {
        return getStringField(LAST_UPLOAD_KEY_PROPERTY);
    }

    public void setLastUploadKey(String value) {
        if (!setStringField(LAST_UPLOAD_KEY_PROPERTY, value)) {
            throw new IllegalArgumentException("overflow lastUploadKey");
        }
    }

    public Date getLastStreamingCursorDate() {
        return getDateField(LAST_STREAMING_CURSOR_DATE_PROPERTY);
    }

    public void setLastStreamingCursorDate(Date value) {
        setDateField(LAST_STREAMING_CURSOR_DATE_PROPERTY, value);
    }

    public String getLastStreamingKey() {
        return getStringField(LAST_STREAMING_KEY_PROPERTY);
    }

    public void setLastStreamingKey(String value) {
        if (!setStringField(LAST_STREAMING_KEY_PROPERTY, value)) {
            throw new IllegalArgumentException("overflow lastStreamingKey");
        }
    }

    public String getAuriService() {
        return getStringField(AURI_SERVICE_PROPERTY);
    }

    public void setAuriService(String value) {
        if (!setStringField(AURI_SERVICE_PROPERTY, value)) {
            throw new IllegalArgumentException("overflow auriService");
        }
    }

    public String getUriMd5FormId() {
        return getStringField(URI_MD5_FORM_ID_PROPERTY);
    }

    public void setUriMd5FormId(String value) {
        if (!setStringField(URI_MD5_FORM_ID_PROPERTY, value)) {
            throw new IllegalArgumentException("overflow uriMd5FormId");
        }
    }

    public String getFormId() {
        return getStringField(FORM_ID_PROPERTY);
    }

    public void setFormId(String value) {
        if (!setStringField(FORM_ID_PROPERTY, value)) {
            throw new IllegalArgumentException("overflow formId");
        }
    }

    public ExternalService getExternalService(CallingContext cc) throws ODKEntityNotFoundException,
            ODKFormNotFoundException, ODKOverQuotaException, ODKDatastoreException {
        IForm form = FormFactory.retrieveFormByFormId(getFormId(), cc);
        return constructExternalService(this, form, cc);
    }

    private static FormServiceCursor relation = null;

    private static synchronized final FormServiceCursor assertRelation(CallingContext cc)
            throws ODKDatastoreException {
        if (relation == null) {
            FormServiceCursor relationPrototype;
            Datastore ds = cc.getDatastore();
            User user = cc.getUserService().getDaemonAccountUser();
            relationPrototype = new FormServiceCursor(ds.getDefaultSchemaName());
            ds.assertRelation(relationPrototype, user); // may throw exception...
            // at this point, the prototype has become fully populated
            relation = relationPrototype; // set static variable only upon success...
        }
        return relation;
    }

    public static final FormServiceCursor createFormServiceCursor(IForm form, ExternalServiceType type,
            CommonFieldsBase service, CallingContext cc) throws ODKDatastoreException {
        FormServiceCursor relation = assertRelation(cc);

        FormServiceCursor c = cc.getDatastore().createEntityUsingRelation(relation, cc.getCurrentUser());

        c.setUriMd5FormId(form.getEntityKey().getKey());
        c.setAuriService(service.getUri());
        c.setFormId(form.getFormId());
        c.setServiceClassname(type);

        return c;
    }

    /**
     * Implement the generic deletion of an ExternalService task here.
     * This needs to be within a TaskLock to prevent a concurrent update of the
     * ExternalServiceCursor from leaving the ExternalServiceCursor object in the
     * database while the underlying Publisher records
     * (e.g., FusionTable2ParameterTable, GoogleSpreadsheet2ParameterTable)
     * are deleted.
     *
     * @param service
     * @param cc
     * @return true if the deletion was successful
     * @throws ODKDatastoreException
     */
    public static final boolean deleteExternalServiceTask(ExternalService service, CallingContext cc)
            throws ODKDatastoreException {
        Datastore ds = cc.getDatastore();
        User user = cc.getCurrentUser();
        String uriExternalService = service.getFormServiceCursor().getUri();
        TaskLock taskLock = ds.createTaskLock(user);
        String pLockId = UUID.randomUUID().toString();
        boolean deleted = false;
        try {
            if (taskLock.obtainLock(pLockId, uriExternalService, TaskLockType.UPLOAD_SUBMISSION)) {
                taskLock = null;
                service.delete(cc);
                deleted = true;
            }
        } catch (ODKTaskLockException e1) {
            e1.printStackTrace();
        } finally {
            if (!deleted) {
                Log logger = LogFactory.getLog(FormServiceCursor.class);
                logger.error("Unable to delete FormServiceCursor: " + service.getFormServiceCursor().getUri());
            }
        }
        taskLock = ds.createTaskLock(user);
        try {
            for (int i = 0; i < 10; i++) {
                if (taskLock.releaseLock(pLockId, uriExternalService, TaskLockType.UPLOAD_SUBMISSION))
                    break;
                try {
                    Thread.sleep(PersistConsts.MIN_SETTLE_MILLISECONDS);
                } catch (InterruptedException e) {
                    // just move on, this retry mechanism is to only
                    // make things
                    // nice
                }
            }
        } catch (ODKTaskLockException e) {
            e.printStackTrace();
        }
        return deleted;
    }

    public static final List<ExternalService> getExternalServicesForForm(IForm form, CallingContext cc)
            throws ODKDatastoreException {
        FormServiceCursor relation = assertRelation(cc);
        Query query = cc.getDatastore().createQuery(relation,
                "FormServiceCursor.getExternalServicesForForm[" + form.getFormId() + "]", cc.getCurrentUser());
        // filter on the Form's Uri. We cannot filter on the FORM_ID since it is a
        // Text field in bigtable
        query.addFilter(URI_MD5_FORM_ID_PROPERTY, FilterOperation.EQUAL, form.getEntityKey().getKey());
        query.addSort(URI_MD5_FORM_ID_PROPERTY, Direction.ASCENDING); // gae optimization
        List<ExternalService> esList = new ArrayList<ExternalService>();

        List<? extends CommonFieldsBase> fscList = query.executeQuery();
        for (CommonFieldsBase cb : fscList) {
            FormServiceCursor c = (FormServiceCursor) cb;

            ExternalService obj = constructExternalService(c, form, cc);
            esList.add(obj);

        }
        return esList;
    }

    public static final FormServiceCursor getFormServiceCursor(String uri, CallingContext cc)
            throws ODKEntityNotFoundException, ODKOverQuotaException, ODKDatastoreException {
        try {
            FormServiceCursor relation = assertRelation(cc);
            CommonFieldsBase entity = cc.getDatastore().getEntity(relation, uri, cc.getCurrentUser());
            return (FormServiceCursor) entity;
        } catch (ODKOverQuotaException e) {
            throw e;
        } catch (ODKEntityNotFoundException e) {
            throw e;
        } catch (ODKDatastoreException e) {
            throw e;
        }
    }

    /**
     * Retrieve the list of FormServiceCursor objects that have not been updated
     * more recently than the olderThanDate.  I.e., old ones that might need to
     * be kicked into action.
     *
     * @param olderThanDate
     * @param cc
     * @return
     * @throws ODKEntityNotFoundException
     * @throws ODKOverQuotaException
     */
    public static final List<FormServiceCursor> queryFormServiceCursorRelation(Date olderThanDate,
            CallingContext cc) throws ODKEntityNotFoundException, ODKOverQuotaException {
        List<FormServiceCursor> fscList = new ArrayList<FormServiceCursor>();
        try {
            FormServiceCursor relation = assertRelation(cc);
            Query query = cc.getDatastore().createQuery(relation,
                    "FormServiceCursor.queryFormServiceCursorRelation", cc.getCurrentUser());
            query.addFilter(relation.lastUpdateDate, FilterOperation.LESS_THAN_OR_EQUAL, olderThanDate);
            query.addSort(relation.lastUpdateDate, Direction.ASCENDING);
            List<? extends CommonFieldsBase> cfbList = query.executeQuery();
            for (CommonFieldsBase cfb : cfbList) {
                fscList.add((FormServiceCursor) cfb);
            }
        } catch (ODKOverQuotaException e) {
            throw e;
        } catch (ODKDatastoreException e) {
            throw new ODKEntityNotFoundException(e);
        }
        return fscList;
    }

    public static final ExternalService constructExternalService(FormServiceCursor fsc, IForm form,
            CallingContext cc) throws ODKEntityNotFoundException, ODKOverQuotaException {
        try {
            switch (fsc.getExternalServiceType()) {
            case GOOGLE_FUSIONTABLES:
                return new FusionTable(fsc, form, cc);
            case GOOGLE_MAPS_ENGINE:
                return new GoogleMapsEngine(fsc, form, cc);
            case GOOGLE_SPREADSHEET:
                return new GoogleSpreadsheet(fsc, form, cc);
            case JSON_SERVER:
                return new JsonServer(fsc, form, cc);
            case OHMAGE_JSON_SERVER:
                return new OhmageJsonServer(fsc, form, cc);
            case REDCAP_SERVER:
                return new REDCapServer(fsc, form, cc);
            default:
                return null;
            }
        } catch (ODKOverQuotaException e) {
            throw e;
        } catch (Exception e) {
            throw new ODKEntityNotFoundException("Somehow DB entities for publisher got into problem state", e);
        }
    }
}