d58d04659827979ae25a63583edbf4741ab09178
[so.git] / bpmn / MSOCoreBPMN / src / main / java / org / openecomp / mso / bpmn / core / PropertyConfiguration.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * OPENECOMP - MSO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.mso.bpmn.core;
22
23 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
24
25 import java.io.File;
26 import java.io.FileReader;
27 import java.io.IOException;
28 import java.nio.file.ClosedWatchServiceException;
29 import java.nio.file.FileSystems;
30 import java.nio.file.Path;
31 import java.nio.file.WatchEvent;
32 import java.nio.file.WatchKey;
33 import java.nio.file.WatchService;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Map.Entry;
41 import java.util.Properties;
42 import java.util.Timer;
43 import java.util.TimerTask;
44 import java.util.concurrent.ConcurrentHashMap;
45
46 import org.slf4j.MDC;
47
48 import org.openecomp.mso.logger.MessageEnum;
49 import org.openecomp.mso.logger.MsoLogger;
50
51 /**
52  * Loads the property configuration from file system and refreshes the
53  * properties when the property gets changed.
54  *
55  * WARNING: automatic refreshes might not work on network filesystems.
56  */
57 public class PropertyConfiguration {
58
59         /**
60          * The base name of the MSO BPMN properties file (mso.bpmn.properties).
61          */
62         public static final String MSO_BPMN_PROPERTIES = "mso.bpmn.properties";
63
64         /**
65          * The base name of the MSO BPMN URN-Mappings properties file (mso.bpmn.urn.properties).
66          */
67         public static final String MSO_BPMN_URN_PROPERTIES = "mso.bpmn.urn.properties";
68
69         /**
70          * The base name of the MSO Topology properties file (topology.properties).
71          */
72         public static final String MSO_TOPOLOGY_PROPERTIES = "topology.properties";
73         /**
74          * The name of the meta-property holding the time the properties were loaded
75          * from the file.
76          */
77         public static final String TIMESTAMP_PROPERTY = "mso.properties.timestamp";
78
79         private static final MsoLogger LOGGER = MsoLogger.getMsoLogger(MsoLogger.Catalog.BPEL);
80
81         private static final List<String> SUPPORTED_FILES =
82                 Arrays.asList(MSO_BPMN_PROPERTIES, MSO_BPMN_URN_PROPERTIES, MSO_TOPOLOGY_PROPERTIES);
83
84         private volatile String msoConfigPath = null;
85
86         private final ConcurrentHashMap<String, Map<String, String>> propFileCache =
87                 new ConcurrentHashMap<String, Map<String, String>>();
88
89         private final Object CACHELOCK = new Object();
90         private FileWatcherThread fileWatcherThread = null;
91
92         // The key is the file name
93         private Map<String, TimerTask> timerTaskMap = new HashMap<String, TimerTask>();
94
95         /**
96          * Singleton holder pattern eliminates locking when accessing the instance
97          * and still provides for lazy initialization.
98          */
99         private static class PropertyConfigurationInstanceHolder {
100                 private static PropertyConfiguration instance = new PropertyConfiguration();
101         }
102
103         /**
104          * Gets the one and only instance of this class.
105          */
106         public static PropertyConfiguration getInstance() {
107                 return PropertyConfigurationInstanceHolder.instance;
108         }
109
110         /**
111          * Returns the list of supported files.
112          */
113         public static List<String> supportedFiles() {
114                 return new ArrayList<String>(SUPPORTED_FILES);
115         }
116
117         /**
118          * Private Constructor.
119          */
120         private PropertyConfiguration() {
121                 startUp();
122         }
123
124         /**
125          * May be called to restart the PropertyConfiguration if it was previously shut down.
126          */
127         public synchronized void startUp() {
128                 msoConfigPath = System.getProperty("mso.config.path");
129
130                 if (msoConfigPath == null) {
131                         LOGGER.debug("mso.config.path JVM system property is not set");
132                         return;
133                 }
134
135                 try {
136                         Path directory = FileSystems.getDefault().getPath(msoConfigPath);
137                         WatchService watchService = FileSystems.getDefault().newWatchService();
138                         directory.register(watchService, ENTRY_MODIFY);
139
140                         LOGGER.info(MessageEnum.BPMN_GENERAL_INFO, "BPMN", "Starting FileWatcherThread");
141                         LOGGER.debug("Starting FileWatcherThread");
142                         fileWatcherThread = new FileWatcherThread(watchService);
143                         fileWatcherThread.start();
144                 } catch (Exception e) {
145                         LOGGER.debug("Error occurred while starting FileWatcherThread:", e);
146                         LOGGER.error(
147                                 MessageEnum.BPMN_GENERAL_EXCEPTION,
148                                 "BPMN",
149                                 "Property Configuration",
150                                 MsoLogger.ErrorCode.UnknownError,
151                                 "Error occurred while starting FileWatcherThread:" + e);
152                 }
153         }
154
155         /**
156          * May be called to shut down the PropertyConfiguration.  A shutDown followed
157          * by a startUp will reset the PropertyConfiguration to its initial state.
158          */
159         public synchronized void shutDown() {
160                 if (fileWatcherThread != null) {
161                         LOGGER.debug("Shutting down FileWatcherThread " + System.identityHashCode(fileWatcherThread));
162                         fileWatcherThread.shutdown();
163
164                         long waitInSeconds = 10;
165
166                         try {
167                                 fileWatcherThread.join(waitInSeconds * 1000);
168                         } catch (InterruptedException e) {
169                                 LOGGER.debug("FileWatcherThread " + System.identityHashCode(fileWatcherThread)
170                                         + " shutdown did not occur within " + waitInSeconds + " seconds");
171                         }
172
173                         LOGGER.debug("Finished shutting down FileWatcherThread " + System.identityHashCode(fileWatcherThread));
174                         fileWatcherThread = null;
175                 }
176
177                 clearCache();
178                 msoConfigPath = null;
179         }
180
181         public synchronized boolean isFileWatcherRunning() {
182                 return fileWatcherThread != null;
183         }
184
185         public void clearCache() {
186                 synchronized(CACHELOCK) {
187                         propFileCache.clear();
188                 }
189         }
190
191         public int cacheSize() {
192                 return propFileCache.size();
193         }
194
195         // TODO: throw IOException?
196         public Map<String, String> getProperties(String fileName) {
197                 Map<String, String> properties = propFileCache.get(fileName);
198
199                 if (properties == null) {
200                         if (!SUPPORTED_FILES.contains(fileName)) {
201                                 throw new IllegalArgumentException("Not a supported property file: " + fileName);
202                         }
203
204                         if (msoConfigPath == null) {
205                                 LOGGER.debug("mso.config.path JVM system property must be set to load " + fileName);
206
207                                 LOGGER.error(
208                                                 MessageEnum.BPMN_GENERAL_EXCEPTION,
209                                                 "BPMN",
210                                                 MDC.get(fileName),
211                                                 MsoLogger.ErrorCode.UnknownError,
212                                                 "mso.config.path JVM system property must be set to load " + fileName);
213
214                                 return null;
215                         }
216
217                         try {
218                                 properties = readProperties(new File(msoConfigPath, fileName));
219                         } catch (Exception e) {
220                                 LOGGER.debug("Error loading " + fileName);
221
222                                 LOGGER.error(
223                                                 MessageEnum.BPMN_GENERAL_EXCEPTION,
224                                                 "BPMN",
225                                                 MDC.get(fileName),
226                                                 MsoLogger.ErrorCode.UnknownError,
227                                                 "Error loading " + fileName, e);
228
229                                 return null;
230                         }
231                 }
232
233                 return Collections.unmodifiableMap(properties);
234         }
235
236         /**
237          * Reads properties from the specified file, updates the property file cache, and
238          * returns the properties in a map.
239          * @param file the file to read
240          * @param reload true if this is a reload event
241          * @return a map of properties
242          */
243         private Map<String, String> readProperties(File file) throws IOException {
244                 String fileName = file.getName();
245                 LOGGER.debug("Reading " + fileName);
246
247                 Map<String, String> properties = new HashMap<String, String>();
248                 Properties newProperties = new Properties();
249
250                 FileReader reader = null;
251                 try {
252                         reader = new FileReader(file);
253                         newProperties.load(reader);
254                 } finally {
255                         if (reader != null) {
256                                 try {
257                                         reader.close();
258                                         LOGGER.debug("Closed " + fileName);
259                                 } catch (Exception e) {
260                                         // Ignore
261                                 }
262                         }
263                 }
264
265                 for (Entry<Object, Object> entry : newProperties.entrySet()) {
266                         properties.put(entry.getKey().toString(), entry.getValue().toString());
267                 }
268
269                 properties.put(TIMESTAMP_PROPERTY, String.valueOf(System.currentTimeMillis()));
270
271                 synchronized(CACHELOCK) {
272                         propFileCache.put(fileName, properties);
273                 }
274
275                 return properties;
276         }
277
278         /**
279          * File watcher thread which monitors a directory for file modification.
280          */
281         private class FileWatcherThread extends Thread {
282                 private final WatchService watchService;
283                 private final Timer timer = new Timer("FileWatcherTimer");
284
285                 public FileWatcherThread(WatchService service) {
286                         this.watchService = service;
287                 }
288
289                 public void shutdown() {
290                         interrupt();
291                 }
292
293                 public void run() {
294                         LOGGER.info(MessageEnum.BPMN_GENERAL_INFO, "BPMN",
295                                 "FileWatcherThread started");
296
297                         LOGGER.debug("Started FileWatcherThread " + System.identityHashCode(fileWatcherThread));
298
299                         try {
300                                 WatchKey watchKey = null;
301
302                                 while (!isInterrupted()) {
303                                         try {
304                                                 if (watchKey != null) {
305                                                         watchKey.reset();
306                                                 }
307
308                                                 watchKey = watchService.take();
309
310                                                 for (WatchEvent<?> event : watchKey.pollEvents()) {
311                                                         @SuppressWarnings("unchecked")
312                                                         WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
313
314                                                         if ("EVENT_OVERFLOW".equals(pathEvent.kind())) {
315                                                                 LOGGER.debug("Ignored overflow event for " + msoConfigPath);
316                                                                 continue;
317                                                         }
318
319                                                         String fileName = pathEvent.context().getFileName().toString();
320
321                                                         if (!SUPPORTED_FILES.contains(fileName)) {
322                                                                 LOGGER.debug("Ignored modify event for " + fileName);
323                                                                 continue;
324                                                         }
325
326                                                         LOGGER.debug("Configuration file has changed: " + fileName);
327
328                                                         LOGGER.info(MessageEnum.BPMN_GENERAL_INFO, "BPMN",
329                                                                         "Configuation file has changed: " + fileName);
330
331                                                         // There's a potential problem here. The MODIFY event is
332                                                         // triggered as soon as somebody starts writing the file but
333                                                         // there's no obvious way to know when the write is done.  If we
334                                                         // read the file while the write is still in progress, then the
335                                                         // cache can really be messed up. As a workaround, we use a timer
336                                                         // to sleep for at least one second, and then we sleep for as long
337                                                         // as it takes for the file's lastModified time to stop changing.
338                                                         // The timer has another benefit: it consolidates multiple events
339                                                         // that we seem to receive when a file is modified.
340
341                                                         synchronized(timerTaskMap) {
342                                                                 TimerTask task = timerTaskMap.get(fileName);
343
344                                                                 if (task != null) {
345                                                                         task.cancel();
346                                                                 }
347
348                                                                 File file = new File(msoConfigPath, fileName);
349                                                                 task = new DelayTimerTask(timer, file, 1000);
350                                                                 timerTaskMap.put(fileName, task);
351                                                         }
352                                                 }
353                                         } catch (InterruptedException e) {
354                                                 break;
355                                         } catch (ClosedWatchServiceException e) {
356                                                 LOGGER.info(
357                                                                 MessageEnum.BPMN_GENERAL_INFO,
358                                                                 "BPMN",
359                                                                 "FileWatcherThread shut down because the watch service was closed");
360                                                 break;
361                                         } catch (Exception e) {
362                                                 LOGGER.error(
363                                                                 MessageEnum.BPMN_GENERAL_EXCEPTION,
364                                                                 "BPMN",
365                                                                 "Property Configuration",
366                                                                 MsoLogger.ErrorCode.UnknownError,
367                                                                 "FileWatcherThread caught unexpected " + e.getClass().getSimpleName(), e);
368                                         }
369
370                                 }
371                         } finally {
372                                 timer.cancel();
373
374                                 synchronized(timerTaskMap) {
375                                         timerTaskMap.clear();
376                                 }
377
378                                 try {
379                                         watchService.close();
380                                 } catch (IOException e) {
381                                         LOGGER.debug("FileWatcherThread caught " + e.getClass().getSimpleName()
382                                                 + " while closing the watch service");
383                                 }
384
385                                 LOGGER.info(MessageEnum.BPMN_GENERAL_INFO, "BPMN",
386                                         "FileWatcherThread stopped");
387                         }
388                 }
389         }
390
391         private class DelayTimerTask extends TimerTask {
392                 private final File file;
393                 private final long lastModifiedTime;
394                 private final Timer timer;
395
396                 public DelayTimerTask(Timer timer, File file, long delay) {
397                         this.timer = timer;
398                         this.file = file;
399                         this.lastModifiedTime = file.lastModified();
400                         timer.schedule(this, delay);
401                 }
402
403                 @Override
404                 public void run() {
405                         try {
406                                 long newLastModifiedTime = file.lastModified();
407
408                                 if (newLastModifiedTime == lastModifiedTime) {
409                                         try {
410                                                 readProperties(file);
411                                         } catch (Exception e) {
412                                                 LOGGER.error(
413                                                         MessageEnum.BPMN_GENERAL_EXCEPTION,
414                                                         "BPMN",
415                                                         "Property Configuration",
416                                                         MsoLogger.ErrorCode.UnknownError,
417                                                         "Unable to reload " + file, e);
418                                         }
419                                 } else {
420                                         LOGGER.debug("Delaying reload of " + file + " by 1 second");
421
422                                         synchronized(timerTaskMap) {
423                                                 TimerTask task = timerTaskMap.get(file.getName());
424
425                                                 if (task != null && task != this) {
426                                                         task.cancel();
427                                                 }
428
429                                                 task = new DelayTimerTask(timer, file, 1000);
430                                                 timerTaskMap.put(file.getName(), task);
431                                         }
432                                 }
433                         } finally {
434                                 synchronized(timerTaskMap) {
435                                         TimerTask task = timerTaskMap.get(file.getName());
436
437                                         if (task == this) {
438                                                 timerTaskMap.remove(file.getName());
439                                         }
440                                 }
441                         }
442                 }
443         }
444 }