package com.devsmart.ubjson;

import java.io.Serializable;
import java.util.Comparator;

public abstract class UBValue implements Comparable<UBValue>, Serializable {

    public static final byte MARKER_NULL = 'Z';
    public static final byte MARKER_TRUE = 'T';
    public static final byte MARKER_FALSE = 'F';
    public static final byte MARKER_CHAR = 'C';
    public static final byte MARKER_INT8 = 'i';
    public static final byte MARKER_UINT8 = 'U';
    public static final byte MARKER_INT16 = 'I';
    public static final byte MARKER_INT32 = 'l';
    public static final byte MARKER_INT64 = 'L';
    public static final byte MARKER_FLOAT32 = 'd';
    public static final byte MARKER_FLOAT64 = 'D';
    public static final byte MARKER_STRING = 'S';
    public static final byte MARKER_ARRAY_START = '[';
    public static final byte MARKER_ARRAY_END = ']';
    public static final byte MARKER_OBJ_START = '{';
    public static final byte MARKER_OBJ_END = '}';
    public static final byte MARKER_OPTIMIZED_TYPE = '$';
    public static final byte MARKER_OPTIMIZED_SIZE = '#';
    private static final long serialVersionUID = -7268494408231769421L;

    public enum Type {
        Null,
        Char,
        Bool,
        Int8,
        Uint8,
        Int16,
        Int32,
        Int64,
        Float32,
        Float64,
        String,
        Array,
        Object
    }

    public abstract Type getType();
    //public abstract void write(OutputStream out) throws IOException;

    public boolean isNull() {
        return getType() == Type.Null;
    }

    public boolean isBool() {
        return getType() == Type.Bool;
    }

    public boolean asBool() {
        return ((UBBool)this).getBool();
    }

    public boolean isChar() {
        return getType() == Type.Char;
    }

    public char asChar() {
        return ((UBChar)this).getChar();
    }

    public boolean isNumber() {
        switch (getType()){
            case Int8:
            case Uint8:
            case Int16:
            case Int32:
            case Int64:
            case Float32:
            case Float64:
                return true;

            default:
                return false;
        }
    }

    public boolean isInteger() {
        switch (getType()){
            case Int8:
            case Uint8:
            case Int16:
            case Int32:
            case Int64:
                return true;

            default:
                return false;
        }
    }

    public boolean isString() {
        return getType() == Type.String;
    }

    public String asString() {
        if(this.isNull()) {
            return null;
        } else {
            UBString thiz = (UBString) this;
            return thiz.getString();
        }
    }

    public byte[] asByteArray() {
        if(isNull()) {
            return null;
        } else if(isString()) {
            return ((UBString) this).asByteArray();
        } else {
            return ((UBInt8Array) this).getValues();
        }
    }

    public byte asByte() {
        return (byte)asInt();
    }

    public short asShort() {
        return (short)asInt();
    }

    public int asInt() {
        switch (getType()){
            case Int8:
                return ((UBInt8)this).getInt();
            case Uint8:
                return ((UBUInt8)this).getInt();
            case Int16:
                return ((UBInt16)this).getInt();
            case Int32:
                return ((UBInt32)this).getInt();
            case Int64:
                return (int)((UBInt64)this).getInt();
            case Float32:
                return (int)((UBFloat32)this).getFloat();
            case Float64:
                return (int)((UBFloat64)this).getDouble();
            case String:
                return Integer.parseInt(asString());

            default:
                throw new RuntimeException("not a number type");

        }
    }

    public long asLong() {
        switch (getType()){
            case Bool:
                return asBool() ? 1 : 0;
            case Char:
                return asChar();
            case Int8:
                return ((UBInt8)this).getInt();
            case Uint8:
                return ((UBUInt8)this).getInt();
            case Int16:
                return ((UBInt16)this).getInt();
            case Int32:
                return ((UBInt32)this).getInt();
            case Int64:
                return (long)((UBInt64)this).getInt();
            case Float32:
                return (long)((UBFloat32)this).getFloat();
            case Float64:
                return (long)((UBFloat64)this).getDouble();
            case String:
                return Long.parseLong(asString());

            default:
                throw new RuntimeException("not a number type");

        }
    }

    public float asFloat32() {
        switch (getType()) {
            case Float32:
                return ((UBFloat32)this).getFloat();
            case Float64:
                return (float)((UBFloat64)this).getDouble();
            case Int8:
                return ((UBInt8)this).getInt();
            case Uint8:
                return ((UBUInt8)this).getInt();
            case Int16:
                return ((UBInt16)this).getInt();
            case Int32:
                return ((UBInt32)this).getInt();
            case Int64:
                return ((UBInt64)this).getInt();
            case String:
                return Float.parseFloat(asString());

            default:
                throw new RuntimeException("not a float type");
        }
    }

    public double asFloat64() {
        switch (getType()) {
            case Float32:
                return ((UBFloat32)this).getFloat();
            case Float64:
                return ((UBFloat64)this).getDouble();
            case Int8:
                return ((UBInt8)this).getInt();
            case Uint8:
                return ((UBUInt8)this).getInt();
            case Int16:
                return ((UBInt16)this).getInt();
            case Int32:
                return ((UBInt32)this).getInt();
            case Int64:
                return ((UBInt64)this).getInt();
            case String:
                return Double.parseDouble(asString());

            default:
                throw new RuntimeException("not a float type");
        }
    }

    public boolean isArray() {
        return getType() == Type.Array;
    }

    public UBArray asArray() {
        return ((UBArray)this);
    }

    public int size() {
        int retval;
        switch (getType()) {
            case Array:
                retval = asArray().size();
                break;

            case String:
                retval = ((UBString)this).length();
                break;

            default:
                retval = -1;
        }

        return retval;
    }

    /**
     * Interprets a strongly-typed number array to array of booleans.
     * If a value found in the array is greater than 0 return true, false otherwise.
     * @return booleans
     */
    public boolean[] asBoolArray() {
        boolean[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            case Int16: {
                short[] data = ((UBInt16Array) array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            case Int32: {
                int[] data = ((UBInt32Array)array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            case Int64: {
                long[] data = ((UBInt64Array)array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            case Float32: {
                float[] data = ((UBFloat32Array) array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            case Float64: {
                double[] data = ((UBFloat64Array) array).getValues();
                retval = new boolean[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i] > 0;
                }
                break;
            }

            default:
                throw new RuntimeException("not an int32[] type");
        }

        return retval;
    }

    public short[] asShortArray() {
        short[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new short[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int16: {
                retval = ((UBInt16Array) array).getValues();
                break;
            }

            case Int32: {
                int[] data = ((UBInt32Array) array).getValues();
                retval = new short[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (short) data[i];
                }
                break;
            }

            case Int64: {
                long[] data = ((UBInt64Array) array).getValues();
                retval = new short[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (short) data[i];
                }
                break;
            }

            case Float32: {
                float[] data = ((UBFloat32Array) array).getValues();
                retval = new short[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (short) data[i];
                }
                break;
            }

            case Float64: {
                double[] data = ((UBFloat64Array) array).getValues();
                retval = new short[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (short) data[i];
                }
                break;
            }

            default:
                throw new RuntimeException("not an int32[] type");
        }

        return retval;
    }

    public int[] asInt32Array() {
        int[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new int[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int16: {
                short[] data = ((UBInt16Array) array).getValues();
                retval = new int[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int32: {
                retval = ((UBInt32Array)array).getValues();
                break;
            }

            case Int64: {
                long[] data = ((UBInt64Array) array).getValues();
                retval = new int[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (int) data[i];
                }
                break;
            }

            case Float32: {
                float[] data = ((UBFloat32Array) array).getValues();
                retval = new int[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (int) data[i];
                }
                break;
            }

            case Float64: {
                double[] data = ((UBFloat64Array) array).getValues();
                retval = new int[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (int) data[i];
                }
                break;
            }

            default:
                throw new RuntimeException("not an int32[] type");
        }

        return retval;
    }

    public long[] asInt64Array() {
        long[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new long[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int16: {
                short[] data = ((UBInt16Array) array).getValues();
                retval = new long[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int32: {
                int[] data = ((UBInt32Array) array).getValues();
                retval = new long[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int64: {
                retval = ((UBInt64Array) array).getValues();
                break;
            }

            case Float32: {
                float[] data = ((UBFloat32Array) array).getValues();
                retval = new long[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (long) data[i];
                }
                break;
            }

            case Float64: {
                double[] data = ((UBFloat64Array) array).getValues();
                retval = new long[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (long) data[i];
                }
                break;
            }

            default:
                throw new RuntimeException("not an int32[] type");
        }

        return retval;
    }

    public float[] asFloat32Array() {
        float[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new float[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int16: {
                short[] data = ((UBInt16Array) array).getValues();
                retval = new float[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int32: {
                int[] data = ((UBInt32Array) array).getValues();
                retval = new float[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int64: {
                long[] data = ((UBInt64Array) array).getValues();
                retval = new float[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Float32:
                retval = ((UBFloat32Array)array).getValues();
                break;

            case Float64: {
                double[] data = ((UBFloat64Array) array).getValues();
                retval = new float[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = (float) data[i];
                }
                break;
            }

            default:
                throw new RuntimeException("not an float32[] type");
        }

        return retval;
    }

    public double[] asFloat64Array() {
        double[] retval;
        UBArray array = asArray();
        switch(array.getStrongType()){
            case Int8: {
                byte[] data = ((UBInt8Array) array).getValues();
                retval = new double[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int16: {
                short[] data = ((UBInt16Array) array).getValues();
                retval = new double[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int32: {
                int[] data = ((UBInt32Array) array).getValues();
                retval = new double[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Int64: {
                long[] data = ((UBInt64Array) array).getValues();
                retval = new double[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Float32: {
                float[] data = ((UBFloat32Array) array).getValues();
                retval = new double[data.length];
                for (int i = 0; i < data.length; i++) {
                    retval[i] = data[i];
                }
                break;
            }

            case Float64: {
                retval = ((UBFloat64Array)array).getValues();
                break;
            }

            default:
                throw new RuntimeException("not an float32[] type");
        }

        return retval;
    }

    public String[] asStringArray() {
        String[] retval;

        UBArray array = asArray();
        if(UBArray.ArrayType.String != array.getStrongType()){
            throw new RuntimeException("not a strongly-typed string array");
        }
        return ((UBStringArray)array).getValues();
    }

    public boolean isObject() {
        return getType() == Type.Object;
    }

    public UBObject asObject() {
        return ((UBObject)this);
    }

    @Override
    public int hashCode() {
        int retval = 0;
        switch (getType()) {
            case Null:
                retval = 0;
                break;
            case Bool:
                retval = asBool() ? 1 : 0;
                break;
            case Char:
                retval = asChar();
                break;
            case Int8:
            case Uint8:
            case Int16:
            case Int32:
                retval = asInt();
                break;
            case Int64: {
                long value = asLong();
                retval = (int) (value ^ (value >>> 32));
                break;
            }

            case Float32:
                retval = Float.floatToIntBits(asFloat32());
                break;

            case Float64: {
                long value = Double.doubleToLongBits(asFloat64());
                retval = (int) (value ^ (value >>> 32));
                break;
            }

            case String:
                retval = asString().hashCode();
                break;

        }
        return retval;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null) {
            return false;
        }
        if(!(obj instanceof UBValue)) {
            return false;
        }
        return COMPARATOR.compare(this, (UBValue)obj) == 0;
    }

    @Override
    public int compareTo(UBValue value) {
        return COMPARATOR.compare(this, value);
    }

    private static int getCompareType(UBValue value) {
        int retval = 0;
        switch(value.getType()) {
            case Null:
                retval = 0;
                break;

            case Bool:
                retval = 1;
                break;
            case Char:
                retval = 2;
                break;
            case Int8:
            case Uint8:
            case Int16:
            case Int32:
            case Int64:
                retval = 3;
                break;
            case Float32:
            case Float64:
                retval = 4;
                break;
            case String:
                retval = 5;
                break;
            case Array:
                retval = 6;
                break;
            case Object:
                retval = 7;
                break;
        }

        return retval;
    }

    private static int compareBool(boolean a, boolean b){
        return (a == b) ? 0 : (a ? 1 : -1);
    }

    private static int compareChar(char a, char b) {
        return (a == b) ? 0 : (a < b ? -1 : 1);
    }

    private static int compareLong(long a, long b) {
        return (a < b) ? -1 : ((a > b) ? 1 : 0);
    }

    public static final Comparator<UBValue> COMPARATOR = new Comparator<UBValue>() {

        @Override
        public int compare(UBValue a, UBValue b) {
            int retval = getCompareType(a) - getCompareType(b);
            if(retval == 0) {
                switch (getCompareType(a)) {
                    case 1:
                        retval = compareBool(a.asBool(), b.asBool());
                        break;

                    case 2:
                        retval = compareChar(a.asChar(), b.asChar());
                        break;

                    case 3:
                        retval = compareLong(a.asLong(), b.asLong());
                        break;

                    case 4:
                        retval = Double.compare(a.asFloat64(), b.asFloat64());
                        break;

                    case 5:
                        retval = a.asString().compareTo(b.asString());
                        break;

                    case 6:
                        retval = a.asArray().compareTo(b.asArray());
                        break;

                    case 7:
                        retval = a.asObject().compareTo(b.asObject());
                        break;
                }
            }

            return retval;
        }
    };
}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.query;

import java.util.ArrayList;
import java.util.List;

import android.database.Cursor;

import com.turbomanage.storm.SQLiteDao;
import com.turbomanage.storm.TableHelper.Column;
import com.turbomanage.storm.types.BooleanConverter;
import com.turbomanage.storm.types.ByteConverter;
import com.turbomanage.storm.types.CharConverter;
import com.turbomanage.storm.types.EnumConverter;
import com.turbomanage.storm.types.IntegerConverter;
import com.turbomanage.storm.types.LongConverter;
import com.turbomanage.storm.types.ShortConverter;

/**
 * Builds a SQL query constructed by ANDing together all conditions.
 *
 * @author David M. Chandler
 *
 * @param <T>
 */
public class Query<T> {

	private static final String TAG = Query.class.getName();
	private SQLiteDao<T> dao;
	private StringBuilder where = new StringBuilder();
	protected List<String> params = new ArrayList<String>();
	protected String orderBy;

	/**
	 * Constructor requires the {@link SQLiteDao} that will be
	 * used to execute the completed query.
	 *
	 * @param dao
	 */
	public Query(SQLiteDao<T> dao) {
		this.dao = dao;
	}

	/**
	 * Executes a query which returns all rows in the entity table
	 * that match the fields of the example object having values other
	 * than the defaults.
	 *
	 * Calling method MUST close the {@link Cursor}!
	 *
	 * @return Cursor
	 */
	public Query<T> byExample(T obj) {
		if (obj != null) {
			return dao.getTableHelper().buildFilter(this, obj);
		}
		return this;
	}

	/*
	 * Convenience methods for comparing equality of each wrapper type
	 */

	public Query<T> eq(Column colName, Boolean param) {
		Integer sqlValue = BooleanConverter.GET.toSql(param);
		where.append(" AND " + colName + "=?");
		params.add(BooleanConverter.GET.toString(sqlValue));
		return this;
	}

	public Query<T> eq(Column colName, Byte param) {
		Short sqlValue = ByteConverter.GET.toSql(param);
		where.append(" AND " + colName + "=?");
		params.add(ByteConverter.GET.toString(sqlValue));
		return this;
	}

	public Query<T> eq(Column colName, byte[] param) {
		throw new IllegalArgumentException("Exact match on type byte[] is not supported");
	}

	public Query<T> eq(Column colName, Character param) {
		Integer sqlValue = CharConverter.GET.toSql(param);
		where.append(" AND " + colName + "=?");
		params.add(CharConverter.GET.toString(sqlValue));
		return this;
	}

	public Query<T> eq(Column colName, Double param) {
		throw new IllegalArgumentException("Exact match on type double is not supported");
	}

	public Query<T> eq(Column colName, Enum param) {
		String sqlValue = EnumConverter.GET.toSql(param);
		where.append(" AND " + colName + "=?");
		params.add(sqlValue);
		return this;
	}

	public Query<T> eq(Column colName, Float param) {
		throw new IllegalArgumentException("Exact match on type float is not supported");
	}

	public Query<T> eq(Column colName, Integer param) {
		where.append(" AND " + colName + "=?");
		params.add(IntegerConverter.GET.toString(param));
		return this;
	}

	public Query<T> eq(Column colName, Long param) {
		where.append(" AND " + colName + "=?");
		params.add(LongConverter.GET.toString(param));
		return this;
	}

	public Query<T> eq(Column colName, Short param) {
		where.append(" AND " + colName + "=?");
		params.add(ShortConverter.GET.toString(param));
		return this;
	}

	public Query<T> eq(Column colName, String param) {
		where.append(" AND " + colName + "=?");
		params.add(param);
		return this;
	}

	/**
	 * Executes the query using the attached DAO.
	 * Calling method MUST close the Cursor!
	 *
	 * @return Cursor result
	 */
	public Cursor exec() {
		return dao.query(where(), params(), orderBy);
	}

	/**
	 * Executes the query and returns the result as an
	 * object.
	 *
	 * @see SQLiteDao#asObject(Cursor)
	 * @return The matching entity or null
	 */
	public T get() {
		return dao.asObject(this.exec());
	}

	public Query<T> in(Column colName, String... values) {
		// WHERE colName IN (?,?,?)
		if (values.length > 0) {
			String valueList = "(?";
			params.add(values[0]);
			for (int i = 1; i < values.length; i++) {
				valueList += ",?";
				params.add(values[i]);
			}
			valueList += ")";
			where.append(" AND " + colName + " IN " + valueList);
		}
		return this;
	}

	/**
	 * Executes the query and returns the result as a {@link java.util.List}.
	 *
	 * @see SQLiteDao#asList(Cursor)
	 * @return A List of matching entities or null
	 */
	public List<T> list() {
		return dao.asList(this.exec());
	}

	public Query<T> order(String...columns) {
		if (columns.length < 1) {
			throw new IllegalArgumentException();
		}
		StringBuilder orderBy = new StringBuilder(columns[0]);
		for (int i = 1; i < columns.length; i++) {
			String col = columns[i];
			orderBy.append(", " + col);
		}
		this.orderBy = orderBy.toString();
		return this;
	}

	/**
	 * Convert the params in each predicate to String[]
	 * used by the query methods
	 *
	 * @return String[] parameters
	 */
	String[] params() {
		return params.toArray(new String[params.size()]);
	}

	/**
	 * Convert the SQL conditions in each predicate by ANDing together
	 * into a single SQL WHERE clause with ? for each parameter
	 *
	 * @return String SQL WHERE clause
	 */
	String where() {
		if (where.length() > 0) {
			return where.substring(5).toString();
		}
		return null;
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm;

import java.io.FileNotFoundException;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils.InsertHelper;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import com.turbomanage.storm.csv.CsvTableReader;
import com.turbomanage.storm.csv.CsvTableWriter;
import com.turbomanage.storm.query.Query;

/**
 * Abstract base class for generated TableHelpers.
 * Contains methods related to database creation and upgrades which
 * are executed before the database is fully ready for use by the DAOs. Its
 * methods are invoked by {@link DatabaseHelper#onCreate(SQLiteDatabase)}. To
 * customize the SQL for a particular table, create your own {@link TableHelper}
 * that overrides one or more of these methods and modify
 * {@link DatabaseHelper#getTableHelpers()} to return an instance of you custom
 * TableHelper.
 *
 * @author David M. Chandler
 */
public abstract class TableHelper<T> {

	/**
	 * Marker interface for column enums
	 *
	 * @author David M. Chandler
	 */
	public interface Column{
		public abstract String asc();
		public abstract String desc();
	};

	private static final String TAG = TableHelper.class.getName();

	/**
	 * @return array of column names in declared order
	 */
	public abstract Column[] getColumns();

	/**
	 * Obtains an entity's ID field. This abstraction supports
	 * the optional @Id annotation.
	 *
	 * @param obj The entity
	 * @return long id
	 */
	public abstract long getId(T obj);

	/**
	 * Sets and entity's ID field. This abstraction supports
	 * the optional @Id annotation.
	 *
	 * @param obj The entity
	 * @param id
	 */
	public abstract void setId(T obj, long id);

	/**
	 * @return SQL table name
	 */
	public abstract String getTableName();

	/**
	 * @return CREATE TABLE statement
	 */
	protected abstract String createSql();

	/**
	 * @return DROP TABLE statement
	 */
	protected abstract String dropSql();

	/**
	 * Return SQL statement to execute when upgrading the table,
	 * probably ALTER TABLE
	 *
	 * @param oldVersion
	 * @param newVersion
	 * @return String SQL statement
	 */
	protected abstract String upgradeSql(int oldVersion, int newVersion);

	/**
	 * Extract from a cursor an array of String values in order of the Columns
	 * enum. Note that column order is not guaranteed when adding or removing
	 * entity fields, so these values are not necessarily in cursor order.
	 * This is used by the CsvWriter and is necessary mainly because
	 * Cursor.getString truncates doubles and blobs need to be Base64 encoded.
	 * Writes columns in order of the Columns enum, not their cursor position.
	 *
	 * @param c Cursor
	 * @return Map<String colName, String colValue>
	 */
	public abstract String[] getRowValues(Cursor c);

	/**
	 * Convert the String values for each column into their appropriate SQL
	 * types and bind to the insert statement using {@link InsertHelper}.
	 *
	 * @param insHelper
	 * @param rowValues
	 */
	public abstract void bindRowValues(InsertHelper insHelper, String[] rowValues);

	/**
	 * Populate an array with the entity's default values for each field obtained
	 * by creating a new instance of the entity. These values are used to fill in
	 * any missing columns when importing from CSV.
	 *
	 * @return
	 */
	public abstract String[] getDefaultValues();

	/**
	 * Populate a {@link ContentValues} object with the values of the supplied
	 * POJO by calling the appropriate getters and converters.
	 *
	 * @param obj
	 * @return ContentValues
	 */
	public abstract ContentValues getEditableValues(T obj);

	/**
	 * @return name of the ID column
	 */
	public abstract Column getIdCol();

	/**
	 * Create a new instance of a POJO by calling its setters with the values
	 * obtained from a {@link Cursor}.
	 *
	 * @param c
	 * @return a new instance
	 */
	public abstract T newInstance(Cursor c);

	/**
	 * Add conditions to a filter for each property of the example object
	 * which does not have its default value.
	 *
	 * @param builder
	 * @param exampleObj
	 * @return FilterBuilder ready to execute
	 */
	public abstract Query<T> buildFilter(Query<T> builder, T exampleObj);

	/**
	 * Creates the table that represents the associated entity.
	 *
	 * @param db
	 */
	protected void onCreate(SQLiteDatabase db) {
		db.execSQL(createSql());
	}

	/**
	 * Drops the table that represents the associated entity.
	 *
	 * @param db
	 */
	protected void onDrop(SQLiteDatabase db) {
		db.execSQL(dropSql());
	}

	/**
	 * Upgrades the table that represents the associated entity. This will
	 * typically be an ALTER TABLE statement.
	 *
	 * @param db
	 * @param oldVersion
	 * @param newVersion
	 */
	protected void onUpgrade(final SQLiteDatabase db, final int oldVersion,
			final int newVersion) {
		db.execSQL(upgradeSql(oldVersion, newVersion));
	}

	/**
	 * Backs up the current table to a CSV file.
	 *
	 * @param db
	 * @param ctx
	 * @param suffix
	 *
	 * @return true on success
	 */
	public boolean backup(SQLiteDatabase db, Context ctx, String suffix) {
		try {
			new CsvTableWriter(this).dumpToCsv(ctx, db, suffix);
		} catch (SQLException e) {
			if (e.getMessage().contains("no such table")) {
				Log.w(TAG, "Table " + this.getTableName() + " doesn't exist. This is expected if the table is new in this db version.");
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	/**
	 * Restores a table from a text file.
	 *
	 * @param db
	 * @param ctx
	 * @param suffix
	 */
	public void restore(SQLiteDatabase db, Context ctx, String suffix) {
		new CsvTableReader(this).importFromCsv(ctx, db, suffix);
	}

	/**
	 * Drops a table and recreates it.
	 *
	 * @param db
	 */
	protected void dropAndCreate(SQLiteDatabase db) {
		this.onDrop(db);
		this.onCreate(db);
	}

	/*
	 * Cursor wrapper methods which bind to primitive type columns
	 * and return the corresponding wrapper type which may be null
	 */

	protected byte[] getBlobOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getBlob(col);
	}

	protected Double getDoubleOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getDouble(col);
	}

	protected Float getFloatOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getFloat(col);
	}

	protected Integer getIntOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getInt(col);
	}

	protected Long getLongOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getLong(col);
	}

	protected Short getShortOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getShort(col);
	}

	protected String getStringOrNull(Cursor c, int col) {
		return c.isNull(col) ? null : c.getString(col);
	}

}


package com.turbomanage.storm.sample.activity;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;

import com.turbomanage.storm.sample.R;
import com.turbomanage.storm.sample.adapter.ContactAdapter;
import com.turbomanage.storm.sample.loader.ContactLoader;
import com.turbomanage.storm.sample.model.Contact;
import com.turbomanage.storm.sample.model.dao.ContactDao;

import java.util.List;

import de.timroes.android.listview.EnhancedListView;

public class ContactActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<List<Contact>> {

    private static final int REQUEST_CODE = 101;

    private EnhancedListView mListView;
	private ContactAdapter mAdapter;
    private ContactDao mContactDao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_contact);
        mListView = (EnhancedListView) findViewById(android.R.id.list);

        mContactDao = new ContactDao(this);

        mListView.setDismissCallback(new EnhancedListView.OnDismissCallback() {
            @Override
            public EnhancedListView.Undoable onDismiss(EnhancedListView enhancedListView, int i) {

                Contact contact = mAdapter.getItem(i);
                mContactDao.delete(contact.getId());
                mAdapter.remove(i);
                return null;
            }
        });

        mListView.enableSwipeToDismiss();
        getSupportLoaderManager().initLoader(1, null, this);
    }

	/**
     * The default content for this Activity has a TextView that is shown when
     * the list is empty. If you would like to change the text, call this method
     * to supply the text it should use.
     */
    public void setEmptyText(CharSequence emptyText) {
		if (mListView != null){
			View emptyView = mListView.getEmptyView();

			if (emptyText instanceof TextView) {
				((TextView) emptyView).setText(emptyText);
			}
		}
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        getMenuInflater().inflate(R.menu.contact, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        if(item.getItemId() == R.id.action_add) {

            startActivityForResult(new Intent(this, NewContactActivity.class), REQUEST_CODE);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        if(resultCode == NewContactActivity.RESULT_CODE_CREATED){

            getSupportLoaderManager().restartLoader(1, null, this);
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public Loader<List<Contact>> onCreateLoader(int id, Bundle args) {
        return new ContactLoader(this);
    }

    @Override
    public void onLoadFinished(Loader<List<Contact>> loader, List<Contact> data) {

        mAdapter = new ContactAdapter(this, data);
        mListView.setAdapter(mAdapter);
    }

    @Override
    public void onLoaderReset(Loader<List<Contact>> loader) {

        setEmptyText("No data");
    }
}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.csv;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.turbomanage.storm.TableHelper;
import com.turbomanage.storm.TableHelper.Column;

/**
 * Contains methods used to dump a table to a CSV file. Each
 * instance is associated with one table.
 *
 * @author David M. Chandler
 */
public class CsvTableWriter extends CsvTableReader {

	/**
	 * Constructor requires a corresponding {@link TableHelper} class.
	 * @param tableHelper
	 */
	@SuppressWarnings("rawtypes")
	public CsvTableWriter(TableHelper tableHelper) {
		super(tableHelper);
	}

	private String buildCsvRow(Cursor c) {
		StringBuilder sb = new StringBuilder();
		String[] values = th.getRowValues(c);
		for (String val : values) {
			sb.append(',');
			sb.append(CsvUtils.escapeCsv(val));
		}
		return sb.toString().substring(1);
	}

	private String buildHeaderRow() {
		// Write column names in first row
		StringBuilder sb = new StringBuilder();
        // TODO write old #, name, type from saved schema
		Column[] cols = th.getColumns();
		for (Column col : cols) {
			sb.append(',');
			sb.append(col.toString());
		}
		return sb.toString().substring(1);
	}

	/**
	 * Dumps a database table to a CSV file in the default location.
	 * Returns the number of rows written to the file.
	 *
	 * @param ctx
	 * @param db
	 * @param suffix
	 *
	 * @return count of rows in the exported file
	 */
	public int dumpToCsv(Context ctx, SQLiteDatabase db, String suffix) throws FileNotFoundException {
		int numRowsWritten = 0;
		Cursor c;
		String filename = getCsvFilename(db.getPath(), db.getVersion(), suffix);
		c = db.query(th.getTableName(), null, null, null, null, null, null);
		FileOutputStream fos;
		fos = ctx.openFileOutput(filename, 0);
		PrintWriter printWriter = new PrintWriter(fos);
		String headerRow = buildHeaderRow();
		printWriter.println(headerRow);
		for (boolean hasItem = c.moveToFirst(); hasItem; hasItem = c
				.moveToNext()) {
			String csv = buildCsvRow(c);
			printWriter.println(csv);
			numRowsWritten++;
		}
		printWriter.flush();
		printWriter.close();
		return numRowsWritten;
	}

}


package com.tri.persistence.jpql;

import com.tri.persistence.jpql.utils.StringUtils;

/**
 * Internal string list to hold a {@literal SQL} Select Clause items with
 * support for Constructor Expressions.
 */
public class SelectQueryClause extends QueryClause {

	/**
	 * Constructor
	 *
	 * @param queryBuilder
	 */
	SelectQueryClause(final QueryBuilder builder) {
		super(builder);
	}

	/**
	 * Adds an expression to this Select Clause with support for Constructor
	 * Expressions.
	 *
	 * @param constructorClass
	 *            class to be use for constructor expression, make shure it has
	 *            a constructor that fits to the given {@literal SQL} Select
	 *            Items
	 * @param expression
	 *            expression used as constructor parameter, will be passed
	 *            unchanged to the appropriated constructor of the given class
	 * @return the query builder
	 */
	public <T> QueryBuilder add(final Class<T> constructorClass,
			final String expression) {
		return add((String) null, constructorClass, expression);
	}

	/**
	 * Adds an expression to this Select Clause with support for Constructor
	 * Expressions.
	 *
	 * @param lhsStatement
	 *            left hand side of the statement, e.g. {@code DISTINCT}
	 * @param constructorClass
	 *            class to be use for Constructor Expression, make sure it has a
	 *            constructor that fits to the given {@literal SQL} Select Items
	 * @param expression
	 *            expression used as constructor parameter, will be passed
	 *            unchanged to the appropriated constructor of the given class
	 * @return the query builder
	 */
	public <T> QueryBuilder add(final String lhsStatement,
			final Class<T> constructorClass, final String expression) {

		StringBuilder itemBuilder = new StringBuilder();
		if (lhsStatement != null) {
			itemBuilder.append(lhsStatement);
			itemBuilder.append(" ");
		}
		itemBuilder.append("NEW ");
		itemBuilder.append(constructorClass.getName());
		itemBuilder.append("(");
		itemBuilder.append(expression);
		itemBuilder.append(")");

		items.add(itemBuilder.toString());

		return builder;

	}

	/**
	 * Adds expressions to this Select Clause with support for Constructor
	 * Expressions.
	 *
	 * @param constructorClass
	 *            class to be use for constructor expression, make shure it has
	 *            a constructor that fits to the given {@literal SQL} Select
	 *            Items
	 * @param expressions
	 *            constructor parameters, will be passed as comma separated list
	 *            to the appropriated constructor of the given class
	 * @return the query builder
	 */
	public <T> QueryBuilder add(final Class<T> constructorClass,
			final String... expressions) {
		return add((String) null, constructorClass, expressions);
	}

	/**
	 * Adds expressions to this Select Clause with support for Constructor
	 * Expressions.
	 *
	 * @param lhsStatement
	 *            left hand side of the statement, e.g. {@code DISTINCT}
	 * @param constructorClass
	 *            class to be use for constructor expression, make shure it has
	 *            a constructor that fits to the given {@literal SQL} Select
	 *            items
	 * @param expressions
	 *            constructor parameters, will be passed as comma separated list
	 *            to the appropriated constructor of the given class
	 * @return the query builder
	 */
	public <T> QueryBuilder add(final String lhsStatement,
			final Class<T> constructorClass, final String... expressions) {
		return add(lhsStatement, constructorClass,
				StringUtils.join(expressions));
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.csv;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * CSV utility methods. Does not implement the full CSV spec.
 * Values containing quotes or commas are enclosed in quotes
 * and quotes are escaped as double quotes.
 *
 * @author David M. Chandler
 */
public class CsvUtils {

	public static final char DELIMITER = ',';
	public static final char QUOTE = '"';
	public static final char CR = '\r';
	public static final char LF = '\n';
	public static final char[] CSV_SEARCH_CHARS = new char[] { DELIMITER,
			QUOTE, CR, LF };
	public static final String QUOTE_STR = String.valueOf(QUOTE);

	/**
	 * Returns a {@link String} for a CSV column enclosed in double
	 * quotes, if required.
	 *
	 * see Wikipedia
	 * and RFC 4180.
	 *
	 * @param str the input String, may be null
	 * @return the input String enclosed in double quotes if required, or null
	 */
	public static String escapeCsv(String str) {
		if (str == null)
			return "";
		if (containsNone(str, CSV_SEARCH_CHARS))
			return str;
		StringWriter out = new StringWriter();
		out.write(QUOTE);
		for (int i = 0; i < str.length(); i++) {
			char c = str.charAt(i);
			if (c == QUOTE)
				out.write(QUOTE);
			out.write(c);
		}
		out.write(QUOTE);
		return out.toString();
	}

	/**
	 * Removes enclosing quotes and unescapes double quotes
	 *
	 * @param str An escaped CSV value
	 * @return the original value
	 */
	public static String unescapeCsv(String str) {
		if (str == null)
			return null;
		if (!(str.charAt(0) == QUOTE && str.charAt(str.length() - 1) == QUOTE))
			return str;
		String quoteless = str.substring(1, str.length() - 1);
		return quoteless.replace(QUOTE_STR + QUOTE_STR, QUOTE_STR);
	}

	private static boolean containsNone(String str, char[] csvSearchChars) {
		if (str == null)
			return true;
		for (int i = 0; i < str.length(); i++)
			for (int j = 0; j < csvSearchChars.length; j++)
				if (str.charAt(i) == csvSearchChars[j])
					return false;
		return true;
	}

	/**
	 * Returns values from a CSV String.
	 *
	 * @param csvRow
	 * @return
	 */
	public static List<String> getValues(String csvRow) {
		List<String> values = new ArrayList<String>();
		StringReader in = new StringReader(csvRow);
		String value;
		try {
			value = nextValue(in);
			while (true) {
				values.add(value);
				value = nextValue(in);
			}
		} catch (IOException e) {
			// TODO handle case of final null value better?
			if (csvRow.lastIndexOf(',') == csvRow.length() - 1)
				values.add(null);
			return values;
		}
	}

	/**
	 * Obtains the next value from a {@link StringReader}.
	 *
	 * @param in
	 * @return An escaped value
	 * @throws IOException
	 */
	public static String nextValue(StringReader in) throws IOException {
		StringWriter w = new StringWriter();
		boolean inQuotedValue = false;
		boolean openQuote = false;
		int c = in.read();
		if (c == QUOTE)
			inQuotedValue = true;
		else if (c == DELIMITER) {
			return null;
		} else if (c >= 0) {
			w.write(c);
		} else {
			throw new IOException("End of line reached");
		}
		c = in.read();
		while (c >= 0) {
			if (c == QUOTE) {
				if (inQuotedValue) {
					if (openQuote) {
						openQuote = false;
						w.write(QUOTE);
					} else {
						openQuote = true;
					}
				} else
					// invalid
					return w.toString();
			} else if (c == DELIMITER) {
				if (openQuote)
					return w.toString();
				else if (inQuotedValue)
					w.write(c);
				else
					// invalid
					return w.toString();
			} else
				w.write(c);
			c = in.read();
		}
		return w.toString();
	}

	/**
	 * Parses a CSV row containing name=value pairs.
	 *
	 * @param csvPairs
	 * @return Map<name,value>
	 */
	public static Map<String,String> getAsMap(String csvPairs) {
		Map<String,String> map = new HashMap<String,String>();
		String[] pairs = csvPairs.split(",");
		for (String pair : pairs) {
			String[] split = pair.split("=");
			map.put(split[0], split[1]);
		}
		return map;
	}

	/**
	 * Returns a String containing a comma-separated list
	 * of name=value pairs from a map.
	 *
	 * @param map
	 * @return String csv
	 */
	public static String mapToCsv(Map<String,String> map) {
		StringBuilder sb = new StringBuilder();
		for (String key : map.keySet()) {
			sb.append(",");
			String val = map.get(key);
			sb.append(key + "=" + val);
		}
		return sb.toString().substring(1);
	}

}


package com.tri.persistence.jpql.utils;

import java.util.List;

public class StringUtils {

	/**
	 * Joins items of a text list, separating items by comma.
	 *
	 * @param list
	 *            list of text items
	 * @return joined list
	 */
	public static String join(final List<String> list) {

		// zero, empty or one element
		if (list == null) {
			return null;
		} else if (list.size() == 0) {
			return "";
		} else if (list.size() == 1) {
			return list.get(0);
		}

		// two or more elements
		final StringBuilder builder = new StringBuilder();
		for (String item : list) {
			if (builder.length() > 0) {
				builder.append(", ");
			}
			builder.append(item);
		}
		return builder.toString();

	}

	/**
	 * Joins items of a text array, separating items by comma.
	 *
	 * @param list
	 *            list of text items
	 * @return joined list
	 */
	public static String join(final String[] list) {

		// zero, empty or one element
		if (list == null) {
			return null;
		} else if (list.length == 0) {
			return "";
		} else if (list.length == 1) {
			return list[0];
		}

		// two or more elements
		final StringBuilder builder = new StringBuilder();
		for (String item : list) {
			if (builder.length() > 0) {
				builder.append(", ");
			}
			builder.append(item);
		}
		return builder.toString();

	}

	/**
	 * Returns true if the given string is null, empty or contains whitespace
	 * only.
	 *
	 * @param str
	 *            the String to check, may be null
	 * @return {@code true} if the String is null, empty or whitespace
	 */
	public static boolean isBlank(final String str) {
		final int strLength = (str == null) ? 0 : str.length();
		if (strLength == 0) {
			return true;
		}
		for (int i = 0; i < strLength; i++) {
			if (Character.isWhitespace(str.charAt(i)) == false) {
				return false;
			}
		}
		return true;
	}

}


package com.tri.persistence.jpql;

/**
 * {@code OrderColumn} objects are immutable.
 */
public class OrderColumn {

	private final String column;

	private final Order order;

	public OrderColumn(final String column) {
		this(column, null);
	}

	public OrderColumn(final String column, final Order order) {
		if (column == null) {
			throw new NullPointerException("Column required");
		}
		this.column = column;
		this.order = order;
	}

	public String getColumn() {
		return column;
	}

	public Order getOrder() {
		return order;
	}

	/**
	 * Renders this object as {@literal JPQL} fragment.
	 *
	 * @return OrderColumn rendered as String
	 */
	public String render() {
		StringBuilder builder = new StringBuilder(column);
		if (order != null) {
			builder.append(" ").append(getOrder().name());
		}
		return builder.toString();
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.apt.database;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.turbomanage.storm.apt.ClassModel;
import com.turbomanage.storm.apt.ProcessorLogger;
import com.turbomanage.storm.apt.StormEnvironment;
import com.turbomanage.storm.apt.entity.EntityModel;
import com.turbomanage.storm.csv.CsvUtils;

public class DatabaseModel extends ClassModel {

	private String dbName;
	private int dbVersion;
	private List<EntityModel> entities = new ArrayList<EntityModel>();
	private List<String> tableHelpers = new ArrayList<String>();

	public DatabaseModel(String dbName, int dbVersion, String helperClass) {
		super();
		this.setDbName(dbName);
		this.setDbVersion(dbVersion);
		parseQualifiedClassName(helperClass);
	}

	public String getDbName() {
		return dbName;
	}

	void setDbName(String dbName) {
		this.dbName = dbName;
	}

	public int getDbVersion() {
		return dbVersion;
	}

	void setDbVersion(int dbVersion) {
		this.dbVersion = dbVersion;
	}

	public String getDbHelperClass() {
		return getQualifiedClassName();
	}

	public String getFactoryName() {
		return capFirst(this.getDbName()) + "Factory";
	}

	public String getFactoryClass() {
		String pkg = this.getPackage();
		if (pkg == null || pkg.length() < 1)
			return getFactoryName();
		return this.getPackage() + "." + getFactoryName();
	}

	public List<String> getDaoClasses() {
		List<String> daoClasses = new ArrayList<String>();
		for (EntityModel em : entities) {
			daoClasses.add(em.getDaoPackage() + "." + em.getDaoName());
		}
		return daoClasses;
	}

	public void addEntity(EntityModel daoModel) {
		this.entities.add(daoModel);
		// Duplicate TableHelper info for use by index writer
		String thClassName = daoModel.getTableHelperClass();
		if (!this.tableHelpers.contains(thClassName)) {
			// Skip it if it's already in the list
			// TODO use a Set instead?
			this.tableHelpers.add(daoModel.getTableHelperClass());
		}
	}

	public String[] getTableHelpers() {
		return tableHelpers.toArray(new String[]{});
	}

	/**
	 * Populate the model of a database and its associated tables from
	 * a file in support of incremental compilation.
	 *
	 * @param reader
	 * @param logger
	 * @return DatabaseModel
	 * @throws IOException
	 */
	public static DatabaseModel readFromIndex(BufferedReader reader, ProcessorLogger logger) throws IOException {
		String dbInfo = reader.readLine();
		logger.info(dbInfo);
		Map<String, String> props = CsvUtils.getAsMap(dbInfo);
		String dbName = props.get("dbName");
		int dbVersion = Integer.parseInt(props.get("dbVersion"));
		String helperClass = props.get("helperClass");
		DatabaseModel dbModel = new DatabaseModel(dbName, dbVersion, helperClass);
		// read TableHelpers
		List<String> tables = new ArrayList<String>();
		String th = reader.readLine();
		while (th != null && !th.equals(StormEnvironment.END_DATABASE)) {
			tables.add(th);
			th = reader.readLine();
		}
		dbModel.tableHelpers = tables;
		return dbModel;
	}

	/**
	 * Write the database info and associated tables to a file
	 * in support of incremental compilation.
	 *
	 * @param out PrintWriter
	 */
	public void writeToIndex(PrintWriter out) {
		out.println(StormEnvironment.BEGIN_DATABASE);
		Map<String,String> dbMap = new HashMap<String,String>();
		dbMap.put("dbName", this.getDbName());
		dbMap.put("dbVersion", String.valueOf(this.getDbVersion()));
		dbMap.put("helperClass", this.getQualifiedClassName());
		String dbInfo = CsvUtils	.mapToCsv(dbMap);
		out.println(dbInfo);
		// write TableHelpers
		for (String th: this.tableHelpers) {
			out.println(th);
		}
		out.println(StormEnvironment.END_DATABASE);
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.apt.entity;

import com.turbomanage.storm.SQLiteDao;
import com.turbomanage.storm.api.Entity;
import com.turbomanage.storm.api.Id;
import com.turbomanage.storm.apt.BaseDaoModel;
import com.turbomanage.storm.apt.ClassProcessor;
import com.turbomanage.storm.apt.SqlUtil;
import com.turbomanage.storm.apt.StormEnvironment;
import com.turbomanage.storm.apt.converter.ConverterModel;
import com.turbomanage.storm.apt.database.DatabaseModel;
import com.turbomanage.storm.exception.TypeNotSupportedException;

import java.util.List;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.persistence.Transient;

public class EntityProcessor extends ClassProcessor {

    private static final String TAG = EntityProcessor.class.getName();
    private EntityModel entityModel;

    public EntityProcessor(Element el, StormEnvironment stormEnv) {
        super(el, stormEnv);
    }

    @Override
    public EntityModel getModel() {
        return this.entityModel;
    }

    @Override
    public void populateModel() {
        // TODO make more elegant
        Entity entity = this.typeElement.getAnnotation(Entity.class);
        if (entity != null) {
            this.entityModel = new EntityModel(entity);
        } else {
            javax.persistence.Entity jpaEntity = this.typeElement.getAnnotation(javax.persistence.Entity.class);
            if (jpaEntity != null) {
                this.entityModel = new EntityModel(jpaEntity);
            }
        }
        super.populateModel();
        this.entityModel.addImport(getQualifiedClassName());
        validateTableName(entityModel.getTableName());
        chooseDatabase(entityModel.getDbName());
        chooseBaseDao(entity);
        readFields(typeElement);
        inspectId();
        // TODO Verify >1 column. If only ID col, insert() will fail
    }

    private void validateTableName(String tableName) {
        if (tableName != null && tableName.length() > 0) {
            this.entityModel.setTableName(tableName);
        } else {
            this.entityModel.setTableName(getClassName());
        }
        if (!SqlUtil.isValidIdentifier(this.entityModel.getTableName())) {
            abort(tableName + " is not a valid identifier. Enclose SQL keywords with [].");
        }
    }

    protected void chooseBaseDao(Entity entity) {
        BaseDaoModel baseDao = getBaseDaoClass(entity);
        this.entityModel.setBaseDaoClass(baseDao);
    }

    protected void chooseDatabase(String dbName) {
        DatabaseModel defaultDb = stormEnv.getDefaultDb();
        if (dbName != null && dbName.length() > 0) {
            // Add db to entity model and vice versa
            DatabaseModel db = stormEnv.getDbByName(dbName);
            if (db != null) {
                this.entityModel.setDatabase(db);
            } else {
                abort("There is no @Database named " + dbName);
            }
        } else if (defaultDb != null) {
            this.entityModel.setDatabase(defaultDb);
        } else {
            abort("You must define at least one @Database");
        }
    }

    @Override
    protected void inspectField(VariableElement field) {
        Set<Modifier> modifiers = field.getModifiers();
        boolean hasId = (field.getAnnotation(Id.class) != null);
        String fieldName = field.getSimpleName().toString();
        if (!SqlUtil.isValidIdentifier(field.getSimpleName().toString())) {
            abort(fieldName + " is not a valid SQL column name.");
        }
        if (!modifiers.contains(Modifier.TRANSIENT) && field.getAnnotation(Transient.class) == null && !modifiers.contains(Modifier.STATIC)) {
            String javaType = getFieldType(field);
            if (TypeKind.DECLARED.equals(field.asType().getKind())) {
                DeclaredType type = (DeclaredType) field.asType();
                TypeElement typeElement = (TypeElement) type.asElement();
                TypeMirror superclass = typeElement.getSuperclass();
                if (ElementKind.ENUM.equals(typeElement.getKind())) {
                    if (hasId) {
                        abort("@Id invalid on enums", field);
                    } else {
                        FieldModel fm = new FieldModel(field.getSimpleName().toString(), javaType, true, stormEnv.getConverterForType("java.lang.Enum"));
                        entityModel.addField(fm);
                    }
                    return;
                }
            }
            // Verify supported type
            try {
                ConverterModel converter = stormEnv.getConverterForType(javaType);
                FieldModel f = new FieldModel(field.getSimpleName().toString(), javaType, false, converter);
                if (hasId) {
                    if (entityModel.getIdField() == null) {
                        if ("long".equals(f.getJavaType())) {
                            entityModel.setIdField(f);
                        } else {
                            abort("@Id field must be of type long", field);
                        }
                    } else {
                        abort("Duplicate @Id", field);
                    }
                }
                entityModel.addField(f);
            } catch (TypeNotSupportedException e) {
                stormEnv.getLogger().error(TAG + "inspectField", e, field);
            } catch (Exception e) {
                stormEnv.getLogger().error(TAG, e, field);
            }
            // TODO verify getter + setter
        } else if (hasId) {
            abort("@Id fields cannot be transient", field);
        }
    }

    /**
     * Verifies that the entity has exactly one id field of type long.
     */
    private void inspectId() {
        if (entityModel.getIdField() == null) {
            // Default to field named "id"
            List<FieldModel> fields = entityModel.getFields();
            for (FieldModel f : fields) {
                if (EntityModel.DEFAULT_ID_FIELD.equals(f.getFieldName())) {
                    entityModel.setIdField(f);
                }
            }
        }
        FieldModel idField = entityModel.getIdField();
        if (idField != null && "long".equals(idField.getJavaType())) {
            return;
        } else {
            abort("Entity must contain a field named id or annotated with @Id of type long");
        }
    }

    /**
     * Trying to get Class<?> from an annotation raises an exception
     * see http://stackoverflow.com/questions/7687829/java-6-annotation-processing-getting-a-class-from-an-annotation
     */
    private static TypeMirror getBaseDaoTypeMirror(Entity entity) {
        if(entity != null) {
            try {
                entity.baseDaoClass();
            } catch (MirroredTypeException mte) {
                return mte.getTypeMirror();
            }
        }
        return null;
    }

    /**
     * Builds a BaseDaoModel from the class passed as attribute baseDaoClass of the annotation Entity
     * @param entity
     * @return BaseDaoModel containing the package name + Class name
     */
    private static BaseDaoModel getBaseDaoClass(Entity entity) {
        String qualifiedName = SQLiteDao.class.getName();
        TypeMirror typeMirror = getBaseDaoTypeMirror(entity);
        if(typeMirror != null) qualifiedName = typeMirror.toString();
        return new BaseDaoModel(qualifiedName);
    }
}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.
 ******************************************************************************/
/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.turbomanage.storm.apt.entity;

import com.turbomanage.storm.apt.converter.ConverterModel;
import com.turbomanage.storm.types.TypeConverter.SqlType;

/**
 * Model of a persisted field
 *
 * @author David M. Chandler
 */
public class FieldModel {

	private String fieldName, colName, javaType;
	private boolean isEnum, isEntityId;
	private ConverterModel converter;

	public FieldModel(String fieldName, String javaType, boolean isEnum, ConverterModel converter) {
		this.fieldName = fieldName;
		this.javaType = javaType;
		this.isEnum = isEnum;
		this.converter = converter;
		// TODO or @ColumnName
		this.colName = fieldName;
	}

	public String getFieldName() {
		return fieldName;
	}

	public String getColName() {
		// CursorAdapter requires lowercase _id col
		if (EntityModel.ID_COL.equals(colName)) {
			return EntityModel.ID_COL;
		} else {
			return colName.toUpperCase();
		}
	}

	void setColName(String colName) {
		this.colName = colName;
	}

	public String getJavaType() {
		return javaType;
	}

	private ConverterModel getConverter() {
		return this.converter;
	}

	/**
	 * Convenience method for more compact Dao templates. Returns the name of
	 * this field's converter class sans package name.
	 *
	 * @return
	 */
	public String getConverterName() {
		return getConverter().getClassName();
	}

	/**
	 * Fully-qualified name of the converter class for this field.
	 *
	 * @return String classname
	 */
	public String getQualifiedConverterClass() {
		return getConverter().getQualifiedClassName();
	}

	/**
	 * Morph bind type like INT ==> Int so it can be used in a Cursor getXxx
	 * method name. Never called at runtime.
	 *
	 * @return
	 */
	public String getBindType() {
		String bindType = getConverter().getBindType().name();
		return bindType.charAt(0) + bindType.toLowerCase().substring(1);
	}

	public String getSqlType() {
		if (this.isEntityId)
			return "INTEGER PRIMARY KEY AUTOINCREMENT";
		else if (isEnum) {
			return SqlType.TEXT.name();
		}
		return this.converter.getSqlType().name();
	}

	public String getSetter() {
		return "set" + capFirst(fieldName);
	}

	public String getGetter() {
		if ("boolean".equals(javaType))
			return "is" + capFirst(fieldName);
		else
			return "get" + capFirst(fieldName);
	}

	/**
	 * Capitalizes the first letter to create a valid getter/setter name.
	 *
	 * @param String
	 * @return String
	 */
	private String capFirst(String anyName) {
		// obscure Java convention:
		// if second letter capitalized, leave it alone
		if (anyName.length() > 1)
			if (anyName.charAt(1) >= 'A' && anyName.charAt(1) <= 'Z')
				return anyName;
		String capFirstLetter = anyName.substring(0, 1).toUpperCase();
		return capFirstLetter + anyName.substring(1);
	}

	public boolean isNullable() {
		return javaType.contains(".") || javaType.contains("[]");
	}

	public boolean isEnum() {
		return isEnum;
	}

	boolean isEntityId() {
		return isEntityId;
	}

	void setEntityId(boolean isEntityId) {
		this.isEntityId = isEntityId;
	}
}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.csv;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;

import android.content.Context;
import android.database.DatabaseUtils;
import android.database.DatabaseUtils.InsertHelper;
import android.database.sqlite.SQLiteDatabase;

import com.turbomanage.storm.TableHelper;

/**
 * Contains methods used to restore a table from a CSV file. Each
 * instance is associated with one table.
 *
 * @author David M. Chandler
 */
public class CsvTableReader {

	@SuppressWarnings("rawtypes")
	protected TableHelper th;
	private InsertHelper insertHelper;

	/**
	 * Constructor requires a corresponding {@link TableHelper} class.
	 *
	 * @param tableHelper
	 */
	@SuppressWarnings("rawtypes")
	public CsvTableReader(TableHelper tableHelper) {
		this.th = tableHelper;
	}

	protected String getCsvFilename(String dbPath, int dbVersion, String suffix) {
		String[] path = dbPath.split("/");
		String dbName = path[path.length-1];
		String tableName = th.getTableName();
		return String.format("%s.v%d.%s%s", dbName, dbVersion, tableName, suffix);
	}

	/**
	 * Attempts to import a database table from a CSV file in the default
	 * location.
	 *
	 * @param ctx
	 * @param db
	 * @param suffix
	 *
	 * @return count of rows imported or -1 if error
	 */
	public int importFromCsv(Context ctx, SQLiteDatabase db, String suffix) {
		String filename = getCsvFilename(db.getPath(), db.getVersion(), suffix);
		FileInputStream fileInputStream;
		try {
			fileInputStream = ctx.openFileInput(filename);
			return importFromCsv(db, fileInputStream);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			return -1;
		}
	}

	/**
	 * Attempts to import a database table from an {@link InputStream}
	 * formatted as a CSV file. Does all inserts in a single transaction
	 * for efficiency and so that all inserts will succeed or fail
	 * together.
	 *
	 * @param db
	 * @param is
	 * @return count of rows imported or -1 if error
	 */
	public int importFromCsv(SQLiteDatabase db, InputStream is) {
		int numInserts = 0;
		db.beginTransaction();
		insertHelper = new DatabaseUtils.InsertHelper(db,
				th.getTableName());
		try {
			InputStreamReader isr = new InputStreamReader(is);
			BufferedReader reader = new BufferedReader(isr);
			String headerRow = reader.readLine();
			String csvRow = reader.readLine();
			while (csvRow != null) {
				long rowId = parseAndInsertRow(csvRow);
				if (rowId == -1L) {
					throw new RuntimeException("Error after row " + numInserts);
				}
				numInserts++;
				csvRow = reader.readLine();
			}
			db.setTransactionSuccessful();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			db.endTransaction();
		}
		return numInserts;
	}

	/**
	 * Parse the values in a CSV row, map them to the table column
	 * order, and insert. Uses {@link InsertHelper} so it may be
	 * called repeatedly within a transaction for max performance.
	 *
	 * @param csvRow
	 * @return row ID of the newly inserted row or -1
	 */
	private long parseAndInsertRow(String csvRow) {
		List<String> textValues = CsvUtils.getValues(csvRow);
		insertHelper.prepareForInsert();
        String[] rowValues = textValues.toArray(new String[textValues.size()]);
		th.bindRowValues(insertHelper, rowValues);
		return insertHelper.execute();
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;

import com.turbomanage.storm.DatabaseHelper.UpgradeStrategy;
import com.turbomanage.storm.api.Database;
import com.turbomanage.storm.api.DatabaseFactory;

/**
 * Default implementation of the SQLiteOpenHelper. Projects should extend this
 * class and annotate it with {@link Database}. Subclasses may override the
 * default onCreate and onUpgrade methods.
 *
 * @author David M. Chandler
 */
public abstract class DatabaseHelper extends SQLiteOpenHelper {

	public enum UpgradeStrategy {
		/**
		 * Drop and recreate all tables without preserving existing data.
		 */
		DROP_CREATE,
		/**
		 * Drop and recreate all tables with attempt to preserve existing data
		 * by first backing up to CSV and restoring from the same.
		 */
		BACKUP_RESTORE,
		/**
		 * Custom upgrade strategy implemented by overriding
		 * {@link DatabaseHelper#getUpgradeStrategy()}.
		 */
		UPGRADE
	}

	private DatabaseFactory dbFactory;

	protected Context mContext;

	/**
	 * The constructor that should be overridden. Simply invoke
	 * the super constructor.
	 *
	 * @param ctx
	 * @param dbFactory
	 */
	public DatabaseHelper(Context ctx, DatabaseFactory dbFactory) {
		this(ctx, dbFactory.getName(), null, dbFactory.getVersion());
		this.mContext = ctx;
		this.setDbFactory(dbFactory);
	}

	/**
	 * Don't extend this one. You need dbFactory from the other constructor.
	 *
	 * @param ctx
	 * @param dbName
	 * @param cursorFactory
	 * @param dbVersion
	 */
	private DatabaseHelper(Context ctx, String dbName,
			CursorFactory cursorFactory, int dbVersion) {
		super(ctx, dbName, null, dbVersion);
	}

	/**
	 * Subclasses must implement to select what to do
	 * when you upgrade the app's database version. To implement
	 * your own upgrade strategy, return {@link UpgradeStrategy}
	 * and override the upgrade() method in this class.
	 *
	 * @return UpgradeStrategy
	 */
	public abstract UpgradeStrategy getUpgradeStrategy();

	/**
	 * Provides a reference to the {@link Context} which was used
	 * to initialize this {@link SQLiteOpenHelper}.
	 *
	 * @return Context
	 */
	public Context getContext() {
		return this.mContext;
	}

	/**
	 * Hook to replace any of the generated TableHelpers with your own.
	 *
	 * @return
	 */
	protected TableHelper[] getTableHelpers() {
		return getDbFactory().getTableHelpers();
	}

	/**
	 * Calls {@link TableHelper#onCreate(SQLiteDatabase)} for each TableHelper.
	 *
	 * @see android.database.sqlite.SQLiteOpenHelper#onCreate(android.database.sqlite.SQLiteDatabase)
	 */
	@Override
	public void onCreate(SQLiteDatabase db) {
		for (TableHelper th : getTableHelpers()) {
			th.onCreate(db);
		}
	}

	/**
	 * Calls a method on each TableHelper depending on the {@link UpgradeStrategy}.
	 * In order to prevent recursive calls to getDatabase(), this method must pass
	 * the db parameter through to any other methods that need it.
	 *
	 * @see Issue 11
	 * @see android.database.sqlite.SQLiteOpenHelper#onUpgrade(android.database.sqlite.SQLiteDatabase, int, int)
	 */
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		switch (getUpgradeStrategy()) {
		case DROP_CREATE:
			this.dropAndCreate(db);
			break;
		case BACKUP_RESTORE:
			this.backupAndRestore(this.getContext(), db);
			break;
		case UPGRADE:
			this.upgrade(db, oldVersion, newVersion);
			break;
		}
	}

	/**
	 * Calls {@link TableHelper#onUpgrade(SQLiteDatabase, int, int)} for
	 * each TableHelper. Override this method to implement your own
	 * upgrade strategy.
	 *
	 * @param db
	 * @param oldVersion
	 * @param newVersion
	 */
	public void upgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		for (TableHelper th : getTableHelpers()) {
			th.onUpgrade(db, oldVersion, newVersion);
		}
	}

	/**
	 * Backs up all tables to CSV, drops and recreates them, then restores
	 * them from CSV. By default, creates CSV filenames with no suffix. You
	 * could override this method to supply a timestamp suffix (make sure it's
	 * the same for both backup and restore), but beware that this could
	 * cause backup files to proliferate. Ideally, this method should
	 * clean up backup files after the database has been restored.
	 *
	 * @param ctx
	 * @param db
	 */
	public void backupAndRestore(Context ctx, SQLiteDatabase db) {
		if (backupAllTablesToCsv(ctx, db, null)) {
			dropAndCreate(db);
			restoreAllTablesFromCsv(ctx, db, null);
		} else {
			throw new RuntimeException("Backup of " + getDatabaseName() + " failed, aborting upgrade");
		}
	}

	/**
	 * Drops and recreates all tables.
	 * @param db
	 */
	public void dropAndCreate(SQLiteDatabase db) {
		for (TableHelper th : getTableHelpers()) {
			th.dropAndCreate(db);
		}
	}

	/**
	 * Backup all tables to CSV files, one per table
	 *
	 * @param ctx
	 * @param db
	 * @param suffix Optional filename suffix, none if null
	 *
	 * @return True if all tables completed without errors
	 */
	public boolean backupAllTablesToCsv(Context ctx, SQLiteDatabase db, String suffix) {
		boolean allSucceeded = true;
		for (TableHelper table : getTableHelpers()) {
			allSucceeded &= table.backup(db, ctx, suffix);
		}
		return allSucceeded;
	}

	/**
	 * Restore all tables from CSV files, one per table
	 *
	 * @param ctx
	 * @param db
	 * @param suffix Optional filename suffix, none if null
	 */
	public void restoreAllTablesFromCsv(Context ctx, SQLiteDatabase db, String suffix) {
		for (TableHelper table : getTableHelpers()) {
			table.restore(db, ctx, suffix);
		}
	}

	/**
	 * Returns the {@link DatabaseFactory} from which this instance
	 * was obtained.
	 *
	 * @return DatabaseFactory
	 */
	public DatabaseFactory getDbFactory() {
		return dbFactory;
	}

	private void setDbFactory(DatabaseFactory dbFactory) {
		this.dbFactory = dbFactory;
	}

}


/*******************************************************************************
 * Copyright 2012 Google, Inc.
 *
 * 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.turbomanage.storm.apt;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.annotation.processing.Filer;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import com.turbomanage.storm.apt.converter.ConverterModel;
import com.turbomanage.storm.apt.database.DatabaseModel;
import com.turbomanage.storm.exception.TypeNotSupportedException;
import com.turbomanage.storm.types.BlobConverter;
import com.turbomanage.storm.types.BooleanConverter;
import com.turbomanage.storm.types.ByteConverter;
import com.turbomanage.storm.types.CharConverter;
import com.turbomanage.storm.types.DateConverter;
import com.turbomanage.storm.types.DoubleConverter;
import com.turbomanage.storm.types.EnumConverter;
import com.turbomanage.storm.types.FloatConverter;
import com.turbomanage.storm.types.IntegerConverter;
import com.turbomanage.storm.types.LongConverter;
import com.turbomanage.storm.types.ShortConverter;
import com.turbomanage.storm.types.StringConverter;
import com.turbomanage.storm.types.TypeConverter;

/**
 * Top-level container for the source code models populated by
 * {@link MainProcessor} and its subsidiaries.
 *
 * @author David M. Chandler
 */
public class StormEnvironment {

	public static final String BEGIN_DATABASE = ":DB_START";
	public static final String END_DATABASE = ":DB_END";
	public static final String BEGIN_CONVERTERS = ":CONV_START";
	public static final String END_CONVERTERS = ":CONV_END";
	private static final String ENV_FILE = "stormEnv";
	private ProcessorLogger logger;
	private Map<String,DatabaseModel> dbModels = new TreeMap<String,DatabaseModel>();
	private List<ConverterModel> converters = new ArrayList<ConverterModel>();
	private Map<String, ConverterModel> typeMap = new HashMap<String, ConverterModel>();

	StormEnvironment(ProcessorLogger logger) {
		this.logger = logger;
		addBuiltInConverters();
	}

	private void addBuiltInConverters() {
		this.addConverter(new ConverterModel(new BlobConverter()));
		this.addConverter(new ConverterModel(new BooleanConverter()));
		this.addConverter(new ConverterModel(new ByteConverter()));
		this.addConverter(new ConverterModel(new CharConverter()));
		this.addConverter(new ConverterModel(new DateConverter()));
		this.addConverter(new ConverterModel(new DoubleConverter()));
		this.addConverter(new ConverterModel(new EnumConverter()));
		this.addConverter(new ConverterModel(new FloatConverter()));
		this.addConverter(new ConverterModel(new IntegerConverter()));
		this.addConverter(new ConverterModel(new LongConverter()));
		this.addConverter(new ConverterModel(new ShortConverter()));
		this.addConverter(new ConverterModel(new StringConverter()));
	}

	public ProcessorLogger getLogger() {
		return this.logger;
	}

	void addDatabase(DatabaseModel dbModel) {
		dbModels.put(dbModel.getQualifiedClassName(), dbModel);
	}

	Collection<DatabaseModel> getDbModels() {
		return dbModels.values();
	}

	public DatabaseModel getDbByName(String helperClass) {
		return dbModels.get(helperClass);
	}

	public DatabaseModel getDefaultDb() {
		if (dbModels.size() > 0) {
			return dbModels.values().iterator().next();
		}
		return null;
	}

	/**
	 * Read the current model state from a file in support of incremental
	 * compilation. This is necessary because the annotation processor has
	 * access to only classes which have been annotated (and any resulting
	 * generated classes on subsequent rounds), but DatabaseHelper classes
	 * aren't available when doing incremental compilation on a new @Entity.
	 *
	 * @see http
	 *      ://stackoverflow.com/questions/10585665/how-can-i-examine-the-whole
	 *      -source-tree-with-an-annotation-processor
	 *      https://github.com/sentinelt/evo-classindex
	 *
	 * @param Filer
	 *            used by the annotation processor
	 */
	void readIndex(Filer filer) {
		StandardLocation location = StandardLocation.SOURCE_OUTPUT;
		FileObject indexFile;
		try {
			indexFile = filer.getResource(location, "com.turbomanage.storm",
					ENV_FILE);
			logger.info("Reading index " + indexFile.toUri());
			// indexFile.openReader() not implemented on all platforms
			Reader fileReader = new InputStreamReader(indexFile.openInputStream());
			BufferedReader reader = new BufferedReader(fileReader);
			String line = reader.readLine(); // BEGIN_CONVERTERS
			line = reader.readLine();
			while (line != null && !line.startsWith(END_CONVERTERS)) {
				ConverterModel converter = ConverterModel.readFromIndex(line, logger);
				this.addConverter(converter);
				line = reader.readLine();
			}
			line = reader.readLine();
			while (line != null && line.startsWith(BEGIN_DATABASE)) {
				DatabaseModel dbModel = DatabaseModel.readFromIndex(reader, logger);
				this.addDatabase(dbModel);
				line = reader.readLine();
			}
			reader.close();
		} catch (IOException e) {
			// gulp--only way to catch not yet existing file on first run
		}
	}

	/**
	 * Write the current model state to a file in support of incremental
	 * compilation.
	 *
	 * @see StormEnvironment#readIndex(Filer)
	 *
	 * @param Filer
	 *            used by the annotation processor
	 */
	void writeIndex(Filer filer) {
		StandardLocation location = StandardLocation.SOURCE_OUTPUT;
		FileObject indexFile;
		try {
			indexFile = filer.createResource(location, "com.turbomanage.storm",
					ENV_FILE);
			OutputStream fos = indexFile.openOutputStream();
			PrintWriter out = new PrintWriter(fos);
			// Dump converters
			out.println(BEGIN_CONVERTERS);
			for (ConverterModel converter : converters) {
				converter.writeToIndex(out);
			}
			out.println(END_CONVERTERS);
			// Dump databases
			for (DatabaseModel dbModel : dbModels.values()) {
				dbModel.writeToIndex(out);
			}
			out.close();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Register a custom {@link TypeConverter} for a given data (field) type.
	 * This method is called at compile time by the annotation processor. In
	 * order for the TypeConverter to be visible, it must be in a jar on the
	 * client project's annotation factory classpath.
	 *
	 * @param converter
	 * @return true if successful, false if converter was already registered for
	 *         type
	 */
	public boolean addConverter(ConverterModel converter) {
		if (converters.contains(converter))
			return true;
		for (String type : converter.getConvertibleTypes()) {
			// TODO what if already put the 1st type?
			if (typeMap.containsKey(type)
					&& !typeMap.get(type).equals(converter))
				return false;
			typeMap.put(type, converter);
		}
		converters.add(converter);
		logger.info("Added " + converter.getQualifiedClassName());
		return true;
	}

	public ConverterModel getConverterForType(String javaType) {
		ConverterModel converter = typeMap.get(javaType);
		if (converter != null)
			return converter;
		throw new TypeNotSupportedException(
				"Could not find converter for type " + javaType);
	}

	public List<ConverterModel> getConverters() {
		return converters;
	}

}


package com.tri.persistence.jpql;

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

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;

import com.tri.persistence.jpql.utils.StringUtils;

/**
 *

 * {@literal JPQL} query builder for building complex dynamic database queries
 * and statements in {@literal JPQL} without getting cluttered with complex
 * String building-If-Else fragments. The builder has a fluent {@literal API} to
 * provide for more readable code. It is just an query/statement string builder
 * and has no knowledge of any {@literal SQL} semantic.
 *

 *

 * Usage
 *

 *

 * Simple query:
 *

 *
 *

 * QueryBuilder builder = new QueryBuilder();
 * builder.select.add("e");
 * builder.from.add("Entity e");
 * builder.where.add("e.fieldname=:fieldname");
 * builder.setParameter("fieldname", "xyz");
 * TypedQuery<Long> query = builder.createQuery(entityManager, Entity.class)
 * return query.getResultList();
 *

 *

 * Same query using fluent api:
 *

 *
 *

 * QueryBuilder builder = new QueryBuilder().select.add("e");
 * 	.from.add("Entity e").where.add("e.fieldname1=:fieldname2")
 * 	.add("e.fieldname2=:fieldname2");
 * return builder.setParameter("fieldname1", "xyz")
 * 	.setParameter("fieldname2", "xyz")
 * 	.createQuery(entityManager, Entity.class).getResultList();
 *

 *
 *

 * A dynamic query:
 *

 *
 *

 * String searchCriteria = "xyz";
 * QueryBuilder builder = QueryBuilder().select.add("e");
 * 	.from.add("Entity e").where.add("e.fieldname1=:fieldname2");
 * builder.setParameter("fieldname1", "xyz");
 * if (searchCriteria != null) {
 * 	builder.where.add("e.fieldname2=:fieldName2")
 * 	builder.setParameter("fieldname2", searchCriteria);
 * }
 * return builder.createQuery(entityManager, Entity.class).getResultList();
 *

 *

 * Also check the helper methods of class {@link WhereItems}, as these methods
 * are most needed to build the Where Clause of an {@literal SQL} statement.
 *

 *
 * @author khennig@pobox.com
 */
public class QueryBuilder implements Cloneable {

	public SelectQueryClause select;

	public StatementItem update;

	public StatementItem deleteFrom;

	public QueryClause set;

	public QueryClause from;

	public WhereAnd where;

	public QueryClause order;

	public QueryClause group;

	private List<Parameter<?>> parameters;

	private QueryBuilder parent;

	/**
	 * Constructor
	 */
	public QueryBuilder() {
		select = new SelectQueryClause(this);
		update = new StatementItem(this);
		deleteFrom = new StatementItem(this);
		set = new QueryClause(this);
		from = new QueryClause(this);
		where = new WhereAnd(this);
		order = new QueryClause(this);
		group = new QueryClause(this);
		parameters = new ArrayList<Parameter<?>>();
	}

	@Override
	public Object clone() throws CloneNotSupportedException {
		final QueryBuilder clone = (QueryBuilder) super.clone();
		// TODO: Evaluate: Implement: null-out parent

		// deep copy
		clone.select = (SelectQueryClause) select.clone();
		clone.select.setBuilder(this);

		clone.update = (StatementItem) update.clone();
		clone.update.setBuilder(this);

		clone.deleteFrom = (StatementItem) deleteFrom.clone();
		clone.deleteFrom.setBuilder(this);

		clone.set = (QueryClause) set.clone();
		clone.set.setBuilder(this);

		clone.from = (QueryClause) from.clone();
		clone.from.setBuilder(this);

		clone.where = (WhereAnd) where.clone();
		clone.where.setBuilder(this);

		clone.order = (QueryClause) order.clone();
		clone.order.setBuilder(this);

		clone.group = (QueryClause) group.clone();
		clone.group.setBuilder(this);

		clone.parameters = new ArrayList<Parameter<?>>(parameters);

		return clone;
	}

	protected void setParent(final QueryBuilder parent) {
		this.parent = parent;
	}

	/**
	 * Returns the parent builder.
	 *
	 * @return parent builder or null if there is none
	 */
	public QueryBuilder parent() {
		return parent;
	}

	/**
	 * Returns a JPA Query with all previously specified parameters added.
	 *
	 * @param manager
	 *            JPA EntityManager
	 * @return JPA Query
	 */
	public Query createQuery(final EntityManager manager) {
		if (manager == null) {
			throw new NullPointerException("Entity Manager required");
		}

		final Query query = manager.createQuery(render());

		for (Parameter<?> parameter : parameters) {
			parameter.apply(query);
		}

		return query;
	}

	/**
	 * Returns a JPA TypedQuery with all previously specified parameters added.
	 *
	 * @param manager
	 *            JPA EntityManager
	 * @param type
	 *            the type of the query result
	 * @return JPA Query
	 */
	public <T> TypedQuery<T> createQuery(final EntityManager manager,
			Class<T> type) {
		if (manager == null) {
			throw new NullPointerException("Entity Manager required");
		}

		TypedQuery<T> result = manager.createQuery(render(), type);

		for (Parameter<?> parameter : parameters) {
			parameter.apply(result);
		}

		return result;
	}

	/**
	 * Adds the given parameter to the builder. All parameters will added to the
	 * JPA Query returned by a call to {@link #createQuery(EntityManager)} or
	 * {@link #createQuery(EntityManager, Class)}.
	 *
	 * @param name
	 *            parameter name
	 * @param value
	 *            parameter value
	 * @param temporalType
	 *            temporal type
	 * @return builder reference, fluent api support
	 */
	public QueryBuilder setParameter(final String name, final Calendar value,
			TemporalType temporalType) {
		return setParameter(new ParameterCalendar(name, value, temporalType));
	}

	/**
	 * Adds the given parameter to the builder. All parameters will added to the
	 * JPA Query returned by a call to {@link #createQuery(EntityManager)} or
	 * {@link #createQuery(EntityManager, Class)}.
	 *
	 * @param name
	 *            parameter name
	 * @param value
	 *            parameter value
	 * @param temporalType
	 *            temporal type
	 * @return builder reference, fluent api support
	 */
	public QueryBuilder setParameter(final String name, final Date value,
			final TemporalType temporalType) {
		return setParameter(new ParameterDate(name, value, temporalType));
	}

	/**
	 * Adds the given parameter to the builder. All parameters will added to the
	 * JPA Query returned by a call to {@link #createQuery(EntityManager)} or
	 * {@link #createQuery(EntityManager, Class)}.
	 *
	 * @param name
	 *            parameter name
	 * @param value
	 *            parameter value
	 * @return builder reference, fluent api support
	 */
	public QueryBuilder setParameter(final String name, final Object value) {
		return setParameter(new ParameterObject(name, value));
	}

	/**
	 * Adds given parameter to the builder. All parameters will added to the JPA
	 * Query returned by a call to {@link #createQuery(EntityManager)} or
	 * {@link #createQuery(EntityManager, Class)}.
	 *
	 * @param parameter
	 *            parameter to add
	 * @return builder reference, fluent api support
	 */
	public QueryBuilder setParameter(final Parameter<?> parameter) {
		if (parent == null) {
			parameters.add(parameter);
		} else {
			parent.setParameter(parameter);
		}
		return this;
	}

	/**
	 * Adds all given parameters to the builder. All parameters will added to
	 * the JPA Query returned by a call to {@link #createQuery(EntityManager)}
	 * or {@link #createQuery(EntityManager, Class)}.
	 *
	 * @param parameters
	 *            parameters to add
	 * @return builder reference, fluent api support
	 */
	public QueryBuilder setParameters(final List<Parameter<?>> parameters) {
		if (parent == null) {
			this.parameters.addAll(parameters);
		} else {
			parent.setParameters(parameters);
		}
		return this;
	}

	/**
	 * Returns all previously set parameters
	 *
	 * @return list of parameters
	 */
	public List<Parameter<?>> getParameters() {
		return parameters;
	}

	/**
	 * Renders this object as JPQL query string.
	 *
	 * @return the query as string
	 */
	public String render() {

		StringBuilder query = new StringBuilder();

		if (!select.isEmpty()) {
			query.append("SELECT ");
			query.append(StringUtils.join(select.items));
		}

		if (!deleteFrom.isEmpty()) {
			query.append("DELETE FROM ");
			query.append(deleteFrom.item);
		}

		if (!update.isEmpty()) {
			query.append("UPDATE ");
			query.append(update.item);

			if (!set.isEmpty()) {
				query.append(" SET ");
				query.append(StringUtils.join(set.items));
			}
		}

		if (!from.isEmpty()) {
			query.append(" FROM ");
			query.append(StringUtils.join(from.items));
		}

		if (!where.isEmpty()) {
			query.append(" WHERE ");
			query.append(where.render());
		}

		if (!group.isEmpty()) {
			query.append(" GROUP BY ");
			query.append(StringUtils.join(group.items));
		}

		if (order.isEmpty() == false) {
			query.append(" ORDER BY ");
			query.append(StringUtils.join(order.items));
		}

		return query.toString();

	}

}