001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.source.helpers;
018:
019: import java.io.File;
020: import java.io.FileOutputStream;
021: import java.io.IOException;
022: import java.io.OutputStreamWriter;
023: import java.io.UnsupportedEncodingException;
024: import java.io.Writer;
025: import java.net.MalformedURLException;
026: import java.util.Collections;
027: import java.util.ConcurrentModificationException;
028: import java.util.HashMap;
029: import java.util.Iterator;
030: import java.util.Map;
031:
032: import org.apache.avalon.framework.CascadingException;
033: import org.apache.avalon.framework.CascadingRuntimeException;
034: import org.apache.avalon.framework.activity.Disposable;
035: import org.apache.avalon.framework.component.WrapperComponentManager;
036: import org.apache.avalon.framework.configuration.Configurable;
037: import org.apache.avalon.framework.configuration.Configuration;
038: import org.apache.avalon.framework.configuration.ConfigurationException;
039: import org.apache.avalon.framework.configuration.SAXConfigurationHandler;
040: import org.apache.avalon.framework.context.Context;
041: import org.apache.avalon.framework.context.ContextException;
042: import org.apache.avalon.framework.context.Contextualizable;
043: import org.apache.avalon.framework.logger.AbstractLogEnabled;
044: import org.apache.avalon.framework.parameters.Parameters;
045: import org.apache.avalon.framework.service.ServiceException;
046: import org.apache.avalon.framework.service.ServiceManager;
047: import org.apache.avalon.framework.service.Serviceable;
048: import org.apache.avalon.framework.thread.ThreadSafe;
049: import org.apache.excalibur.source.Source;
050: import org.apache.excalibur.source.SourceException;
051: import org.apache.excalibur.source.SourceResolver;
052:
053: import org.apache.cocoon.Constants;
054: import org.apache.cocoon.Processor;
055: import org.apache.cocoon.components.CocoonComponentManager;
056: import org.apache.cocoon.components.source.SourceUtil;
057: import org.apache.cocoon.components.thread.RunnableManager;
058: import org.apache.cocoon.environment.background.BackgroundEnvironment;
059: import org.apache.cocoon.util.NetUtils;
060:
061: /**
062: * Default implementation of the refresher.
063: *
064: * @since 2.1.1
065: * @version $Id: DelaySourceRefresher.java 485495 2006-12-11 04:44:23Z crossley $
066: */
067: public class DelaySourceRefresher extends AbstractLogEnabled implements
068: Contextualizable, Serviceable, Configurable, Disposable,
069: ThreadSafe, SourceRefresher {
070:
071: private static final String PARAM_WRITE_FILE = "write-file";
072:
073: private static final String DEFAULT_WRITE_FILE = "refresher-targets.xml";
074:
075: private static final String TAGNAME_TARGET = "target";
076: private static final String ATTR_KEY = "key";
077: private static final String ATTR_URI = "uri";
078: private static final String ATTR_INTERVAL = "interval";
079:
080: protected Context context;
081:
082: // service dependencies
083: protected ServiceManager manager;
084: protected SourceResolver resolver;
085: protected RunnableManager runnable;
086:
087: // the scheduled targets to be persisted and recovered upon restart
088: protected Map entries = Collections.synchronizedMap(new HashMap());
089:
090: // the cocoon working directory
091: protected File workDir;
092:
093: /** The source to persist refresher entries into */
094: protected File configFile;
095:
096: // whether anything changed to the entries since last persisting them
097: protected volatile boolean changed;
098:
099: protected ConfigurationTask configurationTask;
100:
101: // ---------------------------------------------------- Lifecycle
102:
103: /* (non-Javadoc)
104: * @see Contextualizable#contextualize(Context)
105: */
106: public void contextualize(Context context) throws ContextException {
107: this .context = context;
108: this .workDir = (File) context.get(Constants.CONTEXT_WORK_DIR);
109: }
110:
111: /* (non-Javadoc)
112: * @see Serviceable#service(ServiceManager)
113: */
114: public void service(ServiceManager manager) throws ServiceException {
115: this .manager = manager;
116: this .resolver = (SourceResolver) this .manager
117: .lookup(SourceResolver.ROLE);
118: this .runnable = (RunnableManager) this .manager
119: .lookup(RunnableManager.ROLE);
120: }
121:
122: public void configure(Configuration configuration)
123: throws ConfigurationException {
124: Parameters parameters = Parameters
125: .fromConfiguration(configuration);
126: long interval = parameters.getParameterAsLong("interval", 0);
127: if (interval > 0) {
128: String fileName = parameters.getParameter(PARAM_WRITE_FILE,
129: DEFAULT_WRITE_FILE);
130: this .configFile = new File(this .workDir, fileName);
131: if (this .configFile.exists() && !this .configFile.canWrite()) {
132: throw new ConfigurationException(
133: "Parameter 'write-source' resolves to not modifiable file: "
134: + this .configFile);
135: }
136: if (!this .configFile.getParentFile().exists()
137: && !this .configFile.getParentFile().mkdirs()) {
138: throw new ConfigurationException(
139: "Can not create parent directory for: "
140: + this .configFile);
141: }
142: if (getLogger().isDebugEnabled()) {
143: getLogger().debug(
144: "Write source location: " + this .configFile);
145: }
146:
147: setupRefreshJobs(readRefreshJobConfiguration());
148: startConfigurationTask(interval);
149: } else {
150: if (getLogger().isInfoEnabled()) {
151: getLogger().info("Not writing update targets to file.");
152: }
153: }
154:
155: // Setup any in-line configured tasks
156: setupRefreshJobs(configuration);
157: }
158:
159: /* (non-Javadoc)
160: * @see Disposable#dispose()
161: */
162: public void dispose() {
163: stopConfigurationTask();
164: if (this .runnable != null) {
165: this .manager.release(this .runnable);
166: this .runnable = null;
167: }
168: if (this .resolver != null) {
169: this .manager.release(this .resolver);
170: this .resolver = null;
171: }
172: this .manager = null;
173: }
174:
175: // ---------------------------------------------------- SourceRefresher implementation
176:
177: /* (non-Javadoc)
178: * @see SourceRefresher#refresh
179: */
180: public void refresh(String name, String uri, Parameters parameters)
181: throws SourceException {
182: final long interval = parameters.getParameterAsLong(
183: PARAM_CACHE_INTERVAL, -1);
184: if (uri != null && interval > 0) {
185: addRefreshSource(name, uri, interval, interval);
186: } else {
187: removeRefreshSource(name);
188: }
189: }
190:
191: protected void addRefreshSource(String key, String uri, long delay,
192: long interval) {
193: RefresherTask task = (RefresherTask) this .entries.get(key);
194: if (task == null) {
195: // New source added.
196: task = new RefresherTask(key, uri, interval);
197: task.enableLogging(getLogger());
198: this .entries.put(key, task);
199: this .runnable.execute(task, interval, interval);
200: this .changed = true;
201: } else if (task.interval != interval) {
202: // Existing source refresh interval updated.
203: task.update(uri, interval);
204: this .runnable.remove(task);
205: this .runnable.execute(task, interval, interval);
206: this .changed = true;
207: } else {
208: // No change.
209: }
210: }
211:
212: protected void removeRefreshSource(String key) {
213: RefresherTask task = (RefresherTask) this .entries.get(key);
214: if (task != null) {
215: this .entries.remove(key);
216: this .runnable.remove(task);
217: this .changed = true;
218: }
219: }
220:
221: // ---------------------------------------------------- Implementation
222:
223: /**
224: *
225: */
226: private Configuration readRefreshJobConfiguration() {
227: Source source = null;
228: SAXConfigurationHandler b = new SAXConfigurationHandler();
229: try {
230: if (this .configFile.exists()) {
231: source = this .resolver.resolveURI(this .configFile
232: .toURL().toString());
233: SourceUtil.toSAX(this .manager, source, source
234: .getMimeType(), b);
235: }
236: } catch (Exception ignore) {
237: getLogger().warn(
238: "Unable to read configuration from "
239: + this .configFile);
240: } finally {
241: if (source != null) {
242: this .resolver.release(source);
243: }
244: }
245: return b.getConfiguration();
246: }
247:
248: /**
249: * @param conf
250: */
251: private void setupRefreshJobs(final Configuration conf) {
252: if (conf != null) {
253: final Configuration[] children = conf
254: .getChildren(TAGNAME_TARGET);
255: if (children != null) {
256: for (int i = 0; i < children.length; i++) {
257: try {
258: setupSingleRefreshJob(children[i]);
259: } catch (CascadingException ignore) {
260: if (getLogger().isDebugEnabled()) {
261: getLogger()
262: .debug(
263: "Setting up refresh job, ignoring exception:",
264: ignore);
265: }
266: }
267: }
268: }
269: }
270: }
271:
272: /**
273: * @param conf
274: * @throws ConfigurationException
275: */
276: private void setupSingleRefreshJob(final Configuration conf)
277: throws ConfigurationException {
278: try {
279: String key = NetUtils.decode(conf.getAttribute(ATTR_KEY),
280: "utf-8");
281: String uri = NetUtils.decode(conf.getAttribute(ATTR_URI),
282: "utf-8");
283: long interval = conf.getAttributeAsLong(ATTR_INTERVAL);
284: addRefreshSource(key, uri, 10, interval);
285: } catch (UnsupportedEncodingException e) {
286: /* Won't happen */
287: }
288: }
289:
290: /**
291: * @param interval
292: */
293: protected void startConfigurationTask(long interval) {
294: configurationTask = new ConfigurationTask();
295: configurationTask.enableLogging(getLogger());
296: runnable.execute(configurationTask, interval, interval);
297: }
298:
299: protected void stopConfigurationTask() {
300: if (this .configurationTask != null) {
301: this .runnable.remove(this .configurationTask);
302: this .configurationTask.run();
303: this .configurationTask = null;
304: }
305: }
306:
307: /**
308: * Task which writes refresher configuraiton into the source.
309: */
310: protected class ConfigurationTask extends AbstractLogEnabled
311: implements Runnable {
312: public void run() {
313: if (changed) {
314: // Reset the flag.
315: changed = false;
316:
317: boolean success = true;
318: Writer writer = null;
319: try {
320: writer = new OutputStreamWriter(
321: new FileOutputStream(configFile), "utf-8");
322: writer.write("<targets>\n");
323:
324: try {
325: final Iterator i = entries.values().iterator();
326: while (i.hasNext()) {
327: RefresherTask task = (RefresherTask) i
328: .next();
329: writer.write(task.toXML());
330: }
331: } catch (ConcurrentModificationException e) {
332: // List of targets has been changed, unable to save it completely.
333: // Will re-try writing the list next time.
334: success = false;
335: }
336:
337: writer.write("</targets>\n");
338: } catch (IOException e) {
339: // Got I/O exception while writing the list.
340: // Will re-try writing the list next time.
341: success = false;
342: if (getLogger().isDebugEnabled()) {
343: getLogger().debug(
344: "Error writing targets to file.", e);
345: }
346: } finally {
347: if (writer != null) {
348: try {
349: writer.close();
350: } catch (IOException e) { /* ignored */
351: }
352: }
353: }
354:
355: // Set the flag to run next time if failed this time
356: if (!success) {
357: changed = true;
358: }
359: }
360: }
361: }
362:
363: protected class RefresherTask extends AbstractLogEnabled implements
364: Runnable {
365: private String key;
366: private String uri;
367: private long interval;
368:
369: public RefresherTask(String key, String uri, long interval) {
370: this .key = key;
371: this .uri = uri;
372: this .interval = interval;
373: }
374:
375: public void run() {
376: if (this .uri != null) {
377: if (getLogger().isDebugEnabled()) {
378: getLogger().debug("Refreshing " + this .uri);
379: }
380:
381: // Setup Environment
382: final BackgroundEnvironment env;
383: try {
384: org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) context
385: .get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
386: env = new BackgroundEnvironment(getLogger(), ctx);
387: } catch (ContextException e) {
388: throw new CascadingRuntimeException(
389: "No context found", e);
390: } catch (MalformedURLException e) {
391: // Unlikely to happen
392: throw new CascadingRuntimeException("Invalid URL",
393: e);
394: }
395: Processor processor;
396: try {
397: processor = (Processor) manager
398: .lookup(Processor.ROLE);
399: } catch (ServiceException e) {
400: throw new CascadingRuntimeException(
401: "No processor found", e);
402: }
403:
404: final Object key = CocoonComponentManager
405: .startProcessing(env);
406: CocoonComponentManager
407: .enterEnvironment(env,
408: new WrapperComponentManager(manager),
409: processor);
410: try {
411: // Refresh Source
412: Source source = null;
413: try {
414: source = resolver.resolveURI(uri);
415: source.refresh();
416: } catch (IOException e) {
417: getLogger().error("Error refreshing source", e);
418: } finally {
419: if (source != null) {
420: resolver.release(source);
421: }
422: }
423: } finally {
424: CocoonComponentManager.leaveEnvironment();
425: CocoonComponentManager.endProcessing(env, key);
426: if (manager != null) {
427: manager.release(processor);
428: }
429: }
430: }
431: }
432:
433: public void update(String uri, long interval) {
434: this .uri = uri;
435: this .interval = interval;
436: }
437:
438: public String toXML() {
439: String key = null;
440: String uri = null;
441: try {
442: key = NetUtils.encode(this .key, "utf-8");
443: uri = NetUtils.encode(this .uri, "utf-8");
444: } catch (UnsupportedEncodingException e) {
445: /* Won't happen */
446: }
447: StringBuffer s = new StringBuffer();
448: s.append('<').append(TAGNAME_TARGET).append(' ');
449: s.append(ATTR_KEY).append("=\"").append(key).append("\" ");
450: s.append(ATTR_URI).append("=\"").append(uri).append("\" ");
451: s.append(ATTR_INTERVAL).append("=\"").append(interval)
452: .append("\" />\n");
453: return s.toString();
454: }
455: }
456: }
|