docs: json_v2 improved var naming and comments (#9907)
This commit is contained in:
parent
66da86017f
commit
0be92db8af
|
|
@ -12,19 +12,27 @@ import (
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Parser adheres to the parser interface, contains the parser configuration, and data required to parse JSON
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
InputJSON []byte
|
// These struct fields are common for a parser
|
||||||
Configs []Config
|
Configs []Config
|
||||||
DefaultTags map[string]string
|
DefaultTags map[string]string
|
||||||
Log telegraf.Logger
|
Log telegraf.Logger
|
||||||
Timestamp time.Time
|
|
||||||
|
|
||||||
|
// **** The struct fields bellow this comment are used for processing indvidual configs ****
|
||||||
|
|
||||||
|
// measurementName is the the name of the current config used in each line protocol
|
||||||
measurementName string
|
measurementName string
|
||||||
|
// timestamp is the timestamp used in each line protocol, defaults to time.Now()
|
||||||
|
timestamp time.Time
|
||||||
|
|
||||||
|
// **** Specific for object configuration ****
|
||||||
|
// subPathResults contains the results of sub-gjson path expressions provided in fields/tags table within object config
|
||||||
|
subPathResults []PathResult
|
||||||
|
// iterateObjects dictates if ExpandArray function will handle objects
|
||||||
iterateObjects bool
|
iterateObjects bool
|
||||||
|
// objectConfig contains the config for an object, some info is needed while iterating over the gjson results
|
||||||
currentSettings JSONObject
|
objectConfig JSONObject
|
||||||
pathResults []PathResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PathResult struct {
|
type PathResult struct {
|
||||||
|
|
@ -83,28 +91,27 @@ type MetricNode struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
|
func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
|
||||||
p.InputJSON = input
|
|
||||||
// Only valid JSON is supported
|
// Only valid JSON is supported
|
||||||
if !gjson.Valid(string(p.InputJSON)) {
|
if !gjson.Valid(string(input)) {
|
||||||
return nil, fmt.Errorf("Invalid JSON provided, unable to parse")
|
return nil, fmt.Errorf("invalid JSON provided, unable to parse")
|
||||||
}
|
}
|
||||||
|
|
||||||
var metrics []telegraf.Metric
|
var metrics []telegraf.Metric
|
||||||
|
|
||||||
for _, c := range p.Configs {
|
for _, c := range p.Configs {
|
||||||
// Measurement name configuration
|
// Measurement name can either be hardcoded, or parsed from the JSON using a GJSON path expression
|
||||||
p.measurementName = c.MeasurementName
|
p.measurementName = c.MeasurementName
|
||||||
if c.MeasurementNamePath != "" {
|
if c.MeasurementNamePath != "" {
|
||||||
result := gjson.GetBytes(p.InputJSON, c.MeasurementNamePath)
|
result := gjson.GetBytes(input, c.MeasurementNamePath)
|
||||||
if !result.IsArray() && !result.IsObject() {
|
if !result.IsArray() && !result.IsObject() {
|
||||||
p.measurementName = result.String()
|
p.measurementName = result.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp configuration
|
// timestamp defaults to current time, or can be parsed from the JSON using a GJSON path expression
|
||||||
p.Timestamp = time.Now()
|
p.timestamp = time.Now()
|
||||||
if c.TimestampPath != "" {
|
if c.TimestampPath != "" {
|
||||||
result := gjson.GetBytes(p.InputJSON, c.TimestampPath)
|
result := gjson.GetBytes(input, c.TimestampPath)
|
||||||
if !result.IsArray() && !result.IsObject() {
|
if !result.IsArray() && !result.IsObject() {
|
||||||
if c.TimestampFormat == "" {
|
if c.TimestampFormat == "" {
|
||||||
err := fmt.Errorf("use of 'timestamp_query' requires 'timestamp_format'")
|
err := fmt.Errorf("use of 'timestamp_query' requires 'timestamp_format'")
|
||||||
|
|
@ -112,24 +119,24 @@ func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
p.Timestamp, err = internal.ParseTimestamp(c.TimestampFormat, result.Value(), c.TimestampTimezone)
|
p.timestamp, err = internal.ParseTimestamp(c.TimestampFormat, result.Value(), c.TimestampTimezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields, err := p.processMetric(c.Fields, false)
|
fields, err := p.processMetric(input, c.Fields, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tags, err := p.processMetric(c.Tags, true)
|
tags, err := p.processMetric(input, c.Tags, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
objects, err := p.processObjects(c.JSONObjects)
|
objects, err := p.processObjects(input, c.JSONObjects)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +162,7 @@ func (p *Parser) Parse(input []byte) ([]telegraf.Metric, error) {
|
||||||
// processMetric will iterate over all 'field' or 'tag' configs and create metrics for each
|
// processMetric will iterate over all 'field' or 'tag' configs and create metrics for each
|
||||||
// A field/tag can either be a single value or an array of values, each resulting in its own metric
|
// A field/tag can either be a single value or an array of values, each resulting in its own metric
|
||||||
// For multiple configs, a set of metrics is created from the cartesian product of each separate config
|
// For multiple configs, a set of metrics is created from the cartesian product of each separate config
|
||||||
func (p *Parser) processMetric(data []DataSet, tag bool) ([]telegraf.Metric, error) {
|
func (p *Parser) processMetric(input []byte, data []DataSet, tag bool) ([]telegraf.Metric, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +174,7 @@ func (p *Parser) processMetric(data []DataSet, tag bool) ([]telegraf.Metric, err
|
||||||
if c.Path == "" {
|
if c.Path == "" {
|
||||||
return nil, fmt.Errorf("GJSON path is required")
|
return nil, fmt.Errorf("GJSON path is required")
|
||||||
}
|
}
|
||||||
result := gjson.GetBytes(p.InputJSON, c.Path)
|
result := gjson.GetBytes(input, c.Path)
|
||||||
|
|
||||||
if result.IsObject() {
|
if result.IsObject() {
|
||||||
p.Log.Debugf("Found object in the path: %s, ignoring it please use 'object' to gather metrics from objects", c.Path)
|
p.Log.Debugf("Found object in the path: %s, ignoring it please use 'object' to gather metrics from objects", c.Path)
|
||||||
|
|
@ -191,7 +198,7 @@ func (p *Parser) processMetric(data []DataSet, tag bool) ([]telegraf.Metric, err
|
||||||
p.measurementName,
|
p.measurementName,
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]interface{}{},
|
map[string]interface{}{},
|
||||||
p.Timestamp,
|
p.timestamp,
|
||||||
),
|
),
|
||||||
Result: result,
|
Result: result,
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +258,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
p.Log.Debugf("Found object in query ignoring it please use 'object' to gather metrics from objects")
|
p.Log.Debugf("Found object in query ignoring it please use 'object' to gather metrics from objects")
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
if result.IncludeCollection == nil && (len(p.currentSettings.FieldPaths) > 0 || len(p.currentSettings.TagPaths) > 0) {
|
if result.IncludeCollection == nil && (len(p.objectConfig.FieldPaths) > 0 || len(p.objectConfig.TagPaths) > 0) {
|
||||||
result.IncludeCollection = p.existsInpathResults(result.Index, result.Raw)
|
result.IncludeCollection = p.existsInpathResults(result.Index, result.Raw)
|
||||||
}
|
}
|
||||||
r, err := p.combineObject(result)
|
r, err := p.combineObject(result)
|
||||||
|
|
@ -264,7 +271,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
|
|
||||||
if result.IsArray() {
|
if result.IsArray() {
|
||||||
var err error
|
var err error
|
||||||
if result.IncludeCollection == nil && (len(p.currentSettings.FieldPaths) > 0 || len(p.currentSettings.TagPaths) > 0) {
|
if result.IncludeCollection == nil && (len(p.objectConfig.FieldPaths) > 0 || len(p.objectConfig.TagPaths) > 0) {
|
||||||
result.IncludeCollection = p.existsInpathResults(result.Index, result.Raw)
|
result.IncludeCollection = p.existsInpathResults(result.Index, result.Raw)
|
||||||
}
|
}
|
||||||
result.ForEach(func(_, val gjson.Result) bool {
|
result.ForEach(func(_, val gjson.Result) bool {
|
||||||
|
|
@ -272,7 +279,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
p.measurementName,
|
p.measurementName,
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]interface{}{},
|
map[string]interface{}{},
|
||||||
p.Timestamp,
|
p.timestamp,
|
||||||
)
|
)
|
||||||
if val.IsObject() {
|
if val.IsObject() {
|
||||||
if p.iterateObjects {
|
if p.iterateObjects {
|
||||||
|
|
@ -280,7 +287,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
n.ParentIndex += val.Index
|
n.ParentIndex += val.Index
|
||||||
n.Metric = m
|
n.Metric = m
|
||||||
n.Result = val
|
n.Result = val
|
||||||
if n.IncludeCollection == nil && (len(p.currentSettings.FieldPaths) > 0 || len(p.currentSettings.TagPaths) > 0) {
|
if n.IncludeCollection == nil && (len(p.objectConfig.FieldPaths) > 0 || len(p.objectConfig.TagPaths) > 0) {
|
||||||
n.IncludeCollection = p.existsInpathResults(n.Index, n.Raw)
|
n.IncludeCollection = p.existsInpathResults(n.Index, n.Raw)
|
||||||
}
|
}
|
||||||
r, err := p.combineObject(n)
|
r, err := p.combineObject(n)
|
||||||
|
|
@ -310,7 +317,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
n.ParentIndex += val.Index
|
n.ParentIndex += val.Index
|
||||||
n.Metric = m
|
n.Metric = m
|
||||||
n.Result = val
|
n.Result = val
|
||||||
if n.IncludeCollection == nil && (len(p.currentSettings.FieldPaths) > 0 || len(p.currentSettings.TagPaths) > 0) {
|
if n.IncludeCollection == nil && (len(p.objectConfig.FieldPaths) > 0 || len(p.objectConfig.TagPaths) > 0) {
|
||||||
n.IncludeCollection = p.existsInpathResults(n.Index, n.Raw)
|
n.IncludeCollection = p.existsInpathResults(n.Index, n.Raw)
|
||||||
}
|
}
|
||||||
r, err := p.expandArray(n)
|
r, err := p.expandArray(n)
|
||||||
|
|
@ -324,12 +331,12 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if result.SetName == p.currentSettings.TimestampKey {
|
if result.SetName == p.objectConfig.TimestampKey {
|
||||||
if p.currentSettings.TimestampFormat == "" {
|
if p.objectConfig.TimestampFormat == "" {
|
||||||
err := fmt.Errorf("use of 'timestamp_query' requires 'timestamp_format'")
|
err := fmt.Errorf("use of 'timestamp_query' requires 'timestamp_format'")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
timestamp, err := internal.ParseTimestamp(p.currentSettings.TimestampFormat, result.Value(), p.currentSettings.TimestampTimezone)
|
timestamp, err := internal.ParseTimestamp(p.objectConfig.TimestampFormat, result.Value(), p.objectConfig.TimestampTimezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +348,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
outputName := result.OutputName
|
outputName := result.OutputName
|
||||||
desiredType := result.DesiredType
|
desiredType := result.DesiredType
|
||||||
|
|
||||||
if len(p.currentSettings.FieldPaths) > 0 || len(p.currentSettings.TagPaths) > 0 {
|
if len(p.objectConfig.FieldPaths) > 0 || len(p.objectConfig.TagPaths) > 0 {
|
||||||
var pathResult *PathResult
|
var pathResult *PathResult
|
||||||
// When IncludeCollection isn't nil, that means the current result is included in the collection.
|
// When IncludeCollection isn't nil, that means the current result is included in the collection.
|
||||||
if result.IncludeCollection != nil {
|
if result.IncludeCollection != nil {
|
||||||
|
|
@ -386,7 +393,7 @@ func (p *Parser) expandArray(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) existsInpathResults(index int, raw string) *PathResult {
|
func (p *Parser) existsInpathResults(index int, raw string) *PathResult {
|
||||||
for _, f := range p.pathResults {
|
for _, f := range p.subPathResults {
|
||||||
if f.result.Index == 0 {
|
if f.result.Index == 0 {
|
||||||
for _, i := range f.result.Indexes {
|
for _, i := range f.result.Indexes {
|
||||||
if i == index {
|
if i == index {
|
||||||
|
|
@ -401,23 +408,23 @@ func (p *Parser) existsInpathResults(index int, raw string) *PathResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
// processObjects will iterate over all 'object' configs and create metrics for each
|
// processObjects will iterate over all 'object' configs and create metrics for each
|
||||||
func (p *Parser) processObjects(objects []JSONObject) ([]telegraf.Metric, error) {
|
func (p *Parser) processObjects(input []byte, objects []JSONObject) ([]telegraf.Metric, error) {
|
||||||
p.iterateObjects = true
|
p.iterateObjects = true
|
||||||
var t []telegraf.Metric
|
var t []telegraf.Metric
|
||||||
for _, c := range objects {
|
for _, c := range objects {
|
||||||
p.currentSettings = c
|
p.objectConfig = c
|
||||||
|
|
||||||
if c.Path == "" {
|
if c.Path == "" {
|
||||||
return nil, fmt.Errorf("GJSON path is required")
|
return nil, fmt.Errorf("GJSON path is required")
|
||||||
}
|
}
|
||||||
result := gjson.GetBytes(p.InputJSON, c.Path)
|
result := gjson.GetBytes(input, c.Path)
|
||||||
|
|
||||||
scopedJSON := []byte(result.Raw)
|
scopedJSON := []byte(result.Raw)
|
||||||
for _, f := range c.FieldPaths {
|
for _, f := range c.FieldPaths {
|
||||||
var r PathResult
|
var r PathResult
|
||||||
r.result = gjson.GetBytes(scopedJSON, f.Path)
|
r.result = gjson.GetBytes(scopedJSON, f.Path)
|
||||||
r.DataSet = f
|
r.DataSet = f
|
||||||
p.pathResults = append(p.pathResults, r)
|
p.subPathResults = append(p.subPathResults, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range c.TagPaths {
|
for _, f := range c.TagPaths {
|
||||||
|
|
@ -425,7 +432,7 @@ func (p *Parser) processObjects(objects []JSONObject) ([]telegraf.Metric, error)
|
||||||
r.result = gjson.GetBytes(scopedJSON, f.Path)
|
r.result = gjson.GetBytes(scopedJSON, f.Path)
|
||||||
r.DataSet = f
|
r.DataSet = f
|
||||||
r.tag = true
|
r.tag = true
|
||||||
p.pathResults = append(p.pathResults, r)
|
p.subPathResults = append(p.subPathResults, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Type == gjson.Null {
|
if result.Type == gjson.Null {
|
||||||
|
|
@ -438,7 +445,7 @@ func (p *Parser) processObjects(objects []JSONObject) ([]telegraf.Metric, error)
|
||||||
p.measurementName,
|
p.measurementName,
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
map[string]interface{}{},
|
map[string]interface{}{},
|
||||||
p.Timestamp,
|
p.timestamp,
|
||||||
),
|
),
|
||||||
Result: result,
|
Result: result,
|
||||||
}
|
}
|
||||||
|
|
@ -472,12 +479,12 @@ func (p *Parser) combineObject(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputName string
|
var outputName string
|
||||||
if p.currentSettings.DisablePrependKeys {
|
if p.objectConfig.DisablePrependKeys {
|
||||||
outputName = strings.ReplaceAll(key.String(), " ", "_")
|
outputName = strings.ReplaceAll(key.String(), " ", "_")
|
||||||
} else {
|
} else {
|
||||||
outputName = setName
|
outputName = setName
|
||||||
}
|
}
|
||||||
for k, n := range p.currentSettings.Renames {
|
for k, n := range p.objectConfig.Renames {
|
||||||
if k == setName {
|
if k == setName {
|
||||||
outputName = n
|
outputName = n
|
||||||
break
|
break
|
||||||
|
|
@ -490,7 +497,7 @@ func (p *Parser) combineObject(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
arrayNode.SetName = setName
|
arrayNode.SetName = setName
|
||||||
arrayNode.Result = val
|
arrayNode.Result = val
|
||||||
|
|
||||||
for k, t := range p.currentSettings.Fields {
|
for k, t := range p.objectConfig.Fields {
|
||||||
if setName == k {
|
if setName == k {
|
||||||
arrayNode.DesiredType = t
|
arrayNode.DesiredType = t
|
||||||
break
|
break
|
||||||
|
|
@ -498,7 +505,7 @@ func (p *Parser) combineObject(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := false
|
tag := false
|
||||||
for _, t := range p.currentSettings.Tags {
|
for _, t := range p.objectConfig.Tags {
|
||||||
if setName == t {
|
if setName == t {
|
||||||
tag = true
|
tag = true
|
||||||
break
|
break
|
||||||
|
|
@ -531,11 +538,11 @@ func (p *Parser) combineObject(result MetricNode) ([]telegraf.Metric, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) isIncluded(key string, val gjson.Result) bool {
|
func (p *Parser) isIncluded(key string, val gjson.Result) bool {
|
||||||
if len(p.currentSettings.IncludedKeys) == 0 {
|
if len(p.objectConfig.IncludedKeys) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// automatically adds tags to included_keys so it does NOT have to be repeated in the config
|
// automatically adds tags to included_keys so it does NOT have to be repeated in the config
|
||||||
allKeys := append(p.currentSettings.IncludedKeys, p.currentSettings.Tags...)
|
allKeys := append(p.objectConfig.IncludedKeys, p.objectConfig.Tags...)
|
||||||
for _, i := range allKeys {
|
for _, i := range allKeys {
|
||||||
if i == key {
|
if i == key {
|
||||||
return true
|
return true
|
||||||
|
|
@ -551,7 +558,7 @@ func (p *Parser) isIncluded(key string, val gjson.Result) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parser) isExcluded(key string) bool {
|
func (p *Parser) isExcluded(key string) bool {
|
||||||
for _, i := range p.currentSettings.ExcludedKeys {
|
for _, i := range p.objectConfig.ExcludedKeys {
|
||||||
if i == key {
|
if i == key {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -576,25 +583,25 @@ func (p *Parser) convertType(input gjson.Result, desiredType string, name string
|
||||||
case "uint":
|
case "uint":
|
||||||
r, err := strconv.ParseUint(inputType, 10, 64)
|
r, err := strconv.ParseUint(inputType, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to convert field '%s' to type uint: %v", name, err)
|
return nil, fmt.Errorf("unable to convert field '%s' to type uint: %v", name, err)
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
case "int":
|
case "int":
|
||||||
r, err := strconv.ParseInt(inputType, 10, 64)
|
r, err := strconv.ParseInt(inputType, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to convert field '%s' to type int: %v", name, err)
|
return nil, fmt.Errorf("unable to convert field '%s' to type int: %v", name, err)
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
case "float":
|
case "float":
|
||||||
r, err := strconv.ParseFloat(inputType, 64)
|
r, err := strconv.ParseFloat(inputType, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to convert field '%s' to type float: %v", name, err)
|
return nil, fmt.Errorf("unable to convert field '%s' to type float: %v", name, err)
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
case "bool":
|
case "bool":
|
||||||
r, err := strconv.ParseBool(inputType)
|
r, err := strconv.ParseBool(inputType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to convert field '%s' to type bool: %v", name, err)
|
return nil, fmt.Errorf("unable to convert field '%s' to type bool: %v", name, err)
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
@ -631,7 +638,7 @@ func (p *Parser) convertType(input gjson.Result, desiredType string, name string
|
||||||
} else if inputType == 1 {
|
} else if inputType == 1 {
|
||||||
return true, nil
|
return true, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Unable to convert field '%s' to type bool", name)
|
return nil, fmt.Errorf("unable to convert field '%s' to type bool", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue