io.requery.processor.EntityGraphValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of requery-processor Show documentation
Show all versions of requery-processor Show documentation
A light but powerful object mapper and SQL generator for Java/Android
/*
* Copyright 2017 requery.io
*
* 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.requery.processor;
import io.requery.ForeignKey;
import io.requery.meta.Cardinality;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.util.Types;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Validates the relationships, e.g. One-to-One, Many-to-Many etc between {@link EntityDescriptor}s
* in a {@link EntityGraph} instance. Ensures their attribute's relationships match up e.g. can't
* map {@link io.requery.ManyToMany} to {@link io.requery.OneToOne} and that the types referenced in
* the attributes are compatible with each other.
*
* @author Nikhil Purushe
*/
class EntityGraphValidator {
private final ProcessingEnvironment processingEnvironment;
private final EntityGraph graph;
EntityGraphValidator(ProcessingEnvironment processingEnvironment, EntityGraph graph) {
this.processingEnvironment = processingEnvironment;
this.graph = graph;
}
Set validate() {
Set results = new LinkedHashSet<>();
for (EntityDescriptor entity : graph.entities()) {
results.addAll( validateEntity(entity) );
}
return results;
}
private Set validateEntity(EntityDescriptor entity) {
Set results = new LinkedHashSet<>();
for (Map.Entry entry :
entity.attributes().entrySet()) {
AttributeDescriptor attribute = entry.getValue();
ElementValidator validator =
new ElementValidator(attribute.element(), processingEnvironment);
// check mapped associations
if (attribute.cardinality() != null) {
Optional referencingEntity = graph.referencingEntity(attribute);
if (referencingEntity.isPresent()) {
EntityDescriptor referenced = referencingEntity.get();
Set mappings =
graph.mappedAttributes(entity, attribute, referenced);
if (mappings.isEmpty()) {
if (attribute.cardinality() == Cardinality.ONE_TO_ONE &&
!attribute.isForeignKey()) {
validator.error(
"Single sided @OneToOne should specify @ForeignKey/@JoinColumn");
} else if (attribute.cardinality() == Cardinality.ONE_TO_MANY) {
validator.error(
"Corresponding @OneToMany relation not present in mapped entity");
}
} else if (mappings.size() == 1) {
// validate the relationship
AttributeDescriptor mapped = mappings.iterator().next();
validateRelationship(validator, attribute, mapped);
} else if (mappings.size() > 1) {
validator.warning(mappings.size() + " mappings found for: " +
attribute + " -> " + referenced.typeName());
}
} else {
validator.warning("Couldn't find referenced element for " + attribute);
}
} else {
Types types = processingEnvironment.getTypeUtils();
for (EntityDescriptor descriptor : graph.entities()) {
if (types.isSubtype(descriptor.element().asType(), attribute.typeMirror()) &&
attribute.converterName() == null) {
validator.error("Entity reference missing relationship annotation");
}
}
}
// checked foreign key reference
if (attribute.isForeignKey()) {
Optional referenced = graph.referencingEntity(attribute);
if (referenced.isPresent()) {
Optional extends AttributeDescriptor> referencedElement =
graph.referencingAttribute(attribute, referenced.get());
if (!referencedElement.isPresent()) {
validator.warning("Couldn't find referenced element " +
referenced.get().typeName() + " for " + attribute);
} else {
// check all the foreign keys and see if they reference this entity
referenced.get().attributes().values().stream()
.filter(AttributeDescriptor::isForeignKey)
.map(graph::referencingEntity)
.filter(Optional::isPresent)
.map(Optional::get)
.filter(entity::equals)
.findAny()
.ifPresent(other -> validator.warning(
"Circular Foreign Key reference found between " +
entity.typeName() + " and " + other.typeName(),
ForeignKey.class));
}
} else if(attribute.cardinality() != null) {
validator.error("Couldn't find referenced attribute " +
attribute.referencedColumn() + " for " + attribute);
}
}
if (validator.hasErrors() || validator.hasWarnings()) {
results.add(validator);
}
}
return results;
}
private void validateRelationship(ElementValidator validator,
AttributeDescriptor source,
AttributeDescriptor mapped) {
Cardinality sourceCardinality = source.cardinality();
Cardinality mappedCardinality = mapped.cardinality();
Cardinality expectedCardinality;
switch (sourceCardinality) {
case ONE_TO_ONE:
expectedCardinality = Cardinality.ONE_TO_ONE;
break;
case ONE_TO_MANY:
expectedCardinality = Cardinality.MANY_TO_ONE;
break;
case MANY_TO_ONE:
expectedCardinality = Cardinality.ONE_TO_MANY;
break;
case MANY_TO_MANY:
expectedCardinality = Cardinality.MANY_TO_MANY;
break;
default:
throw new IllegalStateException();
}
if (mappedCardinality != expectedCardinality && mapped.cardinality() != null) {
String message = mappingErrorMessage(source, mapped, expectedCardinality);
validator.error(message);
} else if (sourceCardinality == Cardinality.MANY_TO_MANY) {
Optional sourceAssociation = source.associativeEntity();
Optional mappedAssociation = mapped.associativeEntity();
if (!sourceAssociation.isPresent() && !mappedAssociation.isPresent()) {
validator.error("One side of the ManyToMany relationship must specify the " +
"@JunctionTable/@JoinTable annotation");
}
if (sourceAssociation.isPresent() && mappedAssociation.isPresent()) {
validator.error("@JunctionTable should be specified on only one side of a " +
"ManyToMany relationship");
}
} else if (sourceCardinality == Cardinality.ONE_TO_ONE) {
if (!source.isForeignKey() && !mapped.isForeignKey()) {
validator.error("One side of the OneToOne relationship must specify the " +
"@ForeignKey/@JoinColumn annotation");
}
if (source.isForeignKey() && mapped.isForeignKey() && source != mapped) {
validator.error("Only one side of the OneToOne relationship can specify the " +
"@ForeignKey/@JoinColumn annotation");
}
}
}
private String mappingErrorMessage(AttributeDescriptor source, AttributeDescriptor mapped,
Cardinality expected) {
String sourceType = source.cardinality().annotationClass().getSimpleName();
String mappedType = mapped.cardinality().annotationClass().getSimpleName();
String expectedType = expected.annotationClass().getSimpleName();
String message = "%s with cardinality %s incorrectly mapped to " +
"%s having cardinality %s, expected %s";
return String.format(message, source, sourceType, mapped, mappedType, expectedType);
}
}