View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.devtools.datadictionary;
17  
18  
19  import no.geosoft.cc.io.FileListener;
20  import no.geosoft.cc.io.FileMonitor;
21  import org.apache.commons.collections.ListUtils;
22  import org.apache.commons.lang.StringUtils;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.kuali.rice.core.api.CoreApiServiceLocator;
26  import org.kuali.rice.core.api.config.property.ConfigurationService;
27  import org.kuali.rice.krad.datadictionary.DataDictionary;
28  import org.kuali.rice.krad.util.KRADConstants;
29  import org.springframework.beans.BeansException;
30  import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
31  import org.springframework.context.ApplicationContext;
32  import org.springframework.context.ApplicationContextAware;
33  import org.springframework.context.ApplicationListener;
34  import org.springframework.context.ConfigurableApplicationContext;
35  import org.springframework.context.event.ContextClosedEvent;
36  import org.springframework.core.io.FileSystemResource;
37  import org.springframework.core.io.InputStreamResource;
38  import org.springframework.core.io.Resource;
39  
40  import java.io.File;
41  import java.io.InputStream;
42  import java.net.URL;
43  import java.util.Arrays;
44  import java.util.HashMap;
45  import java.util.List;
46  import java.util.Map;
47  
48  /**
49   * Extends the DataDictionary to add reloading of changed dictionary files
50   * without a restart of the web container
51   *
52   * <p>
53   * To use modify the "dataDictionaryService" spring definition
54   * (KRADSpringBeans.xml) and change the constructor arg bean class from
55   * "org.kuali.rice.krad.datadictionary.DataDictionary" to
56   * "ReloadingDataDictionary"
57   * </p>
58   *
59   * <p>
60   * NOTE: For Development Purposes Only!
61   * </p>
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  public class ReloadingDataDictionary extends DataDictionary implements FileListener, URLMonitor.URLContentChangedListener, ApplicationContextAware {
66      private static final Log LOG = LogFactory.getLog(DataDictionary.class);
67  
68      private static final String CLASS_DIR_CONFIG_PARM = "reload.data.dictionary.classes.dir";
69      private static final String SOURCE_DIR_CONFIG_PARM = "reload.data.dictionary.source.dir";
70      private static final String INTERVAL_CONFIG_PARM = "reload.data.dictionary.interval";
71  
72      private Map<String, String> fileToNamespaceMapping;
73      private Map<String, String> urlToNamespaceMapping;
74      
75      private FileMonitor dictionaryFileMonitor;
76      private URLMonitor dictionaryUrlMonitor;
77  
78      public ReloadingDataDictionary() {
79          super();
80      }
81  
82      /**
83       * After dictionary has been loaded, determine the source files and add them
84       * to the monitor
85       *
86       * @see org.kuali.rice.krad.datadictionary.DataDictionary#parseDataDictionaryConfigurationFiles(boolean)
87       */
88      @Override
89      public void parseDataDictionaryConfigurationFiles(boolean allowConcurrentValidation) {
90          ConfigurationService configurationService = CoreApiServiceLocator.getKualiConfigurationService();
91  
92          // class directory part of the path that should be replaced
93          String classesDir = configurationService.getPropertyValueAsString(CLASS_DIR_CONFIG_PARM);
94  
95          // source directory where dictionary files are found
96          String sourceDir = configurationService.getPropertyValueAsString(SOURCE_DIR_CONFIG_PARM);
97  
98          // interval to poll for changes in milliseconds
99          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 }