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    }