// Copyright 2019 Yunion // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package db import ( "context" "reflect" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" "yunion.io/x/pkg/util/version" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/apis" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/stringutils2" ) type Caller struct { modelVal reflect.Value funcName string inputs []interface{} funcVal reflect.Value } func NewCaller(model interface{}, fName string) *Caller { return &Caller{ modelVal: reflect.ValueOf(model), funcName: fName, } } func (c *Caller) Inputs(inputs ...interface{}) *Caller { c.inputs = inputs return c } func (c *Caller) Call() ([]reflect.Value, error) { return callObject(c.modelVal, c.funcName, c.inputs...) } func call(obj interface{}, fName string, inputs ...interface{}) ([]reflect.Value, error) { return callObject(reflect.ValueOf(obj), fName, inputs...) } func findFunc(modelVal reflect.Value, fName string) (reflect.Value, error) { funcVal := modelVal.MethodByName(fName) if !funcVal.IsValid() || funcVal.IsNil() { log.Debugf("find method %s for %s", fName, modelVal.Type()) if modelVal.Kind() != reflect.Ptr { return funcVal, errors.Wrapf(httperrors.ErrNotImplemented, "%s not implemented", fName) } modelVal = modelVal.Elem() if modelVal.Kind() != reflect.Struct { return funcVal, errors.Wrapf(httperrors.ErrNotImplemented, "%s not implemented", fName) } modelType := modelVal.Type() for i := 0; i < modelType.NumField(); i += 1 { fieldType := modelType.Field(i) if fieldType.Anonymous { fieldValue := modelVal.Field(i) if fieldValue.Kind() != reflect.Ptr && fieldValue.CanAddr() { newFuncVal, err := findFunc(fieldValue.Addr(), fName) if err == nil { if !funcVal.IsValid() || funcVal.IsNil() { funcVal = newFuncVal } else { return funcVal, errors.Wrapf(httperrors.ErrConflict, "%s is ambiguous", fName) } } } else if fieldValue.Kind() == reflect.Ptr { newFuncVal, err := findFunc(fieldValue, fName) if err == nil { if !funcVal.IsValid() || funcVal.IsNil() { funcVal = newFuncVal } else { return funcVal, errors.Wrapf(httperrors.ErrConflict, "%s is ambiguous", fName) } } } } } if !funcVal.IsValid() || funcVal.IsNil() { return funcVal, errors.Wrapf(httperrors.ErrNotImplemented, "%s is not implemented", fName) } } return funcVal, nil } const ( MethodNotFoundError = errors.Error("MethodNotFoundError") ) func callObject(modelVal reflect.Value, fName string, inputs ...interface{}) ([]reflect.Value, error) { funcVal := modelVal.MethodByName(fName) if !funcVal.IsValid() || funcVal.IsNil() { return nil, errors.Wrapf(MethodNotFoundError, "%s method not found, please check service version, current version: %s", fName, version.GetShortString()) } return callFunc(funcVal, fName, inputs...) } func callFunc(funcVal reflect.Value, fName string, inputs ...interface{}) ([]reflect.Value, error) { funcType := funcVal.Type() paramLen := funcType.NumIn() if paramLen != len(inputs) { return nil, httperrors.NewInternalServerError("%s method params length not match, expected %d, input %d", fName, paramLen, len(inputs)) } params := make([]*param, paramLen) for i := range inputs { params[i] = newParam(funcType.In(i), inputs[i]) } args, err := convertParams(params) if err != nil { return nil, err } return funcVal.Call(args), nil } func convertParams(params []*param) ([]reflect.Value, error) { ret := make([]reflect.Value, 0) for _, p := range params { val, err := p.convert() if err != nil { return ret, err } ret = append(ret, val) } return ret, nil } type param struct { pType reflect.Type input interface{} } func newParam(pType reflect.Type, input interface{}) *param { return ¶m{ pType: pType, input: input, } } func isJSONObject(input interface{}) (jsonutils.JSONObject, bool) { val := reflect.ValueOf(input) obj, ok := val.Interface().(jsonutils.JSONObject) if !ok { return nil, false } return obj, true } func (p *param) convert() (reflect.Value, error) { if p.input == nil { return reflect.New(p.pType).Elem(), nil } obj, ok := isJSONObject(p.input) if !ok { return reflect.ValueOf(p.input), nil } // generate object by type val := reflect.New(p.pType) err := obj.Unmarshal(val.Interface()) if err != nil { return reflect.Value{}, errors.Wrapf(err, "unable to convert '%v' to Type %q", p.input, p.pType.Name()) } return val.Elem(), nil } func ValueToJSONObject(out reflect.Value) jsonutils.JSONObject { return _valueToJSONObject(out, false) } func _valueToJSONObject(out reflect.Value, allFields bool) jsonutils.JSONObject { if gotypes.IsNil(out.Interface()) { return nil } if obj, ok := isJSONObject(out); ok { return obj } if allFields { return jsonutils.MarshalAll(out.Interface()) } else { return jsonutils.Marshal(out.Interface()) } } func ValueToJSONDict(out reflect.Value) *jsonutils.JSONDict { return _valueToJSONDict(out, false) } func _valueToJSONDict(out reflect.Value, allFields bool) *jsonutils.JSONDict { jsonObj := _valueToJSONObject(out, allFields) if jsonObj == nil { return nil } return jsonObj.(*jsonutils.JSONDict) } func ValueToError(out reflect.Value) error { errVal := out.Interface() if !gotypes.IsNil(errVal) { return errVal.(error) } return nil } func mergeInputOutputData(input *jsonutils.JSONDict, resVal reflect.Value) *jsonutils.JSONDict { output := _valueToJSONDict(resVal, true) // preserve the input info not returned by caller ret := input.Copy() jsonMap, _ := output.GetMap() for k, v := range jsonMap { if input.Contains(k) && v == jsonutils.JSONNull { ret.Remove(k) continue } if v != jsonutils.JSONNull && !v.IsZero() { ret.Set(k, v) } } return ret } func ValidateCreateData(funcName string, manager IModelManager, ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { ret, err := call(manager, funcName, ctx, userCred, ownerId, query, data) if err != nil { return nil, httperrors.NewGeneralError(err) } if len(ret) != 2 { return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) } resVal := ret[0] if err := ValueToError(ret[1]); err != nil { return nil, err } return mergeInputOutputData(data, resVal), nil } func ExpandBatchCreateData(manager IModelManager, ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict, index int) (*jsonutils.JSONDict, error) { funcName := "ExpandBatchCreateData" ret, err := call(manager, funcName, ctx, userCred, ownerId, query, data, index) if err != nil { return nil, errors.Wrapf(err, "call %s", funcName) } if len(ret) != 2 { return nil, httperrors.NewInternalServerError("Invald %s return value", funcName) } resVal := ret[0] if err := ValueToError(ret[1]); err != nil { return nil, errors.Wrap(err, "ValueToError") } return mergeInputOutputData(data, resVal), nil } func ListItemFilter(manager IModelManager, ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { return _callListQueryFilter(manager, "ListItemFilter", ctx, q, userCred, query) } func ExtendListQuery(manager IModelManager, ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { return _callListQueryFilter(manager, "ExtendListQuery", ctx, q, userCred, query) } func _callListQueryFilter(manager IModelManager, funcName string, ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { ret, err := call(manager, funcName, ctx, q, userCred, query) if err != nil { return nil, httperrors.NewGeneralError(err) } if len(ret) != 2 { return nil, httperrors.NewInternalServerError("Invald %s return value count %d", funcName, len(ret)) } if err := ValueToError(ret[1]); err != nil { return nil, err } return ret[0].Interface().(*sqlchemy.SQuery), nil } func OrderByExtraFields( manager IModelManager, ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject, ) (*sqlchemy.SQuery, error) { ret, err := call(manager, "OrderByExtraFields", ctx, q, userCred, query) if err != nil { return nil, httperrors.NewGeneralError(err) } if len(ret) != 2 { return nil, httperrors.NewInternalServerError("Invald OrderByExtraFields return value count %d", len(ret)) } if err := ValueToError(ret[1]); err != nil { return nil, err } return ret[0].Interface().(*sqlchemy.SQuery), nil } func FetchCustomizeColumns( manager IModelManager, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool, ) ([]*jsonutils.JSONDict, error) { ret, err := call(manager, "FetchCustomizeColumns", ctx, userCred, query, objs, fields, isList) if err != nil { return nil, httperrors.NewGeneralError(err) } if len(ret) != 1 { return nil, httperrors.NewInternalServerError("Invalid FetchCustomizeColumns return value count %d", len(ret)) } if ret[0].IsNil() { return nil, nil } if ret[0].Kind() != reflect.Slice { return nil, httperrors.NewInternalServerError("Invalid FetchCustomizeColumns return value type, not a slice!") } if ret[0].Len() != len(objs) { return nil, httperrors.NewInternalServerError("Invalid FetchCustomizeColumns return value, inconsistent obj count: input %d != output %d", len(objs), ret[0].Len()) } showReason := false if query.Contains("show_fail_reason") { showReason = true } retVal := make([]*jsonutils.JSONDict, ret[0].Len()) for i := 0; i < ret[0].Len(); i += 1 { jsonDict := ValueToJSONDict(ret[0].Index(i)) // NOTE: don't use obj update jsonDict as retval jsonDict.Update(jsonutils.Marshal(objs[i]).(*jsonutils.JSONDict)) out := apis.ModelBaseDetails{ CanDelete: true, CanUpdate: true, } err = ValidateDeleteCondition(objs[i].(IModel), ctx, jsonDict) if err != nil { out.CanDelete = false if showReason { out.DeleteFailReason = httperrors.NewErrorFromGeneralError(ctx, err) } } err = ValidateUpdateCondition(objs[i].(IModel), ctx) if err != nil { out.CanUpdate = false if showReason { out.UpdateFailReason = httperrors.NewErrorFromGeneralError(ctx, err) } } jsonDict.Update(jsonutils.Marshal(out)) retVal[i] = jsonDict } return retVal, nil } func ValidateUpdateData(model IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { ret, err := call(model, "ValidateUpdateData", ctx, userCred, query, data) if err != nil { return nil, httperrors.NewGeneralError(err) } if len(ret) != 2 { return nil, httperrors.NewInternalServerError("Invald ValidateUpdateData return value") } resVal := ret[0] if err := ValueToError(ret[1]); err != nil { return nil, err } return mergeInputOutputData(data, resVal), nil } func CustomizeDelete(model IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { ret, err := call(model, "CustomizeDelete", ctx, userCred, query, data) if err != nil { return httperrors.NewGeneralError(err) } if len(ret) != 1 { return httperrors.NewInternalServerError("Invald CustomizeDelete return value") } return ValueToError(ret[0]) } func ValidateDeleteCondition(model IModel, ctx context.Context, data jsonutils.JSONObject) error { ret, err := call(model, "ValidateDeleteCondition", ctx, data) if err != nil { return httperrors.NewGeneralError(err) } if len(ret) != 1 { return httperrors.NewInternalServerError("Invald ValidateDeleteCondition return value") } return ValueToError(ret[0]) } func ValidateUpdateCondition(model IModel, ctx context.Context) error { ret, err := call(model, "ValidateUpdateCondition", ctx) if err != nil { return httperrors.NewGeneralError(err) } if len(ret) != 1 { return httperrors.NewInternalServerError("Invald ValidateUpdateCondition return value") } return ValueToError(ret[0]) }