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}