Skip to content
Snippets Groups Projects
url.go 15.7 KiB
Newer Older
AlexStocks's avatar
AlexStocks committed
/*
 * 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.
 */
fangyincheng's avatar
fangyincheng committed

vito.he's avatar
vito.he committed
package common
fangyincheng's avatar
fangyincheng committed

import (
	"bytes"
	"encoding/base64"
fangyincheng's avatar
fangyincheng committed
	"fmt"
zonghaishang's avatar
zonghaishang committed
	"math"
fangyincheng's avatar
fangyincheng committed
	"net"
	"net/url"
	"strconv"
	"strings"
fangyincheng's avatar
fangyincheng committed
)

import (
邹毅贤's avatar
邹毅贤 committed
	gxset "github.com/dubbogo/gost/container/set"
	"github.com/jinzhu/copier"
fangyincheng's avatar
fangyincheng committed
	perrors "github.com/pkg/errors"
xujianhai666's avatar
xujianhai666 committed
	"github.com/satori/go.uuid"
fangyincheng's avatar
fangyincheng committed
)
vito.he's avatar
vito.he committed
import (
	"github.com/apache/dubbo-go/common/constant"
vito.he's avatar
vito.he committed
)
/////////////////////////////////
// dubbo role type
/////////////////////////////////

邹毅贤's avatar
邹毅贤 committed
// role constant
	// CONSUMER is consumer role
	// CONFIGURATOR is configurator role
	// ROUTER is router role
	// PROVIDER is provider role
	// DubboNodes Dubbo service node
	DubboNodes = [...]string{"consumers", "configurators", "routers", "providers"}
邹毅贤's avatar
邹毅贤 committed
	// DubboRole Dubbo service role
邹毅贤's avatar
邹毅贤 committed
	DubboRole = [...]string{"consumer", "", "routers", "provider"}
// RoleType
type RoleType int

func (t RoleType) String() string {
	return DubboNodes[t]
}

// Role returns role
func (t RoleType) Role() string {
	return DubboRole[t]
vito.he's avatar
vito.he committed
type baseUrl struct {
	Protocol string
	Location string // ip+port
	Ip       string
	Port     string
	//url.Values is not safe map, add to avoid concurrent map read and map write error
vito.he's avatar
vito.he committed
	paramsLock   sync.RWMutex
vito.he's avatar
vito.he committed
	params       url.Values
vito.he's avatar
vito.he committed
	PrimitiveURL string
// URL is used to locate resourse to transfer data between nodes
type URL struct {
vito.he's avatar
vito.he committed
	baseUrl
	Path     string // like  /com.ikurento.dubbo.UserProvider3
	Username string
	Password string
	SubURL *URL
// Option accepts url
// Option will define a function of handling URL
type option func(*URL)

// WithUsername set username for url
func WithUsername(username string) option {
	return func(url *URL) {
		url.Username = username
	}
}

// WithPassword set password for url
func WithPassword(pwd string) option {
	return func(url *URL) {
		url.Password = pwd
	}
}
// WithMethods set methods for url
func WithMethods(methods []string) option {
	return func(url *URL) {
		url.Methods = methods
	}
}

// WithParams set params for url
func WithParams(params url.Values) option {
	return func(url *URL) {
vito.he's avatar
vito.he committed
		url.params = params
// WithParamsValue set params field for url
func WithParamsValue(key, val string) option {
	return func(url *URL) {
vito.he's avatar
vito.he committed
		url.SetParam(key, val)
// WithProtocol set protocol for url
vito.he's avatar
vito.he committed
func WithProtocol(proto string) option {
	return func(url *URL) {
		url.Protocol = proto
	}
}
// WithIp set ip for url
vito.he's avatar
vito.he committed
func WithIp(ip string) option {
	return func(url *URL) {
		url.Ip = ip
	}
}

// WithPort set port for url
vito.he's avatar
vito.he committed
func WithPort(port string) option {
	return func(url *URL) {
		url.Port = port
	}
}

// WithPath set path for url
func WithPath(path string) option {
	return func(url *URL) {
		url.Path = "/" + strings.TrimPrefix(path, "/")
// WithLocation set location for url
func WithLocation(location string) option {
	return func(url *URL) {
		url.Location = location
	}
}
// WithToken set token for url
xujianhai666's avatar
xujianhai666 committed
func WithToken(token string) option {
	return func(url *URL) {
		if len(token) > 0 {
			value := token
			if strings.ToLower(token) == "true" || strings.ToLower(token) == "default" {
				value = uuid.NewV4().String()
xujianhai666's avatar
xujianhai666 committed
			}
			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)
	}
fangyincheng's avatar
fangyincheng committed
	url.Location = url.Ip + ":" + url.Port
// NewURL will create a new url
// the urlString should not be empty
func NewURL(urlString string, opts ...option) (URL, error) {
fangyincheng's avatar
fangyincheng committed
	var (
		err          error
		rawUrlString string
		serviceUrl   *url.URL
		s            = URL{baseUrl: baseUrl{}}
	// new a null instance
	if urlString == "" {
		return s, nil
	}

fangyincheng's avatar
fangyincheng committed
	rawUrlString, err = url.QueryUnescape(urlString)
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.QueryUnescape(%s),  error{%v}", urlString, err)
	//rawUrlString = "//" + rawUrlString
		t := URL{baseUrl: baseUrl{}}
		for _, opt := range opts {
			opt(&t)
		}
		rawUrlString = t.Protocol + "://" + rawUrlString
	}
fangyincheng's avatar
fangyincheng committed
	serviceUrl, err = url.Parse(rawUrlString)
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.Parse(url string{%s}),  error{%v}", rawUrlString, err)
vito.he's avatar
vito.he committed
	s.params, err = url.ParseQuery(serviceUrl.RawQuery)
fangyincheng's avatar
fangyincheng committed
	if err != nil {
fangyincheng's avatar
fangyincheng committed
		return s, perrors.Errorf("url.ParseQuery(raw url string{%s}),  error{%v}", serviceUrl.RawQuery, err)
vito.he's avatar
vito.he committed
	s.PrimitiveURL = urlString
	s.Protocol = serviceUrl.Scheme
	s.Username = serviceUrl.User.Username()
	s.Password, _ = serviceUrl.User.Password()
vito.he's avatar
vito.he committed
	s.Location = serviceUrl.Host
	s.Path = serviceUrl.Path
	if strings.Contains(s.Location, ":") {
		s.Ip, s.Port, err = net.SplitHostPort(s.Location)
fangyincheng's avatar
fangyincheng committed
		if err != nil {
aliiohs's avatar
aliiohs committed
			return s, perrors.Errorf("net.SplitHostPort(url.Host{%s}), error{%v}", s.Location, err)
fangyincheng's avatar
fangyincheng committed
	return s, nil
}

// URLEqual judge @url and @c is equal or not.
func (c URL) URLEqual(url URL) bool {
fangyincheng's avatar
fangyincheng committed
	c.Ip = ""
	c.Port = ""
vito.he's avatar
vito.he committed
	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)
	}
Xargin's avatar
Xargin committed

	// 1. protocol, username, password, ip, port, service name, group, version should be equal
	if cKey != urlKey {
vito.he's avatar
vito.he committed
		return false
	}
Xargin's avatar
Xargin committed

	// 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
	}
Xargin's avatar
Xargin committed

	//TODO :may need add interface key any value condition
Xargin's avatar
Xargin committed
	return isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY))
vito.he's avatar
vito.he committed
}
xg.gao's avatar
xg.gao committed

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 {
vito.he's avatar
vito.he committed
		return strings.Contains(category2, category1)
xg.gao's avatar
xg.gao committed

func (c URL) String() string {
	var buildString string
	if len(c.Username) == 0 && len(c.Password) == 0 {
		buildString = fmt.Sprintf(
			"%s://%s:%s%s?",
			c.Protocol, c.Ip, c.Port, c.Path)
	} else {
		buildString = fmt.Sprintf(
			"%s://%s:%s@%s:%s%s?",
			c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Path)
	}
vito.he's avatar
vito.he committed
	c.paramsLock.RLock()
	buildString += c.params.Encode()
	c.paramsLock.RUnlock()
	return buildString
vito.he's avatar
vito.he committed
func (c URL) Key() string {
	buildString := fmt.Sprintf(
		"%s://%s:%s@%s:%s/?interface=%s&group=%s&version=%s",
fangyincheng's avatar
fangyincheng committed
		c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Service(), c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, ""))
vito.he's avatar
vito.he committed
	return buildString
// ServiceKey get a unique key of a service.
func (c URL) ServiceKey() string {
	intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/"))
	if intf == "" {
		return ""
	}
	buf := &bytes.Buffer{}
	group := c.GetParam(constant.GROUP_KEY, "")
	if group != "" {
		buf.WriteString(group)
		buf.WriteString("/")
	}

	buf.WriteString(intf)

fangyincheng's avatar
fangyincheng committed
	version := c.GetParam(constant.VERSION_KEY, "")
	if version != "" && version != "0.0.0" {
		buf.WriteString(":")
		buf.WriteString(version)
	}

	return buf.String()
vito.he's avatar
vito.he committed
}

Ooo0oO0o0oO's avatar
Ooo0oO0o0oO committed
// 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 ""
	}
	buf := &bytes.Buffer{}
	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 is used to encode service key
func (c *URL) EncodedServiceKey() string {
	serviceKey := c.ServiceKey()
	return strings.Replace(serviceKey, "/", "*", 1)
}

// Service is used to get 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, "/"))
aliiohs's avatar
aliiohs committed
		if service != "" { //if url.path is "" then return suburl's path, special for registry url
			return service
		}
	}
	return ""
// AddParam is used to add value for key
func (c *URL) AddParam(key string, value string) {
vito.he's avatar
vito.he committed
	c.params.Add(key, value)
	c.paramsLock.Unlock()
}

// SetParam is used to set key and value
vito.he's avatar
vito.he committed
func (c *URL) SetParam(key string, value string) {
	c.paramsLock.Lock()
	c.params.Set(key, value)
// RangeParams is used to range params
vito.he's avatar
vito.he committed
func (c *URL) RangeParams(f func(key, value string) bool) {
	c.paramsLock.RLock()
vito.he's avatar
vito.he committed
	defer c.paramsLock.RUnlock()
vito.he's avatar
vito.he committed
	for k, v := range c.params {
		if !f(k, v[0]) {
			break
		}
	}
}

// GetParam is used to get value by key
func (c URL) GetParam(s string, d string) string {
vito.he's avatar
vito.he committed
	c.paramsLock.RLock()
	defer c.paramsLock.RUnlock()
	r := c.params.Get(s)
	if len(r) == 0 {
vito.he's avatar
vito.he committed

// GetParams is used to get values
vito.he's avatar
vito.he committed
func (c URL) GetParams() url.Values {
	return c.params
}

// GetParamAndDecoded is used to get values and decode
aliiohs's avatar
aliiohs committed
func (c URL) GetParamAndDecoded(key string) (string, error) {
vito.he's avatar
vito.he committed
	c.paramsLock.RLock()
	defer c.paramsLock.RUnlock()
	ruleDec, err := base64.URLEncoding.DecodeString(c.GetParam(key, ""))
	value := string(ruleDec)
	return value, err
}
// GetRawParam is used to get raw param
aliiohs's avatar
aliiohs committed
func (c URL) GetRawParam(key string) string {
aliiohs's avatar
aliiohs committed
	switch key {
	case "protocol":
aliiohs's avatar
aliiohs committed
	case "username":
aliiohs's avatar
aliiohs committed
	case "host":
		return strings.Split(c.Location, ":")[0]
aliiohs's avatar
aliiohs committed
	case "password":
aliiohs's avatar
aliiohs committed
	case "port":
aliiohs's avatar
aliiohs committed
	case "path":
aliiohs's avatar
aliiohs committed
	default:
vito.he's avatar
vito.he committed
		return c.GetParam(key, "")
// GetParamBool is used to judge whether key exists or not
aliiohs's avatar
aliiohs committed
func (c URL) GetParamBool(s string, d bool) bool {
	r, err := strconv.ParseBool(c.GetParam(s, ""))
	if err != nil {
aliiohs's avatar
aliiohs committed
		return d
	}
	return r
}
// GetParamInt is used to get int value by key
func (c URL) GetParamInt(s string, d int64) int64 {
	r, err := strconv.Atoi(c.GetParam(s, ""))
	if r == 0 || err != nil {
// GetMethodParamInt is used to get 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 {
// GetMethodParamInt64 is used to get int64 method param
zonghaishang's avatar
zonghaishang committed
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 is used to get method param
func (c URL) GetMethodParam(method string, key string, d string) string {
	r := c.GetParam("methods."+method+"."+key, "")
	if r == "" {
// GetMethodParamBool is used to judge whether method param exists or not
Ooo0oO0o0oO's avatar
Ooo0oO0o0oO committed
func (c URL) GetMethodParamBool(method string, key string, d bool) bool {
	r := c.GetParamBool("methods."+method+"."+key, d)
	return r
}

// RemoveParams is used to remove params
fangyincheng's avatar
fangyincheng committed
func (c *URL) RemoveParams(set *gxset.HashSet) {
vito.he's avatar
vito.he committed
	c.paramsLock.Lock()
	defer c.paramsLock.Unlock()
	for k := range set.Items {
vito.he's avatar
vito.he committed
		s := k.(string)
vito.he's avatar
vito.he committed
		delete(c.params, s)
// SetParams is used to set params
vito.he's avatar
vito.he committed
func (c *URL) SetParams(m url.Values) {
	for k := range m {
		c.SetParam(k, m.Get(k))
aliiohs's avatar
aliiohs committed
// ToMap transfer URL to Map
func (c URL) ToMap() map[string]string {
	paramsMap := make(map[string]string)
vito.he's avatar
vito.he committed
	c.RangeParams(func(key, value string) bool {
		paramsMap[key] = value
		return true
	})

aliiohs's avatar
aliiohs committed
	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
aliiohs's avatar
aliiohs committed
	}
	if c.Protocol != "" {
		paramsMap["protocol"] = c.Protocol
	}
	if c.Path != "" {
		paramsMap["path"] = c.Path
	}
aliiohs's avatar
aliiohs committed
	if len(paramsMap) == 0 {
		return nil
	}
aliiohs's avatar
aliiohs committed
	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.
vito.he's avatar
vito.he committed

// MergeUrl is used to merge url
vito.he's avatar
vito.he committed
func MergeUrl(serviceUrl *URL, referenceUrl *URL) *URL {
	mergedUrl := serviceUrl.Clone()

	//iterator the referenceUrl if serviceUrl not have the key ,merge in
vito.he's avatar
vito.he committed
	referenceUrl.RangeParams(func(key, value string) bool {
		if v := mergedUrl.GetParam(key, ""); len(v) == 0 {
			mergedUrl.SetParam(key, value)
vito.he's avatar
vito.he committed
		return true
vito.he's avatar
vito.he committed
	//loadBalance,cluster,retries strategy config
	methodConfigMergeFcn := mergeNormalParam(mergedUrl, referenceUrl, []string{constant.LOADBALANCE_KEY, constant.CLUSTER_KEY, constant.RETRIES_KEY, constant.TIMEOUT_KEY})
vito.he's avatar
vito.he committed
	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 is used to clone a url
func (c *URL) Clone() *URL {
	newUrl := &URL{}
	copier.Copy(newUrl, c)
vito.he's avatar
vito.he committed
	newUrl.params = url.Values{}
	c.RangeParams(func(key, value string) bool {
		newUrl.SetParam(key, value)
		return true
	})
vito.he's avatar
vito.he committed

xg.gao's avatar
xg.gao committed
// Copy url based on the reserved parameters' keys.
func (c *URL) CloneWithParams(reserveParams []string) *URL {
	params := url.Values{}
	for _, reserveParam := range reserveParams {
		v := c.GetParam(reserveParam, "")
xg.gao's avatar
xg.gao committed
		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),
xg.gao's avatar
xg.gao committed
		WithMethods(c.Methods),
vito.he's avatar
vito.he committed
func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []func(method string) {
	methodConfigMergeFcn := make([]func(method string), 0, len(paramKeys))
vito.he's avatar
vito.he committed
	for _, paramKey := range paramKeys {
vito.he's avatar
vito.he committed
		if v := referenceUrl.GetParam(paramKey, ""); len(v) > 0 {
			mergedUrl.SetParam(paramKey, v)
vito.he's avatar
vito.he committed
		}
		methodConfigMergeFcn = append(methodConfigMergeFcn, func(method string) {
vito.he's avatar
vito.he committed
			if v := referenceUrl.GetParam(method+"."+paramKey, ""); len(v) > 0 {
				mergedUrl.SetParam(method+"."+paramKey, v)
vito.he's avatar
vito.he committed
			}
		})
	}
vito.he's avatar
vito.he committed
	return methodConfigMergeFcn
vito.he's avatar
vito.he committed
}