package org.zstack.query; import org.apache.commons.lang.StringUtils; import org.objenesis.Objenesis; import org.objenesis.ObjenesisStd; import org.objenesis.instantiator.ObjectInstantiator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.Component; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.apimediator.GlobalApiMessageInterceptor; import org.zstack.header.configuration.PythonApiBindingWriter; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.errorcode.SysErrors; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.APIMessage; import org.zstack.header.query.*; import org.zstack.header.rest.APINoSee; import org.zstack.header.search.Inventory; import org.zstack.header.search.Parent; import org.zstack.header.search.TypeField; import org.zstack.utils.*; import org.zstack.utils.function.Function; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import static org.zstack.core.Platform.argerr; import javax.persistence.*; import javax.persistence.metamodel.StaticMetamodel; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Timestamp; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; import static org.zstack.utils.StringDSL.s; /** */ public class MysqlQueryBuilderImpl3 implements Component, QueryBuilder, GlobalApiMessageInterceptor, PythonApiBindingWriter { private static final CLogger logger = Utils.getLogger(MysqlQueryBuilderImpl3.class); @Autowired private DatabaseFacade dbf; @Autowired private ErrorFacade errf; @Autowired private PluginRegistry pluginRgty; private Objenesis objenesis = new ObjenesisStd(); private static final String USER_TAG = "__userTag__"; private static final String SYSTEM_TAG = "__systemTag__"; @Override public List<Class> getMessageClassToIntercept() { List<Class> clz = new ArrayList<>(); clz.add(APIQueryMessage.class); return clz; } @Override public InterceptorPosition getPosition() { return InterceptorPosition.FRONT; } @Override public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { List<ExpandedQueryAliasInfo> infos = aliasInfos.get(msg.getClass()); if (infos != null) { APIQueryMessage qmsg = (APIQueryMessage) msg; for (QueryCondition qcond : qmsg.getConditions()) { for (ExpandedQueryAliasInfo info : infos) { if (qcond.getName().startsWith(String.format("%s.", info.alias))) { qcond.setName(qcond.getName().replaceFirst(info.alias, info.expandField)); } } } } return msg; } private class ExpandedQueryAliasInfo { Class queryMessageClass; Class inventoryClassDefiningThisAlias; Class inventoryClass; String expandField; String alias; boolean isFromAnnotation; void check() { String[] slices = expandField.split("\\."); String firstExpand = slices[0]; if (isFromAnnotation) { ExpandedQueries eqs = (ExpandedQueries) inventoryClassDefiningThisAlias.getAnnotation(ExpandedQueries.class); DebugUtils.Assert(eqs != null, String.format("inventory[%s] having annotation[ExpandedQueryAliases] must also have annotation[ExpandedQueries]", inventoryClassDefiningThisAlias.getName())); for (ExpandedQuery at : eqs.value()) { if (at.expandedField().equals(firstExpand)) { return; } } throw new CloudRuntimeException(String.format("inventory[%s] has an expanded query alias[%s]," + " but it doesn't have an expand query that has expandedField[%s]", inventoryClassDefiningThisAlias.getName(), alias, firstExpand)); } else { List<ExpandedQueryStruct> expds = expandedQueryStructs.get(inventoryClassDefiningThisAlias); for (ExpandedQueryStruct s : expds) { if (s.getExpandedField().equals(firstExpand)) { return; } } throw new CloudRuntimeException(String.format("inventory[%s] has an expanded query alias[%s](added by AddExpandedQueryExtensionPoint]," + " but the extension doesn't declare any expanded query having expandedField[%s]", inventoryClassDefiningThisAlias.getClass(), alias, firstExpand)); } } } private class EntityInfo { EntityInfo parent; List<EntityInfo> children = new ArrayList<EntityInfo>(); Inventory inventoryAnnotation; Class entityClass; Class jpaMetaClass; Class inventoryClass; String primaryKey; Field entityPrimaryKeyField; Field inventoryPrimaryKeyField; Field inventoryTypeField; Field entityTypeField; Map<String, ExpandedQueryStruct> expandedQueries = new HashMap<String, ExpandedQueryStruct>(); Map<String, EntityInfo> flatTypeEntityMap = new HashMap<String, EntityInfo>(); Method inventoryValueOf; Method inventoryCollectionValueOf; ObjectInstantiator objectInstantiator; Map<String, Field> allFieldsMap = new HashMap<String, Field>(); Map<String, ExpandedQueryAliasInfo> aliases = new HashMap<String, ExpandedQueryAliasInfo>(); List<String> premitiveFieldNames = new ArrayList<String>(); EntityInfo(Class invClass) throws NoSuchMethodException { inventoryAnnotation = (Inventory) invClass.getAnnotation(Inventory.class); entityClass = inventoryAnnotation.mappingVOClass(); if (!entityClass.isAnnotationPresent(Entity.class)) { throw new CloudRuntimeException(String.format("class[%s] is not annotated by @Entity, but it's stated as entity class by @Inventory of %s", entityClass.getName(), invClass.getName())); } jpaMetaClass = metaModelClasses.get(entityClass); if (jpaMetaClass == null) { throw new CloudRuntimeException(String.format("cannot find JPA meta model class for entity class[%s], the meta model class is expected as %s", entityClass.getName(), entityClass.getName() + "_")); } inventoryClass = invClass; entityPrimaryKeyField = FieldUtils.getAnnotatedField(Id.class, entityClass); primaryKey = entityPrimaryKeyField.getName(); entityPrimaryKeyField.setAccessible(true); inventoryPrimaryKeyField = FieldUtils.getField(primaryKey, inventoryClass); if (inventoryPrimaryKeyField != null) { inventoryPrimaryKeyField.setAccessible(true); } inventoryTypeField = FieldUtils.getAnnotatedFieldOfThisClass(TypeField.class, invClass); if (inventoryTypeField != null) { inventoryTypeField.setAccessible(true); entityTypeField = FieldUtils.getField(inventoryTypeField.getName(), entityClass); DebugUtils.Assert(entityTypeField != null, String.format("the type field[%s] of inventory class[%s] is not on entity class[%s]", inventoryTypeField.getName(), inventoryClass.getName(), entityClass.getName())); entityTypeField.setAccessible(true); } String methodName = inventoryAnnotation.collectionValueOfMethod(); if (methodName.equals("")) { methodName = "valueOf"; } inventoryCollectionValueOf = invClass.getMethod(methodName, Collection.class); inventoryValueOf = invClass.getMethod("valueOf", entityClass); List<ExpandedQueryStruct> structs = expandedQueryStructs.get(inventoryClass); if (structs != null) { for (ExpandedQueryStruct s : structs) { s.check(); this.expandedQueries.put(s.getExpandedField(), s); } } ExpandedQueries expandedQueries = (ExpandedQueries) invClass.getAnnotation(ExpandedQueries.class); if (expandedQueries != null) { for (ExpandedQuery e : expandedQueries.value()) { ExpandedQueryStruct s = ExpandedQueryStruct.fromExpandedQueryAnnotation(inventoryClass, e); s.check(); this.expandedQueries.put(s.getExpandedField(), s); } } objectInstantiator = objenesis.getInstantiatorOf(inventoryClass); List<Field> allFields = FieldUtils.getAllFields(inventoryClass); for (Field f : allFields) { f.setAccessible(true); allFieldsMap.put(f.getName(), f); if (!f.isAnnotationPresent(Unqueryable.class) && !f.isAnnotationPresent(Queryable.class)) { premitiveFieldNames.add(f.getName()); } } for (List<ExpandedQueryAliasInfo> aliasList : aliasInfos.values()) { for (ExpandedQueryAliasInfo struct : aliasList) { if (struct.inventoryClassDefiningThisAlias == inventoryClass) { aliases.put(struct.alias, struct); } } } } void buildFlatTypeEntityMap() { for (EntityInfo e : children) { Parent pat = e.inventoryAnnotation.parent()[0]; flatTypeEntityMap.put(pat.type(), e); } } Object getPrimaryKeyValue(Object vo) { try { return entityPrimaryKeyField.get(vo); } catch (IllegalAccessException e) { throw new CloudRuntimeException(e); } } Class selectInventoryClass(final APIQueryMessage msg) { if (inventoryTypeField == null) { return inventoryClass; } QueryCondition typeCondition = null; for (QueryCondition cond : msg.getConditions()) { if (QueryOp.EQ.equals(cond.getOp()) && inventoryTypeField.getName().equals(cond.getName())) { typeCondition = cond; break; } } if (typeCondition != null) { EntityInfo child = flatTypeEntityMap.get(typeCondition.getValue()); if (child != null) { return child.inventoryClass; } } return inventoryClass; } void addQueryAliases(List<ExpandedQueryAliasInfo> aliases) { for (ExpandedQueryAliasInfo info : aliases) { this.aliases.put(info.alias, info); } } } private class SubQueryInfo { Class joinInventoryClass; } private class ExpandedSubQuery extends SubQueryInfo { ExpandedQueryStruct struct; } private class InherentSubQuery extends SubQueryInfo { Queryable at; Field parentField; } private Map<Class, EntityInfo> entityInfos = new HashMap<Class, EntityInfo>(); private Map<Class, Class> metaModelClasses = new HashMap<Class, Class>(); private List<String> escapeConditionNames = new ArrayList<String>(); private List<MysqlQuerySubQueryExtension> subQueryExtensions = new ArrayList<MysqlQuerySubQueryExtension>(); private Map<Class, List<ExpandedQueryStruct>> expandedQueryStructs = new HashMap<Class, List<ExpandedQueryStruct>>(); private Map<Class, List<AddExtraConditionToQueryExtensionPoint>> extraConditionsExts = new HashMap<Class, List<AddExtraConditionToQueryExtensionPoint>>(); private Map<Class, List<ExpandedQueryAliasInfo>> aliasInfos = new HashMap<Class, List<ExpandedQueryAliasInfo>>(); private Map<Class, Class> inventoryQueryMessageMap = new HashMap<Class, Class>(); private EntityInfo buildEntityInfo(Class invClass) throws NoSuchMethodException { EntityInfo info = entityInfos.get(invClass); if (info != null) { return info; } info = new EntityInfo(invClass); entityInfos.put(invClass, info); return info; } private void populateEntityInfo() throws NoSuchMethodException { List<Class> metaClasses = BeanUtils.scanClass("org.zstack", StaticMetamodel.class); for (Class it : metaClasses) { StaticMetamodel at = (StaticMetamodel) it.getAnnotation(StaticMetamodel.class); metaModelClasses.put(at.value(), it); } List<Class> invClasses = BeanUtils.scanClass("org.zstack", Inventory.class); for (Class invClass : invClasses) { EntityInfo info = buildEntityInfo(invClass); if (info.inventoryAnnotation.parent().length > 0) { Parent pat = info.inventoryAnnotation.parent()[0]; Class pinvClass = pat.inventoryClass(); DebugUtils.Assert(pinvClass.isAnnotationPresent(Inventory.class), String.format("inventory[%s]'s parent inventory class[%s] is not annotated by @Inventory", info.inventoryClass.getName(), pinvClass.getName())); EntityInfo pinfo = buildEntityInfo(pinvClass); info.parent = pinfo; pinfo.children.add(info); } } for (EntityInfo e : entityInfos.values()) { e.buildFlatTypeEntityMap(); } } private class MetaCondition { String attr; String op; String value; Class inventoryClass; String attrValueName; boolean skipInventoryCheck; int index; private Field entityField; QueryCondition toQueryCondtion() { QueryCondition qcond = new QueryCondition(); qcond.setName(attr); qcond.setOp(op); qcond.setValue(value); return qcond; } private Class getEntityFieldType() { if (Collection.class.isAssignableFrom(entityField.getType())) { return FieldUtils.getGenericType(entityField); } else if (Map.class.isAssignableFrom(entityField.getType())) { throw new CloudRuntimeException(String.format("query cannot support Map type. %s.%s", entityField.getDeclaringClass(), entityField.getName())); } else { return entityField.getType(); } } private Object doNormalizeValue(String value) { try { Class entityType = getEntityFieldType(); if (Timestamp.class.isAssignableFrom(entityType)) { return Timestamp.valueOf(value); } else if (Enum.class.isAssignableFrom(entityType)) { Method valueOf = entityType.getMethod("valueOf", String.class); return valueOf.invoke(entityType, value); } else if (Boolean.class.isAssignableFrom(entityType) || Boolean.TYPE.isAssignableFrom(entityType)) { return Boolean.valueOf(value); } else { return TypeUtils.stringToValue(value, entityType); } } catch (Exception e) { throw new CloudRuntimeException(String.format("failed to parse value[%s]", value), e); } } Object normalizeValue() { if (QueryOp.IS_NULL.equals(op) || QueryOp.NOT_NULL.equals(op)) { return null; } if (QueryOp.IN.equals(op) || QueryOp.NOT_IN.equals(op)) { List<Object> ret = new ArrayList(); for (String it : value.split(",")) { ret.add(doNormalizeValue(it.trim())); } if (ret.isEmpty()) { // the query value is like ",,,,", // in this case, compliment an empty string ret.add(""); } return ret; } else { return doNormalizeValue(value); } } private String formatSql(String entityName, String attr, String op) { StringBuilder sb = new StringBuilder(); sb.append(String.format("%s.%s", entityName, attr)); if (QueryOp.IN.equals(op)) { sb.append(String.format(" in (:%s)", attrValueName)); } else if (QueryOp.NOT_IN.equals(op)) { sb.append(String.format(" not in (:%s)", attrValueName)); } else if (QueryOp.IS_NULL.equals(op)) { sb.append(" is null"); attrValueName = null; } else if (QueryOp.NOT_NULL.equals(op)) { sb.append(" is not null"); attrValueName = null; } else { sb.append(String.format(" %s :%s", op, attrValueName)); } return sb.toString(); } String toJpql() { String entityName = inventoryClass.getSimpleName().toLowerCase(); attrValueName = entityName + "_" + attr + "_" + "value" + index; Field inventoryField = FieldUtils.getField(attr, inventoryClass); if (!skipInventoryCheck) { if (inventoryField == null || inventoryField.isAnnotationPresent(APINoSee.class)) { throw new OperationFailureException(argerr("condition name[%s] is invalid, no such field on inventory class[%s]", attr, inventoryClass.getName())); } if (inventoryField.isAnnotationPresent(Unqueryable.class)) { throw new OperationFailureException(argerr("condition name[%s] is invalid, field[%s] of inventory[%s] is annotated as @Unqueryable field", attr, attr, inventoryClass.getName())); } } Queryable at = inventoryField.getAnnotation(Queryable.class); EntityInfo info = entityInfos.get(inventoryClass); if (at == null) { Field metaField = FieldUtils.getField(attr, info.jpaMetaClass); if (metaField == null) { throw new OperationFailureException(argerr("entity meta class[%s] has no field[%s]", info.jpaMetaClass.getName(), attr)); } entityField = FieldUtils.getField(attr, info.entityClass); DebugUtils.Assert(entityField != null, String.format("mismatching between inventory[%s] and entity[%s], field[%s] is not present on entity", inventoryClass.getName(), info.entityClass.getName(), attr)); return formatSql(entityName, attr, op); } else { entityField = inventoryField; JoinColumn jc = at.joinColumn(); String refName = jc.referencedColumnName(); DebugUtils.Assert(!"".equals(refName), String.format("referencedColumnName of JoinColumn of field[%s] on inventory class[%s] cannot be empty string", inventoryField.getName(), inventoryClass.getName())); String foreignKey = jc.name(); DebugUtils.Assert(!"".equals(foreignKey), String.format("name of JoinColumn of field[%s] on inventory class[%s] cannot be empty string", inventoryField.getName(), inventoryClass.getName())); Class mappingInvClass = at.mappingClass(); Inventory mappingInvAt = (Inventory) mappingInvClass.getAnnotation(Inventory.class); DebugUtils.Assert(mappingInvAt != null, String.format("Mapping inventory class[%s] of inventory class[%s] is not annotated by @Inventory", mappingInvClass.getName(), inventoryClass.getName())); Class foreignVOClass = mappingInvAt.mappingVOClass(); DebugUtils.Assert(FieldUtils.hasField(refName, foreignVOClass), String.format("referencedColumnName of JoinColumn of field[%s] on inventory class[%s] is invalid, class[%s] doesn't have field[%s]", inventoryField.getName(), inventoryClass.getName(), foreignVOClass.getName(), refName)); DebugUtils.Assert(FieldUtils.hasField(foreignKey, foreignVOClass), String.format("name of JoinColumn of field[%s] on inventory class[%s] is invalid, class[%s] doesn't have field[%s]", inventoryField.getName(), inventoryClass.getName(), foreignVOClass.getName(), foreignKey)); Map<String, String> var = new HashMap(); var.put("entity", entityName); var.put("primaryKey", info.primaryKey); var.put("subEntity", foreignVOClass.getSimpleName().toLowerCase()); var.put("foreignKey", foreignKey); var.put("foreignVO", foreignVOClass.getSimpleName()); if (QueryOp.NOT_IN.equals(op)) { // NOT_IN needs special handle op = QueryOp.IN.toString(); var.put("condition", formatSql(foreignVOClass.getSimpleName().toLowerCase(), refName, op)); return s("{entity}.{primaryKey} not in (select {subEntity}.{foreignKey} from {foreignVO} {subEntity} where {condition})").formatByMap(var); } else { var.put("condition", formatSql(foreignVOClass.getSimpleName().toLowerCase(), refName, op)); return s("{entity}.{primaryKey} in (select {subEntity}.{foreignKey} from {foreignVO} {subEntity} where {condition})").formatByMap(var); } } } } private class QueryObject { EntityInfo info; List<MetaCondition> conditions = new ArrayList<MetaCondition>(); QueryObject parent; List<QueryObject> children = new ArrayList<QueryObject>(); SubQueryInfo subQueryInfo; APIQueryMessage msg; // NOTE: we hard code tag specific logic here because we think current query model is not sustainable, // it worth nothing to waste effort on making this as extension point; we will switch the entire // query framework to ANTLR based DSL in next version. class TagSqlBuilder { List<String> IN_CONDITIONS; List<String> NOT_IN_CONDITIONS; { IN_CONDITIONS = CollectionDSL.list( QueryOp.EQ.toString(), QueryOp.GT.toString(), QueryOp.GT_AND_EQ.toString(), QueryOp.LT.toString(), QueryOp.LT_AND_EQ.toString(), QueryOp.IN.toString(), QueryOp.LIKE.toString(), QueryOp.NOT_NULL.toString() ); NOT_IN_CONDITIONS = CollectionDSL.list( QueryOp.NOT_EQ.toString(), QueryOp.NOT_IN.toString(), QueryOp.IS_NULL.toString(), QueryOp.NOT_LIKE.toString() ); } private String reverseOpIfNeed(QueryCondition cond) { if (QueryOp.NOT_EQ.equals(cond.getOp())) { return QueryOp.EQ.toString(); } else if (QueryOp.NOT_IN.equals(cond.getOp())) { return QueryOp.IN.toString(); } else if (QueryOp.IS_NULL.equals(cond.getOp())) { return QueryOp.NOT_NULL.toString(); } else if (QueryOp.NOT_LIKE.equals(cond.getOp())) { return QueryOp.LIKE.toString(); } else { return cond.getOp(); } } private String buildCondition(String field, QueryCondition cond) { if (QueryOp.IN.equals(cond.getOp()) || QueryOp.NOT_IN.equals(cond.getOp())) { String[] values = cond.getValue().split(","); List<String> vals = new ArrayList<String>(); for (String val : values) { vals.add(String.format("'%s'", val)); } return String.format("%s %s (%s)", field, reverseOpIfNeed(cond), StringUtils.join(vals, ",")); } else if (QueryOp.IS_NULL.equals(cond.getOp()) || QueryOp.NOT_NULL.equals(cond.getOp())) { return String.format("%s %s", field, reverseOpIfNeed(cond)); } else { return String.format("%s %s '%s'", field, reverseOpIfNeed(cond), cond.getValue()); } } private String chooseOp(QueryCondition cond) { if (IN_CONDITIONS.contains(cond.getOp())) { return "in"; } if (NOT_IN_CONDITIONS.contains(cond.getOp())) { return "not in"; } throw new CloudRuntimeException(String.format("invalid comparison operator[%s]; %s", cond.getOp(), JSONObjectUtil.toJsonString(cond))); } private List<String> getAllResourceTypesForTag() { List<String> types = new ArrayList<String>(); Class c = info.entityClass; while (c != Object.class) { types.add(String.format("'%s'", c.getSimpleName())); c = c.getSuperclass(); } return types; } String toJpql() { List<String> resultQuery = new ArrayList<String>(); List<String> rtypes = getAllResourceTypesForTag(); String primaryKey = info.primaryKey; String invname = info.inventoryClass.getSimpleName().toLowerCase(); List<QueryCondition> conds = CollectionUtils.transformToList(conditions, new Function<QueryCondition, MetaCondition>() { @Override public QueryCondition call(MetaCondition arg) { return USER_TAG.equals(arg.attr) || SYSTEM_TAG.equals(arg.attr) ? arg.toQueryCondtion() : null; } }); String typeString = StringUtils.join(rtypes, ","); for (QueryCondition cond : conds) { if (cond.getName().equals(USER_TAG)) { List<String> condStrs = new ArrayList<String>(); condStrs.add(buildCondition("user.tag", cond)); condStrs.add(String.format("user.resourceType in (%s)", typeString)); resultQuery.add(String.format("%s.%s %s (select user.resourceUuid from UserTagVO user where %s)", invname, primaryKey, chooseOp(cond), StringUtils.join(condStrs, " and "))); } else if (cond.getName().equals(SYSTEM_TAG)) { List<String> condStrs = new ArrayList<String>(); condStrs.add(buildCondition("sys.tag", cond)); condStrs.add(String.format("sys.resourceType in (%s)", typeString)); resultQuery.add(String.format("%s.%s %s (select sys.resourceUuid from SystemTagVO sys where %s)", invname, primaryKey, chooseOp(cond), StringUtils.join(condStrs, " and "))); } } if (resultQuery.isEmpty()) { return null; } else { return StringUtils.join(resultQuery, " and "); } } } String toJpql(boolean isCount) { List<String> where = new ArrayList<String>(); boolean hasTag = false; int index = 0; for (MetaCondition it : conditions) { if (USER_TAG.equals(it.attr) || SYSTEM_TAG.equals(it.attr)) { hasTag = true; continue; } // use an index to differentiate multiple conditions with the same name it.index = index++; where.add(it.toJpql()); } //conditions = tmpConditions; if (hasTag) { where.add(new TagSqlBuilder().toJpql()); } for (QueryObject it : children) { where.add(it.toJpql(false)); } if (parent != null) { // this is a sub query if (subQueryInfo instanceof InherentSubQuery) { InherentSubQuery isub = (InherentSubQuery) subQueryInfo; JoinColumn jc = isub.at.joinColumn(); String foreignKey = jc.name(); DebugUtils.Assert(!"".equals(foreignKey), String.format("name of JoinColumn of field[%s] on inventory class[%s] cannot be empty string", isub.parentField.getName(), parent.info.inventoryClass.getName())); Class foreignVOClass = info.entityClass; DebugUtils.Assert(FieldUtils.hasField(foreignKey, foreignVOClass), String.format("name of JoinColumn of field[%s] on inventory class[%s] is invalid, class[%s] doesn't have field[%s]", isub.parentField.getName(), parent.info.inventoryClass.getName(), foreignVOClass.getName(), foreignKey)); Map<String, String> var = new HashMap<String, String>(); var.put("entity", parent.info.inventoryClass.getSimpleName().toLowerCase()); var.put("primaryKey", parent.info.primaryKey); var.put("subEntity", info.inventoryClass.getSimpleName().toLowerCase()); var.put("foreignKey", foreignKey); var.put("foreignVO", foreignVOClass.getSimpleName()); if (where.isEmpty()) { return s("{entity}.{primaryKey} in (select {subEntity}.{foreignKey} from {foreignVO} {subEntity})").formatByMap(var); } else { var.put("condition", StringUtils.join(where, " and ").trim()); return s("{entity}.{primaryKey} in (select {subEntity}.{foreignKey} from {foreignVO} {subEntity} where {condition})").formatByMap(var); } } else if (subQueryInfo instanceof ExpandedSubQuery) { ExpandedSubQuery esub = (ExpandedSubQuery) subQueryInfo; Inventory joinAt = (Inventory) esub.struct.getInventoryClass().getAnnotation(Inventory.class); Class joinVO = joinAt.mappingVOClass(); Map<String, String> var = new HashMap<String, String>(); var.put("entity", parent.info.inventoryClass.getSimpleName().toLowerCase()); var.put("foreignKey", esub.struct.getForeignKey()); var.put("expandedEntity", esub.struct.getInventoryClass().getSimpleName().toLowerCase()); var.put("expandedVO", joinVO.getSimpleName()); var.put("expandedKey", esub.struct.getExpandedInventoryKey()); if (where.isEmpty()) { return s("{entity}.{foreignKey} in (select {expandedEntity}.{expandedKey} from {expandedVO} {expandedEntity})").formatByMap(var); } else { var.put("condition", StringUtils.join(where, " and ").trim()); return s("{entity}.{foreignKey} in (select {expandedEntity}.{expandedKey} from {expandedVO} {expandedEntity} where {condition})").formatByMap(var); } } throw new CloudRuntimeException("cannot be here"); } else { // this is root query for (MysqlQuerySubQueryExtension ext : subQueryExtensions) { String sub = ext.makeSubquery(msg, info.inventoryClass); if (sub != null) { where.add(sub); } } String entityName = info.inventoryClass.getSimpleName().toLowerCase(); String entity = info.entityClass.getSimpleName(); String condition = StringUtils.join(where, " and ").trim(); if (isCount) { if (where.isEmpty()) { return String.format("select count(%s) from %s %s", entityName, entity, entityName); } else { return String.format("select count(%s) from %s %s where %s", entityName, entity, entityName, condition); } } else { String ret = null; String selector = null; if (msg.isFieldQuery()) { List<String> ss = new ArrayList<String>(); for (String f : msg.getFields()) { ss.add(String.format("%s.%s", entityName, f)); } selector = StringUtils.join(ss, ","); } else { selector = entityName; } if (where.isEmpty()) { ret = String.format("select %s from %s %s", selector, entity, entityName); } else { ret = String.format("select %s from %s %s where %s", selector, entity, entityName, condition); } if (msg.getSortBy() != null) { if (!FieldUtils.hasField(msg.getSortBy(), info.entityClass)) { throw new IllegalArgumentException(String.format("illegal sortBy[%s], entity[%s] doesn't have this field", msg.getSortBy(), info.entityClass.getName())); } ret = String.format("%s order by %s.%s %s", ret, entityName, msg.getSortBy(), msg.getSortDirection().toUpperCase()); } if (msg.getGroupBy() != null) { if (!FieldUtils.hasField(msg.getGroupBy(), info.entityClass)) { throw new IllegalArgumentException(String.format("illegal groupBy[%s], entity[%s] doesn't have this field", msg.getGroupBy(), info.entityClass.getName())); } ret = String.format("%s group by %s", ret, msg.getGroupBy()); } return ret; } } } } private class QueryContext { private APIQueryMessage msg; private Class inventoryClass; private QueryObject root; private Map<Class, QueryObject> tmpMap = new HashMap<Class, QueryObject>(); private MetaCondition buildCondition(QueryCondition qcond, EntityInfo info) { MetaCondition mcond = new MetaCondition(); mcond.attr = qcond.getName(); mcond.op = qcond.getOp(); mcond.inventoryClass = info.inventoryClass; mcond.value = qcond.getValue(); return mcond; } private void buildSubQuery(QueryCondition qcond, QueryObject parent) { String[] slices = qcond.getName().split("\\."); String currentFieldName = slices[0]; Class parentInvClass = parent.info.inventoryClass; DebugUtils.Assert(parentInvClass != null, String.format("parent inventory class cannot be null. Parent entity class[%s]", parent.info.entityClass.getName())); SubQueryInfo subQueryInfo = null; ExpandedQueryStruct expandedQuery = parent.info.expandedQueries.get(currentFieldName); if (expandedQuery == null) { // try finding alias ExpandedQueryAliasInfo alias = parent.info.aliases.get(currentFieldName); if (alias != null) { QueryCondition ncond = new QueryCondition(); ncond.setName(qcond.getName().replaceFirst(alias.alias, alias.expandField)); ncond.setOp(qcond.getOp()); ncond.setValue(qcond.getValue()); buildSubQuery(ncond, parent); return; } } if (expandedQuery != null) { // an expanded query ExpandedSubQuery esub = new ExpandedSubQuery(); esub.struct = expandedQuery; esub.joinInventoryClass = expandedQuery.getInventoryClass(); subQueryInfo = esub; } else { Field currentField = FieldUtils.getField(currentFieldName, parentInvClass); DebugUtils.Assert(currentField != null, String.format("cannot find field[%s] on class[%s], wrong subquery name[%s]", currentFieldName, parentInvClass.getName(), qcond.getName())); InherentSubQuery isub = new InherentSubQuery(); Queryable at = currentField.getAnnotation(Queryable.class); DebugUtils.Assert(at != null, String.format("nested query field[%s] on inventory[%s] must be annotated as @Queryable", currentFieldName, parentInvClass.getName())); isub.at = at; isub.joinInventoryClass = at.mappingClass(); isub.parentField = currentField; DebugUtils.Assert(isub.joinInventoryClass.isAnnotationPresent(Inventory.class), String.format("field[%s] of inventory[%s] can only be type of Collection whose generic type is inventory class or a object whose type is inventory class. Current class is %s which is not annotated by @Inventory", currentField.getName(), parentInvClass.getName(), isub.joinInventoryClass.getName()) ); subQueryInfo = isub; } EntityInfo info = entityInfos.get(subQueryInfo.joinInventoryClass); QueryObject qobj = tmpMap.get(info.entityClass); if (qobj == null) { qobj = new QueryObject(); qobj.info = info; qobj.parent = parent; qobj.subQueryInfo = subQueryInfo; qobj.msg = msg; parent.children.add(qobj); tmpMap.put(info.entityClass, qobj); } slices = Arrays.copyOfRange(slices, 1, slices.length); String subFieldName = StringUtils.join(slices, "."); QueryCondition ncond = new QueryCondition(); ncond.setName(subFieldName); ncond.setOp(qcond.getOp()); ncond.setValue(qcond.getValue()); if (!subFieldName.contains(".")) { qobj.conditions.add(buildCondition(ncond, info)); } else { buildSubQuery(ncond, qobj); } } private void buildMetaCondition(QueryCondition qcond, EntityInfo info, boolean skipInventoryCheck) { QueryObject qobj = tmpMap.get(info.entityClass); if (qobj == null) { qobj = new QueryObject(); qobj.info = info; tmpMap.put(info.entityClass, qobj); } MetaCondition mcond = buildCondition(qcond, info); mcond.skipInventoryCheck = skipInventoryCheck; qobj.conditions.add(mcond); } private void buildMetaCondition(QueryCondition qcond, EntityInfo info) { buildMetaCondition(qcond, info, false); } private String build(boolean isCount) { root = new QueryObject(); root.msg = msg; root.info = entityInfos.get(inventoryClass); DebugUtils.Assert(root.info != null, String.format("class[%s] is not annotated by @Inventory", inventoryClass.getName())); tmpMap.put(root.info.entityClass, root); for (QueryCondition qcond : msg.getConditions()) { if (escapeConditionNames.contains(qcond.getName())) { continue; } if (!qcond.getName().contains(".")) { buildMetaCondition(qcond, root.info); } else { buildSubQuery(qcond, root); } } List<AddExtraConditionToQueryExtensionPoint> exts = extraConditionsExts.get(msg.getClass()); if (exts != null) { for (AddExtraConditionToQueryExtensionPoint ext : exts) { try { for (QueryCondition cond : ext.getExtraQueryConditionForMessage(msg)) { buildMetaCondition(cond, root.info, true); } } catch (Throwable t) { logger.warn(String.format("unhandled exception when calling %s", ext.getClass().getName()), t); } } } return root.toJpql(isCount); } private void setQueryValue(Query q, QueryObject qobj) { for (MetaCondition mcond : qobj.conditions) { if (USER_TAG.equals(mcond.attr) || SYSTEM_TAG.equals(mcond.attr)) { continue; } Object val = mcond.normalizeValue(); if (val != null) { q.setParameter(mcond.attrValueName, val); } } for (QueryObject child : qobj.children) { setQueryValue(q, child); } } public List convertVOsToInventories(final List vos) { try { if (vos.isEmpty()) { return new ArrayList(); } if (root.info.children.isEmpty()) { return (List) root.info.inventoryCollectionValueOf.invoke(inventoryClass, vos); } final LinkedHashMap flatMap = new LinkedHashMap(); final List primaryKeysNeedResolve = new ArrayList(); for (Object vo : vos) { String type = (String) root.info.entityTypeField.get(vo); Object priKey = root.info.getPrimaryKeyValue(vo); if (!root.info.flatTypeEntityMap.containsKey(type)) { flatMap.put(priKey, root.info.inventoryValueOf.invoke(inventoryClass, vo)); } else { flatMap.put(priKey, null); primaryKeysNeedResolve.add(priKey); } } if (primaryKeysNeedResolve.isEmpty()) { return (List) root.info.inventoryCollectionValueOf.invoke(inventoryClass, vos); } // the inventory has child inventory inheriting it, we have to find out all child inventory and // reload them from DB and keep them in order. class SubInventoryResolver { class SQL { String sql; EntityInfo entityInfo; } List<SQL> subInventoryQuerySQL = new ArrayList<SQL>(); List resolve() throws InvocationTargetException, IllegalAccessException { buildSubInventoryQuerySQL(root.info.children); querySubInventory(); List result = new ArrayList(flatMap.values().size()); result.addAll(flatMap.values()); return result; } @Transactional(readOnly = true) private void querySubInventory() throws InvocationTargetException, IllegalAccessException { for (SQL sql : subInventoryQuerySQL) { if (primaryKeysNeedResolve.isEmpty()) { break; } TypedQuery<Tuple> q = dbf.getEntityManager().createQuery(sql.sql, Tuple.class); q.setParameter("ids", primaryKeysNeedResolve); List<Tuple> res = q.getResultList(); for (Tuple t : res) { Object priKey = t.get(0); Object vo = t.get(1); flatMap.put(priKey, sql.entityInfo.inventoryValueOf.invoke(sql.entityInfo.inventoryClass, vo)); primaryKeysNeedResolve.remove(priKey); } } } private void buildSubInventoryQuerySQL(List<EntityInfo> infos) { // child queries execute first for (EntityInfo info : infos) { if (!info.children.isEmpty()) { buildSubInventoryQuerySQL(info.children); } } for (EntityInfo info : infos) { SQL sql = new SQL(); sql.entityInfo = info; sql.sql = String.format("select e.%s, e from %s e where e.%s in (:ids)", info.primaryKey, info.entityClass.getSimpleName(), info.primaryKey); subInventoryQuerySQL.add(sql); } } } return new SubInventoryResolver().resolve(); } catch (Exception e) { throw new CloudRuntimeException(e); } } private void validateFields() { EntityInfo info = entityInfos.get(inventoryClass); for (String f : msg.getFields()) { if (!info.premitiveFieldNames.contains(f)) { throw new OperationFailureException(argerr("field[%s] is not a primitive of the inventory %s; you cannot specify it in the parameter 'fields';" + "valid fields are %s", f, info.inventoryClass.getSimpleName(), info.premitiveFieldNames)); } } } private List convertFieldsTOPartialInventories(List fieldTuple) { if (fieldTuple.isEmpty()) { return new ArrayList(); } EntityInfo info = entityInfos.get(inventoryClass); List ret = new ArrayList(fieldTuple.size()); for (Object t : fieldTuple) { Tuple tuple = (Tuple) t; Object inv = info.objectInstantiator.newInstance(); for (int i = 0; i < msg.getFields().size(); i++) { String fname = msg.getFields().get(i); Object value = tuple.get(i); Field f = info.allFieldsMap.get(fname); try { if (value != null && String.class.isAssignableFrom(f.getType())) { value = value.toString(); } f.set(inv, value); } catch (IllegalAccessException e) { throw new CloudRuntimeException(e); } } ret.add(inv); } return ret; } @Transactional(readOnly = true) List query() { if (msg.isFieldQuery()) { validateFields(); } String jpql = build(false); Query q = msg.isFieldQuery() ? dbf.getEntityManager().createQuery(jpql, Tuple.class) : dbf.getEntityManager().createQuery(jpql); if (logger.isTraceEnabled()) { org.hibernate.Query hq = q.unwrap(org.hibernate.Query.class); logger.trace(hq.getQueryString()); } setQueryValue(q, root); if (msg.getLimit() != null) { q.setMaxResults(msg.getLimit()); } if (msg.getStart() != null) { q.setFirstResult(msg.getStart()); } List vos = q.getResultList(); if (msg.isFieldQuery()) { return convertFieldsTOPartialInventories(vos); } else { return convertVOsToInventories(vos); } } @Transactional(readOnly = true) long count() { String jpql = build(true); Query q = dbf.getEntityManager().createQuery(jpql); if (logger.isTraceEnabled()) { org.hibernate.Query hq = q.unwrap(org.hibernate.Query.class); logger.trace(hq.getQueryString()); } setQueryValue(q, root); return (Long) q.getSingleResult(); } } private void populateExtensions() { subQueryExtensions.addAll(pluginRgty.getExtensionList(MysqlQuerySubQueryExtension.class)); for (MysqlQuerySubQueryExtension ext : subQueryExtensions) { if (ext.getEscapeConditionNames() != null) { escapeConditionNames.addAll(ext.getEscapeConditionNames()); } } for (AddExpandedQueryExtensionPoint ext : pluginRgty.getExtensionList(AddExpandedQueryExtensionPoint.class)) { List<ExpandedQueryStruct> expandedQueries = ext.getExpandedQueryStructs(); if (expandedQueries != null) { for (ExpandedQueryStruct s : expandedQueries) { List<ExpandedQueryStruct> exts = expandedQueryStructs.get(s.getInventoryClassToExpand()); if (exts == null) { exts = new ArrayList<ExpandedQueryStruct>(); expandedQueryStructs.put(s.getInventoryClassToExpand(), exts); } exts.add(s); } } List<ExpandedQueryAliasStruct> aliases = ext.getExpandedQueryAliasesStructs(); if (aliases != null) { for (ExpandedQueryAliasStruct as : aliases) { ExpandedQueryAliasInfo info = new ExpandedQueryAliasInfo(); info.isFromAnnotation = false; info.inventoryClassDefiningThisAlias = as.getInventoryClass(); info.queryMessageClass = inventoryQueryMessageMap.get(info.inventoryClassDefiningThisAlias); DebugUtils.Assert(info.queryMessageClass != null, String.format("AddExpandedQueryExtensionPoint[%s] defines an expanded query alias[%s], but no query message declares inventory class[%s] to which the alias maps", ext.getClass().getName(), as.getAlias(), as.getInventoryClass())); info.expandField = as.getExpandedField(); info.alias = as.getAlias(); List<ExpandedQueryAliasInfo> infos = aliasInfos.get(info.queryMessageClass); if (infos == null) { infos = new ArrayList<ExpandedQueryAliasInfo>(); aliasInfos.put(info.queryMessageClass, infos); } infos.add(info); } } } for (AddExtraConditionToQueryExtensionPoint ext : pluginRgty.getExtensionList(AddExtraConditionToQueryExtensionPoint.class)) { for (Class clz : ext.getMessageClassesForAddExtraConditionToQueryExtensionPoint()) { List<AddExtraConditionToQueryExtensionPoint> exts = extraConditionsExts.get(clz); if (exts == null) { exts = new ArrayList<AddExtraConditionToQueryExtensionPoint>(); extraConditionsExts.put(clz, exts); } exts.add(ext); } } } private void buildExpandedQueryAliasInfo() { List<Class> invClasses = BeanUtils.scanClass("org.zstack", Inventory.class); for (Class invClass : invClasses) { ExpandedQueryAliases aliases = (ExpandedQueryAliases) invClass.getAnnotation(ExpandedQueryAliases.class); if (aliases == null) { continue; } for (ExpandedQueryAlias alias : aliases.value()) { ExpandedQueryAliasInfo info = new ExpandedQueryAliasInfo(); info.alias = alias.alias(); info.expandField = alias.expandedField(); info.queryMessageClass = inventoryQueryMessageMap.get(invClass); info.inventoryClassDefiningThisAlias = invClass; info.isFromAnnotation = true; if (info.queryMessageClass == null) { throw new CloudRuntimeException(String.format("inventory[%s] declares expanded query alias, but not query message declare this inventory class in AutoQuery annotation", invClass.getName())); } List<ExpandedQueryAliasInfo> lst = aliasInfos.get(info.queryMessageClass); if (lst == null) { lst = new ArrayList<ExpandedQueryAliasInfo>(); aliasInfos.put(info.queryMessageClass, lst); } lst.add(info); } } } @Override public boolean start() { try { List<Class> queryMessageClasses = BeanUtils.scanClassByType("org.zstack", APIQueryMessage.class); for (Class msgClass : queryMessageClasses) { AutoQuery at = (AutoQuery) msgClass.getAnnotation(AutoQuery.class); if (at == null) { logger.warn(String.format("query message[%s] doesn't have AutoQuery annotation, expanded query alias would not take effect", msgClass.getName())); continue; } inventoryQueryMessageMap.put(at.inventoryClass(), msgClass); } // NOTE: don't change the order populateExtensions(); buildExpandedQueryAliasInfo(); populateEntityInfo(); completeAliasInfo(); inheritExpandedQueryAndAliases(); removeSuppressedExpandedQuery(); } catch (Exception e) { throw new CloudRuntimeException(e); } return true; } private void removeSuppressedExpandedQuery() { for (EntityInfo info : entityInfos.values()) { Map<String, ExpandedQueryStruct> ess = new HashMap<String, ExpandedQueryStruct>(); ess.putAll(info.expandedQueries); for (Entry<String, ExpandedQueryStruct> e : ess.entrySet()) { if (e.getValue().getSuppressedInventoryClass() != null) { ExpandedQueryStruct toSuppressed = null; for (ExpandedQueryStruct s : info.expandedQueries.values()) { if (s.getInventoryClass() == e.getValue().getSuppressedInventoryClass()) { toSuppressed = s; break; } } DebugUtils.Assert(toSuppressed != null, String.format("ExpandedQuery[%s] of %s is going to suppress a undefined ExpandedQuery that has inventory class[%s]", e.getValue().getExpandedField(), info.inventoryClass, e.getValue().getSuppressedInventoryClass())); info.expandedQueries.remove(toSuppressed.getExpandedField()); logger.debug(String.format("ExpandedQuery[%s] of %s suppresses ExpandedQuery[%s]", e.getValue().getExpandedField(), info.inventoryClass, toSuppressed.getExpandedField())); } } } } private void completeAliasInfo() { class ExpandedQueryAliasInfoCompletion { ExpandedQueryAliasInfo alias; ExpandedQueryAliasInfoCompletion(ExpandedQueryAliasInfo alias) { this.alias = alias; } private ExpandedQueryStruct findTargetExpandedQueryStruct(String[] expandedFields, EntityInfo entityInfo) { ExpandedQueryStruct struct = null; for (String exf : expandedFields) { struct = entityInfo.expandedQueries.get(exf); if (struct == null) { ExpandedQueryAliasInfo exalias = entityInfo.aliases.get(exf); DebugUtils.Assert(exalias != null, String.format("cannot find expanded query or alias[%s] on %s", exf, entityInfo.inventoryClass)); struct = findTargetExpandedQueryStruct(exalias.expandField.split("\\."), entityInfo); } entityInfo = entityInfos.get(struct.getInventoryClass()); } return struct; } void complete() { EntityInfo entityInfo = entityInfos.get(alias.inventoryClassDefiningThisAlias); String[] expandedFields = alias.expandField.split("\\."); DebugUtils.Assert(expandedFields.length != 0, String.format("alias[%s] defined in %s is invalid", alias.expandField, alias.inventoryClassDefiningThisAlias)); ExpandedQueryStruct struct = findTargetExpandedQueryStruct(expandedFields, entityInfo); alias.inventoryClass = struct.getInventoryClass(); alias.check(); } } for (List<ExpandedQueryAliasInfo> infos : aliasInfos.values()) { for (ExpandedQueryAliasInfo alias : infos) { new ExpandedQueryAliasInfoCompletion(alias).complete(); } } } private void inheritExpandedQueryAndAliases(EntityInfo current, EntityInfo ancestor) { if (ancestor == null) { return; } if (!ancestor.aliases.isEmpty()) { Class msgClz = inventoryQueryMessageMap.get(current.inventoryClass); List<ExpandedQueryAliasInfo> aliases = aliasInfos.get(msgClz); if (aliases == null) { aliases = new ArrayList<ExpandedQueryAliasInfo>(); } aliases.addAll(ancestor.aliases.values()); current.addQueryAliases(aliases); } if (!ancestor.expandedQueries.isEmpty()) { current.expandedQueries.putAll(ancestor.expandedQueries); } inheritExpandedQueryAndAliases(current, ancestor.parent); } private void inheritExpandedQueryAndAliases() { for (EntityInfo info : entityInfos.values()) { inheritExpandedQueryAndAliases(info, info.parent); } } @Override public boolean stop() { return true; } @Override public <T> List<T> query(APIQueryMessage msg, Class<T> inventoryClass) { QueryContext context = new QueryContext(); context.msg = msg; context.inventoryClass = selectInventoryClass(msg, inventoryClass); return context.query(); } private Class selectInventoryClass(APIQueryMessage msg, Class inventoryClass) { EntityInfo info = entityInfos.get(inventoryClass); return info.selectInventoryClass(msg); } @Override public long count(APIQueryMessage msg, Class inventoryClass) { QueryContext context = new QueryContext(); context.msg = msg; context.inventoryClass = selectInventoryClass(msg, inventoryClass); return context.count(); } @Override public Map<String, List<String>> populateQueryableFields() { //throw new CloudRuntimeException("it's impossible enumerate all combinations"); Map<String, List<String>> ret = new HashMap<>(); class QueryableBuilder { private Class inventoryClass; private List<String> queryableFields = new ArrayList<>(); private EntityInfo info; private Stack<Class> visitedPath = new Stack<>(); private QueryableBuilder() { } private QueryableBuilder(Stack<Class> path) { visitedPath = path; } private List<String> build() { info = entityInfos.get(inventoryClass); if (!visitedPath.contains(inventoryClass)) { visitedPath.push(inventoryClass); buildExpandedQueryableFields(); buildInherentQueryableFields(); normalizeToAliases(); visitedPath.pop(); } return queryableFields; } private String normalizeToAlias(String fieldName) { for (ExpandedQueryAliasInfo alias : info.aliases.values()) { if (fieldName.startsWith(String.format("%s.", alias.expandField))) { return fieldName.replaceFirst(alias.expandField, alias.alias); } } return fieldName; } private void normalizeToAliases() { Set<String> set = new HashSet<>(); for (String ret : queryableFields) { set.add(normalizeToAlias(ret)); } queryableFields.clear(); queryableFields.addAll(set); } private void buildExpandedQueryableFields() { for (ExpandedQueryStruct struct : info.expandedQueries.values()) { //QueryableBuilder nbuilder = new QueryableBuilder(inherentPath, expandedPath); QueryableBuilder nbuilder = new QueryableBuilder(visitedPath); nbuilder.inventoryClass = struct.getInventoryClass(); List<String> expandedFields = nbuilder.build(); for (String ef : expandedFields) { String ff = String.format("%s.%s", struct.getExpandedField(), ef); //logger.debug(ff); queryableFields.add(ff); } } } private void buildInherentQueryableFields() { for (Field field : info.allFieldsMap.values()) { if (field.isAnnotationPresent(APINoSee.class)) { continue; } if (field.isAnnotationPresent(Unqueryable.class)) { continue; } if (TypeUtils.isZstackBeanPrimitive(field.getType())) { queryableFields.add(field.getName()); continue; } if (Map.class.isAssignableFrom(field.getType())) { logger.warn(String.format("%s.%s is Map type, not support", field.getDeclaringClass(), field.getName())); continue; } if (Collection.class.isAssignableFrom(field.getType()) && field.isAnnotationPresent(Queryable.class)) { FieldUtils.CollectionGenericType gtype = (FieldUtils.CollectionGenericType) FieldUtils.inferGenericTypeOnMapOrCollectionField(field); if (!gtype.isInferred()) { throw new CloudRuntimeException(String.format("unable infer generic type of %s.%s", field.getDeclaringClass(), field.getName())); } if (gtype.getNestedGenericValue() != null) { throw new CloudRuntimeException(String.format("%s.%s is nested Collection, not support", field.getDeclaringClass(), field.getName())); } if (TypeUtils.isZstackBeanPrimitive(gtype.getValueType())) { queryableFields.add(field.getName()); continue; } Class nestedInventory = gtype.getValueType(); if (!nestedInventory.isAnnotationPresent(Inventory.class)) { throw new CloudRuntimeException(String.format("field[%s] on inventory class[%s] is collection type with @Queryable, but its generic type[%s] is not an inventory class", field.getName(), inventoryClass.getName(), nestedInventory.getName())); } QueryableBuilder nbuilder = new QueryableBuilder(visitedPath); nbuilder.inventoryClass = nestedInventory; List<String> nestedFields = nbuilder.build(); for (String nf : nestedFields) { queryableFields.add(String.format("%s.%s", field.getName(), nf)); } } } } } for (Map.Entry<Class, Class> e : inventoryQueryMessageMap.entrySet()) { QueryableBuilder builder = new QueryableBuilder(); builder.inventoryClass = e.getKey(); List<String> queryables = builder.build(); ret.put(e.getValue().getName(), queryables); } LinkedHashMap<String, List<String>> orderedRet = new LinkedHashMap<>(); // order List<String> keys = new ArrayList<>(); keys.addAll(ret.keySet()); Collections.sort(keys); for (String k : keys) { List<String> lst = ret.get(k); Collections.sort(lst); orderedRet.put(k, lst); } return orderedRet; } @Override public void writePython(final StringBuilder sb) { class PythonQueryObjectWriter { private String makeClassName(Class clazz) { return String.format("QueryObject%s", clazz.getSimpleName()); } private void write() { sb.append("\n#QueryObjectInventory"); Set<String> objectNameHavingWritten = new HashSet<>(); for (EntityInfo info : entityInfos.values() .stream() .sorted((i1, i2) -> { return i1.inventoryClass.getSimpleName().compareTo(i2.inventoryClass.getSimpleName()); }) .collect(Collectors.toList())) { if (objectNameHavingWritten.contains(info.inventoryClass.getName())) { continue; } write(info); objectNameHavingWritten.add(info.inventoryClass.getName()); } sb.append("\n\n").append("#QueryMessageInventoryMap").append("\nqueryMessageInventoryMap = {"); for (Map.Entry<Class, Class> e : inventoryQueryMessageMap.entrySet() .stream() .sorted((e1, e2) -> { return e1.getValue().getSimpleName().compareTo(e2.getValue().getSimpleName()); }) .collect(Collectors.toList())) { sb.append(String.format("\n%s '%s' : %s,", StringUtils.repeat(" ", 4), e.getValue().getSimpleName(), makeClassName(e.getKey()))); } sb.append("\n}\n"); } private void write(EntityInfo info) { sb.append(String.format("\nclass %s(object):", makeClassName(info.inventoryClass))); List<String> primitiveFields = new ArrayList<>(); List<String> expandedFields = new ArrayList<>(); Map<String, Class> nestedAndExpandedFields = new HashMap<>(); for (Field f : info.allFieldsMap.values()) { if (f.isAnnotationPresent(Unqueryable.class)) { continue; } if (f.isAnnotationPresent(APINoSee.class)) { continue; } if (Collection.class.isAssignableFrom(f.getType())) { Class invClass = FieldUtils.getGenericType(f); if (!TypeUtils.isZstackBeanPrimitive(invClass)) { if (invClass.isAnnotationPresent(Inventory.class)) { expandedFields.add(String.format("'%s'", f.getName())); nestedAndExpandedFields.put(f.getName(), invClass); } } } else { primitiveFields.add(String.format("'%s'", f.getName())); } } primitiveFields.add("'__userTag__'"); primitiveFields.add("'__systemTag__'"); sb.append(String.format("\n%s PRIMITIVE_FIELDS = [%s]", StringUtils.repeat(" ", 4), StringUtils.join(primitiveFields, ","))); for (ExpandedQueryStruct s : info.expandedQueries.values()) { if (s.isHidden()) { continue; } expandedFields.add(String.format("'%s'", s.getExpandedField())); nestedAndExpandedFields.put(s.getExpandedField(), s.getInventoryClass()); } for (ExpandedQueryAliasInfo i : info.aliases.values()) { expandedFields.add(String.format("'%s'", i.alias)); nestedAndExpandedFields.put(i.alias, i.inventoryClass); } sb.append(String.format("\n%s EXPANDED_FIELDS = [%s]", StringUtils.repeat(" ", 4), StringUtils.join(expandedFields, ","))); sb.append(String.format("\n%s QUERY_OBJECT_MAP = {", StringUtils.repeat(" ", 4))); for (Map.Entry<String, Class> e : nestedAndExpandedFields.entrySet()) { sb.append(String.format("\n%s'%s' : '%s',", StringUtils.repeat(" ", 8), e.getKey(), makeClassName(e.getValue()))); } sb.append(String.format("\n%s}\n", StringUtils.repeat(" ", 5))); } } new PythonQueryObjectWriter().write(); } }