/** * */ package com.ebay.cloud.cms.typsafe.service; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang3.StringUtils; import com.ebay.cloud.cms.typsafe.entity.CMSQuery; import com.ebay.cloud.cms.typsafe.entity.CMSQueryResult; import com.ebay.cloud.cms.typsafe.entity.GenericCMSEntity; import com.ebay.cloud.cms.typsafe.entity.ICMSEntity; import com.ebay.cloud.cms.typsafe.entity.internal.QueryFilterBuilder; import com.ebay.cloud.cms.typsafe.exception.CMSEntityException; import com.ebay.cloud.cms.typsafe.metadata.model.MetaClass; import com.ebay.cloud.cms.typsafe.metadata.model.MetaField; import com.ebay.cloud.cms.typsafe.metadata.model.MetaRelationship; import com.ebay.cloud.cms.typsafe.restful.CMSQueryConstants; import com.ebay.cloud.cms.typsafe.restful.QueryRestExecutor; import com.ebay.cloud.cms.typsafe.restful.RestExecutor.HttpRequest; import com.ebay.cloud.cms.typsafe.restful.URLBuilder; import com.ebay.cloud.cms.typsafe.restful.URLBuilder.Url; import com.google.common.base.Preconditions; import com.google.common.base.Strings; /** * @author liasu * */ public class QueryService { // private Logger logger = LoggerFactory.getLogger(QueryService.class); private final CMSClientService service; private final MetadataService metaService; QueryService(CMSClientService service, MetadataService metaService) { this.service = service; this.metaService = metaService; } public <T extends ICMSEntity> QueryIterator<T> queryIterator(CMSQuery query, Class<T> clz, CMSClientContext context) { context = context != null ? context : new CMSClientContext(); return new QueryIterator<T>(clz, query, service, context); } /** * Returns a cast list of entity. Based on given query and target class. * * @param query * - The query object to be executed. * @param targetClass * - The generated model class that query result to be cast to. - * If given {@link ICMSEntity}, a list of ICMSEntity will be * return. The type will be determined by query result. Always * make sure _type is available if you want to dynamically * determine the type. * @param context * @return */ public <T extends ICMSEntity> CMSQueryResult<T> query(CMSQuery query, Class<T> targetClass, CMSClientContext context) { context = context != null ? context : new CMSClientContext(); service.checkLiveness(); Preconditions.checkNotNull(targetClass, "target class can not be null"); Preconditions.checkNotNull(query, "query can not be null"); Preconditions.checkArgument(!Strings.isNullOrEmpty(query.getQueryString()), "query string can not be null or empty"); if (StringUtils.isEmpty(query.getRepository())) { query.setRepository(service.getRepository()); } else { Preconditions.checkArgument(service.getRepository().equals(query.getRepository()), "query repository not match the client service configuration, make it consistent, or simple not set in query!"); } if (StringUtils.isEmpty(query.getBranch())) { query.setBranch(service.getBranch()); } else { Preconditions.checkArgument(service.getBranch().equals(query.getBranch()), "query branch not match the client service configuration, make it consistent, or simple not set in query!"); } // check use get or post :: use get is better in the cases for server side simply check access log could find the request Map<String,String> parameters = service.getQueryParameter(context); parameters.putAll(query.getQueryParams()); HttpRequest request = null; String fullUrl = null; { request = HttpRequest.GET; fullUrl = new URLBuilder(service.getClientConfig(), Url.GET_QUERY_URL, query).buildCanonicalPath(); int urlLength = fullUrl.length(); urlLength++; // the '?' character for (Entry<String, String> paramPair : parameters.entrySet()) { urlLength += paramPair.getKey() != null ? paramPair.getKey().length() : 0; urlLength += paramPair.getValue() != null ? paramPair.getValue().length() : 0; urlLength++; // the '&' character } // reserved 100+ character for the url prefix if (urlLength > 900) { // use POST for long query string request = HttpRequest.POST; fullUrl = new URLBuilder(service.getClientConfig(), Url.QUERY_URL, query).buildCanonicalPath(); } } QueryRestExecutor<T> queryExecutor = new QueryRestExecutor<T>(service.getClientConfig(), service.getClient(), query, fullUrl, request, parameters, service.getHeader(context), targetClass, context); return queryExecutor.build(); } public <T extends ICMSEntity> CMSQueryResult<T> fullQuery(CMSQuery query, Class<T> targetClass, CMSClientContext context) { long dbTimeCost = 0; long totalTimeCost = 0; boolean hasMore = false; Map<String, T> resultsMap = new HashMap<String, T>(); do { CMSQueryResult<T> result = query(query, targetClass, context); // merge entities List<T> entities = result.getEntities(); for (T entity : entities) { String id = entity.get_id(); ICMSEntity mergedEntity = resultsMap.get(id); if (mergedEntity == null) { resultsMap.put(id, entity); } else { mergeEntity(mergedEntity, entity, context); } } // next round hasMore = result.isHasMore(); int nextHint = result.getHint(); long[] nextSkips = result.getSkips(); long[] nextLimits = result.getLimits(); String cursorString = result.getCursor(); dbTimeCost += result.getDbTimeCost(); totalTimeCost += result.getTotalTimeCost(); query.setHint(nextHint); query.setSkips(nextSkips); query.setLimits(nextLimits); query.setCursor(cursorString); } while (hasMore); CMSQueryResult<T> finalResult = new CMSQueryResult<T>(); finalResult.addResults(resultsMap.values()); finalResult.setCount(Long.valueOf(resultsMap.values().size())); finalResult.setDbTimeCost(dbTimeCost); finalResult.setTotalTimeCost(totalTimeCost); return finalResult; } // merge entity2 to entity1 @SuppressWarnings("unchecked") private <T extends ICMSEntity> void mergeEntity(T entity1, T entity2, CMSClientContext context) { String type = entity1.get_type(); MetaClass meta = metaService.getMetadata(type, context); for (String fieldName : entity2.getFieldNames()) { MetaField field = meta.getField(fieldName); if (field instanceof MetaRelationship) { List<T> list1 = null; List<T> list2 = null; Object value1 = entity1.getFieldValue(fieldName); Object value2 = entity2.getFieldValue(fieldName); if (!(value1 instanceof List)) { list1 = Arrays.asList((T)value1); } else { list1 = (List<T>) value1; } if (!(value2 instanceof List)) { list2 = Arrays.asList((T)value2); } else { list2 = (List<T>) value2; } List<T> mergedEntities = mergeFieldValues(list1, list2, context); entity1.setFieldValue(fieldName, mergedEntities); } else { entity1.setFieldValue(fieldName, entity2.getFieldValue(fieldName)); } } } private <T extends ICMSEntity> List<T> mergeFieldValues(List<T> entityList1, List<T> entityList2, CMSClientContext context) { List<T> mergedList = new LinkedList<T>(); Map<String, T> idEntityMap = new HashMap<String, T>(); for (T entity : entityList1) { String entityId = entity.get_id(); T originEntity = idEntityMap.get(entityId); if (originEntity != null) { mergeEntity(originEntity, entity, context); } else { mergedList.add(entity); idEntityMap.put(entityId, entity); } } for (T entity2 : entityList2) { String entityId = entity2.get_id(); if (idEntityMap.containsKey(entityId)) { T entity1 = idEntityMap.get(entityId); if (entity1 != entity2) { mergeEntity(entity1, entity2, context); } } else { mergedList.add(entity2); idEntityMap.put(entityId, entity2); } } return mergedList; } public <T extends ICMSEntity> QueryIterator<T> getDanglingReference(Class<T> clz, String attribute, CMSClientContext context) { context = context != null ? context : new CMSClientContext(); String metadata = clz.getSimpleName(); Map<String, MetaClass> mm = service.getMetadatas(context); MetaClass mc = mm.get(metadata); MetaField field = mc.getField(attribute); if (!(field instanceof MetaRelationship)) { throw new CMSEntityException(String.format("Attribute %s must be an existing relationship on give metadata %s!", attribute, metadata)); } return danglingReference(metadata, attribute, ((MetaRelationship) field).getRefDataType(), clz, context); } <T extends ICMSEntity> QueryIterator<T> danglingReference(String metadata, String attribute, String refDataType, Class<T> targetClass, CMSClientContext context) { CMSQuery query = new CMSQuery(String.format(CMSQueryConstants.QUERY_STRING, metadata, attribute, attribute, refDataType)); query.setAllowFullTableScan(true); // affect the object id selection in the sub-query. FIXME: make this number more reasonable, currently, // the max collection size more than 1, 000, 000. Fetch all object would be a 30M query from server side. query.setMaxFetch(2000000); return queryIterator(query, targetClass, context); } public <T extends ICMSEntity> QueryIterator<T> getEmptyReference(Class<T> clz, String attribute, CMSClientContext context) { context = context != null ? context : new CMSClientContext(); Map<String, MetaClass> mm = service.getMetadatas(context); MetaClass mc = mm.get(clz.getSimpleName()); MetaField field = mc.getField(attribute); if (!(field instanceof MetaRelationship)) { throw new CMSEntityException(String.format("Attribute %s must be an existing relationship on give metadata %s!", attribute, clz.getSimpleName())); } return emptyReference(clz.getSimpleName(), attribute, clz, context); } <T extends ICMSEntity> QueryIterator<T> emptyReference(String metadata, String attribute, Class<T> targetClass, CMSClientContext context) { CMSQuery query = new CMSQuery(String .format(CMSQueryConstants.QUERY_EMPTY_REFERENCE_STRING, metadata, attribute)); query.setAllowFullTableScan(true); // affect the object id selection in the sub-query. FIXME: make this number more reasonable, currently, // the max collection size more than 1, 000, 000. Fetch all object would be a 30M query from server side. query.setMaxFetch(2000000); return queryIterator(query, targetClass, context); } public List<GenericCMSEntity> getEntitiesByField(String queryPath, String fieldName, Object fieldValue, CMSClientContext context, String... includeFieldNames) { context = context != null ? context : new CMSClientContext(); Preconditions.checkArgument(queryPath != null && !queryPath.isEmpty(), "queryPath could not be empty."); Preconditions.checkArgument(fieldName != null && !fieldName.isEmpty(), "fieldName could not be empty."); CMSQuery query = new CMSQuery(queryPath + QueryFilterBuilder.convertQueryValue(fieldName, fieldValue, includeFieldNames)); query.setAllowFullTableScan(true); return query(query, GenericCMSEntity.class, context).getEntities(); } public <T extends ICMSEntity> List<T> getEntitiesByField(Class<T> entityClass, String queryPath, String fieldName, Object fieldValue, CMSClientContext context, String... includeFieldNames) { context = context != null ? context : new CMSClientContext(); Preconditions.checkArgument(entityClass != null, "entity class could not be null."); Preconditions.checkArgument(!Strings.isNullOrEmpty(fieldName), "fieldName could not be null."); String metaType = queryPath; if (queryPath == null) { metaType = entityClass.getSimpleName(); } CMSQuery query = new CMSQuery(metaType + QueryFilterBuilder.convertQueryValue(entityClass, fieldName, fieldValue, includeFieldNames)); query.setAllowFullTableScan(true); return query(query, entityClass, context).getEntities(); } }