package rocks.inspectit.server.property; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import javax.xml.bind.JAXBException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.xml.sax.SAXException; import rocks.inspectit.server.util.ShutdownService; import rocks.inspectit.shared.all.util.ResourcesPathResolver; import rocks.inspectit.shared.cs.cmr.property.configuration.AbstractProperty; import rocks.inspectit.shared.cs.cmr.property.configuration.Configuration; import rocks.inspectit.shared.cs.cmr.property.configuration.PropertySection; import rocks.inspectit.shared.cs.cmr.property.configuration.SingleProperty; import rocks.inspectit.shared.cs.cmr.property.configuration.validation.PropertyValidation; import rocks.inspectit.shared.cs.cmr.property.update.AbstractPropertyUpdate; import rocks.inspectit.shared.cs.cmr.property.update.IPropertyUpdate; import rocks.inspectit.shared.cs.cmr.property.update.configuration.ConfigurationUpdate; import rocks.inspectit.shared.cs.jaxb.JAXBTransformator; /** * Properties manager bean that controls all properties specified in the configuration files and * provides the {@link Properties} object as a bean for the Spring context property-placeholder. * * @author Ivan Senic * */ @org.springframework.context.annotation.Configuration public class PropertyManager { /** * The logger of this class. * <p> * Must be declared manually since loading of the properties is done before beans * post-processing. */ private static final Logger LOG = LoggerFactory.getLogger(PropertyManager.class); /** * Name of the local properties bean that will be created. */ public static final String LOCAL_PROPERTIES_BEAN_NAME = "localPropertiesBean"; /** * Directory where configuration files are places. */ private static final String CONFIG_DIR = "config"; /** * Directory where configuration files are places. */ private static final String SCHEMA_DIR = "schema"; /** * Name of the schema file for configuration. */ private static final String CONFIGURATION_SCHEMA_FILE = "configurationSchema.xsd"; /** * Name of the schema file for configuration update. */ private static final String CONFIGURATION_UPDATE_SCHEMA_FILE = "configurationUpdateSchema.xsd"; /** * File name where default configuration is stored. */ private static final String DEFAULT_CONFIG_FILE = "default.xml"; /** * File name where the current configuration updates are stored. */ private static final String CONFIG_UPDATE_FILE = "configurationUpdates.xml"; /** * Used with {@link ResourcesPathResolver} to get the file of the config dir. */ private File configDirFile; /** * Default configuration. */ private Configuration configuration; /** * Currently used configuration update. */ private ConfigurationUpdate configurationUpdate; /** * {@link PropertyUpdateExecutor} that executes methods need to be executed after properties * changes. */ @Autowired private PropertyUpdateExecutor propertyUpdateExecutor; /** * Shutdown service for executing restarts. */ @Autowired private ShutdownService shutdownService; /** * {@link JAXBTransformator}. * <p> * Can not auto-wire this component at the point of start-up, because it's a component. Thus, * direct access. */ private JAXBTransformator transformator = new JAXBTransformator(); /** * Returns the currently existing {@link PropertySection} in the CMR configuration. * * @return Returns the currently existing {@link PropertySection} in the CMR configuration. */ public Collection<PropertySection> getConfigurationPropertySections() { return configuration.getSections(); } /** * Returns the currently CMR configuration. * * @return Returns the currently CMR configuration. */ Configuration getConfiguration() { return configuration; } /** * Updates the current configuration of the CMR. * * @param update * {@link ConfigurationUpdate} containing all {@link AbstractPropertyUpdate}s that * should be reflected in the current configuration of the CMR. * @param executeRestart * Should restart be automatically executed after properties update. * @throws Exception * If update is not valid */ public synchronized void updateConfiguration(ConfigurationUpdate update, boolean executeRestart) throws Exception { // first validate all changes // if property does not exist or can not be updated throw exception for (IPropertyUpdate<?> propertyUpdate : update.getPropertyUpdates()) { SingleProperty<Object> property = configuration.forLogicalName(propertyUpdate.getPropertyLogicalName()); if (null == property) { throw new Exception("Property " + propertyUpdate.getPropertyLogicalName() + " can not be updated because the property does not exist in the current configuration."); } else if (!property.canUpdate(propertyUpdate)) { throw new Exception("Property " + propertyUpdate.getPropertyLogicalName() + " can not be updated because the property update value is not valid."); } } // if all valid update all List<SingleProperty<?>> updatedProperties = new ArrayList<>(); for (IPropertyUpdate<?> propertyUpdate : update.getPropertyUpdates()) { SingleProperty<Object> property = configuration.forLogicalName(propertyUpdate.getPropertyLogicalName()); if (propertyUpdate.isRestoreDefault()) { property.setToDefaultValue(); } else { property.setValue(propertyUpdate.getUpdateValue()); } updatedProperties.add(property); if (LOG.isInfoEnabled()) { LOG.info("Property '" + property.getName() + "' successfully updated, new value is " + property.getFormattedValue()); } } // merge the update file // note that restore to default updates will also be part of the update if (null == configurationUpdate) { configurationUpdate = update; } else { configurationUpdate.merge(update, true); } // back up the old configuration file if it exists if (Files.exists(getConfigurationUpdatePath())) { String backupPathString = getConfigurationUpdatePath().toString() + "~" + System.currentTimeMillis() + ".backup"; Path backupPath = Paths.get(backupPathString); try { Files.copy(getConfigurationUpdatePath(), backupPath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { LOG.warn("Could not back up the current configuration update", e); } } // flush the new configuration update try { transformator.marshall(getConfigurationUpdatePath(), configurationUpdate, getBaseConfigDir().relativize(getConfigurationUpdateSchemaPath()).toString()); } catch (JAXBException | IOException e) { LOG.warn("Could not flush the new configuration update", e); } if (executeRestart) { // just restart, properties will be reloaded anyway shutdownService.restart(); } else { // activate property update executor propertyUpdateExecutor.executePropertyUpdates(updatedProperties); } } /** * Returns {@link Properties} containing key/value property pairs defined in CMR configuration. * * @return Returns {@link Properties} containing key/value property pairs defined in CMR * configuration. */ @Bean(name = LOCAL_PROPERTIES_BEAN_NAME) protected synchronized Properties getProperties() { init(); try { loadConfigurationAndUpdates(); } catch (JAXBException | IOException | SAXException e) { LOG.warn("|-Default CMR configuration can not be loaded.", e); throw new BeanInitializationException("Default CMR configuration can not be loaded.", e); } if (null == configuration) { throw new BeanInitializationException("Default CMR configuration was not loaded. Aborting.."); } // check if there is update file if (null != configurationUpdate) { LOG.info("|-Updates to the CMR Configuration found, applying the updates"); List<IPropertyUpdate<?>> notValidList = new ArrayList<>(); // if there is update file validate updates and set to the configuration for (IPropertyUpdate<?> propertyUpdate : configurationUpdate.getPropertyUpdates()) { SingleProperty<Object> property = configuration.forLogicalName(propertyUpdate.getPropertyLogicalName()); // if property does not exist or can not be update add to not valid if ((null == property) || !property.canUpdate(propertyUpdate)) { notValidList.add(propertyUpdate); continue; } if (propertyUpdate.isRestoreDefault()) { property.setToDefaultValue(); } else { property.setValue(propertyUpdate.getUpdateValue()); } } // if not valid list is not empty // log all wrong properties and rewrite the configuration update if (CollectionUtils.isNotEmpty(notValidList)) { for (IPropertyUpdate<?> propertyUpdate : notValidList) { configurationUpdate.removePropertyUpdate(propertyUpdate); LOG.info("|-Update of the property " + propertyUpdate.getPropertyLogicalName() + " can not be performed either because property does not exist in the default configuration or the update value is not valid"); } try { transformator.marshall(getConfigurationUpdatePath(), configurationUpdate, getBaseConfigDir().relativize(getConfigurationUpdateSchemaPath()).toString()); } catch (JAXBException | IOException e) { LOG.warn("|-CMR Configuration update can not be re-written", e); } } } else { LOG.info("|-No CMR Configuration updates found, continuing to use default configuration"); } // validate configuration Map<AbstractProperty, PropertyValidation> validationMap = configuration.validate(); // if we have some validation problems log them if (MapUtils.isNotEmpty(validationMap)) { for (Entry<AbstractProperty, PropertyValidation> entry : validationMap.entrySet()) { LOG.warn(entry.getValue().getMessage()); } } else { LOG.info("|-CMR Configuration verified with no errors"); } // create properties from correct ones Properties properties = new Properties(); for (AbstractProperty property : configuration.getAllProperties()) { if (!validationMap.containsKey(property)) { property.register(properties); } } return properties; } /** * This is a workaround for the problem of providing the {@link PropertyManager} to the bean * factory for auto-wiring. The problem is that because Properties are provided via @Bean * annotation, the {@link PropertyManager} itself will not be completely auto-wired with needed * dependencies. * * @return Object it self. */ @Bean protected PropertyManager getPropertyManager() { return this; } /** * Initializes {@link #configDirFile}. */ protected void init() { try { configDirFile = ResourcesPathResolver.getResourceFile(CONFIG_DIR); } catch (IOException exception) { throw new BeanInitializationException("Property manager can not locate configuration directory.", exception); } } /** * @return Returns base config directory. */ Path getBaseConfigDir() { return configDirFile.toPath(); } /** * @return Returns path to the default configuration path. */ Path getDefaultConfigurationPath() { return getBaseConfigDir().resolve(DEFAULT_CONFIG_FILE); } /** * @return Returns path to the current configuration update path. */ Path getConfigurationUpdatePath() { return getBaseConfigDir().resolve(CONFIG_UPDATE_FILE); } /** * @return Returns path to the configuration XSD schema file. */ Path getConfigurationSchemaPath() { return getBaseConfigDir().resolve(SCHEMA_DIR).resolve(CONFIGURATION_SCHEMA_FILE); } /** * @return Returns path to the configuration update XSD schema file. */ Path getConfigurationUpdateSchemaPath() { return getBaseConfigDir().resolve(SCHEMA_DIR).resolve(CONFIGURATION_UPDATE_SCHEMA_FILE); } /** * Loads the default configuration if it is not already loaded. If successfully loaded * configuration will be placed in the {@link #configuration} field. * * * @throws JAXBException * If {@link JAXBException} occurs during loading. * @throws IOException * If {@link IOException} occurs during loading. * @throws SAXException * If {@link SAXException} occurs during schema parsing. */ void loadConfigurationAndUpdates() throws JAXBException, IOException, SAXException { // first default configuration LOG.info("|-Loading the default CMR configuration"); Path defaultConfigurationPath = getDefaultConfigurationPath(); if (Files.exists(defaultConfigurationPath)) { configuration = transformator.unmarshall(defaultConfigurationPath, getConfigurationSchemaPath(), Configuration.class); } else { String path = defaultConfigurationPath.toAbsolutePath().toString(); LOG.warn("||-Default configuration file is not present on the path " + path); throw new IOException("Default configuration file does not exist on the path " + path); } // then updates LOG.info("|-Loading the CMR configuration updates"); configurationUpdate = transformator.unmarshall(getConfigurationUpdatePath(), getConfigurationUpdateSchemaPath(), ConfigurationUpdate.class); } }