1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import json 7import os 8 9 10class ConfigJsonIteratorError(Exception): 11 """"Exception for config json iterator""" 12 pass 13 14 15class ConfigJsonIterator(object): 16 """Class to consolidate multiple config json files. 17 18 This class reads and combines input JSON instances into one based on the 19 following rules: 20 1. "_deps" value in the root config file contains a list of common config 21 file paths. Each path represents a RELATIVE path to 22 the root config file. 23 For example (common.config is in the same directory as root.config): 24 root.config: 25 { "a": "123", 26 "_deps": ["../common.config"]} 27 common.config: 28 { "b": "xxx" } 29 End output: 30 { "a": "123", 31 "b": "xxx" } 32 2. common config files defined in "_deps" MUST NOT contain identical keys 33 (otherwise an exception will be thrown), for example (invalid - common1 34 and common2.config are in the same directory as root.config): 35 root.config: 36 { "a": "123", 37 "_deps": ["../common1.config", 38 "../common2.config"]} 39 common1.config: 40 { "b": "xxx" } 41 common2.config: 42 { "b": "yyy" } 43 3. values in the root config will override the ones in the common config 44 files. This logic applies to any dependency config file (imagine that 45 the common config also has "_deps"), thus is recursive. 46 For example (common.config is in the same directory as root.config): 47 root.config: 48 { "a": "123", 49 "_deps": ["../common.config"]} 50 common.config: 51 { "a": "456", 52 "b": "xxx" } 53 End output: 54 { "a": "123", 55 "b": "xxx" } 56 """ 57 DEPS = '_deps' 58 59 60 def __init__(self, config_path=None): 61 """Constructor. 62 63 @param config_path: String of root config file path. 64 """ 65 if config_path: 66 self.set_config_dir(config_path) 67 68 69 def set_config_dir(self, config_path): 70 """Sets config dictionary. 71 72 @param config_path: String of config file path. 73 @raises ConfigJsonIteratorError if config does not exist. 74 """ 75 if not os.path.isfile(config_path): 76 raise ConfigJsonIteratorError('config file does not exist %s' 77 % config_path) 78 self._config_dir = os.path.abspath(os.path.dirname(config_path)) 79 80 81 def _load_config(self, config_path): 82 """Iterate the base config file. 83 84 @param config_path: String of config file path. 85 @return Dictionary of the config file. 86 @raises ConfigJsonIteratorError: if config file is not found or invalid. 87 """ 88 if not os.path.isfile(config_path): 89 raise ConfigJsonIteratorError('config file does not exist %s' 90 % config_path) 91 with open(config_path, 'r') as config_file: 92 try: 93 return json.load(config_file) 94 except ValueError: 95 raise ConfigJsonIteratorError( 96 'invalid JSON file %s' % config_file) 97 98 99 def aggregated_config(self, config_path): 100 """Returns dictionary of aggregated config files. 101 The dependency list contains the RELATIVE path to the root config. 102 103 @param config_path: String of config file path. 104 @return Dictionary containing the aggregated config files. 105 @raises ConfigJsonIteratorError: if dependency config list 106 does not exist. 107 """ 108 ret_dict = self._load_config(config_path) 109 if ConfigJsonIterator.DEPS not in ret_dict: 110 return ret_dict 111 else: 112 deps_list = ret_dict[ConfigJsonIterator.DEPS] 113 if not isinstance(deps_list, list): 114 raise ConfigJsonIteratorError('dependency must be a list %s' 115 % deps_list) 116 del ret_dict[ConfigJsonIterator.DEPS] 117 common_dict = {} 118 for dep in deps_list: 119 common_config_path = os.path.join(self._config_dir, dep) 120 dep_dict = self.aggregated_config(common_config_path) 121 common_dict = self._merge_dict(common_dict, dep_dict, 122 allow_override=False) 123 return self._merge_dict(common_dict, ret_dict, allow_override=True) 124 125 126 def _merge_dict(self, dict_one, dict_two, allow_override=True): 127 """Returns a merged dictionary. 128 129 @param dict_one: Dictionary to merge (first). 130 @param dict_two: Dictionary to merge (second). 131 @param allow_override: Boolean to allow override or not. 132 @return Dictionary containing merged result. 133 @raises ConfigJsonIteratorError: if no dictionary given. 134 """ 135 if not isinstance(dict_one, dict) or not isinstance(dict_two, dict): 136 raise ConfigJsonIteratorError('Input is not a dictionary') 137 if allow_override: 138 return dict(dict_one.items() + dict_two.items()) 139 else: 140 merge = collections.Counter( 141 dict_one.keys() + dict_two.keys()).most_common()[0] 142 if merge[1] > 1: 143 raise ConfigJsonIteratorError( 144 'Duplicate key %s found', merge[0]) 145 return dict_one 146