github.com/gogf/gf/v2@v2.7.4/os/gcfg/gcfg_adapter_file.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gcfg
     8  
     9  import (
    10  	"context"
    11  
    12  	"github.com/gogf/gf/v2/container/garray"
    13  	"github.com/gogf/gf/v2/container/gmap"
    14  	"github.com/gogf/gf/v2/container/gvar"
    15  	"github.com/gogf/gf/v2/encoding/gjson"
    16  	"github.com/gogf/gf/v2/errors/gerror"
    17  	"github.com/gogf/gf/v2/internal/command"
    18  	"github.com/gogf/gf/v2/internal/intlog"
    19  	"github.com/gogf/gf/v2/os/gfile"
    20  	"github.com/gogf/gf/v2/os/gfsnotify"
    21  	"github.com/gogf/gf/v2/os/gres"
    22  	"github.com/gogf/gf/v2/util/gmode"
    23  	"github.com/gogf/gf/v2/util/gutil"
    24  )
    25  
    26  // AdapterFile implements interface Adapter using file.
    27  type AdapterFile struct {
    28  	defaultFileNameOrPath string           // Default configuration file name or file path.
    29  	searchPaths           *garray.StrArray // Searching path array.
    30  	jsonMap               *gmap.StrAnyMap  // The pared JSON objects for configuration files.
    31  	violenceCheck         bool             // Whether it does violence check in value index searching. It affects the performance when set true(false in default).
    32  }
    33  
    34  const (
    35  	commandEnvKeyForFile = "gf.gcfg.file" // commandEnvKeyForFile is the configuration key for command argument or environment configuring file name.
    36  	commandEnvKeyForPath = "gf.gcfg.path" // commandEnvKeyForPath is the configuration key for command argument or environment configuring directory path.
    37  )
    38  
    39  var (
    40  	supportedFileTypes     = []string{"toml", "yaml", "yml", "json", "ini", "xml", "properties"} // All supported file types suffixes.
    41  	localInstances         = gmap.NewStrAnyMap(true)                                             // Instances map containing configuration instances.
    42  	customConfigContentMap = gmap.NewStrStrMap(true)                                             // Customized configuration content.
    43  
    44  	// Prefix array for trying searching in resource manager.
    45  	resourceTryFolders = []string{
    46  		"", "/", "config/", "config", "/config", "/config/",
    47  		"manifest/config/", "manifest/config", "/manifest/config", "/manifest/config/",
    48  	}
    49  
    50  	// Prefix array for trying searching in local system.
    51  	localSystemTryFolders = []string{"", "config/", "manifest/config"}
    52  )
    53  
    54  // NewAdapterFile returns a new configuration management object.
    55  // The parameter `file` specifies the default configuration file name for reading.
    56  func NewAdapterFile(fileNameOrPath ...string) (*AdapterFile, error) {
    57  	var (
    58  		err                error
    59  		usedFileNameOrPath = DefaultConfigFileName
    60  	)
    61  	if len(fileNameOrPath) > 0 {
    62  		usedFileNameOrPath = fileNameOrPath[0]
    63  	} else {
    64  		// Custom default configuration file name from command line or environment.
    65  		if customFile := command.GetOptWithEnv(commandEnvKeyForFile); customFile != "" {
    66  			usedFileNameOrPath = customFile
    67  		}
    68  	}
    69  	config := &AdapterFile{
    70  		defaultFileNameOrPath: usedFileNameOrPath,
    71  		searchPaths:           garray.NewStrArray(true),
    72  		jsonMap:               gmap.NewStrAnyMap(true),
    73  	}
    74  	// Customized dir path from env/cmd.
    75  	if customPath := command.GetOptWithEnv(commandEnvKeyForPath); customPath != "" {
    76  		if gfile.Exists(customPath) {
    77  			if err = config.SetPath(customPath); err != nil {
    78  				return nil, err
    79  			}
    80  		} else {
    81  			return nil, gerror.Newf(`configuration directory path "%s" does not exist`, customPath)
    82  		}
    83  	} else {
    84  		// ================================================================================
    85  		// Automatic searching directories.
    86  		// It does not affect adapter object cresting if these directories do not exist.
    87  		// ================================================================================
    88  
    89  		// Dir path of working dir.
    90  		if err = config.AddPath(gfile.Pwd()); err != nil {
    91  			intlog.Errorf(context.TODO(), `%+v`, err)
    92  		}
    93  
    94  		// Dir path of main package.
    95  		if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
    96  			if err = config.AddPath(mainPath); err != nil {
    97  				intlog.Errorf(context.TODO(), `%+v`, err)
    98  			}
    99  		}
   100  
   101  		// Dir path of binary.
   102  		if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
   103  			if err = config.AddPath(selfPath); err != nil {
   104  				intlog.Errorf(context.TODO(), `%+v`, err)
   105  			}
   106  		}
   107  	}
   108  	return config, nil
   109  }
   110  
   111  // SetViolenceCheck sets whether to perform hierarchical conflict checking.
   112  // This feature needs to be enabled when there is a level symbol in the key name.
   113  // It is off in default.
   114  //
   115  // Note that, turning on this feature is quite expensive, and it is not recommended
   116  // allowing separators in the key names. It is best to avoid this on the application side.
   117  func (a *AdapterFile) SetViolenceCheck(check bool) {
   118  	a.violenceCheck = check
   119  	a.Clear()
   120  }
   121  
   122  // SetFileName sets the default configuration file name.
   123  func (a *AdapterFile) SetFileName(fileNameOrPath string) {
   124  	a.defaultFileNameOrPath = fileNameOrPath
   125  }
   126  
   127  // GetFileName returns the default configuration file name.
   128  func (a *AdapterFile) GetFileName() string {
   129  	return a.defaultFileNameOrPath
   130  }
   131  
   132  // Get retrieves and returns value by specified `pattern`.
   133  // It returns all values of current Json object if `pattern` is given empty or string ".".
   134  // It returns nil if no value found by `pattern`.
   135  //
   136  // We can also access slice item by its index number in `pattern` like:
   137  // "list.10", "array.0.name", "array.0.1.id".
   138  //
   139  // It returns a default value specified by `def` if value for `pattern` is not found.
   140  func (a *AdapterFile) Get(ctx context.Context, pattern string) (value interface{}, err error) {
   141  	j, err := a.getJson()
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	if j != nil {
   146  		return j.Get(pattern).Val(), nil
   147  	}
   148  	return nil, nil
   149  }
   150  
   151  // Set sets value with specified `pattern`.
   152  // It supports hierarchical data access by char separator, which is '.' in default.
   153  // It is commonly used for updates certain configuration value in runtime.
   154  // Note that, it is not recommended using `Set` configuration at runtime as the configuration would be
   155  // automatically refreshed if underlying configuration file changed.
   156  func (a *AdapterFile) Set(pattern string, value interface{}) error {
   157  	j, err := a.getJson()
   158  	if err != nil {
   159  		return err
   160  	}
   161  	if j != nil {
   162  		return j.Set(pattern, value)
   163  	}
   164  	return nil
   165  }
   166  
   167  // Data retrieves and returns all configuration data as map type.
   168  func (a *AdapterFile) Data(ctx context.Context) (data map[string]interface{}, err error) {
   169  	j, err := a.getJson()
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	if j != nil {
   174  		return j.Var().Map(), nil
   175  	}
   176  	return nil, nil
   177  }
   178  
   179  // MustGet acts as function Get, but it panics if error occurs.
   180  func (a *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var {
   181  	v, err := a.Get(ctx, pattern)
   182  	if err != nil {
   183  		panic(err)
   184  	}
   185  	return gvar.New(v)
   186  }
   187  
   188  // Clear removes all parsed configuration files content cache,
   189  // which will force reload configuration content from file.
   190  func (a *AdapterFile) Clear() {
   191  	a.jsonMap.Clear()
   192  }
   193  
   194  // Dump prints current Json object with more manually readable.
   195  func (a *AdapterFile) Dump() {
   196  	if j, _ := a.getJson(); j != nil {
   197  		j.Dump()
   198  	}
   199  }
   200  
   201  // Available checks and returns whether configuration of given `file` is available.
   202  func (a *AdapterFile) Available(ctx context.Context, fileName ...string) bool {
   203  	checkFileName := gutil.GetOrDefaultStr(a.defaultFileNameOrPath, fileName...)
   204  	// Custom configuration content exists.
   205  	if a.GetContent(checkFileName) != "" {
   206  		return true
   207  	}
   208  	// Configuration file exists in system path.
   209  	if path, _ := a.GetFilePath(checkFileName); path != "" {
   210  		return true
   211  	}
   212  	return false
   213  }
   214  
   215  // autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main
   216  // to the searching path list if it's currently in development environment.
   217  func (a *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() {
   218  	if gmode.IsDevelop() {
   219  		mainPkgPath := gfile.MainPkgPath()
   220  		if mainPkgPath != "" {
   221  			if !a.searchPaths.Contains(mainPkgPath) {
   222  				a.searchPaths.Append(mainPkgPath)
   223  			}
   224  		}
   225  	}
   226  }
   227  
   228  // getJson returns a *gjson.Json object for the specified `file` content.
   229  // It would print error if file reading fails. It returns nil if any error occurs.
   230  func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json, err error) {
   231  	var (
   232  		usedFileNameOrPath = a.defaultFileNameOrPath
   233  	)
   234  	if len(fileNameOrPath) > 0 && fileNameOrPath[0] != "" {
   235  		usedFileNameOrPath = fileNameOrPath[0]
   236  	} else {
   237  		usedFileNameOrPath = a.defaultFileNameOrPath
   238  	}
   239  	// It uses json map to cache specified configuration file content.
   240  	result := a.jsonMap.GetOrSetFuncLock(usedFileNameOrPath, func() interface{} {
   241  		var (
   242  			content  string
   243  			filePath string
   244  		)
   245  		// The configured content can be any kind of data type different from its file type.
   246  		isFromConfigContent := true
   247  		if content = a.GetContent(usedFileNameOrPath); content == "" {
   248  			isFromConfigContent = false
   249  			filePath, err = a.GetFilePath(usedFileNameOrPath)
   250  			if err != nil {
   251  				return nil
   252  			}
   253  			if filePath == "" {
   254  				return nil
   255  			}
   256  			if file := gres.Get(filePath); file != nil {
   257  				content = string(file.Content())
   258  			} else {
   259  				content = gfile.GetContents(filePath)
   260  			}
   261  		}
   262  		// Note that the underlying configuration json object operations are concurrent safe.
   263  		dataType := gjson.ContentType(gfile.ExtName(filePath))
   264  		if gjson.IsValidDataType(dataType) && !isFromConfigContent {
   265  			configJson, err = gjson.LoadContentType(dataType, content, true)
   266  		} else {
   267  			configJson, err = gjson.LoadContent(content, true)
   268  		}
   269  		if err != nil {
   270  			if filePath != "" {
   271  				err = gerror.Wrapf(err, `load config file "%s" failed`, filePath)
   272  			} else {
   273  				err = gerror.Wrap(err, `load configuration failed`)
   274  			}
   275  			return nil
   276  		}
   277  		configJson.SetViolenceCheck(a.violenceCheck)
   278  		// Add monitor for this configuration file,
   279  		// any changes of this file will refresh its cache in Config object.
   280  		if filePath != "" && !gres.Contains(filePath) {
   281  			_, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
   282  				a.jsonMap.Remove(usedFileNameOrPath)
   283  			})
   284  			if err != nil {
   285  				return nil
   286  			}
   287  		}
   288  		return configJson
   289  	})
   290  	if result != nil {
   291  		return result.(*gjson.Json), err
   292  	}
   293  	return
   294  }