public class EclipselinkRepository extends AbstractVendorRepository implements JoinerVendorRepository {

    private static final int MAX_NESTED_JOIN_DEPTH = 7;
    private static final String DOT_ESCAPED = "\\.";

    @Override
    public <R> ExtendedJPAQuery<R> createQuery(EntityManager entityManager) {
        JPAQuery<R> query = new JPAQuery<>(entityManager, EclipseLinkTemplates.DEFAULT);
        makeInsertionOrderHints(query);
        return new ExtendedJPAQuery<>(entityManager, query);
    }

    private void makeInsertionOrderHints(AbstractJPAQuery<?, ?> sourceQuery) {
        Field f = ReflectionUtils.findField(AbstractJPAQuery.class, "hints");
        ReflectionUtils.setField(f, sourceQuery, ArrayListMultimap.create());
    }

    @Override
    public void addFetch(JPAQuery<?> query, JoinDescription joinDescription, Collection<JoinDescription> joins, EntityPath<?> rootPath) {
        String rootEntityAlias = rootPath.getMetadata().getName();
        String path = resolvePathToFieldFromRoot(rootEntityAlias, joinDescription, joins);

        String fetchHint = joinDescription.getJoinType().equals(JoinType.LEFTJOIN) ? "eclipselink.left-join-fetch" : "eclipselink.join-fetch";
        query.setHint(fetchHint, path);
    }

    @Override
    public void addJoin(JPAQuery query, JoinDescription joinDescription) {
        if (joinDescription.getJoinType().equals(JoinType.RIGHTJOIN)) {
            throw new JoinerException("Right join is not supported in EclipseLink!");
        }

        super.addJoin(query, joinDescription);
    }

    private String resolvePathToFieldFromRoot(String rootAlias, JoinDescription targetJoinDescription, Collection<JoinDescription> joins) {
        // Contains two elements: current attribute and it's parent (i.e. 'group' and 'users' for "group.users")
        String[] holder;

        if (targetJoinDescription.getCollectionPath() != null) {
            holder = targetJoinDescription.getCollectionPath().toString().split(DOT_ESCAPED);
        } else if (targetJoinDescription.getSinglePath() != null) {
            holder = targetJoinDescription.getSinglePath().toString().split(DOT_ESCAPED);
        } else {
            return null;
        }

        String parentSimpleName = holder[0];
        String result = holder[1];

        int i = 0;
        while ((holder = findJoinByEntitySimpleName(parentSimpleName, joins)) != null && i++ < MAX_NESTED_JOIN_DEPTH) {
            parentSimpleName = holder[0];
            result = holder[1] + "." + result;
        }

        return rootAlias + "." + result;
    }

    private String[] findJoinByEntitySimpleName(String targetAlias, Collection<JoinDescription> joins) {
        String[] result = null;
        for (JoinDescription join : joins) {
            String candidate = join.getAlias().getMetadata().getName();

            if (candidate.equals(targetAlias)) {
                if (join.getCollectionPath() != null) {
                    result = join.getCollectionPath().toString().split(DOT_ESCAPED);
                } else if (join.getSinglePath() != null) {
                    result = join.getSinglePath().toString().split(DOT_ESCAPED);
                }
                break;
            }
        }
        return result;
    }

} public class HibernateRepository extends AbstractVendorRepository implements JoinerVendorRepository {

    @Override
    public <R> ExtendedJPAQuery<R> createQuery(EntityManager entityManager) {
        return new ExtendedJPAQuery<>(entityManager, new JPAQuery<>(entityManager, HQLTemplates.DEFAULT));
    }

    @Override
    public void addFetch(JPAQuery<?> query, JoinDescription joinDescription, Collection<JoinDescription> joins, EntityPath<?> rootPath) {
        query.fetchJoin();
    }

} public interface JoinerVendorRepository {

    <R> ExtendedJPAQuery<R> createQuery(EntityManager entityManager);

    void addJoin(JPAQuery<?> query, JoinDescription joinDescription);

    void addFetch(JPAQuery<?> query, JoinDescription joinDescription, Collection<JoinDescription> joins, EntityPath<?> rootPath);

    <T> List<T> getResultList(JPAQuery<T> query, Expression<T> projection);

} public class AliasMissingException extends JoinerException {

    public AliasMissingException(String message) {
        super(message);
    }

} public class JoinerException extends RuntimeException {

    public JoinerException(String message) {
        super(message);
    }

    public JoinerException(String message, Exception exception) {
        super(message, exception);
    }
} public class JoinerExceptions {

    public static JoinerException entityNotFound() {
        return new JoinerException("FindOne returned no result, exactly 1 is expected");
    }

    public static JoinerException multipleEntitiesFound() {
        return new JoinerException("FindOne returned multiple result");
    }

} public class ExtendedJPAQuery<T> extends JPAQuery<T> {

    public ExtendedJPAQuery(EntityManager entityManager, JPAQuery<T> another) {
        super(entityManager, another.getMetadata().clone());
        clone(another);
    }

    public JPQLSerializer getSerializer(boolean forCount) {
        return serialize(forCount);
    }

} public interface JoinerQuery<T, R> extends JoinRoot {

    EntityPath<T> getFrom();

    Expression<R> getReturnProjection();

    JoinerQuery<T, R> where(Predicate where);

    Predicate getWhere();

    JoinerQuery<T, R> distinct(boolean isDistinct);

    boolean isDistinct();

    JoinerQuery<T, R> groupBy(Path<?> groupBy);

    Path<?> getGroupBy();

    JoinerQuery<T, R> having(Predicate having);

    Predicate getHaving();

    /**
     * Add join graphs to the query.
     *
     * @param names names of join graphs
     * @return this
     * @see cz.encircled.joiner.query.join.JoinGraphRegistry
     */
    JoinerQuery<T, R> joinGraphs(String... names);

    /**
     * Add join graphs to the query.
     *
     * @param names names of join graphs
     * @return this
     * @see cz.encircled.joiner.query.join.JoinGraphRegistry
     */
    JoinerQueryBase<T, R> joinGraphs(Enum... names);

    /**
     * Add join graphs to the query.
     *
     * @param names names of join graphs
     * @return this
     * @see cz.encircled.joiner.query.join.JoinGraphRegistry
     */
    JoinerQueryBase<T, R> joinGraphs(Collection<?> names);

    Set<Object> getJoinGraphs();

    /**
     * Add <b>left</b> joins for specified paths
     *
     * @param paths join paths
     * @return this
     */
    JoinerQueryBase<T, R> joins(EntityPath<?>... paths);

    JoinerQueryBase<T, R> joins(CollectionPathBase<?, ?, ?>... path);

    JoinerQueryBase<T, R> joins(JoinDescription... joins);

    JoinerQueryBase<T, R> joins(Collection<JoinDescription> joins);

    Collection<JoinDescription> getJoins();

    JoinerQueryBase<T, R> addHint(String hint, Object value);

    LinkedHashMap<String, List<Object>> getHints();

    JoinerQueryBase<T, R> addFeatures(QueryFeature... features);

    JoinerQueryBase<T, R> addFeatures(Collection<QueryFeature> features);

    List<QueryFeature> getFeatures();

    /**
     * Set offset for the query results
     *
     * @param offset value
     * @return this
     */
    JoinerQuery<T, R> offset(Long offset);

    Long getOffset();

    /**
     * Set max results for the query results
     *
     * @param limit value
     * @return this
     */
    JoinerQuery<T, R> limit(Long limit);

    Long getLimit();

    JoinerQuery<T, R> asc(Expression<?> orderBy);

    JoinerQuery<T, R> desc(Expression<?> orderBy);

    List<QueryOrder> getOrder();

    JoinerQuery<T, R> copy();

    boolean isCount();

} public interface QueryFeature {

    /**
     * This method is called before JPA query creation and allows request modification
     *
     * @param request initial joiner request
     * @param <T>     query from
     * @param <R>     query return type
     * @return modified request
     */
    <T, R> JoinerQuery<T, R> before(JoinerQuery<T, R> request);

    /**
     * This method is called just before JPA query execution and allows to modify result query directly
     *
     * @param request initial joiner request
     * @param query   JPA query
     * @return modified JPA query to be executed
     */
    default <T, R> ExtendedJPAQuery<R> after(JoinerQuery<T, R> request, ExtendedJPAQuery<R> query) {
        return query;
    }

} public class QueryOrder<T> {

    private final boolean isAsc;

    private final Expression<T> target;

    public QueryOrder(boolean isAsc, Expression<T> target) {
        this.isAsc = isAsc;
        this.target = target;
    }

    public boolean isAsc() {
        return isAsc;
    }

    public Expression<T> getTarget() {
        return target;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof QueryOrder)) return false;
        QueryOrder<?> that = (QueryOrder<?>) o;
        return isAsc == that.isAsc &&
                Objects.equals(target, that.target);
    }

    @Override
    public int hashCode() {

        return Objects.hash(isAsc, target);
    }
} public class J {

    /**
     * Aliases of nested joins are determined at runtime. To refer a nested join, this method should be used to get a correct alias.
     * For example, there is a query
     * <p>
     * <code>Q.from(QGroup.group).joins(J.left(QPerson.person).nested(J.left(QContact.contact)))</code>
     * </p>
     * To refer a <code>Contact</code> entity in the 'where' clause, one should use <code>J.path(QPerson.person.contacts).number.eq(12345)</code>
     *
     * @param path path on parent entity
     * @param <T>  any entity path
     * @return entity path with correct alias
     */
    @SuppressWarnings("unchcecked")
    public static <P extends SimpleExpression<?>, T extends EntityPath<P>> P path(CollectionPathBase<?, ?, P> path) {
        Assert.notNull(path);
        EntityPath<?> current = JoinerUtils.getDefaultPath(path);
        return ReflectionUtils.instantiate(current.getClass(), current + "_on_" + path.getMetadata().getParent());
    }

    /**
     * Aliases of nested joins are determined at runtime. To refer a nested join, this method should be used to get a correct alias.
     * For example, there is a query
     * <p>
     * <code>Q.from(QGroup.group).joins(J.left(QPerson.person).nested(J.left(QContact.contact)))</code>
     * </p>
     * To refer a <code>Contact</code> entity in the 'where' clause, one should use <code>J.path(QPerson.person, QContact.contact).number.eq(12345)</code>
     *
     * @param parent parent join path
     * @param path   target join path
     * @param <T>    any entity path
     * @return entity path with correct alias
     */
    @SuppressWarnings("unchcecked")
    public static <T extends EntityPath> T path(EntityPath<?> parent, T path) {
        if (parent != null) {
            return ReflectionUtils.instantiate(path.getClass(), path.toString() + "_on_" + parent.toString());
        }
        return path;
    }

    /**
     * Aliases of nested joins are determined at runtime. To refer a nested join, this method should be used to get a correct alias.
     * For example, there is a query
     * <p>
     * <code>Q.from(QGroup.group).joins(J.left(QPerson.person).nested(J.left(QContact.contact).nested(QStatus.status)))</code>
     * </p>
     * To refer a <code>Status</code> entity in the 'where' clause, one should use <code>J.path(QPerson.person, QContact.contact. QStatus.status).state.eq("active")</code>
     *
     * @param grandFather parent of parent join path
     * @param father      parent join path
     * @param path        target join path
     * @param <T>         any entity path
     * @return entity path with correct alias
     */
    @SuppressWarnings("unchcecked")
    public static <T extends EntityPath> T path(EntityPath<?> grandFather, EntityPath<?> father, T path) {
        Assert.notNull(father);
        Assert.notNull(grandFather);

        EntityPath<?> parentPath = path(grandFather, father);

        return path(parentPath, path);
    }

    /**
     * Add <b>left</b> join for given <code>path</code>
     *
     * @param path alias of object to be joined
     * @return join description
     */
    public static JoinDescription left(EntityPath<?> path) {
        return getBasicJoin(path).left();
    }

    /**
     * Add <b>right</b> join for given <code>path</code>
     *
     * @param path alias of object to be joined
     * @return join description
     */
    public static JoinDescription right(EntityPath<?> path) {
        return getBasicJoin(path).right();
    }

    /**
     * Add <b>left</b> join for given <code>path</code>
     *
     * @param path path to an object to be joined
     * @return join description
     */
    public static JoinDescription left(CollectionPathBase<?, ?, ?> path) {
        return getBasicJoin(JoinerUtils.getDefaultPath(path)).left();
    }

    /**
     * Add <b>inner</b> join for given <code>path</code>
     *
     * @param path alias of object to be joined
     * @return join description
     */
    public static JoinDescription inner(EntityPath<?> path) {
        return getBasicJoin(path).inner();
    }

    /**
     * Add <b>inner</b> join for given <code>path</code>
     *
     * @param path path to an object to be joined
     * @return join description
     */
    public static JoinDescription inner(CollectionPathBase<?, ?, ?> path) {
        return getBasicJoin(JoinerUtils.getDefaultPath(path)).inner();
    }

    /**
     * Collect all joins and its children to single collection
     *
     * @param joins root joins
     * @return all joins, including children
     */
    public static List<JoinDescription> unrollChildrenJoins(Collection<JoinDescription> joins) {
        List<JoinDescription> collection = new LinkedList<>();

        for (JoinDescription joinDescription : joins) {
            unrollChildrenInternal(joinDescription, collection);
        }

        return collection;
    }

    private static void unrollChildrenInternal(JoinDescription join, List<JoinDescription> collection) {
        collection.add(join);
        for (JoinDescription child : join.getChildren()) {
            unrollChildrenInternal(child, collection);
        }
    }

    private static JoinDescription getBasicJoin(EntityPath<?> path) {
        Assert.notNull(path);

        return new JoinDescription(path);
    }

} public class JoinDescription implements JoinRoot {

    private final EntityPath<?> originalAlias;
    private CollectionPathBase<?, ?, ?> collectionPath;
    private EntityPath<?> singlePath;
    private EntityPath<?> alias;
    private JoinType joinType = JoinType.LEFTJOIN;

    private boolean fetch = true;

    private Predicate on;

    private JoinDescription parent;

    private Map<String, JoinDescription> children = new LinkedHashMap<>(4);

    public JoinDescription(EntityPath<?> alias) {
        Assert.notNull(alias);

        originalAlias = alias;
        alias(alias);
    }

    public JoinDescription copy() {
        JoinDescription copy = new JoinDescription(originalAlias);
        copy.alias = alias;

        copy.children = new HashMap<>(children.size());
        children.forEach((k, v) -> copy.children.put(k, v.copy()));

        copy.fetch = fetch;
        copy.on = on;
        copy.parent = parent;
        copy.collectionPath = collectionPath;
        copy.singlePath = singlePath;
        copy.joinType = joinType;
        return copy;
    }

    public boolean isFetch() {
        return fetch;
    }

    public JoinDescription fetch(boolean fetch) {
        this.fetch = fetch;
        return this;
    }

    public JoinDescription on(Predicate on) {
        this.on = on;
        return this;
    }

    public Predicate getOn() {
        return on;
    }

    public JoinType getJoinType() {
        return joinType;
    }

    private JoinDescription joinType(final JoinType joinType) {
        this.joinType = joinType;
        return this;
    }

    public EntityPath<?> getAlias() {
        return alias;
    }

    public EntityPath<?> getOriginalAlias() {
        return originalAlias;
    }

    /**
     * Set different alias for current join
     */
    private JoinDescription alias(EntityPath<?> alias) {
        this.alias = alias;
        return this;
    }

    public CollectionPathBase<?, ?, ?> getCollectionPath() {
        return collectionPath;
    }

    public EntityPath<?> getSinglePath() {
        return singlePath;
    }

    public JoinDescription singlePath(EntityPath<?> path) {
        Assert.notNull(path);

        singlePath = path;
        collectionPath = null;
        return this;
    }

    public JoinDescription collectionPath(CollectionPathBase<?, ?, ?> path) {
        Assert.notNull(path);

        collectionPath = path;
        singlePath = null;
        return this;
    }

    public boolean isCollectionPath() {
        return collectionPath != null;
    }

    public JoinDescription inner() {
        return joinType(JoinType.INNERJOIN);
    }

    public JoinDescription left() {
        return joinType(JoinType.LEFTJOIN);
    }

    public JoinDescription right() {
        return joinType(JoinType.RIGHTJOIN);
    }

    /**
     * Add children joins to current join
     *
     * @param joins children joins
     * @return current join
     */
    public JoinDescription nested(JoinDescription... joins) {
        for (JoinDescription join : joins) {
            join.parent = this;
            join.alias(J.path(this.getAlias(), join.getOriginalAlias()));
            addJoin(join);

            reAliasChildren(join);
        }

        return this;
    }

    /**
     * Re-alias due to order of 'nested' method execution (the very last nested is executed first and it's parent is not re-aliased yet)
     */
    private void reAliasChildren(JoinDescription join) {
        for (JoinDescription child : join.children.values()) {
            child.alias(J.path(join.getAlias(), child.getOriginalAlias()));
            reAliasChildren(child);
        }
    }

    /**
     * Add children joins to current join from specified paths
     *
     * @param paths children join paths
     * @return current join
     */
    public JoinDescription nested(EntityPath<?>... paths) {
        for (EntityPath<?> path : paths) {
            nested(J.left(path));
        }

        return this;
    }

    /**
     * Add children joins to current join from specified paths
     *
     * @param paths children join paths
     * @return current join
     */
    public JoinDescription nested(CollectionPathBase<?, ?, ?>... paths) {
        for (CollectionPathBase<?, ?, ?> path : paths) {
            nested(J.left(path));
        }

        return this;
    }

    public JoinDescription getParent() {
        return parent;
    }

    public Collection<JoinDescription> getChildren() {
        return children.values();
    }

    @Override
    public Map<String, JoinDescription> getAllJoins() {
        return children;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof JoinDescription)) return false;

        JoinDescription that = (JoinDescription) o;

        if (!alias.equals(that.alias)) return false;
        return parent != null ? parent.equals(that.parent) : that.parent == null;

    }

    @Override
    public int hashCode() {
        int result = alias.hashCode();
        result = 31 * result + (parent != null ? parent.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "JoinDescription{" +
                "collectionPath=" + collectionPath +
                ", singlePath=" + singlePath +
                ", alias=" + alias +
                '}';
    }

} public interface JoinGraphRegistry {

    /**
     * Adds new join graph to the registry.
     *
     * @param graphName graph unique name. Generally, any object may be used as a name, it should have correct hashCode method. String or enum is recommended.
     * @param joins associated joins
     * @param rootClasses target class for a new join graph
     */
    void registerJoinGraph(Object graphName, Collection<JoinDescription> joins, Class<?>... rootClasses);

    /**
     * Replace an existing join graph with a new one
     *
     * @param graphName   graph unique name. Generally, any object may be used as a name, it should have correct hashCode method. String or enum is recommended.
     * @param joins       associated joins
     * @param rootClasses target class for a new join graph
     */
    void replaceJoinGraph(Object graphName, Collection<JoinDescription> joins, Class<?>... rootClasses);

    /**
     * Adds new join graph to the registry or replace an existing one.
     *
     * @param graphName   graph unique name. Generally, any object may be used as a name, it should have correct hashCode method. String or enum is recommended.
     * @param joins       associated joins
     * @param rootClasses target class for a new join graph
     */
    void registerOrReplaceJoinGraph(Object graphName, Collection<JoinDescription> joins, Class<?>... rootClasses);

    /**
     *
     * @param clazz target class
     * @param name join graph name
     * @return collection of registered join graphs
     */
    List<JoinDescription> getJoinGraph(Class<?> clazz, Object name);

    /**
     * @param clazz target class
     * @return map of all registered join graphs
     */
    Map<Object, List<JoinDescription>> getAllJoinGraphs(Class<?> clazz);

} public class Assert {

    public static void notNull(Object object) {
        if (object == null) {
            throw new IllegalArgumentException("Argument must be not null!");
        }
    }

    public static void assertThat(boolean predicate) {
        if (!predicate) {
            throw new IllegalArgumentException("Predicate must be true!");
        }
    }

} public final class JoinerUtils {

    private JoinerUtils() {

    }

    public static List<Path<?>> collectPredicatePaths(Expression<?> expression) {
        List<Path<?>> result = new ArrayList<>();
        collectPredicatePathsInternal(expression, result);
        return result;
    }

    // TODO cache? and test
    /**
     * Find default path of an entity (e.g. QUser.user for QUser class)
     */
    public static <T extends EntityPath<?>> T getDefaultPath(Class<T> entityPath) {
        // Default path name is equal to the class name without `Q` prefix
        String name = StringUtils.uncapitalize(entityPath.getSimpleName().substring(1));

        Field f = ReflectionUtils.findField(entityPath, name);
        if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isFinal(f.getModifiers())) {
            // TODO iterate deeper...
            f = ReflectionUtils.findField(entityPath, name + "1");
        }
        return (T) ReflectionUtils.getField(f, null);
    }

    /**
     * Find default path of an entity (e.g. QUser.user for QUser class)
     */
    public static <T extends EntityPath<?>> T getDefaultPath(CollectionPathBase<?, ?, ?> path) {
        Class qClass = (Class<?>) ReflectionUtils.getField(ReflectionUtils.findField(path.getClass(), "queryType"), path);
        return (T) getDefaultPath(qClass);
    }

    private static void collectPredicatePathsInternal(Expression<?> expression, List<Path<?>> paths) {
        if (expression instanceof Path) {
            paths.add((Path<?>) expression);
        } else if (expression instanceof Operation) {
            for (Expression exp : ((Operation<?>) expression).getArgs()) {
                collectPredicatePathsInternal(exp, paths);
            }
        }
    }

} public class ReflectionUtils {

    @SuppressWarnings("unchecked")
    public static <T> T instantiate(Class<?> generatedClass, Object... arguments) {
        Assert.notNull(generatedClass);

        try {
            // TODO nulls?
            Class[] classesOfArgs = new Class[arguments.length];
            for (int i = 0; i < arguments.length; i++) {
                classesOfArgs[i] = arguments[i].getClass();
            }

            Constructor<?> constructor = findConstructor(generatedClass, classesOfArgs);
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }
            return (T) constructor.newInstance(arguments);
        } catch (Exception e) {
            throw new JoinerException("Failed to create new instance of " + generatedClass, e);
        }
    }

    public static Constructor<?> findConstructor(Class<?> clazz, Class[] argumentClasses) {
        outer:
        for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
            final Class<?>[] params = constructor.getParameterTypes();
            if (params.length == argumentClasses.length) {
                for (int i = 0; i < argumentClasses.length; i++) {
                    if (!params[i].isAssignableFrom(argumentClasses[i])) {
                        continue outer;
                    }
                }
                return constructor;
            }
        }

        throw new RuntimeException("Constructor with params [" + Arrays.toString(argumentClasses) + "] is not found in [" + clazz + "]");
    }

    public static Field findField(Class<?> clazz, String name) {
        Assert.notNull(clazz);
        Assert.notNull(name);

        try {
            return clazz.getDeclaredField(name);
        } catch (NoSuchFieldException e) {
            return null;
        }
    }

    public static void setField(Field field, Object targetObject, Object value) {
        Assert.notNull(field);

        makeAccessible(field);
        try {
            field.set(targetObject, value);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static Object getField(String field, Object object) {
        return getField(findField(object.getClass(), field), object);
    }

    public static Object getField(Field field, Object object) {
        Assert.notNull(field);
        makeAccessible(field);

        try {
            return field.get(object);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static Set<Class> getSubclasses(Class<?> parent, EntityManager entityManager) {
        return entityManager.getMetamodel().getEntities().stream()
                .filter(entityType -> parent != entityType.getJavaType() && parent.isAssignableFrom(entityType.getJavaType()))
                .map(Type::getJavaType)
                .collect(Collectors.toCollection(HashSet::new));
    }

    private static void makeAccessible(Field field) {
        if (field != null && !field.isAccessible()) {
            field.setAccessible(true);
        }
    }

}
import com.google.common.collect.ArrayListMultimap;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.AbstractJPAQuery;
import com.querydsl.jpa.impl.JPAQuery;
import cz.encircled.joiner.core.vendor.EclipselinkRepository;
import cz.encircled.joiner.core.vendor.HibernateRepository;
import cz.encircled.joiner.core.vendor.JoinerVendorRepository;
import cz.encircled.joiner.exception.AliasMissingException;
import cz.encircled.joiner.exception.JoinerException;
import cz.encircled.joiner.exception.JoinerExceptions;
import cz.encircled.joiner.query.ExtendedJPAQuery;
import cz.encircled.joiner.query.JoinerQuery;
import cz.encircled.joiner.query.QueryFeature;
import cz.encircled.joiner.query.QueryOrder;
import cz.encircled.joiner.query.join.J;
import cz.encircled.joiner.query.join.JoinDescription;
import cz.encircled.joiner.query.join.JoinGraphRegistry;
import cz.encircled.joiner.util.Assert;
import cz.encircled.joiner.util.JoinerUtils;
import cz.encircled.joiner.util.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.ThreadSafe;
import javax.persistence.EntityManager;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
package cz.encircled.joiner.core;



/**
 * Base class of Joiner. Contains basic database operations.
 * <p>
 *     In spring-based environment can be instantiated using spring JoinerConfiguration.
 * </p>
 * <p>
 *     For repository-per-entity approach this class should not be accessed directly. Instead repositories can implement {@link JoinerRepository}.
 * </p>
 *
 * @author Kisel on 26.01.2016.
 */
@ThreadSafe
public class Joiner {

    private static final Logger log = LoggerFactory.getLogger(Joiner.class);

    private EntityManager entityManager;

    private JoinerVendorRepository joinerVendorRepository;

    private JoinGraphRegistry joinGraphRegistry;