Relationship.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 org.springframework.util.Assert;

/**
 * The relationship definition. It is thread safe and can be used with different
 * query results which have the same relationship.
 * 
 * <pre>
 * 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>
 * <p>
 * For more details see the <a href=
 * "https://github.com/spring-jdbc-crud/simplejdbcmapper#assembling-relationships-from-custom-queries">documentation</a>
 * 
 * 
 * @author Antony Joseph
 */
public class Relationship {
	private final Class<?> mainType;
	private final Class<?> relatedType;
	private final Class<?> throughType;
	private final ToOne toOne;
	private final ToMany toMany;
	private final ToManyThrough toManyThrough;
	private final String relationshipType;

	Relationship(RelationshipBuilder builder) {
		this.mainType = builder.mainType;
		this.relatedType = builder.relatedType;
		this.throughType = builder.throughType;
		this.toOne = builder.toOne;
		this.toMany = builder.toMany;
		this.toManyThrough = builder.toManyThrough;
		this.relationshipType = builder.relationshipType;
	}

	/**
	 * Starts the relationship fluent flow.
	 * 
	 * @param type the main object type
	 * @return RelationshipFluent.RelationshipType
	 */
	public static RelationshipFluent.RelationshipType type(Class<?> type) {
		Assert.notNull(type, "type must not be null");
		return new RelationshipBuilder(type);
	}

	Class<?> getMainType() {
		return mainType;
	}

	Class<?> getRelatedType() {
		return relatedType;
	}

	String getRelationshipType() {
		return relationshipType;
	}

	Class<?> getThroughType() {
		return throughType;
	}

	ToOne getToOne() {
		return toOne;
	}

	ToMany getToMany() {
		return toMany;
	}

	ToManyThrough getToManyThrough() {
		return toManyThrough;
	}

	@Override
	public String toString() {
		return mainType.getSimpleName() + " " + relationshipType + " " + relatedType.getSimpleName();
	}

	private static class RelationshipBuilder implements RelationshipFluent.RelationshipType, RelationshipFluent.ToOne,
			RelationshipFluent.ToMany, RelationshipFluent.Populate {
		private Class<?> mainType;
		private Class<?> relatedType;
		private Class<?> throughType;
		private ToOne toOne;
		private ToMany toMany;
		private ToManyThrough toManyThrough;
		private String relationshipType;

		public RelationshipBuilder(Class<?> mainType) {
			this.mainType = mainType;
		}

		public RelationshipFluent.ToOne toOne(Class<?> relatedType) {
			Assert.notNull(relatedType, "relatedType must not be null");
			if (mainType == relatedType) {
				throw new IllegalArgumentException("mainType and relatedType cannot be same.");
			}
			this.relatedType = relatedType;
			this.relationshipType = RelationshipMapper.TO_ONE;
			this.toOne = new ToOne(mainType, relatedType);
			return this;
		}

		public RelationshipFluent.ToMany toMany(Class<?> relatedType) {
			Assert.notNull(relatedType, "relatedType must not be null");
			if (mainType == relatedType) {
				throw new IllegalArgumentException("mainType and relatedType cannot be same.");
			}
			this.relatedType = relatedType;
			this.relationshipType = RelationshipMapper.TO_MANY;
			this.toMany = new ToMany(mainType, relatedType);
			return this;
		}

		public RelationshipFluent.Populate joinOn(String mainObjJoinProperty, String relatedObjJoinProperty) {
			if (relationshipType.equals(RelationshipMapper.TO_ONE)) {
				toOne.joinOn(mainObjJoinProperty, relatedObjJoinProperty);
			} else if (relationshipType.equals(RelationshipMapper.TO_MANY)) {
				toMany.joinOn(mainObjJoinProperty, relatedObjJoinProperty);
			}
			return this;
		}

		public RelationshipFluent.Populate through(Class<?> throughType, String fkPropertyToMainObjId,
				String fkPropertyToRelatedObjId) {
			this.throughType = throughType;
			this.relationshipType = RelationshipMapper.TO_MANY_THROUGH;
			this.toManyThrough = new ToManyThrough(mainType, relatedType);
			toManyThrough.through(throughType, fkPropertyToMainObjId, fkPropertyToRelatedObjId);
			return this;
		}

		public Relationship populate(String mainObjPropertyToPopulate) {
			if (relationshipType.equals(RelationshipMapper.TO_ONE)) {
				toOne.populate(mainObjPropertyToPopulate);
			} else if (relationshipType.equals(RelationshipMapper.TO_MANY)) {
				toMany.populate(mainObjPropertyToPopulate);
			} else {
				// toManyThrough
				toManyThrough.populate(mainObjPropertyToPopulate);
			}
			return new Relationship(this);
		}

	}

}