001: /*
002: * All content copyright (c) 2003-2006 Terracotta, Inc., except as may otherwise be noted in a separate copyright
003: * notice. All rights reserved.
004: */
005: package com.tc.config.schema.setup;
006:
007: import org.apache.commons.io.CopyUtils;
008: import org.apache.xmlbeans.XmlError;
009: import org.apache.xmlbeans.XmlException;
010: import org.apache.xmlbeans.XmlInteger;
011: import org.xml.sax.SAXException;
012:
013: import com.tc.config.schema.beanfactory.BeanWithErrors;
014: import com.tc.config.schema.beanfactory.ConfigBeanFactory;
015: import com.tc.config.schema.defaults.DefaultValueProvider;
016: import com.tc.config.schema.defaults.FromSchemaDefaultValueProvider;
017: import com.tc.config.schema.dynamic.ParameterSubstituter;
018: import com.tc.config.schema.repository.ApplicationsRepository;
019: import com.tc.config.schema.repository.MutableBeanRepository;
020: import com.tc.config.schema.setup.sources.ConfigurationSource;
021: import com.tc.config.schema.setup.sources.FileConfigurationSource;
022: import com.tc.config.schema.setup.sources.ResourceConfigurationSource;
023: import com.tc.config.schema.setup.sources.ServerConfigurationSource;
024: import com.tc.config.schema.setup.sources.URLConfigurationSource;
025: import com.tc.logging.CustomerLogging;
026: import com.tc.logging.TCLogger;
027: import com.tc.logging.TCLogging;
028: import com.tc.util.Assert;
029: import com.terracottatech.config.Server;
030: import com.terracottatech.config.Servers;
031: import com.terracottatech.config.TcConfigDocument;
032: import com.terracottatech.config.TcConfigDocument.TcConfig;
033:
034: import java.io.ByteArrayInputStream;
035: import java.io.ByteArrayOutputStream;
036: import java.io.File;
037: import java.io.IOException;
038: import java.io.InputStream;
039: import java.io.StringWriter;
040: import java.util.regex.Matcher;
041: import java.util.regex.Pattern;
042:
043: import javax.xml.parsers.ParserConfigurationException;
044:
045: /**
046: * A {@link ConfigurationCreator} that works off XML files, using the standard config-spec model.
047: */
048: public class StandardXMLFileConfigurationCreator implements
049: ConfigurationCreator {
050:
051: private static final TCLogger consoleLogger = CustomerLogging
052: .getConsoleLogger();
053:
054: private static final long GET_CONFIGURATION_TOTAL_TIMEOUT = 5 * 60 * 1000; // five
055: // minutes
056: private static final long GET_CONFIGURATION_ONE_SOURCE_TIMEOUT = 30 * 1000; // thirty
057: // seconds
058: private static final long MIN_RETRY_TIMEOUT = 5 * 1000; // five
059: // seconds
060:
061: protected final String configurationSpec;
062: protected final File defaultDirectory;
063: protected final ConfigBeanFactory beanFactory;
064:
065: private TCLogger logger;
066: private boolean loadedFromTrustedSource;
067: private String configDescription;
068: private File directoryLoadedFrom;
069:
070: public StandardXMLFileConfigurationCreator(
071: String configurationSpec, File defaultDirectory,
072: ConfigBeanFactory beanFactory) {
073: this (TCLogging
074: .getLogger(StandardXMLFileConfigurationCreator.class),
075: configurationSpec, defaultDirectory, beanFactory);
076: }
077:
078: public StandardXMLFileConfigurationCreator(TCLogger logger,
079: String configurationSpec, File defaultDirectory,
080: ConfigBeanFactory beanFactory) {
081: Assert.assertNotBlank(configurationSpec);
082: Assert.assertNotNull(defaultDirectory);
083: Assert.assertNotNull(beanFactory);
084:
085: this .logger = logger;
086: this .configurationSpec = configurationSpec;
087: this .defaultDirectory = defaultDirectory;
088: this .beanFactory = beanFactory;
089: this .configDescription = null;
090: }
091:
092: private static final Pattern SERVER_PATTERN = Pattern.compile(
093: "(.*):(.*)", Pattern.CASE_INSENSITIVE);
094:
095: private static final Pattern RESOURCE_PATTERN = Pattern.compile(
096: "resource://(.*)", Pattern.CASE_INSENSITIVE);
097:
098: // We require more than one character before the colon so that we don't mistake Windows-style directory paths as URLs.
099: private static final Pattern URL_PATTERN = Pattern
100: .compile("[A-Za-z][A-Za-z]+://.*");
101:
102: private ConfigurationSource[] createConfigurationSources()
103: throws ConfigurationSetupException {
104: String[] components = configurationSpec.split(",");
105: ConfigurationSource[] out = new ConfigurationSource[components.length];
106:
107: for (int i = 0; i < components.length; ++i) {
108: String this Component = components[i];
109: ConfigurationSource source = null;
110:
111: if (source == null)
112: source = attemptToCreateServerSource(this Component);
113: if (source == null)
114: source = attemptToCreateResourceSource(this Component);
115: if (source == null)
116: source = attemptToCreateURLSource(this Component);
117: if (source == null)
118: source = attemptToCreateFileSource(this Component);
119:
120: if (source == null) {
121: // formatting
122: throw new ConfigurationSetupException(
123: "The location '"
124: + this Component
125: + "' is not in any recognized format -- it doesn't "
126: + "seem to be a server, resource, URL, or file.");
127: }
128:
129: out[i] = source;
130: }
131:
132: return out;
133: }
134:
135: private ConfigurationSource attemptToCreateServerSource(String text) {
136: Matcher matcher = SERVER_PATTERN.matcher(text);
137: if (matcher.matches()) {
138: String host = matcher.group(1);
139: String portText = matcher.group(2);
140:
141: try {
142: return new ServerConfigurationSource(host, Integer
143: .parseInt(portText));
144: } catch (Exception e) {/**/
145: }
146: }
147: return null;
148: }
149:
150: private ConfigurationSource attemptToCreateResourceSource(
151: String text) {
152: Matcher matcher = RESOURCE_PATTERN.matcher(text);
153: if (matcher.matches())
154: return new ResourceConfigurationSource(matcher.group(1),
155: getClass());
156: else
157: return null;
158: }
159:
160: private ConfigurationSource attemptToCreateFileSource(String text) {
161: return new FileConfigurationSource(text, defaultDirectory);
162: }
163:
164: private ConfigurationSource attemptToCreateURLSource(String text) {
165: Matcher matcher = URL_PATTERN.matcher(text);
166: if (matcher.matches())
167: return new URLConfigurationSource(text);
168: else
169: return null;
170: }
171:
172: public void createConfigurationIntoRepositories(
173: MutableBeanRepository l1BeanRepository,
174: MutableBeanRepository l2sBeanRepository,
175: MutableBeanRepository systemBeanRepository,
176: ApplicationsRepository applicationsRepository)
177: throws ConfigurationSetupException {
178: Assert.assertNotNull(l1BeanRepository);
179: Assert.assertNotNull(l2sBeanRepository);
180: Assert.assertNotNull(systemBeanRepository);
181: Assert.assertNotNull(applicationsRepository);
182:
183: ConfigurationSource[] sources = createConfigurationSources();
184: long startTime = System.currentTimeMillis();
185: ConfigurationSource[] remainingSources = new ConfigurationSource[sources.length];
186: ConfigurationSource loadedSource = null;
187: System.arraycopy(sources, 0, remainingSources, 0,
188: sources.length);
189: long lastLoopStartTime = 0;
190: int iteration = 0;
191: InputStream out = null;
192: boolean trustedSource = false;
193: String descrip = null;
194:
195: while (iteration == 0
196: || (System.currentTimeMillis() - startTime < GET_CONFIGURATION_TOTAL_TIMEOUT)) {
197: sleepIfNecessaryToAvoidPoundingSources(lastLoopStartTime);
198: lastLoopStartTime = System.currentTimeMillis();
199:
200: for (int i = 0; i < remainingSources.length; ++i) {
201: if (remainingSources[i] == null)
202: continue;
203: out = trySource(remainingSources, i);
204:
205: if (out != null) {
206: loadedSource = remainingSources[i];
207: trustedSource = loadedSource.isTrusted();
208: descrip = loadedSource.toString();
209: break;
210: }
211: }
212:
213: if (out != null)
214: break;
215:
216: ++iteration;
217:
218: boolean haveSources = false;
219: for (int i = 0; i < remainingSources.length; ++i)
220: haveSources = haveSources
221: || remainingSources[i] != null;
222: if (!haveSources) {
223: // All sources have failed; bail out.
224: break;
225: }
226: }
227:
228: if (out == null)
229: configurationFetchFailed(sources, startTime);
230:
231: loadConfigurationData(out, trustedSource, descrip,
232: l1BeanRepository, l2sBeanRepository,
233: systemBeanRepository, applicationsRepository);
234: consoleLogger.info("Configuration loaded from the " + descrip
235: + ".");
236: }
237:
238: private void configurationFetchFailed(
239: ConfigurationSource[] sources, long startTime)
240: throws ConfigurationSetupException {
241: String text = "Could not fetch configuration data from ";
242: if (sources.length > 1)
243: text += "" + sources.length
244: + " different configuration sources";
245: else
246: text += "the " + sources[0];
247: text += ". ";
248:
249: if (sources.length > 1) {
250: text += " The sources we tried are: ";
251: for (int i = 0; i < sources.length; ++i) {
252: if (i > 0)
253: text += ", ";
254: if (i == sources.length - 1)
255: text += "and ";
256: text += "the " + sources[i].toString();
257: }
258: text += ". ";
259: }
260:
261: if (System.currentTimeMillis() - startTime >= GET_CONFIGURATION_TOTAL_TIMEOUT) {
262: text += " Fetch attempt duration: "
263: + ((System.currentTimeMillis() - startTime) / 1000)
264: + " seconds.";
265: }
266:
267: text += "\n\nTo correct this problem specify a valid configuration location using the ";
268: text += "-f/--config command-line options.";
269:
270: consoleLogger.error(text);
271: throw new ConfigurationSetupException(text);
272: }
273:
274: private InputStream trySource(
275: ConfigurationSource[] remainingSources, int i) {
276: InputStream out = null;
277:
278: try {
279: logger.info("Attempting to load configuration from the "
280: + remainingSources[i] + "...");
281: out = remainingSources[i]
282: .getInputStream(GET_CONFIGURATION_ONE_SOURCE_TIMEOUT);
283: directoryLoadedFrom = remainingSources[i]
284: .directoryLoadedFrom();
285: } catch (ConfigurationSetupException cse) {
286: String text = "We couldn't load configuration data from the "
287: + remainingSources[i];
288: text += "; this error is permanent, so this source will not be retried.";
289:
290: if (remainingSources.length > 1)
291: text += " Skipping this source and going to the next one.";
292:
293: text += " (Error: " + cse.getLocalizedMessage() + ".)";
294:
295: consoleLogger.warn(text);
296:
297: remainingSources[i] = null;
298: } catch (IOException ioe) {
299: String text = "We couldn't load configuration data from the "
300: + remainingSources[i];
301:
302: if (remainingSources.length > 1) {
303: text += "; this error is temporary, so this source will be retried later if configuration can't be loaded elsewhere. ";
304: text += "Skipping this source and going to the next one.";
305: } else {
306: text += "; retrying.";
307: }
308:
309: text += " (Error: " + ioe.getLocalizedMessage() + ".)";
310: consoleLogger.warn(text);
311: }
312:
313: return out;
314: }
315:
316: private void sleepIfNecessaryToAvoidPoundingSources(
317: long lastLoopStartTime) {
318: long delay = MIN_RETRY_TIMEOUT
319: - (System.currentTimeMillis() - lastLoopStartTime);
320: if (delay > 0) {
321: try {
322: logger
323: .info("Waiting "
324: + delay
325: + " ms until we try to get configuration data again...");
326: Thread.sleep(delay);
327: } catch (InterruptedException ie) {
328: // whatever
329: }
330: }
331: }
332:
333: private void logCopyOfConfig(InputStream in, String descrip)
334: throws IOException {
335: StringWriter sw = new StringWriter();
336: CopyUtils.copy(in, sw);
337:
338: logger.info("Successfully loaded configuration from the "
339: + descrip + ". Config is:\n\n" + sw.toString());
340: }
341:
342: private void loadConfigurationData(InputStream in,
343: boolean trustedSource, String descrip,
344: MutableBeanRepository clientBeanRepository,
345: MutableBeanRepository serversBeanRepository,
346: MutableBeanRepository systemBeanRepository,
347: ApplicationsRepository applicationsRepository)
348: throws ConfigurationSetupException {
349: try {
350: loadedFromTrustedSource = trustedSource;
351: configDescription = descrip;
352:
353: ByteArrayOutputStream dataCopy = new ByteArrayOutputStream();
354: CopyUtils.copy(in, dataCopy);
355:
356: logCopyOfConfig(new ByteArrayInputStream(dataCopy
357: .toByteArray()), descrip);
358:
359: InputStream copyIn = new ByteArrayInputStream(dataCopy
360: .toByteArray());
361: BeanWithErrors beanWithErrors = beanFactory.createBean(
362: copyIn, descrip);
363:
364: if (beanWithErrors.errors() != null
365: && beanWithErrors.errors().length > 0) {
366: logger
367: .debug("Configuration didn't parse; it had "
368: + beanWithErrors.errors().length
369: + " error(s).");
370:
371: StringBuffer buf = new StringBuffer();
372: for (int i = 0; i < beanWithErrors.errors().length; ++i) {
373: XmlError error = beanWithErrors.errors()[i];
374: buf.append(" [" + i + "]: Line " + error.getLine()
375: + ", column " + error.getColumn() + ": "
376: + error.getMessage() + "\n");
377: }
378:
379: throw new ConfigurationSetupException(
380: "The configuration data in the " + descrip
381: + " does not obey the "
382: + "Terracotta schema:\n" + buf);
383: } else {
384: logger.debug("Configuration is valid.");
385: }
386:
387: TcConfig config = ((TcConfigDocument) beanWithErrors.bean())
388: .getTcConfig();
389: Servers servers = config.getServers();
390: if (servers != null) {
391: Server server;
392: for (int i = 0; i < servers.sizeOfServerArray(); i++) {
393: server = servers.getServerArray(i);
394: // CDV-166: per our documentation in the schema itself, host is supposed to default to '%i' and name is
395: // supposed to default to 'host:dso-port'
396: if (!server.isSetHost()
397: || server.getHost().trim().length() == 0) {
398: server.setHost("%i");
399: }
400: if (!server.isSetName()
401: || server.getName().trim().length() == 0) {
402: int dsoPort = server.getDsoPort();
403: if (dsoPort == 0) {
404: // Find the default value, if we can
405: final DefaultValueProvider defaultValueProvider = new FromSchemaDefaultValueProvider();
406: if (defaultValueProvider.hasDefault(server
407: .schemaType(), "dso-port")) {
408: final XmlInteger defaultValue = (XmlInteger) defaultValueProvider
409: .defaultFor(
410: server.schemaType(),
411: "dso-port");
412: dsoPort = defaultValue
413: .getBigIntegerValue()
414: .intValue();
415: }
416: }
417: server.setName(server.getHost()
418: + (dsoPort > 0 ? ":" + dsoPort : ""));
419: }
420: // CDV-77: add parameter expansion to the <server> attributes ('host' and 'name')
421: server.setHost(ParameterSubstituter
422: .substitute(server.getHost()));
423: server.setName(ParameterSubstituter
424: .substitute(server.getName()));
425: }
426: }
427:
428: clientBeanRepository.setBean(config.getClients(), descrip);
429: serversBeanRepository.setBean(config.getServers(), descrip);
430: systemBeanRepository.setBean(config.getSystem(), descrip);
431:
432: if (config.isSetApplication()) {
433: applicationsRepository
434: .repositoryFor(
435: TVSConfigurationSetupManagerFactory.DEFAULT_APPLICATION_NAME)
436: .setBean(config.getApplication(), descrip);
437: }
438: } catch (IOException ioe) {
439: throw new ConfigurationSetupException(
440: "We were unable to read configuration data from the "
441: + descrip + ": "
442: + ioe.getLocalizedMessage(), ioe);
443: } catch (SAXException saxe) {
444: throw new ConfigurationSetupException(
445: "The configuration data in the " + descrip
446: + " is not well-formed XML: "
447: + saxe.getLocalizedMessage(), saxe);
448: } catch (ParserConfigurationException pce) {
449: throw Assert
450: .failure(
451: "The XML parser can't be configured correctly; this should not happen.",
452: pce);
453: } catch (XmlException xmle) {
454: throw new ConfigurationSetupException(
455: "The configuration data in the " + descrip
456: + " does not obey the "
457: + "Terracotta schema: "
458: + xmle.getLocalizedMessage(), xmle);
459: }
460: }
461:
462: public File directoryConfigurationLoadedFrom() {
463: return directoryLoadedFrom;
464: }
465:
466: public boolean loadedFromTrustedSource() {
467: return loadedFromTrustedSource;
468: }
469:
470: public String describeSources() {
471: if (configDescription == null) {
472: return "The configuration specified by '"
473: + configurationSpec + "'";
474: } else {
475: return configDescription;
476: }
477: }
478:
479: }
|