diff --git a/org.springframework.context.support/build.xml b/org.springframework.context.support/build.xml
new file mode 100644
index 00000000000..643fa1f7b80
--- /dev/null
+++ b/org.springframework.context.support/build.xml
@@ -0,0 +1,6 @@
+
+
If the specified named cache is not configured in the cache configuration descriptor, + * this FactoryBean will construct an instance of a Cache with the provided name and the + * specified cache properties and add it to the CacheManager for later retrieval. If some + * or all properties are not set at configuration time, this FactoryBean will use defaults. + * + *
Note: If the named Cache instance is found, the properties will be ignored and the
+ * Cache instance will be retrieved from the CacheManager.
+ *
+ * @author Dmitriy Kopylenko
+ * @author Juergen Hoeller
+ * @since 1.1.1
+ * @see #setCacheManager
+ * @see EhCacheManagerFactoryBean
+ * @see net.sf.ehcache.Cache
+ */
+public class EhCacheFactoryBean implements FactoryBean, BeanNameAware, InitializingBean {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private CacheManager cacheManager;
+
+ private String cacheName;
+
+ private int maxElementsInMemory = 10000;
+
+ private int maxElementsOnDisk = 10000000;
+
+ private MemoryStoreEvictionPolicy memoryStoreEvictionPolicy = MemoryStoreEvictionPolicy.LRU;
+
+ private boolean overflowToDisk = true;
+
+ private String diskStorePath;
+
+ private boolean eternal = false;
+
+ private int timeToLive = 120;
+
+ private int timeToIdle = 120;
+
+ private boolean diskPersistent = false;
+
+ private int diskExpiryThreadIntervalSeconds = 120;
+
+ private boolean blocking = false;
+
+ private CacheEntryFactory cacheEntryFactory;
+
+ private String beanName;
+
+ private Ehcache cache;
+
+
+ /**
+ * Set a CacheManager from which to retrieve a named Cache instance.
+ * By default, CacheManager.getInstance() will be called.
+ *
Note that in particular for persistent caches, it is advisable to + * properly handle the shutdown of the CacheManager: Set up a separate + * EhCacheManagerFactoryBean and pass a reference to this bean property. + *
A separate EhCacheManagerFactoryBean is also necessary for loading + * EHCache configuration from a non-default config location. + * @see EhCacheManagerFactoryBean + * @see net.sf.ehcache.CacheManager#getInstance + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Set a name for which to retrieve or create a cache instance. + * Default is the bean name of this EhCacheFactoryBean. + */ + public void setCacheName(String cacheName) { + this.cacheName = cacheName; + } + + /** + * Specify the maximum number of cached objects in memory. + * Default is 10000 elements. + */ + public void setMaxElementsInMemory(int maxElementsInMemory) { + this.maxElementsInMemory = maxElementsInMemory; + } + + /** + * Specify the maximum number of cached objects on disk. + * Default is 10000000 elements. + */ + public void setMaxElementsOnDisk(int maxElementsOnDisk) { + this.maxElementsOnDisk = maxElementsOnDisk; + } + + /** + * Set the memory style eviction policy for this cache. + * Supported values are "LRU", "LFU" and "FIFO", according to the + * constants defined in EHCache's MemoryStoreEvictionPolicy class. + * Default is "LRU". + */ + public void setMemoryStoreEvictionPolicy(MemoryStoreEvictionPolicy memoryStoreEvictionPolicy) { + Assert.notNull(memoryStoreEvictionPolicy, "memoryStoreEvictionPolicy must not be null"); + this.memoryStoreEvictionPolicy = memoryStoreEvictionPolicy; + } + + /** + * Set whether elements can overflow to disk when the in-memory cache + * has reached the maximum size limit. Default is "true". + */ + public void setOverflowToDisk(boolean overflowToDisk) { + this.overflowToDisk = overflowToDisk; + } + + /** + * Set whether elements are considered as eternal. If "true", timeouts + * are ignored and the element is never expired. Default is "false". + */ + public void setEternal(boolean eternal) { + this.eternal = eternal; + } + + /** + * Set t he time in seconds to live for an element before it expires, + * i.e. the maximum time between creation time and when an element expires. + * It is only used if the element is not eternal. Default is 120 seconds. + */ + public void setTimeToLive(int timeToLive) { + this.timeToLive = timeToLive; + } + + /** + * Set the time in seconds to idle for an element before it expires, that is, + * the maximum amount of time between accesses before an element expires. + * This is only used if the element is not eternal. Default is 120 seconds. + */ + public void setTimeToIdle(int timeToIdle) { + this.timeToIdle = timeToIdle; + } + + /** + * Set whether the disk store persists between restarts of the Virtual Machine. + * The default is "false". + */ + public void setDiskPersistent(boolean diskPersistent) { + this.diskPersistent = diskPersistent; + } + + /** + * Set the number of seconds between runs of the disk expiry thread. + * The default is 120 seconds. + */ + public void setDiskExpiryThreadIntervalSeconds(int diskExpiryThreadIntervalSeconds) { + this.diskExpiryThreadIntervalSeconds = diskExpiryThreadIntervalSeconds; + } + + /** + * Set whether to use a blocking cache that lets read attempts block + * until the requested element is created. + *
If you intend to build a self-populating blocking cache, + * consider specifying a {@link #setCacheEntryFactory CacheEntryFactory}. + * @see net.sf.ehcache.constructs.blocking.BlockingCache + * @see #setCacheEntryFactory + */ + public void setBlocking(boolean blocking) { + this.blocking = blocking; + } + + /** + * Set an EHCache {@link net.sf.ehcache.constructs.blocking.CacheEntryFactory} + * to use for a self-populating cache. If such a factory is specified, + * the cache will be decorated with EHCache's + * {@link net.sf.ehcache.constructs.blocking.SelfPopulatingCache}. + *
The specified factory can be of type + * {@link net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory}, + * which will lead to the use of an + * {@link net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache}. + *
Note: Any such self-populating cache is automatically a blocking cache. + * @see net.sf.ehcache.constructs.blocking.SelfPopulatingCache + * @see net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache + * @see net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory + */ + public void setCacheEntryFactory(CacheEntryFactory cacheEntryFactory) { + this.cacheEntryFactory = cacheEntryFactory; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + public void afterPropertiesSet() throws CacheException, IOException { + // If no CacheManager given, fetch the default. + if (this.cacheManager == null) { + if (logger.isDebugEnabled()) { + logger.debug("Using default EHCache CacheManager for cache region '" + this.cacheName + "'"); + } + this.cacheManager = CacheManager.getInstance(); + } + + // If no cache name given, use bean name as cache name. + if (this.cacheName == null) { + this.cacheName = this.beanName; + } + + // Fetch cache region: If none with the given name exists, + // create one on the fly. + Ehcache rawCache = null; + if (this.cacheManager.cacheExists(this.cacheName)) { + if (logger.isDebugEnabled()) { + logger.debug("Using existing EHCache cache region '" + this.cacheName + "'"); + } + rawCache = this.cacheManager.getEhcache(this.cacheName); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Creating new EHCache cache region '" + this.cacheName + "'"); + } + rawCache = createCache(); + this.cacheManager.addCache(rawCache); + } + + // Decorate cache if necessary. + Ehcache decoratedCache = decorateCache(rawCache); + if (decoratedCache != rawCache) { + this.cacheManager.replaceCacheWithDecoratedCache(rawCache, decoratedCache); + } + this.cache = decoratedCache; + } + + /** + * Create a raw Cache object based on the configuration of this FactoryBean. + */ + private Cache createCache() { + return new Cache( + this.cacheName, this.maxElementsInMemory, this.memoryStoreEvictionPolicy, + this.overflowToDisk, null, this.eternal, this.timeToLive, this.timeToIdle, + this.diskPersistent, this.diskExpiryThreadIntervalSeconds, null, null, this.maxElementsOnDisk); + } + + /** + * Decorate the given Cache, if necessary. + * @param cache the raw Cache object, based on the configuration of this FactoryBean + * @return the (potentially decorated) cache object to be registered with the CacheManager + */ + protected Ehcache decorateCache(Ehcache cache) { + if (this.cacheEntryFactory != null) { + if (this.cacheEntryFactory instanceof UpdatingCacheEntryFactory) { + return new UpdatingSelfPopulatingCache(cache, (UpdatingCacheEntryFactory) this.cacheEntryFactory); + } + else { + return new SelfPopulatingCache(cache, this.cacheEntryFactory); + } + } + if (this.blocking) { + return new BlockingCache(cache); + } + return cache; + } + + + public Object getObject() { + return this.cache; + } + + public Class getObjectType() { + return (this.cache != null ? this.cache.getClass() : Ehcache.class); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java new file mode 100644 index 00000000000..85f1da4e301 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.cache.ehcache; + +import java.io.IOException; + +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +/** + * FactoryBean that exposes an EHCache {@link net.sf.ehcache.CacheManager} instance + * (independent or shared), configured from a specified config location. + * + *
If no config location is specified, a CacheManager will be configured from + * "ehcache.xml" in the root of the class path (that is, default EHCache initialization + * - as defined in the EHCache docs - will apply). + * + *
Setting up a separate EhCacheManagerFactoryBean is also advisable when using + * EhCacheFactoryBean, as it provides a (by default) independent CacheManager instance + * and cares for proper shutdown of the CacheManager. EhCacheManagerFactoryBean is + * also necessary for loading EHCache configuration from a non-default config location. + * + *
Note: As of Spring 2.0, this FactoryBean will by default create an independent + * CacheManager instance, which requires EHCache 1.2 or higher. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 1.1.1 + * @see #setConfigLocation + * @see #setShared + * @see EhCacheFactoryBean + * @see net.sf.ehcache.CacheManager + */ +public class EhCacheManagerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private Resource configLocation; + + private boolean shared = false; + + private String cacheManagerName; + + private CacheManager cacheManager; + + + /** + * Set the location of the EHCache config file. A typical value is "/WEB-INF/ehcache.xml". + *
Default is "ehcache.xml" in the root of the class path, or if not found, + * "ehcache-failsafe.xml" in the EHCache jar (default EHCache initialization). + * @see net.sf.ehcache.CacheManager#create(java.io.InputStream) + * @see net.sf.ehcache.CacheManager#CacheManager(java.io.InputStream) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + /** + * Set whether the EHCache CacheManager should be shared (as a singleton at the VM level) + * or independent (typically local within the application). Default is "false", creating + * an independent instance. + * @see net.sf.ehcache.CacheManager#create() + * @see net.sf.ehcache.CacheManager#CacheManager() + */ + public void setShared(boolean shared) { + this.shared = shared; + } + + /** + * Set the name of the EHCache CacheManager (if a specific name is desired). + * @see net.sf.ehcache.CacheManager#setName(String) + */ + public void setCacheManagerName(String cacheManagerName) { + this.cacheManagerName = cacheManagerName; + } + + + public void afterPropertiesSet() throws IOException, CacheException { + logger.info("Initializing EHCache CacheManager"); + if (this.shared) { + // Shared CacheManager singleton at the VM level. + if (this.configLocation != null) { + this.cacheManager = CacheManager.create(this.configLocation.getInputStream()); + } + else { + this.cacheManager = CacheManager.create(); + } + } + else { + // Independent CacheManager instance (the default). + if (this.configLocation != null) { + this.cacheManager = new CacheManager(this.configLocation.getInputStream()); + } + else { + this.cacheManager = new CacheManager(); + } + } + if (this.cacheManagerName != null) { + this.cacheManager.setName(this.cacheManagerName); + } + } + + + public Object getObject() { + return this.cacheManager; + } + + public Class getObjectType() { + return (this.cacheManager != null ? this.cacheManager.getClass() : CacheManager.class); + } + + public boolean isSingleton() { + return true; + } + + + public void destroy() { + logger.info("Shutting down EHCache CacheManager"); + this.cacheManager.shutdown(); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/package.html b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/package.html new file mode 100644 index 00000000000..f05383fe8c0 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/package.html @@ -0,0 +1,10 @@ + +
+ +Support classes for the open source cache +EHCache, +allowing to set up an EHCache CacheManager and Caches +as beans in a Spring context. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailAuthenticationException.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailAuthenticationException.java new file mode 100644 index 00000000000..6a54761ebcf --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailAuthenticationException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.mail; + +/** + * Exception thrown on failed authentication. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +public class MailAuthenticationException extends MailException { + + /** + * Constructor for MailAuthenticationException. + * @param msg message + */ + public MailAuthenticationException(String msg) { + super(msg); + } + + /** + * Constructor for MailAuthenticationException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailAuthenticationException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for MailAuthenticationException. + * @param cause the root cause from the mail API in use + */ + public MailAuthenticationException(Throwable cause) { + super("Authentication failed", cause); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailException.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailException.java new file mode 100644 index 00000000000..407a42a838c --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.mail; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for all mail exceptions. + * + * @author Dmitriy Kopylenko + */ +public abstract class MailException extends NestedRuntimeException { + + /** + * Constructor for MailException. + * @param msg the detail message + */ + public MailException(String msg) { + super(msg); + } + + /** + * Constructor for MailException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailMessage.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailMessage.java new file mode 100644 index 00000000000..c33f0e76b24 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailMessage.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.mail; + +import java.util.Date; + +/** + * This is a common interface for mail messages, allowing a user to set key + * values required in assembling a mail message, without needing to know if + * the underlying message is a simple text message or a more sophisticated + * MIME message. + * + *Implemented by both SimpleMailMessage and MimeMessageHelper, + * to let message population code interact with a simple message or a + * MIME message through a common interface. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see SimpleMailMessage + * @see org.springframework.mail.javamail.MimeMessageHelper + */ +public interface MailMessage { + + public void setFrom(String from) throws MailParseException; + + public void setReplyTo(String replyTo) throws MailParseException; + + public void setTo(String to) throws MailParseException; + + public void setTo(String[] to) throws MailParseException; + + public void setCc(String cc) throws MailParseException; + + public void setCc(String[] cc) throws MailParseException; + + public void setBcc(String bcc) throws MailParseException; + + public void setBcc(String[] bcc) throws MailParseException; + + public void setSentDate(Date sentDate) throws MailParseException; + + public void setSubject(String subject) throws MailParseException; + + public void setText(String text) throws MailParseException; + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailParseException.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailParseException.java new file mode 100644 index 00000000000..b09676e22c1 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailParseException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.mail; + +/** + * Exception thrown if illegal message properties are encountered. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +public class MailParseException extends MailException { + + /** + * Constructor for MailParseException. + * @param msg the detail message + */ + public MailParseException(String msg) { + super(msg); + } + + /** + * Constructor for MailParseException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailParseException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for MailParseException. + * @param cause the root cause from the mail API in use + */ + public MailParseException(Throwable cause) { + super("Could not parse mail", cause); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailPreparationException.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailPreparationException.java new file mode 100644 index 00000000000..7068c0f94c0 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailPreparationException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.mail; + +/** + * Exception to be thrown by user code if a mail cannot be prepared properly, + * for example when a Velocity template cannot be rendered for the mail text. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.ui.velocity.VelocityEngineUtils#mergeTemplateIntoString + * @see org.springframework.ui.freemarker.FreeMarkerTemplateUtils#processTemplateIntoString + */ +public class MailPreparationException extends MailException { + + /** + * Constructor for MailPreparationException. + * @param msg the detail message + */ + public MailPreparationException(String msg) { + super(msg); + } + + /** + * Constructor for MailPreparationException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailPreparationException(String msg, Throwable cause) { + super(msg, cause); + } + + public MailPreparationException(Throwable cause) { + super("Could not prepare mail", cause); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailSendException.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailSendException.java new file mode 100644 index 00000000000..c12cb88f67a --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailSendException.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed 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.springframework.mail; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.ObjectUtils; + +/** + * Exception thrown when a mail sending error is encountered. + * Can register failed messages with their exceptions. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +public class MailSendException extends MailException { + + private transient Map failedMessages; + + private Exception[] messageExceptions; + + + /** + * Constructor for MailSendException. + * @param msg the detail message + */ + public MailSendException(String msg) { + super(msg); + } + + /** + * Constructor for MailSendException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailSendException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for registration of failed messages, with the + * messages that failed as keys, and the thrown exceptions as values. + *
The messages should be the same that were originally passed + * to the invoked send method. + * @param failedMessages Map of failed messages as keys and thrown + * exceptions as values + */ + public MailSendException(Map failedMessages) { + super(null); + this.failedMessages = new LinkedHashMap(failedMessages); + this.messageExceptions = (Exception[]) failedMessages.values().toArray(new Exception[failedMessages.size()]); + } + + + /** + * Return a Map with the failed messages as keys, and the thrown exceptions + * as values. + *
Note that a general mail server connection failure will not result + * in failed messages being returned here: A message will only be + * contained here if actually sending it was attempted but failed. + *
The messages will be the same that were originally passed to the + * invoked send method, that is, SimpleMailMessages in case of using + * the generic MailSender interface. + *
In case of sending MimeMessage instances via JavaMailSender, + * the messages will be of type MimeMessage. + *
NOTE: This Map will not be available after serialization. + * Use {@link #getMessageExceptions()} in such a scenario, which will + * be available after serialization as well. + * @return the Map of failed messages as keys and thrown exceptions as + * values, or an empty Map if no failed messages + * @see SimpleMailMessage + * @see javax.mail.internet.MimeMessage + */ + public final Map getFailedMessages() { + return (this.failedMessages != null ? this.failedMessages : Collections.EMPTY_MAP); + } + + /** + * Return an array with thrown message exceptions. + *
Note that a general mail server connection failure will not result + * in failed messages being returned here: A message will only be + * contained here if actually sending it was attempted but failed. + * @return the array of thrown message exceptions, + * or an empty array if no failed messages + */ + public final Exception[] getMessageExceptions() { + return (this.messageExceptions != null ? this.messageExceptions : new Exception[0]); + } + + + public String getMessage() { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + return super.getMessage(); + } + else { + StringBuffer sb = new StringBuffer("Failed messages: "); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + sb.append(subEx.toString()); + if (i < this.messageExceptions.length - 1) { + sb.append("; "); + } + } + return sb.toString(); + } + } + + public String toString() { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + return super.toString(); + } + else { + StringBuffer sb = new StringBuffer(getClass().getName()); + sb.append("; nested exceptions (").append(this.messageExceptions.length).append(") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + sb.append('\n').append("Failed message ").append(i + 1).append(": "); + sb.append(subEx); + } + return sb.toString(); + } + } + + public void printStackTrace(PrintStream ps) { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + super.printStackTrace(ps); + } + else { + ps.println(getClass().getName() + "; nested exception details (" + + this.messageExceptions.length + ") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + ps.println("Failed message " + (i + 1) + ":"); + subEx.printStackTrace(ps); + } + } + } + + public void printStackTrace(PrintWriter pw) { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + super.printStackTrace(pw); + } + else { + pw.println(getClass().getName() + "; nested exception details (" + + this.messageExceptions.length + ") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + pw.println("Failed message " + (i + 1) + ":"); + subEx.printStackTrace(pw); + } + } + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/MailSender.java b/org.springframework.context.support/src/main/java/org/springframework/mail/MailSender.java new file mode 100644 index 00000000000..390b9508715 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/MailSender.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed 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.springframework.mail; + +/** + * This interface defines a strategy for sending simple mails. Can be + * implemented for a variety of mailing systems due to the simple requirements. + * For richer functionality like MIME messages, consider JavaMailSender. + * + *
Allows for easy testing of clients, as it does not depend on JavaMail's + * infrastructure classes: no mocking of JavaMail Session or Transport necessary. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 10.09.2003 + * @see org.springframework.mail.javamail.JavaMailSender + */ +public interface MailSender { + + /** + * Send the given simple mail message. + * @param simpleMessage the message to send + * @throws org.springframework.mail.MailParseException + * in case of failure when parsing the message + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending the message + */ + void send(SimpleMailMessage simpleMessage) throws MailException; + + /** + * Send the given array of simple mail messages in batch. + * @param simpleMessages the messages to send + * @throws org.springframework.mail.MailParseException + * in case of failure when parsing a message + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending a message + */ + void send(SimpleMailMessage[] simpleMessages) throws MailException; + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/org.springframework.context.support/src/main/java/org/springframework/mail/SimpleMailMessage.java new file mode 100644 index 00000000000..8ab24e2854c --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed 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.springframework.mail; + +import java.io.Serializable; +import java.util.Date; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.Assert; + +/** + * Models a simple mail message, including data such as the from, to, cc, subject, and text fields. + * + *
Consider JavaMailSender and JavaMail MimeMessages for creating
+ * more sophisticated messages, for example messages with attachments, special
+ * character encodings, or personal names that accompany mail addresses.
+ *
+ * @author Dmitriy Kopylenko
+ * @author Juergen Hoeller
+ * @since 10.09.2003
+ * @see MailSender
+ * @see org.springframework.mail.javamail.JavaMailSender
+ * @see org.springframework.mail.javamail.MimeMessagePreparator
+ * @see org.springframework.mail.javamail.MimeMessageHelper
+ * @see org.springframework.mail.javamail.MimeMailMessage
+ */
+public class SimpleMailMessage implements MailMessage, Serializable {
+
+ private String from;
+
+ private String replyTo;
+
+ private String[] to;
+
+ private String[] cc;
+
+ private String[] bcc;
+
+ private Date sentDate;
+
+ private String subject;
+
+ private String text;
+
+
+ /**
+ * Create a new SimpleMailMessage.
+ */
+ public SimpleMailMessage() {
+ }
+
+ /**
+ * Copy constructor for creating a new SimpleMailMessage from the state
+ * of an existing SimpleMailMessage instance.
+ * @throws IllegalArgumentException if the supplied message is null
+ */
+ public SimpleMailMessage(SimpleMailMessage original) {
+ Assert.notNull(original, "The 'original' message argument cannot be null");
+ this.from = original.getFrom();
+ this.replyTo = original.getReplyTo();
+ if (original.getTo() != null) {
+ this.to = copy(original.getTo());
+ }
+ if (original.getCc() != null) {
+ this.cc = copy(original.getCc());
+ }
+ if (original.getBcc() != null) {
+ this.bcc = copy(original.getBcc());
+ }
+ this.sentDate = original.getSentDate();
+ this.subject = original.getSubject();
+ this.text = original.getText();
+ }
+
+
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ public String getFrom() {
+ return this.from;
+ }
+
+ public void setReplyTo(String replyTo) {
+ this.replyTo = replyTo;
+ }
+
+ public String getReplyTo() {
+ return replyTo;
+ }
+
+ public void setTo(String to) {
+ this.to = new String[] {to};
+ }
+
+ public void setTo(String[] to) {
+ this.to = to;
+ }
+
+ public String[] getTo() {
+ return this.to;
+ }
+
+ public void setCc(String cc) {
+ this.cc = new String[] {cc};
+ }
+
+ public void setCc(String[] cc) {
+ this.cc = cc;
+ }
+
+ public String[] getCc() {
+ return cc;
+ }
+
+ public void setBcc(String bcc) {
+ this.bcc = new String[] {bcc};
+ }
+
+ public void setBcc(String[] bcc) {
+ this.bcc = bcc;
+ }
+
+ public String[] getBcc() {
+ return bcc;
+ }
+
+ public void setSentDate(Date sentDate) {
+ this.sentDate = sentDate;
+ }
+
+ public Date getSentDate() {
+ return sentDate;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getSubject() {
+ return this.subject;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public String getText() {
+ return this.text;
+ }
+
+
+ /**
+ * Copy the contents of this message to the given target message.
+ * @param target the MailMessage to copy to
+ * @throws IllegalArgumentException if the supplied target is null
+ */
+ public void copyTo(MailMessage target) {
+ Assert.notNull(target, "The 'target' message argument cannot be null");
+ if (getFrom() != null) {
+ target.setFrom(getFrom());
+ }
+ if (getReplyTo() != null) {
+ target.setReplyTo(getReplyTo());
+ }
+ if (getTo() != null) {
+ target.setTo(getTo());
+ }
+ if (getCc() != null) {
+ target.setCc(getCc());
+ }
+ if (getBcc() != null) {
+ target.setBcc(getBcc());
+ }
+ if (getSentDate() != null) {
+ target.setSentDate(getSentDate());
+ }
+ if (getSubject() != null) {
+ target.setSubject(getSubject());
+ }
+ if (getText() != null) {
+ target.setText(getText());
+ }
+ }
+
+
+ public String toString() {
+ StringBuffer sb = new StringBuffer("SimpleMailMessage: ");
+ sb.append("from=").append(this.from).append("; ");
+ sb.append("replyTo=").append(this.replyTo).append("; ");
+ sb.append("to=").append(StringUtils.arrayToCommaDelimitedString(this.to)).append("; ");
+ sb.append("cc=").append(StringUtils.arrayToCommaDelimitedString(this.cc)).append("; ");
+ sb.append("bcc=").append(StringUtils.arrayToCommaDelimitedString(this.bcc)).append("; ");
+ sb.append("sentDate=").append(this.sentDate).append("; ");
+ sb.append("subject=").append(this.subject).append("; ");
+ sb.append("text=").append(this.text);
+ return sb.toString();
+ }
+
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof SimpleMailMessage)) {
+ return false;
+ }
+ SimpleMailMessage otherMessage = (SimpleMailMessage) other;
+ return (ObjectUtils.nullSafeEquals(this.from, otherMessage.from) &&
+ ObjectUtils.nullSafeEquals(this.replyTo, otherMessage.replyTo) &&
+ java.util.Arrays.equals(this.to, otherMessage.to) &&
+ java.util.Arrays.equals(this.cc, otherMessage.cc) &&
+ java.util.Arrays.equals(this.bcc, otherMessage.bcc) &&
+ ObjectUtils.nullSafeEquals(this.sentDate, otherMessage.sentDate) &&
+ ObjectUtils.nullSafeEquals(this.subject, otherMessage.subject) &&
+ ObjectUtils.nullSafeEquals(this.text, otherMessage.text));
+ }
+
+ public int hashCode() {
+ int hashCode = (this.from == null ? 0 : this.from.hashCode());
+ hashCode = 29 * hashCode + (this.replyTo == null ? 0 : this.replyTo.hashCode());
+ for (int i = 0; this.to != null && i < this.to.length; i++) {
+ hashCode = 29 * hashCode + (this.to == null ? 0 : this.to[i].hashCode());
+ }
+ for (int i = 0; this.cc != null && i < this.cc.length; i++) {
+ hashCode = 29 * hashCode + (this.cc == null ? 0 : this.cc[i].hashCode());
+ }
+ for (int i = 0; this.bcc != null && i < this.bcc.length; i++) {
+ hashCode = 29 * hashCode + (this.bcc == null ? 0 : this.bcc[i].hashCode());
+ }
+ hashCode = 29 * hashCode + (this.sentDate == null ? 0 : this.sentDate.hashCode());
+ hashCode = 29 * hashCode + (this.subject == null ? 0 : this.subject.hashCode());
+ hashCode = 29 * hashCode + (this.text == null ? 0 : this.text.hashCode());
+ return hashCode;
+ }
+
+
+ private static String[] copy(String[] state) {
+ String[] copy = new String[state.length];
+ System.arraycopy(state, 0, copy, 0, state.length);
+ return copy;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java
new file mode 100644
index 00000000000..9010d78e9a3
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+/**
+ * Spring-configurable FileTypeMap implementation that will read
+ * MIME type to file extension mappings from a standard JavaMail MIME type
+ * mapping file, using a standard MimetypesFileTypeMap underneath.
+ *
+ *
The mapping file should be in the following format, as specified by the + * Java Activation Framework: + * + *
+ * # map text/html to .htm and .html files + * text/html html htm HTML HTM+ * + * Lines starting with
# are treated as comments and are ignored. All
+ * other lines are treated as mappings. Each mapping line should contain the MIME
+ * type as the first entry and then each file extension to map to that MIME type
+ * as subsequent entries. Each entry is separated by spaces or tabs.
+ *
+ * By default, the mappings in the mime.types file located in the
+ * same package as this class are used, which cover many common file extensions
+ * (in contrast to the out-of-the-box mappings in activation.jar).
+ * This can be overridden using the mappingLocation property.
+ *
+ *
Additional mappings can be added via the Needs to follow the The default implementation creates an Activation Framework {@link MimetypesFileTypeMap},
+ * passing in an InputStream from the mapping resource (if any) and registering
+ * the mapping lines programmatically.
+ * @param mappingLocation a Expects the same syntax as InternetAddress's constructor with
+ * a String argument. Converts empty Strings into null values.
+ *
+ * @author Juergen Hoeller
+ * @since 1.2.3
+ * @see javax.mail.internet.InternetAddress
+ */
+public class InternetAddressEditor extends PropertyEditorSupport {
+
+ public void setAsText(String text) throws IllegalArgumentException {
+ if (StringUtils.hasText(text)) {
+ try {
+ setValue(new InternetAddress(text));
+ }
+ catch (AddressException ex) {
+ throw new IllegalArgumentException("Could not parse mail address: " + ex.getMessage());
+ }
+ }
+ else {
+ setValue(null);
+ }
+ }
+
+ public String getAsText() {
+ InternetAddress value = (InternetAddress) getValue();
+ return (value != null ? value.toUnicodeString() : "");
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java
new file mode 100644
index 00000000000..ea2a0efbd0a
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.io.InputStream;
+
+import javax.mail.internet.MimeMessage;
+
+import org.springframework.mail.MailException;
+import org.springframework.mail.MailSender;
+
+/**
+ * Extended {@link org.springframework.mail.MailSender} interface for JavaMail,
+ * supporting MIME messages both as direct arguments and through preparation
+ * callbacks. Typically used in conjunction with the {@link MimeMessageHelper}
+ * class for convenient creation of JavaMail {@link MimeMessage MimeMessages},
+ * including attachments etc.
+ *
+ * Clients should talk to the mail sender through this interface if they need
+ * mail functionality beyond {@link org.springframework.mail.SimpleMailMessage}.
+ * The production implementation is {@link JavaMailSenderImpl}; for testing,
+ * mocks can be created based on this interface. Clients will typically receive
+ * the JavaMailSender reference through dependency injection.
+ *
+ * The recommended way of using this interface is the {@link MimeMessagePreparator}
+ * mechanism, possibly using a {@link MimeMessageHelper} for populating the message.
+ * See {@link MimeMessageHelper MimeMessageHelper's javadoc} for an example.
+ *
+ * The entire JavaMail {@link javax.mail.Session} management is abstracted
+ * by the JavaMailSender. Client code should not deal with a Session in any way,
+ * rather leave the entire JavaMail configuration and resource handling to the
+ * JavaMailSender implementation. This also increases testability.
+ *
+ * A JavaMailSender client is not as easy to test as a plain
+ * {@link org.springframework.mail.MailSender} client, but still straightforward
+ * compared to traditional JavaMail code: Just let {@link #createMimeMessage()}
+ * return a plain {@link MimeMessage} created with a
+ * Alternative way to prepare MimeMessage instances, instead of
+ * {@link #createMimeMessage()} and {@link #send(MimeMessage)} calls.
+ * Takes care of proper exception conversion.
+ * @param mimeMessagePreparator the preparator to use
+ * @throws org.springframework.mail.MailPreparationException
+ * in case of failure when preparing the message
+ * @throws org.springframework.mail.MailParseException
+ * in case of failure when parsing the message
+ * @throws org.springframework.mail.MailAuthenticationException
+ * in case of authentication failure
+ * @throws org.springframework.mail.MailSendException
+ * in case of failure when sending the message
+ */
+ void send(MimeMessagePreparator mimeMessagePreparator) throws MailException;
+
+ /**
+ * Send the JavaMail MIME messages prepared by the given MimeMessagePreparators.
+ * Alternative way to prepare MimeMessage instances, instead of
+ * {@link #createMimeMessage()} and {@link #send(MimeMessage[])} calls.
+ * Takes care of proper exception conversion.
+ * @param mimeMessagePreparators the preparator to use
+ * @throws org.springframework.mail.MailPreparationException
+ * in case of failure when preparing a message
+ * @throws org.springframework.mail.MailParseException
+ * in case of failure when parsing a message
+ * @throws org.springframework.mail.MailAuthenticationException
+ * in case of authentication failure
+ * @throws org.springframework.mail.MailSendException
+ * in case of failure when sending a message
+ */
+ void send(MimeMessagePreparator[] mimeMessagePreparators) throws MailException;
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java
new file mode 100644
index 00000000000..83f78f37659
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.activation.FileTypeMap;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.MessagingException;
+import javax.mail.NoSuchProviderException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.MimeMessage;
+
+import org.springframework.mail.MailAuthenticationException;
+import org.springframework.mail.MailException;
+import org.springframework.mail.MailParseException;
+import org.springframework.mail.MailPreparationException;
+import org.springframework.mail.MailSendException;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.util.Assert;
+
+/**
+ * Production implementation of the {@link JavaMailSender} interface,
+ * supporting both JavaMail {@link MimeMessage MimeMessages} and Spring
+ * {@link SimpleMailMessage SimpleMailMessages}. Can also be used as a
+ * plain {@link org.springframework.mail.MailSender} implementation.
+ *
+ * Allows for defining all settings locally as bean properties.
+ * Alternatively, a pre-configured JavaMail {@link javax.mail.Session} can be
+ * specified, possibly pulled from an application server's JNDI environment.
+ *
+ * Non-default properties in this object will always override the settings
+ * in the JavaMail Initializes the {@link #setDefaultFileTypeMap "defaultFileTypeMap"}
+ * property with a default {@link ConfigurableMimeFileTypeMap}.
+ */
+ public JavaMailSenderImpl() {
+ ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
+ fileTypeMap.afterPropertiesSet();
+ this.defaultFileTypeMap = fileTypeMap;
+ }
+
+
+ /**
+ * Set JavaMail properties for the A new Non-default properties in this instance will override given
+ * JavaMail properties.
+ */
+ public void setJavaMailProperties(Properties javaMailProperties) {
+ this.javaMailProperties = javaMailProperties;
+ synchronized (this) {
+ this.session = null;
+ }
+ }
+
+ /**
+ * Allow Map access to the JavaMail properties of this sender,
+ * with the option to add or override specific entries.
+ * Useful for specifying entries directly, for example via
+ * "javaMailProperties[mail.smtp.auth]".
+ */
+ public Properties getJavaMailProperties() {
+ return this.javaMailProperties;
+ }
+
+ /**
+ * Set the JavaMail Default is a new If using a pre-configured Default is the default host of the underlying JavaMail Session.
+ */
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ /**
+ * Return the mail server host.
+ */
+ public String getHost() {
+ return this.host;
+ }
+
+ /**
+ * Set the mail server port.
+ * Default is {@link #DEFAULT_PORT}, letting JavaMail use the default
+ * SMTP port (25).
+ */
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Return the mail server port.
+ */
+ public int getPort() {
+ return this.port;
+ }
+
+ /**
+ * Set the username for the account at the mail host, if any.
+ * Note that the underlying JavaMail Note that the underlying JavaMail Such an encoding will be auto-detected by {@link MimeMessageHelper}.
+ */
+ public void setDefaultEncoding(String defaultEncoding) {
+ this.defaultEncoding = defaultEncoding;
+ }
+
+ /**
+ * Return the default encoding for {@link MimeMessage MimeMessages},
+ * or A For example, you can specify a custom instance of Spring's
+ * {@link ConfigurableMimeFileTypeMap} here. If not explicitly specified,
+ * a default Can be overridden in subclasses, e.g. to return a mock Transport object.
+ * @see javax.mail.Session#getTransport(String)
+ * @see #getProtocol()
+ */
+ protected Transport getTransport(Session session) throws NoSuchProviderException {
+ return session.getTransport(getProtocol());
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java
new file mode 100644
index 00000000000..242f0e5f272
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.util.Date;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.springframework.mail.MailMessage;
+import org.springframework.mail.MailParseException;
+
+/**
+ * Implementation of the MailMessage interface for a JavaMail MIME message,
+ * to let message population code interact with a simple message or a MIME
+ * message through a common interface.
+ *
+ * Uses a MimeMessageHelper underneath. Can either be created with a
+ * MimeMessageHelper instance or with a JavaMail MimeMessage instance.
+ *
+ * @author Juergen Hoeller
+ * @since 1.1.5
+ * @see MimeMessageHelper
+ * @see javax.mail.internet.MimeMessage
+ */
+public class MimeMailMessage implements MailMessage {
+
+ private final MimeMessageHelper helper;
+
+
+ /**
+ * Create a new MimeMailMessage based on the given MimeMessageHelper.
+ * @param mimeMessageHelper the MimeMessageHelper
+ */
+ public MimeMailMessage(MimeMessageHelper mimeMessageHelper) {
+ this.helper = mimeMessageHelper;
+ }
+
+ /**
+ * Create a new MimeMailMessage based on the given JavaMail MimeMessage.
+ * @param mimeMessage the JavaMail MimeMessage
+ */
+ public MimeMailMessage(MimeMessage mimeMessage) {
+ this.helper = new MimeMessageHelper(mimeMessage);
+ }
+
+ /**
+ * Return the MimeMessageHelper that this MimeMailMessage is based on.
+ */
+ public final MimeMessageHelper getMimeMessageHelper() {
+ return this.helper;
+ }
+
+ /**
+ * Return the JavaMail MimeMessage that this MimeMailMessage is based on.
+ */
+ public final MimeMessage getMimeMessage() {
+ return this.helper.getMimeMessage();
+ }
+
+
+ public void setFrom(String from) throws MailParseException {
+ try {
+ this.helper.setFrom(from);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setReplyTo(String replyTo) throws MailParseException {
+ try {
+ this.helper.setReplyTo(replyTo);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setTo(String to) throws MailParseException {
+ try {
+ this.helper.setTo(to);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setTo(String[] to) throws MailParseException {
+ try {
+ this.helper.setTo(to);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setCc(String cc) throws MailParseException {
+ try {
+ this.helper.setCc(cc);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setCc(String[] cc) throws MailParseException {
+ try {
+ this.helper.setCc(cc);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setBcc(String bcc) throws MailParseException {
+ try {
+ this.helper.setBcc(bcc);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setBcc(String[] bcc) throws MailParseException {
+ try {
+ this.helper.setBcc(bcc);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setSentDate(Date sentDate) throws MailParseException {
+ try {
+ this.helper.setSentDate(sentDate);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setSubject(String subject) throws MailParseException {
+ try {
+ this.helper.setSubject(subject);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+ public void setText(String text) throws MailParseException {
+ try {
+ this.helper.setText(text);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java
new file mode 100644
index 00000000000..b30a4a9efc3
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java
@@ -0,0 +1,1086 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.activation.FileDataSource;
+import javax.activation.FileTypeMap;
+import javax.mail.BodyPart;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimePart;
+
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.Assert;
+
+/**
+ * Helper class for populating a {@link javax.mail.internet.MimeMessage}.
+ *
+ * Mirrors the simple setters of {@link org.springframework.mail.SimpleMailMessage},
+ * directly applying the values to the underlying MimeMessage. Allows for defining
+ * a character encoding for the entire message, automatically applied by all methods
+ * of this helper class.
+ *
+ * Offers support for HTML text content, inline elements such as images, and typical
+ * mail attachments. Also supports personal names that accompany mail addresses. Note that
+ * advanced settings can still be applied directly to the underlying MimeMessage object!
+ *
+ * Typically used in {@link MimeMessagePreparator} implementations or
+ * {@link JavaMailSender} client code: simply instantiating it as a MimeMessage wrapper,
+ * invoking setters on the wrapper, using the underlying MimeMessage for mail sending.
+ * Also used internally by {@link JavaMailSenderImpl}.
+ *
+ * Sample code for an HTML mail with an inline image and a PDF attachment:
+ *
+ * Warning regarding multipart mails: Simple MIME messages that
+ * just contain HTML text but no inline elements or attachments will work on
+ * more or less any email client that is capable of HTML rendering. However,
+ * inline elements and attachments are still a major compatibility issue
+ * between email clients: It's virtually impossible to get inline elements
+ * and attachments working across Microsoft Outlook, Lotus Notes and Mac Mail.
+ * Consider choosing a specific multipart mode for your needs: The javadoc
+ * on the MULTIPART_MODE constants contains more detailed information.
+ *
+ * @author Juergen Hoeller
+ * @since 19.01.2004
+ * @see #setText(String, boolean)
+ * @see #setText(String, String)
+ * @see #addInline(String, org.springframework.core.io.Resource)
+ * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
+ * @see #MULTIPART_MODE_MIXED_RELATED
+ * @see #MULTIPART_MODE_RELATED
+ * @see #getMimeMessage()
+ * @see JavaMailSender
+ */
+public class MimeMessageHelper {
+
+ /**
+ * Constant indicating a non-multipart message.
+ */
+ public static final int MULTIPART_MODE_NO = 0;
+
+ /**
+ * Constant indicating a multipart message with a single root multipart
+ * element of type "mixed". Texts, inline elements and attachements
+ * will all get added to that root element.
+ * This was Spring 1.0's default behavior. It is known to work properly
+ * on Outlook. However, other mail clients tend to misinterpret inline
+ * elements as attachments and/or show attachments inline as well.
+ */
+ public static final int MULTIPART_MODE_MIXED = 1;
+
+ /**
+ * Constant indicating a multipart message with a single root multipart
+ * element of type "related". Texts, inline elements and attachements
+ * will all get added to that root element.
+ * This was the default behavior from Spring 1.1 up to 1.2 final.
+ * This is the "Microsoft multipart mode", as natively sent by Outlook.
+ * It is known to work properly on Outlook, Outlook Express, Yahoo Mail, and
+ * to a large degree also on Mac Mail (with an additional attachment listed
+ * for an inline element, despite the inline element also shown inline).
+ * Does not work properly on Lotus Notes (attachments won't be shown there).
+ */
+ public static final int MULTIPART_MODE_RELATED = 2;
+
+ /**
+ * Constant indicating a multipart message with a root multipart element
+ * "mixed" plus a nested multipart element of type "related". Texts and
+ * inline elements will get added to the nested "related" element,
+ * while attachments will get added to the "mixed" root element.
+ * This is the default since Spring 1.2.1. This is arguably the most correct
+ * MIME structure, according to the MIME spec: It is known to work properly
+ * on Outlook, Outlook Express, Yahoo Mail, and Lotus Notes. Does not work
+ * properly on Mac Mail. If you target Mac Mail or experience issues with
+ * specific mails on Outlook, consider using MULTIPART_MODE_RELATED instead.
+ */
+ public static final int MULTIPART_MODE_MIXED_RELATED = 3;
+
+
+ private static final String MULTIPART_SUBTYPE_MIXED = "mixed";
+
+ private static final String MULTIPART_SUBTYPE_RELATED = "related";
+
+ private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative";
+
+ private static final String CONTENT_TYPE_ALTERNATIVE = "text/alternative";
+
+ private static final String CONTENT_TYPE_HTML = "text/html";
+
+ private static final String CONTENT_TYPE_CHARSET_SUFFIX = ";charset=";
+
+ private static final String HEADER_PRIORITY = "X-Priority";
+
+ private static final String HEADER_CONTENT_ID = "Content-ID";
+
+
+ private final MimeMessage mimeMessage;
+
+ private MimeMultipart rootMimeMultipart;
+
+ private MimeMultipart mimeMultipart;
+
+ private final String encoding;
+
+ private FileTypeMap fileTypeMap;
+
+ private boolean validateAddresses = false;
+
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * assuming a simple text message (no multipart content,
+ * i.e. no alternative texts and no inline elements or attachments).
+ * The character encoding for the message will be taken from
+ * the passed-in MimeMessage object, if carried there. Else,
+ * JavaMail's default encoding will be used.
+ * @param mimeMessage MimeMessage to work on
+ * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
+ * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
+ * @see JavaMailSenderImpl#setDefaultEncoding
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage) {
+ this(mimeMessage, null);
+ }
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * assuming a simple text message (no multipart content,
+ * i.e. no alternative texts and no inline elements or attachments).
+ * @param mimeMessage MimeMessage to work on
+ * @param encoding the character encoding to use for the message
+ * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean)
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage, String encoding) {
+ this.mimeMessage = mimeMessage;
+ this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
+ this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
+ }
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * in multipart mode (supporting alternative texts, inline
+ * elements and attachments) if requested.
+ * Consider using the MimeMessageHelper constructor that
+ * takes a multipartMode argument to choose a specific multipart
+ * mode other than MULTIPART_MODE_MIXED_RELATED.
+ * The character encoding for the message will be taken from
+ * the passed-in MimeMessage object, if carried there. Else,
+ * JavaMail's default encoding will be used.
+ * @param mimeMessage MimeMessage to work on
+ * @param multipart whether to create a multipart message that
+ * supports alternative texts, inline elements and attachments
+ * (corresponds to MULTIPART_MODE_MIXED_RELATED)
+ * @throws MessagingException if multipart creation failed
+ * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int)
+ * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
+ * @see JavaMailSenderImpl#setDefaultEncoding
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException {
+ this(mimeMessage, multipart, null);
+ }
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * in multipart mode (supporting alternative texts, inline
+ * elements and attachments) if requested.
+ * Consider using the MimeMessageHelper constructor that
+ * takes a multipartMode argument to choose a specific multipart
+ * mode other than MULTIPART_MODE_MIXED_RELATED.
+ * @param mimeMessage MimeMessage to work on
+ * @param multipart whether to create a multipart message that
+ * supports alternative texts, inline elements and attachments
+ * (corresponds to MULTIPART_MODE_MIXED_RELATED)
+ * @param encoding the character encoding to use for the message
+ * @throws MessagingException if multipart creation failed
+ * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String)
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, String encoding)
+ throws MessagingException {
+
+ this(mimeMessage, (multipart ? MULTIPART_MODE_MIXED_RELATED : MULTIPART_MODE_NO), encoding);
+ }
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * in multipart mode (supporting alternative texts, inline
+ * elements and attachments) if requested.
+ * The character encoding for the message will be taken from
+ * the passed-in MimeMessage object, if carried there. Else,
+ * JavaMail's default encoding will be used.
+ * @param mimeMessage MimeMessage to work on
+ * @param multipartMode which kind of multipart message to create
+ * (MIXED, RELATED, MIXED_RELATED, or NO)
+ * @throws MessagingException if multipart creation failed
+ * @see #MULTIPART_MODE_NO
+ * @see #MULTIPART_MODE_MIXED
+ * @see #MULTIPART_MODE_RELATED
+ * @see #MULTIPART_MODE_MIXED_RELATED
+ * @see #getDefaultEncoding(javax.mail.internet.MimeMessage)
+ * @see JavaMailSenderImpl#setDefaultEncoding
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
+ this(mimeMessage, multipartMode, null);
+ }
+
+ /**
+ * Create a new MimeMessageHelper for the given MimeMessage,
+ * in multipart mode (supporting alternative texts, inline
+ * elements and attachments) if requested.
+ * @param mimeMessage MimeMessage to work on
+ * @param multipartMode which kind of multipart message to create
+ * (MIXED, RELATED, MIXED_RELATED, or NO)
+ * @param encoding the character encoding to use for the message
+ * @throws MessagingException if multipart creation failed
+ * @see #MULTIPART_MODE_NO
+ * @see #MULTIPART_MODE_MIXED
+ * @see #MULTIPART_MODE_RELATED
+ * @see #MULTIPART_MODE_MIXED_RELATED
+ */
+ public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode, String encoding)
+ throws MessagingException {
+
+ this.mimeMessage = mimeMessage;
+ createMimeMultiparts(mimeMessage, multipartMode);
+ this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage));
+ this.fileTypeMap = getDefaultFileTypeMap(mimeMessage);
+ }
+
+
+ /**
+ * Return the underlying MimeMessage object.
+ */
+ public final MimeMessage getMimeMessage() {
+ return this.mimeMessage;
+ }
+
+
+ /**
+ * Determine the MimeMultipart objects to use, which will be used
+ * to store attachments on the one hand and text(s) and inline elements
+ * on the other hand.
+ * Texts and inline elements can either be stored in the root element
+ * itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED) or in a nested element
+ * rather than the root element directly (MULTIPART_MODE_MIXED_RELATED).
+ * By default, the root MimeMultipart element will be of type "mixed"
+ * (MULTIPART_MODE_MIXED) or "related" (MULTIPART_MODE_RELATED).
+ * The main multipart element will either be added as nested element of
+ * type "related" (MULTIPART_MODE_MIXED_RELATED) or be identical to the root
+ * element itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED).
+ * @param mimeMessage the MimeMessage object to add the root MimeMultipart
+ * object to
+ * @param multipartMode the multipart mode, as passed into the constructor
+ * (MIXED, RELATED, MIXED_RELATED, or NO)
+ * @throws MessagingException if multipart creation failed
+ * @see #setMimeMultiparts
+ * @see #MULTIPART_MODE_NO
+ * @see #MULTIPART_MODE_MIXED
+ * @see #MULTIPART_MODE_RELATED
+ * @see #MULTIPART_MODE_MIXED_RELATED
+ */
+ protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException {
+ switch (multipartMode) {
+ case MULTIPART_MODE_NO:
+ setMimeMultiparts(null, null);
+ break;
+ case MULTIPART_MODE_MIXED:
+ MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
+ mimeMessage.setContent(mixedMultipart);
+ setMimeMultiparts(mixedMultipart, mixedMultipart);
+ break;
+ case MULTIPART_MODE_RELATED:
+ MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
+ mimeMessage.setContent(relatedMultipart);
+ setMimeMultiparts(relatedMultipart, relatedMultipart);
+ break;
+ case MULTIPART_MODE_MIXED_RELATED:
+ MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED);
+ mimeMessage.setContent(rootMixedMultipart);
+ MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED);
+ MimeBodyPart relatedBodyPart = new MimeBodyPart();
+ relatedBodyPart.setContent(nestedRelatedMultipart);
+ rootMixedMultipart.addBodyPart(relatedBodyPart);
+ setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart);
+ break;
+ default:
+ throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported");
+ }
+ }
+
+ /**
+ * Set the given MimeMultipart objects for use by this MimeMessageHelper.
+ * @param root the root MimeMultipart object, which attachments will be added to;
+ * or This will be the direct content of the MimeMessage,
+ * in case of a multipart mail.
+ * @throws IllegalStateException if this helper is not in multipart mode
+ * @see #isMultipart
+ * @see #getMimeMessage
+ * @see javax.mail.internet.MimeMultipart#addBodyPart
+ */
+ public final MimeMultipart getRootMimeMultipart() throws IllegalStateException {
+ checkMultipart();
+ return this.rootMimeMultipart;
+ }
+
+ /**
+ * Return the underlying MIME "multipart/related" object, if any.
+ * Can be used to manually add body parts, inline elements, etc.
+ * This will be nested within the root MimeMultipart,
+ * in case of a multipart mail.
+ * @throws IllegalStateException if this helper is not in multipart mode
+ * @see #isMultipart
+ * @see #getRootMimeMultipart
+ * @see javax.mail.internet.MimeMultipart#addBodyPart
+ */
+ public final MimeMultipart getMimeMultipart() throws IllegalStateException {
+ checkMultipart();
+ return this.mimeMultipart;
+ }
+
+
+ /**
+ * Determine the default encoding for the given MimeMessage.
+ * @param mimeMessage the passed-in MimeMessage
+ * @return the default encoding associated with the MimeMessage,
+ * or Default is the Note that this is by default just available for JavaMail >= 1.3.
+ * You can override the default Default implementation invokes Note that this method will just work on JavaMail >= 1.3. You can override
+ * it for validation on older JavaMail versions or for custom validation.
+ * @param address the address to validate
+ * @throws AddressException if validation failed
+ * @see #isValidateAddresses()
+ * @see javax.mail.internet.InternetAddress#validate()
+ */
+ protected void validateAddress(InternetAddress address) throws AddressException {
+ if (isValidateAddresses()) {
+ address.validate();
+ }
+ }
+
+ /**
+ * Validate all given mail addresses.
+ * Default implementation simply delegates to validateAddress for each address.
+ * @param addresses the addresses to validate
+ * @throws AddressException if validation failed
+ * @see #validateAddress(InternetAddress)
+ */
+ protected void validateAddresses(InternetAddress[] addresses) throws AddressException {
+ for (int i = 0; i < addresses.length; i++) {
+ validateAddress(addresses[i]);
+ }
+ }
+
+
+ public void setFrom(InternetAddress from) throws MessagingException {
+ Assert.notNull(from, "From address must not be null");
+ validateAddress(from);
+ this.mimeMessage.setFrom(from);
+ }
+
+ public void setFrom(String from) throws MessagingException {
+ Assert.notNull(from, "From address must not be null");
+ setFrom(new InternetAddress(from));
+ }
+
+ public void setFrom(String from, String personal) throws MessagingException, UnsupportedEncodingException {
+ Assert.notNull(from, "From address must not be null");
+ setFrom(getEncoding() != null ?
+ new InternetAddress(from, personal, getEncoding()) : new InternetAddress(from, personal));
+ }
+
+ public void setReplyTo(InternetAddress replyTo) throws MessagingException {
+ Assert.notNull(replyTo, "Reply-to address must not be null");
+ validateAddress(replyTo);
+ this.mimeMessage.setReplyTo(new InternetAddress[] {replyTo});
+ }
+
+ public void setReplyTo(String replyTo) throws MessagingException {
+ Assert.notNull(replyTo, "Reply-to address must not be null");
+ setReplyTo(new InternetAddress(replyTo));
+ }
+
+ public void setReplyTo(String replyTo, String personal) throws MessagingException, UnsupportedEncodingException {
+ Assert.notNull(replyTo, "Reply-to address must not be null");
+ InternetAddress replyToAddress = (getEncoding() != null) ?
+ new InternetAddress(replyTo, personal, getEncoding()) : new InternetAddress(replyTo, personal);
+ setReplyTo(replyToAddress);
+ }
+
+
+ public void setTo(InternetAddress to) throws MessagingException {
+ Assert.notNull(to, "To address must not be null");
+ validateAddress(to);
+ this.mimeMessage.setRecipient(Message.RecipientType.TO, to);
+ }
+
+ public void setTo(InternetAddress[] to) throws MessagingException {
+ Assert.notNull(to, "To address array must not be null");
+ validateAddresses(to);
+ this.mimeMessage.setRecipients(Message.RecipientType.TO, to);
+ }
+
+ public void setTo(String to) throws MessagingException {
+ Assert.notNull(to, "To address must not be null");
+ setTo(new InternetAddress(to));
+ }
+
+ public void setTo(String[] to) throws MessagingException {
+ Assert.notNull(to, "To address array must not be null");
+ InternetAddress[] addresses = new InternetAddress[to.length];
+ for (int i = 0; i < to.length; i++) {
+ addresses[i] = new InternetAddress(to[i]);
+ }
+ setTo(addresses);
+ }
+
+ public void addTo(InternetAddress to) throws MessagingException {
+ Assert.notNull(to, "To address must not be null");
+ validateAddress(to);
+ this.mimeMessage.addRecipient(Message.RecipientType.TO, to);
+ }
+
+ public void addTo(String to) throws MessagingException {
+ Assert.notNull(to, "To address must not be null");
+ addTo(new InternetAddress(to));
+ }
+
+ public void addTo(String to, String personal) throws MessagingException, UnsupportedEncodingException {
+ Assert.notNull(to, "To address must not be null");
+ addTo(getEncoding() != null ?
+ new InternetAddress(to, personal, getEncoding()) :
+ new InternetAddress(to, personal));
+ }
+
+
+ public void setCc(InternetAddress cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address must not be null");
+ validateAddress(cc);
+ this.mimeMessage.setRecipient(Message.RecipientType.CC, cc);
+ }
+
+ public void setCc(InternetAddress[] cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address array must not be null");
+ validateAddresses(cc);
+ this.mimeMessage.setRecipients(Message.RecipientType.CC, cc);
+ }
+
+ public void setCc(String cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address must not be null");
+ setCc(new InternetAddress(cc));
+ }
+
+ public void setCc(String[] cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address array must not be null");
+ InternetAddress[] addresses = new InternetAddress[cc.length];
+ for (int i = 0; i < cc.length; i++) {
+ addresses[i] = new InternetAddress(cc[i]);
+ }
+ setCc(addresses);
+ }
+
+ public void addCc(InternetAddress cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address must not be null");
+ validateAddress(cc);
+ this.mimeMessage.addRecipient(Message.RecipientType.CC, cc);
+ }
+
+ public void addCc(String cc) throws MessagingException {
+ Assert.notNull(cc, "Cc address must not be null");
+ addCc(new InternetAddress(cc));
+ }
+
+ public void addCc(String cc, String personal) throws MessagingException, UnsupportedEncodingException {
+ Assert.notNull(cc, "Cc address must not be null");
+ addCc(getEncoding() != null ?
+ new InternetAddress(cc, personal, getEncoding()) :
+ new InternetAddress(cc, personal));
+ }
+
+
+ public void setBcc(InternetAddress bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address must not be null");
+ validateAddress(bcc);
+ this.mimeMessage.setRecipient(Message.RecipientType.BCC, bcc);
+ }
+
+ public void setBcc(InternetAddress[] bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address array must not be null");
+ validateAddresses(bcc);
+ this.mimeMessage.setRecipients(Message.RecipientType.BCC, bcc);
+ }
+
+ public void setBcc(String bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address must not be null");
+ setBcc(new InternetAddress(bcc));
+ }
+
+ public void setBcc(String[] bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address array must not be null");
+ InternetAddress[] addresses = new InternetAddress[bcc.length];
+ for (int i = 0; i < bcc.length; i++) {
+ addresses[i] = new InternetAddress(bcc[i]);
+ }
+ setBcc(addresses);
+ }
+
+ public void addBcc(InternetAddress bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address must not be null");
+ validateAddress(bcc);
+ this.mimeMessage.addRecipient(Message.RecipientType.BCC, bcc);
+ }
+
+ public void addBcc(String bcc) throws MessagingException {
+ Assert.notNull(bcc, "Bcc address must not be null");
+ addBcc(new InternetAddress(bcc));
+ }
+
+ public void addBcc(String bcc, String personal) throws MessagingException, UnsupportedEncodingException {
+ Assert.notNull(bcc, "Bcc address must not be null");
+ addBcc(getEncoding() != null ?
+ new InternetAddress(bcc, personal, getEncoding()) :
+ new InternetAddress(bcc, personal));
+ }
+
+
+ /**
+ * Set the priority ("X-Priority" header) of the message.
+ * @param priority the priority value;
+ * typically between 1 (highest) and 5 (lowest)
+ * @throws MessagingException in case of errors
+ */
+ public void setPriority(int priority) throws MessagingException {
+ this.mimeMessage.setHeader(HEADER_PRIORITY, Integer.toString(priority));
+ }
+
+ /**
+ * Set the sent-date of the message.
+ * @param sentDate the date to set (never NOTE: Invoke {@link #addInline} after NOTE: Invoke {@link #addInline} after NOTE: Invoke {@link #addInline} after Note that the InputStream returned by the DataSource implementation
+ * needs to be a fresh one on each call, as JavaMail will invoke
+ * NOTE: Invoke The content type will be determined by the name of the given
+ * content file. Do not use this for temporary files with arbitrary
+ * filenames (possibly ending in ".tmp" or the like)!
+ * NOTE: Invoke The content type will be determined by the name of the given
+ * content file. Do not use this for temporary files with arbitrary
+ * filenames (possibly ending in ".tmp" or the like)!
+ * Note that the InputStream returned by the Resource implementation
+ * needs to be a fresh one on each call, as JavaMail will invoke
+ * NOTE: Invoke You can determine the content type for any given filename via a Java
+ * Activation Framework's FileTypeMap, for example the one held by this helper.
+ * Note that the InputStream returned by the InputStreamSource implementation
+ * needs to be a fresh one on each call, as JavaMail will invoke
+ * NOTE: Invoke Note that the InputStream returned by the DataSource implementation
+ * needs to be a fresh one on each call, as JavaMail will invoke
+ * The content type will be determined by the name of the given
+ * content file. Do not use this for temporary files with arbitrary
+ * filenames (possibly ending in ".tmp" or the like)!
+ * @param attachmentFilename the name of the attachment as it will
+ * appear in the mail
+ * @param file the File resource to take the content from
+ * @throws MessagingException in case of errors
+ * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
+ * @see #addAttachment(String, javax.activation.DataSource)
+ */
+ public void addAttachment(String attachmentFilename, File file) throws MessagingException {
+ Assert.notNull(file, "File must not be null");
+ FileDataSource dataSource = new FileDataSource(file);
+ dataSource.setFileTypeMap(getFileTypeMap());
+ addAttachment(attachmentFilename, dataSource);
+ }
+
+ /**
+ * Add an attachment to the MimeMessage, taking the content from an
+ * The content type will be determined by the given filename for
+ * the attachment. Thus, any content source will be fine, including
+ * temporary files with arbitrary filenames.
+ * Note that the InputStream returned by the InputStreamSource
+ * implementation needs to be a fresh one on each call, as
+ * JavaMail will invoke Note that the InputStream returned by the InputStreamSource
+ * implementation needs to be a fresh one on each call, as
+ * JavaMail will invoke The corresponding It is often convenient to use a {@link MimeMessageHelper} for populating
+ * the passed-in MimeMessage, in particular when working with attachments or
+ * special character encodings.
+ * See {@link MimeMessageHelper MimeMessageHelper's javadoc} for an example.
+ *
+ * @author Juergen Hoeller
+ * @since 07.10.2003
+ * @see JavaMailSender#send(MimeMessagePreparator)
+ * @see JavaMailSender#send(MimeMessagePreparator[])
+ * @see MimeMessageHelper
+ */
+public interface MimeMessagePreparator {
+
+ /**
+ * Prepare the given new MimeMessage instance.
+ * @param mimeMessage the message to prepare
+ * @throws javax.mail.MessagingException passing any exceptions thrown by MimeMessage
+ * methods through for automatic conversion to the MailException hierarchy
+ * @throws java.io.IOException passing any exceptions thrown by MimeMessage methods
+ * through for automatic conversion to the MailException hierarchy
+ * @throws Exception if mail preparation failed, for example when a
+ * Velocity template cannot be rendered for the mail text
+ */
+ void prepare(MimeMessage mimeMessage) throws Exception;
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java
new file mode 100644
index 00000000000..6a3bcd49570
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import javax.activation.FileTypeMap;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+
+/**
+ * Special subclass of the standard JavaMail {@link MimeMessage}, carrying a
+ * default encoding to be used when populating the message and a default Java
+ * Activation {@link FileTypeMap} to be used for resolving attachment types.
+ *
+ * Created by {@link JavaMailSenderImpl} in case of a specified default encoding
+ * and/or default FileTypeMap. Autodetected by {@link MimeMessageHelper}, which
+ * will use the carried encoding and FileTypeMap unless explicitly overridden.
+ *
+ * @author Juergen Hoeller
+ * @since 1.2
+ * @see JavaMailSenderImpl#createMimeMessage()
+ * @see MimeMessageHelper#getDefaultEncoding(javax.mail.internet.MimeMessage)
+ * @see MimeMessageHelper#getDefaultFileTypeMap(javax.mail.internet.MimeMessage)
+ */
+class SmartMimeMessage extends MimeMessage {
+
+ private final String defaultEncoding;
+
+ private final FileTypeMap defaultFileTypeMap;
+
+
+ /**
+ * Create a new SmartMimeMessage.
+ * @param session the JavaMail Session to create the message for
+ * @param defaultEncoding the default encoding, or The CommonJ TimerManager does not offer more sophisticated scheduling
+ * options such as cron expressions. Consider using Quartz for such
+ * advanced needs.
+ *
+ * Note that the TimerManager uses a TimerListener instance that is
+ * shared between repeated executions, in contrast to Quartz which
+ * instantiates a new Job for each execution.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see commonj.timers.TimerListener
+ * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long)
+ * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long)
+ */
+public class ScheduledTimerListener {
+
+ private TimerListener timerListener;
+
+ private long delay = 0;
+
+ private long period = -1;
+
+ private boolean fixedRate = false;
+
+
+ /**
+ * Create a new ScheduledTimerListener,
+ * to be populated via bean properties.
+ * @see #setTimerListener
+ * @see #setDelay
+ * @see #setPeriod
+ * @see #setFixedRate
+ */
+ public ScheduledTimerListener() {
+ }
+
+ /**
+ * Create a new ScheduledTimerListener, with default
+ * one-time execution without delay.
+ * @param timerListener the TimerListener to schedule
+ */
+ public ScheduledTimerListener(TimerListener timerListener) {
+ this.timerListener = timerListener;
+ }
+
+ /**
+ * Create a new ScheduledTimerListener, with default
+ * one-time execution with the given delay.
+ * @param timerListener the TimerListener to schedule
+ * @param delay the delay before starting the task for the first time (ms)
+ */
+ public ScheduledTimerListener(TimerListener timerListener, long delay) {
+ this.timerListener = timerListener;
+ this.delay = delay;
+ }
+
+ /**
+ * Create a new ScheduledTimerListener.
+ * @param timerListener the TimerListener to schedule
+ * @param delay the delay before starting the task for the first time (ms)
+ * @param period the period between repeated task executions (ms)
+ * @param fixedRate whether to schedule as fixed-rate execution
+ */
+ public ScheduledTimerListener(TimerListener timerListener, long delay, long period, boolean fixedRate) {
+ this.timerListener = timerListener;
+ this.delay = delay;
+ this.period = period;
+ this.fixedRate = fixedRate;
+ }
+
+ /**
+ * Create a new ScheduledTimerListener, with default
+ * one-time execution without delay.
+ * @param timerTask the Runnable to schedule as TimerListener
+ */
+ public ScheduledTimerListener(Runnable timerTask) {
+ setRunnable(timerTask);
+ }
+
+ /**
+ * Create a new ScheduledTimerListener, with default
+ * one-time execution with the given delay.
+ * @param timerTask the Runnable to schedule as TimerListener
+ * @param delay the delay before starting the task for the first time (ms)
+ */
+ public ScheduledTimerListener(Runnable timerTask, long delay) {
+ setRunnable(timerTask);
+ this.delay = delay;
+ }
+
+ /**
+ * Create a new ScheduledTimerListener.
+ * @param timerTask the Runnable to schedule as TimerListener
+ * @param delay the delay before starting the task for the first time (ms)
+ * @param period the period between repeated task executions (ms)
+ * @param fixedRate whether to schedule as fixed-rate execution
+ */
+ public ScheduledTimerListener(Runnable timerTask, long delay, long period, boolean fixedRate) {
+ setRunnable(timerTask);
+ this.delay = delay;
+ this.period = period;
+ this.fixedRate = fixedRate;
+ }
+
+
+ /**
+ * Set the Runnable to schedule as TimerListener.
+ * @see DelegatingTimerListener
+ */
+ public void setRunnable(Runnable timerTask) {
+ this.timerListener = new DelegatingTimerListener(timerTask);
+ }
+
+ /**
+ * Set the TimerListener to schedule.
+ */
+ public void setTimerListener(TimerListener timerListener) {
+ this.timerListener = timerListener;
+ }
+
+ /**
+ * Return the TimerListener to schedule.
+ */
+ public TimerListener getTimerListener() {
+ return this.timerListener;
+ }
+
+ /**
+ * Set the delay before starting the task for the first time,
+ * in milliseconds. Default is 0, immediately starting the
+ * task after successful scheduling.
+ * If the "firstTime" property is specified, this property will be ignored.
+ * Specify one or the other, not both.
+ */
+ public void setDelay(long delay) {
+ this.delay = delay;
+ }
+
+ /**
+ * Return the delay before starting the job for the first time.
+ */
+ public long getDelay() {
+ return this.delay;
+ }
+
+ /**
+ * Set the period between repeated task executions, in milliseconds.
+ * Default is -1, leading to one-time execution. In case of zero or a
+ * positive value, the task will be executed repeatedly, with the given
+ * interval inbetween executions.
+ * Note that the semantics of the period value vary between fixed-rate
+ * and fixed-delay execution.
+ * Note: A period of 0 (for example as fixed delay) is
+ * supported, because the CommonJ specification defines this as a legal value.
+ * Hence a value of 0 will result in immediate re-execution after a job has
+ * finished (not in one-time execution like with See TimerManager javadoc for details on those execution modes.
+ * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long)
+ * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long)
+ */
+ public void setFixedRate(boolean fixedRate) {
+ this.fixedRate = fixedRate;
+ }
+
+ /**
+ * Return whether to schedule as fixed-rate execution.
+ */
+ public boolean isFixedRate() {
+ return this.fixedRate;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java
new file mode 100644
index 00000000000..572b2d96773
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.commonj;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.naming.NamingException;
+
+import commonj.timers.Timer;
+import commonj.timers.TimerManager;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.Lifecycle;
+import org.springframework.jndi.JndiLocatorSupport;
+
+/**
+ * {@link org.springframework.beans.factory.FactoryBean} that retrieves a
+ * CommonJ {@link commonj.timers.TimerManager} and exposes it for bean references.
+ *
+ * This is the central convenience class for setting up a
+ * CommonJ TimerManager in a Spring context.
+ *
+ * Allows for registration of ScheduledTimerListeners. This is the main
+ * purpose of this class; the TimerManager itself could also be fetched
+ * from JNDI via {@link org.springframework.jndi.JndiObjectFactoryBean}.
+ * In scenarios that just require static registration of tasks at startup,
+ * there is no need to access the TimerManager itself in application code.
+ *
+ * Note that the TimerManager uses a TimerListener instance that is
+ * shared between repeated executions, in contrast to Quartz which
+ * instantiates a new Job for each execution.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see ScheduledTimerListener
+ * @see commonj.timers.TimerManager
+ * @see commonj.timers.TimerListener
+ */
+public class TimerManagerFactoryBean extends JndiLocatorSupport
+ implements FactoryBean, InitializingBean, DisposableBean, Lifecycle {
+
+ private TimerManager timerManager;
+
+ private String timerManagerName;
+
+ private boolean shared = false;
+
+ private ScheduledTimerListener[] scheduledTimerListeners;
+
+ private final List timers = new LinkedList();
+
+
+ /**
+ * Specify the CommonJ TimerManager to delegate to.
+ * Note that the given TimerManager's lifecycle will be managed
+ * by this FactoryBean.
+ * Alternatively (and typically), you can specify the JNDI name
+ * of the target TimerManager.
+ * @see #setTimerManagerName
+ */
+ public void setTimerManager(TimerManager timerManager) {
+ this.timerManager = timerManager;
+ }
+
+ /**
+ * Set the JNDI name of the CommonJ TimerManager.
+ * This can either be a fully qualified JNDI name, or the JNDI name relative
+ * to the current environment naming context if "resourceRef" is set to "true".
+ * @see #setTimerManager
+ * @see #setResourceRef
+ */
+ public void setTimerManagerName(String timerManagerName) {
+ this.timerManagerName = timerManagerName;
+ }
+
+ /**
+ * Specify whether the TimerManager obtained by this FactoryBean
+ * is a shared instance ("true") or an independent instance ("false").
+ * The lifecycle of the former is supposed to be managed by the application
+ * server, while the lifecycle of the latter is up to the application.
+ * Default is "false", i.e. managing an independent TimerManager instance.
+ * This is what the CommonJ specification suggests that application servers
+ * are supposed to offer via JNDI lookups, typically declared as a
+ * Switch this flag to "true" if you are obtaining a shared TimerManager,
+ * typically through specifying the JNDI location of a TimerManager that
+ * has been explicitly declared as 'Shareable'. Note that WebLogic's
+ * cluster-aware Job Scheduler is a shared TimerManager too.
+ * The sole difference between this FactoryBean being in shared or
+ * non-shared mode is that it will only attempt to suspend / resume / stop
+ * the underlying TimerManager in case of an independent (non-shared) instance.
+ * This only affects the {@link org.springframework.context.Lifecycle} support
+ * as well as application context shutdown.
+ * @see #stop()
+ * @see #start()
+ * @see #destroy()
+ * @see commonj.timers.TimerManager
+ */
+ public void setShared(boolean shared) {
+ this.shared = shared;
+ }
+
+ /**
+ * Register a list of ScheduledTimerListener objects with the TimerManager
+ * that this FactoryBean creates. Depending on each ScheduledTimerListener's settings,
+ * it will be registered via one of TimerManager's schedule methods.
+ * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long)
+ * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long)
+ * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long)
+ */
+ public void setScheduledTimerListeners(ScheduledTimerListener[] scheduledTimerListeners) {
+ this.scheduledTimerListeners = scheduledTimerListeners;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of InitializingBean interface
+ //---------------------------------------------------------------------
+
+ public void afterPropertiesSet() throws NamingException {
+ if (this.timerManager == null) {
+ if (this.timerManagerName == null) {
+ throw new IllegalArgumentException("Either 'timerManager' or 'timerManagerName' must be specified");
+ }
+ this.timerManager = (TimerManager) lookup(this.timerManagerName, TimerManager.class);
+ }
+
+ if (this.scheduledTimerListeners != null) {
+ for (int i = 0; i < this.scheduledTimerListeners.length; i++) {
+ ScheduledTimerListener scheduledTask = this.scheduledTimerListeners[i];
+ Timer timer = null;
+ if (scheduledTask.isOneTimeTask()) {
+ timer = this.timerManager.schedule(scheduledTask.getTimerListener(), scheduledTask.getDelay());
+ }
+ else {
+ if (scheduledTask.isFixedRate()) {
+ timer = this.timerManager.scheduleAtFixedRate(
+ scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod());
+ }
+ else {
+ timer = this.timerManager.schedule(
+ scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod());
+ }
+ }
+ this.timers.add(timer);
+ }
+ }
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of FactoryBean interface
+ //---------------------------------------------------------------------
+
+ public Object getObject() {
+ return this.timerManager;
+ }
+
+ public Class getObjectType() {
+ return (this.timerManager != null ? this.timerManager.getClass() : TimerManager.class);
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of Lifecycle interface
+ //---------------------------------------------------------------------
+
+ /**
+ * Resumes the underlying TimerManager (if not shared).
+ * @see commonj.timers.TimerManager#resume()
+ */
+ public void start() {
+ if (!this.shared) {
+ this.timerManager.resume();
+ }
+ }
+
+ /**
+ * Suspends the underlying TimerManager (if not shared).
+ * @see commonj.timers.TimerManager#suspend()
+ */
+ public void stop() {
+ if (!this.shared) {
+ this.timerManager.suspend();
+ }
+ }
+
+ /**
+ * Considers the underlying TimerManager as running if it is
+ * neither suspending nor stopping.
+ * @see commonj.timers.TimerManager#isSuspending()
+ * @see commonj.timers.TimerManager#isStopping()
+ */
+ public boolean isRunning() {
+ return (!this.timerManager.isSuspending() && !this.timerManager.isStopping());
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of DisposableBean interface
+ //---------------------------------------------------------------------
+
+ /**
+ * Cancels all statically registered Timers on shutdown,
+ * and stops the underlying TimerManager (if not shared).
+ * @see commonj.timers.Timer#cancel()
+ * @see commonj.timers.TimerManager#stop()
+ */
+ public void destroy() {
+ // Cancel all registered timers.
+ for (Iterator it = this.timers.iterator(); it.hasNext();) {
+ Timer timer = (Timer) it.next();
+ try {
+ timer.cancel();
+ }
+ catch (Throwable ex) {
+ logger.warn("Could not cancel CommonJ Timer", ex);
+ }
+ }
+ this.timers.clear();
+
+ // Stop the entire TimerManager, if necessary.
+ if (!this.shared) {
+ // May return early, but at least we already cancelled all known Timers.
+ this.timerManager.stop();
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java
new file mode 100644
index 00000000000..9030dc28ab6
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.commonj;
+
+import java.util.Collection;
+
+import javax.naming.NamingException;
+
+import commonj.work.Work;
+import commonj.work.WorkException;
+import commonj.work.WorkItem;
+import commonj.work.WorkListener;
+import commonj.work.WorkManager;
+import commonj.work.WorkRejectedException;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.task.TaskRejectedException;
+import org.springframework.jndi.JndiLocatorSupport;
+import org.springframework.scheduling.SchedulingException;
+import org.springframework.scheduling.SchedulingTaskExecutor;
+import org.springframework.util.Assert;
+
+/**
+ * TaskExecutor implementation that delegates to a CommonJ WorkManager,
+ * implementing the {@link commonj.work.WorkManager} interface,
+ * which either needs to be specified as reference or through the JNDI name.
+ *
+ * This is the central convenience class for setting up a
+ * CommonJ WorkManager in a Spring context.
+ *
+ * Also implements the CommonJ WorkManager interface itself, delegating all
+ * calls to the target WorkManager. Hence, a caller can choose whether it wants
+ * to talk to this executor through the Spring TaskExecutor interface or the
+ * CommonJ WorkManager interface.
+ *
+ * The CommonJ WorkManager will usually be retrieved from the application
+ * server's JNDI environment, as defined in the server's management console.
+ *
+ * Note: At the time of this writing, the CommonJ WorkManager facility
+ * is only supported on IBM WebSphere 6.0+ and BEA WebLogic 9.0+,
+ * despite being such a crucial API for an application server.
+ * (There is a similar facility available on WebSphere 5.1 Enterprise,
+ * though, which we will discuss below.)
+ *
+ * On JBoss and GlassFish, a similar facility is available through
+ * the JCA WorkManager. See the
+ * {@link org.springframework.jca.work.jboss.JBossWorkManagerTaskExecutor}
+ * {@link org.springframework.jca.work.glassfish.GlassFishWorkManagerTaskExecutor}
+ * classes which are the direct equivalent of this CommonJ adapter class.
+ *
+ * A similar facility is available on WebSphere 5.1, under the name
+ * "Asynch Beans". Its central interface is called WorkManager too and is
+ * also obtained from JNDI, just like a standard CommonJ WorkManager.
+ * However, this WorkManager variant is notably different: The central
+ * execution method is called "startWork" instead of "schedule",
+ * and takes a slightly different Work interface as parameter.
+ *
+ * Support for this WebSphere 5.1 variant can be built with this class
+ * and its helper DelegatingWork as template: Call the WorkManager's
+ * Alternatively, you can also specify the JNDI name
+ * of the target WorkManager.
+ * @see #setWorkManagerName
+ */
+ public void setWorkManager(WorkManager workManager) {
+ this.workManager = workManager;
+ }
+
+ /**
+ * Set the JNDI name of the CommonJ WorkManager.
+ * This can either be a fully qualified JNDI name,
+ * or the JNDI name relative to the current environment
+ * naming context if "resourceRef" is set to "true".
+ * @see #setWorkManager
+ * @see #setResourceRef
+ */
+ public void setWorkManagerName(String workManagerName) {
+ this.workManagerName = workManagerName;
+ }
+
+ /**
+ * Specify a CommonJ WorkListener to apply, if any.
+ * This shared WorkListener instance will be passed on to the
+ * WorkManager by all {@link #execute} calls on this TaskExecutor.
+ */
+ public void setWorkListener(WorkListener workListener) {
+ this.workListener = workListener;
+ }
+
+ public void afterPropertiesSet() throws NamingException {
+ if (this.workManager == null) {
+ if (this.workManagerName == null) {
+ throw new IllegalArgumentException("Either 'workManager' or 'workManagerName' must be specified");
+ }
+ this.workManager = (WorkManager) lookup(this.workManagerName, WorkManager.class);
+ }
+ }
+
+
+ //-------------------------------------------------------------------------
+ // Implementation of the Spring SchedulingTaskExecutor interface
+ //-------------------------------------------------------------------------
+
+ public void execute(Runnable task) {
+ Assert.state(this.workManager != null, "No WorkManager specified");
+ Work work = new DelegatingWork(task);
+ try {
+ if (this.workListener != null) {
+ this.workManager.schedule(work, this.workListener);
+ }
+ else {
+ this.workManager.schedule(work);
+ }
+ }
+ catch (WorkRejectedException ex) {
+ throw new TaskRejectedException("CommonJ WorkManager did not accept task: " + task, ex);
+ }
+ catch (WorkException ex) {
+ throw new SchedulingException("Could not schedule task on CommonJ WorkManager", ex);
+ }
+ }
+
+ /**
+ * This task executor prefers short-lived work units.
+ */
+ public boolean prefersShortLivedTasks() {
+ return true;
+ }
+
+
+ //-------------------------------------------------------------------------
+ // Implementation of the CommonJ WorkManager interface
+ //-------------------------------------------------------------------------
+
+ public WorkItem schedule(Work work)
+ throws WorkException, IllegalArgumentException {
+
+ return this.workManager.schedule(work);
+ }
+
+ public WorkItem schedule(Work work, WorkListener workListener)
+ throws WorkException, IllegalArgumentException {
+
+ return this.workManager.schedule(work, workListener);
+ }
+
+ public boolean waitForAll(Collection workItems, long timeout)
+ throws InterruptedException, IllegalArgumentException {
+
+ return this.workManager.waitForAll(workItems, timeout);
+ }
+
+ public Collection waitForAny(Collection workItems, long timeout)
+ throws InterruptedException, IllegalArgumentException {
+
+ return this.workManager.waitForAny(workItems, timeout);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/package.html b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/package.html
new file mode 100644
index 00000000000..70e1354dd5f
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/package.html
@@ -0,0 +1,8 @@
+
+ Can be overridden to post-process the job instance.
+ * @param bundle the TriggerFiredBundle from which the JobDetail
+ * and other info relating to the trigger firing can be obtained
+ * @return the job instance
+ * @throws Exception if job instantiation failed
+ */
+ protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
+ return bundle.getJobDetail().getJobClass().newInstance();
+ }
+
+ /**
+ * Adapt the given job object to the Quartz Job interface.
+ * The default implementation supports straight Quartz Jobs
+ * as well as Runnables, which get wrapped in a DelegatingJob.
+ * @param jobObject the original instance of the specified job class
+ * @return the adapted Quartz Job instance
+ * @throws Exception if the given job could not be adapted
+ * @see DelegatingJob
+ */
+ protected Job adaptJob(Object jobObject) throws Exception {
+ if (jobObject instanceof Job) {
+ return (Job) jobObject;
+ }
+ else if (jobObject instanceof Runnable) {
+ return new DelegatingJob((Runnable) jobObject);
+ }
+ else {
+ throw new IllegalArgumentException("Unable to execute job class [" + jobObject.getClass().getName() +
+ "]: only [org.quartz.Job] and [java.lang.Runnable] supported.");
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/CronTriggerBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/CronTriggerBean.java
new file mode 100644
index 00000000000..817c697a11e
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/CronTriggerBean.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.text.ParseException;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.quartz.CronTrigger;
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.Constants;
+
+/**
+ * Convenience subclass of Quartz's {@link org.quartz.CronTrigger}
+ * class, making bean-style usage easier.
+ *
+ * CronTrigger itself is already a JavaBean but lacks sensible defaults.
+ * This class uses the Spring bean name as job name, the Quartz default group
+ * ("DEFAULT") as job group, the current time as start time, and indefinite
+ * repetition, if not specified.
+ *
+ * This class will also register the trigger with the job name and group of
+ * a given {@link org.quartz.JobDetail}. This allows {@link SchedulerFactoryBean}
+ * to automatically register a trigger for the corresponding JobDetail,
+ * instead of registering the JobDetail separately.
+ *
+ * NOTE: This convenience subclass does not work with trigger
+ * persistence in Quartz 1.6, due to a change in Quartz's trigger handling.
+ * Use Quartz 1.5 if you rely on trigger persistence based on this class,
+ * or the standard Quartz {@link org.quartz.CronTrigger} class instead.
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see #setName
+ * @see #setGroup
+ * @see #setStartTime
+ * @see #setJobName
+ * @see #setJobGroup
+ * @see #setJobDetail
+ * @see SchedulerFactoryBean#setTriggers
+ * @see SchedulerFactoryBean#setJobDetails
+ * @see SimpleTriggerBean
+ */
+public class CronTriggerBean extends CronTrigger
+ implements JobDetailAwareTrigger, BeanNameAware, InitializingBean {
+
+ /** Constants for the CronTrigger class */
+ private static final Constants constants = new Constants(CronTrigger.class);
+
+
+ private JobDetail jobDetail;
+
+ private String beanName;
+
+
+ /**
+ * Register objects in the JobDataMap via a given Map.
+ * These objects will be available to this Trigger only,
+ * in contrast to objects in the JobDetail's data map.
+ * @param jobDataAsMap Map with String keys and any objects as values
+ * (for example Spring-managed beans)
+ * @see JobDetailBean#setJobDataAsMap
+ */
+ public void setJobDataAsMap(Map jobDataAsMap) {
+ getJobDataMap().putAll(jobDataAsMap);
+ }
+
+ /**
+ * Set the misfire instruction via the name of the corresponding
+ * constant in the {@link org.quartz.CronTrigger} class.
+ * Default is A TriggerListener name always refers to the name returned
+ * by the TriggerListener implementation.
+ * @see SchedulerFactoryBean#setTriggerListeners
+ * @see org.quartz.TriggerListener#getName
+ */
+ public void setTriggerListenerNames(String[] names) {
+ for (int i = 0; i < names.length; i++) {
+ addTriggerListener(names[i]);
+ }
+ }
+
+ /**
+ * Set the JobDetail that this trigger should be associated with.
+ * This is typically used with a bean reference if the JobDetail
+ * is a Spring-managed bean. Alternatively, the trigger can also
+ * be associated with a job by name and group.
+ * @see #setJobName
+ * @see #setJobGroup
+ */
+ public void setJobDetail(JobDetail jobDetail) {
+ this.jobDetail = jobDetail;
+ }
+
+ public JobDetail getJobDetail() {
+ return this.jobDetail;
+ }
+
+ public void setBeanName(String beanName) {
+ this.beanName = beanName;
+ }
+
+
+ public void afterPropertiesSet() throws ParseException {
+ if (getName() == null) {
+ setName(this.beanName);
+ }
+ if (getGroup() == null) {
+ setGroup(Scheduler.DEFAULT_GROUP);
+ }
+ if (getStartTime() == null) {
+ setStartTime(new Date());
+ }
+ if (getTimeZone() == null) {
+ setTimeZone(TimeZone.getDefault());
+ }
+ if (this.jobDetail != null) {
+ setJobName(this.jobDetail.getName());
+ setJobGroup(this.jobDetail.getGroup());
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java
new file mode 100644
index 00000000000..bcec0e566a6
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+
+import org.springframework.util.Assert;
+
+/**
+ * Simple Quartz {@link org.quartz.Job} adapter that delegates to a
+ * given {@link java.lang.Runnable} instance.
+ *
+ * Typically used in combination with property injection on the
+ * Runnable instance, receiving parameters from the Quartz JobDataMap
+ * that way instead of via the JobExecutionContext.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see SpringBeanJobFactory
+ * @see org.quartz.Job#execute(org.quartz.JobExecutionContext)
+ */
+public class DelegatingJob implements Job {
+
+ private final Runnable delegate;
+
+
+ /**
+ * Create a new DelegatingJob.
+ * @param delegate the Runnable implementation to delegate to
+ */
+ public DelegatingJob(Runnable delegate) {
+ Assert.notNull(delegate, "Delegate must not be null");
+ this.delegate = delegate;
+ }
+
+ /**
+ * Return the wrapped Runnable implementation.
+ */
+ public final Runnable getDelegate() {
+ return this.delegate;
+ }
+
+
+ /**
+ * Delegates execution to the underlying Runnable.
+ */
+ public void execute(JobExecutionContext context) throws JobExecutionException {
+ this.delegate.run();
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailAwareTrigger.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailAwareTrigger.java
new file mode 100644
index 00000000000..3da4c9c271e
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailAwareTrigger.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.JobDetail;
+
+/**
+ * Interface to be implemented by Quartz Triggers that are aware
+ * of the JobDetail object that they are associated with.
+ *
+ * SchedulerFactoryBean will auto-detect Triggers that implement this
+ * interface and register them for the respective JobDetail accordingly.
+ *
+ * The alternative is to configure a Trigger for a Job name and group:
+ * This involves the need to register the JobDetail object separately
+ * with SchedulerFactoryBean.
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see SchedulerFactoryBean#setTriggers
+ * @see SchedulerFactoryBean#setJobDetails
+ * @see org.quartz.Trigger#setJobName
+ * @see org.quartz.Trigger#setJobGroup
+ */
+public interface JobDetailAwareTrigger {
+
+ /**
+ * Return the JobDetail that this Trigger is associated with.
+ * @return the associated JobDetail, or These objects will be available to this Job only,
+ * in contrast to objects in the SchedulerContext.
+ * Note: When using persistent Jobs whose JobDetail will be kept in the
+ * database, do not put Spring-managed beans or an ApplicationContext
+ * reference into the JobDataMap but rather into the SchedulerContext.
+ * @param jobDataAsMap Map with String keys and any objects as values
+ * (for example Spring-managed beans)
+ * @see SchedulerFactoryBean#setSchedulerContextAsMap
+ */
+ public void setJobDataAsMap(Map jobDataAsMap) {
+ getJobDataMap().putAll(jobDataAsMap);
+ }
+
+ /**
+ * Set a list of JobListener names for this job, referring to
+ * non-global JobListeners registered with the Scheduler.
+ * A JobListener name always refers to the name returned
+ * by the JobListener implementation.
+ * @see SchedulerFactoryBean#setJobListeners
+ * @see org.quartz.JobListener#getName
+ */
+ public void setJobListenerNames(String[] names) {
+ for (int i = 0; i < names.length; i++) {
+ addJobListener(names[i]);
+ }
+ }
+
+ public void setBeanName(String beanName) {
+ this.beanName = beanName;
+ }
+
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * Set the key of an ApplicationContext reference to expose in the JobDataMap,
+ * for example "applicationContext". Default is none.
+ * Only applicable when running in a Spring ApplicationContext.
+ * In case of a QuartzJobBean, the reference will be applied to the Job
+ * instance as bean property. An "applicationContext" attribute will correspond
+ * to a "setApplicationContext" method in that scenario.
+ * Note that BeanFactory callback interfaces like ApplicationContextAware
+ * are not automatically applied to Quartz Job instances, because Quartz
+ * itself is responsible for the lifecycle of its Jobs.
+ * Note: When using persistent job stores where JobDetail contents will
+ * be kept in the database, do not put an ApplicationContext reference into
+ * the JobDataMap but rather into the SchedulerContext.
+ * @see SchedulerFactoryBean#setApplicationContextSchedulerContextKey
+ * @see org.springframework.context.ApplicationContext
+ */
+ public void setApplicationContextJobDataKey(String applicationContextJobDataKey) {
+ this.applicationContextJobDataKey = applicationContextJobDataKey;
+ }
+
+
+ public void afterPropertiesSet() {
+ if (getName() == null) {
+ setName(this.beanName);
+ }
+ if (getGroup() == null) {
+ setGroup(Scheduler.DEFAULT_GROUP);
+ }
+ if (this.applicationContextJobDataKey != null) {
+ if (this.applicationContext == null) {
+ throw new IllegalStateException(
+ "JobDetailBean needs to be set up in an ApplicationContext " +
+ "to be able to handle an 'applicationContextJobDataKey'");
+ }
+ getJobDataMap().put(this.applicationContextJobDataKey, this.applicationContext);
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java
new file mode 100644
index 00000000000..2b04f76d8d0
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.springframework.core.NestedRuntimeException;
+import org.springframework.util.MethodInvoker;
+
+/**
+ * Unchecked exception that wraps an exception thrown from a target method.
+ * Propagated to the Quartz scheduler from a Job that reflectively invokes
+ * an arbitrary target method.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5.3
+ * @see MethodInvokingJobDetailFactoryBean
+ */
+public class JobMethodInvocationFailedException extends NestedRuntimeException {
+
+ /**
+ * Constructor for JobMethodInvocationFailedException.
+ * @param methodInvoker the MethodInvoker used for reflective invocation
+ * @param cause the root cause (as thrown from the target method)
+ */
+ public JobMethodInvocationFailedException(MethodInvoker methodInvoker, Throwable cause) {
+ super("Invocation of method '" + methodInvoker.getTargetMethod() +
+ "' on target class [" + methodInvoker.getTargetClass() + "] failed", cause);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
new file mode 100644
index 00000000000..f4cc7293187
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.quartz.SchedulerConfigException;
+import org.quartz.impl.jdbcjobstore.JobStoreCMT;
+import org.quartz.spi.ClassLoadHelper;
+import org.quartz.spi.SchedulerSignaler;
+import org.quartz.utils.ConnectionProvider;
+import org.quartz.utils.DBConnectionManager;
+
+import org.springframework.jdbc.datasource.DataSourceUtils;
+
+/**
+ * Subclass of Quartz's JobStoreCMT class that delegates to a Spring-managed
+ * DataSource instead of using a Quartz-managed connection pool. This JobStore
+ * will be used if SchedulerFactoryBean's "dataSource" property is set.
+ *
+ * Supports both transactional and non-transactional DataSource access.
+ * With a non-XA DataSource and local Spring transactions, a single DataSource
+ * argument is sufficient. In case of an XA DataSource and global JTA transactions,
+ * SchedulerFactoryBean's "nonTransactionalDataSource" property should be set,
+ * passing in a non-XA DataSource that will not participate in global transactions.
+ *
+ * Operations performed by this JobStore will properly participate in any
+ * kind of Spring-managed transaction, as it uses Spring's DataSourceUtils
+ * connection handling methods that are aware of a current transaction.
+ *
+ * Note that all Quartz Scheduler operations that affect the persistent
+ * job store should usually be performed within active transactions,
+ * as they assume to get proper locks etc.
+ *
+ * @author Juergen Hoeller
+ * @since 1.1
+ * @see SchedulerFactoryBean#setDataSource
+ * @see SchedulerFactoryBean#setNonTransactionalDataSource
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
+ */
+public class LocalDataSourceJobStore extends JobStoreCMT {
+
+ /**
+ * Name used for the transactional ConnectionProvider for Quartz.
+ * This provider will delegate to the local Spring-managed DataSource.
+ * @see org.quartz.utils.DBConnectionManager#addConnectionProvider
+ * @see SchedulerFactoryBean#setDataSource
+ */
+ public static final String TX_DATA_SOURCE_PREFIX = "springTxDataSource.";
+
+ /**
+ * Name used for the non-transactional ConnectionProvider for Quartz.
+ * This provider will delegate to the local Spring-managed DataSource.
+ * @see org.quartz.utils.DBConnectionManager#addConnectionProvider
+ * @see SchedulerFactoryBean#setDataSource
+ */
+ public static final String NON_TX_DATA_SOURCE_PREFIX = "springNonTxDataSource.";
+
+
+ private DataSource dataSource;
+
+
+ public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler)
+ throws SchedulerConfigException {
+
+ // Absolutely needs thread-bound DataSource to initialize.
+ this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource();
+ if (this.dataSource == null) {
+ throw new SchedulerConfigException(
+ "No local DataSource found for configuration - " +
+ "'dataSource' property must be set on SchedulerFactoryBean");
+ }
+
+ // Configure transactional connection settings for Quartz.
+ setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
+ setDontSetAutoCommitFalse(true);
+
+ // Register transactional ConnectionProvider for Quartz.
+ DBConnectionManager.getInstance().addConnectionProvider(
+ TX_DATA_SOURCE_PREFIX + getInstanceName(),
+ new ConnectionProvider() {
+ public Connection getConnection() throws SQLException {
+ // Return a transactional Connection, if any.
+ return DataSourceUtils.doGetConnection(dataSource);
+ }
+ public void shutdown() {
+ // Do nothing - a Spring-managed DataSource has its own lifecycle.
+ }
+ }
+ );
+
+ // Non-transactional DataSource is optional: fall back to default
+ // DataSource if not explicitly specified.
+ DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
+ final DataSource nonTxDataSourceToUse =
+ (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
+
+ // Configure non-transactional connection settings for Quartz.
+ setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
+
+ // Register non-transactional ConnectionProvider for Quartz.
+ DBConnectionManager.getInstance().addConnectionProvider(
+ NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
+ new ConnectionProvider() {
+ public Connection getConnection() throws SQLException {
+ // Always return a non-transactional Connection.
+ return nonTxDataSourceToUse.getConnection();
+ }
+ public void shutdown() {
+ // Do nothing - a Spring-managed DataSource has its own lifecycle.
+ }
+ }
+ );
+
+ super.initialize(loadHelper, signaler);
+ }
+
+ protected void closeConnection(Connection con) {
+ // Will work for transactional and non-transactional connections.
+ DataSourceUtils.releaseConnection(con, this.dataSource);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java
new file mode 100644
index 00000000000..bc41625baaf
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.SchedulerConfigException;
+import org.quartz.spi.ThreadPool;
+
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.core.task.TaskRejectedException;
+
+/**
+ * Quartz ThreadPool adapter that delegates to a Spring-managed
+ * TaskExecutor instance, specified on SchedulerFactoryBean.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see SchedulerFactoryBean#setTaskExecutor
+ */
+public class LocalTaskExecutorThreadPool implements ThreadPool {
+
+ /** Logger available to subclasses */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private TaskExecutor taskExecutor;
+
+
+ public void initialize() throws SchedulerConfigException {
+ // Absolutely needs thread-bound TaskExecutor to initialize.
+ this.taskExecutor = SchedulerFactoryBean.getConfigTimeTaskExecutor();
+ if (this.taskExecutor == null) {
+ throw new SchedulerConfigException(
+ "No local TaskExecutor found for configuration - " +
+ "'taskExecutor' property must be set on SchedulerFactoryBean");
+ }
+ }
+
+ public void shutdown(boolean waitForJobsToComplete) {
+ }
+
+ public int getPoolSize() {
+ return -1;
+ }
+
+
+ public boolean runInThread(Runnable runnable) {
+ if (runnable == null) {
+ return false;
+ }
+ try {
+ this.taskExecutor.execute(runnable);
+ return true;
+ }
+ catch (TaskRejectedException ex) {
+ logger.error("Task has been rejected by TaskExecutor", ex);
+ return false;
+ }
+ }
+
+ public int blockForAvailableThreads() {
+ // The present implementation always returns 1, making Quartz (1.6)
+ // always schedule any tasks that it feels like scheduling.
+ // This could be made smarter for specific TaskExecutors,
+ // for example calling Inherits common configuration properties from the {@link MethodInvoker}
+ * base class, such as {@link #setTargetObject "targetObject"} and
+ * {@link #setTargetMethod "targetMethod"}, adding support for lookup of the target
+ * bean by name through the {@link #setTargetBeanName "targetBeanName"} property
+ * (as alternative to specifying a "targetObject" directly, allowing for
+ * non-singleton target objects).
+ *
+ * Supports both concurrently running jobs and non-currently running
+ * jobs through the "concurrent" property. Jobs created by this
+ * MethodInvokingJobDetailFactoryBean are by default volatile and durable
+ * (according to Quartz terminology).
+ *
+ * NOTE: JobDetails created via this FactoryBean are not
+ * serializable and thus not suitable for persistent job stores.
+ * You need to implement your own Quartz Job as a thin wrapper for each case
+ * where you want a persistent job to delegate to a specific service method.
+ *
+ * @author Juergen Hoeller
+ * @author Alef Arendsen
+ * @since 18.02.2004
+ * @see #setTargetBeanName
+ * @see #setTargetObject
+ * @see #setTargetMethod
+ * @see #setConcurrent
+ */
+public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker
+ implements FactoryBean, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean {
+
+ private String name;
+
+ private String group = Scheduler.DEFAULT_GROUP;
+
+ private boolean concurrent = true;
+
+ private String targetBeanName;
+
+ private String[] jobListenerNames;
+
+ private String beanName;
+
+ private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
+
+ private BeanFactory beanFactory;
+
+ private JobDetail jobDetail;
+
+
+ /**
+ * Set the name of the job.
+ * Default is the bean name of this FactoryBean.
+ * @see org.quartz.JobDetail#setName
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Set the group of the job.
+ * Default is the default group of the Scheduler.
+ * @see org.quartz.JobDetail#setGroup
+ * @see org.quartz.Scheduler#DEFAULT_GROUP
+ */
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ /**
+ * Specify whether or not multiple jobs should be run in a concurrent
+ * fashion. The behavior when one does not want concurrent jobs to be
+ * executed is realized through adding the {@link StatefulJob} interface.
+ * More information on stateful versus stateless jobs can be found
+ * here.
+ * The default setting is to run jobs concurrently.
+ */
+ public void setConcurrent(boolean concurrent) {
+ this.concurrent = concurrent;
+ }
+
+ /**
+ * Set the name of the target bean in the Spring BeanFactory.
+ * This is an alternative to specifying {@link #setTargetObject "targetObject"},
+ * allowing for non-singleton beans to be invoked. Note that specified
+ * "targetObject" and {@link #setTargetClass "targetClass"} values will
+ * override the corresponding effect of this "targetBeanName" setting
+ * (i.e. statically pre-define the bean type or even the bean object).
+ */
+ public void setTargetBeanName(String targetBeanName) {
+ this.targetBeanName = targetBeanName;
+ }
+
+ /**
+ * Set a list of JobListener names for this job, referring to
+ * non-global JobListeners registered with the Scheduler.
+ * A JobListener name always refers to the name returned
+ * by the JobListener implementation.
+ * @see SchedulerFactoryBean#setJobListeners
+ * @see org.quartz.JobListener#getName
+ */
+ public void setJobListenerNames(String[] names) {
+ this.jobListenerNames = names;
+ }
+
+ public void setBeanName(String beanName) {
+ this.beanName = beanName;
+ }
+
+ public void setBeanClassLoader(ClassLoader classLoader) {
+ this.beanClassLoader = classLoader;
+ }
+
+ public void setBeanFactory(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ protected Class resolveClassName(String className) throws ClassNotFoundException {
+ return ClassUtils.forName(className, this.beanClassLoader);
+ }
+
+
+ public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
+ prepare();
+
+ // Use specific name if given, else fall back to bean name.
+ String name = (this.name != null ? this.name : this.beanName);
+
+ // Consider the concurrent flag to choose between stateful and stateless job.
+ Class jobClass = (this.concurrent ? (Class) MethodInvokingJob.class : StatefulMethodInvokingJob.class);
+
+ // Build JobDetail instance.
+ this.jobDetail = new JobDetail(name, this.group, jobClass);
+ this.jobDetail.getJobDataMap().put("methodInvoker", this);
+ this.jobDetail.setVolatility(true);
+ this.jobDetail.setDurability(true);
+
+ // Register job listener names.
+ if (this.jobListenerNames != null) {
+ for (int i = 0; i < this.jobListenerNames.length; i++) {
+ this.jobDetail.addJobListener(this.jobListenerNames[i]);
+ }
+ }
+
+ postProcessJobDetail(this.jobDetail);
+ }
+
+ /**
+ * Callback for post-processing the JobDetail to be exposed by this FactoryBean.
+ * The default implementation is empty. Can be overridden in subclasses.
+ * @param jobDetail the JobDetail prepared by this FactoryBean
+ */
+ protected void postProcessJobDetail(JobDetail jobDetail) {
+ }
+
+
+ /**
+ * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature.
+ */
+ public Class getTargetClass() {
+ Class targetClass = super.getTargetClass();
+ if (targetClass == null && this.targetBeanName != null) {
+ Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'");
+ targetClass = this.beanFactory.getType(this.targetBeanName);
+ }
+ return targetClass;
+ }
+
+ /**
+ * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature.
+ */
+ public Object getTargetObject() {
+ Object targetObject = super.getTargetObject();
+ if (targetObject == null && this.targetBeanName != null) {
+ Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'");
+ targetObject = this.beanFactory.getBean(this.targetBeanName);
+ }
+ return targetObject;
+ }
+
+
+ public Object getObject() {
+ return this.jobDetail;
+ }
+
+ public Class getObjectType() {
+ return JobDetail.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+
+ /**
+ * Quartz Job implementation that invokes a specified method.
+ * Automatically applied by MethodInvokingJobDetailFactoryBean.
+ */
+ public static class MethodInvokingJob extends QuartzJobBean {
+
+ protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class);
+
+ private MethodInvoker methodInvoker;
+
+ /**
+ * Set the MethodInvoker to use.
+ */
+ public void setMethodInvoker(MethodInvoker methodInvoker) {
+ this.methodInvoker = methodInvoker;
+ }
+
+ /**
+ * Invoke the method via the MethodInvoker.
+ */
+ protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
+ try {
+ this.methodInvoker.invoke();
+ }
+ catch (InvocationTargetException ex) {
+ if (ex.getTargetException() instanceof JobExecutionException) {
+ // -> JobExecutionException, to be logged at info level by Quartz
+ throw (JobExecutionException) ex.getTargetException();
+ }
+ else {
+ // -> "unhandled exception", to be logged at error level by Quartz
+ throw new JobMethodInvocationFailedException(this.methodInvoker, ex.getTargetException());
+ }
+ }
+ catch (Exception ex) {
+ // -> "unhandled exception", to be logged at error level by Quartz
+ throw new JobMethodInvocationFailedException(this.methodInvoker, ex);
+ }
+ }
+ }
+
+
+ /**
+ * Extension of the MethodInvokingJob, implementing the StatefulJob interface.
+ * Quartz checks whether or not jobs are stateful and if so,
+ * won't let jobs interfere with each other.
+ */
+ public static class StatefulMethodInvokingJob extends MethodInvokingJob implements StatefulJob {
+
+ // No implementation, just an addition of the tag interface StatefulJob
+ // in order to allow stateful method invoking jobs.
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java
new file mode 100644
index 00000000000..02882241403
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.SchedulerException;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.MutablePropertyValues;
+import org.springframework.beans.PropertyAccessorFactory;
+
+/**
+ * Simple implementation of the Quartz Job interface, applying the
+ * passed-in JobDataMap and also the SchedulerContext as bean property
+ * values. This is appropriate because a new Job instance will be created
+ * for each execution. JobDataMap entries will override SchedulerContext
+ * entries with the same keys.
+ *
+ * For example, let's assume that the JobDataMap contains a key
+ * "myParam" with value "5": The Job implementation can then expose
+ * a bean property "myParam" of type int to receive such a value,
+ * i.e. a method "setMyParam(int)". This will also work for complex
+ * types like business objects etc.
+ *
+ * Note: The QuartzJobBean class itself only implements the standard
+ * Quartz {@link org.quartz.Job} interface. Let your subclass explicitly
+ * implement the Quartz {@link org.quartz.StatefulJob} interface to
+ * mark your concrete job bean as stateful.
+ *
+ * This version of QuartzJobBean requires Quartz 1.5 or higher,
+ * due to the support for trigger-specific job data.
+ *
+ * Note that as of Spring 2.0 and Quartz 1.5, the preferred way
+ * to apply dependency injection to Job instances is via a JobFactory:
+ * that is, to specify {@link SpringBeanJobFactory} as Quartz JobFactory
+ * (typically via
+ * {@link SchedulerFactoryBean#setJobFactory} SchedulerFactoryBean's "jobFactory" property}).
+ * This allows to implement dependency-injected Quartz Jobs without
+ * a dependency on Spring base classes.
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see org.quartz.JobExecutionContext#getMergedJobDataMap()
+ * @see org.quartz.Scheduler#getContext()
+ * @see JobDetailBean#setJobDataAsMap
+ * @see SimpleTriggerBean#setJobDataAsMap
+ * @see CronTriggerBean#setJobDataAsMap
+ * @see SchedulerFactoryBean#setSchedulerContextAsMap
+ * @see SpringBeanJobFactory
+ * @see SchedulerFactoryBean#setJobFactory
+ */
+public abstract class QuartzJobBean implements Job {
+
+ /**
+ * This implementation applies the passed-in job data map as bean property
+ * values, and delegates to For concrete usage, check out the {@link SchedulerFactoryBean} and
+ * {@link SchedulerAccessorBean} classes.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5.6
+ */
+public abstract class SchedulerAccessor implements ResourceLoaderAware {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+
+ private boolean overwriteExistingJobs = false;
+
+ private String[] jobSchedulingDataLocations;
+
+ private List jobDetails;
+
+ private Map calendars;
+
+ private List triggers;
+
+
+ private SchedulerListener[] schedulerListeners;
+
+ private JobListener[] globalJobListeners;
+
+ private JobListener[] jobListeners;
+
+ private TriggerListener[] globalTriggerListeners;
+
+ private TriggerListener[] triggerListeners;
+
+
+ private PlatformTransactionManager transactionManager;
+
+ protected ResourceLoader resourceLoader;
+
+
+ /**
+ * Set whether any jobs defined on this SchedulerFactoryBean should overwrite
+ * existing job definitions. Default is "false", to not overwrite already
+ * registered jobs that have been read in from a persistent job store.
+ */
+ public void setOverwriteExistingJobs(boolean overwriteExistingJobs) {
+ this.overwriteExistingJobs = overwriteExistingJobs;
+ }
+
+ /**
+ * Set the location of a Quartz job definition XML file that follows the
+ * "job_scheduling_data_1_5" XSD. Can be specified to automatically
+ * register jobs that are defined in such a file, possibly in addition
+ * to jobs defined directly on this SchedulerFactoryBean.
+ * @see org.quartz.xml.JobSchedulingDataProcessor
+ */
+ public void setJobSchedulingDataLocation(String jobSchedulingDataLocation) {
+ this.jobSchedulingDataLocations = new String[] {jobSchedulingDataLocation};
+ }
+
+ /**
+ * Set the locations of Quartz job definition XML files that follow the
+ * "job_scheduling_data_1_5" XSD. Can be specified to automatically
+ * register jobs that are defined in such files, possibly in addition
+ * to jobs defined directly on this SchedulerFactoryBean.
+ * @see org.quartz.xml.JobSchedulingDataProcessor
+ */
+ public void setJobSchedulingDataLocations(String[] jobSchedulingDataLocations) {
+ this.jobSchedulingDataLocations = jobSchedulingDataLocations;
+ }
+
+ /**
+ * Register a list of JobDetail objects with the Scheduler that
+ * this FactoryBean creates, to be referenced by Triggers.
+ * This is not necessary when a Trigger determines the JobDetail
+ * itself: In this case, the JobDetail will be implicitly registered
+ * in combination with the Trigger.
+ * @see #setTriggers
+ * @see org.quartz.JobDetail
+ * @see JobDetailBean
+ * @see JobDetailAwareTrigger
+ * @see org.quartz.Trigger#setJobName
+ */
+ public void setJobDetails(JobDetail[] jobDetails) {
+ // Use modifiable ArrayList here, to allow for further adding of
+ // JobDetail objects during autodetection of JobDetailAwareTriggers.
+ this.jobDetails = new ArrayList(Arrays.asList(jobDetails));
+ }
+
+ /**
+ * Register a list of Quartz Calendar objects with the Scheduler
+ * that this FactoryBean creates, to be referenced by Triggers.
+ * @param calendars Map with calendar names as keys as Calendar
+ * objects as values
+ * @see org.quartz.Calendar
+ * @see org.quartz.Trigger#setCalendarName
+ */
+ public void setCalendars(Map calendars) {
+ this.calendars = calendars;
+ }
+
+ /**
+ * Register a list of Trigger objects with the Scheduler that
+ * this FactoryBean creates.
+ * If the Trigger determines the corresponding JobDetail itself,
+ * the job will be automatically registered with the Scheduler.
+ * Else, the respective JobDetail needs to be registered via the
+ * "jobDetails" property of this FactoryBean.
+ * @see #setJobDetails
+ * @see org.quartz.JobDetail
+ * @see JobDetailAwareTrigger
+ * @see CronTriggerBean
+ * @see SimpleTriggerBean
+ */
+ public void setTriggers(Trigger[] triggers) {
+ this.triggers = Arrays.asList(triggers);
+ }
+
+
+ /**
+ * Specify Quartz SchedulerListeners to be registered with the Scheduler.
+ */
+ public void setSchedulerListeners(SchedulerListener[] schedulerListeners) {
+ this.schedulerListeners = schedulerListeners;
+ }
+
+ /**
+ * Specify global Quartz JobListeners to be registered with the Scheduler.
+ * Such JobListeners will apply to all Jobs in the Scheduler.
+ */
+ public void setGlobalJobListeners(JobListener[] globalJobListeners) {
+ this.globalJobListeners = globalJobListeners;
+ }
+
+ /**
+ * Specify named Quartz JobListeners to be registered with the Scheduler.
+ * Such JobListeners will only apply to Jobs that explicitly activate
+ * them via their name.
+ * @see org.quartz.JobListener#getName
+ * @see org.quartz.JobDetail#addJobListener
+ * @see JobDetailBean#setJobListenerNames
+ */
+ public void setJobListeners(JobListener[] jobListeners) {
+ this.jobListeners = jobListeners;
+ }
+
+ /**
+ * Specify global Quartz TriggerListeners to be registered with the Scheduler.
+ * Such TriggerListeners will apply to all Triggers in the Scheduler.
+ */
+ public void setGlobalTriggerListeners(TriggerListener[] globalTriggerListeners) {
+ this.globalTriggerListeners = globalTriggerListeners;
+ }
+
+ /**
+ * Specify named Quartz TriggerListeners to be registered with the Scheduler.
+ * Such TriggerListeners will only apply to Triggers that explicitly activate
+ * them via their name.
+ * @see org.quartz.TriggerListener#getName
+ * @see org.quartz.Trigger#addTriggerListener
+ * @see CronTriggerBean#setTriggerListenerNames
+ * @see SimpleTriggerBean#setTriggerListenerNames
+ */
+ public void setTriggerListeners(TriggerListener[] triggerListeners) {
+ this.triggerListeners = triggerListeners;
+ }
+
+
+ /**
+ * Set the transaction manager to be used for registering jobs and triggers
+ * that are defined by this SchedulerFactoryBean. Default is none; setting
+ * this only makes sense when specifying a DataSource for the Scheduler.
+ */
+ public void setTransactionManager(PlatformTransactionManager transactionManager) {
+ this.transactionManager = transactionManager;
+ }
+
+ public void setResourceLoader(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+
+ /**
+ * Register jobs and triggers (within a transaction, if possible).
+ */
+ protected void registerJobsAndTriggers() throws SchedulerException {
+ TransactionStatus transactionStatus = null;
+ if (this.transactionManager != null) {
+ transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
+ }
+ try {
+
+ if (this.jobSchedulingDataLocations != null) {
+ ClassLoadHelper clh = new ResourceLoaderClassLoadHelper(this.resourceLoader);
+ clh.initialize();
+ JobSchedulingDataProcessor dataProcessor = new JobSchedulingDataProcessor(clh, true, true);
+ for (int i = 0; i < this.jobSchedulingDataLocations.length; i++) {
+ dataProcessor.processFileAndScheduleJobs(
+ this.jobSchedulingDataLocations[i], getScheduler(), this.overwriteExistingJobs);
+ }
+ }
+
+ // Register JobDetails.
+ if (this.jobDetails != null) {
+ for (Iterator it = this.jobDetails.iterator(); it.hasNext();) {
+ JobDetail jobDetail = (JobDetail) it.next();
+ addJobToScheduler(jobDetail);
+ }
+ }
+ else {
+ // Create empty list for easier checks when registering triggers.
+ this.jobDetails = new LinkedList();
+ }
+
+ // Register Calendars.
+ if (this.calendars != null) {
+ for (Iterator it = this.calendars.keySet().iterator(); it.hasNext();) {
+ String calendarName = (String) it.next();
+ Calendar calendar = (Calendar) this.calendars.get(calendarName);
+ getScheduler().addCalendar(calendarName, calendar, true, true);
+ }
+ }
+
+ // Register Triggers.
+ if (this.triggers != null) {
+ for (Iterator it = this.triggers.iterator(); it.hasNext();) {
+ Trigger trigger = (Trigger) it.next();
+ addTriggerToScheduler(trigger);
+ }
+ }
+ }
+
+ catch (Throwable ex) {
+ if (transactionStatus != null) {
+ try {
+ this.transactionManager.rollback(transactionStatus);
+ }
+ catch (TransactionException tex) {
+ logger.error("Job registration exception overridden by rollback exception", ex);
+ throw tex;
+ }
+ }
+ if (ex instanceof SchedulerException) {
+ throw (SchedulerException) ex;
+ }
+ if (ex instanceof Exception) {
+ throw new SchedulerException(
+ "Registration of jobs and triggers failed: " + ex.getMessage(), (Exception) ex);
+ }
+ throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage());
+ }
+
+ if (transactionStatus != null) {
+ this.transactionManager.commit(transactionStatus);
+ }
+ }
+
+ /**
+ * Add the given job to the Scheduler, if it doesn't already exist.
+ * Overwrites the job in any case if "overwriteExistingJobs" is set.
+ * @param jobDetail the job to add
+ * @return Schedulers can be registered in the repository through custom bootstrapping,
+ * e.g. via the {@link org.quartz.impl.StdSchedulerFactory} or
+ * {@link org.quartz.impl.DirectSchedulerFactory} factory classes.
+ * However, in general, it's preferable to use Spring's {@link SchedulerFactoryBean}
+ * which includes the job/trigger/listener capabilities of this accessor as well.
+ */
+ public void setSchedulerName(String schedulerName) {
+ this.schedulerName = schedulerName;
+ }
+
+ /**
+ * Specify the Quartz Scheduler instance to operate on.
+ */
+ public void setScheduler(Scheduler scheduler) {
+ this.scheduler = scheduler;
+ }
+
+ /**
+ * Return the Quartz Scheduler instance that this accessor operates on.
+ */
+ public Scheduler getScheduler() {
+ return this.scheduler;
+ }
+
+ public void setBeanFactory(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+
+ public void afterPropertiesSet() throws SchedulerException {
+ if (this.scheduler == null) {
+ if (this.schedulerName != null) {
+ this.scheduler = findScheduler(this.schedulerName);
+ }
+ else {
+ throw new IllegalStateException("No Scheduler specified");
+ }
+ }
+ registerListeners();
+ registerJobsAndTriggers();
+ }
+
+ protected Scheduler findScheduler(String schedulerName) throws SchedulerException {
+ if (this.beanFactory instanceof ListableBeanFactory) {
+ ListableBeanFactory lbf = (ListableBeanFactory) this.beanFactory;
+ String[] beanNames = lbf.getBeanNamesForType(Scheduler.class);
+ for (int i = 0; i < beanNames.length; i++) {
+ Scheduler schedulerBean = (Scheduler) lbf.getBean(beanNames[i]);
+ if (schedulerName.equals(schedulerBean.getSchedulerName())) {
+ return schedulerBean;
+ }
+ }
+ }
+ Scheduler schedulerInRepo = SchedulerRepository.getInstance().lookup(schedulerName);
+ if (schedulerInRepo == null) {
+ throw new IllegalStateException("No Scheduler named '" + schedulerName + "' found");
+ }
+ return schedulerInRepo;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java
new file mode 100644
index 00000000000..72f71f6501e
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.SchedulerContext;
+
+/**
+ * Callback interface to be implemented by Spring-managed
+ * Quartz artifacts that need access to the SchedulerContext
+ * (without having natural access to it).
+ *
+ * Currently only supported for custom JobFactory implementations
+ * that are passed in via Spring's SchedulerFactoryBean.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see org.quartz.spi.JobFactory
+ * @see SchedulerFactoryBean#setJobFactory
+ */
+public interface SchedulerContextAware {
+
+ /**
+ * Set the SchedulerContext of the current Quartz Scheduler.
+ * @see org.quartz.Scheduler#getContext()
+ */
+ void setSchedulerContext(SchedulerContext schedulerContext);
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java
new file mode 100644
index 00000000000..4ff102fd66c
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.SchedulerFactory;
+import org.quartz.impl.RemoteScheduler;
+import org.quartz.impl.SchedulerRepository;
+import org.quartz.impl.StdSchedulerFactory;
+import org.quartz.simpl.SimpleThreadPool;
+import org.quartz.spi.JobFactory;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.Lifecycle;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.SchedulingException;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * {@link FactoryBean} that creates and configures a Quartz {@link org.quartz.Scheduler},
+ * manages its lifecycle as part of the Spring application context, and exposes the
+ * Scheduler as bean reference for dependency injection.
+ *
+ * Allows registration of JobDetails, Calendars and Triggers, automatically
+ * starting the scheduler on initialization and shutting it down on destruction.
+ * In scenarios that just require static registration of jobs at startup, there
+ * is no need to access the Scheduler instance itself in application code.
+ *
+ * For dynamic registration of jobs at runtime, use a bean reference to
+ * this SchedulerFactoryBean to get direct access to the Quartz Scheduler
+ * ( Note that Quartz instantiates a new Job for each execution, in
+ * contrast to Timer which uses a TimerTask instance that is shared
+ * between repeated executions. Just JobDetail descriptors are shared.
+ *
+ * When using persistent jobs, it is strongly recommended to perform all
+ * operations on the Scheduler within Spring-managed (or plain JTA) transactions.
+ * Else, database locking will not properly work and might even break.
+ * (See {@link #setDataSource setDataSource} javadoc for details.)
+ *
+ * The preferred way to achieve transactional execution is to demarcate
+ * declarative transactions at the business facade level, which will
+ * automatically apply to Scheduler operations performed within those scopes.
+ * Alternatively, you may add transactional advice for the Scheduler itself.
+ *
+ * Note: This version of Spring's SchedulerFactoryBean requires
+ * Quartz 1.5.x or 1.6.x. The "jobSchedulingDataLocation" feature requires
+ * Quartz 1.6.1 or higher (as of Spring 2.5.5).
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see #setDataSource
+ * @see org.quartz.Scheduler
+ * @see org.quartz.SchedulerFactory
+ * @see org.quartz.impl.StdSchedulerFactory
+ * @see org.springframework.transaction.interceptor.TransactionProxyFactoryBean
+ */
+public class SchedulerFactoryBean extends SchedulerAccessor
+ implements FactoryBean, BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, Lifecycle {
+
+ public static final String PROP_THREAD_COUNT = "org.quartz.threadPool.threadCount";
+
+ public static final int DEFAULT_THREAD_COUNT = 10;
+
+
+ private static final ThreadLocal configTimeResourceLoaderHolder = new ThreadLocal();
+
+ private static final ThreadLocal configTimeTaskExecutorHolder = new ThreadLocal();
+
+ private static final ThreadLocal configTimeDataSourceHolder = new ThreadLocal();
+
+ private static final ThreadLocal configTimeNonTransactionalDataSourceHolder = new ThreadLocal();
+
+ /**
+ * Return the ResourceLoader for the currently configured Quartz Scheduler,
+ * to be used by ResourceLoaderClassLoadHelper.
+ * This instance will be set before initialization of the corresponding
+ * Scheduler, and reset immediately afterwards. It is thus only available
+ * during configuration.
+ * @see #setApplicationContext
+ * @see ResourceLoaderClassLoadHelper
+ */
+ public static ResourceLoader getConfigTimeResourceLoader() {
+ return (ResourceLoader) configTimeResourceLoaderHolder.get();
+ }
+
+ /**
+ * Return the TaskExecutor for the currently configured Quartz Scheduler,
+ * to be used by LocalTaskExecutorThreadPool.
+ * This instance will be set before initialization of the corresponding
+ * Scheduler, and reset immediately afterwards. It is thus only available
+ * during configuration.
+ * @see #setTaskExecutor
+ * @see LocalTaskExecutorThreadPool
+ */
+ public static TaskExecutor getConfigTimeTaskExecutor() {
+ return (TaskExecutor) configTimeTaskExecutorHolder.get();
+ }
+
+ /**
+ * Return the DataSource for the currently configured Quartz Scheduler,
+ * to be used by LocalDataSourceJobStore.
+ * This instance will be set before initialization of the corresponding
+ * Scheduler, and reset immediately afterwards. It is thus only available
+ * during configuration.
+ * @see #setDataSource
+ * @see LocalDataSourceJobStore
+ */
+ public static DataSource getConfigTimeDataSource() {
+ return (DataSource) configTimeDataSourceHolder.get();
+ }
+
+ /**
+ * Return the non-transactional DataSource for the currently configured
+ * Quartz Scheduler, to be used by LocalDataSourceJobStore.
+ * This instance will be set before initialization of the corresponding
+ * Scheduler, and reset immediately afterwards. It is thus only available
+ * during configuration.
+ * @see #setNonTransactionalDataSource
+ * @see LocalDataSourceJobStore
+ */
+ public static DataSource getConfigTimeNonTransactionalDataSource() {
+ return (DataSource) configTimeNonTransactionalDataSourceHolder.get();
+ }
+
+
+ private Class schedulerFactoryClass = StdSchedulerFactory.class;
+
+ private String schedulerName;
+
+ private Resource configLocation;
+
+ private Properties quartzProperties;
+
+
+ private TaskExecutor taskExecutor;
+
+ private DataSource dataSource;
+
+ private DataSource nonTransactionalDataSource;
+
+
+ private Map schedulerContextMap;
+
+ private ApplicationContext applicationContext;
+
+ private String applicationContextSchedulerContextKey;
+
+ private JobFactory jobFactory;
+
+ private boolean jobFactorySet = false;
+
+
+ private boolean autoStartup = true;
+
+ private int startupDelay = 0;
+
+ private boolean exposeSchedulerInRepository = false;
+
+ private boolean waitForJobsToCompleteOnShutdown = false;
+
+
+ private Scheduler scheduler;
+
+
+ /**
+ * Set the Quartz SchedulerFactory implementation to use.
+ * Default is StdSchedulerFactory, reading in the standard
+ * If not specified, the bean name will be used as default scheduler name.
+ * @see #setBeanName
+ * @see org.quartz.SchedulerFactory#getScheduler()
+ * @see org.quartz.SchedulerFactory#getScheduler(String)
+ */
+ public void setSchedulerName(String schedulerName) {
+ this.schedulerName = schedulerName;
+ }
+
+ /**
+ * Set the location of the Quartz properties config file, for example
+ * as classpath resource "classpath:quartz.properties".
+ * Note: Can be omitted when all necessary properties are specified
+ * locally via this bean, or when relying on Quartz' default configuration.
+ * @see #setQuartzProperties
+ */
+ public void setConfigLocation(Resource configLocation) {
+ this.configLocation = configLocation;
+ }
+
+ /**
+ * Set Quartz properties, like "org.quartz.threadPool.class".
+ * Can be used to override values in a Quartz properties config file,
+ * or to specify all necessary properties locally.
+ * @see #setConfigLocation
+ */
+ public void setQuartzProperties(Properties quartzProperties) {
+ this.quartzProperties = quartzProperties;
+ }
+
+
+ /**
+ * Set the Spring TaskExecutor to use as Quartz backend.
+ * Exposed as thread pool through the Quartz SPI.
+ * Can be used to assign a JDK 1.5 ThreadPoolExecutor or a CommonJ
+ * WorkManager as Quartz backend, to avoid Quartz's manual thread creation.
+ * By default, a Quartz SimpleThreadPool will be used, configured through
+ * the corresponding Quartz properties.
+ * @see #setQuartzProperties
+ * @see LocalTaskExecutorThreadPool
+ * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
+ * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor
+ */
+ public void setTaskExecutor(TaskExecutor taskExecutor) {
+ this.taskExecutor = taskExecutor;
+ }
+
+ /**
+ * Set the default DataSource to be used by the Scheduler. If set,
+ * this will override corresponding settings in Quartz properties.
+ * Note: If this is set, the Quartz settings should not define
+ * a job store "dataSource" to avoid meaningless double configuration.
+ * A Spring-specific subclass of Quartz' JobStoreCMT will be used.
+ * It is therefore strongly recommended to perform all operations on
+ * the Scheduler within Spring-managed (or plain JTA) transactions.
+ * Else, database locking will not properly work and might even break
+ * (e.g. if trying to obtain a lock on Oracle without a transaction).
+ * Supports both transactional and non-transactional DataSource access.
+ * With a non-XA DataSource and local Spring transactions, a single DataSource
+ * argument is sufficient. In case of an XA DataSource and global JTA transactions,
+ * SchedulerFactoryBean's "nonTransactionalDataSource" property should be set,
+ * passing in a non-XA DataSource that will not participate in global transactions.
+ * @see #setNonTransactionalDataSource
+ * @see #setQuartzProperties
+ * @see #setTransactionManager
+ * @see LocalDataSourceJobStore
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ /**
+ * Set the DataSource to be used by the Scheduler for non-transactional access.
+ * This is only necessary if the default DataSource is an XA DataSource that will
+ * always participate in transactions: A non-XA version of that DataSource should
+ * be specified as "nonTransactionalDataSource" in such a scenario.
+ * This is not relevant with a local DataSource instance and Spring transactions.
+ * Specifying a single default DataSource as "dataSource" is sufficient there.
+ * @see #setDataSource
+ * @see LocalDataSourceJobStore
+ */
+ public void setNonTransactionalDataSource(DataSource nonTransactionalDataSource) {
+ this.nonTransactionalDataSource = nonTransactionalDataSource;
+ }
+
+
+ /**
+ * Register objects in the Scheduler context via a given Map.
+ * These objects will be available to any Job that runs in this Scheduler.
+ * Note: When using persistent Jobs whose JobDetail will be kept in the
+ * database, do not put Spring-managed beans or an ApplicationContext
+ * reference into the JobDataMap but rather into the SchedulerContext.
+ * @param schedulerContextAsMap Map with String keys and any objects as
+ * values (for example Spring-managed beans)
+ * @see JobDetailBean#setJobDataAsMap
+ */
+ public void setSchedulerContextAsMap(Map schedulerContextAsMap) {
+ this.schedulerContextMap = schedulerContextAsMap;
+ }
+
+ /**
+ * Set the key of an ApplicationContext reference to expose in the
+ * SchedulerContext, for example "applicationContext". Default is none.
+ * Only applicable when running in a Spring ApplicationContext.
+ * Note: When using persistent Jobs whose JobDetail will be kept in the
+ * database, do not put an ApplicationContext reference into the JobDataMap
+ * but rather into the SchedulerContext.
+ * In case of a QuartzJobBean, the reference will be applied to the Job
+ * instance as bean property. An "applicationContext" attribute will
+ * correspond to a "setApplicationContext" method in that scenario.
+ * Note that BeanFactory callback interfaces like ApplicationContextAware
+ * are not automatically applied to Quartz Job instances, because Quartz
+ * itself is reponsible for the lifecycle of its Jobs.
+ * @see JobDetailBean#setApplicationContextJobDataKey
+ * @see org.springframework.context.ApplicationContext
+ */
+ public void setApplicationContextSchedulerContextKey(String applicationContextSchedulerContextKey) {
+ this.applicationContextSchedulerContextKey = applicationContextSchedulerContextKey;
+ }
+
+ /**
+ * Set the Quartz JobFactory to use for this Scheduler.
+ * Default is Spring's {@link AdaptableJobFactory}, which supports
+ * {@link java.lang.Runnable} objects as well as standard Quartz
+ * {@link org.quartz.Job} instances. Note that this default only applies
+ * to a local Scheduler, not to a RemoteScheduler (where setting
+ * a custom JobFactory is not supported by Quartz).
+ * Specify an instance of Spring's {@link SpringBeanJobFactory} here
+ * (typically as an inner bean definition) to automatically populate a job's
+ * bean properties from the specified job data map and scheduler context.
+ * @see AdaptableJobFactory
+ * @see SpringBeanJobFactory
+ */
+ public void setJobFactory(JobFactory jobFactory) {
+ this.jobFactory = jobFactory;
+ this.jobFactorySet = true;
+ }
+
+
+ /**
+ * Set whether to automatically start the scheduler after initialization.
+ * Default is "true"; set this to "false" to allow for manual startup.
+ */
+ public void setAutoStartup(boolean autoStartup) {
+ this.autoStartup = autoStartup;
+ }
+
+ /**
+ * Set the number of seconds to wait after initialization before
+ * starting the scheduler asynchronously. Default is 0, meaning
+ * immediate synchronous startup on initialization of this bean.
+ * Setting this to 10 or 20 seconds makes sense if no jobs
+ * should be run before the entire application has started up.
+ */
+ public void setStartupDelay(int startupDelay) {
+ this.startupDelay = startupDelay;
+ }
+
+ /**
+ * Set whether to expose the Spring-managed {@link Scheduler} instance in the
+ * Quartz {@link SchedulerRepository}. Default is "false", since the Spring-managed
+ * Scheduler is usually exclusively intended for access within the Spring context.
+ * Switch this flag to "true" in order to expose the Scheduler globally.
+ * This is not recommended unless you have an existing Spring application that
+ * relies on this behavior. Note that such global exposure was the accidental
+ * default in earlier Spring versions; this has been fixed as of Spring 2.5.6.
+ */
+ public void setExposeSchedulerInRepository(boolean exposeSchedulerInRepository) {
+ this.exposeSchedulerInRepository = exposeSchedulerInRepository;
+ }
+
+ /**
+ * Set whether to wait for running jobs to complete on shutdown.
+ * Default is "false". Switch this to "true" if you prefer
+ * fully completed jobs at the expense of a longer shutdown phase.
+ * @see org.quartz.Scheduler#shutdown(boolean)
+ */
+ public void setWaitForJobsToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) {
+ this.waitForJobsToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
+ }
+
+
+ public void setBeanName(String name) {
+ if (this.schedulerName == null) {
+ this.schedulerName = name;
+ }
+ }
+
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of InitializingBean interface
+ //---------------------------------------------------------------------
+
+ public void afterPropertiesSet() throws Exception {
+ if (this.applicationContext != null && this.resourceLoader == null) {
+ this.resourceLoader = this.applicationContext;
+ }
+
+ if (this.dataSource == null && this.nonTransactionalDataSource != null) {
+ this.dataSource = this.nonTransactionalDataSource;
+ }
+
+ // Create SchedulerFactory instance.
+ SchedulerFactory schedulerFactory = (SchedulerFactory) BeanUtils.instantiateClass(this.schedulerFactoryClass);
+
+ initSchedulerFactory(schedulerFactory);
+
+ if (this.resourceLoader != null) {
+ // Make given ResourceLoader available for SchedulerFactory configuration.
+ configTimeResourceLoaderHolder.set(this.resourceLoader);
+ }
+ if (this.taskExecutor != null) {
+ // Make given TaskExecutor available for SchedulerFactory configuration.
+ configTimeTaskExecutorHolder.set(this.taskExecutor);
+ }
+ if (this.dataSource != null) {
+ // Make given DataSource available for SchedulerFactory configuration.
+ configTimeDataSourceHolder.set(this.dataSource);
+ }
+ if (this.nonTransactionalDataSource != null) {
+ // Make given non-transactional DataSource available for SchedulerFactory configuration.
+ configTimeNonTransactionalDataSourceHolder.set(this.nonTransactionalDataSource);
+ }
+
+
+ // Get Scheduler instance from SchedulerFactory.
+ try {
+ this.scheduler = createScheduler(schedulerFactory, this.schedulerName);
+ populateSchedulerContext();
+
+ if (!this.jobFactorySet && !(this.scheduler instanceof RemoteScheduler)) {
+ // Use AdaptableJobFactory as default for a local Scheduler, unless when
+ // explicitly given a null value through the "jobFactory" bean property.
+ this.jobFactory = new AdaptableJobFactory();
+ }
+ if (this.jobFactory != null) {
+ if (this.jobFactory instanceof SchedulerContextAware) {
+ ((SchedulerContextAware) this.jobFactory).setSchedulerContext(this.scheduler.getContext());
+ }
+ this.scheduler.setJobFactory(this.jobFactory);
+ }
+ }
+
+ finally {
+ if (this.resourceLoader != null) {
+ configTimeResourceLoaderHolder.set(null);
+ }
+ if (this.taskExecutor != null) {
+ configTimeTaskExecutorHolder.set(null);
+ }
+ if (this.dataSource != null) {
+ configTimeDataSourceHolder.set(null);
+ }
+ if (this.nonTransactionalDataSource != null) {
+ configTimeNonTransactionalDataSourceHolder.set(null);
+ }
+ }
+
+ registerListeners();
+ registerJobsAndTriggers();
+
+ // Start Scheduler immediately, if demanded.
+ if (this.autoStartup) {
+ startScheduler(this.scheduler, this.startupDelay);
+ }
+ }
+
+
+ /**
+ * Load and/or apply Quartz properties to the given SchedulerFactory.
+ * @param schedulerFactory the SchedulerFactory to initialize
+ */
+ private void initSchedulerFactory(SchedulerFactory schedulerFactory)
+ throws SchedulerException, IOException {
+
+ if (!(schedulerFactory instanceof StdSchedulerFactory)) {
+ if (this.configLocation != null || this.quartzProperties != null ||
+ this.taskExecutor != null || this.dataSource != null) {
+ throw new IllegalArgumentException(
+ "StdSchedulerFactory required for applying Quartz properties: " + schedulerFactory);
+ }
+ // Otherwise assume that no initialization is necessary...
+ return;
+ }
+
+ Properties mergedProps = new Properties();
+
+ if (this.resourceLoader != null) {
+ mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS,
+ ResourceLoaderClassLoadHelper.class.getName());
+ }
+
+ if (this.taskExecutor != null) {
+ mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS,
+ LocalTaskExecutorThreadPool.class.getName());
+ }
+ else {
+ // Set necessary default properties here, as Quartz will not apply
+ // its default configuration when explicitly given properties.
+ mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName());
+ mergedProps.setProperty(PROP_THREAD_COUNT, Integer.toString(DEFAULT_THREAD_COUNT));
+ }
+
+ if (this.configLocation != null) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Loading Quartz config from [" + this.configLocation + "]");
+ }
+ PropertiesLoaderUtils.fillProperties(mergedProps, this.configLocation);
+ }
+
+ CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps);
+
+ if (this.dataSource != null) {
+ mergedProps.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
+ }
+
+ // Make sure to set the scheduler name as configured in the Spring configuration.
+ if (this.schedulerName != null) {
+ mergedProps.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.schedulerName);
+ }
+
+ ((StdSchedulerFactory) schedulerFactory).initialize(mergedProps);
+ }
+
+ /**
+ * Create the Scheduler instance for the given factory and scheduler name.
+ * Called by {@link #afterPropertiesSet}.
+ * The default implementation invokes SchedulerFactory's Can be used as a thread-pooling TaskExecutor backend, in particular
+ * on JDK <= 1.5 (where the JDK ThreadPoolExecutor isn't available yet).
+ * Can be shared between a Quartz Scheduler (specified as "taskExecutor")
+ * and other TaskExecutor users, or even used completely independent of
+ * a Quartz Scheduler (as plain TaskExecutor backend).
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see org.quartz.simpl.SimpleThreadPool
+ * @see org.springframework.core.task.TaskExecutor
+ * @see SchedulerFactoryBean#setTaskExecutor
+ */
+public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool
+ implements SchedulingTaskExecutor, InitializingBean, DisposableBean {
+
+ private boolean waitForJobsToCompleteOnShutdown = false;
+
+
+ /**
+ * Set whether to wait for running jobs to complete on shutdown.
+ * Default is "false".
+ * @see org.quartz.simpl.SimpleThreadPool#shutdown(boolean)
+ */
+ public void setWaitForJobsToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) {
+ this.waitForJobsToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
+ }
+
+ public void afterPropertiesSet() throws SchedulerConfigException {
+ initialize();
+ }
+
+
+ public void execute(Runnable task) {
+ Assert.notNull(task, "Runnable must not be null");
+ if (!runInThread(task)) {
+ throw new SchedulingException("Quartz SimpleThreadPool already shut down");
+ }
+ }
+
+ /**
+ * This task executor prefers short-lived work units.
+ */
+ public boolean prefersShortLivedTasks() {
+ return true;
+ }
+
+
+ public void destroy() {
+ shutdown(this.waitForJobsToCompleteOnShutdown);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerBean.java
new file mode 100644
index 00000000000..d91f34e6b9d
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerBean.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.text.ParseException;
+import java.util.Date;
+import java.util.Map;
+
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+import org.quartz.SimpleTrigger;
+
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.Constants;
+
+/**
+ * Convenience subclass of Quartz's {@link org.quartz.SimpleTrigger}
+ * class, making bean-style usage easier.
+ *
+ * SimpleTrigger itself is already a JavaBean but lacks sensible defaults.
+ * This class uses the Spring bean name as job name, the Quartz default group
+ * ("DEFAULT") as job group, the current time as start time, and indefinite
+ * repetition, if not specified.
+ *
+ * This class will also register the trigger with the job name and group of
+ * a given {@link org.quartz.JobDetail}. This allows {@link SchedulerFactoryBean}
+ * to automatically register a trigger for the corresponding JobDetail,
+ * instead of registering the JobDetail separately.
+ *
+ * NOTE: This convenience subclass does not work with trigger
+ * persistence in Quartz 1.6, due to a change in Quartz's trigger handling.
+ * Use Quartz 1.5 if you rely on trigger persistence based on this class,
+ * or the standard Quartz {@link org.quartz.SimpleTrigger} class instead.
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see #setName
+ * @see #setGroup
+ * @see #setStartTime
+ * @see #setJobName
+ * @see #setJobGroup
+ * @see #setJobDetail
+ * @see SchedulerFactoryBean#setTriggers
+ * @see SchedulerFactoryBean#setJobDetails
+ * @see CronTriggerBean
+ */
+public class SimpleTriggerBean extends SimpleTrigger
+ implements JobDetailAwareTrigger, BeanNameAware, InitializingBean {
+
+ /** Constants for the SimpleTrigger class */
+ private static final Constants constants = new Constants(SimpleTrigger.class);
+
+
+ private long startDelay = 0;
+
+ private JobDetail jobDetail;
+
+ private String beanName;
+
+
+ public SimpleTriggerBean() {
+ setRepeatCount(REPEAT_INDEFINITELY);
+ }
+
+ /**
+ * Register objects in the JobDataMap via a given Map.
+ * These objects will be available to this Trigger only,
+ * in contrast to objects in the JobDetail's data map.
+ * @param jobDataAsMap Map with String keys and any objects as values
+ * (for example Spring-managed beans)
+ * @see JobDetailBean#setJobDataAsMap
+ */
+ public void setJobDataAsMap(Map jobDataAsMap) {
+ getJobDataMap().putAll(jobDataAsMap);
+ }
+
+ /**
+ * Set the misfire instruction via the name of the corresponding
+ * constant in the {@link org.quartz.SimpleTrigger} class.
+ * Default is A TriggerListener name always refers to the name returned
+ * by the TriggerListener implementation.
+ * @see SchedulerFactoryBean#setTriggerListeners
+ * @see org.quartz.TriggerListener#getName
+ */
+ public void setTriggerListenerNames(String[] names) {
+ for (int i = 0; i < names.length; i++) {
+ addTriggerListener(names[i]);
+ }
+ }
+
+ /**
+ * Set the delay before starting the job for the first time.
+ * The given number of milliseconds will be added to the current
+ * time to calculate the start time. Default is 0.
+ * This delay will just be applied if no custom start time was
+ * specified. However, in typical usage within a Spring context,
+ * the start time will be the container startup time anyway.
+ * Specifying a relative delay is appropriate in that case.
+ * @see #setStartTime
+ */
+ public void setStartDelay(long startDelay) {
+ this.startDelay = startDelay;
+ }
+
+ /**
+ * Set the JobDetail that this trigger should be associated with.
+ * This is typically used with a bean reference if the JobDetail
+ * is a Spring-managed bean. Alternatively, the trigger can also
+ * be associated with a job by name and group.
+ * @see #setJobName
+ * @see #setJobGroup
+ */
+ public void setJobDetail(JobDetail jobDetail) {
+ this.jobDetail = jobDetail;
+ }
+
+ public JobDetail getJobDetail() {
+ return this.jobDetail;
+ }
+
+ public void setBeanName(String beanName) {
+ this.beanName = beanName;
+ }
+
+
+ public void afterPropertiesSet() throws ParseException {
+ if (getName() == null) {
+ setName(this.beanName);
+ }
+ if (getGroup() == null) {
+ setGroup(Scheduler.DEFAULT_GROUP);
+ }
+ if (getStartTime() == null) {
+ setStartTime(new Date(System.currentTimeMillis() + this.startDelay));
+ }
+ if (this.jobDetail != null) {
+ setJobName(this.jobDetail.getName());
+ setJobGroup(this.jobDetail.getGroup());
+ }
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java
new file mode 100644
index 00000000000..9a604930950
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.SchedulerContext;
+import org.quartz.spi.TriggerFiredBundle;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.MutablePropertyValues;
+import org.springframework.beans.PropertyAccessorFactory;
+import org.springframework.beans.BeanUtils;
+
+/**
+ * Subclass of {@link AdaptableJobFactory} that also supports Spring-style
+ * dependency injection on bean properties. This is essentially the direct
+ * equivalent of Spring's {@link QuartzJobBean} in the shape of a
+ * Quartz 1.5 {@link org.quartz.spi.JobFactory}.
+ *
+ * Applies scheduler context, job data map and trigger data map entries
+ * as bean property values. If no matching bean property is found, the entry
+ * is by default simply ignored. This is analogous to QuartzJobBean's behavior.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ * @see SchedulerFactoryBean#setJobFactory
+ * @see QuartzJobBean
+ */
+public class SpringBeanJobFactory extends AdaptableJobFactory implements SchedulerContextAware {
+
+ private String[] ignoredUnknownProperties;
+
+ private SchedulerContext schedulerContext;
+
+
+ /**
+ * Specify the unknown properties (not found in the bean) that should be ignored.
+ * Default is The default implementation ignores {@link QuartzJobBean} instances,
+ * which will inject bean properties themselves.
+ * @param jobObject the job object to introspect
+ * @see QuartzJobBean
+ */
+ protected boolean isEligibleForPropertyPopulation(Object jobObject) {
+ return (!(jobObject instanceof QuartzJobBean));
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/package.html b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/package.html
new file mode 100644
index 00000000000..612d2738f7c
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/package.html
@@ -0,0 +1,11 @@
+
+ The optional "configLocation" property sets the location of a FreeMarker
+ * properties file, within the current application. FreeMarker properties can be
+ * overridden via "freemarkerSettings". All of these properties will be set by
+ * calling FreeMarker's The "freemarkerVariables" property can be used to specify a Map of
+ * shared variables that will be applied to the Configuration via the
+ * The simplest way to use this class is to specify a "templateLoaderPath";
+ * FreeMarker does not need any further configuration then.
+ *
+ * Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
+ *
+ * @author Darren Davison
+ * @author Juergen Hoeller
+ * @since 03.03.2004
+ * @see #setConfigLocation
+ * @see #setFreemarkerSettings
+ * @see #setFreemarkerVariables
+ * @see #setTemplateLoaderPath
+ * @see #createConfiguration
+ * @see FreeMarkerConfigurationFactoryBean
+ * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
+ * @see freemarker.template.Configuration
+ */
+public class FreeMarkerConfigurationFactory {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private Resource configLocation;
+
+ private Properties freemarkerSettings;
+
+ private Map freemarkerVariables;
+
+ private String defaultEncoding;
+
+ private final List templateLoaders = new ArrayList();
+
+ private List preTemplateLoaders;
+
+ private List postTemplateLoaders;
+
+ private String[] templateLoaderPaths;
+
+ private ResourceLoader resourceLoader = new DefaultResourceLoader();
+
+ private boolean preferFileSystemAccess = true;
+
+
+ /**
+ * Set the location of the FreeMarker config file.
+ * Alternatively, you can specify all setting locally.
+ * @see #setFreemarkerSettings
+ * @see #setTemplateLoaderPath
+ */
+ public void setConfigLocation(Resource resource) {
+ configLocation = resource;
+ }
+
+ /**
+ * Set properties that contain well-known FreeMarker keys which will be
+ * passed to FreeMarker's Used for template rendering unless there is an explicit encoding specified
+ * for the rendering process (for example, on Spring's FreeMarkerView).
+ * @see freemarker.template.Configuration#setDefaultEncoding
+ * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding
+ */
+ public void setDefaultEncoding(String defaultEncoding) {
+ this.defaultEncoding = defaultEncoding;
+ }
+
+ /**
+ * Set a List of The {@link TemplateLoader TemplateLoaders} specified here will be
+ * registered before the default template loaders that this factory
+ * registers (such as loaders for specified "templateLoaderPaths" or any
+ * loaders registered in {@link #postProcessTemplateLoaders}).
+ * @see #setTemplateLoaderPaths
+ * @see #postProcessTemplateLoaders
+ */
+ public void setPreTemplateLoaders(TemplateLoader[] preTemplateLoaders) {
+ this.preTemplateLoaders = Arrays.asList(preTemplateLoaders);
+ }
+
+ /**
+ * Set a List of The {@link TemplateLoader TemplateLoaders} specified here will be
+ * registered after the default template loaders that this factory
+ * registers (such as loaders for specified "templateLoaderPaths" or any
+ * loaders registered in {@link #postProcessTemplateLoaders}).
+ * @see #setTemplateLoaderPaths
+ * @see #postProcessTemplateLoaders
+ */
+ public void setPostTemplateLoaders(TemplateLoader[] postTemplateLoaders) {
+ this.postTemplateLoaders = Arrays.asList(postTemplateLoaders);
+ }
+
+ /**
+ * Set the Freemarker template loader path via a Spring resource location.
+ * See the "templateLoaderPaths" property for details on path handling.
+ * @see #setTemplateLoaderPaths
+ */
+ public void setTemplateLoaderPath(String templateLoaderPath) {
+ this.templateLoaderPaths = new String[] {templateLoaderPath};
+ }
+
+ /**
+ * Set multiple Freemarker template loader paths via Spring resource locations.
+ * When populated via a String, standard URLs like "file:" and "classpath:"
+ * pseudo URLs are supported, as understood by ResourceEditor. Allows for
+ * relative paths when running in an ApplicationContext.
+ * Will define a path for the default FreeMarker template loader.
+ * If a specified resource cannot be resolved to a To enforce the use of SpringTemplateLoader, i.e. to not resolve a path
+ * as file system resource in any case, turn off the "preferFileSystemAccess"
+ * flag. See the latter's javadoc for details.
+ * If you wish to specify your own list of TemplateLoaders, do not set this
+ * property and instead use If this is enabled, FreeMarkerConfigurationFactory will try to resolve
+ * the specified "templateLoaderPath" as file system resource (which will work
+ * for expanded class path resources and ServletContext resources too).
+ * Default is "true". Turn this off to always load via SpringTemplateLoader
+ * (i.e. as stream, without hot detection of template changes), which might
+ * be necessary if some of your templates reside in an expanded classes
+ * directory while others reside in jar files.
+ * @see #setTemplateLoaderPath
+ */
+ public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
+ this.preferFileSystemAccess = preferFileSystemAccess;
+ }
+
+ /**
+ * Return whether to prefer file system access for template loading.
+ */
+ protected boolean isPreferFileSystemAccess() {
+ return preferFileSystemAccess;
+ }
+
+
+ /**
+ * Prepare the FreeMarker Configuration and return it.
+ * @return the FreeMarker Configuration object
+ * @throws IOException if the config file wasn't found
+ * @throws TemplateException on FreeMarker initialization failure
+ */
+ public Configuration createConfiguration() throws IOException, TemplateException {
+ Configuration config = newConfiguration();
+ Properties props = new Properties();
+
+ // Load config file if specified.
+ if (this.configLocation != null) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Loading FreeMarker configuration from " + this.configLocation);
+ }
+ PropertiesLoaderUtils.fillProperties(props, this.configLocation);
+ }
+
+ // Merge local properties if specified.
+ if (this.freemarkerSettings != null) {
+ props.putAll(this.freemarkerSettings);
+ }
+
+ // FreeMarker will only accept known keys in its setSettings and
+ // setAllSharedVariables methods.
+ if (!props.isEmpty()) {
+ config.setSettings(props);
+ }
+
+ if (!CollectionUtils.isEmpty(this.freemarkerVariables)) {
+ config.setAllSharedVariables(new SimpleHash(this.freemarkerVariables));
+ }
+
+ if (this.defaultEncoding != null) {
+ config.setDefaultEncoding(this.defaultEncoding);
+ }
+
+ // Register template loaders that are supposed to kick in early.
+ if (this.preTemplateLoaders != null) {
+ this.templateLoaders.addAll(this.preTemplateLoaders);
+ }
+
+ // Register default template loaders.
+ if (this.templateLoaderPaths != null) {
+ for (int i = 0; i < this.templateLoaderPaths.length; i++) {
+ this.templateLoaders.add(getTemplateLoaderForPath(this.templateLoaderPaths[i]));
+ }
+ }
+ postProcessTemplateLoaders(this.templateLoaders);
+
+ // Register template loaders that are supposed to kick in late.
+ if (this.postTemplateLoaders != null) {
+ this.templateLoaders.addAll(this.postTemplateLoaders);
+ }
+
+ TemplateLoader loader = getAggregateTemplateLoader(this.templateLoaders);
+ if (loader != null) {
+ config.setTemplateLoader(loader);
+ }
+
+ postProcessConfiguration(config);
+ return config;
+ }
+
+ /**
+ * Return a new Configuration object. Subclasses can override this for
+ * custom initialization, or for using a mock object for testing.
+ * Called by Default implementation creates either a FileTemplateLoader or
+ * a SpringTemplateLoader.
+ * @param templateLoaderPath the path to load templates from
+ * @return an appropriate TemplateLoader
+ * @see freemarker.cache.FileTemplateLoader
+ * @see SpringTemplateLoader
+ */
+ protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
+ if (isPreferFileSystemAccess()) {
+ // Try to load via the file system, fall back to SpringTemplateLoader
+ // (for hot detection of template changes, if possible).
+ try {
+ Resource path = getResourceLoader().getResource(templateLoaderPath);
+ File file = path.getFile(); // will fail if not resolvable in the file system
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ "Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]");
+ }
+ return new FileTemplateLoader(file);
+ }
+ catch (IOException ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Cannot resolve template loader path [" + templateLoaderPath +
+ "] to [java.io.File]: using SpringTemplateLoader as fallback", ex);
+ }
+ return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
+ }
+ }
+ else {
+ // Always load via SpringTemplateLoader (without hot detection of template changes).
+ logger.debug("File system access not preferred: using SpringTemplateLoader");
+ return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
+ }
+ }
+
+ /**
+ * To be overridden by subclasses that want to to register custom
+ * TemplateLoader instances after this factory created its default
+ * template loaders.
+ * Called by Called by Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
+ *
+ * @author Darren Davison
+ * @since 03.03.2004
+ * @see #setConfigLocation
+ * @see #setFreemarkerSettings
+ * @see #setTemplateLoaderPath
+ * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
+ */
+public class FreeMarkerConfigurationFactoryBean extends FreeMarkerConfigurationFactory
+ implements FactoryBean, InitializingBean, ResourceLoaderAware {
+
+ private Configuration configuration;
+
+
+ public void afterPropertiesSet() throws IOException, TemplateException {
+ this.configuration = createConfiguration();
+ }
+
+
+ public Object getObject() {
+ return this.configuration;
+ }
+
+ public Class getObjectType() {
+ return Configuration.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java
new file mode 100644
index 00000000000..f4ba0677529
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.freemarker;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * Utility class for working with FreeMarker.
+ * Provides convenience methods to process a FreeMarker template with a model.
+ *
+ * @author Juergen Hoeller
+ * @since 14.03.2004
+ */
+public abstract class FreeMarkerTemplateUtils {
+
+ /**
+ * Process the specified FreeMarker template with the given model and write
+ * the result to the given Writer.
+ * When using this method to prepare a text for a mail to be sent with Spring's
+ * mail support, consider wrapping IO/TemplateException in MailPreparationException.
+ * @param model the model object, typically a Map that contains model names
+ * as keys and model objects as values
+ * @return the result as String
+ * @throws IOException if the template wasn't found or couldn't be read
+ * @throws freemarker.template.TemplateException if rendering failed
+ * @see org.springframework.mail.MailPreparationException
+ */
+ public static String processTemplateIntoString(Template template, Object model)
+ throws IOException, TemplateException {
+ StringWriter result = new StringWriter();
+ template.process(model, result);
+ return result.toString();
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java
new file mode 100644
index 00000000000..1fce80718e9
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.freemarker;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import freemarker.cache.TemplateLoader;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+
+/**
+ * FreeMarker TemplateLoader adapter that loads via a Spring ResourceLoader.
+ * Used by FreeMarkerConfigurationFactory for any resource loader path that
+ * cannot be resolved to a java.io.File.
+ *
+ * Note that this loader does not allow for modification detection:
+ * Use FreeMarker's default TemplateLoader for java.io.File resources.
+ *
+ * @author Juergen Hoeller
+ * @since 14.03.2004
+ * @see FreeMarkerConfigurationFactory#setTemplateLoaderPath
+ * @see freemarker.template.Configuration#setDirectoryForTemplateLoading
+ */
+public class SpringTemplateLoader implements TemplateLoader {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private final ResourceLoader resourceLoader;
+
+ private final String templateLoaderPath;
+
+
+ /**
+ * Create a new SpringTemplateLoader.
+ * @param resourceLoader the Spring ResourceLoader to use
+ * @param templateLoaderPath the template loader path to use
+ */
+ public SpringTemplateLoader(ResourceLoader resourceLoader, String templateLoaderPath) {
+ this.resourceLoader = resourceLoader;
+ if (!templateLoaderPath.endsWith("/")) {
+ templateLoaderPath += "/";
+ }
+ this.templateLoaderPath = templateLoaderPath;
+ if (logger.isInfoEnabled()) {
+ logger.info("SpringTemplateLoader for FreeMarker: using resource loader [" + this.resourceLoader +
+ "] and template loader path [" + this.templateLoaderPath + "]");
+ }
+ }
+
+ public Object findTemplateSource(String name) throws IOException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Looking for FreeMarker template with name [" + name + "]");
+ }
+ Resource resource = this.resourceLoader.getResource(this.templateLoaderPath + name);
+ return (resource.exists() ? resource : null);
+ }
+
+ public Reader getReader(Object templateSource, String encoding) throws IOException {
+ Resource resource = (Resource) templateSource;
+ try {
+ return new InputStreamReader(resource.getInputStream(), encoding);
+ }
+ catch (IOException ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not find FreeMarker template: " + resource);
+ }
+ throw ex;
+ }
+ }
+
+
+ public long getLastModified(Object templateSource) {
+ return -1;
+ }
+
+ public void closeTemplateSource(Object templateSource) throws IOException {
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/package.html b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/package.html
new file mode 100644
index 00000000000..156e4511d01
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/package.html
@@ -0,0 +1,9 @@
+
+ In the default implementation, a Make sure that the Make sure that the Note that this loader does not allow for modification detection:
+ * Use Velocity's default FileResourceLoader for Expects "spring.resource.loader" and "spring.resource.loader.path"
+ * application attributes in the Velocity runtime: the former of type
+ * The optional "configLocation" property sets the location of the Velocity
+ * properties file, within the current application. Velocity properties can be
+ * overridden via "velocityProperties", or even completely specified locally,
+ * avoiding the need for an external properties file.
+ *
+ * The "resourceLoaderPath" property can be used to specify the Velocity
+ * resource loader path via Spring's Resource abstraction, possibly relative
+ * to the Spring application context.
+ *
+ * If "overrideLogging" is true (the default), the VelocityEngine will be
+ * configured to log via Commons Logging, that is, using the Spring-provided
+ * {@link CommonsLoggingLogSystem} as log system.
+ *
+ * The simplest way to use this class is to specify a
+ * {@link #setResourceLoaderPath(String) "resourceLoaderPath"}; the
+ * VelocityEngine typically then does not need any further configuration.
+ *
+ * @author Juergen Hoeller
+ * @see #setConfigLocation
+ * @see #setVelocityProperties
+ * @see #setResourceLoaderPath
+ * @see #setOverrideLogging
+ * @see #createVelocityEngine
+ * @see CommonsLoggingLogSystem
+ * @see VelocityEngineFactoryBean
+ * @see org.springframework.web.servlet.view.velocity.VelocityConfigurer
+ * @see org.apache.velocity.app.VelocityEngine
+ */
+public class VelocityEngineFactory {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private Resource configLocation;
+
+ private final Map velocityProperties = new HashMap();
+
+ private String resourceLoaderPath;
+
+ private ResourceLoader resourceLoader = new DefaultResourceLoader();
+
+ private boolean preferFileSystemAccess = true;
+
+ private boolean overrideLogging = true;
+
+
+ /**
+ * Set the location of the Velocity config file.
+ * Alternatively, you can specify all properties locally.
+ * @see #setVelocityProperties
+ * @see #setResourceLoaderPath
+ */
+ public void setConfigLocation(Resource configLocation) {
+ this.configLocation = configLocation;
+ }
+
+ /**
+ * Set Velocity properties, like "file.resource.loader.path".
+ * Can be used to override values in a Velocity config file,
+ * or to specify all necessary properties locally.
+ * Note that the Velocity resource loader path also be set to any
+ * Spring resource location via the "resourceLoaderPath" property.
+ * Setting it here is just necessary when using a non-file-based
+ * resource loader.
+ * @see #setVelocityPropertiesMap
+ * @see #setConfigLocation
+ * @see #setResourceLoaderPath
+ */
+ public void setVelocityProperties(Properties velocityProperties) {
+ setVelocityPropertiesMap(velocityProperties);
+ }
+
+ /**
+ * Set Velocity properties as Map, to allow for non-String values
+ * like "ds.resource.loader.instance".
+ * @see #setVelocityProperties
+ */
+ public void setVelocityPropertiesMap(Map velocityPropertiesMap) {
+ if (velocityPropertiesMap != null) {
+ this.velocityProperties.putAll(velocityPropertiesMap);
+ }
+ }
+
+ /**
+ * Set the Velocity resource loader path via a Spring resource location.
+ * Accepts multiple locations in Velocity's comma-separated path style.
+ * When populated via a String, standard URLs like "file:" and "classpath:"
+ * pseudo URLs are supported, as understood by ResourceLoader. Allows for
+ * relative paths when running in an ApplicationContext.
+ * Will define a path for the default Velocity resource loader with the name
+ * "file". If the specified resource cannot be resolved to a Note that resource caching will be enabled in any case. With the file
+ * resource loader, the last-modified timestamp will be checked on access to
+ * detect changes. With SpringResourceLoader, the resource will be cached
+ * forever (for example for class path resources).
+ * To specify a modification check interval for files, use Velocity's
+ * standard "file.resource.loader.modificationCheckInterval" property. By default,
+ * the file timestamp is checked on every access (which is surprisingly fast).
+ * Of course, this just applies when loading resources from the file system.
+ * To enforce the use of SpringResourceLoader, i.e. to not resolve a path
+ * as file system resource in any case, turn off the "preferFileSystemAccess"
+ * flag. See the latter's javadoc for details.
+ * @see #setResourceLoader
+ * @see #setVelocityProperties
+ * @see #setPreferFileSystemAccess
+ * @see SpringResourceLoader
+ * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+ */
+ public void setResourceLoaderPath(String resourceLoaderPath) {
+ this.resourceLoaderPath = resourceLoaderPath;
+ }
+
+ /**
+ * Set the Spring ResourceLoader to use for loading Velocity template files.
+ * The default is DefaultResourceLoader. Will get overridden by the
+ * ApplicationContext if running in a context.
+ * @see org.springframework.core.io.DefaultResourceLoader
+ * @see org.springframework.context.ApplicationContext
+ */
+ public void setResourceLoader(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * Return the Spring ResourceLoader to use for loading Velocity template files.
+ */
+ protected ResourceLoader getResourceLoader() {
+ return this.resourceLoader;
+ }
+
+ /**
+ * Set whether to prefer file system access for template loading.
+ * File system access enables hot detection of template changes.
+ * If this is enabled, VelocityEngineFactory will try to resolve the
+ * specified "resourceLoaderPath" as file system resource (which will work
+ * for expanded class path resources and ServletContext resources too).
+ * Default is "true". Turn this off to always load via SpringResourceLoader
+ * (i.e. as stream, without hot detection of template changes), which might
+ * be necessary if some of your templates reside in an expanded classes
+ * directory while others reside in jar files.
+ * @see #setResourceLoaderPath
+ */
+ public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
+ this.preferFileSystemAccess = preferFileSystemAccess;
+ }
+
+ /**
+ * Return whether to prefer file system access for template loading.
+ */
+ protected boolean isPreferFileSystemAccess() {
+ return this.preferFileSystemAccess;
+ }
+
+ /**
+ * Set whether Velocity should log via Commons Logging, i.e. whether Velocity's
+ * log system should be set to CommonsLoggingLogSystem. Default value is true.
+ * @see CommonsLoggingLogSystem
+ */
+ public void setOverrideLogging(boolean overrideLogging) {
+ this.overrideLogging = overrideLogging;
+ }
+
+
+ /**
+ * Prepare the VelocityEngine instance and return it.
+ * @return the VelocityEngine instance
+ * @throws IOException if the config file wasn't found
+ * @throws VelocityException on Velocity initialization failure
+ */
+ public VelocityEngine createVelocityEngine() throws IOException, VelocityException {
+ VelocityEngine velocityEngine = newVelocityEngine();
+ Properties props = new Properties();
+
+ // Load config file if set.
+ if (this.configLocation != null) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Loading Velocity config from [" + this.configLocation + "]");
+ }
+ PropertiesLoaderUtils.fillProperties(props, this.configLocation);
+ }
+
+ // Merge local properties if set.
+ if (!this.velocityProperties.isEmpty()) {
+ props.putAll(this.velocityProperties);
+ }
+
+ // Set a resource loader path, if required.
+ if (this.resourceLoaderPath != null) {
+ initVelocityResourceLoader(velocityEngine, this.resourceLoaderPath);
+ }
+
+ // Log via Commons Logging?
+ if (this.overrideLogging) {
+ velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new CommonsLoggingLogSystem());
+ }
+
+ // Apply properties to VelocityEngine.
+ for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ if (!(entry.getKey() instanceof String)) {
+ throw new IllegalArgumentException(
+ "Illegal property key [" + entry.getKey() + "]: only Strings allowed");
+ }
+ velocityEngine.setProperty((String) entry.getKey(), entry.getValue());
+ }
+
+ postProcessVelocityEngine(velocityEngine);
+
+ try {
+ // Perform actual initialization.
+ velocityEngine.init();
+ }
+ catch (IOException ex) {
+ throw ex;
+ }
+ catch (VelocityException ex) {
+ throw ex;
+ }
+ catch (RuntimeException ex) {
+ throw ex;
+ }
+ catch (Exception ex) {
+ logger.error("Why does VelocityEngine throw a generic checked exception, after all?", ex);
+ throw new VelocityException(ex.toString());
+ }
+
+ return velocityEngine;
+ }
+
+ /**
+ * Return a new VelocityEngine. Subclasses can override this for
+ * custom initialization, or for using a mock object for testing.
+ * Called by Called by Called by Called by The simplest way to use this class is to specify a "resourceLoaderPath";
+ * you do not need any further configuration then. For example, in a web
+ * application context:
+ *
+ * When using this method to prepare a text for a mail to be sent with Spring's
+ * mail support, consider wrapping VelocityException in MailPreparationException.
+ * @param velocityEngine VelocityEngine to work with
+ * @param templateLocation the location of template, relative to Velocity's
+ * resource loader path
+ * @param model the Map that contains model names as keys and model objects
+ * as values
+ * @return the result as String
+ * @throws VelocityException if the template wasn't found or rendering failed
+ * @see org.springframework.mail.MailPreparationException
+ */
+ public static String mergeTemplateIntoString(
+ VelocityEngine velocityEngine, String templateLocation, Map model)
+ throws VelocityException {
+
+ StringWriter result = new StringWriter();
+ mergeTemplate(velocityEngine, templateLocation, model, result);
+ return result.toString();
+ }
+
+ /**
+ * Merge the specified Velocity template with the given model into a String.
+ * When using this method to prepare a text for a mail to be sent with Spring's
+ * mail support, consider wrapping VelocityException in MailPreparationException.
+ * @param velocityEngine VelocityEngine to work with
+ * @param templateLocation the location of template, relative to Velocity's
+ * resource loader path
+ * @param encoding the encoding of the template file
+ * @param model the Map that contains model names as keys and model objects
+ * as values
+ * @return the result as String
+ * @throws VelocityException if the template wasn't found or rendering failed
+ * @see org.springframework.mail.MailPreparationException
+ */
+ public static String mergeTemplateIntoString(
+ VelocityEngine velocityEngine, String templateLocation, String encoding, Map model)
+ throws VelocityException {
+
+ StringWriter result = new StringWriter();
+ mergeTemplate(velocityEngine, templateLocation, encoding, model, result);
+ return result.toString();
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/package.html b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/package.html
new file mode 100644
index 00000000000..9bd3dc90a79
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/package.html
@@ -0,0 +1,9 @@
+
+
+The Spring Data Binding framework, an internal library used by Spring Web Flow.
+mappings bean property,
+ * as lines that follow the mime.types file format.
+ *
+ * @author Rob Harrop
+ * @author Juergen Hoeller
+ * @since 1.2
+ * @see #setMappingLocation
+ * @see #setMappings
+ * @see javax.activation.MimetypesFileTypeMap
+ */
+public class ConfigurableMimeFileTypeMap extends FileTypeMap implements InitializingBean {
+
+ /**
+ * The Resource to load the mapping file from.
+ */
+ private Resource mappingLocation = new ClassPathResource("mime.types", getClass());
+
+ /**
+ * Used to configure additional mappings.
+ */
+ private String[] mappings;
+
+ /**
+ * The delegate FileTypeMap, compiled from the mappings in the mapping file
+ * and the entries in the mappings property.
+ */
+ private FileTypeMap fileTypeMap;
+
+
+ /**
+ * Specify the Resource from which mappings are loaded.
+ * mime.types file format, as specified
+ * by the Java Activation Framework, containing lines such as:
+ * text/html html htm HTML HTM
+ */
+ public void setMappingLocation(Resource mappingLocation) {
+ this.mappingLocation = mappingLocation;
+ }
+
+ /**
+ * Specify additional MIME type mappings as lines that follow the
+ * mime.types file format, as specified by the
+ * Java Activation Framework, for example:
+ * text/html html htm HTML HTM
+ */
+ public void setMappings(String[] mappings) {
+ this.mappings = mappings;
+ }
+
+
+ /**
+ * Creates the final merged mapping set.
+ */
+ public void afterPropertiesSet() {
+ getFileTypeMap();
+ }
+
+ /**
+ * Return the delegate FileTypeMap, compiled from the mappings in the mapping file
+ * and the entries in the mappings property.
+ * @see #setMappingLocation
+ * @see #setMappings
+ * @see #createFileTypeMap
+ */
+ protected final FileTypeMap getFileTypeMap() {
+ if (this.fileTypeMap == null) {
+ try {
+ this.fileTypeMap = createFileTypeMap(this.mappingLocation, this.mappings);
+ }
+ catch (IOException ex) {
+ IllegalStateException ise = new IllegalStateException(
+ "Could not load specified MIME type mapping file: " + this.mappingLocation);
+ ise.initCause(ex);
+ throw ise;
+ }
+ }
+ return this.fileTypeMap;
+ }
+
+ /**
+ * Compile a {@link FileTypeMap} from the mappings in the given mapping file
+ * and the given mapping entries.
+ * mime.types mapping resource (can be null)
+ * @param mappings MIME type mapping lines (can be null)
+ * @return the compiled FileTypeMap
+ * @throws IOException if resource access failed
+ * @see javax.activation.MimetypesFileTypeMap#MimetypesFileTypeMap(java.io.InputStream)
+ * @see javax.activation.MimetypesFileTypeMap#addMimeTypes(String)
+ */
+ protected FileTypeMap createFileTypeMap(Resource mappingLocation, String[] mappings) throws IOException {
+ MimetypesFileTypeMap fileTypeMap = null;
+ if (mappingLocation != null) {
+ InputStream is = mappingLocation.getInputStream();
+ try {
+ fileTypeMap = new MimetypesFileTypeMap(is);
+ }
+ finally {
+ is.close();
+ }
+ }
+ else {
+ fileTypeMap = new MimetypesFileTypeMap();
+ }
+ if (mappings != null) {
+ for (int i = 0; i < mappings.length; i++) {
+ fileTypeMap.addMimeTypes(mappings[i]);
+ }
+ }
+ return fileTypeMap;
+ }
+
+
+ /**
+ * Delegates to the underlying FileTypeMap.
+ * @see #getFileTypeMap()
+ */
+ public String getContentType(File file) {
+ return getFileTypeMap().getContentType(file);
+ }
+
+ /**
+ * Delegates to the underlying FileTypeMap.
+ * @see #getFileTypeMap()
+ */
+ public String getContentType(String fileName) {
+ return getFileTypeMap().getContentType(fileName);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java
new file mode 100644
index 00000000000..7dfada99a08
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import java.beans.PropertyEditorSupport;
+
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Editor for java.mail.internet.InternetAddress,
+ * to directly populate an InternetAddress property.
+ *
+ * Session.getInstance(new Properties()) call, and check the passed-in
+ * messages in your mock implementations of the various send methods.
+ *
+ * @author Juergen Hoeller
+ * @since 07.10.2003
+ * @see javax.mail.internet.MimeMessage
+ * @see javax.mail.Session
+ * @see JavaMailSenderImpl
+ * @see MimeMessagePreparator
+ * @see MimeMessageHelper
+ */
+public interface JavaMailSender extends MailSender {
+
+ /**
+ * Create a new JavaMail MimeMessage for the underlying JavaMail Session
+ * of this sender. Needs to be called to create MimeMessage instances
+ * that can be prepared by the client and passed to send(MimeMessage).
+ * @return the new MimeMessage instance
+ * @see #send(MimeMessage)
+ * @see #send(MimeMessage[])
+ */
+ MimeMessage createMimeMessage();
+
+ /**
+ * Create a new JavaMail MimeMessage for the underlying JavaMail Session
+ * of this sender, using the given input stream as the message source.
+ * @param contentStream the raw MIME input stream for the message
+ * @return the new MimeMessage instance
+ * @throws org.springframework.mail.MailParseException
+ * in case of message creation failure
+ */
+ MimeMessage createMimeMessage(InputStream contentStream) throws MailException;
+
+ /**
+ * Send the given JavaMail MIME message.
+ * The message needs to have been created with {@link #createMimeMessage()}.
+ * @param mimeMessage message to send
+ * @throws org.springframework.mail.MailAuthenticationException
+ * in case of authentication failure
+ * @throws org.springframework.mail.MailSendException
+ * in case of failure when sending the message
+ * @see #createMimeMessage
+ */
+ void send(MimeMessage mimeMessage) throws MailException;
+
+ /**
+ * Send the given array of JavaMail MIME messages in batch.
+ * The messages need to have been created with {@link #createMimeMessage()}.
+ * @param mimeMessages messages to send
+ * @throws org.springframework.mail.MailAuthenticationException
+ * in case of authentication failure
+ * @throws org.springframework.mail.MailSendException
+ * in case of failure when sending a message
+ * @see #createMimeMessage
+ */
+ void send(MimeMessage[] mimeMessages) throws MailException;
+
+ /**
+ * Send the JavaMail MIME message prepared by the given MimeMessagePreparator.
+ * Session. Note that if overriding all values locally,
+ * there is no added value in setting a pre-configured Session.
+ *
+ * @author Dmitriy Kopylenko
+ * @author Juergen Hoeller
+ * @since 10.09.2003
+ * @see javax.mail.internet.MimeMessage
+ * @see javax.mail.Session
+ * @see #setSession
+ * @see #setJavaMailProperties
+ * @see #setHost
+ * @see #setPort
+ * @see #setUsername
+ * @see #setPassword
+ */
+public class JavaMailSenderImpl implements JavaMailSender {
+
+ /** The default protocol: 'smtp' */
+ public static final String DEFAULT_PROTOCOL = "smtp";
+
+ /** The default port: -1 */
+ public static final int DEFAULT_PORT = -1;
+
+ private static final String HEADER_MESSAGE_ID = "Message-ID";
+
+
+ private Properties javaMailProperties = new Properties();
+
+ private Session session;
+
+ private String protocol = DEFAULT_PROTOCOL;
+
+ private String host;
+
+ private int port = DEFAULT_PORT;
+
+ private String username;
+
+ private String password;
+
+ private String defaultEncoding;
+
+ private FileTypeMap defaultFileTypeMap;
+
+
+ /**
+ * Create a new instance of the JavaMailSenderImpl class.
+ * Session.
+ * Session will be created with those properties.
+ * Use either this method or {@link #setSession}, but not both.
+ * Session, possibly pulled from JNDI.
+ * Session without defaults, that is
+ * completely configured via this instance's properties.
+ * Session, non-default properties
+ * in this instance will override the settings in the Session.
+ * @see #setJavaMailProperties
+ */
+ public synchronized void setSession(Session session) {
+ Assert.notNull(session, "Session must not be null");
+ this.session = session;
+ }
+
+ /**
+ * Return the JavaMail Session,
+ * lazily initializing it if hasn't been specified explicitly.
+ */
+ public synchronized Session getSession() {
+ if (this.session == null) {
+ this.session = Session.getInstance(this.javaMailProperties);
+ }
+ return this.session;
+ }
+
+ /**
+ * Set the mail protocol. Default is "smtp".
+ */
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ /**
+ * Return the mail protocol.
+ */
+ public String getProtocol() {
+ return this.protocol;
+ }
+
+ /**
+ * Set the mail server host, typically an SMTP host.
+ * Session has to be
+ * configured with the property "mail.smtp.auth" set to
+ * true, else the specified username will not be sent to the
+ * mail server by the JavaMail runtime. If you are not explicitly passing
+ * in a Session to use, simply specify this setting via
+ * {@link #setJavaMailProperties}.
+ * @see #setSession
+ * @see #setPassword
+ */
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ /**
+ * Return the username for the account at the mail host.
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Set the password for the account at the mail host, if any.
+ * Session has to be
+ * configured with the property "mail.smtp.auth" set to
+ * true, else the specified password will not be sent to the
+ * mail server by the JavaMail runtime. If you are not explicitly passing
+ * in a Session to use, simply specify this setting via
+ * {@link #setJavaMailProperties}.
+ * @see #setSession
+ * @see #setUsername
+ */
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ /**
+ * Return the password for the account at the mail host.
+ */
+ public String getPassword() {
+ return this.password;
+ }
+
+ /**
+ * Set the default encoding to use for {@link MimeMessage MimeMessages}
+ * created by this instance.
+ * null if none.
+ */
+ public String getDefaultEncoding() {
+ return this.defaultEncoding;
+ }
+
+ /**
+ * Set the default Java Activation {@link FileTypeMap} to use for
+ * {@link MimeMessage MimeMessages} created by this instance.
+ * FileTypeMap specified here will be autodetected by
+ * {@link MimeMessageHelper}, avoiding the need to specify the
+ * FileTypeMap for each MimeMessageHelper instance.
+ * ConfigurableMimeFileTypeMap will be used, containing
+ * an extended set of MIME type mappings (as defined by the
+ * mime.types file contained in the Spring jar).
+ * @see MimeMessageHelper#setFileTypeMap
+ */
+ public void setDefaultFileTypeMap(FileTypeMap defaultFileTypeMap) {
+ this.defaultFileTypeMap = defaultFileTypeMap;
+ }
+
+ /**
+ * Return the default Java Activation {@link FileTypeMap} for
+ * {@link MimeMessage MimeMessages}, or null if none.
+ */
+ public FileTypeMap getDefaultFileTypeMap() {
+ return this.defaultFileTypeMap;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of MailSender
+ //---------------------------------------------------------------------
+
+ public void send(SimpleMailMessage simpleMessage) throws MailException {
+ send(new SimpleMailMessage[] { simpleMessage });
+ }
+
+ public void send(SimpleMailMessage[] simpleMessages) throws MailException {
+ List mimeMessages = new ArrayList(simpleMessages.length);
+ for (int i = 0; i < simpleMessages.length; i++) {
+ SimpleMailMessage simpleMessage = simpleMessages[i];
+ MimeMailMessage message = new MimeMailMessage(createMimeMessage());
+ simpleMessage.copyTo(message);
+ mimeMessages.add(message.getMimeMessage());
+ }
+ doSend((MimeMessage[]) mimeMessages.toArray(new MimeMessage[mimeMessages.size()]), simpleMessages);
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of JavaMailSender
+ //---------------------------------------------------------------------
+
+ /**
+ * This implementation creates a SmartMimeMessage, holding the specified
+ * default encoding and default FileTypeMap. This special defaults-carrying
+ * message will be autodetected by {@link MimeMessageHelper}, which will use
+ * the carried encoding and FileTypeMap unless explicitly overridden.
+ * @see #setDefaultEncoding
+ * @see #setDefaultFileTypeMap
+ */
+ public MimeMessage createMimeMessage() {
+ return new SmartMimeMessage(getSession(), getDefaultEncoding(), getDefaultFileTypeMap());
+ }
+
+ public MimeMessage createMimeMessage(InputStream contentStream) throws MailException {
+ try {
+ return new MimeMessage(getSession(), contentStream);
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException("Could not parse raw MIME content", ex);
+ }
+ }
+
+ public void send(MimeMessage mimeMessage) throws MailException {
+ send(new MimeMessage[] { mimeMessage });
+ }
+
+ public void send(MimeMessage[] mimeMessages) throws MailException {
+ doSend(mimeMessages, null);
+ }
+
+ public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
+ send(new MimeMessagePreparator[] { mimeMessagePreparator });
+ }
+
+ public void send(MimeMessagePreparator[] mimeMessagePreparators) throws MailException {
+ try {
+ List mimeMessages = new ArrayList(mimeMessagePreparators.length);
+ for (int i = 0; i < mimeMessagePreparators.length; i++) {
+ MimeMessage mimeMessage = createMimeMessage();
+ mimeMessagePreparators[i].prepare(mimeMessage);
+ mimeMessages.add(mimeMessage);
+ }
+ send((MimeMessage[]) mimeMessages.toArray(new MimeMessage[mimeMessages.size()]));
+ }
+ catch (MailException ex) {
+ throw ex;
+ }
+ catch (MessagingException ex) {
+ throw new MailParseException(ex);
+ }
+ catch (IOException ex) {
+ throw new MailPreparationException(ex);
+ }
+ catch (Exception ex) {
+ throw new MailPreparationException(ex);
+ }
+ }
+
+
+ /**
+ * Actually send the given array of MimeMessages via JavaMail.
+ * @param mimeMessages MimeMessage objects to send
+ * @param originalMessages corresponding original message objects
+ * that the MimeMessages have been created from (with same array
+ * length and indices as the "mimeMessages" array), if any
+ * @throws org.springframework.mail.MailAuthenticationException
+ * in case of authentication failure
+ * @throws org.springframework.mail.MailSendException
+ * in case of failure when sending a message
+ */
+ protected void doSend(MimeMessage[] mimeMessages, Object[] originalMessages) throws MailException {
+ Map failedMessages = new LinkedHashMap();
+ try {
+ Transport transport = getTransport(getSession());
+ transport.connect(getHost(), getPort(), getUsername(), getPassword());
+ try {
+ for (int i = 0; i < mimeMessages.length; i++) {
+ MimeMessage mimeMessage = mimeMessages[i];
+ try {
+ if (mimeMessage.getSentDate() == null) {
+ mimeMessage.setSentDate(new Date());
+ }
+ String messageId = mimeMessage.getMessageID();
+ mimeMessage.saveChanges();
+ if (messageId != null) {
+ // Preserve explicitly specified message id...
+ mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
+ }
+ transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
+ }
+ catch (MessagingException ex) {
+ Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
+ failedMessages.put(original, ex);
+ }
+ }
+ }
+ finally {
+ transport.close();
+ }
+ }
+ catch (AuthenticationFailedException ex) {
+ throw new MailAuthenticationException(ex);
+ }
+ catch (MessagingException ex) {
+ throw new MailSendException("Mail server connection failed", ex);
+ }
+ if (!failedMessages.isEmpty()) {
+ throw new MailSendException(failedMessages);
+ }
+ }
+
+ /**
+ * Obtain a Transport object from the given JavaMail Session,
+ * using the configured protocol.
+ *
+ * mailSender.send(new MimeMessagePreparator() {
+ * public void prepare(MimeMessage mimeMessage) throws MessagingException {
+ * MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
+ * message.setFrom("me@mail.com");
+ * message.setTo("you@mail.com");
+ * message.setSubject("my subject");
+ * message.setText("my text <img src='cid:myLogo'>", true);
+ * message.addInline("myLogo", new ClassPathResource("img/mylogo.gif"));
+ * message.addAttachment("myDocument.pdf", new ClassPathResource("doc/myDocument.pdf"));
+ * }
+ * });
+ *
+ * Consider using {@link MimeMailMessage} (which implements the common
+ * {@link org.springframework.mail.MailMessage} interface, just like
+ * {@link org.springframework.mail.SimpleMailMessage}) on top of this helper,
+ * in order to let message population code interact with a simple message
+ * or a MIME message through a common interface.
+ *
+ * null to indicate no multipart at all
+ * @param main the main MimeMultipart object, which text(s) and inline elements
+ * will be added to (can be the same as the root multipart object, or an element
+ * nested underneath the root multipart element)
+ */
+ protected final void setMimeMultiparts(MimeMultipart root, MimeMultipart main) {
+ this.rootMimeMultipart = root;
+ this.mimeMultipart = main;
+ }
+
+ /**
+ * Return whether this helper is in multipart mode,
+ * i.e. whether it holds a multipart message.
+ * @see #MimeMessageHelper(MimeMessage, boolean)
+ */
+ public final boolean isMultipart() {
+ return (this.rootMimeMultipart != null);
+ }
+
+ /**
+ * Throw an IllegalStateException if this helper is not in multipart mode.
+ */
+ private void checkMultipart() throws IllegalStateException {
+ if (!isMultipart()) {
+ throw new IllegalStateException("Not in multipart mode - " +
+ "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " +
+ "if you need to set alternative texts or add inline elements or attachments.");
+ }
+ }
+
+ /**
+ * Return the root MIME "multipart/mixed" object, if any.
+ * Can be used to manually add attachments.
+ * null if none found
+ */
+ protected String getDefaultEncoding(MimeMessage mimeMessage) {
+ if (mimeMessage instanceof SmartMimeMessage) {
+ return ((SmartMimeMessage) mimeMessage).getDefaultEncoding();
+ }
+ return null;
+ }
+
+ /**
+ * Return the specific character encoding used for this message, if any.
+ */
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ /**
+ * Determine the default Java Activation FileTypeMap for the given MimeMessage.
+ * @param mimeMessage the passed-in MimeMessage
+ * @return the default FileTypeMap associated with the MimeMessage,
+ * or a default ConfigurableMimeFileTypeMap if none found for the message
+ * @see ConfigurableMimeFileTypeMap
+ */
+ protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) {
+ if (mimeMessage instanceof SmartMimeMessage) {
+ FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap();
+ if (fileTypeMap != null) {
+ return fileTypeMap;
+ }
+ }
+ ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap();
+ fileTypeMap.afterPropertiesSet();
+ return fileTypeMap;
+ }
+
+ /**
+ * Set the Java Activation Framework FileTypeMap to use
+ * for determining the content type of inline content and attachments
+ * that get added to the message.
+ * FileTypeMap that the underlying
+ * MimeMessage carries, if any, or the Activation Framework's default
+ * FileTypeMap instance else.
+ * @see #addInline
+ * @see #addAttachment
+ * @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage)
+ * @see JavaMailSenderImpl#setDefaultFileTypeMap
+ * @see javax.activation.FileTypeMap#getDefaultFileTypeMap
+ * @see ConfigurableMimeFileTypeMap
+ */
+ public void setFileTypeMap(FileTypeMap fileTypeMap) {
+ this.fileTypeMap = (fileTypeMap != null ? fileTypeMap : getDefaultFileTypeMap(getMimeMessage()));
+ }
+
+ /**
+ * Return the FileTypeMap used by this MimeMessageHelper.
+ */
+ public FileTypeMap getFileTypeMap() {
+ return this.fileTypeMap;
+ }
+
+
+ /**
+ * Set whether to validate all addresses which get passed to this helper.
+ * Default is "false".
+ * validateAddress method for
+ * validation on older JavaMail versions (or for custom validation).
+ * @see #validateAddress
+ */
+ public void setValidateAddresses(boolean validateAddresses) {
+ this.validateAddresses = validateAddresses;
+ }
+
+ /**
+ * Return whether this helper will validate all addresses passed to it.
+ */
+ public boolean isValidateAddresses() {
+ return this.validateAddresses;
+ }
+
+ /**
+ * Validate the given mail address.
+ * Called by all of MimeMessageHelper's address setters and adders.
+ * InternetAddress.validate(),
+ * provided that address validation is activated for the helper instance.
+ * null)
+ * @throws MessagingException in case of errors
+ */
+ public void setSentDate(Date sentDate) throws MessagingException {
+ Assert.notNull(sentDate, "Sent date must not be null");
+ this.mimeMessage.setSentDate(sentDate);
+ }
+
+ /**
+ * Set the subject of the message, using the correct encoding.
+ * @param subject the subject text
+ * @throws MessagingException in case of errors
+ */
+ public void setSubject(String subject) throws MessagingException {
+ Assert.notNull(subject, "Subject must not be null");
+ if (getEncoding() != null) {
+ this.mimeMessage.setSubject(subject, getEncoding());
+ }
+ else {
+ this.mimeMessage.setSubject(subject);
+ }
+ }
+
+
+ /**
+ * Set the given text directly as content in non-multipart mode
+ * or as default body part in multipart mode.
+ * Always applies the default content type "text/plain".
+ * setText;
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param text the text for the message
+ * @throws MessagingException in case of errors
+ */
+ public void setText(String text) throws MessagingException {
+ setText(text, false);
+ }
+
+ /**
+ * Set the given text directly as content in non-multipart mode
+ * or as default body part in multipart mode.
+ * The "html" flag determines the content type to apply.
+ * setText;
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param text the text for the message
+ * @param html whether to apply content type "text/html" for an
+ * HTML mail, using default content type ("text/plain") else
+ * @throws MessagingException in case of errors
+ */
+ public void setText(String text, boolean html) throws MessagingException {
+ Assert.notNull(text, "Text must not be null");
+ MimePart partToUse = null;
+ if (isMultipart()) {
+ partToUse = getMainPart();
+ }
+ else {
+ partToUse = this.mimeMessage;
+ }
+ if (html) {
+ setHtmlTextToMimePart(partToUse, text);
+ }
+ else {
+ setPlainTextToMimePart(partToUse, text);
+ }
+ }
+
+ /**
+ * Set the given plain text and HTML text as alternatives, offering
+ * both options to the email client. Requires multipart mode.
+ * setText;
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param plainText the plain text for the message
+ * @param htmlText the HTML text for the message
+ * @throws MessagingException in case of errors
+ */
+ public void setText(String plainText, String htmlText) throws MessagingException {
+ Assert.notNull(plainText, "Plain text must not be null");
+ Assert.notNull(htmlText, "HTML text must not be null");
+
+ MimeMultipart messageBody = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE);
+ getMainPart().setContent(messageBody, CONTENT_TYPE_ALTERNATIVE);
+
+ // Create the plain text part of the message.
+ MimeBodyPart plainTextPart = new MimeBodyPart();
+ setPlainTextToMimePart(plainTextPart, plainText);
+ messageBody.addBodyPart(plainTextPart);
+
+ // Create the HTML text part of the message.
+ MimeBodyPart htmlTextPart = new MimeBodyPart();
+ setHtmlTextToMimePart(htmlTextPart, htmlText);
+ messageBody.addBodyPart(htmlTextPart);
+ }
+
+ private MimeBodyPart getMainPart() throws MessagingException {
+ MimeMultipart mimeMultipart = getMimeMultipart();
+ MimeBodyPart bodyPart = null;
+ for (int i = 0; i < mimeMultipart.getCount(); i++) {
+ BodyPart bp = mimeMultipart.getBodyPart(i);
+ if (bp.getFileName() == null) {
+ bodyPart = (MimeBodyPart) bp;
+ }
+ }
+ if (bodyPart == null) {
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeMultipart.addBodyPart(mimeBodyPart);
+ bodyPart = mimeBodyPart;
+ }
+ return bodyPart;
+ }
+
+ private void setPlainTextToMimePart(MimePart mimePart, String text) throws MessagingException {
+ if (getEncoding() != null) {
+ mimePart.setText(text, getEncoding());
+ }
+ else {
+ mimePart.setText(text);
+ }
+ }
+
+ private void setHtmlTextToMimePart(MimePart mimePart, String text) throws MessagingException {
+ if (getEncoding() != null) {
+ mimePart.setContent(text, CONTENT_TYPE_HTML + CONTENT_TYPE_CHARSET_SUFFIX + getEncoding());
+ }
+ else {
+ mimePart.setContent(text, CONTENT_TYPE_HTML);
+ }
+ }
+
+
+ /**
+ * Add an inline element to the MimeMessage, taking the content from a
+ * javax.activation.DataSource.
+ * getInputStream() multiple times.
+ * addInline after {@link #setText};
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param contentId the content ID to use. Will end up as "Content-ID" header
+ * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>".
+ * Can be referenced in HTML source via src="cid:myId" expressions.
+ * @param dataSource the javax.activation.DataSource to take
+ * the content from, determining the InputStream and the content type
+ * @throws MessagingException in case of errors
+ * @see #addInline(String, java.io.File)
+ * @see #addInline(String, org.springframework.core.io.Resource)
+ */
+ public void addInline(String contentId, DataSource dataSource) throws MessagingException {
+ Assert.notNull(contentId, "Content ID must not be null");
+ Assert.notNull(dataSource, "DataSource must not be null");
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeBodyPart.setDisposition(MimeBodyPart.INLINE);
+ // We're using setHeader here to remain compatible with JavaMail 1.2,
+ // rather than JavaMail 1.3's setContentID.
+ mimeBodyPart.setHeader(HEADER_CONTENT_ID, "<" + contentId + ">");
+ mimeBodyPart.setDataHandler(new DataHandler(dataSource));
+ getMimeMultipart().addBodyPart(mimeBodyPart);
+ }
+
+ /**
+ * Add an inline element to the MimeMessage, taking the content from a
+ * java.io.File.
+ * addInline after {@link #setText};
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param contentId the content ID to use. Will end up as "Content-ID" header
+ * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>".
+ * Can be referenced in HTML source via src="cid:myId" expressions.
+ * @param file the File resource to take the content from
+ * @throws MessagingException in case of errors
+ * @see #setText
+ * @see #addInline(String, org.springframework.core.io.Resource)
+ * @see #addInline(String, javax.activation.DataSource)
+ */
+ public void addInline(String contentId, File file) throws MessagingException {
+ Assert.notNull(file, "File must not be null");
+ FileDataSource dataSource = new FileDataSource(file);
+ dataSource.setFileTypeMap(getFileTypeMap());
+ addInline(contentId, dataSource);
+ }
+
+ /**
+ * Add an inline element to the MimeMessage, taking the content from a
+ * org.springframework.core.io.Resource.
+ * getInputStream() multiple times.
+ * addInline after {@link #setText};
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param contentId the content ID to use. Will end up as "Content-ID" header
+ * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>".
+ * Can be referenced in HTML source via src="cid:myId" expressions.
+ * @param resource the resource to take the content from
+ * @throws MessagingException in case of errors
+ * @see #setText
+ * @see #addInline(String, java.io.File)
+ * @see #addInline(String, javax.activation.DataSource)
+ */
+ public void addInline(String contentId, Resource resource) throws MessagingException {
+ Assert.notNull(resource, "Resource must not be null");
+ String contentType = getFileTypeMap().getContentType(resource.getFilename());
+ addInline(contentId, resource, contentType);
+ }
+
+ /**
+ * Add an inline element to the MimeMessage, taking the content from an
+ * org.springframework.core.InputStreamResource, and
+ * specifying the content type explicitly.
+ * getInputStream() multiple times.
+ * addInline after setText;
+ * else, mail readers might not be able to resolve inline references correctly.
+ * @param contentId the content ID to use. Will end up as "Content-ID" header
+ * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>".
+ * Can be referenced in HTML source via src="cid:myId" expressions.
+ * @param inputStreamSource the resource to take the content from
+ * @param contentType the content type to use for the element
+ * @throws MessagingException in case of errors
+ * @see #setText
+ * @see #getFileTypeMap
+ * @see #addInline(String, org.springframework.core.io.Resource)
+ * @see #addInline(String, javax.activation.DataSource)
+ */
+ public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType)
+ throws MessagingException {
+
+ Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
+ if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) {
+ throw new IllegalArgumentException(
+ "Passed-in Resource contains an open stream: invalid argument. " +
+ "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
+ }
+ DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline");
+ addInline(contentId, dataSource);
+ }
+
+ /**
+ * Add an attachment to the MimeMessage, taking the content from a
+ * javax.activation.DataSource.
+ * getInputStream() multiple times.
+ * @param attachmentFilename the name of the attachment as it will
+ * appear in the mail (the content type will be determined by this)
+ * @param dataSource the javax.activation.DataSource to take
+ * the content from, determining the InputStream and the content type
+ * @throws MessagingException in case of errors
+ * @see #addAttachment(String, org.springframework.core.io.InputStreamSource)
+ * @see #addAttachment(String, java.io.File)
+ */
+ public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException {
+ Assert.notNull(attachmentFilename, "Attachment filename must not be null");
+ Assert.notNull(dataSource, "DataSource must not be null");
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT);
+ mimeBodyPart.setFileName(attachmentFilename);
+ mimeBodyPart.setDataHandler(new DataHandler(dataSource));
+ getRootMimeMultipart().addBodyPart(mimeBodyPart);
+ }
+
+ /**
+ * Add an attachment to the MimeMessage, taking the content from a
+ * java.io.File.
+ * org.springframework.core.io.InputStreamResource.
+ * getInputStream() multiple times.
+ * @param attachmentFilename the name of the attachment as it will
+ * appear in the mail
+ * @param inputStreamSource the resource to take the content from
+ * (all of Spring's Resource implementations can be passed in here)
+ * @throws MessagingException in case of errors
+ * @see #addAttachment(String, java.io.File)
+ * @see #addAttachment(String, javax.activation.DataSource)
+ * @see org.springframework.core.io.Resource
+ */
+ public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource)
+ throws MessagingException {
+
+ String contentType = getFileTypeMap().getContentType(attachmentFilename);
+ addAttachment(attachmentFilename, inputStreamSource, contentType);
+ }
+
+ /**
+ * Add an attachment to the MimeMessage, taking the content from an
+ * org.springframework.core.io.InputStreamResource.
+ * getInputStream() multiple times.
+ * @param attachmentFilename the name of the attachment as it will
+ * appear in the mail
+ * @param inputStreamSource the resource to take the content from
+ * (all of Spring's Resource implementations can be passed in here)
+ * @param contentType the content type to use for the element
+ * @throws MessagingException in case of errors
+ * @see #addAttachment(String, java.io.File)
+ * @see #addAttachment(String, javax.activation.DataSource)
+ * @see org.springframework.core.io.Resource
+ */
+ public void addAttachment(
+ String attachmentFilename, InputStreamSource inputStreamSource, String contentType)
+ throws MessagingException {
+
+ Assert.notNull(inputStreamSource, "InputStreamSource must not be null");
+ if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) {
+ throw new IllegalArgumentException(
+ "Passed-in Resource contains an open stream: invalid argument. " +
+ "JavaMail requires an InputStreamSource that creates a fresh stream for every call.");
+ }
+ DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename);
+ addAttachment(attachmentFilename, dataSource);
+ }
+
+ /**
+ * Create an Activation Framework DataSource for the given InputStreamSource.
+ * @param inputStreamSource the InputStreamSource (typically a Spring Resource)
+ * @param contentType the content type
+ * @param name the name of the DataSource
+ * @return the Activation Framework DataSource
+ */
+ protected DataSource createDataSource(
+ final InputStreamSource inputStreamSource, final String contentType, final String name) {
+
+ return new DataSource() {
+ public InputStream getInputStream() throws IOException {
+ return inputStreamSource.getInputStream();
+ }
+ public OutputStream getOutputStream() {
+ throw new UnsupportedOperationException("Read-only javax.activation.DataSource");
+ }
+ public String getContentType() {
+ return contentType;
+ }
+ public String getName() {
+ return name;
+ }
+ };
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java
new file mode 100644
index 00000000000..8ac0e8be2ea
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.mail.javamail;
+
+import javax.mail.internet.MimeMessage;
+
+/**
+ * Callback interface for the preparation of JavaMail MIME messages.
+ *
+ * send methods of {@link JavaMailSender}
+ * will take care of the actual creation of a {@link MimeMessage} instance,
+ * and of proper exception conversion.
+ *
+ * null if none
+ * @param defaultFileTypeMap the default FileTypeMap, or null if none
+ */
+ public SmartMimeMessage(Session session, String defaultEncoding, FileTypeMap defaultFileTypeMap) {
+ super(session);
+ this.defaultEncoding = defaultEncoding;
+ this.defaultFileTypeMap = defaultFileTypeMap;
+ }
+
+
+ /**
+ * Return the default encoding of this message, or null if none.
+ */
+ public final String getDefaultEncoding() {
+ return this.defaultEncoding;
+ }
+
+ /**
+ * Return the default FileTypeMap of this message, or null if none.
+ */
+ public final FileTypeMap getDefaultFileTypeMap() {
+ return this.defaultFileTypeMap;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/mime.types b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/mime.types
new file mode 100644
index 00000000000..182357ea73f
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/mime.types
@@ -0,0 +1,306 @@
+################################################################################
+#
+# Defaults for the Java Activation Framework
+# Additional extensions registered in this file:
+# text/plain java c c++ pl cc h
+#
+################################################################################
+
+text/html html htm HTML HTM
+text/plain txt text TXT TEXT java c c++ pl cc h
+image/gif gif GIF
+image/ief ief
+image/jpeg jpeg jpg jpe JPG
+image/tiff tiff tif
+image/x-xwindowdump xwd
+application/postscript ai eps ps
+application/rtf rtf
+application/x-tex tex
+application/x-texinfo texinfo texi
+application/x-troff t tr roff
+audio/basic au
+audio/midi midi mid
+audio/x-aifc aifc
+audio/x-aiff aif aiff
+audio/x-mpeg mpeg mpg
+audio/x-wav wav
+video/mpeg mpeg mpg mpe
+video/quicktime qt mov
+video/x-msvideo avi
+
+################################################################################
+#
+# Additional file types adapted from
+# http://www.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html
+#
+################################################################################
+
+# TEXT TYPES
+
+text/x-speech talk
+text/css css
+text/csv csv
+
+# IMAGE TYPES
+
+# X-Windows bitmap (b/w)
+image/x-xbitmap xbm
+# X-Windows pixelmap (8-bit color)
+image/x-xpixmap xpm
+# Portable Network Graphics
+image/x-png png
+# Image Exchange Format (RFC 1314)
+image/ief ief
+# JPEG
+image/jpeg jpeg jpg jpe
+# RGB
+image/rgb rgb
+# Group III Fax (RFC 1494)
+image/g3fax g3f
+# X Windowdump format
+image/x-xwindowdump xwd
+# Macintosh PICT format
+image/x-pict pict
+# PPM (UNIX PPM package)
+image/x-portable-pixmap ppm
+# PGM (UNIX PPM package)
+image/x-portable-graymap pgm
+# PBM (UNIX PPM package)
+image/x-portable-bitmap pbm
+# PNM (UNIX PPM package)
+image/x-portable-anymap pnm
+# Microsoft Windows bitmap
+image/x-ms-bmp bmp
+# CMU raster
+image/x-cmu-raster ras
+# Kodak Photo-CD
+image/x-photo-cd pcd
+# Computer Graphics Metafile
+image/cgm cgm
+# CALS Type 1 or 2
+image/x-cals mil cal
+# Fractal Image Format (Iterated Systems)
+image/fif fif
+# QuickSilver active image (Micrografx)
+image/x-mgx-dsf dsf
+# CMX vector image (Corel)
+image/x-cmx cmx
+# Wavelet-compressed (Summus)
+image/wavelet wi
+# AutoCad Drawing (SoftSource)
+image/vnd.dwg dwg
+# AutoCad DXF file (SoftSource)
+image/vnd.dxf dxf
+# Simple Vector Format (SoftSource)
+image/vnd.svf svf
+
+# AUDIO/VOICE/MUSIC RELATED TYPES
+
+# """basic""audio - 8-bit u-law PCM"
+audio/basic au snd
+# Macintosh audio format (AIpple)
+audio/x-aiff aif aiff aifc
+# Microsoft audio
+audio/x-wav wav
+# MPEG audio
+audio/x-mpeg mpa abs mpega
+# MPEG-2 audio
+audio/x-mpeg-2 mp2a mpa2
+# compressed speech (Echo Speech Corp.)
+audio/echospeech es
+# Toolvox speech audio (Voxware)
+audio/voxware vox
+# RapidTransit compressed audio (Fast Man)
+application/fastman lcc
+# Realaudio (Progressive Networks)
+application/x-pn-realaudio ra ram
+# MIDI music data
+x-music/x-midi mmid
+# Koan music data (SSeyo)
+application/vnd.koan skp
+# Speech synthesis data (MVP Solutions)
+text/x-speech talk
+
+# VIDEO TYPES
+
+# MPEG video
+video/mpeg mpeg mpg mpe
+# MPEG-2 video
+video/mpeg-2 mpv2 mp2v
+# Macintosh Quicktime
+video/quicktime qt mov
+# Microsoft video
+video/x-msvideo avi
+# SGI Movie format
+video/x-sgi-movie movie
+# VDOlive streaming video (VDOnet)
+video/vdo vdo
+# Vivo streaming video (Vivo software)
+video/vnd.vivo viv
+
+# SPECIAL HTTP/WEB APPLICATION TYPES
+
+# Proxy autoconfiguration (Netscape browsers)
+application/x-ns-proxy-autoconfig pac
+# Netscape Cooltalk chat data (Netscape)
+x-conference/x-cooltalk ice
+
+# TEXT-RELATED
+
+# PostScript
+application/postscript ai eps ps
+# Microsoft Rich Text Format
+application/rtf rtf
+# Adobe Acrobat PDF
+application/pdf pdf
+# Maker Interchange Format (FrameMaker)
+application/vnd.mif mif
+# Troff document
+application/x-troff t tr roff
+# Troff document with MAN macros
+application/x-troff-man man
+# Troff document with ME macros
+application/x-troff-me me
+# Troff document with MS macros
+application/x-troff-ms ms
+# LaTeX document
+application/x-latex latex
+# Tex/LateX document
+application/x-tex tex
+# GNU TexInfo document
+application/x-texinfo texinfo texi
+# TeX dvi format
+application/x-dvi dvi
+# MS word document
+application/msword doc DOC
+# Office Document Architecture
+application/oda oda
+# Envoy Document
+application/envoy evy
+
+# ARCHIVE/COMPRESSED ARCHIVES
+
+# Gnu tar format
+application/x-gtar gtar
+# 4.3BSD tar format
+application/x-tar tar
+# POSIX tar format
+application/x-ustar ustar
+# Old CPIO format
+application/x-bcpio bcpio
+# POSIX CPIO format
+application/x-cpio cpio
+# UNIX sh shell archive
+application/x-shar shar
+# DOS/PC - Pkzipped archive
+application/zip zip
+# Macintosh Binhexed archive
+application/mac-binhex40 hqx
+# Macintosh Stuffit Archive
+application/x-stuffit sit sea
+# Fractal Image Format
+application/fractals fif
+# "Binary UUencoded"
+application/octet-stream bin uu
+# PC executable
+application/octet-stream exe
+# "WAIS ""sources"""
+application/x-wais-source src wsrc
+# NCSA HDF data format
+application/hdf hdf
+
+# DOWNLOADABLE PROGRAM/SCRIPTS
+
+# Javascript program
+text/javascript js ls mocha
+# UNIX bourne shell program
+application/x-sh sh
+# UNIX c-shell program
+application/x-csh csh
+# Perl program
+application/x-perl pl
+# Tcl (Tool Control Language) program
+application/x-tcl tcl
+
+# ANIMATION/MULTIMEDIA
+
+# FutureSplash vector animation (FutureWave)
+application/futuresplash spl
+# mBED multimedia data (mBED)
+application/mbedlet mbd
+# PowerMedia multimedia (RadMedia)
+application/x-rad-powermedia rad
+
+# PRESENTATION
+
+# PowerPoint presentation (Microsoft)
+application/mspowerpoint ppz
+# ASAP WordPower (Software Publishing Corp.)
+application/x-asap asp
+# Astound Web Player multimedia data (GoldDisk)
+application/astound asn
+
+# SPECIAL EMBEDDED OBJECT
+
+# OLE script e.g. Visual Basic (Ncompass)
+application/x-olescript axs
+# OLE Object (Microsoft/NCompass)
+application/x-oleobject ods
+# OpenScape OLE/OCX objects (Business@Web)
+x-form/x-openscape opp
+# Visual Basic objects (Amara)
+application/x-webbasic wba
+# Specialized data entry forms (Alpha Software)
+application/x-alpha-form frm
+# client-server objects (Wayfarer Communications)
+x-script/x-wfxclient wfx
+
+# GENERAL APPLICATIONS
+
+# Undefined binary data (often executable progs)
+application/octet-stream exe com
+# Pointcast news data (Pointcast)
+application/x-pcn pcn
+# Excel spreadsheet (Microsoft)
+application/vnd.ms-excel xls
+# PowerPoint (Microsoft)
+application/vnd.ms-powerpoint ppt
+# Microsoft Project (Microsoft)
+application/vnd.ms-project mpp
+# SourceView document (Dataware Electronics)
+application/vnd.svd svd
+# Net Install - software install (20/20 Software)
+application/x-net-install ins
+# Carbon Copy - remote control/access (Microcom)
+application/ccv ccv
+# Spreadsheets (Visual Components)
+workbook/formulaone vts
+
+# 2D/3D DATA/VIRTUAL REALITY TYPES
+
+# VRML data file
+x-world/x-vrml wrl vrml
+# WIRL - VRML data (VREAM)
+x-world/x-vream vrw
+# Play3D 3d scene data (Play3D)
+application/x-p3d p3d
+# Viscape Interactive 3d world data (Superscape)
+x-world/x-svr svr
+# WebActive 3d data (Plastic Thought)
+x-world/x-wvr wvr
+# QuickDraw3D scene data (Apple)
+x-world/x-3dmf 3dmf
+
+# SCIENTIFIC/MATH/CAD TYPES
+
+# Mathematica notebook
+application/mathematica ma
+# Computational meshes for numerical simulations
+x-model/x-mesh msh
+# Vis5D 5-dimensional data
+application/vis5d v5d
+# IGES models -- CAD/CAM (CGM) data
+application/iges igs
+# Autocad WHIP vector drawings
+drawing/x-dwf dwf
+
diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/package.html b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/package.html
new file mode 100644
index 00000000000..da49cc07165
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/package.html
@@ -0,0 +1,9 @@
+
+java.util.Timer).
+ * @see #setFixedRate
+ * @see #isOneTimeTask()
+ * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long)
+ */
+ public void setPeriod(long period) {
+ this.period = period;
+ }
+
+ /**
+ * Return the period between repeated task executions.
+ */
+ public long getPeriod() {
+ return this.period;
+ }
+
+ /**
+ * Is this task only ever going to execute once?
+ * @return true if this task is only ever going to execute once
+ * @see #getPeriod()
+ */
+ public boolean isOneTimeTask() {
+ return (this.period < 0);
+ }
+
+ /**
+ * Set whether to schedule as fixed-rate execution, rather than
+ * fixed-delay execution. Default is "false", i.e. fixed delay.
+ * resource-ref of type commonj.timers.TimerManager
+ * in web.xml, with res-sharing-scope set to 'Unshareable'.
+ * startWork(Work) instead of schedule(Work)
+ * in the execute(Runnable) implementation. Furthermore,
+ * for simplicity's sake, drop the entire "Implementation of the CommonJ
+ * WorkManager interface" section (and the corresponding
+ * implements WorkManager clause at the class level).
+ * Of course, you also need to change all commonj.work imports in
+ * your WorkManagerTaskExecutor and DelegatingWork variants to the corresponding
+ * WebSphere API imports (com.ibm.websphere.asynchbeans.WorkManager
+ * and com.ibm.websphere.asynchbeans.Work, respectively).
+ * This should be sufficient to get a TaskExecutor adapter for WebSphere 5.
+ *
+ * @author Juergen Hoeller
+ * @since 2.0
+ */
+public class WorkManagerTaskExecutor extends JndiLocatorSupport
+ implements SchedulingTaskExecutor, WorkManager, InitializingBean {
+
+ private WorkManager workManager;
+
+ private String workManagerName;
+
+ private WorkListener workListener;
+
+
+ /**
+ * Specify the CommonJ WorkManager to delegate to.
+ * MISFIRE_INSTRUCTION_SMART_POLICY.
+ * @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
+ * @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_DO_NOTHING
+ * @see org.quartz.Trigger#MISFIRE_INSTRUCTION_SMART_POLICY
+ */
+ public void setMisfireInstructionName(String constantName) {
+ setMisfireInstruction(constants.asNumber(constantName).intValue());
+ }
+
+ /**
+ * Set a list of TriggerListener names for this job, referring to
+ * non-global TriggerListeners registered with the Scheduler.
+ * null if none
+ */
+ JobDetail getJobDetail();
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailBean.java
new file mode 100644
index 00000000000..07c54e40619
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailBean.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.util.Map;
+
+import org.quartz.Job;
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+/**
+ * Convenience subclass of Quartz' JobDetail class that eases bean-style
+ * usage.
+ *
+ * JobDetail itself is already a JavaBean but lacks
+ * sensible defaults. This class uses the Spring bean name as job name,
+ * and the Quartz default group ("DEFAULT") as job group if not specified.
+ *
+ * @author Juergen Hoeller
+ * @since 18.02.2004
+ * @see #setName
+ * @see #setGroup
+ * @see org.springframework.beans.factory.BeanNameAware
+ * @see org.quartz.Scheduler#DEFAULT_GROUP
+ */
+public class JobDetailBean extends JobDetail
+ implements BeanNameAware, ApplicationContextAware, InitializingBean {
+
+ private Class actualJobClass;
+
+ private String beanName;
+
+ private ApplicationContext applicationContext;
+
+ private String applicationContextJobDataKey;
+
+
+ /**
+ * Overridden to support any job class, to allow a custom JobFactory
+ * to adapt the given job class to the Quartz Job interface.
+ * @see SchedulerFactoryBean#setJobFactory
+ */
+ public void setJobClass(Class jobClass) {
+ if (jobClass != null && !Job.class.isAssignableFrom(jobClass)) {
+ super.setJobClass(DelegatingJob.class);
+ this.actualJobClass = jobClass;
+ }
+ else {
+ super.setJobClass(jobClass);
+ }
+ }
+
+ /**
+ * Overridden to support any job class, to allow a custom JobFactory
+ * to adapt the given job class to the Quartz Job interface.
+ */
+ public Class getJobClass() {
+ return (this.actualJobClass != null ? this.actualJobClass : super.getJobClass());
+ }
+
+ /**
+ * Register objects in the JobDataMap via a given Map.
+ * getMaximumPoolSize() - getActiveCount()
+ // on a java.util.concurrent.ThreadPoolExecutor.
+ return 1;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java
new file mode 100644
index 00000000000..d760307a5c8
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.Scheduler;
+import org.quartz.StatefulJob;
+
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.support.ArgumentConvertingMethodInvoker;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.MethodInvoker;
+
+/**
+ * {@link org.springframework.beans.factory.FactoryBean} that exposes a
+ * {@link org.quartz.JobDetail} object which delegates job execution to a
+ * specified (static or non-static) method. Avoids the need for implementing
+ * a one-line Quartz Job that just invokes an existing service method on a
+ * Spring-managed target bean.
+ *
+ * executeInternal afterwards.
+ * @see #executeInternal
+ */
+ public final void execute(JobExecutionContext context) throws JobExecutionException {
+ try {
+ BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
+ MutablePropertyValues pvs = new MutablePropertyValues();
+ pvs.addPropertyValues(context.getScheduler().getContext());
+ pvs.addPropertyValues(context.getMergedJobDataMap());
+ bw.setPropertyValues(pvs, true);
+ }
+ catch (SchedulerException ex) {
+ throw new JobExecutionException(ex);
+ }
+ executeInternal(context);
+ }
+
+ /**
+ * Execute the actual job. The job data map will already have been
+ * applied as bean property values by execute. The contract is
+ * exactly the same as for the standard Quartz execute method.
+ * @see #execute
+ */
+ protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
new file mode 100644
index 00000000000..18593865af4
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.spi.ClassLoadHelper;
+
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+
+/**
+ * Wrapper that adapts from the Quartz {@link ClassLoadHelper} interface
+ * onto Spring's {@link ResourceLoader} interface. Used by default when
+ * the SchedulerFactoryBean runs in a Spring ApplicationContext.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5.5
+ * @see SchedulerFactoryBean#setApplicationContext
+ */
+public class ResourceLoaderClassLoadHelper implements ClassLoadHelper {
+
+ protected static final Log logger = LogFactory.getLog(ResourceLoaderClassLoadHelper.class);
+
+ private ResourceLoader resourceLoader;
+
+
+ /**
+ * Create a new ResourceLoaderClassLoadHelper for the default
+ * ResourceLoader.
+ * @see SchedulerFactoryBean#getConfigTimeResourceLoader()
+ */
+ public ResourceLoaderClassLoadHelper() {
+ }
+
+ /**
+ * Create a new ResourceLoaderClassLoadHelper for the given ResourceLoader.
+ * @param resourceLoader the ResourceLoader to delegate to
+ */
+ public ResourceLoaderClassLoadHelper(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+
+ public void initialize() {
+ if (this.resourceLoader == null) {
+ this.resourceLoader = SchedulerFactoryBean.getConfigTimeResourceLoader();
+ if (this.resourceLoader == null) {
+ this.resourceLoader = new DefaultResourceLoader();
+ }
+ }
+ }
+
+ public Class loadClass(String name) throws ClassNotFoundException {
+ return this.resourceLoader.getClassLoader().loadClass(name);
+ }
+
+ public URL getResource(String name) {
+ Resource resource = this.resourceLoader.getResource(name);
+ try {
+ return resource.getURL();
+ }
+ catch (FileNotFoundException ex) {
+ return null;
+ }
+ catch (IOException ex) {
+ logger.warn("Could not load " + resource);
+ return null;
+ }
+ }
+
+ public InputStream getResourceAsStream(String name) {
+ Resource resource = this.resourceLoader.getResource(name);
+ try {
+ return resource.getInputStream();
+ }
+ catch (FileNotFoundException ex) {
+ return null;
+ }
+ catch (IOException ex) {
+ logger.warn("Could not load " + resource);
+ return null;
+ }
+ }
+
+ public ClassLoader getClassLoader() {
+ return this.resourceLoader.getClassLoader();
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java
new file mode 100644
index 00000000000..04ea6c3253d
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.Calendar;
+import org.quartz.JobDetail;
+import org.quartz.JobListener;
+import org.quartz.ObjectAlreadyExistsException;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.SchedulerListener;
+import org.quartz.Trigger;
+import org.quartz.TriggerListener;
+import org.quartz.spi.ClassLoadHelper;
+import org.quartz.xml.JobSchedulingDataProcessor;
+
+import org.springframework.context.ResourceLoaderAware;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+/**
+ * Common base class for accessing a Quartz Scheduler, i.e. for registering jobs,
+ * triggers and listeners on a {@link org.quartz.Scheduler} instance.
+ *
+ * true if the job was actually added,
+ * false if it already existed before
+ * @see #setOverwriteExistingJobs
+ */
+ private boolean addJobToScheduler(JobDetail jobDetail) throws SchedulerException {
+ if (this.overwriteExistingJobs ||
+ getScheduler().getJobDetail(jobDetail.getName(), jobDetail.getGroup()) == null) {
+ getScheduler().addJob(jobDetail, true);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Add the given trigger to the Scheduler, if it doesn't already exist.
+ * Overwrites the trigger in any case if "overwriteExistingJobs" is set.
+ * @param trigger the trigger to add
+ * @return true if the trigger was actually added,
+ * false if it already existed before
+ * @see #setOverwriteExistingJobs
+ */
+ private boolean addTriggerToScheduler(Trigger trigger) throws SchedulerException {
+ boolean triggerExists = (getScheduler().getTrigger(trigger.getName(), trigger.getGroup()) != null);
+ if (!triggerExists || this.overwriteExistingJobs) {
+ // Check if the Trigger is aware of an associated JobDetail.
+ if (trigger instanceof JobDetailAwareTrigger) {
+ JobDetail jobDetail = ((JobDetailAwareTrigger) trigger).getJobDetail();
+ // Automatically register the JobDetail too.
+ if (!this.jobDetails.contains(jobDetail) && addJobToScheduler(jobDetail)) {
+ this.jobDetails.add(jobDetail);
+ }
+ }
+ if (!triggerExists) {
+ try {
+ getScheduler().scheduleJob(trigger);
+ }
+ catch (ObjectAlreadyExistsException ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Unexpectedly found existing trigger, assumably due to cluster race condition: " +
+ ex.getMessage() + " - can safely be ignored");
+ }
+ if (this.overwriteExistingJobs) {
+ getScheduler().rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
+ }
+ }
+ }
+ else {
+ getScheduler().rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Register all specified listeners with the Scheduler.
+ */
+ protected void registerListeners() throws SchedulerException {
+ if (this.schedulerListeners != null) {
+ for (int i = 0; i < this.schedulerListeners.length; i++) {
+ getScheduler().addSchedulerListener(this.schedulerListeners[i]);
+ }
+ }
+ if (this.globalJobListeners != null) {
+ for (int i = 0; i < this.globalJobListeners.length; i++) {
+ getScheduler().addGlobalJobListener(this.globalJobListeners[i]);
+ }
+ }
+ if (this.jobListeners != null) {
+ for (int i = 0; i < this.jobListeners.length; i++) {
+ getScheduler().addJobListener(this.jobListeners[i]);
+ }
+ }
+ if (this.globalTriggerListeners != null) {
+ for (int i = 0; i < this.globalTriggerListeners.length; i++) {
+ getScheduler().addGlobalTriggerListener(this.globalTriggerListeners[i]);
+ }
+ }
+ if (this.triggerListeners != null) {
+ for (int i = 0; i < this.triggerListeners.length; i++) {
+ getScheduler().addTriggerListener(this.triggerListeners[i]);
+ }
+ }
+ }
+
+
+ /**
+ * Template method that determines the Scheduler to operate on.
+ * To be implemented by subclasses.
+ */
+ protected abstract Scheduler getScheduler();
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java
new file mode 100644
index 00000000000..7b53354019f
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.impl.SchedulerRepository;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.ListableBeanFactory;
+
+/**
+ * Spring bean-style class for accessing a Quartz Scheduler, i.e. for registering jobs,
+ * triggers and listeners on a given {@link org.quartz.Scheduler} instance.
+ *
+ * @author Juergen Hoeller
+ * @since 2.5.6
+ * @see #setScheduler
+ * @see #setSchedulerName
+ */
+public class SchedulerAccessorBean extends SchedulerAccessor implements BeanFactoryAware, InitializingBean {
+
+ private String schedulerName;
+
+ private Scheduler scheduler;
+
+ private BeanFactory beanFactory;
+
+
+ /**
+ * Specify the Quartz Scheduler to operate on via its scheduler name in the Spring
+ * application context or also in the Quartz {@link org.quartz.impl.SchedulerRepository}.
+ * org.quartz.Scheduler). This allows you to create new jobs
+ * and triggers, and also to control and monitor the entire Scheduler.
+ *
+ * quartz.properties from quartz.jar.
+ * To use custom Quartz properties, specify the "configLocation"
+ * or "quartzProperties" bean property on this FactoryBean.
+ * @see org.quartz.impl.StdSchedulerFactory
+ * @see #setConfigLocation
+ * @see #setQuartzProperties
+ */
+ public void setSchedulerFactoryClass(Class schedulerFactoryClass) {
+ if (schedulerFactoryClass == null || !SchedulerFactory.class.isAssignableFrom(schedulerFactoryClass)) {
+ throw new IllegalArgumentException("schedulerFactoryClass must implement [org.quartz.SchedulerFactory]");
+ }
+ this.schedulerFactoryClass = schedulerFactoryClass;
+ }
+
+ /**
+ * Set the name of the Scheduler to create via the SchedulerFactory.
+ * getScheduler
+ * method. Can be overridden for custom Scheduler creation.
+ * @param schedulerFactory the factory to create the Scheduler with
+ * @param schedulerName the name of the scheduler to create
+ * @return the Scheduler instance
+ * @throws SchedulerException if thrown by Quartz methods
+ * @see #afterPropertiesSet
+ * @see org.quartz.SchedulerFactory#getScheduler
+ */
+ protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String schedulerName)
+ throws SchedulerException {
+
+ // Override thread context ClassLoader to work around naive Quartz ClassLoadHelper loading.
+ Thread currentThread = Thread.currentThread();
+ ClassLoader threadContextClassLoader = currentThread.getContextClassLoader();
+ boolean overrideClassLoader = (this.resourceLoader != null &&
+ !this.resourceLoader.getClassLoader().equals(threadContextClassLoader));
+ if (overrideClassLoader) {
+ currentThread.setContextClassLoader(this.resourceLoader.getClassLoader());
+ }
+ try {
+ SchedulerRepository repository = SchedulerRepository.getInstance();
+ synchronized (repository) {
+ Scheduler existingScheduler = (schedulerName != null ? repository.lookup(schedulerName) : null);
+ Scheduler newScheduler = schedulerFactory.getScheduler();
+ if (newScheduler == existingScheduler) {
+ throw new IllegalStateException("Active Scheduler of name '" + schedulerName + "' already registered " +
+ "in Quartz SchedulerRepository. Cannot create a new Spring-managed Scheduler of the same name!");
+ }
+ if (!this.exposeSchedulerInRepository) {
+ // Need to remove it in this case, since Quartz shares the Scheduler instance by default!
+ SchedulerRepository.getInstance().remove(newScheduler.getSchedulerName());
+ }
+ return newScheduler;
+ }
+ }
+ finally {
+ if (overrideClassLoader) {
+ // Reset original thread context ClassLoader.
+ currentThread.setContextClassLoader(threadContextClassLoader);
+ }
+ }
+ }
+
+ /**
+ * Expose the specified context attributes and/or the current
+ * ApplicationContext in the Quartz SchedulerContext.
+ */
+ private void populateSchedulerContext() throws SchedulerException {
+ // Put specified objects into Scheduler context.
+ if (this.schedulerContextMap != null) {
+ this.scheduler.getContext().putAll(this.schedulerContextMap);
+ }
+
+ // Register ApplicationContext in Scheduler context.
+ if (this.applicationContextSchedulerContextKey != null) {
+ if (this.applicationContext == null) {
+ throw new IllegalStateException(
+ "SchedulerFactoryBean needs to be set up in an ApplicationContext " +
+ "to be able to handle an 'applicationContextSchedulerContextKey'");
+ }
+ this.scheduler.getContext().put(this.applicationContextSchedulerContextKey, this.applicationContext);
+ }
+ }
+
+
+ /**
+ * Start the Quartz Scheduler, respecting the "startupDelay" setting.
+ * @param scheduler the Scheduler to start
+ * @param startupDelay the number of seconds to wait before starting
+ * the Scheduler asynchronously
+ */
+ protected void startScheduler(final Scheduler scheduler, final int startupDelay) throws SchedulerException {
+ if (startupDelay <= 0) {
+ logger.info("Starting Quartz Scheduler now");
+ scheduler.start();
+ }
+ else {
+ if (logger.isInfoEnabled()) {
+ logger.info("Will start Quartz Scheduler [" + scheduler.getSchedulerName() +
+ "] in " + startupDelay + " seconds");
+ }
+ Thread schedulerThread = new Thread() {
+ public void run() {
+ try {
+ Thread.sleep(startupDelay * 1000);
+ }
+ catch (InterruptedException ex) {
+ // simply proceed
+ }
+ if (logger.isInfoEnabled()) {
+ logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds");
+ }
+ try {
+ scheduler.start();
+ }
+ catch (SchedulerException ex) {
+ throw new SchedulingException("Could not start Quartz Scheduler after delay", ex);
+ }
+ }
+ };
+ schedulerThread.setName("Quartz Scheduler [" + scheduler.getSchedulerName() + "]");
+ schedulerThread.start();
+ }
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of FactoryBean interface
+ //---------------------------------------------------------------------
+
+ public Scheduler getScheduler() {
+ return this.scheduler;
+ }
+
+ public Object getObject() {
+ return this.scheduler;
+ }
+
+ public Class getObjectType() {
+ return (this.scheduler != null) ? this.scheduler.getClass() : Scheduler.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of Lifecycle interface
+ //---------------------------------------------------------------------
+
+ public void start() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ this.scheduler.start();
+ }
+ catch (SchedulerException ex) {
+ throw new SchedulingException("Could not start Quartz Scheduler", ex);
+ }
+ }
+ }
+
+ public void stop() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ this.scheduler.standby();
+ }
+ catch (SchedulerException ex) {
+ throw new SchedulingException("Could not stop Quartz Scheduler", ex);
+ }
+ }
+ }
+
+ public boolean isRunning() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ return !this.scheduler.isInStandbyMode();
+ }
+ catch (SchedulerException ex) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of DisposableBean interface
+ //---------------------------------------------------------------------
+
+ /**
+ * Shut down the Quartz scheduler on bean factory shutdown,
+ * stopping all scheduled jobs.
+ */
+ public void destroy() throws SchedulerException {
+ logger.info("Shutting down Quartz Scheduler");
+ this.scheduler.shutdown(this.waitForJobsToCompleteOnShutdown);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java
new file mode 100644
index 00000000000..0152b88d5ab
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.scheduling.quartz;
+
+import org.quartz.SchedulerConfigException;
+import org.quartz.simpl.SimpleThreadPool;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.scheduling.SchedulingException;
+import org.springframework.scheduling.SchedulingTaskExecutor;
+import org.springframework.util.Assert;
+
+/**
+ * Subclass of Quartz's SimpleThreadPool that implements Spring's
+ * TaskExecutor interface and listens to Spring lifecycle callbacks.
+ *
+ * MISFIRE_INSTRUCTION_SMART_POLICY.
+ * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_FIRE_NOW
+ * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
+ * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
+ * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
+ * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
+ * @see org.quartz.Trigger#MISFIRE_INSTRUCTION_SMART_POLICY
+ */
+ public void setMisfireInstructionName(String constantName) {
+ setMisfireInstruction(constants.asNumber(constantName).intValue());
+ }
+
+ /**
+ * Set a list of TriggerListener names for this job, referring to
+ * non-global TriggerListeners registered with the Scheduler.
+ * null, indicating that all unknown properties
+ * should be ignored. Specify an empty array to throw an exception in case
+ * of any unknown properties, or a list of property names that should be
+ * ignored if there is no corresponding property found on the particular
+ * job class (all other unknown properties will still trigger an exception).
+ */
+ public void setIgnoredUnknownProperties(String[] ignoredUnknownProperties) {
+ this.ignoredUnknownProperties = ignoredUnknownProperties;
+ }
+
+ public void setSchedulerContext(SchedulerContext schedulerContext) {
+ this.schedulerContext = schedulerContext;
+ }
+
+
+ /**
+ * Create the job instance, populating it with property values taken
+ * from the scheduler context, job data map and trigger data map.
+ */
+ protected Object createJobInstance(TriggerFiredBundle bundle) {
+ Object job = BeanUtils.instantiateClass(bundle.getJobDetail().getJobClass());
+ BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
+ if (isEligibleForPropertyPopulation(bw.getWrappedInstance())) {
+ MutablePropertyValues pvs = new MutablePropertyValues();
+ if (this.schedulerContext != null) {
+ pvs.addPropertyValues(this.schedulerContext);
+ }
+ pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
+ pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
+ if (this.ignoredUnknownProperties != null) {
+ for (int i = 0; i < this.ignoredUnknownProperties.length; i++) {
+ String propName = this.ignoredUnknownProperties[i];
+ if (pvs.contains(propName) && !bw.isWritableProperty(propName)) {
+ pvs.removePropertyValue(propName);
+ }
+ }
+ bw.setPropertyValues(pvs);
+ }
+ else {
+ bw.setPropertyValues(pvs, true);
+ }
+ }
+ return job;
+ }
+
+ /**
+ * Return whether the given job object is eligible for having
+ * its bean properties populated.
+ * Configuration.setSettings() method and are
+ * subject to constraints set by FreeMarker.
+ *
+ * setAllSharedVariables() method. Like setSettings(),
+ * these entries are subject to FreeMarker constraints.
+ *
+ * Configuration.setSettings method.
+ * @see freemarker.template.Configuration#setSettings
+ */
+ public void setFreemarkerSettings(Properties settings) {
+ this.freemarkerSettings = settings;
+ }
+
+ /**
+ * Set a Map that contains well-known FreeMarker objects which will be passed
+ * to FreeMarker's Configuration.setAllSharedVariables() method.
+ * @see freemarker.template.Configuration#setAllSharedVariables
+ */
+ public void setFreemarkerVariables(Map variables) {
+ this.freemarkerVariables = variables;
+ }
+
+ /**
+ * Set the default encoding for the FreeMarker configuration.
+ * If not specified, FreeMarker will use the platform file encoding.
+ * TemplateLoaders that will be used to search
+ * for templates. For example, one or more custom loaders such as database
+ * loaders could be configured and injected here.
+ * @deprecated as of Spring 2.0.1, in favor of the "preTemplateLoaders"
+ * and "postTemplateLoaders" properties
+ * @see #setPreTemplateLoaders
+ * @see #setPostTemplateLoaders
+ */
+ public void setTemplateLoaders(TemplateLoader[] templateLoaders) {
+ if (templateLoaders != null) {
+ this.templateLoaders.addAll(Arrays.asList(templateLoaders));
+ }
+ }
+
+ /**
+ * Set a List of TemplateLoaders that will be used to search
+ * for templates. For example, one or more custom loaders such as database
+ * loaders could be configured and injected here.
+ * TemplateLoaders that will be used to search
+ * for templates. For example, one or more custom loaders such as database
+ * loaders can be configured.
+ * java.io.File,
+ * a generic SpringTemplateLoader will be used, without modification detection.
+ * setTemplateLoaders(List templateLoaders)
+ * @see org.springframework.core.io.ResourceEditor
+ * @see org.springframework.context.ApplicationContext#getResource
+ * @see freemarker.template.Configuration#setDirectoryForTemplateLoading
+ * @see SpringTemplateLoader
+ * @see #setTemplateLoaders
+ */
+ public void setTemplateLoaderPaths(String[] templateLoaderPaths) {
+ this.templateLoaderPaths = templateLoaderPaths;
+ }
+
+ /**
+ * Set the Spring ResourceLoader to use for loading FreeMarker template files.
+ * The default is DefaultResourceLoader. Will get overridden by the
+ * ApplicationContext if running in a context.
+ * @see org.springframework.core.io.DefaultResourceLoader
+ */
+ public void setResourceLoader(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * Return the Spring ResourceLoader to use for loading FreeMarker template files.
+ */
+ protected ResourceLoader getResourceLoader() {
+ return resourceLoader;
+ }
+
+ /**
+ * Set whether to prefer file system access for template loading.
+ * File system access enables hot detection of template changes.
+ * createConfiguration().
+ * @return the Configuration object
+ * @throws IOException if a config file wasn't found
+ * @throws TemplateException on FreeMarker initialization failure
+ * @see #createConfiguration()
+ */
+ protected Configuration newConfiguration() throws IOException, TemplateException {
+ return new Configuration();
+ }
+
+ /**
+ * Determine a FreeMarker TemplateLoader for the given path.
+ * createConfiguration(). Note that specified
+ * "postTemplateLoaders" will be registered after any loaders
+ * registered by this callback; as a consequence, they are are not
+ * included in the given List.
+ * @param templateLoaders the current List of TemplateLoader instances,
+ * to be modified by a subclass
+ * @see #createConfiguration()
+ * @see #setPostTemplateLoaders
+ */
+ protected void postProcessTemplateLoaders(List templateLoaders) {
+ }
+
+ /**
+ * Return a TemplateLoader based on the given TemplateLoader list.
+ * If more than one TemplateLoader has been registered, a FreeMarker
+ * MultiTemplateLoader needs to be created.
+ * @param templateLoaders the final List of TemplateLoader instances
+ * @return the aggregate TemplateLoader
+ */
+ protected TemplateLoader getAggregateTemplateLoader(List templateLoaders) {
+ int loaderCount = templateLoaders.size();
+ switch (loaderCount) {
+ case 0:
+ logger.info("No FreeMarker TemplateLoaders specified");
+ return null;
+ case 1:
+ return (TemplateLoader) templateLoaders.get(0);
+ default:
+ TemplateLoader[] loaders = (TemplateLoader[]) templateLoaders.toArray(new TemplateLoader[loaderCount]);
+ return new MultiTemplateLoader(loaders);
+ }
+ }
+
+ /**
+ * To be overridden by subclasses that want to to perform custom
+ * post-processing of the Configuration object after this factory
+ * performed its default initialization.
+ * createConfiguration().
+ * @param config the current Configuration object
+ * @throws IOException if a config file wasn't found
+ * @throws TemplateException on FreeMarker initialization failure
+ * @see #createConfiguration()
+ */
+ protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException {
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java
new file mode 100644
index 00000000000..0f5935628ab
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.freemarker;
+
+import java.io.IOException;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ResourceLoaderAware;
+
+/**
+ * Factory bean that creates a FreeMarker Configuration and provides it as
+ * bean reference. This bean is intended for any kind of usage of FreeMarker
+ * in application code, e.g. for generating email content. For web views,
+ * FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory.
+ *
+ * The simplest way to use this class is to specify just a "templateLoaderPath";
+ * you do not need any further configuration then. For example, in a web
+ * application context:
+ *
+ * <bean id="freemarkerConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean">
+ * <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
+ * </bean>
+
+ * See the base class FreeMarkerConfigurationFactory for configuration details.
+ *
+ * JRDataSource.
+ * JRDataSource,
+ * java.util.Collection or object array is detected.
+ * The latter are converted to JRBeanCollectionDataSource
+ * or JRBeanArrayDataSource, respectively.
+ * @param value the report data value to convert
+ * @return the JRDataSource (never null)
+ * @throws IllegalArgumentException if the value could not be converted
+ * @see net.sf.jasperreports.engine.JRDataSource
+ * @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
+ * @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
+ */
+ public static JRDataSource convertReportData(Object value) throws IllegalArgumentException {
+ if (value instanceof JRDataSource) {
+ return (JRDataSource) value;
+ }
+ else if (value instanceof Collection) {
+ return new JRBeanCollectionDataSource((Collection) value);
+ }
+ else if (value instanceof Object[]) {
+ return new JRBeanArrayDataSource((Object[]) value);
+ }
+ else {
+ throw new IllegalArgumentException("Value [" + value + "] cannot be converted to a JRDataSource");
+ }
+ }
+
+ /**
+ * Render the supplied JasperPrint instance using the
+ * supplied JRAbstractExporter instance and write the results
+ * to the supplied Writer.
+ * JRAbstractExporter implementation
+ * you supply is capable of writing to a Writer.
+ * @param exporter the JRAbstractExporter to use to render the report
+ * @param print the JasperPrint instance to render
+ * @param writer the Writer to write the result to
+ * @throws JRException if rendering failed
+ */
+ public static void render(JRExporter exporter, JasperPrint print, Writer writer)
+ throws JRException {
+
+ exporter.setParameter(JRExporterParameter.JASPER_PRINT, print);
+ exporter.setParameter(JRExporterParameter.OUTPUT_WRITER, writer);
+ exporter.exportReport();
+ }
+
+ /**
+ * Render the supplied JasperPrint instance using the
+ * supplied JRAbstractExporter instance and write the results
+ * to the supplied OutputStream.
+ * JRAbstractExporter implementation you
+ * supply is capable of writing to a OutputStream.
+ * @param exporter the JRAbstractExporter to use to render the report
+ * @param print the JasperPrint instance to render
+ * @param outputStream the OutputStream to write the result to
+ * @throws JRException if rendering failed
+ */
+ public static void render(JRExporter exporter, JasperPrint print, OutputStream outputStream)
+ throws JRException {
+
+ exporter.setParameter(JRExporterParameter.JASPER_PRINT, print);
+ exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, outputStream);
+ exporter.exportReport();
+ }
+
+ /**
+ * Render a report in CSV format using the supplied report data.
+ * Writes the results to the supplied Writer.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param writer the Writer to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsCsv(JasperReport report, Map parameters, Object reportData, Writer writer)
+ throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ render(new JRCsvExporter(), print, writer);
+ }
+
+ /**
+ * Render a report in CSV format using the supplied report data.
+ * Writes the results to the supplied Writer.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param writer the Writer to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @param exporterParameters a {@link Map} of {@link JRExporterParameter exporter parameters}
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsCsv(JasperReport report, Map parameters, Object reportData, Writer writer,
+ Map exporterParameters) throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ JRCsvExporter exporter = new JRCsvExporter();
+ exporter.setParameters(exporterParameters);
+ render(exporter, print, writer);
+ }
+
+ /**
+ * Render a report in HTML format using the supplied report data.
+ * Writes the results to the supplied Writer.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param writer the Writer to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsHtml(JasperReport report, Map parameters, Object reportData, Writer writer)
+ throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ render(new JRHtmlExporter(), print, writer);
+ }
+
+ /**
+ * Render a report in HTML format using the supplied report data.
+ * Writes the results to the supplied Writer.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param writer the Writer to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @param exporterParameters a {@link Map} of {@link JRExporterParameter exporter parameters}
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsHtml(JasperReport report, Map parameters, Object reportData, Writer writer,
+ Map exporterParameters) throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ JRHtmlExporter exporter = new JRHtmlExporter();
+ exporter.setParameters(exporterParameters);
+ render(exporter, print, writer);
+ }
+
+ /**
+ * Render a report in PDF format using the supplied report data.
+ * Writes the results to the supplied OutputStream.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param stream the OutputStream to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsPdf(JasperReport report, Map parameters, Object reportData, OutputStream stream)
+ throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ render(new JRPdfExporter(), print, stream);
+ }
+
+ /**
+ * Render a report in PDF format using the supplied report data.
+ * Writes the results to the supplied OutputStream.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param stream the OutputStream to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @param exporterParameters a {@link Map} of {@link JRExporterParameter exporter parameters}
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsPdf(JasperReport report, Map parameters, Object reportData, OutputStream stream,
+ Map exporterParameters) throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ JRPdfExporter exporter = new JRPdfExporter();
+ exporter.setParameters(exporterParameters);
+ render(exporter, print, stream);
+ }
+
+ /**
+ * Render a report in XLS format using the supplied report data.
+ * Writes the results to the supplied OutputStream.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param stream the OutputStream to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsXls(JasperReport report, Map parameters, Object reportData, OutputStream stream)
+ throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ render(new JRXlsExporter(), print, stream);
+ }
+
+ /**
+ * Render a report in XLS format using the supplied report data.
+ * Writes the results to the supplied OutputStream.
+ * @param report the JasperReport instance to render
+ * @param parameters the parameters to use for rendering
+ * @param stream the OutputStream to write the rendered report to
+ * @param reportData a JRDataSource, java.util.Collection
+ * or object array (converted accordingly), representing the report data to read
+ * fields from
+ * @param exporterParameters a {@link Map} of {@link JRExporterParameter exporter parameters}
+ * @throws JRException if rendering failed
+ * @see #convertReportData
+ */
+ public static void renderAsXls(JasperReport report, Map parameters, Object reportData, OutputStream stream,
+ Map exporterParameters) throws JRException {
+
+ JasperPrint print = JasperFillManager.fillReport(report, parameters, convertReportData(reportData));
+ JRXlsExporter exporter = new JRXlsExporter();
+ exporter.setParameters(exporterParameters);
+ render(exporter, print, stream);
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/package.html b/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/package.html
new file mode 100644
index 00000000000..946d17152dc
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/package.html
@@ -0,0 +1,8 @@
+
+java.io.File.
+ *
+ * java.io.File
+ * resources.
+ *
+ * org.springframework.core.io.ResourceLoader, the latter a String.
+ *
+ * @author Juergen Hoeller
+ * @since 14.03.2004
+ * @see VelocityEngineFactory#setResourceLoaderPath
+ * @see org.springframework.core.io.ResourceLoader
+ * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+ */
+public class SpringResourceLoader extends ResourceLoader {
+
+ public static final String NAME = "spring";
+
+ public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class";
+
+ public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache";
+
+ public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader";
+
+ public static final String SPRING_RESOURCE_LOADER_PATH = "spring.resource.loader.path";
+
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private org.springframework.core.io.ResourceLoader resourceLoader;
+
+ private String[] resourceLoaderPaths;
+
+
+ public void init(ExtendedProperties configuration) {
+ this.resourceLoader = (org.springframework.core.io.ResourceLoader)
+ this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER);
+ String resourceLoaderPath = (String) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER_PATH);
+ if (this.resourceLoader == null) {
+ throw new IllegalArgumentException(
+ "'resourceLoader' application attribute must be present for SpringResourceLoader");
+ }
+ if (resourceLoaderPath == null) {
+ throw new IllegalArgumentException(
+ "'resourceLoaderPath' application attribute must be present for SpringResourceLoader");
+ }
+ this.resourceLoaderPaths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
+ for (int i = 0; i < this.resourceLoaderPaths.length; i++) {
+ String path = this.resourceLoaderPaths[i];
+ if (!path.endsWith("/")) {
+ this.resourceLoaderPaths[i] = path + "/";
+ }
+ }
+ if (logger.isInfoEnabled()) {
+ logger.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader +
+ "] and resource loader paths " + Arrays.asList(this.resourceLoaderPaths));
+ }
+ }
+
+ public InputStream getResourceStream(String source) throws ResourceNotFoundException {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Looking for Velocity resource with name [" + source + "]");
+ }
+ for (int i = 0; i < this.resourceLoaderPaths.length; i++) {
+ org.springframework.core.io.Resource resource =
+ this.resourceLoader.getResource(this.resourceLoaderPaths[i] + source);
+ try {
+ return resource.getInputStream();
+ }
+ catch (IOException ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not find Velocity resource: " + resource);
+ }
+ }
+ }
+ throw new ResourceNotFoundException(
+ "Could not find resource [" + source + "] in Spring resource loader path");
+ }
+
+ public boolean isSourceModified(Resource resource) {
+ return false;
+ }
+
+ public long getLastModified(Resource resource) {
+ return 0;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactory.java b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactory.java
new file mode 100644
index 00000000000..a20789976f2
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactory.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.velocity;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.RuntimeConstants;
+
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Factory that configures a VelocityEngine. Can be used standalone,
+ * but typically you will either use {@link VelocityEngineFactoryBean}
+ * for preparing a VelocityEngine as bean reference, or
+ * {@link org.springframework.web.servlet.view.velocity.VelocityConfigurer}
+ * for web views.
+ *
+ * java.io.File,
+ * a generic SpringResourceLoader will be used under the name "spring", without
+ * modification detection.
+ * createVelocityEngine().
+ * @return the VelocityEngine instance
+ * @throws IOException if a config file wasn't found
+ * @throws VelocityException on Velocity initialization failure
+ * @see #createVelocityEngine()
+ */
+ protected VelocityEngine newVelocityEngine() throws IOException, VelocityException {
+ return new VelocityEngine();
+ }
+
+ /**
+ * Initialize a Velocity resource loader for the given VelocityEngine:
+ * either a standard Velocity FileResourceLoader or a SpringResourceLoader.
+ * createVelocityEngine().
+ * @param velocityEngine the VelocityEngine to configure
+ * @param resourceLoaderPath the path to load Velocity resources from
+ * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
+ * @see SpringResourceLoader
+ * @see #initSpringResourceLoader
+ * @see #createVelocityEngine()
+ */
+ protected void initVelocityResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
+ if (isPreferFileSystemAccess()) {
+ // Try to load via the file system, fall back to SpringResourceLoader
+ // (for hot detection of template changes, if possible).
+ try {
+ StringBuffer resolvedPath = new StringBuffer();
+ String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);
+ for (int i = 0; i < paths.length; i++) {
+ String path = paths[i];
+ Resource resource = getResourceLoader().getResource(path);
+ File file = resource.getFile(); // will fail if not resolvable in the file system
+ if (logger.isDebugEnabled()) {
+ logger.debug("Resource loader path [" + path + "] resolved to file [" + file.getAbsolutePath() + "]");
+ }
+ resolvedPath.append(file.getAbsolutePath());
+ if (i < paths.length - 1) {
+ resolvedPath.append(',');
+ }
+ }
+ velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");
+ velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");
+ velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, resolvedPath.toString());
+ }
+ catch (IOException ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Cannot resolve resource loader path [" + resourceLoaderPath +
+ "] to [java.io.File]: using SpringResourceLoader", ex);
+ }
+ initSpringResourceLoader(velocityEngine, resourceLoaderPath);
+ }
+ }
+ else {
+ // Always load via SpringResourceLoader
+ // (without hot detection of template changes).
+ if (logger.isDebugEnabled()) {
+ logger.debug("File system access not preferred: using SpringResourceLoader");
+ }
+ initSpringResourceLoader(velocityEngine, resourceLoaderPath);
+ }
+ }
+
+ /**
+ * Initialize a SpringResourceLoader for the given VelocityEngine.
+ * initVelocityResourceLoader.
+ * @param velocityEngine the VelocityEngine to configure
+ * @param resourceLoaderPath the path to load Velocity resources from
+ * @see SpringResourceLoader
+ * @see #initVelocityResourceLoader
+ */
+ protected void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
+ velocityEngine.setProperty(
+ RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME);
+ velocityEngine.setProperty(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName());
+ velocityEngine.setProperty(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true");
+ velocityEngine.setApplicationAttribute(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER, getResourceLoader());
+ velocityEngine.setApplicationAttribute(
+ SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath);
+ }
+
+ /**
+ * To be implemented by subclasses that want to to perform custom
+ * post-processing of the VelocityEngine after this FactoryBean
+ * performed its default configuration (but before VelocityEngine.init).
+ * createVelocityEngine().
+ * @param velocityEngine the current VelocityEngine
+ * @throws IOException if a config file wasn't found
+ * @throws VelocityException on Velocity initialization failure
+ * @see #createVelocityEngine()
+ * @see org.apache.velocity.app.VelocityEngine#init
+ */
+ protected void postProcessVelocityEngine(VelocityEngine velocityEngine)
+ throws IOException, VelocityException {
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactoryBean.java
new file mode 100644
index 00000000000..16baa02032f
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactoryBean.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.velocity;
+
+import java.io.IOException;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ResourceLoaderAware;
+
+/**
+ * Factory bean that configures a VelocityEngine and provides it as bean
+ * reference. This bean is intended for any kind of usage of Velocity in
+ * application code, e.g. for generating email content. For web views,
+ * VelocityConfigurer is used to set up a VelocityEngine for views.
+ *
+ * <bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
+ * <property name="resourceLoaderPath" value="/WEB-INF/velocity/"/>
+ * </bean>
+ *
+ * See the base class VelocityEngineFactory for configuration details.
+ *
+ * @author Juergen Hoeller
+ * @see #setConfigLocation
+ * @see #setVelocityProperties
+ * @see #setResourceLoaderPath
+ * @see org.springframework.web.servlet.view.velocity.VelocityConfigurer
+ */
+public class VelocityEngineFactoryBean extends VelocityEngineFactory
+ implements FactoryBean, InitializingBean, ResourceLoaderAware {
+
+ private VelocityEngine velocityEngine;
+
+
+ public void afterPropertiesSet() throws IOException, VelocityException {
+ this.velocityEngine = createVelocityEngine();
+ }
+
+
+ public Object getObject() {
+ return this.velocityEngine;
+ }
+
+ public Class getObjectType() {
+ return VelocityEngine.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+}
diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineUtils.java b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineUtils.java
new file mode 100644
index 00000000000..c888482fd45
--- /dev/null
+++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineUtils.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2002-2006 the original author or authors.
+ *
+ * Licensed 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.springframework.ui.velocity;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.VelocityException;
+
+/**
+ * Utility class for working with a VelocityEngine.
+ * Provides convenience methods to merge a Velocity template with a model.
+ *
+ * @author Juergen Hoeller
+ * @since 22.01.2004
+ */
+public abstract class VelocityEngineUtils {
+
+ private static final Log logger = LogFactory.getLog(VelocityEngineUtils.class);
+
+
+ /**
+ * Merge the specified Velocity template with the given model and write
+ * the result to the given Writer.
+ * @param velocityEngine VelocityEngine to work with
+ * @param templateLocation the location of template, relative to Velocity's
+ * resource loader path
+ * @param model the Map that contains model names as keys and model objects
+ * as values
+ * @param writer the Writer to write the result to
+ * @throws VelocityException if the template wasn't found or rendering failed
+ */
+ public static void mergeTemplate(
+ VelocityEngine velocityEngine, String templateLocation, Map model, Writer writer)
+ throws VelocityException {
+
+ try {
+ VelocityContext velocityContext = new VelocityContext(model);
+ velocityEngine.mergeTemplate(templateLocation, velocityContext, writer);
+ }
+ catch (VelocityException ex) {
+ throw ex;
+ }
+ catch (RuntimeException ex) {
+ throw ex;
+ }
+ catch (Exception ex) {
+ logger.error("Why does VelocityEngine throw a generic checked exception, after all?", ex);
+ throw new VelocityException(ex.toString());
+ }
+ }
+
+ /**
+ * Merge the specified Velocity template with the given model and write
+ * the result to the given Writer.
+ * @param velocityEngine VelocityEngine to work with
+ * @param templateLocation the location of template, relative to Velocity's
+ * resource loader path
+ * @param encoding the encoding of the template file
+ * @param model the Map that contains model names as keys and model objects
+ * as values
+ * @param writer the Writer to write the result to
+ * @throws VelocityException if the template wasn't found or rendering failed
+ */
+ public static void mergeTemplate(
+ VelocityEngine velocityEngine, String templateLocation, String encoding, Map model, Writer writer)
+ throws VelocityException {
+
+ try {
+ VelocityContext velocityContext = new VelocityContext(model);
+ velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);
+ }
+ catch (VelocityException ex) {
+ throw ex;
+ }
+ catch (RuntimeException ex) {
+ throw ex;
+ }
+ catch (Exception ex) {
+ logger.error("Why does VelocityEngine throw a generic checked exception, after all?", ex);
+ throw new VelocityException(ex.toString());
+ }
+ }
+
+ /**
+ * Merge the specified Velocity template with the given model into a String.
+ *