/*
* Copyright 2017 Realm 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 io.realm;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import io.realm.internal.Table;
import io.realm.internal.Util;
/**
* Class for interacting with the Realm schema using a dynamic API. This makes it possible
* to add, delete and change the classes in the Realm.
* <p>
* All changes must happen inside a write transaction for the particular Realm.
*
* @see io.realm.RealmMigration
*/
class StandardRealmSchema extends RealmSchema {
public static final String EMPTY_STRING_MSG = "Null or empty class names are not allowed";
// Caches Dynamic Class objects given as Strings to Realm Tables
private final Map<String, Table> dynamicClassToTable = new HashMap<>();
// Caches Class objects (both model classes and proxy classes) to Realm Tables
private final Map<Class<? extends RealmModel>, Table> classToTable = new HashMap<>();
// Caches Class objects (both model classes and proxy classes) to their Schema object
private final Map<Class<? extends RealmModel>, StandardRealmObjectSchema> classToSchema = new HashMap<>();
// Caches Class Strings to their Schema object
private final Map<String, StandardRealmObjectSchema> dynamicClassToSchema = new HashMap<>();
private final BaseRealm realm;
/**
* Creates a wrapper to easily manipulate the current schema of a Realm.
*/
StandardRealmSchema(BaseRealm realm) {
this.realm = realm;
}
@Override
public void close() { }
/**
* Returns the Realm schema for a given class.
*
* @param className name of the class
* @return schema object for that class or {@code null} if the class doesn't exists.
*/
@Override
public RealmObjectSchema get(String className) {
checkEmpty(className, EMPTY_STRING_MSG);
String internalClassName = Table.getTableNameForClass(className);
if (!realm.getSharedRealm().hasTable(internalClassName)) { return null; }
Table table = realm.getSharedRealm().getTable(internalClassName);
return new StandardRealmObjectSchema(realm, this, table);
}
/**
* Returns the {@link StandardRealmObjectSchema} for all RealmObject classes that can be saved in this Realm.
*
* @return the set of all classes in this Realm or no RealmObject classes can be saved in the Realm.
*/
@Override
public Set<RealmObjectSchema> getAll() {
int tableCount = (int) realm.getSharedRealm().size();
Set<RealmObjectSchema> schemas = new LinkedHashSet<>(tableCount);
for (int i = 0; i < tableCount; i++) {
String tableName = realm.getSharedRealm().getTableName(i);
if (!Table.isModelTable(tableName)) {
continue;
}
schemas.add(new StandardRealmObjectSchema(realm, this, realm.getSharedRealm().getTable(tableName)));
}
return schemas;
}
/**
* Adds a new class to the Realm.
*
* @param className name of the class.
* @return a Realm schema object for that class.
*/
@Override
public RealmObjectSchema create(String className) {
// Adding a class is always permitted.
checkEmpty(className, EMPTY_STRING_MSG);
String internalTableName = Table.getTableNameForClass(className);
if (internalTableName.length() > Table.TABLE_MAX_LENGTH) {
throw new IllegalArgumentException("Class name is too long. Limit is 56 characters: " + className.length());
}
if (realm.getSharedRealm().hasTable(internalTableName)) {
throw new IllegalArgumentException("Class already exists: " + className);
}
return new StandardRealmObjectSchema(realm, this, realm.getSharedRealm().getTable(internalTableName));
}
/**
* Checks if a given class already exists in the schema.
*
* @param className class name to check.
* @return {@code true} if the class already exists. {@code false} otherwise.
*/
@Override
public boolean contains(String className) {
return realm.getSharedRealm().hasTable(Table.getTableNameForClass(className));
}
/**
* Removes a class from the Realm. All data will be removed. Removing a class while other classes point
* to it will throw an {@link IllegalStateException}. Removes those classes or fields first.
*
* @param className name of the class to remove.
*/
@Override
public void remove(String className) {
realm.checkNotInSync(); // Destructive modifications are not permitted.
checkEmpty(className, EMPTY_STRING_MSG);
String internalTableName = Table.getTableNameForClass(className);
checkHasTable(className, "Cannot remove class because it is not in this Realm: " + className);
Table table = getTable(className);
if (table.hasPrimaryKey()) {
table.setPrimaryKey(null);
}
realm.getSharedRealm().removeTable(internalTableName);
}
/**
* Renames a class already in the Realm.
*
* @param oldClassName old class name.
* @param newClassName new class name.
* @return a schema object for renamed class.
*/
@Override
public RealmObjectSchema rename(String oldClassName, String newClassName) {
realm.checkNotInSync(); // Destructive modifications are not permitted.
checkEmpty(oldClassName, "Class names cannot be empty or null");
checkEmpty(newClassName, "Class names cannot be empty or null");
String oldInternalName = Table.getTableNameForClass(oldClassName);
String newInternalName = Table.getTableNameForClass(newClassName);
checkHasTable(oldClassName, "Cannot rename class because it doesn't exist in this Realm: " + oldClassName);
if (realm.getSharedRealm().hasTable(newInternalName)) {
throw new IllegalArgumentException(oldClassName + " cannot be renamed because the new class already exists: " + newClassName);
}
// Checks if there is a primary key defined for the old class.
Table oldTable = getTable(oldClassName);
String pkField = null;
if (oldTable.hasPrimaryKey()) {
pkField = oldTable.getColumnName(oldTable.getPrimaryKey());
oldTable.setPrimaryKey(null);
}
realm.getSharedRealm().renameTable(oldInternalName, newInternalName);
Table table = realm.getSharedRealm().getTable(newInternalName);
// Sets the primary key for the new class if necessary.
if (pkField != null) {
table.setPrimaryKey(pkField);
}
return new StandardRealmObjectSchema(realm, this, table);
}
private void checkEmpty(String str, String error) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException(error);
}
}
private void checkHasTable(String className, String errorMsg) {
String internalTableName = Table.getTableNameForClass(className);
if (!realm.getSharedRealm().hasTable(internalTableName)) {
throw new IllegalArgumentException(errorMsg);
}
}
@Override
Table getTable(String className) {
String tableName = Table.getTableNameForClass(className);
Table table = dynamicClassToTable.get(tableName);
if (table != null) { return table; }
if (!realm.getSharedRealm().hasTable(tableName)) {
throw new IllegalArgumentException("The class " + className + " doesn't exist in this Realm.");
}
table = realm.getSharedRealm().getTable(tableName);
dynamicClassToTable.put(tableName, table);
return table;
}
@Override
Table getTable(Class<? extends RealmModel> clazz) {
Table table = classToTable.get(clazz);
if (table != null) { return table; }
Class<? extends RealmModel> originalClass = Util.getOriginalModelClass(clazz);
if (isProxyClass(originalClass, clazz)) {
// If passed 'clazz' is the proxy, try again with model class.
table = classToTable.get(originalClass);
}
if (table == null) {
table = realm.getSharedRealm().getTable(realm.getConfiguration().getSchemaMediator().getTableName(originalClass));
classToTable.put(originalClass, table);
}
if (isProxyClass(originalClass, clazz)) {
// 'clazz' is the proxy class for 'originalClass'.
classToTable.put(clazz, table);
}
return table;
}
@Override
StandardRealmObjectSchema getSchemaForClass(Class<? extends RealmModel> clazz) {
StandardRealmObjectSchema classSchema = classToSchema.get(clazz);
if (classSchema != null) { return classSchema; }
Class<? extends RealmModel> originalClass = Util.getOriginalModelClass(clazz);
if (isProxyClass(originalClass, clazz)) {
// If passed 'clazz' is the proxy, try again with model class.
classSchema = classToSchema.get(originalClass);
}
if (classSchema == null) {
Table table = getTable(clazz);
classSchema = new StandardRealmObjectSchema(realm, this, table, getColumnInfo(originalClass));
classToSchema.put(originalClass, classSchema);
}
if (isProxyClass(originalClass, clazz)) {
// 'clazz' is the proxy class for 'originalClass'.
classToSchema.put(clazz, classSchema);
}
return classSchema;
}
@Override
StandardRealmObjectSchema getSchemaForClass(String className) {
String tableName = Table.getTableNameForClass(className);
StandardRealmObjectSchema dynamicSchema = dynamicClassToSchema.get(tableName);
if (dynamicSchema == null) {
if (!realm.getSharedRealm().hasTable(tableName)) {
throw new IllegalArgumentException("The class " + className + " doesn't exist in this Realm.");
}
dynamicSchema = new StandardRealmObjectSchema(realm, this, realm.getSharedRealm().getTable(tableName));
dynamicClassToSchema.put(tableName, dynamicSchema);
}
return dynamicSchema;
}
}