/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.cocoon.components.source.helpers; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.MalformedURLException; import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.apache.avalon.framework.CascadingException; import org.apache.avalon.framework.CascadingRuntimeException; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.component.WrapperComponentManager; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.configuration.SAXConfigurationHandler; import org.apache.avalon.framework.context.Context; import org.apache.avalon.framework.context.ContextException; import org.apache.avalon.framework.context.Contextualizable; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.avalon.framework.thread.ThreadSafe; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceResolver; import org.apache.cocoon.Constants; import org.apache.cocoon.Processor; import org.apache.cocoon.components.CocoonComponentManager; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.components.thread.RunnableManager; import org.apache.cocoon.environment.background.BackgroundEnvironment; import org.apache.cocoon.util.NetUtils; /** * Default implementation of the refresher. * * @since 2.1.1 * @version $Id$ */ public class DelaySourceRefresher extends AbstractLogEnabled implements Contextualizable, Serviceable, Configurable, Disposable, ThreadSafe, SourceRefresher { private static final String PARAM_WRITE_FILE = "write-file"; private static final String DEFAULT_WRITE_FILE = "refresher-targets.xml"; private static final String TAGNAME_TARGET = "target"; private static final String ATTR_KEY = "key"; private static final String ATTR_URI = "uri"; private static final String ATTR_INTERVAL = "interval"; protected Context context; // service dependencies protected ServiceManager manager; protected SourceResolver resolver; protected RunnableManager runnable; // the scheduled targets to be persisted and recovered upon restart protected Map entries = Collections.synchronizedMap(new HashMap()); // the cocoon working directory protected File workDir; /** The source to persist refresher entries into */ protected File configFile; // whether anything changed to the entries since last persisting them protected volatile boolean changed; protected ConfigurationTask configurationTask; // ---------------------------------------------------- Lifecycle /* (non-Javadoc) * @see Contextualizable#contextualize(Context) */ public void contextualize(Context context) throws ContextException { this.context = context; this.workDir = (File) context.get(Constants.CONTEXT_WORK_DIR); } /* (non-Javadoc) * @see Serviceable#service(ServiceManager) */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; this.resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE); this.runnable = (RunnableManager) this.manager.lookup(RunnableManager.ROLE); } public void configure(Configuration configuration) throws ConfigurationException { Parameters parameters = Parameters.fromConfiguration(configuration); long interval = parameters.getParameterAsLong("interval", 0); if (interval > 0) { String fileName = parameters.getParameter(PARAM_WRITE_FILE, DEFAULT_WRITE_FILE); this.configFile = new File(this.workDir, fileName); if (this.configFile.exists() && !this.configFile.canWrite()) { throw new ConfigurationException("Parameter 'write-source' resolves to not modifiable file: " + this.configFile); } if (!this.configFile.getParentFile().exists() && !this.configFile.getParentFile().mkdirs()) { throw new ConfigurationException("Can not create parent directory for: " + this.configFile); } if (getLogger().isDebugEnabled()) { getLogger().debug("Write source location: " + this.configFile); } setupRefreshJobs(readRefreshJobConfiguration()); startConfigurationTask(interval); } else { if (getLogger().isInfoEnabled()) { getLogger().info("Not writing update targets to file."); } } // Setup any in-line configured tasks setupRefreshJobs(configuration); } /* (non-Javadoc) * @see Disposable#dispose() */ public void dispose() { stopConfigurationTask(); if (this.runnable != null) { this.manager.release(this.runnable); this.runnable = null; } if (this.resolver != null) { this.manager.release(this.resolver); this.resolver = null; } this.manager = null; } // ---------------------------------------------------- SourceRefresher implementation /* (non-Javadoc) * @see SourceRefresher#refresh */ public void refresh(String name, String uri, Parameters parameters) throws SourceException { final long interval = parameters.getParameterAsLong(PARAM_CACHE_INTERVAL, -1); if (uri != null && interval > 0) { addRefreshSource(name, uri, interval, interval); } else { removeRefreshSource(name); } } protected void addRefreshSource(String key, String uri, long delay, long interval) { RefresherTask task = (RefresherTask) this.entries.get(key); if (task == null) { // New source added. task = new RefresherTask(key, uri, interval); task.enableLogging(getLogger()); this.entries.put(key, task); this.runnable.execute(task, interval, interval); this.changed = true; } else if (task.interval != interval) { // Existing source refresh interval updated. task.update(uri, interval); this.runnable.remove(task); this.runnable.execute(task, interval, interval); this.changed = true; } else { // No change. } } protected void removeRefreshSource(String key) { RefresherTask task = (RefresherTask) this.entries.get(key); if (task != null) { this.entries.remove(key); this.runnable.remove(task); this.changed = true; } } // ---------------------------------------------------- Implementation /** * */ private Configuration readRefreshJobConfiguration() { Source source = null; SAXConfigurationHandler b = new SAXConfigurationHandler(); try { if (this.configFile.exists()) { source = this.resolver.resolveURI(this.configFile.toURL().toString()); SourceUtil.toSAX(this.manager, source, source.getMimeType(), b); } } catch (Exception ignore) { getLogger().warn("Unable to read configuration from " + this.configFile); } finally { if (source != null) { this.resolver.release(source); } } return b.getConfiguration(); } /** * @param conf */ private void setupRefreshJobs(final Configuration conf) { if (conf != null) { final Configuration[] children = conf.getChildren(TAGNAME_TARGET); if (children != null) { for (int i = 0; i < children.length; i++) { try { setupSingleRefreshJob(children[i]); } catch (CascadingException ignore) { if (getLogger().isDebugEnabled()) { getLogger().debug("Setting up refresh job, ignoring exception:", ignore); } } } } } } /** * @param conf * @throws ConfigurationException */ private void setupSingleRefreshJob(final Configuration conf) throws ConfigurationException { try { String key = NetUtils.decode(conf.getAttribute(ATTR_KEY), "utf-8"); String uri = NetUtils.decode(conf.getAttribute(ATTR_URI), "utf-8"); long interval = conf.getAttributeAsLong(ATTR_INTERVAL); addRefreshSource(key, uri, 10, interval); } catch (UnsupportedEncodingException e) { /* Won't happen */ } } /** * @param interval */ protected void startConfigurationTask(long interval) { configurationTask = new ConfigurationTask(); configurationTask.enableLogging(getLogger()); runnable.execute(configurationTask, interval, interval); } protected void stopConfigurationTask() { if (this.configurationTask != null) { this.runnable.remove(this.configurationTask); this.configurationTask.run(); this.configurationTask = null; } } /** * Task which writes refresher configuraiton into the source. */ protected class ConfigurationTask extends AbstractLogEnabled implements Runnable { public void run() { if (changed) { // Reset the flag. changed = false; boolean success = true; Writer writer = null; try { writer = new OutputStreamWriter(new FileOutputStream(configFile), "utf-8"); writer.write("<targets>\n"); try { final Iterator i = entries.values().iterator(); while (i.hasNext()) { RefresherTask task = (RefresherTask) i.next(); writer.write(task.toXML()); } } catch (ConcurrentModificationException e) { // List of targets has been changed, unable to save it completely. // Will re-try writing the list next time. success = false; } writer.write("</targets>\n"); } catch (IOException e) { // Got I/O exception while writing the list. // Will re-try writing the list next time. success = false; if (getLogger().isDebugEnabled()) { getLogger().debug("Error writing targets to file.", e); } } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { /* ignored */ } } } // Set the flag to run next time if failed this time if (!success) { changed = true; } } } } protected class RefresherTask extends AbstractLogEnabled implements Runnable { private String key; private String uri; private long interval; public RefresherTask(String key, String uri, long interval) { this.key = key; this.uri = uri; this.interval = interval; } public void run() { if (this.uri != null) { if (getLogger().isDebugEnabled()) { getLogger().debug("Refreshing " + this.uri); } // Setup Environment final BackgroundEnvironment env; try { org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); env = new BackgroundEnvironment(getLogger(), ctx); } catch (ContextException e) { throw new CascadingRuntimeException("No context found", e); } catch (MalformedURLException e) { // Unlikely to happen throw new CascadingRuntimeException("Invalid URL", e); } Processor processor; try { processor = (Processor) manager.lookup(Processor.ROLE); } catch (ServiceException e) { throw new CascadingRuntimeException("No processor found", e); } final Object key = CocoonComponentManager.startProcessing(env); CocoonComponentManager.enterEnvironment(env, new WrapperComponentManager(manager), processor); try { // Refresh Source Source source = null; try { source = resolver.resolveURI(uri); source.refresh(); } catch (IOException e) { getLogger().error("Error refreshing source", e); } finally { if (source != null) { resolver.release(source); } } } finally { CocoonComponentManager.leaveEnvironment(); CocoonComponentManager.endProcessing(env, key); if (manager != null) { manager.release(processor); } } } } public void update(String uri, long interval) { this.uri = uri; this.interval = interval; } public String toXML() { String key = null; String uri = null; try { key = NetUtils.encode(this.key, "utf-8"); uri = NetUtils.encode(this.uri, "utf-8"); } catch (UnsupportedEncodingException e) { /* Won't happen */ } StringBuffer s = new StringBuffer(); s.append('<').append(TAGNAME_TARGET).append(' '); s.append(ATTR_KEY).append("=\"").append(key).append("\" "); s.append(ATTR_URI).append("=\"").append(uri).append("\" "); s.append(ATTR_INTERVAL).append("=\"").append(interval).append("\" />\n"); return s.toString(); } } }