package org.zstack.core.config; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.GLock; import org.zstack.core.db.SimpleQuery; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.AbstractService; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.Message; import org.zstack.utils.BeanUtils; import org.zstack.utils.DebugUtils; import org.zstack.utils.TypeUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; import org.zstack.utils.path.PathUtil; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.File; import java.io.FileNotFoundException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.zstack.core.Platform.argerr; public class GlobalConfigFacadeImpl extends AbstractService implements GlobalConfigFacade { private static final CLogger logger = Utils.getLogger(GlobalConfigFacadeImpl.class); @Autowired private CloudBus bus; @Autowired private DatabaseFacade dbf; @Autowired private ErrorFacade errf; private JAXBContext context; private Map<String, GlobalConfig> allConfigs = new ConcurrentHashMap<>(); private static final String CONFIG_FOLDER = "globalConfig"; private static final String OTHER_CATEGORY = "Others"; private static final String LOCK = "GlobalFacade.lock"; @Override @MessageSafe public void handleMessage(Message msg) { if (msg instanceof APIUpdateGlobalConfigMsg) { handle((APIUpdateGlobalConfigMsg) msg); } else if (msg instanceof APIListGlobalConfigMsg) { handle((APIListGlobalConfigMsg) msg); } else if (msg instanceof APIGetGlobalConfigMsg) { handle((APIGetGlobalConfigMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } private void handle(APIGetGlobalConfigMsg msg) { GlobalConfig c = allConfigs.get(msg.getIdentity()); APIGetGlobalConfigReply reply = new APIGetGlobalConfigReply(); if (c == null) { ErrorCode err = argerr("unable to find GlobalConfig[category:%s, name:%s]", msg.getCategory(), msg.getName()); reply.setError(err); } else { GlobalConfigInventory inv = GlobalConfigInventory.valueOf(c); reply.setInventory(inv); } bus.reply(msg, reply); } private List<GlobalConfigVO> listGlobalConfig(APIListGlobalConfigMsg msg) { return dbf.listAll(msg.getOffset(), msg.getLength(), GlobalConfigVO.class); } private void handle(APIListGlobalConfigMsg msg) { APIListGlobalConfigReply reply = new APIListGlobalConfigReply(); List<GlobalConfigVO> vos = listGlobalConfig(msg); GlobalConfigInventory[] invs = new GlobalConfigInventory[vos.size()]; for (int i = 0; i < vos.size(); i++) { invs[i] = GlobalConfigInventory.valueOf(vos.get(i)); } reply.setInventories(invs); bus.reply(msg, reply); } private void handle(APIUpdateGlobalConfigMsg msg) { APIUpdateGlobalConfigEvent evt = new APIUpdateGlobalConfigEvent(msg.getId()); GlobalConfig globalConfig = allConfigs.get(msg.getIdentity()); if (globalConfig == null) { ErrorCode err = argerr("Unable to find GlobalConfig[category: %s, name: %s]", msg.getCategory(), msg.getName()); evt.setError(err); bus.publish(evt); return; } try { globalConfig.updateValue(msg.getValue()); GlobalConfigInventory inv = GlobalConfigInventory.valueOf(globalConfig.reload()); evt.setInventory(inv); } catch (GlobalConfigException e) { evt.setError(argerr(e.getMessage())); logger.warn(e.getMessage(), e); } bus.publish(evt); } @Override public String getId() { return bus.makeLocalServiceId(GlobalConfigConstant.SERVICE_ID); } @Override public boolean start() { class GlobalConfigInitializer { Map<String, GlobalConfig> configsFromXml = new HashMap<String, GlobalConfig>(); Map<String, GlobalConfig> configsFromDatabase = new HashMap<String, GlobalConfig>(); List<Field> globalConfigFields = new ArrayList<Field>(); void init() { GLock lock = new GLock(LOCK, 320); lock.lock(); try { parseGlobalConfigFields(); loadConfigFromXml(); loadConfigFromJava(); loadConfigFromDatabase(); createValidatorForBothXmlAndDatabase(); validateConfigFromXml(); validateConfigFromDatabase(); persistConfigInXmlButNotInDatabase(); mergeXmlDatabase(); initAllConfig(); link(); allConfigs.putAll(configsFromXml); // re-validate after merging xml's with db's validateAll(); } catch (IllegalArgumentException ie) { throw ie; } catch (Exception e) { throw new CloudRuntimeException(e); } finally { lock.unlock(); } } private void parseGlobalConfigFields() { List<Class> definitionClasses = BeanUtils.scanClass("org.zstack", GlobalConfigDefinition.class); for (Class def : definitionClasses) { for (Field field : def.getDeclaredFields()) { if (Modifier.isStatic(field.getModifiers()) && GlobalConfig.class.isAssignableFrom(field.getType())) { field.setAccessible(true); try { GlobalConfig config = (GlobalConfig) field.get(null); if (config == null) { throw new CloudRuntimeException(String.format("GlobalConfigDefinition[%s] defines a null GlobalConfig[%s]." + "You must assign a value to it using new GlobalConfig(category, name)", def.getClass().getName(), field.getName())); } globalConfigFields.add(field); } catch (IllegalAccessException e) { throw new CloudRuntimeException(e); } } } } } private void loadConfigFromJava() { for (Field field : globalConfigFields) { try { GlobalConfig config = (GlobalConfig) field.get(null); if (config == null) { throw new CloudRuntimeException(String.format("GlobalConfigDefinition[%s] defines a null GlobalConfig[%s]." + "You must assign a value to it using new GlobalConfig(category, name)", field.getDeclaringClass().getClass().getName(), field.getName())); } GlobalConfigDef d = field.getAnnotation(GlobalConfigDef.class); if (d == null) { continue; } GlobalConfig c = new GlobalConfig(); c.setCategory(config.getCategory()); c.setName(config.getName()); c.setDescription(d.description()); c.setDefaultValue(d.defaultValue()); c.setValue(d.defaultValue()); c.setType(d.type().getName()); if (!"".equals(d.validatorRegularExpression())) { c.setValidatorRegularExpression(d.validatorRegularExpression()); } if (configsFromXml.containsKey(c.getIdentity())) { throw new CloudRuntimeException(String.format("duplicate global configuration. %s defines a" + " global config[category: %s, name: %s] that has been defined by a XML configure or" + " another java class", field.getDeclaringClass().getName(), c.getCategory(), c.getName())); } configsFromXml.put(c.getIdentity(), c); } catch (IllegalAccessException e) { throw new CloudRuntimeException(e); } } } private void initAllConfig() { for (GlobalConfig config : configsFromXml.values()) { config.init(); } } private void mergeXmlDatabase() { for (GlobalConfig g : configsFromDatabase.values()) { GlobalConfig x = configsFromXml.get(g.getIdentity()); if (x == null) { configsFromXml.put(g.getIdentity(), g); } else { x.setValue(g.value()); x.setDefaultValue(g.getDefaultValue()); } } } private void validateAll() { for (GlobalConfig g : allConfigs.values()) { try { g.validate(); } catch (Exception e) { throw new IllegalArgumentException(String.format("exception happened when validating global config:\n%s", g.toString()), e); } } } private void validateConfigFromDatabase() { logger.debug(String.format("validating global config loaded from database")); for (GlobalConfig g : configsFromDatabase.values()) { g.validate(); } } private void validateConfigFromXml() { logger.debug(String.format("validating global config loaded from XML files")); for (GlobalConfig g : configsFromXml.values()) { g.validate(); } } private void persistConfigInXmlButNotInDatabase() { List<GlobalConfigVO> toSave = new ArrayList<GlobalConfigVO>(); for (GlobalConfig config : configsFromXml.values()) { if (configsFromDatabase.containsKey(config.getIdentity())) { continue; } logger.debug(String.format("Add a new global config to database: %s", config.toString())); toSave.add(config.toVO()); } if (!toSave.isEmpty()) { dbf.persistCollection(toSave); } } private void loadConfigFromDatabase() { List<GlobalConfigVO> vos = dbf.listAll(GlobalConfigVO.class); for (GlobalConfigVO vo : vos) { GlobalConfig c = GlobalConfig.valueOf(vo); configsFromDatabase.put(c.getIdentity(), c); } } private void loadConfigFromXml() throws JAXBException, FileNotFoundException { context = JAXBContext.newInstance("org.zstack.core.config.schema"); List<String> filePaths = PathUtil.scanFolderOnClassPath(CONFIG_FOLDER); for (String path : filePaths) { File f = new File(path); parseConfig(f); } } private void createValidator(final GlobalConfig g) throws ClassNotFoundException { g.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { Class<?> typeClass; Method typeClassValueOfMethod; String regularExpression; { if (g.getType() != null) { typeClass = Class.forName(g.getType()); try { typeClassValueOfMethod = typeClass.getMethod("valueOf", String.class); } catch (Exception e) { String err = String.format("GlobalConfig[category:%s, name:%s] specifies type[%s] which doesn't have a static valueOf() method, ignore this type", g.getCategory(), g.getName(), g.getType()); logger.warn(err); } } regularExpression = g.getValidatorRegularExpression(); } @Override public void validateGlobalConfig(String category, String name, String oldValue, String newValue) throws GlobalConfigException { if (typeClassValueOfMethod != null) { try { typeClassValueOfMethod.invoke(typeClass, newValue); } catch (Exception e) { String err = String.format("GlobalConfig[category:%s, name:%s] is of type %s, the value[%s] cannot be converted to that type, %s", g.getCategory(), g.getName(), typeClass.getName(), newValue, e.getMessage()); throw new GlobalConfigException(err, e); } try { typeClassValueOfMethod.invoke(typeClass, g.getDefaultValue()); } catch (Exception e) { String err = String.format("GlobalConfig[category:%s, name:%s] is of type %s, the default value[%s] cannot be converted to that type, %s", g.getCategory(), g.getName(), typeClass.getName(), g.getDefaultValue(), e.getMessage()); throw new GlobalConfigException(err, e); } } if (typeClass != null && (Boolean.class).isAssignableFrom(typeClass)) { if (newValue == null || (!newValue.equalsIgnoreCase("true") && !newValue.equalsIgnoreCase("false")) ) { String err = String.format("GlobalConfig[category:%s, name:%s]'s value[%s] is not a valid boolean string[true, false].", g.getCategory(), g.getName(), newValue); throw new GlobalConfigException(err); } if (g.getDefaultValue() == null || (!g.getDefaultValue().equalsIgnoreCase("true") && !g.getDefaultValue().equalsIgnoreCase("false")) ) { String err = String.format("GlobalConfig[category:%s, name:%s]'s default value[%s] is not a valid boolean string[true, false].", g.getCategory(), g.getName(), g.getDefaultValue()); throw new GlobalConfigException(err); } } if (regularExpression != null) { Pattern p = Pattern.compile(regularExpression); if (newValue != null) { Matcher mt = p.matcher(newValue); if (!mt.matches()) { String err = String.format("GlobalConfig[category:%s, name:%s]'s value[%s] doesn't match validatorRegularExpression[%s]", g.getCategory(), g.getName(), newValue, regularExpression); throw new GlobalConfigException(err); } } if (g.getDefaultValue() != null) { Matcher mt = p.matcher(g.getDefaultValue()); if (!mt.matches()) { String err = String.format("GlobalConfig[category:%s, name:%s]'s default value[%s] doesn't match validatorRegularExpression[%s]", g.getCategory(), g.getName(), g.getDefaultValue(), regularExpression); throw new GlobalConfigException(err); } } } } }); } private void createValidatorForBothXmlAndDatabase() throws ClassNotFoundException { for (GlobalConfig g : configsFromXml.values()) { createValidator(g); } for (GlobalConfig g : configsFromDatabase.values()) { createValidator(g); } } private void parseConfig(File file) throws JAXBException, FileNotFoundException { if (!file.getName().endsWith("xml")) { logger.warn(String.format("file[%s] in global config folder is not end with .xml, skip it", file.getAbsolutePath())); return; } Unmarshaller unmarshaller = context.createUnmarshaller(); org.zstack.core.config.schema.GlobalConfig gb = (org.zstack.core.config.schema.GlobalConfig) unmarshaller.unmarshal(file); for (org.zstack.core.config.schema.GlobalConfig.Config c : gb.getConfig()) { String category = c.getCategory(); category = category == null ? OTHER_CATEGORY : category; c.setCategory(category); if (c.getValue() == null) { c.setValue(c.getDefaultValue()); } if (c.getDefaultValue() == null) { throw new IllegalArgumentException(String.format("GlobalConfig[category:%s, name:%s] must have a default value", c.getCategory(), c.getName())); } GlobalConfig config = GlobalConfig.valueOf(c); if (configsFromXml.containsKey(config.getIdentity())) { throw new IllegalArgumentException(String.format("duplicate GlobalConfig[category: %s, name: %s]", config.getCategory(), config.getName())); } configsFromXml.put(config.getIdentity(), config); } } private void link() { for (Field field : globalConfigFields) { field.setAccessible(true); try { GlobalConfig config = (GlobalConfig) field.get(null); if (config == null) { throw new CloudRuntimeException(String.format("GlobalConfigDefinition[%s] defines a null GlobalConfig[%s]." + "You must assign a value to it using new GlobalConfig(category, name)", field.getDeclaringClass().getName(), field.getName())); } link(field, config); } catch (IllegalAccessException e) { throw new CloudRuntimeException(e); } } for (GlobalConfig c : configsFromXml.values()) { if (!c.isLinked()) { logger.warn(String.format("GlobalConfig[category: %s, name: %s] is not linked to any definition", c.getCategory(), c.getName())); } } } private void link(Field field, final GlobalConfig old) throws IllegalAccessException { final GlobalConfig config = configsFromXml.get(old.getIdentity()); DebugUtils.Assert(config != null, String.format("unable to find GlobalConfig[category:%s, name:%s] for linking to %s.%s", old.getCategory(), old.getName(), field.getDeclaringClass().getName(), field.getName())); field.set(null, config); final GlobalConfigValidation at = field.getAnnotation(GlobalConfigValidation.class); if (at != null) { config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException { if (at.notNull() && value == null) { throw new GlobalConfigException(String.format("%s cannot be null", config.getCanonicalName())); } } }); config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String newValue) throws GlobalConfigException { if (at.notEmpty() && newValue.trim().equals("")) { throw new GlobalConfigException(String.format("%s cannot be empty string", config.getCanonicalName())); } } }); if (at.inNumberRange().length > 0 || at.numberGreaterThan() != Long.MIN_VALUE || at.numberLessThan() != Long.MAX_VALUE) { if (config.getType() != null && TypeUtils.isTypeOf(config.getType(), Long.class, Integer.class)) { throw new CloudRuntimeException(String.format("%s has @GlobalConfigValidation defined on field[%s.%s] which indicates its numeric type, but its type is neither Long nor Integer, it's %s", config.getCanonicalName(), field.getDeclaringClass(), field.getName(), config.getType())); } if (config.getType() == null) { logger.warn(String.format("%s has @GlobalConfigValidation defined on field[%s.%s] which indicates it's numeric type, but its is null, assume it's Long type", config.getCanonicalName(), field.getDeclaringClass(), field.getName())); config.setType(Long.class.getName()); } } if (at.numberLessThan() != Long.MAX_VALUE) { config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException { try { long num = Long.valueOf(value); if (num > at.numberLessThan()) { throw new GlobalConfigException(String.format("%s must be less than %s, but got %s", config.getCanonicalName(), at.numberLessThan(), num)); } } catch (NumberFormatException e) { throw new GlobalConfigException(String.format("%s is not a number or out of range of a Long type", value), e); } } }); } if (at.numberGreaterThan() != Long.MIN_VALUE) { config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException { try { long num = Long.valueOf(value); if (num < at.numberGreaterThan()) { throw new GlobalConfigException(String.format("%s must be greater than %s, but got %s", config.getCanonicalName(), at.numberGreaterThan(), num)); } } catch (NumberFormatException e) { throw new GlobalConfigException(String.format("%s is not a number or out of range of a Long type", value), e); } } }); } if (at.inNumberRange().length > 0) { DebugUtils.Assert(at.inNumberRange().length == 2, String.format("@GlobalConfigValidation.inNumberRange defined on field[%s.%s] must have two elements, where the first one is lower bound and the second one is upper bound", field.getDeclaringClass(), field.getName())); config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException { try { long num = Long.valueOf(value); long lowBound = at.inNumberRange()[0]; long upBound = at.inNumberRange()[1]; if (!(num >= lowBound && num <= upBound)) { throw new GlobalConfigException(String.format("%s must in range of [%s, %s]", config.getCanonicalName(), lowBound, upBound)); } } catch (NumberFormatException e) { throw new GlobalConfigException(String.format("%s is not a number or out of range of a Long type", value), e); } } }); } if (at.validValues().length > 0) { final List<String> validValues = new ArrayList<String>(); Collections.addAll(validValues, at.validValues()); config.installValidateExtension(new GlobalConfigValidatorExtensionPoint() { @Override public void validateGlobalConfig(String category, String name, String oldValue, String newValue) throws GlobalConfigException { if (!validValues.contains(newValue)) { throw new GlobalConfigException(String.format("%s is not a valid value. Valid values are %s", newValue, validValues)); } } }); } } config.setConfigDef(field.getAnnotation(GlobalConfigDef.class)); config.setLinked(true); logger.debug(String.format("linked GlobalConfig[category:%s, name:%s, value:%s] to %s.%s", config.getCategory(), config.getName(), config.getDefaultValue(), field.getDeclaringClass().getName(), field.getName())); } } GlobalConfigInitializer initializer = new GlobalConfigInitializer(); initializer.init(); return true; } @Override public boolean stop() { return true; } @Override public Map<String, GlobalConfig> getAllConfig() { SimpleQuery<GlobalConfigVO> query = dbf.createQuery(GlobalConfigVO.class); List<GlobalConfigVO> vos = query.list(); Map<String, GlobalConfig> ret = new HashMap<String, GlobalConfig>(vos.size()); for (GlobalConfigVO vo : vos) { GlobalConfig c = GlobalConfig.valueOf(vo); ret.put(c.getIdentity(), c); } return ret; } @Override public <T> T getConfigValue(String category, String name, Class<T> clz) { GlobalConfig c = allConfigs.get(GlobalConfig.produceIdentity(category, name)); DebugUtils.Assert(c!=null, String.format("cannot find GlobalConfig[category:%s, name:%s]", category, name)); return c.value(clz); } @Override public GlobalConfig createGlobalConfig(GlobalConfigVO vo) { vo = dbf.persistAndRefresh(vo); GlobalConfig c = GlobalConfig.valueOf(vo); allConfigs.put(GlobalConfig.produceIdentity(vo.getCategory(), vo.getName()), c); return c; } @Override public String updateConfig(String category, String name, String value) { GlobalConfig c = allConfigs.get(GlobalConfig.produceIdentity(category,name)); DebugUtils.Assert(c != null, String.format("cannot find GlobalConfig[category:%s, name:%s]", category, name)); c.updateValue(value); return c.value(); } }