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}