blob: e571200a1e240cfbcf2400e35331740ae191fc62 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package profile
import (
"bytes"
"encoding/json"
"fmt"
"os"
)
// ConfigPropertyHandler is an interfaces for handlers associated with top-level
// properties in the config file. Handlers are associated with top-level
// properties by calling function AddHandler on JSONConfig.
type ConfigPropertyHandler interface {
parseJSONData(jsonData string) error
}
// JSONConfig is a helper class for parsing JSON configuration files. By iteself,
// JSONConfig scan top-level properties JSON files and recursively processes
// config files listed in top-level property with name "include". It becomes more
// useful when handlers are associated with top-level properties other than
// "include".
type JSONConfig struct {
handlers map[string]ConfigPropertyHandler
jsonData map[string]interface{}
includeDepth int
}
// CreateJSONConfig creates and returns a JSONConfig instance.
func CreateJSONConfig() *JSONConfig {
var jc = JSONConfig{}
jc.handlers = make(map[string]ConfigPropertyHandler)
return &jc
}
// AddHandler adds a handler for property with name <fieldName> to the JSONConfig
// object. The handler will be invoked each time a top-level property with that
// name is found in the config file or in the included files.
func (jc *JSONConfig) AddHandler(fieldName string, handler ConfigPropertyHandler) error {
if fieldName == "include" {
return fmt.Errorf("invalid field name for custom handler: %s", fieldName)
}
jc.handlers[fieldName] = handler
return nil
}
// OpenJSONConfigFile open json file with path <jsonFile> and ensures it is ready
// for processing. If the file doesn't look like a file that can be processed
// with this JSONConfig instance, an error is returned.
func (jc *JSONConfig) OpenJSONConfigFile(jsonFile string) error {
file, err := os.Open(jsonFile)
if err != nil {
return fmt.Errorf("cannot open config file <%s>; error=%w", jsonFile, err)
}
defer file.Close()
var decode = json.NewDecoder(file)
if err := decode.Decode(&jc.jsonData); err != nil {
return err
}
if !jc.canProcessData(jc.jsonData) {
return fmt.Errorf("unable to process JSON data in %s", jsonFile)
}
return nil
}
// Process processes a json config file that was successfully opened with
// OpenJSONConfigFile.
func (jc *JSONConfig) Process() error {
return jc.processJSONData(jc.jsonData)
}
// Returns whether a json data map (a map of property names to values) is one
// that can be processed by JSNConfig. That is so when the json data has either
// an "include" property or at least one property that can be processed by one
// of the available handlers.
func (jc *JSONConfig) canProcessData(jsonData map[string]interface{}) bool {
if jsonData != nil {
if _, ok := jsonData["include"]; ok {
return true
}
for propertyName := range jsonData {
if _, ok := jc.handlers[propertyName]; ok {
return ok
}
}
}
return false
}
// Process config json data that is represented as a map of property names to values.
func (jc *JSONConfig) processJSONData(data map[string]interface{}) error {
// Process the include files first.
if includes, ok := data["include"]; ok {
if err := jc.includeFiles(includes); err != nil {
return err
}
}
// Process property values that have an associated handler.
for propertyName := range data {
if handler, ok := jc.handlers[propertyName]; ok {
if err := jc.invokeHandler(handler, data[propertyName]); err != nil {
return err
}
}
}
return nil
}
// Process included files up to the maximum include depth.
func (jc *JSONConfig) includeFiles(files interface{}) error {
// The include property value may be either a single file (a string) or
// a list of files (array of strings). Map either into a file list.
fileList := make([]string, 0, 16)
if f, ok := files.(string); ok {
fileList = append(fileList, f)
} else if list, ok := files.([]interface{}); ok {
for _, incFile := range list {
if f, ok := incFile.(string); ok {
fileList = append(fileList, f)
}
}
} else {
return fmt.Errorf("invalid value type for include: %v", files)
}
// Process the included files in the order found. We limit how deep we can
// recursively include files as a way of avoiding infinite include loops.
for _, oneFile := range fileList {
file, err := os.Open(oneFile)
if err != nil {
return fmt.Errorf("cannot include file <%s>; error=%w", oneFile, err)
}
defer file.Close()
jc.includeDepth++
if jc.includeDepth == 10 {
return fmt.Errorf("too many includes: %d", jc.includeDepth)
}
if err := jc.parseFile(file); err != nil {
return err
}
jc.includeDepth--
}
return nil
}
// Process a json file.
func (jc *JSONConfig) parseFile(file *os.File) error {
var decode = json.NewDecoder(file)
var data map[string]interface{}
if err := decode.Decode(&data); err != nil {
return err
}
return jc.processJSONData(data)
}
// Look for a handler associated with a top-level property and invoke it if found.
func (jc *JSONConfig) invokeHandler(handler ConfigPropertyHandler, data interface{}) error {
// The handler expect the json data as a string. So we must map the interface{} value
// back to a string. We can do that with the json encoder.
var buffer = new(bytes.Buffer)
var encode = json.NewEncoder(buffer)
if err := encode.Encode(data); err != nil {
return err
}
return handler.parseJSONData(buffer.String())
}