RelationshipMapper.java
/*
* Copyright 2025-present the original author or authors.
*
* 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
*
* https://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.github.simplejdbcmapper.relationship;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* This holds the results from a multi-entity query. See
* {@link io.github.simplejdbcmapper.core.SimpleJdbcMapper#resultSetExtractor}
*
* <p>
* You can build relationships from multiple individual related lists. For
* example if you have a list of orders and another list of related orderLines
* you can do something like:
*
* <pre>
* RelationhipMapper relationshipMapper = new RelationshipMapper();
* relationshipMapper.addEntityResult(Order.class, orders, "id");
* relationshipMapper.addEntityResult(OrderLine.class, orderLines, "id");
*
* Relationship orderToManyOrderLine =
* Relationshp.type(Order.class).toMany(OrderLine.class).joinOn("id", "orderId".populate("orderLines");
*
* {@code List<Order>} orders = relationshipMapper.assemble(orderToManyOrderLine).getList(Order.class);
*
* </pre>
*
* For more details see <a href=
* "https://github.com/spring-jdbc-crud/simplejdbcmapper#assembling-relationships-from-custom-queries">documentation</a>
* and {@link io.github.simplejdbcmapper.relationship.Relationship}
*
* @author Antony Joseph
*/
public class RelationshipMapper implements GetListSpec {
static final String IS_PREFIX = "is";
static final String GET_PREFIX = "get";
static final String SET_PREFIX = "set";
static final String TO_MANY = "toMany";
static final String TO_ONE = "toOne";
static final String TO_MANY_THROUGH = "toManyThrough";
private List<ExtractorEntityResult> results = new ArrayList<>();
/**
* Add an entity result
*
* @param <T> the type
* @param entityType the entity type
* @param list the list of results
* @param idPropertyName the id property name of the entity
*/
public <T> void addEntityResult(Class<T> entityType, List<T> list, String idPropertyName) {
Assert.notNull(entityType, "entityType must not be null");
Assert.notNull(list, "list must not be null");
Assert.notNull(idPropertyName, "idPropertyName must not be null");
checkDuplicatesForAddEntityResult(entityType);
results.add(new ExtractorEntityResult(entityType, list, idPropertyName));
}
/**
* @deprecated As of release 2.4.0, Replaced by
* {@link io.github.simplejdbcmapper.relationship.Relationship#type}
* Starts the relationship processing flow.
*
* @param <T> the type
* @param type the type
* @return RelationshipSpec the relationship spec
*/
@Deprecated(since = "2.4.0", forRemoval = true)
public <T> RelationshipSpec type(Class<T> type) {
Assert.notNull(type, "type must not be null");
// will throw an exception for invalid type
getList(type);
return RelationshipLegacy.newInstance(type, results);
}
/**
* Assembles the relationships from the query results.
*
* @param relationships an array of relationships
* @return GetListSpec
*/
public GetListSpec assemble(Relationship... relationships) {
validateAssemble(relationships);
for (Relationship rel : relationships) {
process(rel);
}
return this;
}
/**
* Get the Id property name for type.
*
* @param type The type
* @return String the id property name
*/
public String getIdPropertyName(Class<?> type) {
ExtractorEntityResult result = getExtractorEntityResult(type, results);
return result.idPropertyName();
}
/**
* returns the results for the type
*
* @param <T> the type
* @param type the type
* @return list of results
*/
@SuppressWarnings("unchecked")
public <T> List<T> getList(Class<T> type) {
ExtractorEntityResult result = getExtractorEntityResult(type, results);
return (List<T>) result.list();
}
void process(Relationship rel) {
List<?> mainList = getList(rel.getMainType());
List<?> relatedList = getList(rel.getRelatedType());
if (rel.getRelationshipType().equals(TO_ONE)) {
rel.getToOne().process(mainList, relatedList);
} else if (rel.getRelationshipType().equals(TO_MANY)) {
rel.getToMany().process(mainList, relatedList);
} else {
// toManyThrough
List<?> throughList = getList(rel.getThroughType());
rel.getToManyThrough().process(mainList, relatedList, throughList, getIdPropertyName(rel.getMainType()),
getIdPropertyName(rel.getRelatedType()));
}
}
private void checkDuplicatesForAddEntityResult(Class<?> entityType) {
for (ExtractorEntityResult result : results) {
if (result.entityType() == entityType) {
throw new IllegalArgumentException("duplicate entityType " + entityType);
}
}
}
private void validateAssemble(Relationship... relationships) {
Assert.notNull(relationships, "relationships must not be null");
if (relationships.length == 0) {
throw new IllegalArgumentException("relationships array must not be empty.");
}
Set<String> set = new HashSet<>();
for (Relationship rel : relationships) {
if (rel == null) {
throw new IllegalArgumentException("relationship must not be null");
}
if (!set.add(rel.getMainType().getName() + "-" + rel.getRelatedType().getName())) {
throw new IllegalArgumentException("Duplicate relationship. " + rel);
}
}
}
@SuppressWarnings("unchecked")
static <T> List<T> getList(Class<?> type, List<ExtractorEntityResult> results) {
ExtractorEntityResult result = getExtractorEntityResult(type, results);
return (List<T>) result.list();
}
static ExtractorEntityResult getExtractorEntityResult(Class<?> type, List<ExtractorEntityResult> results) {
for (ExtractorEntityResult result : results) {
if (result.entityType() == type) {
return result;
}
}
throw new IllegalArgumentException(type + " was not part of the query results");
}
static Method getReadMethod(Class<?> type, String propertyName) {
Method m = ReflectionUtils.findMethod(type, GET_PREFIX + StringUtils.capitalize(propertyName));
if (m == null) {
m = ReflectionUtils.findMethod(type, IS_PREFIX + StringUtils.capitalize(propertyName));
}
if (m == null) {
throw new IllegalArgumentException(
"Invalid argument. Could not find getter for " + type.getName() + "." + propertyName);
}
// turn off jvm access verification for invoke()
m.trySetAccessible();
return m;
}
static Method getWriteMethod(Class<?> type, String propertyName) {
Field field = ReflectionUtils.findField(type, propertyName);
if (field == null) {
throw new IllegalArgumentException(
"Invalid argument. Property name " + propertyName + " does not exist for " + type.getName());
} else {
Method m = ReflectionUtils.findMethod(type, SET_PREFIX + StringUtils.capitalize(propertyName),
field.getType());
if (m == null) {
throw new IllegalArgumentException(
"Invalid argument. Could not find setter for " + type.getName() + "." + propertyName);
}
// turn off jvm access verification for invoke()
m.trySetAccessible();
return m;
}
}
static Class<?> getPropertyType(Class<?> type, String propertyName) {
Field field = ReflectionUtils.findField(type, propertyName);
if (field != null) {
return field.getType();
} else {
throw new IllegalArgumentException(
"Invalid argument. Property name " + propertyName + " does not exist for " + type.getName());
}
}
record ExtractorEntityResult(Class<?> entityType, List<?> list, String idPropertyName) {
}
}