/* * Copyright (C) 2012 Facebook, Inc. * * Licensed 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. */ package com.facebook.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Queue; import java.util.Set; /** * Reads and expands a JSON config object through a series of includes. * Expects JSON config objects to consist of two top level keys: 'conf' and 'includes': * * conf - key-value pairs to be consolidated in the returned JSONObject [req] * includes - list of JSON configs (of the same format) from which to pull and * include key-value pairs [opt] * * JSON config file format: * { * conf : { * key1 : value1, * key2 : value2 * ... * }, * includes : [ * object1, * object2 * ] * } * * Keys in the existing or closer config objects will take precedence over the same * key defined in more distantly included objects. */ public abstract class AbstractExpandedConfJSONProvider implements JSONProvider { private static final Logger LOG = LoggerFactory.getLogger(AbstractExpandedConfJSONProvider.class); private static final String CONF_KEY = "conf"; private static final String INCLUDES_KEY = "includes"; private final String root; public AbstractExpandedConfJSONProvider(String root) { this.root = root; } private JSONObject getExpandedJSONConfig() throws JSONException { Set<String> traversedFiles = new HashSet<>(); Queue<String> toTraverse = new LinkedList<>(); // Seed the graph traversal with the root node traversedFiles.add(root); toTraverse.add(root); // Policy: parent configs will override children (included) configs JSONObject expanded = new JSONObject(); while (!toTraverse.isEmpty()) { String current = toTraverse.remove(); JSONObject json = load(current); JSONObject conf = json.getJSONObject(CONF_KEY); Iterator<String> iter = conf.keys(); while (iter.hasNext()) { String key = iter.next(); // Current config will get to insert keys before its include files if (!expanded.has(key)) { expanded.put(key, conf.get(key)); } } // Check if the file itself has any included files if (json.has(INCLUDES_KEY)) { JSONArray includes = json.getJSONArray(INCLUDES_KEY); for (int idx = 0; idx < includes.length(); idx++) { String include = resolve(current, includes.getString(idx)); if (traversedFiles.contains(include)) { LOG.warn("Config file was included twice: " + include); } else { toTraverse.add(include); traversedFiles.add(include); } } } } return expanded; } @Override public JSONObject get() throws JSONException { return getExpandedJSONConfig(); } /** * Determines the canonical resource identifier for the given <tt>config</tt>. * This hook allows subclasses to resolve relative paths used in includes. * * @param parent the resource which listed this config in its includes * (or <tt>null</tt> if it is the root) * @param config a config resource identifier * @return the canonical resource identifier * @see com.facebook.config.ExpandedConfFileJSONProvider#resolve(String, String) */ abstract protected String resolve(String parent, String config); /** * Returns the configuration JSON identified by <tt>config</tt>. * <tt>config</tt> is guaranteed to be the result of some call to * {@link #resolve(String, String)}. * * @param config a canonical resource identifier * @return a JSON representation of the resource's contents * @see com.facebook.config.ExpandedConfFileJSONProvider#load(String) */ abstract protected JSONObject load(String config) throws JSONException; }