public class EntityIndexInfo<V> {
    private static final Logger LOGGER = LoggerFactory.getLogger(EntityIndexInfo.class);

    private Set<String> indexesMap = new LinkedHashSet<>();
    private Set<String> indexPrefixMap = new LinkedHashSet<>();

    public EntityIndexInfo(Class<V> entityClazz, List<DBObject> indexInfo) {
        List<String> indexStrings = new ArrayList<>();
        for (DBObject dbObject : indexInfo) {
            BasicDBObject dbo = (BasicDBObject) dbObject;
            BasicDBObject keyObj = (BasicDBObject) dbo.get("key");
            List<String> components = new ArrayList<>();
            for (Map.Entry<String, Object> e : keyObj.entrySet()) {
                String val = e.getValue().toString();
                String mongoIndexNameStr = e.getKey();
                String entityFieldName = getJavaFieldNames(entityClazz, mongoIndexNameStr);
                if (entityFieldName == null) {
                    LOGGER.error("Cannot find the mapping from MongoDB index '" + mongoIndexNameStr + "' to Java entities, skipping...");
                    continue;
                }
                Integer intVal;
                if ("hashed".equals(val)) {
                    intVal = 1;
                } else {
                    try {
                        intVal = Double.valueOf(val).intValue();
                    } catch (NumberFormatException e1) {
                        LOGGER.error("Unrecognized index: " + mongoIndexNameStr + "->" + e.getValue());
                        continue;
                    }
                }
                switch (intVal) {
                    case -1:
                        components.add("-" + entityFieldName);
                        break;
                    case 1:
                        components.add(entityFieldName);
                        break;
                    default:
                        LOGGER.warn("Unrecognized index: " + mongoIndexNameStr + "->" + e.getValue());
                        break;
                }
            }
            String morphiaIndex = Joiner.on(",").join(components);
            LOGGER.info("Mapped MongoDB index for " + entityClazz.getName() + " '" + keyObj.toString() + "' to '" + morphiaIndex + "'  ");

            indexStrings.add(morphiaIndex);
        }
        computeMaps(indexStrings);
    }

    private String getJavaFieldNames(Class<?> entityClazz, String mongoIndexName) {
        List<String> comps = Splitter.on('.').splitToList(mongoIndexName);
        List<String> javaComps = new ArrayList<>();
        Class<?> currentClazz = entityClazz;
        for (String comp : comps) {
            MappedClass mc = TypeInfoUtil.MAPPER.getMappedClass(currentClazz);
            MappedField mf = mc.getMappedField(comp);
            if (mf == null) {
                return null;
            }
            javaComps.add(mf.getJavaFieldName());
            //noinspection unchecked
            if (mf.getType().isAssignableFrom(List.class)) {
                currentClazz = (Class<?>) mf.getSubType();
            } else {
                currentClazz = mf.getConcreteType();
            }
        }

        return Joiner.on('.').join(javaComps);
    }

    private void computeMaps(List<String> indexStrings) {
        for (String indexString : indexStrings) {
            indexesMap.add(indexString);
            String sanVal = indexString.replace("-", "");

            List<String> split = Splitter.on(",").splitToList(sanVal);
            String acc = split.get(0);

            indexPrefixMap.add(acc);
            for (int i = 1; i < split.size(); i++) {
                acc += "," + split.get(i);
                indexPrefixMap.add(acc);
            }
        }
    }

    public Set<String> getIndexesMap() {
        return indexesMap;
    }

    public Set<String> getIndexPrefixMap() {
        return indexPrefixMap;
    }

} public class EntityInfo<V> {
    private String idFieldName;
    private Class<?> idFieldClazz;
    private MappedField mappedIdField;
    private static final Map<Class<?>, EntityInfo<?>> CACHE = new HashMap<>();

    @SuppressWarnings("unchecked")
    public static <V> EntityInfo<V> get(Class<V> clazz) {
        EntityInfo<V> entityInfo = (EntityInfo<V>) CACHE.get(clazz);
        if (entityInfo == null) {
            //TODO: recheck whether it's 100% correct
            synchronized (clazz) {
                entityInfo = (EntityInfo<V>) CACHE.get(clazz);
                if (entityInfo == null) {
                    entityInfo = new EntityInfo<>(clazz);
                    CACHE.put(clazz, entityInfo);
                }
            }
        }
        return entityInfo;
    }

    private EntityInfo(Class<V> clazz) {
        MappedClass mc = TypeInfoUtil.MAPPER.getMappedClass(clazz);
        mappedIdField = mc.getMappedIdField();
        if(mappedIdField == null) {
            throw new RestDslException("No id field annotated on " + clazz.getCanonicalName(), RestDslException.Type.ENTITY_ERROR);
        }
        idFieldName = mappedIdField.getJavaFieldName();
        idFieldClazz = mappedIdField.getConcreteType();
    }

    public String getIdFieldName() {
        return idFieldName;
    }

    public MappedField getMappedIdField() {
        return mappedIdField;
    }

    public Class<?> getIdFieldClazz() {
        return idFieldClazz;
    }

    public <K> K getIdFieldValue(V entity) {
        //noinspection unchecked
        return (K) mappedIdField.getFieldValue(entity);
    }

    public void setIdFieldValue(V entity, Object val) {
        mappedIdField.setFieldValue(entity, val);
    }

} public class RestDslException extends RuntimeException {
    //TODO: generialize types, rethink them.
    // TODO: need something like generic conflict state
    public enum Type {
        // DB constraint violation
        DUPLICATE_KEY,

        // Entity provided is not valid
        ENTITY_ERROR,

        // Unknown or implementation error
        GENERAL_ERROR,

        // params supplied via HTTP are invalid
        PARAMS_ERROR,

        // Service query contains errors
        QUERY_ERROR
    }

    // default type
    private Type type = Type.GENERAL_ERROR;

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

    public RestDslException(String message, Type type) {
        super(message);
        this.type = type;
    }

    public RestDslException(String message, Throwable cause, Type type) {
        super(message, cause);
        this.type = type;
    }

    public Type getType() {
        return type;
    }

} public class NoOpStatsReporter implements StatsReporter {
    public static final NoOpStatsReporter INSTANCE = new NoOpStatsReporter();

    private NoOpStatsReporter() {
    }

    @Override
    public void timing(String key, long value, double sampleRate) {

    }

    @Override
    public void increment(String key, int magnitude, double sampleRate) {

    }

    @Override
    public void gauge(String key, double value) {

    }
} public interface StatsReporter {

    void timing(String key, long value, double sampleRate);

    void increment(String key, int magnitude, double sampleRate);

    void gauge(String key, double value);


    default void timing(String key, long value) {
        timing(key, value, 1.0);
    }

    default void decrement(String key) {
        increment(key, -1, 1.0);
    }

    default void decrement(String key, int magnitude) {
        decrement(key, magnitude, 1.0);
    }

    default void increment(String key) {
        increment(key, 1, 1.0);
    }

    default void increment(String key, int magnitude) {
        increment(key, magnitude, 1.0);
    }

    default void decrement(String key, int magnitude, double sampleRate) {
        increment(key, -magnitude, sampleRate);
    }
} public class StatsTimingWrapper implements Closeable {
    private final StatsReporter statsReporter;
    private final String key;
    private final long time;

    public static StatsTimingWrapper of(StatsReporter statsService, String key) {
        return new StatsTimingWrapper(statsService, key);
    }

    private StatsTimingWrapper(StatsReporter statsReporter, String key) {
        this.statsReporter = statsReporter;
        this.key = key;
        time = System.currentTimeMillis();
    }

    @Override
    public void close() {
        statsReporter.timing(key, System.currentTimeMillis() - time);
    }
} public final class ServiceQuery<K> {
    private static final int MAX_LIMIT = 10000;

    private Integer limit;
    private Integer originalLimit;
    private int offset = 0;
    private boolean countTotalItems = true;
    private String order;
    private Set<String> fields;
    private Collection<K> ids;
    private Multimap<String, Object> criteria;
    private boolean indexValidation = true;
    private String groupBy;
    private Boolean countOnly = false;

    // field that ensures that all subelements of these fields must match in "sync"
    // it's like $elemMatch in Mongo https://docs.mongodb.com/v3.2/reference/operator/query/elemMatch/
    private Set<String> syncMatch;

    // calculated fields
    private String queryShape;

    public boolean isCountTotalItems() {
        return countTotalItems;
    }

    public int getLimit() {
        return limit;
    }

    public int getOffset() {
        return offset;
    }

    public String getOrder() {
        return order;
    }

    public String getGroupBy() {
        return groupBy;
    }

    public Boolean getCountOnly() {
        return countOnly;
    }

    public Set<String> getFields() {
        return fields;
    }

    public Multimap<String, Object> getCriteria() {
        return criteria;
    }

    public boolean isIndexValidation() {
        return indexValidation;
    }

    public Set<String> getSyncMatch() {
        return syncMatch;
    }

    public String getQueryShape() {
        return queryShape;
    }

    public static <K> ServiceQuery<K> all() {
        return new ServiceQueryBuilder<K>().build();
    }

    public static <K> ServiceQuery<K> byId(K id) {
        return new ServiceQueryBuilder<K>().ids(Collections.singletonList(id)).build();
    }

    public static <K> ServiceQuery<K> byIds(Iterable<K> ids) {
        return new ServiceQueryBuilder<K>().ids(ids).build();
    }


    @SuppressWarnings("unchecked")
    public static <K> ServiceQuery<K> byCriteria(String key, Object value) throws RestDslException {
        return new ServiceQueryBuilder<K>().withCriteria(key, Collections.singletonList(value)).build();
    }

    public Collection<K> getIdList() {
        return ids;
    }

    protected ServiceQuery() {
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof ServiceQuery<?> && toString().equals(o.toString());
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public String toString() {
        return toUrlPart();
    }


    public static <K> ServiceQueryBuilder<K> builder() {
        return new ServiceQueryBuilder<>();
    }

    public static class ServiceQueryBuilder<K> {
        // on purpose in the buider, so that the query is already formed when was built
        private ServiceQueryParams serviceQueryParams = ServiceQueryParams.ALL_QUERY_PARAMS;

        private ServiceQueryBuilder() {
        }

        private ServiceQuery<K> query = new ServiceQuery<>();

        public ServiceQueryBuilder<K> offset(Integer offset) throws RestDslException {
            if (offset != null) {
                if (offset < 0) {
                    throw new RestDslException("Offset cannot be less than 0");
                }
                query.offset = offset;
            }
            return this;
        }

        public ServiceQueryBuilder<K> limit(Integer limit) throws RestDslException {
            if (limit != null) {
                if (limit < 0) {
                    throw new RestDslException("Limit cannot be negative", RestDslException.Type.QUERY_ERROR);
                }
                query.limit = limit;
            }
            query.originalLimit = query.limit;
            return this;
        }

        public ServiceQueryBuilder<K> fields(Collection<String> fields) {
            if (fields != null) {
                query.fields = Collections.unmodifiableSet(Sets.newHashSet(fields));
            }
            return this;
        }

        public ServiceQueryBuilder<K> syncMatch(Collection<String> syncMatch) {
            if (syncMatch != null) {
                query.syncMatch = Collections.unmodifiableSet(Sets.newHashSet(syncMatch));
            }
            return this;
        }

        public ServiceQueryBuilder<K> indexValidation(Boolean indexValidation) {
            if (indexValidation != null) {
                query.indexValidation = indexValidation;
            }
            return this;
        }


        public ServiceQueryBuilder<K> order(String order) {
            query.order = order;
            return this;
        }

        public ServiceQueryBuilder<K> groupBy(String groupBy) {
            query.groupBy = groupBy;
            return this;
        }

        public ServiceQueryBuilder<K> id(K id) {
            query.ids = Collections.singletonList(id);
            return this;
        }

        public ServiceQueryBuilder<K> ids(Iterable<K> ids) {
            if (ids instanceof Collection) {
                query.ids = (Collection<K>) ids;
            } else {
                query.ids = Lists.newArrayList(ids);
            }
            return this;
        }

        public ServiceQueryBuilder<K> countTotalItems(Boolean countTotalItems) {
            if (countTotalItems != null) {
                query.countTotalItems = countTotalItems;
            }
            return this;
        }

        public ServiceQueryBuilder<K> withCriterion(String key, Object value) throws RestDslException {
            return withCriteria(key, Collections.singletonList(value));
        }

        public ServiceQueryBuilder<K> withCriteria(String key, Collection<?> value) throws RestDslException {
            if (value == null || value.isEmpty()) {
                throw new RestDslException("Criteria values for field '" + key + "' cannot be empty",
                        RestDslException.Type.QUERY_ERROR);
            }
            if (query.criteria == null) {
                query.criteria = HashMultimap.create();
            }

            query.criteria.putAll(key, value);
            return this;
        }

        // VZ: varargs are bad!
//        public ServiceQueryBuilder<K> withCriteria(String key, Object ... value) throws RestDslException {
//           return withCriteria(key, Arrays.asList(value));
//        }

        public ServiceQueryBuilder<K> withServiceQueryParams(ServiceQueryParams serviceQueryParams) {
            this.serviceQueryParams = serviceQueryParams;
            return this;
        }

        public ServiceQuery<K> build() {
            applyServiceQueryParams();
            query.calculateQueryShape();
            return query;
        }

        private void applyServiceQueryParams() {
            // CRITERIA
            Multimap<String, Object> defaultCriteria = serviceQueryParams.getDefaultCriteria();
            if (defaultCriteria != null && !defaultCriteria.isEmpty()) {
                Multimap<String, Object> combinedCriteria = query.criteria != null ? LinkedHashMultimap.create(query.criteria) : LinkedHashMultimap.create();
                // Merge defaultCriteria and user defined criteria
                for (String key : defaultCriteria.keys()) {
                    if (!combinedCriteria.containsKey(key)) {
                        combinedCriteria.putAll(key, defaultCriteria.get(key));
                    }
                }
                // ServiceQuery.ANY_VALUE is used as a placeholder for "$any" in criteria (see ServiceResource.parseRequest)
                combinedCriteria.keys().stream()
                        .filter(key -> combinedCriteria.get(key).size() == 1 &&
                                combinedCriteria.get(key).contains(ServiceQueryReservedValue.ANY)
                        ).forEach(combinedCriteria::removeAll);
                query.criteria = combinedCriteria;
            }

            // FIELDS
            if (query.fields == null) {
                query.fields = serviceQueryParams.getDefaultFields();
            }

            // LIMIT
            if (query.limit != null && query.limit == 0) {
                query.countOnly = true;
            }
            int curLimit = query.limit == null ? serviceQueryParams.getDefaultLimit() : query.limit;
            query.limit = curLimit > MAX_LIMIT || curLimit < 0 ? MAX_LIMIT : curLimit;

        }
    }

    //TODO : make sure that & won't affect toUrl or query shape
    public String toUrlPart() {
        StringBuilder sb = new StringBuilder();
        if (ids != null && !ids.isEmpty()) {
            sb.append(Joiner.on(",").join(ids));
        } else {
            sb.append("-;");
        }
        if (criteria != null) {
            for (String c : criteria.keySet()) {
                sb.append(c).append("=");
                sb.append(Joiner.on(',').join(criteria.get(c).stream()
                        .map(this::criteriaValToStr)
                        .collect(Collectors.toList()))).append(";");
            }
        }
        sb.append("?");
        sb.append("limit=").append(getLimit()).append("&");
        if (getOffset() != 0) {
            sb.append("offset=").append(getOffset()).append("&");
        }
        if (fields != null) {
            sb.append("fields=").append(Joiner.on(',').join(getFields())).append("&");
        }

        if (syncMatch != null) {
            sb.append("syncMatch=").append(Joiner.on(',').join(getSyncMatch())).append("&");
        }

        if (!indexValidation) {
            sb.append("indexValidation=false&");
        }

        if (order != null) {
            sb.append("order=").append(getOrder()).append("&");
        }

        return sb.toString();
    }

    private String criteriaValToStr(Object v) {
        if (v == null) {
            return ServiceQueryReservedValue.NULL.toString();
        }
        if (v instanceof ServiceQueryReservedValue) {
            return ((ServiceQueryReservedValue) v).getStrVal();
        }
        if (v instanceof Date) {
            return new ThreadLocalDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format((Date) v);
        }
        return v.toString();
    }

    private final static String QS_FIELD_SEP = "-";
    private final static String QS_KV_SEP = "-";
    private final static String QS_VAL_JOINER = "_";

    private void calculateQueryShape() {
        StringBuilder sb = new StringBuilder();
        if (!CollectionUtils.isEmpty(ids)) {
            sb.append("IDS").append(QS_FIELD_SEP);
        } else {
            sb.append(QS_FIELD_SEP);
        }

        if (criteria != null) {
            sb.append("CRITERIA").append(QS_KV_SEP);
            sb.append(Joiner.on(QS_VAL_JOINER).join(criteria.keySet()));
        }

        if (order != null) {
            sb.append(QS_FIELD_SEP).append("ORDER").append(QS_KV_SEP).append(order);
        }
        if (groupBy != null) {
            sb.append(QS_FIELD_SEP).append("GROUPBY").append(QS_KV_SEP).append(groupBy);
        }

        if (syncMatch != null) {
            sb.append(QS_FIELD_SEP).append("SYNCMATCH").append(QS_KV_SEP).append(syncMatch);
        }

        if (originalLimit != null) {
            sb.append(QS_FIELD_SEP).append("LIMIT");
        }

        queryShape = sb.toString();
    }

} public class ServiceQueryInfo<K> {
    private ServiceQuery<K> query;
    private String urlPart;
    private boolean safeQuery;

    public ServiceQueryInfo(ServiceQuery<K> query, boolean safeQuery) {
        this.query = query;
        this.urlPart = query.toUrlPart();
        this.safeQuery = safeQuery;
    }

    public ServiceQuery<?> getQuery() {
        return query;
    }

    public String getUrlPart() {
        return urlPart;
    }

    public boolean isSafeQuery() {
        return safeQuery;
    }
} public enum ServiceQueryReservedValue {
    NULL("$null"), 
    ANY("$any"), 
    EXISTS("$exists");

    private final String strVal;
    private final static Map<String, ServiceQueryReservedValue> map = Maps.newHashMap();
    static {
        for (ServiceQueryReservedValue v : values()) {
            map.put(v.strVal, v);
        }
    }

    public static ServiceQueryReservedValue fromString(String strVal) {
        return map.get(strVal);
    }

    ServiceQueryReservedValue(String strVal) {
        this.strVal = strVal;
    }

    public String getStrVal() {
        return strVal;
    }

    @Override
    public String toString() {
        return strVal;
    }
} @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
@JsonPropertyOrder({"items", "totalItems"})
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntityList<T> implements EntityContainer<T>{
    private List<T> items;
    private final Long totalItems;

    private EntityList() {
        this.totalItems = null;
    }

    public EntityList(List<T> items, Long totalItems) {
        Preconditions.checkNotNull(items, "Items list cannot be null");
        this.items = items;
        this.totalItems = totalItems;
    }

    @Override
    public Iterator<T> iterator() {
        return items.iterator();
    }

    public List<T> getItems() {
        return items;
    }

    @Override
    public Long getTotalItems() {
        return totalItems;
    }
} @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
@JsonPropertyOrder({"items", "totalItems"})
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntityMultimap<T> implements EntityContainer<T> {
    private Map<Object, EntityList<T>> items;
    private final Long totalItems;

    private EntityMultimap() {
        this.totalItems = null;
    }

    public EntityMultimap(Map<Object, EntityList<T>> items, Long totalItems) {
        Preconditions.checkNotNull(items, "Items map cannot be null");
        this.items = items;
        this.totalItems = totalItems;
    }

    @Override
    public Iterator<T> iterator() {
        Iterator<Iterator<T>> its = Iterables.transform(items.values(), EntityList::iterator).iterator();
        return Iterators.concat(its);
    }

    public Map<Object, EntityList<T>> getItems() {
        return items;
    }

    @Override
    public Long getTotalItems() {
        return totalItems;
    }
} @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntityResult<T> implements EntityContainer<T> {
    private EntityList<T> list;
    private EntityMap<T> map;
    private EntityMultimap<T> multimap;

    @JsonIgnore
    private EntityContainer<T> currentContainer;

    public EntityResult(List<T> items, Long totalItems) {
        this(new EntityList<>(items, totalItems));
    }

    // for Jackson
    private EntityResult() {
        this.currentContainer = null;
    }

    public EntityResult(EntityList<T> list) {
        Preconditions.checkNotNull(list, "List cannot be null");
        this.list = list;
        this.currentContainer = list;
    }

    public EntityResult(EntityMap<T> map) {
        Preconditions.checkNotNull(map, "Map cannot be null");
        this.map = map;
        this.currentContainer = map;
    }

    public EntityResult(EntityMultimap<T> multimap) {
        Preconditions.checkNotNull(multimap, "Multimap cannot be null");
        this.multimap = multimap;
        this.currentContainer = multimap;
    }

    @Override
    public Iterator<T> iterator() {
        return getCurrentEntityContainer().iterator();
    }

    @Override
    @JsonIgnore
    public Long getTotalItems() {
        return getCurrentEntityContainer().getTotalItems();
    }

    @JsonIgnore
    /* NOTE: use with care to avoid necessary transformation into List. Use as iterable if possible
     */
    public List<T> asList() {
        if (list != null) {
            return list.getItems();
        } else {
            return Lists.newArrayList(this);
        }
    }

    @JsonIgnore
    public boolean isEmpty() {
        return !iterator().hasNext();
    }

    /**
     * Returns null if collection is empty
     *
     * @return just one element from the result
     * Typically used when it's clear that we have at most one result
     */
    @JsonIgnore
    public T getOne() {
        Iterator<T> it = iterator();
        if (!it.hasNext()) {
            return null;
        }
        return it.next();
    }

    public EntityList<T> getList() {
        return list;
    }

    public EntityMap<T> getMap() {
        return map;
    }

    public EntityMultimap<T> getMultimap() {
        return multimap;
    }


    // *** HELPERS ***

    // in case Jackson deserialized EntityResult, currentContainer will be null, need to set it
    private EntityContainer<T> getCurrentEntityContainer() {
        if (currentContainer == null) {
            synchronized (this) {
                if (currentContainer == null) {
                    if (list != null) {
                        currentContainer = list;
                    } else if (map != null) {
                        currentContainer = map;
                    } else if (multimap != null) {
                        currentContainer = multimap;
                    }
                }
            }
        }
        return currentContainer;
    }

    /**
     * need to read response into EntityResult with certain type without a need to subclass EntityResult with specific T
     * One cannot say in java "read this JSON into EntityResult of T, where T is a Publication.
     * Type info is erased at runtime. Therefore Jersey has some helper classes to address such an issue.
     * <p>
     * example usage: EntityResult&lt;Publication&gt; result = response.readEntity(EntityResult.getGenericType(Publication.class));
     * <p>
     * this won't work: EntityResult&lt;Publication&gt; result = response.readEntity(EntityResult.class);
     * LinkedHashMap will be put as T and at some point you will get:
     * java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to net.researchgate.restler.domain.Publication
     */

    public static <T> GenericType<EntityResult<T>> getGenericType(Class<T> entityClazz) {
        ParameterizedType parameterizedGenericType = new ParameterizedType() {
            public Type[] getActualTypeArguments() {
                return new Type[]{entityClazz};
            }

            public Type getRawType() {
                return EntityResult.class;
            }

            public Type getOwnerType() {
                return EntityResult.class;
            }
        };

        return new GenericType<>(parameterizedGenericType);
    }

} public class TypeInfoUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(TypeInfoUtil.class);

    public static final Mapper MAPPER = new Morphia().getMapper();

    private static Map<Class, TypeConverter<?>> converters = new HashMap<>();


    public static void addConverter(TypeConverter<?> converter) {
        if (converters.containsKey(converter.getType())) {
            LOGGER.warn("Converter for class " + converter.getType().getName() + " has been already registered");
        }
        converters.put(converter.getType(), converter);
    }


    //TODO: converters via annotations
    @SuppressWarnings("unchecked")
    // fieldName and parentClazz are needed for potential reflection to get serde info from a field
    public static <K> K getValue(String strVal, String fieldName, Class<K> clazz, Class<?> parentClazz) throws RestDslException {
        try {
            ServiceQueryReservedValue reservedValue = ServiceQueryReservedValue.fromString(strVal);
            if (reservedValue != null) {
                if (reservedValue == ServiceQueryReservedValue.NULL) {
                    return null;
                }
                return (K) reservedValue;
            }
            if (converters.containsKey(clazz)) {
                return (K) converters.get(clazz).deserialize(strVal);
            }

            if (clazz == String.class) {
                return (K) strVal;
            } else if (clazz == Long.class || clazz == long.class) {
                return (K) Long.valueOf(strVal, 10);
            } else if (clazz == Integer.class || clazz == int.class) {
                return (K) Integer.valueOf(strVal, 10);
            } else if (clazz == ObjectId.class) {
                return (K) new ObjectId(strVal);
            } else if (clazz == Boolean.class || clazz == boolean.class) {
                return (K) Boolean.valueOf(strVal);
            } else if (clazz.isEnum()) {
                Class c = clazz;
                Converter<String, K> converter = Enums.stringConverter(c);
                return converter.convert(strVal);
            } else if (clazz == Date.class) {
                return (K) new Date(Long.valueOf(strVal, 10));
            }
        } catch (Exception e) {
            throw new RestDslException("Cannot convert '" + strVal + "' to object of class " + clazz.getName(), e, RestDslException.Type.PARAMS_ERROR);
        }
        throw new RestDslException("Unsupported type " + clazz.getName() + " for value '" + strVal + "'", RestDslException.Type.PARAMS_ERROR);
    }


    //TODO: get rid of Pair
    public static Pair<Class<?>, Class<?>> getFieldExpressionClazz(Class<?> cl, String fieldExpression) throws RestDslException {
        return getFieldExpressionClazz(cl, null, fieldExpression, Splitter.on('.').splitToList(fieldExpression), 0);
    }

    private static Pair<Class<?>, Class<?>> getFieldExpressionClazz(Class<?> cl, Class<?> prevClazz, String fieldExpression, List<String> fields, int ptr) throws RestDslException {
        if (ptr >= fields.size()) {
            return Pair.of(cl, prevClazz);
        }
        String fieldName = fields.get(ptr);

        //TODO: cache mapped info
        MappedClass mc = MAPPER.getMappedClass(cl);
        MappedField nestedField = mc.getMappedFieldByJavaField(fieldName);
        if (nestedField == null) {
            throw new RestDslException("Cannot find field " + fieldExpression, RestDslException.Type.PARAMS_ERROR);
        }
        Class<?> nestedClazz = nestedField.getType();
        //TODO: better type checking
        if (nestedClazz.isAssignableFrom(List.class)) {
            Type subType = nestedField.getSubType();
            return getFieldExpressionClazz((Class<?>) subType, cl, fieldExpression, fields, ptr + 1);
        } else {
            return getFieldExpressionClazz(nestedClazz, cl, fieldExpression, fields, ptr + 1);
        }
    }
} public class ServiceQueryUtil {
    private static final String CRITERIA_SEPARATOR = "__";

    public static class ParsedQueryField {
        private String fullCriteria;
        private String fieldName;

        private ParsedQueryField(String fullCriteria, String fieldName) {
            this.fullCriteria = fullCriteria;
            this.fieldName = fieldName;
        }

        public String getFullCriteria() {
            return fullCriteria;
        }

        public String getFieldName() {
            return fieldName;
        }
    }

    public static ParsedQueryField parseQueryField(String providedField) {

        if (providedField.contains(CRITERIA_SEPARATOR)) {
            // supporting cases like 'rating__gte=20' to avoid URL encoding while testing from the browser URL bar
            List<String> split = Splitter.on(CRITERIA_SEPARATOR).splitToList(providedField);
            if (split.size() != 2) {
                throw new RestDslException("Field '" + providedField + "' must not contain more than 1 separator", RestDslException.Type.PARAMS_ERROR);
            }
            String op = parseCriteriaOperation(split.get(1));
            return new ParsedQueryField(split.get(0) + " " + op, split.get(0));
        } else {
            return new ParsedQueryField(providedField, getPlainFieldName(providedField));
        }
    }

    private static String parseCriteriaOperation(String op) {
        switch (op) {
            case "gt":
                return ">";
            case "gte":
                return ">=";
            case "lt":
                return "<";
            case "lte":
                return "<=";
            case "ne":
                return "<>";
            default:
                throw new RestDslException("Unsupported operation: " + op, RestDslException.Type.PARAMS_ERROR);
        }
    }

    private static String getPlainFieldName(String criteriaKey) {
        return Splitter.on(' ').split(criteriaKey).iterator().next();
    }

}
import com.google.common.base.Joiner;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import net.researchgate.restdsl.domain.EntityIndexInfo;
import net.researchgate.restdsl.domain.EntityInfo;
import net.researchgate.restdsl.exceptions.RestDslException;
import net.researchgate.restdsl.metrics.NoOpStatsReporter;
import net.researchgate.restdsl.metrics.StatsReporter;
import net.researchgate.restdsl.metrics.StatsTimingWrapper;
import net.researchgate.restdsl.queries.ServiceQuery;
import net.researchgate.restdsl.queries.ServiceQueryInfo;
import net.researchgate.restdsl.queries.ServiceQueryReservedValue;
import net.researchgate.restdsl.results.EntityList;
import net.researchgate.restdsl.results.EntityMultimap;
import net.researchgate.restdsl.results.EntityResult;
import net.researchgate.restdsl.types.TypeInfoUtil;
import net.researchgate.restdsl.util.ServiceQueryUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import dev.morphia.Datastore;
import dev.morphia.dao.BasicDAO;
import dev.morphia.query.Query;
import dev.morphia.query.UpdateOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
package net.researchgate.restdsl.dao;



/**
 * This Dao implements common access to the underlying mongo collection.
 * Use this dao if you want to make sure that your data is only written to in a controlled way.
 *
 * Supported operations
 * - get by restler dsl
 * - delete by id
 *
 * If you want to simply expose CRUD via REST, use a {@link MongoServiceDao}.
 *
 * @param <V> Type of the entity
 * @param <K> Type of the entity's id field
 */
public class MongoBaseServiceDao<V, K> implements BaseServiceDao<V, K>{
    private static final Logger LOGGER = LoggerFactory.getLogger(MongoBaseServiceDao.class);

    private static final String QUERY_KEY = "queries.shapes.%s.%%H";
    protected final String collectionName;
    protected final BasicDAO<V, K> morphiaDao;
    protected final Class<V> entityClazz;
    protected final EntityInfo<V> entityInfo;
    protected final EntityIndexInfo<V> entityIndexInfo;