001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.datadictionary; 017 018import no.geosoft.cc.io.FileListener; 019import no.geosoft.cc.io.FileMonitor; 020import org.apache.commons.lang.StringUtils; 021import org.apache.commons.logging.Log; 022import org.apache.commons.logging.LogFactory; 023import org.kuali.rice.core.api.config.property.ConfigurationService; 024import org.kuali.rice.krad.service.KRADServiceLocator; 025import org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor; 026import org.springframework.beans.BeansException; 027import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; 028import org.springframework.context.ApplicationContext; 029import org.springframework.context.ApplicationContextAware; 030import org.springframework.context.ApplicationListener; 031import org.springframework.context.ConfigurableApplicationContext; 032import org.springframework.context.event.ContextClosedEvent; 033import org.springframework.core.io.FileSystemResource; 034import org.springframework.core.io.InputStreamResource; 035import org.springframework.core.io.Resource; 036 037import java.io.File; 038import java.io.InputStream; 039import java.net.URL; 040import java.util.ArrayList; 041import java.util.List; 042 043 044/** 045 * Extends the DataDictionary to add reloading of changed dictionary files 046 * without a restart of the web container 047 * 048 * <p> 049 * To use modify the "dataDictionaryService" spring definition 050 * (KRADSpringBeans.xml) and change the constructor arg bean class from 051 * "org.kuali.rice.krad.datadictionary.DataDictionary" to 052 * "ReloadingDataDictionary" 053 * </p> 054 * 055 * <p> 056 * NOTE: For Development Purposes Only! 057 * </p> 058 * 059 * @author Kuali Rice Team (rice.collab@kuali.org) 060 */ 061public class ReloadingDataDictionary extends DataDictionary implements FileListener, URLMonitor.URLContentChangedListener, ApplicationContextAware { 062 private static final Log LOG = LogFactory.getLog(DataDictionary.class); 063 064 private static final String CLASS_DIR_CONFIG_PARM = "reload.data.dictionary.classes.dir"; 065 private static final String SOURCE_DIR_CONFIG_PARM = "reload.data.dictionary.source.dir"; 066 private static final String INTERVAL_CONFIG_PARM = "reload.data.dictionary.interval"; 067 068 private URLMonitor dictionaryUrlMonitor; 069 070 071 public ReloadingDataDictionary() { 072 super(); 073 } 074 075 /** 076 * After dictionary has been loaded, determine the source files and add them 077 * to the monitor 078 * 079 * @see org.kuali.rice.krad.datadictionary.DataDictionary#parseDataDictionaryConfigurationFiles(boolean) 080 */ 081 @Override 082 public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) { 083 ConfigurationService configurationService = KRADServiceLocator.getKualiConfigurationService(); 084 085 // class directory part of the path that should be replaced 086 String classesDir = configurationService.getPropertyValueAsString(CLASS_DIR_CONFIG_PARM); 087 088 // source directory where dictionary files are found 089 String sourceDir = configurationService.getPropertyValueAsString(SOURCE_DIR_CONFIG_PARM); 090 091 // interval to poll for changes in milliseconds 092 int reloadInterval = Integer.parseInt(configurationService.getPropertyValueAsString(INTERVAL_CONFIG_PARM)); 093 094 FileMonitor dictionaryFileMonitor = new FileMonitor(reloadInterval); 095 096 dictionaryUrlMonitor = new URLMonitor(reloadInterval); 097 dictionaryUrlMonitor.addListener(this); 098 099 // need to copy the configFileLocations list here because it gets 100 // cleared out after processing by super 101 List<String> configLocations = new ArrayList<String>(configFileLocations); 102 103 super.parseDataDictionaryConfigurationFiles(allowConcurrentValidation); 104 for (String configLocation : configLocations) { 105 Resource classFileResource = getFileResource(configLocation); 106 try { 107 if (classFileResource.getURI().toString().startsWith("jar:")) { 108 LOG.debug("Monitoring dictionary file at URI: " + classFileResource.getURI().toString()); 109 dictionaryUrlMonitor.addURI(classFileResource.getURL()); 110 } else { 111 String filePathClassesDir = classFileResource.getFile().getAbsolutePath(); 112 String sourceFilePath = StringUtils.replace(filePathClassesDir, classesDir, sourceDir); 113 File dictionaryFile = new File(filePathClassesDir); 114 if (dictionaryFile.exists()) { 115 LOG.debug("Monitoring dictionary file: " + dictionaryFile.getName()); 116 dictionaryFileMonitor.addFile(dictionaryFile); 117 } 118 } 119 } 120 catch (Exception e) { 121 LOG.info("Exception in picking up dictionary file for monitoring: " + e.getMessage(), e); 122 } 123 } 124 125 // add the dictionary as a listener for file changes 126 dictionaryFileMonitor.addListener(this); 127 } 128 129 /** 130 * Call back when a dictionary file is changed. Calls the spring bean reader 131 * to reload the file (which will override beans as necessary and destroy 132 * singletons) and runs the indexer 133 * 134 * @see no.geosoft.cc.io.FileListener#fileChanged(java.io.File) 135 */ 136 @Override 137 public void fileChanged(File file) { 138 LOG.info("reloading dictionary configuration for " + file.getName()); 139 try { 140 Resource resource = new FileSystemResource(file); 141 xmlReader.loadBeanDefinitions(resource); 142 143 UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor(); 144 factoryPostProcessor.postProcessBeanFactory(ddBeans); 145 146 // re-index 147 ddIndex.run(); 148 } 149 catch (Exception e) { 150 LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e); 151 } 152 } 153 154 public void urlContentChanged(final URL url) { 155 LOG.info("reloading dictionary configuration for " + url.toString()); 156 try { 157 InputStream urlStream = url.openStream(); 158 InputStreamResource resource = new InputStreamResource(urlStream); 159 160 int originalValidationMode = xmlReader.getValidationMode(); 161 xmlReader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); 162 xmlReader.loadBeanDefinitions(resource); 163 xmlReader.setValidationMode(originalValidationMode); 164 165 UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor(); 166 factoryPostProcessor.postProcessBeanFactory(ddBeans); 167 168 // re-index 169 ddIndex.run(); 170 } 171 catch (Exception e) { 172 LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e); 173 } 174 } 175 176 @Override 177 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 178 // register a context close handler 179 if (applicationContext instanceof ConfigurableApplicationContext) { 180 ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext; 181 context.addApplicationListener(new ApplicationListener<ContextClosedEvent>() { 182 @Override 183 public void onApplicationEvent(ContextClosedEvent e) { 184 LOG.info("Context '" + e.getApplicationContext().getDisplayName() + "' closed, shutting down URLMonitor scheduler"); 185 dictionaryUrlMonitor.shutdownScheduler(); 186 } 187 }); 188 } 189 } 190}