/* * Copyright 2014 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.processor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import io.realm.annotations.Ignore; import io.realm.annotations.Index; import io.realm.annotations.LinkingObjects; import io.realm.annotations.PrimaryKey; import io.realm.annotations.Required; /** * Utility class for holding metadata for RealmProxy classes. */ public class ClassMetaData { private final TypeElement classType; // Reference to model class. private final String className; // Model class simple name. private final List<VariableElement> fields = new ArrayList<VariableElement>(); // List of all fields in the class except those @Ignored. private final List<VariableElement> indexedFields = new ArrayList<VariableElement>(); // list of all fields marked @Index. private final Set<Backlink> backlinks = new HashSet<Backlink>(); private final Set<VariableElement> nullableFields = new HashSet<VariableElement>(); // Set of fields which can be nullable private String packageName; // package name for model class. private boolean hasDefaultConstructor; // True if model has a public no-arg constructor. private VariableElement primaryKey; // Reference to field used as primary key, if any. private boolean containsToString; private boolean containsEquals; private boolean containsHashCode; private final List<TypeMirror> validPrimaryKeyTypes; private final Types typeUtils; private final Elements elements; public ClassMetaData(ProcessingEnvironment env, TypeElement clazz) { this.classType = clazz; this.className = clazz.getSimpleName().toString(); typeUtils = env.getTypeUtils(); elements = env.getElementUtils(); TypeMirror stringType = env.getElementUtils().getTypeElement("java.lang.String").asType(); validPrimaryKeyTypes = Arrays.asList( stringType, typeUtils.getPrimitiveType(TypeKind.SHORT), typeUtils.getPrimitiveType(TypeKind.INT), typeUtils.getPrimitiveType(TypeKind.LONG), typeUtils.getPrimitiveType(TypeKind.BYTE) ); for (Element element : classType.getEnclosedElements()) { if (element instanceof ExecutableElement) { Name name = element.getSimpleName(); if (name.contentEquals("toString")) { this.containsToString = true; } else if (name.contentEquals("equals")) { this.containsEquals = true; } else if (name.contentEquals("hashCode")) { this.containsHashCode = true; } } } } @Override public String toString() { return "class " + getFullyQualifiedClassName(); } public String getSimpleClassName() { return className; } public String getPackageName() { return packageName; } public String getFullyQualifiedClassName() { return packageName + "." + className; } public List<VariableElement> getFields() { return Collections.unmodifiableList(fields); } public Set<Backlink> getBacklinkFields() { return Collections.unmodifiableSet(backlinks); } public String getInternalGetter(String fieldName) { return "realmGet$" + fieldName; } public String getInternalSetter(String fieldName) { return "realmSet$" + fieldName; } public List<VariableElement> getIndexedFields() { return Collections.unmodifiableList(indexedFields); } public boolean hasPrimaryKey() { return primaryKey != null; } public VariableElement getPrimaryKey() { return primaryKey; } public String getPrimaryKeyGetter() { return getInternalGetter(primaryKey.getSimpleName().toString()); } public boolean containsToString() { return containsToString; } public boolean containsEquals() { return containsEquals; } public boolean containsHashCode() { return containsHashCode; } /** * Checks if a VariableElement is nullable. * * @return {@code true} if a VariableElement is nullable type, {@code false} otherwise. */ public boolean isNullable(VariableElement variableElement) { return nullableFields.contains(variableElement); } /** * Checks if a VariableElement is indexed. * * @param variableElement the element/field * @return {@code true} if a VariableElement is indexed, {@code false} otherwise. */ public boolean isIndexed(VariableElement variableElement) { return indexedFields.contains(variableElement); } /** * Checks if a VariableElement is a primary key. * * @param variableElement the element/field * @return {@code true} if a VariableElement is primary key, {@code false} otherwise. */ public boolean isPrimaryKey(VariableElement variableElement) { return primaryKey != null && primaryKey.equals(variableElement); } /** * Returns {@code true} if the class is considered to be a valid RealmObject class. * RealmObject and Proxy classes also have the @RealmClass annotation but are not considered valid * RealmObject classes. */ public boolean isModelClass() { String type = classType.toString(); return !type.equals("io.realm.DynamicRealmObject") && !type.endsWith(".RealmObject") && !type.endsWith("RealmProxy"); } /** * Find the named field in this classes list of fields. * This method is called only during backlink checking, * so creating a map, even lazily, doesn't seem like a worthwhile optimization. * If it gets used more widely, that decision should be revisited. * * @param fieldName The name of the sought field * @return the named field's VariableElement, or null if not found */ public VariableElement getDeclaredField(String fieldName) { if (fieldName == null) { return null; } for (VariableElement field : fields) { if (field.getSimpleName().toString().equals(fieldName)) { return field; } } return null; } /** * Builds the meta data structures for this class. Any errors or messages will be * posted on the provided Messager. * * @return True if meta data was correctly created and processing can continue, false otherwise. */ public boolean generate() { // Get the package of the class Element enclosingElement = classType.getEnclosingElement(); if (!enclosingElement.getKind().equals(ElementKind.PACKAGE)) { Utils.error("The RealmClass annotation does not support nested classes.", classType); return false; } TypeElement parentElement = (TypeElement) Utils.getSuperClass(classType); if (!parentElement.toString().equals("java.lang.Object") && !parentElement.toString().equals("io.realm.RealmObject")) { Utils.error("Valid model classes must either extend RealmObject or implement RealmModel.", classType); return false; } PackageElement packageElement = (PackageElement) enclosingElement; packageName = packageElement.getQualifiedName().toString(); if (!categorizeClassElements()) { return false; } if (!checkListTypes()) { return false; } if (!checkReferenceTypes()) { return false; } if (!checkDefaultConstructor()) { return false; } if (!checkForFinalFields()) { return false; } if (!checkForVolatileFields()) { return false; } return true; // Meta data was successfully generated } // Iterate through all class elements and add them to the appropriate internal data structures. // Returns true if all elements could be categorized and false otherwise. private boolean categorizeClassElements() { for (Element element : classType.getEnclosedElements()) { ElementKind elementKind = element.getKind(); switch (elementKind) { case CONSTRUCTOR: if (Utils.isDefaultConstructor(element)) { hasDefaultConstructor = true; } break; case FIELD: if (!categorizeField(element)) { return false; } break; default: } } if (fields.size() == 0) { Utils.error(String.format("Class \"%s\" must contain at least 1 persistable field.", className)); } return true; } private boolean checkListTypes() { for (VariableElement field : fields) { if (Utils.isRealmList(field) || Utils.isRealmResults(field)) { // Check for missing generic (default back to Object) if (Utils.getGenericTypeQualifiedName(field) == null) { Utils.error("No generic type supplied for field", field); return false; } // Check that the referenced type is a concrete class and not an interface TypeMirror fieldType = field.asType(); List<? extends TypeMirror> typeArguments = ((DeclaredType) fieldType).getTypeArguments(); String genericCanonicalType = typeArguments.get(0).toString(); TypeElement typeElement = elements.getTypeElement(genericCanonicalType); if (typeElement.getSuperclass().getKind() == TypeKind.NONE) { Utils.error( "Only concrete Realm classes are allowed in RealmLists. " + "Neither interfaces nor abstract classes are allowed.", field); return false; } } } return true; } private boolean checkReferenceTypes() { for (VariableElement field : fields) { if (Utils.isRealmModel(field)) { // Check that the referenced type is a concrete class and not an interface TypeElement typeElement = elements.getTypeElement(field.asType().toString()); if (typeElement.getSuperclass().getKind() == TypeKind.NONE) { Utils.error( "Only concrete Realm classes can be referenced from model classes. " + "Neither interfaces nor abstract classes are allowed.", field); return false; } } } return true; } // Report if the default constructor is missing private boolean checkDefaultConstructor() { if (!hasDefaultConstructor) { Utils.error(String.format( "Class \"%s\" must declare a public constructor with no arguments if it contains custom constructors.", className)); return false; } else { return true; } } private boolean checkForFinalFields() { for (VariableElement field : fields) { if (field.getModifiers().contains(Modifier.FINAL)) { Utils.error(String.format( "Class \"%s\" contains illegal final field \"%s\".", className, field.getSimpleName().toString())); return false; } } return true; } private boolean checkForVolatileFields() { for (VariableElement field : fields) { if (field.getModifiers().contains(Modifier.VOLATILE)) { Utils.error(String.format( "Class \"%s\" contains illegal volatile field \"%s\".", className, field.getSimpleName().toString())); return false; } } return true; } private boolean categorizeField(Element element) { VariableElement field = (VariableElement) element; // completely ignore any static fields if (field.getModifiers().contains(Modifier.STATIC)) { return true; } // Ignore fields marked with @Ignore or if they are transient if (field.getAnnotation(Ignore.class) != null || field.getModifiers().contains(Modifier.TRANSIENT)) { return true; } if (field.getAnnotation(Index.class) != null) { if (!categorizeIndexField(element, field)) { return false; } } if (field.getAnnotation(Required.class) != null) { categorizeRequiredField(element, field); } else { // The field doesn't have the @Required annotation. // Without @Required annotation, boxed types/RealmObject/Date/String/bytes should be added to // nullableFields. // RealmList and Primitive types are NOT nullable always. @Required annotation is not supported. if (!Utils.isPrimitiveType(field) && !Utils.isRealmList(field)) { nullableFields.add(field); } } if (field.getAnnotation(PrimaryKey.class) != null) { if (!categorizePrimaryKeyField(field)) { return false; } } // Check @LinkingObjects last since it is not allowed to be either @Index, @Required or @PrimaryKey if (field.getAnnotation(LinkingObjects.class) != null) { return categorizeBacklinkField(field); } // Standard field that appear valid (more fine grained checks might fail later). fields.add(field); return true; } private boolean categorizeIndexField(Element element, VariableElement variableElement) { // The field has the @Index annotation. It's only valid for column types: // STRING, DATE, INTEGER, BOOLEAN Constants.RealmFieldType realmType = Constants.JAVA_TO_REALM_TYPES.get(variableElement.asType().toString()); if (realmType != null) { switch (realmType) { case STRING: case DATE: case INTEGER: case BOOLEAN: indexedFields.add(variableElement); return true; } } Utils.error(String.format("Field \"%s\" of type \"%s\" cannot be an @Index.", element, element.asType())); return false; } // The field has the @Required annotation private void categorizeRequiredField(Element element, VariableElement variableElement) { if (Utils.isPrimitiveType(variableElement)) { Utils.error(String.format( "@Required annotation is unnecessary for primitive field \"%s\".", element)); } else if (Utils.isRealmList(variableElement) || Utils.isRealmModel(variableElement)) { Utils.error(String.format( "Field \"%s\" with type \"%s\" cannot be @Required.", element, element.asType())); } else { // Should never get here - user should remove @Required if (nullableFields.contains(variableElement)) { Utils.error(String.format( "Field \"%s\" with type \"%s\" appears to be nullable. Consider removing @Required.", element, element.asType())); } } } // The field has the @PrimaryKey annotation. It is only valid for // String, short, int, long and must only be present one time private boolean categorizePrimaryKeyField(VariableElement variableElement) { if (primaryKey != null) { Utils.error(String.format( "A class cannot have more than one @PrimaryKey. Both \"%s\" and \"%s\" are annotated as @PrimaryKey.", primaryKey.getSimpleName().toString(), variableElement.getSimpleName().toString())); return false; } TypeMirror fieldType = variableElement.asType(); if (!isValidPrimaryKeyType(fieldType)) { Utils.error(String.format( "Field \"%s\" with type \"%s\" cannot be used as primary key. See @PrimaryKey for legal types.", variableElement.getSimpleName().toString(), fieldType)); return false; } primaryKey = variableElement; // Also add as index. All types of primary key can be indexed. if (!indexedFields.contains(variableElement)) { indexedFields.add(variableElement); } return true; } private boolean categorizeBacklinkField(VariableElement variableElement) { Backlink backlink = new Backlink(this, variableElement); if (!backlink.validateSource()) { return false; } backlinks.add(backlink); return true; } private boolean isValidPrimaryKeyType(TypeMirror type) { for (TypeMirror validType : validPrimaryKeyTypes) { if (typeUtils.isAssignable(type, validType)) { return true; } } return false; } }