001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
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, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package org.outerj.daisy.frontend.components.config.impl;
017:
018: import org.apache.avalon.framework.activity.Initializable;
019: import org.apache.avalon.framework.activity.Disposable;
020: import org.apache.avalon.framework.context.Contextualizable;
021: import org.apache.avalon.framework.context.Context;
022: import org.apache.avalon.framework.context.ContextException;
023: import org.apache.avalon.framework.service.Serviceable;
024: import org.apache.avalon.framework.service.ServiceManager;
025: import org.apache.avalon.framework.service.ServiceException;
026: import org.apache.avalon.framework.logger.LogEnabled;
027: import org.apache.avalon.framework.logger.Logger;
028: import org.apache.avalon.framework.configuration.Configuration;
029: import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
030: import org.apache.avalon.framework.thread.ThreadSafe;
031: import org.apache.excalibur.source.SourceResolver;
032: import org.apache.excalibur.source.Source;
033: import org.apache.excalibur.source.impl.FileSource;
034: import org.outerj.daisy.frontend.util.WikiDataDirHelper;
035: import org.outerj.daisy.frontend.components.config.ConfigurationManager;
036:
037: import java.io.File;
038: import java.io.IOException;
039: import java.io.FileFilter;
040: import java.util.*;
041: import java.util.concurrent.ConcurrentHashMap;
042: import java.util.concurrent.ScheduledThreadPoolExecutor;
043: import java.util.concurrent.TimeUnit;
044:
045: public class ConfigurationManagerImpl implements ConfigurationManager,
046: ThreadSafe, Contextualizable, Initializable, Serviceable,
047: LogEnabled, Disposable {
048: private Context context;
049: private ServiceManager serviceManager;
050: private Logger logger;
051:
052: private File webappConfDir;
053: private File datadirConfDir;
054: private File sitesDir;
055:
056: // The cached are read by many threads, but only updated by one thread
057: private Map<String, CachedConfig> webappConfs = new ConcurrentHashMap<String, CachedConfig>(
058: INITIAL_CACHE_SIZE, CACHE_LOAD_FACTOR,
059: CACHE_CONCURRENCY_LEVEL);
060: private Map<String, CachedConfig> datadirConfs = new ConcurrentHashMap<String, CachedConfig>(
061: INITIAL_CACHE_SIZE, CACHE_LOAD_FACTOR,
062: CACHE_CONCURRENCY_LEVEL);
063: private Map<String, Map<String, CachedConfig>> siteConfs = new ConcurrentHashMap<String, Map<String, CachedConfig>>(
064: INITIAL_CACHE_SIZE, CACHE_LOAD_FACTOR,
065: CACHE_CONCURRENCY_LEVEL);
066:
067: private static int INITIAL_CACHE_SIZE = 16;
068: private static float CACHE_LOAD_FACTOR = .75f;
069: private static int CACHE_CONCURRENCY_LEVEL = 1;
070:
071: private ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
072: 1);
073: private int delay = 10;
074:
075: private static final String CONFIG_FILE_EXT = ".xml";
076:
077: public void enableLogging(Logger logger) {
078: this .logger = logger;
079: }
080:
081: public void contextualize(Context context) throws ContextException {
082: this .context = context;
083: }
084:
085: public void service(ServiceManager serviceManager)
086: throws ServiceException {
087: this .serviceManager = serviceManager;
088: }
089:
090: public void initialize() throws Exception {
091: // Determine locations of conf directories
092:
093: // webapp conf
094: webappConfDir = resolve("context:/daisy/conf");
095:
096: // datadir conf
097: String wikiDataDir = WikiDataDirHelper.getWikiDataDir(context);
098: this .datadirConfDir = new File(wikiDataDir, "conf");
099: if (!datadirConfDir.exists())
100: logger.info("wiki data dir conf dir does not exist.");
101:
102: // sites dir
103: sitesDir = new File(wikiDataDir, "sites");
104:
105: // do initial read of configuration
106: scanConfiguration();
107:
108: // schedule thread for regular up-to-date check of configuration
109: executor.scheduleWithFixedDelay(new ConfigurationRefresher(),
110: delay, delay, TimeUnit.SECONDS);
111: }
112:
113: public void dispose() {
114: executor.shutdownNow();
115: }
116:
117: public Configuration getConfiguration(String path, boolean fallback) {
118: CachedConfig cached = datadirConfs.get(path);
119: if (cached == null && fallback)
120: cached = webappConfs.get(path);
121: return cached != null ? cached.configuration : null;
122: }
123:
124: public Configuration getConfiguration(String site, String path,
125: boolean fallback) {
126: Map<String, CachedConfig> siteConf = siteConfs.get(site);
127: CachedConfig cached = null;
128:
129: if (siteConf != null) {
130: cached = siteConf.get(path);
131: }
132:
133: if (cached == null && fallback) {
134: return getConfiguration(path, true);
135: }
136:
137: return cached != null ? cached.configuration : null;
138: }
139:
140: public Configuration getConfiguration(String site, String path) {
141: return getConfiguration(site, path, true);
142: }
143:
144: public Configuration getConfiguration(String path) {
145: return getConfiguration(path, true);
146: }
147:
148: private File resolve(String location) throws ServiceException,
149: IOException {
150: SourceResolver sourceResolver = null;
151: try {
152: sourceResolver = (SourceResolver) serviceManager
153: .lookup(SourceResolver.ROLE);
154: Source source = sourceResolver.resolveURI(location);
155: if (source instanceof FileSource) {
156: return ((FileSource) source).getFile();
157: } else {
158: throw new RuntimeException(
159: "Unexpected error: resolved location isn't a file. Location = "
160: + location);
161: }
162: } finally {
163: if (sourceResolver != null)
164: serviceManager.release(sourceResolver);
165: }
166: }
167:
168: private static class CachedConfig {
169: long lastModified;
170: Configuration configuration;
171: ConfigState state;
172: }
173:
174: private synchronized void scanConfiguration() {
175: logger.debug("Scanning configuration");
176: updateCache(webappConfDir, webappConfs);
177: updateCache(datadirConfDir, datadirConfs);
178:
179: List<Site> sites = findSitesWithConfs();
180:
181: // Delete sites from cache which do no longer exist
182: Iterator<String> currentSitesIt = siteConfs.keySet().iterator();
183: while (currentSitesIt.hasNext()) {
184: String siteName = currentSitesIt.next();
185: boolean found = false;
186: for (Site site : sites) {
187: if (site.siteName.equals(siteName)) {
188: found = true;
189: break;
190: }
191: }
192: if (!found) {
193: currentSitesIt.remove();
194: if (logger.isDebugEnabled())
195: logger
196: .debug("Configuration: detected removed site: "
197: + siteName);
198: }
199: }
200:
201: //
202: for (Site site : sites) {
203: Map<String, CachedConfig> cache = siteConfs
204: .get(site.siteName);
205: if (cache == null) {
206: cache = new ConcurrentHashMap<String, CachedConfig>(
207: INITIAL_CACHE_SIZE, CACHE_LOAD_FACTOR,
208: CACHE_CONCURRENCY_LEVEL);
209: siteConfs.put(site.siteName, cache);
210: }
211: updateCache(site.confDir, cache);
212: }
213:
214: logger.debug("Scanning configuration done.");
215: }
216:
217: private void updateCache(File directory,
218: Map<String, CachedConfig> cache) {
219: // Search all config files on disk
220: List<ConfigPath> configPaths = new ArrayList<ConfigPath>();
221: collectConfigFiles(directory, configPaths, directory);
222:
223: // Delete configs from cache which don't exist on disk anymore
224: Iterator<String> currentEntriesIt = cache.keySet().iterator();
225: while (currentEntriesIt.hasNext()) {
226: String path = currentEntriesIt.next();
227: boolean found = false;
228: for (ConfigPath configPath : configPaths) {
229: if (configPath.path.equals(path)) {
230: found = true;
231: break;
232: }
233: }
234: if (!found) {
235: currentEntriesIt.remove();
236: if (logger.isDebugEnabled())
237: logger
238: .debug("Configuration: detected removed config "
239: + path
240: + " in "
241: + directory.getAbsolutePath());
242: }
243: }
244:
245: // Add/update configs
246: for (ConfigPath configPath : configPaths) {
247: CachedConfig cachedConfig = cache.get(configPath.path);
248: if (cachedConfig == null
249: || cachedConfig.lastModified != configPath.file
250: .lastModified()) {
251: if (logger.isDebugEnabled())
252: logger
253: .debug("Configuration: detected updated or added config "
254: + configPath.path
255: + " in "
256: + directory.getAbsolutePath());
257: long lastModified = configPath.file.lastModified();
258: Configuration configuration = parseConfiguration(configPath.file);
259: cachedConfig = new CachedConfig();
260: cachedConfig.lastModified = lastModified;
261: cachedConfig.configuration = configuration;
262: cachedConfig.state = configuration == null ? ConfigState.ERROR
263: : ConfigState.OK;
264: cache.put(configPath.path, cachedConfig);
265: } else {
266: if (logger.isDebugEnabled())
267: logger
268: .debug("Configuration: detected deleted config "
269: + configPath.path
270: + " in "
271: + directory.getAbsolutePath());
272: }
273: }
274: }
275:
276: private void collectConfigFiles(File dir,
277: List<ConfigPath> configPaths, File rootDir) {
278: File[] files = dir.listFiles(CONFIG_FILE_FILTER);
279: if (files != null) {
280: for (File file : files) {
281: if (file.isDirectory()) {
282: collectConfigFiles(file, configPaths, rootDir);
283: } else {
284: String path = getConfigPathForFile(file, rootDir);
285: if (path.endsWith(CONFIG_FILE_EXT)) { // should be the case
286: path = path.substring(0, path.length()
287: - CONFIG_FILE_EXT.length());
288: }
289: configPaths.add(new ConfigPath(path, file));
290: }
291: }
292: }
293: }
294:
295: private String getConfigPathForFile(File file, File reference) {
296: File parent = file.getParentFile();
297: if (parent != null && !parent.equals(reference)) {
298: return getConfigPathForFile(parent, reference) + "/"
299: + file.getName();
300: } else if (parent != null) {
301: return file.getName();
302: } else {
303: return "";
304: }
305: }
306:
307: private static class ConfigPath {
308: String path;
309: File file;
310:
311: public ConfigPath(String path, File file) {
312: this .path = path;
313: this .file = file;
314: }
315: }
316:
317: private static final FileFilter CONFIG_FILE_FILTER = new FileFilter() {
318: public boolean accept(File pathname) {
319: if (pathname.isDirectory()) {
320: String name = pathname.getName();
321: return !name.startsWith(".") && !name.equals(".svn")
322: && !name.equals("CVS");
323: } else {
324: return pathname.getName().endsWith(CONFIG_FILE_EXT);
325: }
326: }
327: };
328:
329: private static final FileFilter DIR_FILE_FILTER = new FileFilter() {
330: public boolean accept(File pathname) {
331: return pathname.isDirectory();
332: }
333: };
334:
335: private Configuration parseConfiguration(File file) {
336: Configuration config = null;
337: try {
338: config = new DefaultConfigurationBuilder()
339: .buildFromFile(file);
340: } catch (Throwable e) {
341: logger.error("Error reading configuration file "
342: + file.getAbsolutePath(), e);
343: }
344: return config;
345: }
346:
347: enum ConfigState {
348: OK, ERROR
349: }
350:
351: private static class Site {
352: String siteName;
353: File confDir;
354:
355: public Site(String siteName, File confDir) {
356: this .siteName = siteName;
357: this .confDir = confDir;
358: }
359: }
360:
361: private List<Site> findSitesWithConfs() {
362: List<Site> sites = new ArrayList<Site>();
363: File[] siteDirs = sitesDir.listFiles(DIR_FILE_FILTER);
364: if (siteDirs != null) {
365: for (File siteDir : siteDirs) {
366: // if it is a site-defining directory
367: if (new File(siteDir, "siteconf.xml").exists()) {
368: File confDir = new File(siteDir, "conf");
369: if (confDir.exists()) {
370: sites.add(new Site(siteDir.getName(), confDir));
371: }
372: }
373: }
374: }
375: return sites;
376: }
377:
378: private class ConfigurationRefresher implements Runnable {
379: public void run() {
380: scanConfiguration();
381: }
382: }
383: }
|