001    /**
002     * Copyright 2005-2012 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.factory.xml.XmlBeanDefinitionReader;
027    import org.springframework.core.io.FileSystemResource;
028    import org.springframework.core.io.InputStreamResource;
029    import org.springframework.core.io.Resource;
030    
031    import java.io.File;
032    import java.io.InputStream;
033    import java.net.URL;
034    import java.util.ArrayList;
035    import java.util.List;
036    
037    
038    /**
039     * Extends the DataDictionary to add reloading of changed dictionary files
040     * without a restart of the web container
041     * 
042     * <p>
043     * To use modify the "dataDictionaryService" spring definition
044     * (KRADSpringBeans.xml) and change the constructor arg bean class from
045     * "org.kuali.rice.krad.datadictionary.DataDictionary" to
046     * "ReloadingDataDictionary"
047     * </p>
048     * 
049     * <p>
050     * NOTE: For Development Purposes Only!
051     * </p>
052     * 
053     * @author Kuali Rice Team (rice.collab@kuali.org)
054     */
055    public class ReloadingDataDictionary extends DataDictionary implements FileListener, URLMonitor.URLContentChangedListener {
056            private static final Log LOG = LogFactory.getLog(DataDictionary.class);
057    
058            private static final String CLASS_DIR_CONFIG_PARM = "reload.data.dictionary.classes.dir";
059            private static final String SOURCE_DIR_CONFIG_PARM = "reload.data.dictionary.source.dir";
060            private static final String INTERVAL_CONFIG_PARM = "reload.data.dictionary.interval";
061    
062        private URLMonitor dictionaryUrlMonitor;
063    
064    
065            public ReloadingDataDictionary() {
066                    super();
067            }
068    
069            /**
070             * After dictionary has been loaded, determine the source files and add them
071             * to the monitor
072             * 
073             * @see org.kuali.rice.krad.datadictionary.DataDictionary#parseDataDictionaryConfigurationFiles(boolean)
074             */
075            @Override
076            public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) {
077                    ConfigurationService configurationService = KRADServiceLocator.getKualiConfigurationService();
078    
079                    // class directory part of the path that should be replaced
080                    String classesDir = configurationService.getPropertyValueAsString(CLASS_DIR_CONFIG_PARM);
081    
082                    // source directory where dictionary files are found
083                    String sourceDir = configurationService.getPropertyValueAsString(SOURCE_DIR_CONFIG_PARM);
084    
085                    // interval to poll for changes in milliseconds
086                    int reloadInterval = Integer.parseInt(configurationService.getPropertyValueAsString(INTERVAL_CONFIG_PARM));
087    
088                    FileMonitor dictionaryFileMonitor = new FileMonitor(reloadInterval);
089    
090            dictionaryUrlMonitor = new URLMonitor(reloadInterval);
091            dictionaryUrlMonitor.addListener(this);
092    
093                    // need to copy the configFileLocations list here because it gets
094                    // cleared out after processing by super
095                    List<String> configLocations = new ArrayList<String>(configFileLocations);
096    
097                    super.parseDataDictionaryConfigurationFiles(allowConcurrentValidation);
098                    for (String configLocation : configLocations) {
099                            Resource classFileResource = getFileResource(configLocation);
100                            try {
101                    if (classFileResource.getURI().toString().startsWith("jar:")) {
102                        LOG.debug("Monitoring dictionary file at URI: " + classFileResource.getURI().toString());
103                        dictionaryUrlMonitor.addURI(classFileResource.getURL());
104                    } else {
105                        String filePathClassesDir = classFileResource.getFile().getAbsolutePath();
106                        String sourceFilePath = StringUtils.replace(filePathClassesDir, classesDir, sourceDir);
107                        File dictionaryFile = new File(filePathClassesDir);
108                        if (dictionaryFile.exists()) {
109                            LOG.debug("Monitoring dictionary file: " + dictionaryFile.getName());
110                            dictionaryFileMonitor.addFile(dictionaryFile);
111                        }
112                    }
113                            }
114                            catch (Exception e) {
115                                    LOG.info("Exception in picking up dictionary file for monitoring:  " + e.getMessage(), e);
116                            }
117                    }
118    
119                    // add the dictionary as a listener for file changes
120                    dictionaryFileMonitor.addListener(this);
121            }
122    
123            /**
124             * Call back when a dictionary file is changed. Calls the spring bean reader
125             * to reload the file (which will override beans as necessary and destroy
126             * singletons) and runs the indexer
127             * 
128             * @see no.geosoft.cc.io.FileListener#fileChanged(java.io.File)
129             */
130            @Override
131            public void fileChanged(File file) {
132                    LOG.info("reloading dictionary configuration for " + file.getName());
133                    try {
134                            Resource resource = new FileSystemResource(file);
135                            xmlReader.loadBeanDefinitions(resource);
136    
137                UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor();
138                factoryPostProcessor.postProcessBeanFactory(ddBeans);
139    
140                            // re-index
141                            ddIndex.run();
142                    }
143                    catch (Exception e) {
144                            LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e);
145                    }
146            }
147    
148        public void urlContentChanged(final URL url) {
149            LOG.info("reloading dictionary configuration for " + url.toString());
150            try {
151                InputStream urlStream = url.openStream();
152                InputStreamResource resource = new InputStreamResource(urlStream);
153    
154                int originalValidationMode = xmlReader.getValidationMode();
155                xmlReader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD);
156                xmlReader.loadBeanDefinitions(resource);
157                xmlReader.setValidationMode(originalValidationMode);
158    
159                UifBeanFactoryPostProcessor factoryPostProcessor = new UifBeanFactoryPostProcessor();
160                factoryPostProcessor.postProcessBeanFactory(ddBeans);
161    
162                // re-index
163                ddIndex.run();
164            }
165            catch (Exception e) {
166                LOG.info("Exception in dictionary hot deploy: " + e.getMessage(), e);
167            }
168        }
169    }