/* * 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.processor; import javax.lang.model.element.Modifier; import javax.lang.model.element.VariableElement; import io.realm.annotations.LinkingObjects; import io.realm.annotations.Required; /** * A <b>Backlink</b> is an implicit backwards reference. If field <code>sourceField</code> in instance <code>I</code> * of type <code>SourceClass</code> holds a reference to instance <code>J</code> of type <code>TargetClass</code>, * then a "backlink" is the automatically created reference from <code>J</code> to <code>I</code>. * Backlinks are automatically created and destroyed when the forward references to which they correspond are * created and destroyed. This can dramatically reduce the complexity of client code. * <p> * To expose backinks for use, create a declaration as follows: * <code> * class TargetClass { * // ... * {@literal @}LinkingObjects("sourceField") * final RealmResults<SourceClass> targetField = null; * } * </code>. * <p> * The targetField, the field annotated with the {@literal @}LinkingObjects annotation must be final. * Its type must be <code>RealmResults</code> whose generic argument is the <code>SourceClass</code>, * the class with the <code>sourceField</code> that will hold the forward reference to an instance of * <code>TargetClass</code> * <p> * The <code>sourceField</code> must be either of type <code>TargetClass</code> * or <code>RealmList<TargetClass></code> * <p> * In the code link direction is from the perspective of the link, not the backlink: the source is the * instance to which the backlink points, the target is the instance holding the pointer. * This is consistent with the use of terms in the Realm Core. * <p> * As should be obvious, from the declaration, backlinks are useful only on managed objects. * An unmanaged Model object will have, as the value of its backlink field, the value with which * the field is initialized (typically null). */ final class Backlink { private final VariableElement backlink; /** * The fully-qualified name of the class containing the <code>targetField</code>, * the field annotated with the {@literal @}LinkingObjects annotation. */ private final String targetClass; /** * The name of the backlink field, in <code>targetClass</code>. * A <code>RealmResults<></code> field annotated with a {@literal @}LinkingObjects annotation. */ private final String targetField; /** * The fully-qualified name of the class to which the backlinks, from <code>targetField</code>, * point: The generic argument to the type of the <code>targetField</code>. */ private final String sourceClass; /** * The name of the field, in <code>SourceClass</code> that creates the backlink. * Making this field, in an instance I of <code>SourceClass</code>, * a reference to an instance J of <code>TargetClass</code> * will cause the <code>targetField</code> of J to contain a backlink to I. */ private final String sourceField; public Backlink(ClassMetaData clazz, VariableElement backlink) { if ((null == clazz) || (null == backlink)) { throw new NullPointerException(String.format("null parameter: %s, %s", clazz, backlink)); } this.backlink = backlink; this.targetClass = clazz.getFullyQualifiedClassName(); this.targetField = backlink.getSimpleName().toString(); this.sourceClass = Utils.getRealmResultsType(backlink); this.sourceField = backlink.getAnnotation(LinkingObjects.class).value(); } public String getTargetClass() { return targetClass; } public String getTargetField() { return targetField; } public String getSourceClass() { return sourceClass; } public String getSourceField() { return sourceField; } public String getTargetFieldType() { return backlink.asType().toString(); } public String getSimpleSourceClass() { return Utils.getFieldTypeSimpleName(Utils.getGenericTypeForContainer(backlink)); } /** * Validate the source side of the backlink. * * @return true if the backlink source looks good. */ public boolean validateSource() { // A @LinkingObjects cannot be @Required if (backlink.getAnnotation(Required.class) != null) { Utils.error(String.format( "The @LinkingObjects field \"%s.%s\" cannot be @Required.", targetClass, targetField)); return false; } // The annotation must have an argument, identifying the linked field if ((sourceField == null) || sourceField.equals("")) { Utils.error(String.format( "The @LinkingObjects annotation for the field \"%s.%s\" must have a parameter identifying the link target.", targetClass, targetField)); return false; } // Using link syntax to try to reference a linked field is not possible. if (sourceField.contains(".")) { Utils.error(String.format( "The parameter to the @LinkingObjects annotation for the field \"%s.%s\" contains a '.'. The use of '.' to specify fields in referenced classes is not supported.", targetClass, targetField)); return false; } // The annotated element must be a RealmResult if (!Utils.isRealmResults(backlink)) { Utils.error(String.format( "The field \"%s.%s\" is a \"%s\". Fields annotated with @LinkingObjects must be RealmResults.", targetClass, targetField, backlink.asType())); return false; } if (sourceClass == null) { Utils.error(String.format( "\"The field \"%s.%s\", annotated with @LinkingObjects, must specify a generic type.", targetClass, targetField)); return false; } // A @LinkingObjects field must be final if (!backlink.getModifiers().contains(Modifier.FINAL)) { Utils.error(String.format( "A @LinkingObjects field \"%s.%s\" must be final.", targetClass, targetField)); return false; } return true; } public boolean validateTarget(ClassMetaData clazz) { VariableElement field = clazz.getDeclaredField(sourceField); if (field == null) { Utils.error(String.format( "Field \"%s\", the target of the @LinkedObjects annotation on field \"%s.%s\", does not exist in class \"%s\".", sourceField, targetClass, targetField, sourceClass)); return false; } String fieldType = field.asType().toString(); if (!(targetClass.equals(fieldType) || targetClass.equals(Utils.getRealmListType(field)))) { Utils.error(String.format( "Field \"%s.%s\", the target of the @LinkedObjects annotation on field \"%s.%s\", has type \"%s\" instead of \"%3$s\".", sourceClass, sourceField, targetClass, targetField, fieldType)); return false; } return true; } @Override public String toString() { return "Backlink{" + sourceClass + "." + sourceField + " ==> " + targetClass + "." + targetField + "}"; } @Override public boolean equals(Object o) { if (null == o) { return false; } if (this == o) { return true; } if (!(o instanceof Backlink)) { return false; } Backlink backlink = (Backlink) o; return targetClass.equals(backlink.targetClass) && targetField.equals(backlink.targetField) && sourceClass.equals(backlink.sourceClass) && sourceField.equals(backlink.sourceField); } @Override public int hashCode() { int result = targetClass.hashCode(); result = 31 * result + targetField.hashCode(); result = 31 * result + sourceClass.hashCode(); result = 31 * result + sourceField.hashCode(); return result; } }