| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 common |
| |
| import ( |
| "encoding/base64" |
| "fmt" |
| "math" |
| "net" |
| "net/url" |
| "strconv" |
| "strings" |
| ) |
| |
| import ( |
| gxset "github.com/dubbogo/gost/container/set" |
| "github.com/jinzhu/copier" |
| perrors "github.com/pkg/errors" |
| "github.com/satori/go.uuid" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go/common/constant" |
| "github.com/apache/dubbo-go/common/logger" |
| ) |
| |
| // /////////////////////////////// |
| // dubbo role type |
| // /////////////////////////////// |
| |
| // role constant |
| const ( |
| // CONSUMER is consumer role |
| CONSUMER = iota |
| // CONFIGURATOR is configurator role |
| CONFIGURATOR |
| // ROUTER is router role |
| ROUTER |
| // PROVIDER is provider role |
| PROVIDER |
| PROTOCOL = "protocol" |
| ) |
| |
| var ( |
| // DubboNodes Dubbo service node |
| DubboNodes = [...]string{"consumers", "configurators", "routers", "providers"} |
| // DubboRole Dubbo service role |
| DubboRole = [...]string{"consumer", "", "routers", "provider"} |
| ) |
| |
| // nolint |
| type RoleType int |
| |
| func (t RoleType) String() string { |
| return DubboNodes[t] |
| } |
| |
| // Role returns role by @RoleType |
| func (t RoleType) Role() string { |
| return DubboRole[t] |
| } |
| |
| type baseUrl struct { |
| Protocol string |
| Location string // ip+port |
| Ip string |
| Port string |
| params url.Values |
| PrimitiveURL string |
| } |
| |
| // URL is not thread-safe. |
| // we fail to define this struct to be immutable object. |
| // but, those method which will update the URL, including SetParam, SetParams |
| // are only allowed to be invoked in creating URL instance |
| // Please keep in mind that this struct is immutable after it has been created and initialized. |
| type URL struct { |
| baseUrl |
| Path string // like /com.ikurento.dubbo.UserProvider3 |
| Username string |
| Password string |
| Methods []string |
| // special for registry |
| SubURL *URL |
| } |
| |
| // Option accepts url |
| // Option will define a function of handling URL |
| type option func(*URL) |
| |
| // WithUsername sets username for url |
| func WithUsername(username string) option { |
| return func(url *URL) { |
| url.Username = username |
| } |
| } |
| |
| // WithPassword sets password for url |
| func WithPassword(pwd string) option { |
| return func(url *URL) { |
| url.Password = pwd |
| } |
| } |
| |
| // WithMethods sets methods for url |
| func WithMethods(methods []string) option { |
| return func(url *URL) { |
| url.Methods = methods |
| } |
| } |
| |
| // WithParams sets params for url |
| func WithParams(params url.Values) option { |
| return func(url *URL) { |
| url.params = params |
| } |
| } |
| |
| // WithParamsValue sets params field for url |
| func WithParamsValue(key, val string) option { |
| return func(url *URL) { |
| url.SetParam(key, val) |
| } |
| } |
| |
| // WithProtocol sets protocol for url |
| func WithProtocol(proto string) option { |
| return func(url *URL) { |
| url.Protocol = proto |
| } |
| } |
| |
| // WithIp sets ip for url |
| func WithIp(ip string) option { |
| return func(url *URL) { |
| url.Ip = ip |
| } |
| } |
| |
| // WithPort sets port for url |
| func WithPort(port string) option { |
| return func(url *URL) { |
| url.Port = port |
| } |
| } |
| |
| // WithPath sets path for url |
| func WithPath(path string) option { |
| return func(url *URL) { |
| url.Path = "/" + strings.TrimPrefix(path, "/") |
| } |
| } |
| |
| // WithLocation sets location for url |
| func WithLocation(location string) option { |
| return func(url *URL) { |
| url.Location = location |
| } |
| } |
| |
| // WithToken sets token for url |
| func WithToken(token string) option { |
| return func(url *URL) { |
| if len(token) > 0 { |
| value := token |
| if strings.ToLower(token) == "true" || strings.ToLower(token) == "default" { |
| u, err := uuid.NewV4() |
| if err != nil { |
| logger.Errorf("could not generator UUID: %v", err) |
| return |
| } |
| value = u.String() |
| } |
| url.SetParam(constant.TOKEN_KEY, value) |
| } |
| } |
| } |
| |
| // NewURLWithOptions will create a new url with options |
| func NewURLWithOptions(opts ...option) *URL { |
| url := &URL{} |
| for _, opt := range opts { |
| opt(url) |
| } |
| url.Location = url.Ip + ":" + url.Port |
| return url |
| } |
| |
| // NewURL will create a new url |
| // the urlString should not be empty |
| func NewURL(urlString string, opts ...option) (URL, error) { |
| var ( |
| err error |
| rawUrlString string |
| serviceUrl *url.URL |
| s = URL{baseUrl: baseUrl{}} |
| ) |
| |
| // new a null instance |
| if urlString == "" { |
| return s, nil |
| } |
| |
| rawUrlString, err = url.QueryUnescape(urlString) |
| if err != nil { |
| return s, perrors.Errorf("url.QueryUnescape(%s), error{%v}", urlString, err) |
| } |
| |
| // rawUrlString = "//" + rawUrlString |
| if strings.Index(rawUrlString, "//") < 0 { |
| t := URL{baseUrl: baseUrl{}} |
| for _, opt := range opts { |
| opt(&t) |
| } |
| rawUrlString = t.Protocol + "://" + rawUrlString |
| } |
| serviceUrl, err = url.Parse(rawUrlString) |
| if err != nil { |
| return s, perrors.Errorf("url.Parse(url string{%s}), error{%v}", rawUrlString, err) |
| } |
| |
| s.params, err = url.ParseQuery(serviceUrl.RawQuery) |
| if err != nil { |
| return s, perrors.Errorf("url.ParseQuery(raw url string{%s}), error{%v}", serviceUrl.RawQuery, err) |
| } |
| |
| s.PrimitiveURL = urlString |
| s.Protocol = serviceUrl.Scheme |
| s.Username = serviceUrl.User.Username() |
| s.Password, _ = serviceUrl.User.Password() |
| s.Location = serviceUrl.Host |
| s.Path = serviceUrl.Path |
| if strings.Contains(s.Location, ":") { |
| s.Ip, s.Port, err = net.SplitHostPort(s.Location) |
| if err != nil { |
| return s, perrors.Errorf("net.SplitHostPort(url.Host{%s}), error{%v}", s.Location, err) |
| } |
| } |
| for _, opt := range opts { |
| opt(&s) |
| } |
| return s, nil |
| } |
| |
| // URLEqual judge @url and @c is equal or not. |
| func (c URL) URLEqual(url URL) bool { |
| c.Ip = "" |
| c.Port = "" |
| url.Ip = "" |
| url.Port = "" |
| cGroup := c.GetParam(constant.GROUP_KEY, "") |
| urlGroup := url.GetParam(constant.GROUP_KEY, "") |
| cKey := c.Key() |
| urlKey := url.Key() |
| |
| if cGroup == constant.ANY_VALUE { |
| cKey = strings.Replace(cKey, "group=*", "group="+urlGroup, 1) |
| } else if urlGroup == constant.ANY_VALUE { |
| urlKey = strings.Replace(urlKey, "group=*", "group="+cGroup, 1) |
| } |
| |
| // 1. protocol, username, password, ip, port, service name, group, version should be equal |
| if cKey != urlKey { |
| return false |
| } |
| |
| // 2. if url contains enabled key, should be true, or * |
| if url.GetParam(constant.ENABLED_KEY, "true") != "true" && url.GetParam(constant.ENABLED_KEY, "") != constant.ANY_VALUE { |
| return false |
| } |
| |
| // TODO :may need add interface key any value condition |
| return isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY)) |
| } |
| |
| func isMatchCategory(category1 string, category2 string) bool { |
| if len(category2) == 0 { |
| return category1 == constant.DEFAULT_CATEGORY |
| } else if strings.Contains(category2, constant.ANY_VALUE) { |
| return true |
| } else if strings.Contains(category2, constant.REMOVE_VALUE_PREFIX) { |
| return !strings.Contains(category2, constant.REMOVE_VALUE_PREFIX+category1) |
| } else { |
| return strings.Contains(category2, category1) |
| } |
| } |
| |
| func (c URL) String() string { |
| var buf strings.Builder |
| if len(c.Username) == 0 && len(c.Password) == 0 { |
| buf.WriteString(fmt.Sprintf( |
| "%s://%s:%s%s?", |
| c.Protocol, c.Ip, c.Port, c.Path)) |
| } else { |
| buf.WriteString(fmt.Sprintf( |
| "%s://%s:%s@%s:%s%s?", |
| c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Path)) |
| } |
| buf.WriteString(c.params.Encode()) |
| return buf.String() |
| } |
| |
| // Key gets key |
| func (c URL) Key() string { |
| buildString := fmt.Sprintf( |
| "%s://%s:%s@%s:%s/?interface=%s&group=%s&version=%s", |
| c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Service(), c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, "")) |
| return buildString |
| } |
| |
| // ServiceKey gets a unique key of a service. |
| func (c URL) ServiceKey() string { |
| intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) |
| if intf == "" { |
| return "" |
| } |
| var buf strings.Builder |
| group := c.GetParam(constant.GROUP_KEY, "") |
| if group != "" { |
| buf.WriteString(group) |
| buf.WriteString("/") |
| } |
| |
| buf.WriteString(intf) |
| |
| version := c.GetParam(constant.VERSION_KEY, "") |
| if version != "" && version != "0.0.0" { |
| buf.WriteString(":") |
| buf.WriteString(version) |
| } |
| |
| return buf.String() |
| } |
| |
| // ColonSeparatedKey |
| // The format is "{interface}:[version]:[group]" |
| func (c *URL) ColonSeparatedKey() string { |
| intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) |
| if intf == "" { |
| return "" |
| } |
| var buf strings.Builder |
| buf.WriteString(intf) |
| buf.WriteString(":") |
| version := c.GetParam(constant.VERSION_KEY, "") |
| if version != "" && version != "0.0.0" { |
| buf.WriteString(version) |
| } |
| group := c.GetParam(constant.GROUP_KEY, "") |
| buf.WriteString(":") |
| if group != "" { |
| buf.WriteString(group) |
| } |
| return buf.String() |
| } |
| |
| // EncodedServiceKey encode the service key |
| func (c *URL) EncodedServiceKey() string { |
| serviceKey := c.ServiceKey() |
| return strings.Replace(serviceKey, "/", "*", 1) |
| } |
| |
| // Service gets service |
| func (c URL) Service() string { |
| service := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) |
| if service != "" { |
| return service |
| } else if c.SubURL != nil { |
| service = c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) |
| if service != "" { // if url.path is "" then return suburl's path, special for registry url |
| return service |
| } |
| } |
| return "" |
| } |
| |
| // AddParam will add the key-value pair |
| // Not thread-safe |
| // think twice before using it. |
| func (c *URL) AddParam(key string, value string) { |
| c.params.Add(key, value) |
| } |
| |
| // SetParam will put the key-value pair into url |
| // it's not thread safe. |
| // think twice before you want to use this method |
| // usually it should only be invoked when you want to initialized an url |
| func (c *URL) SetParam(key string, value string) { |
| c.params.Set(key, value) |
| } |
| |
| // RangeParams will iterate the params |
| // it's not thread-safe |
| func (c *URL) RangeParams(f func(key, value string) bool) { |
| for k, v := range c.params { |
| if !f(k, v[0]) { |
| break |
| } |
| } |
| } |
| |
| // GetParam gets value by key |
| func (c URL) GetParam(s string, d string) string { |
| r := c.params.Get(s) |
| if len(r) == 0 { |
| r = d |
| } |
| return r |
| } |
| |
| // GetParams gets values |
| func (c URL) GetParams() url.Values { |
| return c.params |
| } |
| |
| // GetParamAndDecoded gets values and decode |
| func (c URL) GetParamAndDecoded(key string) (string, error) { |
| ruleDec, err := base64.URLEncoding.DecodeString(c.GetParam(key, "")) |
| value := string(ruleDec) |
| return value, err |
| } |
| |
| // GetRawParam gets raw param |
| func (c URL) GetRawParam(key string) string { |
| switch key { |
| case PROTOCOL: |
| return c.Protocol |
| case "username": |
| return c.Username |
| case "host": |
| return strings.Split(c.Location, ":")[0] |
| case "password": |
| return c.Password |
| case "port": |
| return c.Port |
| case "path": |
| return c.Path |
| default: |
| return c.GetParam(key, "") |
| } |
| } |
| |
| // GetParamBool judge whether @key exists or not |
| func (c URL) GetParamBool(key string, d bool) bool { |
| r, err := strconv.ParseBool(c.GetParam(key, "")) |
| if err != nil { |
| return d |
| } |
| return r |
| } |
| |
| // GetParamInt gets int value by @key |
| func (c URL) GetParamInt(key string, d int64) int64 { |
| r, err := strconv.Atoi(c.GetParam(key, "")) |
| if r == 0 || err != nil { |
| return d |
| } |
| return int64(r) |
| } |
| |
| // GetMethodParamInt gets int method param |
| func (c URL) GetMethodParamInt(method string, key string, d int64) int64 { |
| r, err := strconv.Atoi(c.GetParam("methods."+method+"."+key, "")) |
| if r == 0 || err != nil { |
| return d |
| } |
| return int64(r) |
| } |
| |
| // GetMethodParamInt64 gets int64 method param |
| func (c URL) GetMethodParamInt64(method string, key string, d int64) int64 { |
| r := c.GetMethodParamInt(method, key, math.MinInt64) |
| if r == math.MinInt64 { |
| return c.GetParamInt(key, d) |
| } |
| return r |
| } |
| |
| // GetMethodParam gets method param |
| func (c URL) GetMethodParam(method string, key string, d string) string { |
| r := c.GetParam("methods."+method+"."+key, "") |
| if r == "" { |
| r = d |
| } |
| return r |
| } |
| |
| // GetMethodParamBool judge whether @method param exists or not |
| func (c URL) GetMethodParamBool(method string, key string, d bool) bool { |
| r := c.GetParamBool("methods."+method+"."+key, d) |
| return r |
| } |
| |
| // SetParams will put all key-value pair into url. |
| // 1. if there already has same key, the value will be override |
| // 2. it's not thread safe |
| // 3. think twice when you want to invoke this method |
| func (c *URL) SetParams(m url.Values) { |
| for k := range m { |
| c.SetParam(k, m.Get(k)) |
| } |
| } |
| |
| // ToMap transfer URL to Map |
| func (c URL) ToMap() map[string]string { |
| paramsMap := make(map[string]string) |
| |
| c.RangeParams(func(key, value string) bool { |
| paramsMap[key] = value |
| return true |
| }) |
| |
| if c.Protocol != "" { |
| paramsMap[PROTOCOL] = c.Protocol |
| } |
| if c.Username != "" { |
| paramsMap["username"] = c.Username |
| } |
| if c.Password != "" { |
| paramsMap["password"] = c.Password |
| } |
| if c.Location != "" { |
| paramsMap["host"] = strings.Split(c.Location, ":")[0] |
| var port string |
| if strings.Contains(c.Location, ":") { |
| port = strings.Split(c.Location, ":")[1] |
| } else { |
| port = "0" |
| } |
| paramsMap["port"] = port |
| } |
| if c.Protocol != "" { |
| paramsMap[PROTOCOL] = c.Protocol |
| } |
| if c.Path != "" { |
| paramsMap["path"] = c.Path |
| } |
| if len(paramsMap) == 0 { |
| return nil |
| } |
| return paramsMap |
| } |
| |
| // configuration > reference config >service config |
| // in this function we should merge the reference local url config into the service url from registry. |
| // TODO configuration merge, in the future , the configuration center's config should merge too. |
| |
| // MergeUrl will merge those two url |
| // the result is based on serviceUrl, and the key which si only contained in referenceUrl |
| // will be added into result. |
| // for example, if serviceUrl contains params (a1->v1, b1->v2) and referenceUrl contains params(a2->v3, b1 -> v4) |
| // the params of result will be (a1->v1, b1->v2, a2->v3). |
| // You should notice that the value of b1 is v2, not v4. |
| // due to URL is not thread-safe, so this method is not thread-safe |
| func MergeUrl(serviceUrl *URL, referenceUrl *URL) *URL { |
| mergedUrl := serviceUrl.Clone() |
| |
| // iterator the referenceUrl if serviceUrl not have the key ,merge in |
| referenceUrl.RangeParams(func(key, value string) bool { |
| if v := mergedUrl.GetParam(key, ""); len(v) == 0 { |
| mergedUrl.SetParam(key, value) |
| } |
| return true |
| }) |
| // loadBalance,cluster,retries strategy config |
| methodConfigMergeFcn := mergeNormalParam(mergedUrl, referenceUrl, []string{constant.LOADBALANCE_KEY, constant.CLUSTER_KEY, constant.RETRIES_KEY, constant.TIMEOUT_KEY}) |
| |
| // remote timestamp |
| if v := serviceUrl.GetParam(constant.TIMESTAMP_KEY, ""); len(v) > 0 { |
| mergedUrl.SetParam(constant.REMOTE_TIMESTAMP_KEY, v) |
| mergedUrl.SetParam(constant.TIMESTAMP_KEY, referenceUrl.GetParam(constant.TIMESTAMP_KEY, "")) |
| } |
| |
| // finally execute methodConfigMergeFcn |
| for _, method := range referenceUrl.Methods { |
| for _, fcn := range methodConfigMergeFcn { |
| fcn("methods." + method) |
| } |
| } |
| |
| return mergedUrl |
| } |
| |
| // Clone will copy the url |
| func (c *URL) Clone() *URL { |
| newUrl := &URL{} |
| copier.Copy(newUrl, c) |
| newUrl.params = url.Values{} |
| c.RangeParams(func(key, value string) bool { |
| newUrl.SetParam(key, value) |
| return true |
| }) |
| return newUrl |
| } |
| |
| func (c *URL) CloneExceptParams(excludeParams *gxset.HashSet) *URL { |
| newUrl := &URL{} |
| copier.Copy(newUrl, c) |
| newUrl.params = url.Values{} |
| c.RangeParams(func(key, value string) bool { |
| if !excludeParams.Contains(key) { |
| newUrl.SetParam(key, value) |
| } |
| return true |
| }) |
| return newUrl |
| } |
| |
| // Copy url based on the reserved parameter's keys. |
| func (c *URL) CloneWithParams(reserveParams []string) *URL { |
| params := url.Values{} |
| for _, reserveParam := range reserveParams { |
| v := c.GetParam(reserveParam, "") |
| if len(v) != 0 { |
| params.Set(reserveParam, v) |
| } |
| } |
| |
| return NewURLWithOptions( |
| WithProtocol(c.Protocol), |
| WithUsername(c.Username), |
| WithPassword(c.Password), |
| WithIp(c.Ip), |
| WithPort(c.Port), |
| WithPath(c.Path), |
| WithMethods(c.Methods), |
| WithParams(params), |
| ) |
| } |
| |
| func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []func(method string) { |
| methodConfigMergeFcn := make([]func(method string), 0, len(paramKeys)) |
| for _, paramKey := range paramKeys { |
| if v := referenceUrl.GetParam(paramKey, ""); len(v) > 0 { |
| mergedUrl.SetParam(paramKey, v) |
| } |
| methodConfigMergeFcn = append(methodConfigMergeFcn, func(method string) { |
| if v := referenceUrl.GetParam(method+"."+paramKey, ""); len(v) > 0 { |
| mergedUrl.SetParam(method+"."+paramKey, v) |
| } |
| }) |
| } |
| return methodConfigMergeFcn |
| } |
| |
| // URLSlice will be used to sort URL instance |
| // Instances will be order by URL.String() |
| type URLSlice []URL |
| |
| // nolint |
| func (s URLSlice) Len() int { |
| return len(s) |
| } |
| |
| // nolint |
| func (s URLSlice) Less(i, j int) bool { |
| return s[i].String() < s[j].String() |
| } |
| |
| // nolint |
| func (s URLSlice) Swap(i, j int) { |
| s[i], s[j] = s[j], s[i] |
| } |