package org.zstack.query; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.AbstractService; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.apimediator.GlobalApiMessageInterceptor; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.query.*; import org.zstack.header.rest.APINoSee; import org.zstack.header.search.Inventory; import org.zstack.header.search.SearchConstant; import org.zstack.utils.BeanUtils; import org.zstack.utils.FieldUtils; import org.zstack.utils.TypeUtils; import org.zstack.utils.Utils; import org.zstack.utils.gson.JSONObjectUtil; import org.zstack.utils.logging.CLogger; import static org.zstack.core.Platform.argerr; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; public class QueryFacadeImpl extends AbstractService implements QueryFacade, GlobalApiMessageInterceptor { private static CLogger logger = Utils.getLogger(QueryFacadeImpl.class); private Map<String, QueryBuilderFactory> builerFactories = new HashMap<>(); private String queryBuilderType = MysqlQueryBuilderFactory.type.toString(); @Autowired private PluginRegistry pluginRgty; @Autowired private CloudBus bus; @Autowired private ErrorFacade errf; private void validateConditions(List<QueryCondition> conditions) { for (QueryCondition cond : conditions) { // will throw out IllegalArgumentException if op is invalid QueryOp.valueOf(cond.getOp()); } } @Override public <T> List<T> query(APIQueryMessage msg, Class<T> inventoryClass) { validateConditions(msg.getConditions()); QueryBuilderFactory factory = getFactory(queryBuilderType); QueryBuilder builder = factory.createQueryBuilder(); return builder.query(msg, inventoryClass); } @Override public long count(APIQueryMessage msg, Class inventoryClass) { validateConditions(msg.getConditions()); QueryBuilderFactory factory = getFactory(queryBuilderType); QueryBuilder builder = factory.createQueryBuilder(); return builder.count(msg, inventoryClass); } private void populateExtensions() { for (QueryBuilderFactory extp : pluginRgty.getExtensionList(QueryBuilderFactory.class)) { QueryBuilderFactory old = builerFactories.get(extp.getQueryBuilderType().toString()); if (old != null) { throw new CloudRuntimeException(String.format("duplicate QueryBuilderFactory[%s, %s] for type[%s]", extp.getClass().getName(), old.getClass().getName(), extp.getQueryBuilderType())); } builerFactories.put(extp.getQueryBuilderType().toString(), extp); } } private QueryBuilderFactory getFactory(String type) { QueryBuilderFactory factory = builerFactories.get(type); if (factory == null) { throw new CloudRuntimeException(String.format("unable to find QueryBuilderFactory with type[%s]", type)); } return factory; } private void checkBoxTypeInInventory() { if (!CoreGlobalProperty.CHECK_BOX_TYPE_IN_INVENTORY) { return; } List<Class> inventoryClasses = BeanUtils.scanClass("org.zstack", Inventory.class); List<String> errors = new ArrayList<>(); for (Class clz : inventoryClasses) { boolean error = false; StringBuilder sb = new StringBuilder(String.format("inventory class[%s] contains below primitive fields:", clz.getName())); for (Field f : FieldUtils.getAllFields(clz)) { if (f.isAnnotationPresent(APINoSee.class)) { continue; } if (TypeUtils.isPrimitiveType(f.getType())) { error = true; sb.append(String.format("\n%s[%s]", f.getName(), f.getType().getName())); } } if (error) { errors.add(sb.toString()); } } if (!errors.isEmpty()) { throw new CloudRuntimeException(String.format("detected some inventory class using primitive type." + " Please change those primitive type field to corresponding box type:\n %s", StringUtils.join(errors, "\n\n"))); } } @Override public boolean start() { checkBoxTypeInInventory(); populateExtensions(); return true; } @Override public boolean stop() { return true; } public void setQueryBuilderType(String queryBuilderType) { this.queryBuilderType = queryBuilderType; } @Override @MessageSafe public void handleMessage(Message msg) { if (msg instanceof APIGenerateInventoryQueryDetailsMsg) { handle((APIGenerateInventoryQueryDetailsMsg) msg); } else if (msg instanceof APIQueryMessage) { handle((APIQueryMessage) msg); } else if (msg instanceof APIGenerateQueryableFieldsMsg) { handle((APIGenerateQueryableFieldsMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } private void handle(APIGenerateQueryableFieldsMsg msg) { QueryBuilderFactory factory = getFactory(queryBuilderType); QueryBuilder builder = factory.createQueryBuilder(); Map<String, List<String>> ret = builder.populateQueryableFields(); if (APIGenerateQueryableFieldsMsg.PYTHON_FORMAT.equals(msg.getFormat())) { QueryableFieldsPythonWriter writer = new QueryableFieldsPythonWriter(msg.getOutputFolder(), ret); writer.write(); } else { throw new CloudRuntimeException(String.format("unknown mediaType[%s]", msg.getFormat())); } APIGenerateQueryableFieldsEvent evt = new APIGenerateQueryableFieldsEvent(msg.getId()); bus.publish(evt); } private Map<Class, Method> replySetter = new HashMap<>(); private Map<Class, AutoQuery> autoQueryMap = new HashMap<>(); private void handle(APIQueryMessage msg) { AutoQuery at = autoQueryMap.get(msg.getClass()); if (at == null) { at = msg.getClass().getAnnotation(AutoQuery.class); if (at == null) { throw new OperationFailureException(errf.stringToInternalError( String.format("message[%s] is not annotated by @AutoQuery", msg.getClass()) )); } autoQueryMap.put(msg.getClass(), at); } Class replyClass = at.replyClass(); Class inventoryClass = at.inventoryClass(); try { APIQueryReply reply = (APIQueryReply) replyClass.newInstance(); Method setter = replySetter.get(inventoryClass); if (setter == null) { setter = replyClass.getDeclaredMethod("setInventories", List.class); if (setter == null) { throw new OperationFailureException(errf.stringToInternalError( String.format("query reply[%s] has no method setInventories()", replyClass.getName()) )); } setter.setAccessible(true); replySetter.put(inventoryClass, setter); } if (msg.isCount()) { long count = count(msg, inventoryClass); reply.setTotal(count); bus.reply(msg, reply); } else { List invs = query(msg, inventoryClass); setter.invoke(reply, invs); //TODO: merge this into mysql query builder if (msg.isReplyWithCount()) { long count = count(msg, inventoryClass); reply.setTotal(count); } bus.reply(msg, reply); } } catch (OperationFailureException of) { throw of; } catch (Exception e) { logger.warn(e.getMessage(), e); throw new OperationFailureException(errf.throwableToInternalError(e)); } } private void handle(APIGenerateInventoryQueryDetailsMsg msg) { InventoryQueryDetailsGenerator.generate(msg.getOutputDir(), msg.getBasePackageNames()); APIGenerateInventoryQueryDetailsEvent evt = new APIGenerateInventoryQueryDetailsEvent(msg.getId()); bus.publish(evt); } @Override public String getId() { return bus.makeLocalServiceId(SearchConstant.QUERY_FACADE_SERVICE_ID); } @Override public List<Class> getMessageClassToIntercept() { List<Class> ret = new ArrayList<>(); ret.add(APIQueryMessage.class); return ret; } @Override public InterceptorPosition getPosition() { return InterceptorPosition.FRONT; } @Override public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { APIQueryMessage qmsg = (APIQueryMessage) msg; for (QueryCondition cond : qmsg.getConditions()) { try { QueryOp.valueOf(cond.getOp()); } catch (IllegalArgumentException e) { throw new ApiMessageInterceptionException(errf.throwableToInvalidArgumentError(e)); } if (!QueryOp.NOT_NULL.equals(cond.getOp()) && !QueryOp.IS_NULL.equals(cond.getOp()) && cond.getValue() == null) { throw new ApiMessageInterceptionException(argerr("'value' of query condition %s cannot be null", JSONObjectUtil.toJsonString(cond))); } } return msg; } }