org.opendatakit.common.persistence.engine.gae.StringFieldLengthMapping.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.common.persistence.engine.gae.StringFieldLengthMapping.java

Source

/**
 * Copyright (C) 2011 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.common.persistence.engine.gae;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opendatakit.common.persistence.CommonFieldsBase;
import org.opendatakit.common.persistence.DataField;
import org.opendatakit.common.persistence.DataField.DataType;
import org.opendatakit.common.persistence.PersistConsts;
import org.opendatakit.common.persistence.engine.DatastoreAccessMetrics;
import org.opendatakit.common.persistence.exception.ODKDatastoreException;
import org.opendatakit.common.security.User;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Text;

/**
 * This class manages accesses to the _STRING_FIELD_LENGTHS_ table.
 * That table has a PK of the gaeEntityKind of a table managed by 
 * the persistence layer.  The LENGTH_MAPPING column is a Text object
 * that contains a space-separated list of (field-name,length) pairs,
 * with an extra space at the end.  It is parsed to return the lengths
 * of the various fields.  
 * 
 * Provides equivalent functionality to DESCRIBE in RDBMS.
 * 
 * @author mitchellsundt@gmail.com
 *
 */
public class StringFieldLengthMapping {

    private static final String TABLE_NAME = "_STRING_FIELD_LENGTHS_";
    private static final String LENGTH_MAPPING = "LENGTH_MAPPING";

    private Log logger = LogFactory.getLog(StringFieldLengthMapping.class);

    public StringFieldLengthMapping() {
    }

    /**
     * Called when a table is dropped to remove the lengths record
     * from the datastore.
     * 
     * @param gaeEntityKind -- entity kind whose column lengths are to be forgotten.
     * @param dam
     * @param datastore
     * @param user
     * @throws ODKDatastoreException
     */
    public synchronized void removeStringFieldLengths(String gaeEntityKind, DatastoreAccessMetrics dam,
            DatastoreImpl datastore, User user) throws ODKDatastoreException {

        logger.info(gaeEntityKind);
        Key key = KeyFactory.createKey(TABLE_NAME, gaeEntityKind);
        try {
            datastore.getDatastoreService().delete(key);
        } catch (Exception e) {
            throw new ODKDatastoreException(e);
        }
        dam.recordDeleteUsage(TABLE_NAME);
    }

    /**
     * Internal routine to serialize and save the column lengths map.
     * 
     * @param e
     * @param lenMap
     * @param dam
     * @param datastore
     * @param user
     */
    private void putEntity(Entity e, Map<String, Long> lenMap, DatastoreAccessMetrics dam, DatastoreImpl datastore,
            User user) {
        logger.info(e.getKey().getName());

        // build up the string that will be saved in BigTables
        StringBuilder b = new StringBuilder();
        for (Map.Entry<String, Long> entry : lenMap.entrySet()) {
            String k = entry.getKey();
            Long v = entry.getValue();
            b.append(k).append(" ").append(v).append(" ");
        }
        e.setProperty(LENGTH_MAPPING, new Text(b.toString()));

        datastore.getDatastoreService().put(e);
        dam.recordPutUsage(TABLE_NAME);
    }

    /**
     * Called when asserting the existence of a table.
     * Upon completion, the maximum lengths of all STRING
     * and URI columns will have been set.
     * 
     * @param gaeEntityKind -- entity kind being asserted 
     * @param relation
     * @param dam
     * @param datastore
     * @param user
     * @throws ODKDatastoreException
     */
    public synchronized void assertStringFieldLengths(String gaeEntityKind, CommonFieldsBase relation,
            DatastoreAccessMetrics dam, DatastoreImpl datastore, User user) throws ODKDatastoreException {

        Map<String, Long> lenMap = new HashMap<String, Long>();

        logger.info(gaeEntityKind);
        Key key = KeyFactory.createKey(TABLE_NAME, gaeEntityKind);

        try {
            dam.recordGetUsage(TABLE_NAME);
            Entity e = datastore.getDatastoreService().get(key);
            // we have a record of this -- get the mapping value...
            Object o = e.getProperty(LENGTH_MAPPING);
            if (!(o instanceof Text)) {
                throw new ODKDatastoreException("Expected Text object but found " + o.getClass().getName());
            }
            Text t = (Text) o;

            // for convenience, the last element of the split array
            // will be an extraneous empty string.  Confirm this!
            String[] splits = t.getValue().split(" ");
            if (splits.length % 2 != 0) {
                throw new ODKDatastoreException("Unexpected non-even-element-count " + splits.length
                        + " for string lengths map: " + t.getValue());
            }

            for (int i = 0; i < splits.length; i = i + 2) {
                String k = splits[i];
                String vStr = splits[i + 1];

                try {
                    Long v = Long.parseLong(vStr);
                    lenMap.put(k, v);
                } catch (NumberFormatException ex) {
                    throw new ODKDatastoreException(
                            "column name: " + k + " length: " + vStr + " could not be parsed: " + ex.toString());
                }
            }

            // we now have the lenMap as saved in the database.
            // Update the relation with the values from this list
            // and detect any new or changed values.
            // 
            // For migrations, we trust any incoming value over 
            // any the saved value.  Track whether the saved
            // value should be updated with additions, but don't
            // change it if there is a change that alters an 
            // existing (specified) length.
            boolean changed = false;
            for (DataField f : relation.getFieldList()) {
                if (f.getDataType() == DataType.STRING || f.getDataType() == DataType.URI) {
                    // string field -- get what GAE thinks
                    // the length should be...
                    Long len = lenMap.get(f.getName());
                    Long reqLen = f.getMaxCharLen();
                    if (len == null) {
                        // GAE doesn't know -- we need to update GAE
                        changed = true;
                        if (reqLen == null) {
                            // we're using the default length -- set field too...
                            reqLen = PersistConsts.DEFAULT_MAX_STRING_LENGTH;
                            f.setMaxCharLen(reqLen);
                        }
                        lenMap.put(f.getName(), reqLen);
                    } else {
                        if (reqLen == null) {
                            // GAE knows the length -- set field now...
                            f.setMaxCharLen(len);
                        } else if (reqLen != len) {
                            // length differs -- update GAE
                            changed = true;
                            lenMap.put(f.getName(), reqLen);
                        }
                    }
                }
            }

            if (changed) {
                // we changed something in lenMap -- update GAE
                putEntity(e, lenMap, dam, datastore, user);
            }

        } catch (EntityNotFoundException ex) {
            // no record of this entity...
            // ensure all default lengths are imposed.
            // build up length map.
            for (DataField f : relation.getFieldList()) {
                if ((f.getDataType() == DataType.STRING || f.getDataType() == DataType.URI)) {
                    Long len = f.getMaxCharLen();
                    if (len == null) {
                        len = PersistConsts.DEFAULT_MAX_STRING_LENGTH;
                        f.setMaxCharLen(len);
                    }
                    lenMap.put(f.getName(), f.getMaxCharLen());
                }
            }

            // define a new entity...
            Entity e = new Entity(TABLE_NAME, gaeEntityKind);
            // set and insert it into GAE...
            putEntity(e, lenMap, dam, datastore, user);
        }

        // And verify that the invariant condition holds...
        // Everything should have a length
        for (DataField f : relation.getFieldList()) {
            if ((f.getDataType() == DataType.STRING || f.getDataType() == DataType.URI)
                    && f.getMaxCharLen() == null) {
                throw new ODKDatastoreException("Failed to set all lengths!");
            }
        }
    }
}