From 4df7d71c1e72d8a31892f58c77d6b47a14014961 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Sun, 26 Oct 2008 17:08:38 +0000 Subject: [PATCH] Initial import of context support module --- org.springframework.context.support/build.xml | 6 + org.springframework.context.support/ivy.xml | 43 + org.springframework.context.support/pom.xml | 141 +++ .../cache/ehcache/EhCacheFactoryBean.java | 313 +++++ .../ehcache/EhCacheManagerFactoryBean.java | 143 +++ .../cache/ehcache/package.html | 10 + .../mail/MailAuthenticationException.java | 52 + .../springframework/mail/MailException.java | 45 + .../org/springframework/mail/MailMessage.java | 60 + .../mail/MailParseException.java | 52 + .../mail/MailPreparationException.java | 51 + .../mail/MailSendException.java | 172 +++ .../org/springframework/mail/MailSender.java | 58 + .../mail/SimpleMailMessage.java | 258 ++++ .../javamail/ConfigurableMimeFileTypeMap.java | 183 +++ .../mail/javamail/InternetAddressEditor.java | 58 + .../mail/javamail/JavaMailSender.java | 143 +++ .../mail/javamail/JavaMailSenderImpl.java | 437 +++++++ .../mail/javamail/MimeMailMessage.java | 175 +++ .../mail/javamail/MimeMessageHelper.java | 1086 +++++++++++++++++ .../mail/javamail/MimeMessagePreparator.java | 53 + .../mail/javamail/SmartMimeMessage.java | 72 ++ .../springframework/mail/javamail/mime.types | 306 +++++ .../mail/javamail/package.html | 9 + .../org/springframework/mail/package.html | 8 + .../commonj/DelegatingTimerListener.java | 54 + .../scheduling/commonj/DelegatingWork.java | 81 ++ .../commonj/ScheduledTimerListener.java | 225 ++++ .../commonj/TimerManagerFactoryBean.java | 252 ++++ .../commonj/WorkManagerTaskExecutor.java | 198 +++ .../scheduling/commonj/package.html | 8 + .../quartz/AdaptableJobFactory.java | 79 ++ .../scheduling/quartz/CronTriggerBean.java | 153 +++ .../scheduling/quartz/DelegatingJob.java | 67 + .../quartz/JobDetailAwareTrigger.java | 47 + .../scheduling/quartz/JobDetailBean.java | 155 +++ .../JobMethodInvocationFailedException.java | 43 + .../quartz/LocalDataSourceJobStore.java | 141 +++ .../quartz/LocalTaskExecutorThreadPool.java | 84 ++ .../MethodInvokingJobDetailFactoryBean.java | 291 +++++ .../scheduling/quartz/QuartzJobBean.java | 97 ++ .../quartz/ResourceLoaderClassLoadHelper.java | 110 ++ .../scheduling/quartz/SchedulerAccessor.java | 407 ++++++ .../quartz/SchedulerAccessorBean.java | 109 ++ .../quartz/SchedulerContextAware.java | 42 + .../quartz/SchedulerFactoryBean.java | 728 +++++++++++ .../quartz/SimpleThreadPoolTaskExecutor.java | 83 ++ .../scheduling/quartz/SimpleTriggerBean.java | 172 +++ .../quartz/SpringBeanJobFactory.java | 108 ++ .../scheduling/quartz/package.html | 11 + .../FreeMarkerConfigurationFactory.java | 422 +++++++ .../FreeMarkerConfigurationFactoryBean.java | 76 ++ .../freemarker/FreeMarkerTemplateUtils.java | 53 + .../ui/freemarker/SpringTemplateLoader.java | 98 ++ .../ui/freemarker/package.html | 9 + .../ui/jasperreports/JasperReportsUtils.java | 278 +++++ .../ui/jasperreports/package.html | 8 + .../ui/velocity/CommonsLoggingLogSystem.java | 57 + .../ui/velocity/SpringResourceLoader.java | 124 ++ .../ui/velocity/VelocityEngineFactory.java | 376 ++++++ .../velocity/VelocityEngineFactoryBean.java | 73 ++ .../ui/velocity/VelocityEngineUtils.java | 149 +++ .../springframework/ui/velocity/package.html | 9 + .../src/main/java/overview.html | 7 + .../src/test/resources/log4j.xml | 28 + .../template.mf | 31 + 66 files changed, 9477 insertions(+) create mode 100644 org.springframework.context.support/build.xml create mode 100644 org.springframework.context.support/ivy.xml create mode 100644 org.springframework.context.support/pom.xml create mode 100644 org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailAuthenticationException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailMessage.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailParseException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailPreparationException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailSendException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/MailSender.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/SimpleMailMessage.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/mime.types create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/javamail/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/mail/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/CronTriggerBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailAwareTrigger.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobDetailBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/JasperReportsUtils.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/package.html create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/CommonsLoggingLogSystem.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/SpringResourceLoader.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactory.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineFactoryBean.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/VelocityEngineUtils.java create mode 100644 org.springframework.context.support/src/main/java/org/springframework/ui/velocity/package.html create mode 100644 org.springframework.context.support/src/main/java/overview.html create mode 100644 org.springframework.context.support/src/test/resources/log4j.xml create mode 100644 org.springframework.context.support/template.mf diff --git a/org.springframework.context.support/build.xml b/org.springframework.context.support/build.xml new file mode 100644 index 0000000000..643fa1f7b8 --- /dev/null +++ b/org.springframework.context.support/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.context.support/ivy.xml b/org.springframework.context.support/ivy.xml new file mode 100644 index 0000000000..cd3edd5fe4 --- /dev/null +++ b/org.springframework.context.support/ivy.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.context.support/pom.xml b/org.springframework.context.support/pom.xml new file mode 100644 index 0000000000..d9c332b8c3 --- /dev/null +++ b/org.springframework.context.support/pom.xml @@ -0,0 +1,141 @@ + + + + org.springframework + org.springframework.parent + 3.0-M1-SNAPSHOT + + 4.0.0 + org.springframework.context.support + jar + Spring Framework: Context Support + + + org.springframework + org.springframework.core + + + org.springframework + org.springframework.context + + + org.springframework + org.springframework.jdbc + true + + + org.springframework + org.springframework.transaction + true + + + javax.mail + com.springsource.javax.mail + true + + + org.apache.velocity + com.springsource.org.apache.velocity + true + + + org.freemarker + com.springsource.freemarker + true + + + net.sourceforge.jasperreports + com.springsource.net.sf.jasperreports + true + + + com.bea.commonj + com.springsource.commonj + true + + + com.opensymphony.quartz + com.springsource.org.quartz + true + + + net.sourceforge.ehcache + com.springsource.net.sf.ehcache + true + + + + \ No newline at end of file diff --git a/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java new file mode 100644 index 0000000000..67c917250e --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java @@ -0,0 +1,313 @@ +/* + * 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.cache.ehcache; + +import java.io.IOException; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.constructs.blocking.BlockingCache; +import net.sf.ehcache.constructs.blocking.CacheEntryFactory; +import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; +import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; +import net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache; +import net.sf.ehcache.store.MemoryStoreEvictionPolicy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * FactoryBean that creates a named EHCache {@link net.sf.ehcache.Cache} instance + * (or a decorator that implements the {@link net.sf.ehcache.Ehcache} interface), + * representing a cache region within an EHCache {@link net.sf.ehcache.CacheManager}. + * + *

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 0000000000..85f1da4e30 --- /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 0000000000..f05383fe8c --- /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 0000000000..6a54761ebc --- /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 0000000000..407a42a838 --- /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 0000000000..c33f0e76b2 --- /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 0000000000..b09676e22c --- /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 0000000000..7068c0f94c --- /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 0000000000..c12cb88f67 --- /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 0000000000..390b950871 --- /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 0000000000..8ab24e2854 --- /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 0000000000..9010d78e9a --- /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 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. + *

Needs to follow the 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. + *

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 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 0000000000..7dfada99a0 --- /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. + * + *

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 0000000000..ea2a0efbd0 --- /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 + * 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. + *

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 0000000000..83f78f3765 --- /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 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. + *

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 Session. + *

A new Session will be created with those properties. + * Use either this method or {@link #setSession}, but not both. + *

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 Session, possibly pulled from JNDI. + *

Default is a new Session without defaults, that is + * completely configured via this instance's properties. + *

If using a pre-configured 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. + *

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 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. + *

Note that the underlying JavaMail 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. + *

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 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. + *

A FileTypeMap specified here will be autodetected by + * {@link MimeMessageHelper}, avoiding the need to specify the + * FileTypeMap for each MimeMessageHelper instance. + *

For example, you can specify a custom instance of Spring's + * {@link ConfigurableMimeFileTypeMap} here. If not explicitly specified, + * a default 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. + *

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 0000000000..242f0e5f27 --- /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 0000000000..b30a4a9efc --- /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: + * + *

+ * 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. + * + *

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 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. + *

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 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. + *

Default is the 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". + *

Note that this is by default just available for JavaMail >= 1.3. + * You can override the default 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. + *

Default implementation invokes InternetAddress.validate(), + * provided that address validation is activated for the helper instance. + *

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 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". + *

NOTE: Invoke {@link #addInline} after 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. + *

NOTE: Invoke {@link #addInline} after 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. + *

NOTE: Invoke {@link #addInline} after 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. + *

Note that the InputStream returned by the DataSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * getInputStream() multiple times. + *

NOTE: Invoke 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. + *

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 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. + *

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 + * getInputStream() multiple times. + *

NOTE: Invoke 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. + *

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 + * getInputStream() multiple times. + *

NOTE: Invoke 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. + *

Note that the InputStream returned by the DataSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * 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. + *

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 + * org.springframework.core.io.InputStreamResource. + *

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 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. + *

Note that the InputStream returned by the InputStreamSource + * implementation needs to be a fresh one on each call, as + * JavaMail will invoke 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 0000000000..8ac0e8be2e --- /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. + * + *

The corresponding send methods of {@link JavaMailSender} + * will take care of the actual creation of a {@link MimeMessage} instance, + * and of proper exception conversion. + * + *

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 0000000000..6a3bcd4957 --- /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 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 0000000000..182357ea73 --- /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 0000000000..da49cc0716 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/javamail/package.html @@ -0,0 +1,9 @@ + + + +JavaMail support for Spring's mail infrastructure. +Provides an extended JavaMailSender interface and a MimeMessageHelper +class for convenient population of a JavaMail MimeMessage. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/mail/package.html b/org.springframework.context.support/src/main/java/org/springframework/mail/package.html new file mode 100644 index 0000000000..3cfdb11305 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/mail/package.html @@ -0,0 +1,8 @@ + + + +Spring's generic mail infrastructure. +Concrete implementations are provided in the subpackages. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java new file mode 100644 index 0000000000..853ea465f2 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java @@ -0,0 +1,54 @@ +/* + * 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.commonj; + +import commonj.timers.Timer; +import commonj.timers.TimerListener; + +import org.springframework.util.Assert; + +/** + * Simple TimerListener adapter that delegates to a given Runnable. + * + * @author Juergen Hoeller + * @since 2.0 + * @see commonj.timers.TimerListener + * @see java.lang.Runnable + */ +public class DelegatingTimerListener implements TimerListener { + + private final Runnable runnable; + + + /** + * Create a new DelegatingTimerListener. + * @param runnable the Runnable implementation to delegate to + */ + public DelegatingTimerListener(Runnable runnable) { + Assert.notNull(runnable, "Runnable is required"); + this.runnable = runnable; + } + + + /** + * Delegates execution to the underlying Runnable. + */ + public void timerExpired(Timer timer) { + this.runnable.run(); + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java new file mode 100644 index 0000000000..6566dc7892 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java @@ -0,0 +1,81 @@ +/* + * 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 commonj.work.Work; + +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.util.Assert; + +/** + * Simple Work adapter that delegates to a given Runnable. + * + * @author Juergen Hoeller + * @since 2.0 + * @see commonj.work.Work + * @see java.lang.Runnable + */ +public class DelegatingWork implements Work { + + private final Runnable delegate; + + + /** + * Create a new DelegatingWork. + * @param delegate the Runnable implementation to delegate to + * (may be a SchedulingAwareRunnable for extended support) + * @see org.springframework.scheduling.SchedulingAwareRunnable + * @see #isDaemon() + */ + public DelegatingWork(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 run() { + this.delegate.run(); + } + + /** + * This implementation delegates to + * {@link org.springframework.scheduling.SchedulingAwareRunnable#isLongLived()}, + * if available. + */ + public boolean isDaemon() { + return (this.delegate instanceof SchedulingAwareRunnable && + ((SchedulingAwareRunnable) this.delegate).isLongLived()); + } + + /** + * This implementation is empty, since we expect the Runnable + * to terminate based on some specific shutdown signal. + */ + public void release() { + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java new file mode 100644 index 0000000000..34a61b8f17 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java @@ -0,0 +1,225 @@ +/* + * 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 commonj.timers.TimerListener; + +/** + * JavaBean that describes a scheduled TimerListener, consisting of + * the TimerListener itself (or a Runnable to create a TimerListener for) + * and a delay plus period. Period needs to be specified; + * there is no point in a default for it. + * + *

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 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. + *

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 0000000000..572b2d9677 --- /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 + * resource-ref of type commonj.timers.TimerManager + * in web.xml, with res-sharing-scope set to 'Unshareable'. + *

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 0000000000..9030dc28ab --- /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 + * 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. + *

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 0000000000..70e1354dd5 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/commonj/package.html @@ -0,0 +1,8 @@ + + + +Convenience classes for scheduling based on the CommonJ WorkManager/TimerManager +facility, as supported by IBM WebSphere 6.0+ and BEA WebLogic 9.0+. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java new file mode 100644 index 0000000000..8ebf83e97e --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java @@ -0,0 +1,79 @@ +/* + * 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.Job; +import org.quartz.SchedulerException; +import org.quartz.spi.JobFactory; +import org.quartz.spi.TriggerFiredBundle; + +/** + * JobFactory implementation that supports {@link java.lang.Runnable} + * objects as well as standard Quartz {@link org.quartz.Job} instances. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DelegatingJob + * @see #adaptJob(Object) + */ +public class AdaptableJobFactory implements JobFactory { + + public Job newJob(TriggerFiredBundle bundle) throws SchedulerException { + try { + Object jobObject = createJobInstance(bundle); + return adaptJob(jobObject); + } + catch (Exception ex) { + throw new SchedulerException("Job instantiation failed", ex); + } + } + + /** + * Create an instance of the specified job class. + *

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 0000000000..817c697a11 --- /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 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. + *

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 0000000000..bcec0e566a --- /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 0000000000..3da4c9c271 --- /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 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 0000000000..07c54e4061 --- /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. + *

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 0000000000..2b04f76d8d --- /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 0000000000..f4cc729318 --- /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 0000000000..bc41625baa --- /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 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 0000000000..d760307a5c --- /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. + * + *

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 0000000000..0288224140 --- /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 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 0000000000..18593865af --- /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 0000000000..04ea6c3253 --- /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. + * + *

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 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 0000000000..7b53354019 --- /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}. + *

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 0000000000..72f71f6501 --- /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 0000000000..4ff102fd66 --- /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 + * (org.quartz.Scheduler). This allows you to create new jobs + * and triggers, and also to control and monitor the entire 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 + * 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. + *

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 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 0000000000..0152b88d5a --- /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. + * + *

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 0000000000..d91f34e6b9 --- /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 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. + *

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 0000000000..9a60493095 --- /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 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. + *

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 0000000000..612d2738f7 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/scheduling/quartz/package.html @@ -0,0 +1,11 @@ + + + +Support classes for the open source scheduler +Quartz, +allowing to set up Quartz Schedulers, JobDetails and +Triggers as beans in a Spring context. Also provides +convenience classes for implementing Quartz Jobs. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java new file mode 100644 index 0000000000..f5c30167e8 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -0,0 +1,422 @@ +/* + * 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.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import freemarker.cache.FileTemplateLoader; +import freemarker.cache.MultiTemplateLoader; +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.SimpleHash; +import freemarker.template.TemplateException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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.CollectionUtils; + +/** + * Factory that configures a FreeMarker Configuration. Can be used standalone, but + * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a + * Configuration as bean reference, or FreeMarkerConfigurer for web views. + * + *

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 Configuration.setSettings() method and are + * subject to constraints set by FreeMarker. + * + *

The "freemarkerVariables" property can be used to specify a Map of + * shared variables that will be applied to the Configuration via the + * setAllSharedVariables() method. Like setSettings(), + * these entries are subject to FreeMarker constraints. + * + *

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 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. + *

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 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. + *

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 TemplateLoaders that will be used to search + * for templates. For example, one or more custom loaders such as database + * loaders can be configured. + *

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 java.io.File, + * a generic SpringTemplateLoader will be used, without modification detection. + *

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 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. + *

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 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. + *

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 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. + *

Called by 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 0000000000..0f5935628a --- /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. + * + *

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 0000000000..f4ba067752 --- /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 0000000000..1fce80718e --- /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 0000000000..156e4511d0 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/freemarker/package.html @@ -0,0 +1,9 @@ + + + +Support classes for setting up +FreeMarker +within a Spring application context. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/JasperReportsUtils.java b/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/JasperReportsUtils.java new file mode 100644 index 0000000000..f05bd2a8a0 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/JasperReportsUtils.java @@ -0,0 +1,278 @@ +/* + * 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.ui.jasperreports; + +import java.io.OutputStream; +import java.io.Writer; +import java.util.Collection; +import java.util.Map; + +import net.sf.jasperreports.engine.JRDataSource; +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRExporter; +import net.sf.jasperreports.engine.JRExporterParameter; +import net.sf.jasperreports.engine.JasperFillManager; +import net.sf.jasperreports.engine.JasperPrint; +import net.sf.jasperreports.engine.JasperReport; +import net.sf.jasperreports.engine.data.JRBeanArrayDataSource; +import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; +import net.sf.jasperreports.engine.export.JRCsvExporter; +import net.sf.jasperreports.engine.export.JRHtmlExporter; +import net.sf.jasperreports.engine.export.JRPdfExporter; +import net.sf.jasperreports.engine.export.JRXlsExporter; + +/** + * Utility methods for working with JasperReports. Provides a set of convenience + * methods for generating reports in a CSV, HTML, PDF and XLS formats. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.1.3 + */ +public abstract class JasperReportsUtils { + + /** + * Convert the given report data value to a JRDataSource. + *

In the default implementation, a 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. + *

Make sure that the 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. + *

Make sure that the 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 0000000000..946d17152d --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/jasperreports/package.html @@ -0,0 +1,8 @@ + + + +Support classes for +JasperReports. + + + diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/CommonsLoggingLogSystem.java b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/CommonsLoggingLogSystem.java new file mode 100644 index 0000000000..5c49db1efe --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/CommonsLoggingLogSystem.java @@ -0,0 +1,57 @@ +/* + * 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.velocity; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.log.LogSystem; + +/** + * Velocity LogSystem implementation for Jakarta Commons Logging. + * Used by VelocityConfigurer to redirect log output. + * + * @author Juergen Hoeller + * @since 07.08.2003 + * @see VelocityEngineFactoryBean + */ +public class CommonsLoggingLogSystem implements LogSystem { + + private static final Log logger = LogFactory.getLog(VelocityEngine.class); + + public void init(RuntimeServices runtimeServices) { + } + + public void logVelocityMessage(int type, String msg) { + switch (type) { + case ERROR_ID: + logger.error(msg); + break; + case WARN_ID: + logger.warn(msg); + break; + case INFO_ID: + logger.info(msg); + break; + case DEBUG_ID: + logger.debug(msg); + break; + } + } + +} diff --git a/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/SpringResourceLoader.java b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/SpringResourceLoader.java new file mode 100644 index 0000000000..dd856dc568 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/SpringResourceLoader.java @@ -0,0 +1,124 @@ +/* + * 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 java.io.InputStream; +import java.util.Arrays; + +import org.apache.commons.collections.ExtendedProperties; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; + +import org.springframework.util.StringUtils; + +/** + * Velocity ResourceLoader adapter that loads via a Spring ResourceLoader. + * Used by VelocityEngineFactory 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 Velocity's default FileResourceLoader for java.io.File + * resources. + * + *

Expects "spring.resource.loader" and "spring.resource.loader.path" + * application attributes in the Velocity runtime: the former of type + * 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 0000000000..a20789976f --- /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. + * + *

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 java.io.File, + * a generic SpringResourceLoader will be used under the name "spring", without + * modification detection. + *

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 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. + *

Called by 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. + *

Called by 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). + *

Called by 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 0000000000..16baa02032 --- /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. + * + *

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: + * + *

 <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 0000000000..c888482fd4 --- /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. + *

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 0000000000..9bd3dc90a7 --- /dev/null +++ b/org.springframework.context.support/src/main/java/org/springframework/ui/velocity/package.html @@ -0,0 +1,9 @@ + + + +Support classes for setting up +Velocity +within a Spring application context. + + + diff --git a/org.springframework.context.support/src/main/java/overview.html b/org.springframework.context.support/src/main/java/overview.html new file mode 100644 index 0000000000..1eb7a2e8c1 --- /dev/null +++ b/org.springframework.context.support/src/main/java/overview.html @@ -0,0 +1,7 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.context.support/src/test/resources/log4j.xml b/org.springframework.context.support/src/test/resources/log4j.xml new file mode 100644 index 0000000000..767b96d620 --- /dev/null +++ b/org.springframework.context.support/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.context.support/template.mf b/org.springframework.context.support/template.mf new file mode 100644 index 0000000000..0538604c5b --- /dev/null +++ b/org.springframework.context.support/template.mf @@ -0,0 +1,31 @@ +Bundle-SymbolicName: org.springframework.context.support +Bundle-Name: Spring Context Support +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + commonj.*;version="[1.1.0, 2.0.0)";resolution:=optional, + freemarker.*;version="[2.3.12, 3.0.0)";resolution:=optional, + javax.activation.*;version="[1.1.0, 2.0.0)";resolution:=optional, + javax.mail.*;version="[1.4.0, 2.0.0)";resolution:=optional, + net.sf.ehcache.*;version="[1.3.0, 2.0.0)";resolution:=optional, + net.sf.jasperreports.*;version="[2.0.5, 3.0.0)";resolution:=optional, + org.apache.commons.collections.*;version="[3.2.0, 4.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)";resolution:=optional, + org.apache.velocity.*;version="[1.5.0, 2.0.0)";resolution:=optional, + org.quartz.*;version="[1.6.0, 2.0.0)";resolution:=optional, + org.springframework.beans.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.context.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.core.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.jdbc.datasource.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.jndi.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.scheduling.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.transaction.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.util.*;version="[2.5.5.A, 2.5.5.A]" +Unversioned-Imports: + javax.naming.*, + javax.sql.* +Ignored-Existing-Headers: + Bnd-LastModified, + Import-Package, + Export-Package, + Tool -- GitLab