001 /**
002 * Copyright 2005-2013 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 */
016 package org.kuali.rice.krad.datadictionary;
017
018 import no.geosoft.cc.io.FileListener;
019 import no.geosoft.cc.io.FileMonitor;
020 import org.apache.commons.lang.StringUtils;
021 import org.apache.commons.logging.Log;
022 import org.apache.commons.logging.LogFactory;
023 import org.kuali.rice.core.api.config.property.ConfigurationService;
024 import org.kuali.rice.krad.service.KRADServiceLocator;
025 import org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor;
026 import org.springframework.beans.BeansException;
027 import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
028 import org.springframework.context.ApplicationContext;
029 import org.springframework.context.ApplicationContextAware;
030 import org.springframework.context.ApplicationListener;
031 import org.springframework.context.ConfigurableApplicationContext;
032 import org.springframework.context.event.ContextClosedEvent;
033 import org.springframework.core.io.FileSystemResource;
034 import org.springframework.core.io.InputStreamResource;
035 import org.springframework.core.io.Resource;
036
037 import java.io.File;
038 import java.io.InputStream;
039 import java.net.URL;
040 import java.util.ArrayList;
041 import 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 */
061 public 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 }