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.devtools.datadictionary; 017 018 019import no.geosoft.cc.io.FileListener; 020import no.geosoft.cc.io.FileMonitor; 021import org.apache.commons.collections.ListUtils; 022import org.apache.commons.lang.StringUtils; 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025import org.kuali.rice.core.api.CoreApiServiceLocator; 026import org.kuali.rice.core.api.config.property.ConfigurationService; 027import org.kuali.rice.krad.datadictionary.DataDictionary; 028import org.kuali.rice.krad.util.KRADConstants; 029import org.springframework.beans.BeansException; 030import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; 031import org.springframework.context.ApplicationContext; 032import org.springframework.context.ApplicationContextAware; 033import org.springframework.context.ApplicationListener; 034import org.springframework.context.ConfigurableApplicationContext; 035import org.springframework.context.event.ContextClosedEvent; 036import org.springframework.core.io.FileSystemResource; 037import org.springframework.core.io.InputStreamResource; 038import org.springframework.core.io.Resource; 039 040import java.io.File; 041import java.io.InputStream; 042import java.net.URL; 043import java.util.Arrays; 044import java.util.HashMap; 045import java.util.List; 046import java.util.Map; 047 048/** 049 * Extends the DataDictionary to add reloading of changed dictionary files 050 * without a restart of the web container 051 * 052 * <p> 053 * To use modify the "dataDictionaryService" spring definition 054 * (KRADSpringBeans.xml) and change the constructor arg bean class from 055 * "org.kuali.rice.krad.datadictionary.DataDictionary" to 056 * "ReloadingDataDictionary" 057 * </p> 058 * 059 * <p> 060 * NOTE: For Development Purposes Only! 061 * </p> 062 * 063 * @author Kuali Rice Team (rice.collab@kuali.org) 064 */ 065public class ReloadingDataDictionary extends DataDictionary implements FileListener, URLMonitor.URLContentChangedListener, ApplicationContextAware { 066 private static final Log LOG = LogFactory.getLog(DataDictionary.class); 067 068 private static final String CLASS_DIR_CONFIG_PARM = "reload.data.dictionary.classes.dir"; 069 private static final String SOURCE_DIR_CONFIG_PARM = "reload.data.dictionary.source.dir"; 070 private static final String INTERVAL_CONFIG_PARM = "reload.data.dictionary.interval"; 071 072 private Map<String, String> fileToNamespaceMapping; 073 private Map<String, String> urlToNamespaceMapping; 074 075 private FileMonitor dictionaryFileMonitor; 076 private URLMonitor dictionaryUrlMonitor; 077 078 public ReloadingDataDictionary() { 079 super(); 080 } 081 082 /** 083 * After dictionary has been loaded, determine the source files and add them 084 * to the monitor 085 * 086 * @see org.kuali.rice.krad.datadictionary.DataDictionary#parseDataDictionaryConfigurationFiles(boolean) 087 */ 088 @Override 089 public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) { 090 ConfigurationService configurationService = CoreApiServiceLocator.getKualiConfigurationService(); 091 092 // class directory part of the path that should be replaced 093 String classesDir = configurationService.getPropertyValueAsString(CLASS_DIR_CONFIG_PARM); 094 095 // source directory where dictionary files are found 096 String sourceDir = configurationService.getPropertyValueAsString(SOURCE_DIR_CONFIG_PARM); 097 098 // interval to poll for changes in milliseconds 099 int reloadInterval = Integer.parseInt(configurationService.getPropertyValueAsString(INTERVAL_CONFIG_PARM)); 100 101 dictionaryFileMonitor = new FileMonitor(reloadInterval); 102 dictionaryFileMonitor.addListener(this); 103 104 dictionaryUrlMonitor = new URLMonitor(reloadInterval); 105 dictionaryUrlMonitor.addListener(this); 106 107 super.parseDataDictionaryConfigurationFiles(allowConcurrentValidation); 108 109 // need to hold mappings of file/url to namespace so we can correctly add beans to the associated 110 // namespace when reloading the resource 111 fileToNamespaceMapping = new HashMap<String, String>(); 112 urlToNamespaceMapping = new HashMap<String, String>(); 113 114 // add listener for each dictionary file 115 for (Map.Entry<String, List<String>> moduleDictionary : moduleDictionaryFiles.entrySet()) { 116 String namespace = moduleDictionary.getKey(); 117 List<String> configLocations = moduleDictionary.getValue(); 118 119 for (String configLocation : configLocations) { 120 Resource classFileResource = getFileResource(configLocation); 121 122 try { 123 if (classFileResource.getURI().toString().startsWith("jar:")) { 124 LOG.trace("Monitoring dictionary file at URI: " + classFileResource.getURI().toString()); 125 126 dictionaryUrlMonitor.addURI(classFileResource.getURL()); 127 urlToNamespaceMapping.put(classFileResource.getURL().toString(), namespace); 128 } else { 129 String filePathClassesDir = classFileResource.getFile().getAbsolutePath(); 130 String sourceFilePath = StringUtils.replace(filePathClassesDir, classesDir, sourceDir); 131 132 File dictionaryFile = new File(filePathClassesDir); 133 if (dictionaryFile.exists()) { 134 LOG.trace("Monitoring dictionary file: " + dictionaryFile.getName()); 135 136 dictionaryFileMonitor.addFile(dictionaryFile); 137 fileToNamespaceMapping.put(dictionaryFile.getAbsolutePath(), namespace); 138 } 139 } 140 } catch (Exception e) { 141 LOG.info("Exception in picking up dictionary file for monitoring: " + e.getMessage(), e); 142 } 143 } 144 } 145 } 146 147 /** 148 * Call back when a dictionary file is changed. Calls the spring bean reader 149 * to reload the file (which will override beans as necessary and destroy 150 * singletons) and runs the indexer 151 * 152 * @see no.geosoft.cc.io.FileListener#fileChanged(java.io.File) 153 */ 154 public void fileChanged(File file) { 155 LOG.info("reloading dictionary configuration for " + file.getName()); 156 try { 157 List<String> beforeReloadBeanNames = Arrays.asList(ddBeans.getBeanDefinitionNames()); 158 159 Resource resource = new FileSystemResource(file); 160 xmlReader.loadBeanDefinitions(resource); 161 162 List<String> afterReloadBeanNames = Arrays.asList(ddBeans.getBeanDefinitionNames()); 163 164 List<String> addedBeanNames = ListUtils.removeAll(afterReloadBeanNames, beforeReloadBeanNames); 165 String namespace = KRADConstants.DEFAULT_NAMESPACE; 166 if (fileToNamespaceMapping.containsKey(file.getAbsolutePath())) { 167 namespace = fileToNamespaceMapping.get(file.getAbsolutePath()); 168 } 169 170 ddIndex.addBeanNamesToNamespace(namespace, addedBeanNames); 171 172 performDictionaryPostProcessing(true); 173 } catch (Exception e) { 174 LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e); 175 } 176 } 177 178 public void urlContentChanged(final URL url) { 179 LOG.info("reloading dictionary configuration for " + url.toString()); 180 try { 181 InputStream urlStream = url.openStream(); 182 InputStreamResource resource = new InputStreamResource(urlStream); 183 184 List<String> beforeReloadBeanNames = Arrays.asList(ddBeans.getBeanDefinitionNames()); 185 186 int originalValidationMode = xmlReader.getValidationMode(); 187 xmlReader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); 188 xmlReader.loadBeanDefinitions(resource); 189 xmlReader.setValidationMode(originalValidationMode); 190 191 List<String> afterReloadBeanNames = Arrays.asList(ddBeans.getBeanDefinitionNames()); 192 193 List<String> addedBeanNames = ListUtils.removeAll(afterReloadBeanNames, beforeReloadBeanNames); 194 String namespace = KRADConstants.DEFAULT_NAMESPACE; 195 if (urlToNamespaceMapping.containsKey(url.toString())) { 196 namespace = urlToNamespaceMapping.get(url.toString()); 197 } 198 199 ddIndex.addBeanNamesToNamespace(namespace, addedBeanNames); 200 201 performDictionaryPostProcessing(true); 202 } catch (Exception e) { 203 LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e); 204 } 205 } 206 207 @Override 208 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 209 // register a context close handler 210 if (applicationContext instanceof ConfigurableApplicationContext) { 211 ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext; 212 context.addApplicationListener(new ApplicationListener<ContextClosedEvent>() { 213 @Override 214 public void onApplicationEvent(ContextClosedEvent e) { 215 LOG.info("Context '" + e.getApplicationContext().getDisplayName() + 216 "' closed, shutting down URLMonitor scheduler"); 217 dictionaryUrlMonitor.shutdownScheduler(); 218 } 219 }); 220 } 221 } 222}