/* * Copyright 2015 Dimitry Ivanov ([email protected]) * * 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 ru.noties.storm.statements; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import ru.noties.storm.Storm; import ru.noties.storm.FieldPrimaryKey; import ru.noties.storm.ForeignKeyAction; import ru.noties.storm.anno.Autoincrement; import ru.noties.storm.anno.DBNonNull; import ru.noties.storm.anno.Default; import ru.noties.storm.anno.ForeignKey; import ru.noties.storm.anno.Index; import ru.noties.storm.FieldHolder; import ru.noties.storm.anno.Unique; import ru.noties.storm.TableNameParser; import ru.noties.storm.util.Pair; /** * Created by Dimitry Ivanov ([email protected]) on 25.01.2015. */ public class TableCreateStatement { public static final String CREATE_TABLE = "CREATE TABLE "; public static final String INDEX_STATEMENT = "CREATE INDEX %1$s ON %2$s(%3$s)"; public static final String AUTOINCREMENT = "AUTOINCREMENT"; public static final String PRIMARY_KEY = "PRIMARY KEY"; public static final String NON_NULL = "NOT NULL"; public static final String DEFAULT = "DEFAULT"; public static final String UNIQUE = "UNIQUE"; public static final String REFERENCES = "REFERENCES %1$s(%2$s)"; public static final String FK_ON_UPDATE = "ON UPDATE"; public static final String FK_ON_DELETE = "ON DELETE"; private static final Class<Autoincrement> AUTOINCREMENT_CLASS = Autoincrement.class; private static final Class<DBNonNull> NOT_NULL_CLASS = DBNonNull.class; private static final Class<Default> DEFAULT_CLASS = Default.class; private static final Class<Index> INDEX_CLASS = Index.class; private static final Class<Unique> UNIQUE_CLASS = Unique.class; private static final Class<ForeignKey> FOREIGN_KEY_CLASS = ForeignKey.class; protected static final char SINGLE_QUOTE = '\''; protected static final char DELIMITER = '`'; protected static final char SPACE = ' '; protected static final char OPEN = '('; protected static final char CLOSE = ')'; protected static final char COMMA = ','; protected static final char NEW_LINE = '\n'; protected static final char SEMICOLON = ';'; private final List<IndexHolder> mIndices; private boolean mIsPrimaryKeySet; public TableCreateStatement() { mIndices = new ArrayList<>(); } public String createField(@NonNull FieldHolder fieldHolder) { final Field field = fieldHolder.getField(); // is autoincrement; // is primary key; // if both - append; // is not null // has default final boolean isAutoincrement = isAutoincrement(field); final boolean isPrimary = isPrimaryKey(field); final boolean isNotNull = isNotNull(field); final String isDefault = isDefault(field); final boolean isUnique = isUnique(field); final String foreignKey = getForeignKeyStatement(field); final StringBuilder builder = new StringBuilder(); final int name = wrapTwoDelimiters(builder); builder.insert(name, fieldHolder.getName()); builder.append(SPACE) .append(fieldHolder.getType().getSqlRepresentation()); if (isNotNull) { appendWithLeadingSpace(builder, NON_NULL); } if (isDefault != null) { appendWithLeadingSpace(builder, DEFAULT); builder.append(SPACE); final int defaultWhere = wrapTwoSingleQuotes(builder); builder.insert(defaultWhere, isDefault); } if (isPrimary) { if (mIsPrimaryKeySet) { if (Storm.isDebug()) { throw new AssertionError("Multiple primary keys are not supported"); } } else { appendWithLeadingSpace(builder, PRIMARY_KEY); if (isAutoincrement) { appendWithLeadingSpace(builder, AUTOINCREMENT); } mIsPrimaryKeySet = true; } } if (isUnique) { appendWithLeadingSpace(builder, UNIQUE); } if (foreignKey != null) { appendWithLeadingSpace(builder, foreignKey); } checkIfFieldIsInIndex(field, fieldHolder.getName()); return builder.toString(); } private int wrapTwoSingleQuotes(StringBuilder builder) { return wrapTwoSymbols(builder, SINGLE_QUOTE); } private int wrapTwoDelimiters(StringBuilder builder) { return wrapTwoSymbols(builder, DELIMITER); } private int wrapTwoSymbols(StringBuilder builder, char symbol) { final int length = builder.length(); builder.append(symbol) .append(symbol); return length + 1; } private void appendWithLeadingSpace(StringBuilder builder, String what) { builder.append(SPACE) .append(what); } private boolean isAutoincrement(Field field) { return field.isAnnotationPresent(AUTOINCREMENT_CLASS); } private boolean isPrimaryKey(Field field) { return FieldPrimaryKey.isPrimaryKey(field); } private boolean isNotNull(Field field) { return field.isAnnotationPresent(NOT_NULL_CLASS); } private @Nullable String isDefault(Field field) { if (!field.isAnnotationPresent(DEFAULT_CLASS)) { return null; } final Default anno = field.getAnnotation(DEFAULT_CLASS); return anno.value(); } private boolean isUnique(Field field) { return field.isAnnotationPresent(UNIQUE_CLASS); } private @Nullable String getForeignKeyStatement(Field field) { if (!field.isAnnotationPresent(FOREIGN_KEY_CLASS)) { return null; } final ForeignKey foreignKey = field.getAnnotation(FOREIGN_KEY_CLASS); // NB there is no checking whether parentTable // in it's bounds (the same db file) and/or already created final Class<?> parentTable = foreignKey.parentTable(); final String parentColumn = foreignKey.parentColumnName(); final String parentTableName = TableNameParser.parse(parentTable); if (parentTableName == null) { return null; } final String mainFKStatement = String.format(REFERENCES, parentTableName, parentColumn); final ForeignKeyAction onUpdate = foreignKey.onUpdate(); final ForeignKeyAction onDelete = foreignKey.onDelete(); final boolean hasOnUpdate = onUpdate != ForeignKeyAction.NO_ACTION; final boolean hasOnDelete = onDelete != ForeignKeyAction.NO_ACTION; if (!hasOnDelete && !hasOnUpdate) { return mainFKStatement; } final StringBuilder builder = new StringBuilder(mainFKStatement); if (hasOnUpdate) { appendWithLeadingSpace(builder, FK_ON_UPDATE); appendWithLeadingSpace(builder, onUpdate.getSqlRepresentation()); } if (hasOnDelete) { appendWithLeadingSpace(builder, FK_ON_DELETE); appendWithLeadingSpace(builder, onDelete.getSqlRepresentation()); } return builder.toString(); } public List<String> getStatements(@NonNull String tableName, @NonNull List<FieldHolder> holders) { final String mainStatement = createMainStatement(tableName, holders); final List<String> indices = createIndicesStatements(tableName); final List<String> statements = new ArrayList<>(); statements.add(mainStatement); if (indices != null) { statements.addAll(indices); } return statements; } public String createMainStatement(@NonNull String tableName, @NonNull List<FieldHolder> holders) { final List<String> fields = new ArrayList<>(); for (FieldHolder holder: holders) { fields.add(createField(holder)); } final StringBuilder builder = new StringBuilder(CREATE_TABLE); final int tableNameWhere = wrapTwoDelimiters(builder); builder.insert(tableNameWhere, tableName); builder.append(OPEN) .append(NEW_LINE); for (int i = 0, size = fields.size(), max = size - 1; i < size; i++) { builder.append(fields.get(i)); if (max != i) { builder.append(COMMA) .append(NEW_LINE); } } builder.append(NEW_LINE) .append(CLOSE) .append(SEMICOLON); return builder.toString(); } public @Nullable List<String> createIndicesStatements(@NonNull String tableName) { if (mIndices.size() == 0) { return null; } final List<String> statements = new ArrayList<>(); StringBuilder builder; Pair.SimplePair<String> pair; for (IndexHolder indexHolder: mIndices) { builder = new StringBuilder(); for (int i = 0, size = indexHolder.columnAndSort.size(), max = size - 1; i < size; i++) { pair = indexHolder.columnAndSort.get(i); builder.append(pair.getFirst()) .append(SPACE) .append(pair.getSecond()); if (max != i) { builder.append(COMMA) .append(SPACE); } } statements.add( String.format( INDEX_STATEMENT, indexHolder.indexName, tableName, builder.toString() ) ); } if (statements.size() == 0) { return null; } return statements; } private void checkIfFieldIsInIndex(Field field, String columnName) { if (!field.isAnnotationPresent(INDEX_CLASS)) { return; } final Index index = field.getAnnotation(INDEX_CLASS); final String indexName = index.value(); final String sorting = index.sorting().getValue(); final IndexHolder newHolder = new IndexHolder(indexName); final int indexIndex = mIndices.indexOf(newHolder); final IndexHolder holder; if (indexIndex >= 0) { holder = mIndices.get(indexIndex); } else { holder = newHolder; mIndices.add(holder); } holder.add(columnName, sorting); } private static class IndexHolder { final String indexName; List<Pair.SimplePair<String>> columnAndSort; IndexHolder(@NonNull String indexName) { this.indexName = indexName; } public void add(String columnName, String sorting) { if (columnAndSort == null) { columnAndSort = new ArrayList<>(); } columnAndSort.add(new Pair.SimplePair<>(columnName, sorting)); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; IndexHolder that = (IndexHolder) o; if (!indexName.equals(that.indexName)) return false; return true; } @Override public int hashCode() { return indexName.hashCode(); } } }