/* * 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 com.facebook.collections.ByteArray; import com.facebook.collectionsbase.Mapper; import com.facebook.logging.Logger; import com.facebook.logging.LoggerImpl; import com.google.common.collect.Maps; import org.joda.time.Duration; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * 1. wraps a JSONObject that contains configuration information. * 2. converts JSONExceptions to a runtime ConfigException * 3. provides clean conversions to primitive types and annotated bean classes */ public class ConfigAccessor { private static final Logger LOG = LoggerImpl.getClassLogger(); private final JSONObject jsonObject; public ConfigAccessor(JSONObject jsonObject) { this.jsonObject = jsonObject; } public static ConfigAccessor emptyConfig() { return new ConfigAccessor(new JSONObject()); } public <T> T getBean( String key, Class<? extends ExtractableBeanBuilder<T>> beanBuilderClass ) { try { JSONObject jsonBean = get(key, null, new JSONObjectExtractor()); ConfigAccessor jsonBeanAccessor = new ConfigAccessor(jsonBean); Object beanBuilder = beanBuilderClass.newInstance(); for (Method m : beanBuilderClass.getMethods()) { if (m.getName().toLowerCase().startsWith("set")) { FieldExtractor extractor = m.getAnnotation(FieldExtractor.class); if (extractor != null) { if (!Extractor.class.isAssignableFrom(extractor.extractorClass())) { String message = String.format( "extractor %s does not extend Extractor.class", extractor ); LOG.error(message); throw new ConfigException(message); } // if the parameter isn't optional check, or if it's optional // and it is present; ie, if it's not optional and not there, let // get() throw a ConfigException if (!extractor.optional() || jsonBeanAccessor.hasKey(extractor.key())) { Object value = jsonBeanAccessor.get( extractor.key(), null, (Extractor) extractor.extractorClass().newInstance() ); m.invoke(beanBuilder, value); } } else { LOG.warn("unable to find annotation for setter method " + m.getName()); } } } return ((ExtractableBeanBuilder<T>) beanBuilder).build(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new ConfigException(e); } } public Class<?> getClass(String key, Class<?> defaultValue) { return get(key, defaultValue, new ClassExtractor()); } public boolean hasKey(String key) { return jsonObject.has(key); } public Duration getDuration(String key, String defaultValue) { return new Duration(getDurationMillis(key, defaultValue)); } public long getDurationSeconds(String key, String defaultValue) { return ConfigUtil.getDurationMillis(getString(key, defaultValue)) / 1000; } public long getDurationMillis(String key, String defaultValue) { return ConfigUtil.getDurationMillis(getString(key, defaultValue)); } public long getSizeBytes(String key, String defaultValue) { return ConfigUtil.getSizeBytes(getString(key, defaultValue)); } public long getSizeBytes(String key, Long defaultValue) { return hasKey(key) ? ConfigUtil.getSizeBytes(getString(key)) : defaultValue; } public Boolean getBoolean(String key) throws ConfigException { return getBoolean(key, null); } public Boolean getBoolean(String key, Boolean defaultValue) throws ConfigException { return get(key, defaultValue, new BooleanExtractor()); } public String getString(String key) throws ConfigException { return getString(key, null); } public String getString(String key, String defaultValue) { return get(key, defaultValue, new StringExtractor()); } public int getInt(String key) throws ConfigException { return getInt(key, null); } public int getInt(String key, Integer defaultValue) { return get(key, defaultValue, new IntegerExtractor()); } public long getLong(String key) throws ConfigException { return getLong(key, null); } public long getLong(String key, Long defaultValue) { return get(key, defaultValue, new LongExtractor()); } public double getDouble(String key) { return getDouble(key, null); } public double getDouble(String key, Double defaultValue) { return get(key, defaultValue, new DoubleExtractor()); } public Map<String, String> getStringMap(String key) { return get( key, null, new Extractor<Map<String, String>>() { @Override public Map<String, String> extract(String key, JSONObject jsonObject) throws JSONException { Map<String, String> map = new HashMap<>(); JSONObject jsonMap = jsonObject.getJSONObject(key); ConfigAccessor mapAccessor = new ConfigAccessor(jsonMap); Iterator<String> keys = jsonMap.keys(); while (keys.hasNext()) { String mapKey = keys.next(); map.put(mapKey, mapAccessor.getString(mapKey)); } return map; } } ); } public <T> List<T> getList(String key, Mapper<String, T> converter) { List<T> result = new ArrayList<>(); for (String item : getStringList(key)) { result.add(converter.map(item)); } return result; } public List<String> getStringList(String key) throws ConfigException { JSONArray items; List<String> result = new ArrayList<>(); try { items = jsonObject.getJSONArray(key); for (int i = 0; i < items.length(); i++) { result.add(items.getString(i)); } } catch (JSONException e) { throw new ConfigException( "unable to parse string list for key " + key, e ); } return result; } public List<ByteArray> getByteArrayList(String key) throws ConfigException { List<ByteArray> result = new ArrayList<>(); for (String item : getStringList(key)) { result.add(ByteArray.wrap(item.getBytes())); } return result; } public List<String> getKeys() { List<String> keys = new ArrayList<>(); Iterator<String> keysIterator = jsonObject.keys(); while (keysIterator.hasNext()) { keys.add(keysIterator.next()); } return keys; } /** * @return a copy of the entire configuration in String -> String form */ public Map<String, String> getConfigAsMap() { Map<String, String> configAsMap = Maps.newHashMap(); Iterator<String> keysIterator = jsonObject.keys(); while (keysIterator.hasNext()) { String key = keysIterator.next(); try { configAsMap.put(key, jsonObject.getString(key)); } catch (JSONException e) { LOG.warn(e, "unable to extract key %s as a string in config", key); } } return configAsMap; } private <T> T get(String key, T defaultValue, Extractor<T> extractor) throws ConfigException { try { if (jsonObject.has(key)) { return extractor.extract(key, jsonObject); } else if (defaultValue != null) { return defaultValue; } else { throw new ConfigException("missing property: " + key); } } catch (JSONException e) { throw new ConfigException(e); } } @Override public String toString() { return jsonObject.toString(); } public String toString(int indentFactor) throws ConfigException { try { return jsonObject.toString(indentFactor); } catch (JSONException e) { throw new ConfigException(e); } } }