diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContext.java new file mode 100644 index 00000000000..16e9f36b6cd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContext.java @@ -0,0 +1,103 @@ +/* + * 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.context; + +import org.springframework.beans.factory.HierarchicalBeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.core.io.support.ResourcePatternResolver; + +/** + * Central interface to provide configuration for an application. + * This is read-only while the application is running, but may be + * reloaded if the implementation supports this. + * + *

An ApplicationContext provides: + *

+ * + *

In addition to standard {@link org.springframework.beans.factory.BeanFactory} + * lifecycle capabilities, ApplicationContext implementations detect and invoke + * {@link ApplicationContextAware} beans as well as {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware} and {@link MessageSourceAware} beans. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see ConfigurableApplicationContext + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.core.io.ResourceLoader + */ +public interface ApplicationContext extends ListableBeanFactory, HierarchicalBeanFactory, + MessageSource, ApplicationEventPublisher, ResourcePatternResolver { + + /** + * Return the unique id of this application context. + * @return the unique id of the context + */ + String getId(); + + /** + * Return a friendly name for this context. + * @return a display name for this context + */ + String getDisplayName(); + + /** + * Return the timestamp when this context was first loaded. + * @return the timestamp (ms) when this context was first loaded + */ + long getStartupDate(); + + /** + * Return the parent context, or null if there is no parent + * and this is the root of the context hierarchy. + * @return the parent context, or null if there is no parent + */ + ApplicationContext getParent(); + + /** + * Expose AutowireCapableBeanFactory functionality for this context. + *

This is not typically used by application code, except for the purpose + * of initializing bean instances that live outside the application context, + * applying the Spring bean lifecycle (fully or partly) to them. + *

Alternatively, the internal BeanFactory exposed by the + * {@link ConfigurableApplicationContext} interface offers access to the + * AutowireCapableBeanFactory interface too. The present method mainly + * serves as convenient, specific facility on the ApplicationContext + * interface itself. + * @return the AutowireCapableBeanFactory for this context + * @throws IllegalStateException if the context does not support + * the AutowireCapableBeanFactory interface or does not hold an autowire-capable + * bean factory yet (usually if refresh() has never been called) + * @see ConfigurableApplicationContext#refresh() + * @see ConfigurableApplicationContext#getBeanFactory() + */ + AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextAware.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextAware.java new file mode 100644 index 00000000000..f26919e81a9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextAware.java @@ -0,0 +1,74 @@ +/* + * 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.context; + +import org.springframework.beans.BeansException; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the {@link ApplicationContext} that it runs in. + * + *

Implementing this interface makes sense for example when an object + * requires access to a set of collaborating beans. Note that configuration + * via bean references is preferable to implementing this interface just + * for bean lookup purposes. + * + *

This interface can also be implemented if an object needs access to file + * resources, i.e. wants to call getResource, wants to publish + * an application event, or requires access to the MessageSource. However, + * it is preferable to implement the more specific {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware} or {@link MessageSourceAware} interface + * in such a specific scenario. + * + *

Note that file resource dependencies can also be exposed as bean properties + * of type {@link org.springframework.core.io.Resource}, populated via Strings + * with automatic type conversion by the bean factory. This removes the need + * for implementing any callback interface just for the purpose of accessing + * a specific file resource. + * + *

{@link org.springframework.context.support.ApplicationObjectSupport} is a + * convenience base class for application objects, implementing this interface. + * + *

For a list of all bean lifecycle methods, see the + * {@link org.springframework.beans.factory.BeanFactory BeanFactory javadocs}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see ResourceLoaderAware + * @see ApplicationEventPublisherAware + * @see MessageSourceAware + * @see org.springframework.context.support.ApplicationObjectSupport + * @see org.springframework.beans.factory.BeanFactoryAware + */ +public interface ApplicationContextAware { + + /** + * Set the ApplicationContext that this object runs in. + * Normally this call will be used to initialize the object. + *

Invoked after population of normal bean properties but before an init callback such + * as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()} + * or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader}, + * {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and + * {@link MessageSourceAware}, if applicable. + * @param applicationContext the ApplicationContext object to be used by this object + * @throws ApplicationContextException in case of context initialization errors + * @throws BeansException if thrown by application context methods + * @see org.springframework.beans.factory.BeanInitializationException + */ + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextException.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextException.java new file mode 100644 index 00000000000..9c9f4066a4d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationContextException.java @@ -0,0 +1,47 @@ +/* + * 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.context; + +import org.springframework.beans.FatalBeanException; + +/** + * Exception thrown during application context initialization. + * + * @author Rod Johnson + */ +public class ApplicationContextException extends FatalBeanException { + + /** + * Create a new ApplicationContextException + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public ApplicationContextException(String msg) { + super(msg); + } + + /** + * Create a new ApplicationContextException + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause + */ + public ApplicationContextException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationEvent.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEvent.java new file mode 100644 index 00000000000..66bddfcfd28 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEvent.java @@ -0,0 +1,54 @@ +/* + * 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.context; + +import java.util.EventObject; + +/** + * Class to be extended by all application events. Abstract as it + * doesn't make sense for generic events to be published directly. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class ApplicationEvent extends EventObject { + + /** use serialVersionUID from Spring 1.2 for interoperability */ + private static final long serialVersionUID = 7099057708183571937L; + + /** System time when the event happened */ + private final long timestamp; + + + /** + * Create a new ApplicationEvent. + * @param source the component that published the event (never null) + */ + public ApplicationEvent(Object source) { + super(source); + this.timestamp = System.currentTimeMillis(); + } + + + /** + * Return the system time in milliseconds when the event happened. + */ + public final long getTimestamp() { + return this.timestamp; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisher.java new file mode 100644 index 00000000000..f92a26ef7a1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisher.java @@ -0,0 +1,41 @@ +/* + * 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.context; + +/** + * Interface that encapsulates event publication functionality. + * Serves as super-interface for ApplicationContext. + * + * @author Juergen Hoeller + * @since 1.1.1 + * @see ApplicationContext + * @see ApplicationEventPublisherAware + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.event.EventPublicationInterceptor + */ +public interface ApplicationEventPublisher { + + /** + * Notify all listeners registered with this application of an application + * event. Events may be framework events (such as RequestHandledEvent) + * or application-specific events. + * @param event the event to publish + * @see org.springframework.web.context.support.RequestHandledEvent + */ + void publishEvent(ApplicationEvent event); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java new file mode 100644 index 00000000000..e0a0c5a4e8e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java @@ -0,0 +1,39 @@ +/* + * 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.context; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the ApplicationEventPublisher (typically the ApplicationContext) + * that it runs in. + * + * @author Juergen Hoeller + * @since 1.1.1 + * @see ApplicationContextAware + */ +public interface ApplicationEventPublisherAware { + + /** + * Set the ApplicationEventPublisher that this object runs in. + *

Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param applicationEventPublisher event publisher to be used by this object + */ + void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ApplicationListener.java b/org.springframework.context/src/main/java/org/springframework/context/ApplicationListener.java new file mode 100644 index 00000000000..5446d75246e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ApplicationListener.java @@ -0,0 +1,37 @@ +/* + * 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.context; + +import java.util.EventListener; + +/** + * Interface to be implemented by application event listeners. + * Based on the standard java.util.EventListener interface + * for the Observer design pattern. + * + * @author Rod Johnson + * @see org.springframework.context.event.ApplicationEventMulticaster + */ +public interface ApplicationListener extends EventListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + void onApplicationEvent(ApplicationEvent event); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java new file mode 100644 index 00000000000..e5266d0bdf9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -0,0 +1,149 @@ +/* + * 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.context; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +/** + * SPI interface to be implemented by most if not all application contexts. + * Provides facilities to configure an application context in addition + * to the application context client methods in the + * {@link org.springframework.context.ApplicationContext} interface. + * + *

Configuration and lifecycle methods are encapsulated here to avoid + * making them obvious to ApplicationContext client code. The present + * methods should only be used by startup and shutdown code. + * + * @author Juergen Hoeller + * @since 03.11.2003 + */ +public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle { + + /** + * Any number of these characters are considered delimiters between + * multiple context config paths in a single String value. + * @see org.springframework.context.support.AbstractXmlApplicationContext#setConfigLocation + * @see org.springframework.web.context.ContextLoader#CONFIG_LOCATION_PARAM + * @see org.springframework.web.servlet.FrameworkServlet#setContextConfigLocation + */ + String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; + + /** + * Name of the LoadTimeWeaver bean in the factory. If such a bean is supplied, + * the context will use a temporary ClassLoader for type matching, in order + * to allow the LoadTimeWeaver to process all actual bean classes. + * @see org.springframework.instrument.classloading.LoadTimeWeaver + */ + String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver"; + + + /** + * Set the parent of this application context. + *

Note that the parent shouldn't be changed: It should only be set outside + * a constructor if it isn't available when an object of this class is created, + * for example in case of WebApplicationContext setup. + * @param parent the parent context + * @see org.springframework.web.context.ConfigurableWebApplicationContext + */ + void setParent(ApplicationContext parent); + + /** + * Add a new BeanFactoryPostProcessor that will get applied to the internal + * bean factory of this application context on refresh, before any of the + * bean definitions get evaluated. To be invoked during context configuration. + * @param beanFactoryPostProcessor the factory processor to register + */ + void addBeanFactoryPostProcessor(BeanFactoryPostProcessor beanFactoryPostProcessor); + + /** + * Add a new ApplicationListener that will be notified on context events + * such as context refresh and context shutdown. + *

Note that any ApplicationListener registered here will be applied + * on refresh of this context. If a listener is added after the initial + * refresh, it will be applied on next refresh of the context. + * @param listener the ApplicationListener to register + * @see org.springframework.context.event.ContextRefreshedEvent + * @see org.springframework.context.event.ContextClosedEvent + */ + void addApplicationListener(ApplicationListener listener); + + /** + * Load or refresh the persistent representation of the configuration, + * which might an XML file, properties file, or relational database schema. + *

As this is a startup method, it should destroy already created singletons + * if it fails, to avoid dangling resources. In other words, after invocation + * of that method, either all or no singletons at all should be instantiated. + * @throws BeansException if the bean factory could not be initialized + * @throws IllegalStateException if already initialized and multiple refresh + * attempts are not supported + */ + void refresh() throws BeansException, IllegalStateException; + + /** + * Register a shutdown hook with the JVM runtime, closing this context + * on JVM shutdown unless it has already been closed at that time. + *

This method can be called multiple times. Only one shutdown hook + * (at max) will be registered for each context instance. + * @see java.lang.Runtime#addShutdownHook + * @see #close() + */ + void registerShutdownHook(); + + /** + * Close this application context, releasing all resources and locks that the + * implementation might hold. This includes destroying all cached singleton beans. + *

Note: Does not invoke close on a parent context; + * parent contexts have their own, independent lifecycle. + *

This method can be called multiple times without side effects: Subsequent + * close calls on an already closed context will be ignored. + */ + void close(); + + /** + * Determine whether this application context is active, that is, + * whether it has been refreshed at least once and has not been closed yet. + * @return whether the context is still active + * @see #refresh() + * @see #close() + * @see #getBeanFactory() + */ + boolean isActive(); + + /** + * Return the internal bean factory of this application context. + * Can be used to access specific functionality of the underlying factory. + *

Note: Do not use this to post-process the bean factory; singletons + * will already have been instantiated before. Use a BeanFactoryPostProcessor + * to intercept the BeanFactory setup process before beans get touched. + *

Generally, this internal factory will only be accessible while the context + * is active, that is, inbetween {@link #refresh()} and {@link #close()}. + * The {@link #isActive()} flag can be used to check whether the context + * is in an appropriate state. + * @return the underlying bean factory + * @throws IllegalStateException if the context does not hold an internal + * bean factory (usually if {@link #refresh()} hasn't been called yet or + * if {@link #close()} has already been called) + * @see #isActive() + * @see #refresh() + * @see #close() + * @see #addBeanFactoryPostProcessor + */ + ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/HierarchicalMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/HierarchicalMessageSource.java new file mode 100644 index 00000000000..45ef4243c76 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/HierarchicalMessageSource.java @@ -0,0 +1,42 @@ +/* + * 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.context; + +/** + * Sub-interface of MessageSource to be implemented by objects that + * can resolve messages hierarchically. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface HierarchicalMessageSource extends MessageSource { + + /** + * Set the parent that will be used to try to resolve messages + * that this object can't resolve. + * @param parent the parent MessageSource that will be used to + * resolve messages that this object can't resolve. + * May be null, in which case no further resolution is possible. + */ + void setParentMessageSource(MessageSource parent); + + /** + * Return the parent of this MessageSource, or null if none. + */ + MessageSource getParentMessageSource(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/Lifecycle.java b/org.springframework.context/src/main/java/org/springframework/context/Lifecycle.java new file mode 100644 index 00000000000..112aebcea5f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/Lifecycle.java @@ -0,0 +1,67 @@ +/* + * 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.context; + +/** + * Interface defining methods for start/stop lifecycle control. + * The typical use case for this is to control asynchronous processing. + * + *

Can be implemented by both components (typically a Spring bean defined in + * a Spring {@link org.springframework.beans.factory.BeanFactory}) and containers + * (typically a Spring {@link ApplicationContext}). Containers will propagate + * start/stop signals to all components that apply. + * + *

Can be used for direct invocations or for management operations via JMX. + * In the latter case, the {@link org.springframework.jmx.export.MBeanExporter} + * will typically be defined with an + * {@link org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler}, + * restricting the visibility of activity-controlled components to the Lifecycle + * interface. + * + * @author Juergen Hoeller + * @since 2.0 + * @see ConfigurableApplicationContext + * @see org.springframework.jms.listener.AbstractMessageListenerContainer + * @see org.springframework.scheduling.quartz.SchedulerFactoryBean + */ +public interface Lifecycle { + + /** + * Start this component. + * Should not throw an exception if the component is already running. + *

In the case of a container, this will propagate the start signal + * to all components that apply. + */ + void start(); + + /** + * Stop this component. + * Should not throw an exception if the component isn't started yet. + *

In the case of a container, this will propagate the stop signal + * to all components that apply. + */ + void stop(); + + /** + * Check whether this component is currently running. + *

In the case of a container, this will return true + * only if all components that apply are currently running. + * @return whether the component is currently running + */ + boolean isRunning(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/MessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/MessageSource.java new file mode 100644 index 00000000000..b83ec2a014d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/MessageSource.java @@ -0,0 +1,83 @@ +/* + * 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.context; + +import java.util.Locale; + +/** + * Strategy interface for resolving messages, with support for the parameterization + * and internationalization of such messages. + * + *

Spring provides two out-of-the-box implementations for production: + *

+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.context.support.ResourceBundleMessageSource + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + */ +public interface MessageSource { + + /** + * Try to resolve the message. Return default message if no message was found. + * @param code the code to lookup up, such as 'calculator.noRateSet'. Users of + * this class are encouraged to base message names on the relevant fully + * qualified class name, thus avoiding conflict and ensuring maximum clarity. + * @param args array of arguments that will be filled in for params within + * the message (params look like "{0}", "{1,date}", "{2,time}" within a message), + * or null if none. + * @param defaultMessage String to return if the lookup fails + * @param locale the Locale in which to do the lookup + * @return the resolved message if the lookup was successful; + * otherwise the default message passed as a parameter + * @see java.text.MessageFormat + */ + String getMessage(String code, Object[] args, String defaultMessage, Locale locale); + + /** + * Try to resolve the message. Treat as an error if the message can't be found. + * @param code the code to lookup up, such as 'calculator.noRateSet' + * @param args Array of arguments that will be filled in for params within + * the message (params look like "{0}", "{1,date}", "{2,time}" within a message), + * or null if none. + * @param locale the Locale in which to do the lookup + * @return the resolved message + * @throws NoSuchMessageException if the message wasn't found + * @see java.text.MessageFormat + */ + String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException; + + /** + * Try to resolve the message using all the attributes contained within the + * MessageSourceResolvable argument that was passed in. + *

NOTE: We must throw a NoSuchMessageException on this method + * since at the time of calling this method we aren't able to determine if the + * defaultMessage property of the resolvable is null or not. + * @param resolvable value object storing attributes required to properly resolve a message + * @param locale the Locale in which to do the lookup + * @return the resolved message + * @throws NoSuchMessageException if the message wasn't found + * @see java.text.MessageFormat + */ + String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/MessageSourceAware.java b/org.springframework.context/src/main/java/org/springframework/context/MessageSourceAware.java new file mode 100644 index 00000000000..3007e274189 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/MessageSourceAware.java @@ -0,0 +1,42 @@ +/* + * 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.context; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the MessageSource (typically the ApplicationContext) that it runs in. + * + *

Note that the MessageSource can usually also be passed on as bean + * reference (to arbitrary bean properties or constructor arguments), because + * it is defined as bean with name "messageSource" in the application context. + * + * @author Juergen Hoeller + * @since 1.1.1 + * @see ApplicationContextAware + */ +public interface MessageSourceAware { + + /** + * Set the MessageSource that this object runs in. + *

Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param messageSource message sourceto be used by this object + */ + void setMessageSource(MessageSource messageSource); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/MessageSourceResolvable.java b/org.springframework.context/src/main/java/org/springframework/context/MessageSourceResolvable.java new file mode 100644 index 00000000000..d1b1f388d06 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/MessageSourceResolvable.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.context; + +/** + * Interface for objects that are suitable for message resolution in a + * {@link MessageSource}. + * + *

Spring's own validation error classes implement this interface. + * + * @author Juergen Hoeller + * @see MessageSource#getMessage(MessageSourceResolvable, java.util.Locale) + * @see org.springframework.validation.ObjectError + * @see org.springframework.validation.FieldError + */ +public interface MessageSourceResolvable { + + /** + * Return the codes to be used to resolve this message, in the order that + * they should get tried. The last code will therefore be the default one. + * @return a String array of codes which are associated with this message + */ + public String[] getCodes(); + + /** + * Return the array of arguments to be used to resolve this message. + * @return an array of objects to be used as parameters to replace + * placeholders within the message text + * @see java.text.MessageFormat + */ + public Object[] getArguments(); + + /** + * Return the default message to be used to resolve this message. + * @return the default message, or null if no default + */ + public String getDefaultMessage(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/NoSuchMessageException.java b/org.springframework.context/src/main/java/org/springframework/context/NoSuchMessageException.java new file mode 100644 index 00000000000..b2ab999a3f5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/NoSuchMessageException.java @@ -0,0 +1,46 @@ +/* + * 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.context; + +import java.util.Locale; + +/** + * Exception thrown when a message can't be resolved. + * + * @author Rod Johnson + */ +public class NoSuchMessageException extends RuntimeException { + + /** + * Create a new exception. + * @param code code that could not be resolved for given locale + * @param locale locale that was used to search for the code within + */ + public NoSuchMessageException(String code, Locale locale) { + super("No message found under code '" + code + "' for locale '" + locale + "'."); + } + + /** + * Create a new exception. + * @param code code that could not be resolved for given locale + */ + public NoSuchMessageException(String code) { + super("No message found under code '" + code + "' for locale '" + Locale.getDefault() + "'."); + } + +} + diff --git a/org.springframework.context/src/main/java/org/springframework/context/ResourceLoaderAware.java b/org.springframework.context/src/main/java/org/springframework/context/ResourceLoaderAware.java new file mode 100644 index 00000000000..c95f8b964bf --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/ResourceLoaderAware.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.context; + +import org.springframework.core.io.ResourceLoader; + +/** + * Interface to be implemented by any object that wishes to be notified of + * the ResourceLoader (typically the ApplicationContext) that it runs in. + * This is an alternative to a full ApplicationContext dependency via the + * ApplicationContextAware interface. + * + *

Note that Resource dependencies can also be exposed as bean properties + * of type Resource, populated via Strings with automatic type conversion by + * the bean factory. This removes the need for implementing any callback + * interface just for the purpose of accessing a specific file resource. + * + *

You typically need a ResourceLoader when your application object has + * to access a variety of file resources whose names are calculated. A good + * strategy is to make the object use a DefaultResourceLoader but still + * implement ResourceLoaderAware to allow for overriding when running in an + * ApplicationContext. See ReloadableResourceBundleMessageSource for an example. + * + *

A passed-in ResourceLoader can also be checked for the + * ResourcePatternResolver interface and cast accordingly, to be able + * to resolve resource patterns into arrays of Resource objects. This will always + * work when running in an ApplicationContext (the context interface extends + * ResourcePatternResolver). Use a PathMatchingResourcePatternResolver as default. + * See also the ResourcePatternUtils.getResourcePatternResolver method. + * + *

As alternative to a ResourcePatternResolver dependency, consider exposing + * bean properties of type Resource array, populated via pattern Strings with + * automatic type conversion by the bean factory. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see ApplicationContextAware + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.core.io.support.ResourcePatternUtils#getResourcePatternResolver + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + */ +public interface ResourceLoaderAware { + + /** + * Set the ResourceLoader that this object runs in. + *

This might be a ResourcePatternResolver, which can be checked + * through instanceof ResourcePatternResolver. See also the + * ResourcePatternUtils.getResourcePatternResolver method. + *

Invoked after population of normal bean properties but before an init callback + * like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param resourceLoader ResourceLoader object to be used by this object + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.core.io.support.ResourcePatternUtils#getResourcePatternResolver + */ + void setResourceLoader(ResourceLoader resourceLoader); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/access/ContextBeanFactoryReference.java b/org.springframework.context/src/main/java/org/springframework/context/access/ContextBeanFactoryReference.java new file mode 100644 index 00000000000..fe56c165ae1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/access/ContextBeanFactoryReference.java @@ -0,0 +1,75 @@ +/* + * 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.context.access; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.access.BeanFactoryReference; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * ApplicationContext-specific implementation of BeanFactoryReference, + * wrapping a newly created ApplicationContext, closing it on release. + * + *

As per BeanFactoryReference contract, release may be called + * more than once, with subsequent calls not doing anything. However, calling + * getFactory after a release call will cause an exception. + * + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @since 13.02.2004 + * @see org.springframework.context.ConfigurableApplicationContext#close + */ +public class ContextBeanFactoryReference implements BeanFactoryReference { + + private ApplicationContext applicationContext; + + + /** + * Create a new ContextBeanFactoryReference for the given context. + * @param applicationContext the ApplicationContext to wrap + */ + public ContextBeanFactoryReference(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + public BeanFactory getFactory() { + if (this.applicationContext == null) { + throw new IllegalStateException( + "ApplicationContext owned by this BeanFactoryReference has been released"); + } + return this.applicationContext; + } + + public void release() { + if (this.applicationContext != null) { + ApplicationContext savedCtx; + + // We don't actually guarantee thread-safety, but it's not a lot of extra work. + synchronized (this) { + savedCtx = this.applicationContext; + this.applicationContext = null; + } + + if (savedCtx != null && savedCtx instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) savedCtx).close(); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/access/ContextJndiBeanFactoryLocator.java b/org.springframework.context/src/main/java/org/springframework/context/access/ContextJndiBeanFactoryLocator.java new file mode 100644 index 00000000000..3a1f46347f0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/access/ContextJndiBeanFactoryLocator.java @@ -0,0 +1,105 @@ +/* + * 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.context.access; + +import javax.naming.NamingException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.access.BeanFactoryLocator; +import org.springframework.beans.factory.access.BeanFactoryReference; +import org.springframework.beans.factory.access.BootstrapException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.util.StringUtils; + +/** + * BeanFactoryLocator implementation that creates the BeanFactory from one or + * more classpath locations specified in a JNDI environment variable. + * + *

This default implementation creates a + * {@link org.springframework.context.support.ClassPathXmlApplicationContext}. + * Subclasses may override {@link #createBeanFactory} for custom instantiation. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @see #createBeanFactory + */ +public class ContextJndiBeanFactoryLocator extends JndiLocatorSupport implements BeanFactoryLocator { + + /** + * Any number of these characters are considered delimiters between + * multiple bean factory config paths in a single String value. + */ + public static final String BEAN_FACTORY_PATH_DELIMITERS = ",; \t\n"; + + + /** + * Load/use a bean factory, as specified by a factory key which is a JNDI + * address, of the form java:comp/env/ejb/BeanFactoryPath. The + * contents of this JNDI location must be a string containing one or more + * classpath resource names (separated by any of the delimiters ',; \t\n' + * if there is more than one. The resulting BeanFactory (or ApplicationContext) + * will be created from the combined resources. + * @see #createBeanFactory + */ + public BeanFactoryReference useBeanFactory(String factoryKey) throws BeansException { + try { + String beanFactoryPath = (String) lookup(factoryKey, String.class); + if (logger.isTraceEnabled()) { + logger.trace("Bean factory path from JNDI environment variable [" + factoryKey + + "] is: " + beanFactoryPath); + } + String[] paths = StringUtils.tokenizeToStringArray(beanFactoryPath, BEAN_FACTORY_PATH_DELIMITERS); + return createBeanFactory(paths); + } + catch (NamingException ex) { + throw new BootstrapException("Define an environment variable [" + factoryKey + "] containing " + + "the class path locations of XML bean definition files", ex); + } + } + + /** + * Create the BeanFactory instance, given an array of class path resource Strings + * which should be combined. This is split out as a separate method so that + * subclasses can override the actual BeanFactory implementation class. + *

Delegates to createApplicationContext by default, + * wrapping the result in a ContextBeanFactoryReference. + * @param resources an array of Strings representing classpath resource names + * @return the created BeanFactory, wrapped in a BeanFactoryReference + * (for example, a ContextBeanFactoryReference wrapping an ApplicationContext) + * @throws BeansException if factory creation failed + * @see #createApplicationContext + * @see ContextBeanFactoryReference + */ + protected BeanFactoryReference createBeanFactory(String[] resources) throws BeansException { + ApplicationContext ctx = createApplicationContext(resources); + return new ContextBeanFactoryReference(ctx); + } + + /** + * Create the ApplicationContext instance, given an array of class path resource + * Strings which should be combined + * @param resources an array of Strings representing classpath resource names + * @return the created ApplicationContext + * @throws BeansException if context creation failed + */ + protected ApplicationContext createApplicationContext(String[] resources) throws BeansException { + return new ClassPathXmlApplicationContext(resources); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java b/org.springframework.context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java new file mode 100644 index 00000000000..a86916a6f8a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/access/ContextSingletonBeanFactoryLocator.java @@ -0,0 +1,159 @@ +/* + * 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.context.access; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.access.BeanFactoryLocator; +import org.springframework.beans.factory.access.SingletonBeanFactoryLocator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; + +/** + *

Variant of {@link org.springframework.beans.factory.access.SingletonBeanFactoryLocator} + * which creates its internal bean factory reference as an + * {@link org.springframework.context.ApplicationContext} instead of + * SingletonBeanFactoryLocator's simple BeanFactory. For almost all usage scenarios, + * this will not make a difference, since within that ApplicationContext or BeanFactory + * you are still free to define either BeanFactory or ApplicationContext instances. + * The main reason one would need to use this class is if bean post-processing + * (or other ApplicationContext specific features are needed in the bean reference + * definition itself). + * + *

Note: This class uses classpath*:beanRefContext.xml + * as the default resource location for the bean factory reference definition files. + * It is not possible nor legal to share definitions with SingletonBeanFactoryLocator + * at the same time. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @see org.springframework.beans.factory.access.SingletonBeanFactoryLocator + * @see org.springframework.context.access.DefaultLocatorFactory + */ +public class ContextSingletonBeanFactoryLocator extends SingletonBeanFactoryLocator { + + private static final String DEFAULT_RESOURCE_LOCATION = "classpath*:beanRefContext.xml"; + + /** The keyed singleton instances */ + private static final Map instances = new HashMap(); + + + /** + * Returns an instance which uses the default "classpath*:beanRefContext.xml", as + * the name of the definition file(s). All resources returned by the current + * thread's context class loader's getResources method with this + * name will be combined to create a definition, which is just a BeanFactory. + * @return the corresponding BeanFactoryLocator instance + * @throws BeansException in case of factory loading failure + */ + public static BeanFactoryLocator getInstance() throws BeansException { + return getInstance(null); + } + + /** + * Returns an instance which uses the the specified selector, as the name of the + * definition file(s). In the case of a name with a Spring "classpath*:" prefix, + * or with no prefix, which is treated the same, the current thread's context class + * loader's getResources method will be called with this value to get + * all resources having that name. These resources will then be combined to form a + * definition. In the case where the name uses a Spring "classpath:" prefix, or + * a standard URL prefix, then only one resource file will be loaded as the + * definition. + * @param selector the location of the resource(s) which will be read and + * combined to form the definition for the BeanFactoryLocator instance. + * Any such files must form a valid ApplicationContext definition. + * @return the corresponding BeanFactoryLocator instance + * @throws BeansException in case of factory loading failure + */ + public static BeanFactoryLocator getInstance(String selector) throws BeansException { + String resourceLocation = selector; + if (resourceLocation == null) { + resourceLocation = DEFAULT_RESOURCE_LOCATION; + } + + // For backwards compatibility, we prepend "classpath*:" to the selector name if there + // is no other prefix (i.e. "classpath*:", "classpath:", or some URL prefix). + if (!ResourcePatternUtils.isUrl(resourceLocation)) { + resourceLocation = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resourceLocation; + } + + synchronized (instances) { + if (logger.isTraceEnabled()) { + logger.trace("ContextSingletonBeanFactoryLocator.getInstance(): instances.hashCode=" + + instances.hashCode() + ", instances=" + instances); + } + BeanFactoryLocator bfl = (BeanFactoryLocator) instances.get(resourceLocation); + if (bfl == null) { + bfl = new ContextSingletonBeanFactoryLocator(resourceLocation); + instances.put(resourceLocation, bfl); + } + return bfl; + } + } + + + /** + * Constructor which uses the the specified name as the resource name + * of the definition file(s). + * @param resourceLocation the Spring resource location to use + * (either a URL or a "classpath:" / "classpath*:" pseudo URL) + */ + protected ContextSingletonBeanFactoryLocator(String resourceLocation) { + super(resourceLocation); + } + + /** + * Overrides the default method to create definition object as an ApplicationContext + * instead of the default BeanFactory. This does not affect what can actually + * be loaded by that definition. + *

The default implementation simply builds a + * {@link org.springframework.context.support.ClassPathXmlApplicationContext}. + */ + protected BeanFactory createDefinition(String resourceLocation, String factoryKey) { + return new ClassPathXmlApplicationContext(new String[] {resourceLocation}, false); + } + + /** + * Overrides the default method to refresh the ApplicationContext, invoking + * {@link ConfigurableApplicationContext#refresh ConfigurableApplicationContext.refresh()}. + */ + protected void initializeDefinition(BeanFactory groupDef) { + if (groupDef instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) groupDef).refresh(); + } + } + + /** + * Overrides the default method to operate on an ApplicationContext, invoking + * {@link ConfigurableApplicationContext#refresh ConfigurableApplicationContext.close()}. + */ + protected void destroyDefinition(BeanFactory groupDef, String selector) { + if (groupDef instanceof ConfigurableApplicationContext) { + if (logger.isTraceEnabled()) { + logger.trace("Context group with selector '" + selector + + "' being released, as there are no more references to it"); + } + ((ConfigurableApplicationContext) groupDef).close(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/access/DefaultLocatorFactory.java b/org.springframework.context/src/main/java/org/springframework/context/access/DefaultLocatorFactory.java new file mode 100644 index 00000000000..66e88b6bfc8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/access/DefaultLocatorFactory.java @@ -0,0 +1,49 @@ +/* + * 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.context.access; + +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.access.BeanFactoryLocator; + +/** + * A factory class to get a default ContextSingletonBeanFactoryLocator instance. + * + * @author Colin Sampaleanu + * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator + */ +public class DefaultLocatorFactory { + + /** + * Return an instance object implementing BeanFactoryLocator. This will normally + * be a singleton instance of the specific ContextSingletonBeanFactoryLocator class, + * using the default resource selector. + */ + public static BeanFactoryLocator getInstance() throws FatalBeanException { + return ContextSingletonBeanFactoryLocator.getInstance(); + } + + /** + * Return an instance object implementing BeanFactoryLocator. This will normally + * be a singleton instance of the specific ContextSingletonBeanFactoryLocator class, + * using the specified resource selector. + * @param selector a selector variable which provides a hint to the factory as to + * which instance to return. + */ + public static BeanFactoryLocator getInstance(String selector) throws FatalBeanException { + return ContextSingletonBeanFactoryLocator.getInstance(selector); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/access/package.html b/org.springframework.context/src/main/java/org/springframework/context/access/package.html new file mode 100644 index 00000000000..161da1e9d02 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/access/package.html @@ -0,0 +1,11 @@ + + + +Helper infrastructure to locate and access shared application contexts. + +

Note: This package is only relevant for special sharing of application +contexts, for example behind EJB facades. It is not used in a typical +web application or standalone application. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java new file mode 100644 index 00000000000..235e7668716 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -0,0 +1,129 @@ +/* + * 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.context.annotation; + +import java.beans.Introspector; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.support.BeanNameGenerator} + * implementation for bean classes annotated with the + * {@link org.springframework.stereotype.Component @Component} annotation + * or with another annotation that is itself annotated with + * {@link org.springframework.stereotype.Component @Component} as a + * meta-annotation. For example, Spring's stereotype annotations (such as + * {@link org.springframework.stereotype.Repository @Repository}) are + * themselves annotated with + * {@link org.springframework.stereotype.Component @Component}. + * + *

If the annotation's value doesn't indicate a bean name, an appropriate + * name will be built based on the short name of the class (with the first + * letter lower-cased). For example: + * + *

com.xyz.FooServiceImpl -> fooServiceImpl
+ * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see org.springframework.stereotype.Component#value() + * @see org.springframework.stereotype.Repository#value() + * @see org.springframework.stereotype.Service#value() + * @see org.springframework.stereotype.Controller#value() + */ +public class AnnotationBeanNameGenerator implements BeanNameGenerator { + + private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component"; + + + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + if (definition instanceof AnnotatedBeanDefinition) { + String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); + if (StringUtils.hasText(beanName)) { + // Explicit bean name found. + return beanName; + } + } + // Fallback: generate a unique default bean name. + return buildDefaultBeanName(definition); + } + + /** + * Derive a bean name from one of the annotations on the class. + * @param annotatedDef the annotation-aware bean definition + * @return the bean name, or null if none is found + */ + protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { + AnnotationMetadata amd = annotatedDef.getMetadata(); + Set types = amd.getAnnotationTypes(); + String beanName = null; + for (String type : types) { + Map attributes = amd.getAnnotationAttributes(type); + if (isStereotypeWithNameValue(type, amd.getMetaAnnotationTypes(type), attributes)) { + String value = (String) attributes.get("value"); + if (StringUtils.hasLength(value)) { + if (beanName != null && !value.equals(beanName)) { + throw new IllegalStateException("Stereotype annotations suggest inconsistent " + + "component names: '" + beanName + "' versus '" + value + "'"); + } + beanName = value; + } + } + } + return beanName; + } + + /** + * Check whether the given annotation is a stereotype that is allowed + * to suggest a component name through its annotation value(). + * @param annotationType the name of the annotation class to check + * @param metaAnnotationTypes the names of meta-annotations on the given annotation + * @param attributes the map of attributes for the given annotation + * @return whether the annotation qualifies as a stereotype with component name + */ + protected boolean isStereotypeWithNameValue(String annotationType, + Set metaAnnotationTypes, Map attributes) { + + boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) || + (metaAnnotationTypes != null && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME)); + return (isStereotype && attributes != null && attributes.containsKey("value")); + } + + /** + * Derive a default bean name from the given bean definition. + *

The default implementation simply builds a decapitalized version + * of the short class name: e.g. "mypackage.MyJdbcDao" -> "myJdbcDao". + *

Note that inner classes will thus have names of the form + * "outerClassName.innerClassName", which because of the period in the + * name may be an issue if you are autowiring by name. + * @param definition the bean definition to build a bean name for + * @return the default bean name (never null) + */ + protected String buildDefaultBeanName(BeanDefinition definition) { + String shortClassName = ClassUtils.getShortName(definition.getBeanClassName()); + return Introspector.decapitalize(shortClassName); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java new file mode 100644 index 00000000000..c25b536f934 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java @@ -0,0 +1,63 @@ +/* + * 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.context.annotation; + +import java.util.Set; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * Parser for the <context:annotation-config/> element. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Christian Dupuis + * @since 2.5 + * @see AnnotationConfigUtils + */ +public class AnnotationConfigBeanDefinitionParser implements BeanDefinitionParser { + + public BeanDefinition parse(Element element, ParserContext parserContext) { + Object source = parserContext.extractSource(element); + + // Obtain bean definitions for all relevant BeanPostProcessors. + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(parserContext.getRegistry(), source); + + // Register component for the surrounding element. + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); + parserContext.pushContainingComponent(compDefinition); + + // Nest the concrete beans in the surrounding component. + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + parserContext.registerComponent(new BeanComponentDefinition(processorDefinition)); + } + + // Finally register the composite component. + parserContext.popAndRegisterContainingComponent(); + + return null; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java new file mode 100644 index 00000000000..5c05e22dabe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -0,0 +1,156 @@ +/* + * 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.context.annotation; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.util.ClassUtils; + +/** + * Utility class that allows for convenient registration of common + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * definitions for annotation-based configuration. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see CommonAnnotationBeanPostProcessor + * @see org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor + * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * @see org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor + */ +public class AnnotationConfigUtils { + + /** + * The bean name of the internally managed JPA annotation processor. + */ + public static final String PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalPersistenceAnnotationProcessor"; + + /** + * The bean name of the internally managed JSR-250 annotation processor. + */ + public static final String COMMON_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalCommonAnnotationProcessor"; + + /** + * The bean name of the internally managed Autowired annotation processor. + */ + public static final String AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalAutowiredAnnotationProcessor"; + + /** + * The bean name of the internally managed Required annotation processor. + */ + public static final String REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalRequiredAnnotationProcessor"; + + + private static final String PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME = + "org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"; + + + private static final boolean jsr250Present = + ClassUtils.isPresent("javax.annotation.Resource", AnnotationConfigUtils.class.getClassLoader()); + + private static final boolean jpaPresent = + ClassUtils.isPresent("javax.persistence.EntityManagerFactory", AnnotationConfigUtils.class.getClassLoader()) && + ClassUtils.isPresent(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, AnnotationConfigUtils.class.getClassLoader()); + + + /** + * Register all relevant annotation post processors in the given registry. + * @param registry the registry to operate on + */ + public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) { + registerAnnotationConfigProcessors(registry, null); + } + + /** + * Register all relevant annotation post processors in the given registry. + * @param registry the registry to operate on + * @param source the configuration source element (already extracted) + * that this registration was triggered from. May be null. + * @return a Set of BeanDefinitionHolders, containing all bean definitions + * that have actually been registered by this call + */ + public static Set registerAnnotationConfigProcessors( + BeanDefinitionRegistry registry, Object source) { + + Set beanDefinitions = new LinkedHashSet(4); + + // Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor. + if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + try { + ClassLoader cl = AnnotationConfigUtils.class.getClassLoader(); + def.setBeanClass(cl.loadClass(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME)); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex); + } + def.setSource(source); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinitions.add(registerBeanPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + // Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor. + if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class); + def.setSource(source); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinitions.add(registerBeanPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class); + def.setSource(source); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinitions.add(registerBeanPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + if (!registry.containsBeanDefinition(REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(RequiredAnnotationBeanPostProcessor.class); + def.setSource(source); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinitions.add(registerBeanPostProcessor(registry, def, REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + return beanDefinitions; + } + + private static BeanDefinitionHolder registerBeanPostProcessor( + BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName) { + + // Default infrastructure bean: lowest order value; role infrastructure. + definition.getPropertyValues().addPropertyValue("order", new Integer(Ordered.LOWEST_PRECEDENCE)); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + registry.registerBeanDefinition(beanName, definition); + return new BeanDefinitionHolder(definition, beanName); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java new file mode 100644 index 00000000000..45104c5ed7f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java @@ -0,0 +1,91 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.util.Assert; + +/** + * A {@link ScopeMetadataResolver} implementation that (by default) checks for + * the presence of the {@link Scope} annotation on the bean class. + * + *

The exact type of annotation that is checked for is configurable via the + * {@link #setScopeAnnotationType(Class)} property. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see Scope + */ +public class AnnotationScopeMetadataResolver implements ScopeMetadataResolver { + + private Class scopeAnnotationType = Scope.class; + + private ScopedProxyMode scopedProxyMode; + + + /** + * Create a new instance of the AnnotationScopeMetadataResolver class. + * @see #AnnotationScopeMetadataResolver(ScopedProxyMode) + * @see ScopedProxyMode#NO + */ + public AnnotationScopeMetadataResolver() { + this(ScopedProxyMode.NO); + } + + /** + * Create a new instance of the AnnotationScopeMetadataResolver class. + * @param scopedProxyMode the desired scoped-proxy mode + */ + public AnnotationScopeMetadataResolver(ScopedProxyMode scopedProxyMode) { + Assert.notNull(scopedProxyMode, "'scopedProxyMode' must not be null"); + this.scopedProxyMode = scopedProxyMode; + } + + + /** + * Set the type of annotation that is checked for by this + * {@link AnnotationScopeMetadataResolver}. + * @param scopeAnnotationType the target annotation type + */ + public void setScopeAnnotationType(Class scopeAnnotationType) { + Assert.notNull(scopeAnnotationType, "'scopeAnnotationType' must not be null"); + this.scopeAnnotationType = scopeAnnotationType; + } + + + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + ScopeMetadata metadata = new ScopeMetadata(); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; + Map attributes = + annDef.getMetadata().getAnnotationAttributes(this.scopeAnnotationType.getName()); + if (attributes != null) { + metadata.setScopeName((String) attributes.get("value")); + } + if (!metadata.getScopeName().equals(BeanDefinition.SCOPE_SINGLETON)) { + metadata.setScopedProxyMode(this.scopedProxyMode); + } + } + return metadata; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java new file mode 100644 index 00000000000..fdadb55ab9d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -0,0 +1,321 @@ +/* + * 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.context.annotation; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; + +/** + * A bean definition scanner that detects bean candidates on the classpath, + * registering corresponding bean definitions with a given registry (BeanFactory + * or ApplicationContext). + * + *

Candidate classes are detected through configurable type filters. The + * default filters include classes that are annotated with Spring's + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, or + * {@link org.springframework.stereotype.Controller @Controller} stereotype. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.stereotype.Component + * @see org.springframework.stereotype.Repository + * @see org.springframework.stereotype.Service + * @see org.springframework.stereotype.Controller + */ +public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider { + + private final BeanDefinitionRegistry registry; + + private BeanDefinitionDefaults beanDefinitionDefaults = new BeanDefinitionDefaults(); + + private String[] autowireCandidatePatterns; + + private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); + + private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); + + private boolean includeAnnotationConfig = true; + + + /** + * Create a new ClassPathBeanDefinitionScanner for the given bean factory. + * @param registry the BeanFactory to load bean definitions into, + * in the form of a BeanDefinitionRegistry + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { + this(registry, true); + } + + /** + * Create a new ClassPathBeanDefinitionScanner for the given bean factory. + *

If the passed-in bean factory does not only implement the BeanDefinitionRegistry + * interface but also the ResourceLoader interface, it will be used as default + * ResourceLoader as well. This will usually be the case for + * {@link org.springframework.context.ApplicationContext} implementations. + *

If given a plain BeanDefinitionRegistry, the default ResourceLoader will be a + * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}. + * @param registry the BeanFactory to load bean definitions into, + * in the form of a BeanDefinitionRegistry + * @param useDefaultFilters whether to include the default filters for the + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, and + * {@link org.springframework.stereotype.Controller @Controller} stereotype + * annotations. + * @see #setResourceLoader + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) { + super(useDefaultFilters); + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + this.registry = registry; + + // Determine ResourceLoader to use. + if (this.registry instanceof ResourceLoader) { + setResourceLoader((ResourceLoader) this.registry); + } + } + + + /** + * Return the BeanDefinitionRegistry that this scanner operates on. + */ + public final BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + /** + * Set the defaults to use for detected beans. + * @see BeanDefinitionDefaults + */ + public void setBeanDefinitionDefaults(BeanDefinitionDefaults beanDefinitionDefaults) { + this.beanDefinitionDefaults = + (beanDefinitionDefaults != null ? beanDefinitionDefaults : new BeanDefinitionDefaults()); + } + + /** + * Set the name-matching patterns for determining autowire candidates. + * @param autowireCandidatePatterns the patterns to match against + */ + public void setAutowireCandidatePatterns(String[] autowireCandidatePatterns) { + this.autowireCandidatePatterns = autowireCandidatePatterns; + } + + /** + * Set the BeanNameGenerator to use for detected bean classes. + *

Default is a {@link AnnotationBeanNameGenerator}. + */ + public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = (beanNameGenerator != null ? beanNameGenerator : new AnnotationBeanNameGenerator()); + } + + /** + * Set the ScopeMetadataResolver to use for detected bean classes. + * Note that this will override any custom "scopedProxyMode" setting. + *

The default is an {@link AnnotationScopeMetadataResolver}. + * @see #setScopedProxyMode + */ + public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = scopeMetadataResolver; + } + + /** + * Specify the proxy behavior for non-singleton scoped beans. + * Note that this will override any custom "scopeMetadataResolver" setting. + *

The default is {@link ScopedProxyMode#NO}. + * @see #setScopeMetadataResolver + */ + public void setScopedProxyMode(ScopedProxyMode scopedProxyMode) { + this.scopeMetadataResolver = new AnnotationScopeMetadataResolver(scopedProxyMode); + } + + /** + * Specify whether to register annotation config post-processors. + *

The default is to register the post-processors. Turn this off + * to be able to ignore the annotations or to process them differently. + */ + public void setIncludeAnnotationConfig(boolean includeAnnotationConfig) { + this.includeAnnotationConfig = includeAnnotationConfig; + } + + + /** + * Perform a scan within the specified base packages. + * @param basePackages the packages to check for annotated classes + * @return number of beans registered + */ + public int scan(String... basePackages) { + int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); + + doScan(basePackages); + + // Register annotation config processors, if necessary. + if (this.includeAnnotationConfig) { + AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); + } + + return this.registry.getBeanDefinitionCount() - beanCountAtScanStart; + } + + /** + * Perform a scan within the specified base packages, + * returning the registered bean definitions. + *

This method does not register an annotation config processor + * but rather leaves this up to the caller. + * @param basePackages the packages to check for annotated classes + * @return number of beans registered + */ + protected Set doScan(String... basePackages) { + Set beanDefinitions = new LinkedHashSet(); + for (int i = 0; i < basePackages.length; i++) { + Set candidates = findCandidateComponents(basePackages[i]); + for (BeanDefinition candidate : candidates) { + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + if (checkCandidate(beanName, candidate)) { + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + definitionHolder = applyScope(definitionHolder, scopeMetadata); + beanDefinitions.add(definitionHolder); + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; + } + + /** + * Apply further settings to the given bean definition, + * beyond the contents retrieved from scanning the component class. + * @param beanDefinition the scanned bean definition + * @param beanName the generated bean name for the given bean + */ + protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { + beanDefinition.applyDefaults(this.beanDefinitionDefaults); + if (this.autowireCandidatePatterns != null) { + beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName)); + } + } + + /** + * Register the specified bean with the given registry. + *

Can be overridden in subclasses, e.g. to adapt the registration + * process or to register further bean definitions for each scanned bean. + * @param definitionHolder the bean definition plus bean name for the bean + * @param registry the BeanDefinitionRegistry to register the bean with + */ + protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); + } + + + /** + * Check the given candidate's bean name, determining whether the corresponding + * bean definition needs to be registered or conflicts with an existing definition. + * @param beanName the suggested name for the bean + * @param beanDefinition the corresponding bean definition + * @return true if the bean can be registered as-is; + * false if it should be skipped because there is an + * existing, compatible bean definition for the specified name + * @throws IllegalStateException if an existing, incompatible + * bean definition has been found for the specified name + */ + protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { + if (!this.registry.containsBeanDefinition(beanName)) { + return true; + } + BeanDefinition existingDef = this.registry.getBeanDefinition(beanName); + BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition(); + if (originatingDef != null) { + existingDef = originatingDef; + } + if (isCompatible(beanDefinition, existingDef)) { + return false; + } + throw new IllegalStateException("Annotation-specified bean name '" + beanName + + "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " + + "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]"); + } + + /** + * Determine whether the given new bean definition is compatible with + * the given existing bean definition. + *

The default implementation simply considers them as compatible + * when the bean class name matches. + * @param newDefinition the new bean definition, originated from scanning + * @param existingDefinition the existing bean definition, potentially an + * explicitly defined one or a previously generated one from scanning + * @return whether the definitions are considered as compatible, with the + * new definition to be skipped in favor of the existing definition + */ + protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) { + return (!(existingDefinition instanceof AnnotatedBeanDefinition) || // explicitly registered overriding bean + newDefinition.getSource().equals(existingDefinition.getSource()) || // scanned same file twice + newDefinition.equals(existingDefinition)); // scanned equivalent class twice + } + + /** + * Apply the specified scope to the given bean definition. + * @param definitionHolder the bean definition to configure + * @param scopeMetadata the corresponding scope metadata + * @return the final bean definition to use (potentially a proxy) + */ + private BeanDefinitionHolder applyScope(BeanDefinitionHolder definitionHolder, ScopeMetadata scopeMetadata) { + String scope = scopeMetadata.getScopeName(); + ScopedProxyMode scopedProxyMode = scopeMetadata.getScopedProxyMode(); + definitionHolder.getBeanDefinition().setScope(scope); + if (BeanDefinition.SCOPE_SINGLETON.equals(scope) || BeanDefinition.SCOPE_PROTOTYPE.equals(scope) || + scopedProxyMode.equals(ScopedProxyMode.NO)) { + return definitionHolder; + } + boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); + return ScopedProxyCreator.createScopedProxy(definitionHolder, this.registry, proxyTargetClass); + } + + + /** + * Inner factory class used to just introduce an AOP framework dependency + * when actually creating a scoped proxy. + */ + private static class ScopedProxyCreator { + + public static BeanDefinitionHolder createScopedProxy( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) { + + return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java new file mode 100644 index 00000000000..e65ae74e295 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -0,0 +1,270 @@ +/* + * 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.context.annotation; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.SystemPropertyUtils; + +/** + * A component provider that scans the classpath from a base package. It then + * applies exclude and include filters to the resulting classes to find candidates. + * + *

This implementation is based on Spring's + * {@link org.springframework.core.type.classreading.MetadataReader MetadataReader} + * facility, backed by an ASM {@link org.objectweb.asm.ClassReader ClassReader}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.5 + * @see org.springframework.core.type.classreading.MetadataReaderFactory + * @see org.springframework.core.type.AnnotationMetadata + * @see ScannedGenericBeanDefinition + */ +public class ClassPathScanningCandidateComponentProvider implements ResourceLoaderAware { + + protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + + private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + + private String resourcePattern = DEFAULT_RESOURCE_PATTERN; + + private final List includeFilters = new LinkedList(); + + private final List excludeFilters = new LinkedList(); + + + /** + * Create a ClassPathScanningCandidateComponentProvider. + * @param useDefaultFilters whether to register the default filters for the + * {@link Component @Component}, {@link Repository @Repository}, + * {@link Service @Service}, and {@link Controller @Controller} + * stereotype annotations + * @see #registerDefaultFilters() + */ + public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) { + if (useDefaultFilters) { + registerDefaultFilters(); + } + } + + + /** + * Set the ResourceLoader to use for resource locations. + * This will typically be a ResourcePatternResolver implementation. + *

Default is PathMatchingResourcePatternResolver, also capable of + * resource pattern resolving through the ResourcePatternResolver interface. + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + } + + /** + * Return the ResourceLoader that this component provider uses. + */ + public final ResourceLoader getResourceLoader() { + return this.resourcePatternResolver; + } + + /** + * Set the resource pattern to use when scanning the classpath. + * This value will be appended to each base package name. + * @see #findCandidateComponents(String) + * @see #DEFAULT_RESOURCE_PATTERN + */ + public void setResourcePattern(String resourcePattern) { + Assert.notNull(resourcePattern, "'resourcePattern' must not be null"); + this.resourcePattern = resourcePattern; + } + + /** + * Add an include type filter to the end of the inclusion list. + */ + public void addIncludeFilter(TypeFilter includeFilter) { + this.includeFilters.add(includeFilter); + } + + /** + * Add an exclude type filter to the front of the exclusion list. + */ + public void addExcludeFilter(TypeFilter excludeFilter) { + this.excludeFilters.add(0, excludeFilter); + } + + /** + * Reset the configured type filters. + * @param useDefaultFilters whether to re-register the default filters for + * the {@link Component @Component}, {@link Repository @Repository}, + * {@link Service @Service}, and {@link Controller @Controller} + * stereotype annotations + * @see #registerDefaultFilters() + */ + public void resetFilters(boolean useDefaultFilters) { + this.includeFilters.clear(); + this.excludeFilters.clear(); + if (useDefaultFilters) { + registerDefaultFilters(); + } + } + + /** + * Register the default filter for {@link Component @Component}. + * This will implicitly register all annotations that have the + * {@link Component @Component} meta-annotation including the + * {@link Repository @Repository}, {@link Service @Service}, and + * {@link Controller @Controller} stereotype annotations. + */ + protected void registerDefaultFilters() { + this.includeFilters.add(new AnnotationTypeFilter(Component.class)); + } + + + /** + * Scan the class path for candidate components. + * @param basePackage the package to check for annotated classes + * @return a corresponding Set of autodetected bean definitions + */ + public Set findCandidateComponents(String basePackage) { + Set candidates = new LinkedHashSet(); + try { + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + resolveBasePackage(basePackage) + "/" + this.resourcePattern; + Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (int i = 0; i < resources.length; i++) { + Resource resource = resources[i]; + if (traceEnabled) { + logger.trace("Scanning " + resource); + } + if (resource.isReadable()) { + MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource); + if (isCandidateComponent(metadataReader)) { + ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); + sbd.setResource(resource); + sbd.setSource(resource); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Identified candidate component class: " + resource); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + resource); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not matching any filter: " + resource); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not readable: " + resource); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; + } + + /** + * Resolve the specified base package into a pattern specification for + * the package search path. + *

The default implementation resolves placeholders against system properties, + * and converts a "."-based package path to a "/"-based resource path. + * @param basePackage the base package as specified by the user + * @return the pattern specification to be used for package searching + */ + protected String resolveBasePackage(String basePackage) { + return ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)); + } + + /** + * Determine whether the given class does not match any exclude filter + * and does match at least one include filter. + * @param metadataReader the ASM ClassReader for the class + * @return whether the class qualifies as a candidate component + */ + protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { + for (TypeFilter tf : this.excludeFilters) { + if (tf.match(metadataReader, this.metadataReaderFactory)) { + return false; + } + } + for (TypeFilter tf : this.includeFilters) { + if (tf.match(metadataReader, this.metadataReaderFactory)) { + return true; + } + } + return false; + } + + /** + * Determine whether the given bean definition qualifies as candidate. + *

The default implementation checks whether the class is concrete + * (i.e. not abstract and not an interface). Can be overridden in subclasses. + * @param beanDefinition the bean definition to check + * @return whether the bean definition qualifies as a candidate component + */ + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return (beanDefinition.getMetadata().isConcrete() && beanDefinition.getMetadata().isIndependent()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java new file mode 100644 index 00000000000..af51a4a31bd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -0,0 +1,712 @@ +/* + * 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.context.annotation; + +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import javax.ejb.EJB; +import javax.xml.namespace.QName; +import javax.xml.ws.Service; +import javax.xml.ws.WebServiceClient; +import javax.xml.ws.WebServiceRef; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; +import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.jndi.support.SimpleJndiBeanFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation + * that supports common Java annotations out of the box, in particular the JSR-250 + * annotations in the javax.annotation package. These common Java + * annotations are supported in many Java EE 5 technologies (e.g. JSF 1.2), + * as well as in Java 6's JAX-WS. + * + *

This post-processor includes support for the {@link javax.annotation.PostConstruct} + * and {@link javax.annotation.PreDestroy} annotations - as init annotation + * and destroy annotation, respectively - through inheriting from + * {@link InitDestroyAnnotationBeanPostProcessor} with pre-configured annotation types. + * + *

The central element is the {@link javax.annotation.Resource} annotation + * for annotation-driven injection of named beans, by default from the containing + * Spring BeanFactory, with only mappedName references resolved in JNDI. + * The {@link #setAlwaysUseJndiLookup "alwaysUseJndiLookup" flag} enforces JNDI lookups + * equivalent to standard Java EE 5 resource injection for name references + * and default names as well. The target beans can be simple POJOs, with no special + * requirements other than the type having to match. + * + *

The JAX-WS {@link javax.xml.ws.WebServiceRef} annotation is supported too, + * analogous to {@link javax.annotation.Resource} but with the capability of creating + * specific JAX-WS service endpoints. This may either point to an explicitly defined + * resource by name or operate on a locally specified JAX-WS service class. Finally, + * this post-processor also supports the EJB 3 {@link javax.ejb.EJB} annotation, + * analogous to {@link javax.annotation.Resource} as well, with the capability to + * specify both a local bean name and a global JNDI name for fallback retrieval. + * The target beans can be plain POJOs as well as EJB 3 Session Beans in this case. + * + *

The common annotations supported by this post-processor are available + * in Java 6 (JDK 1.6) as well as in Java EE 5 (which provides a standalone jar for + * its common annotations as well, allowing for use in any Java 5 based application). + * Hence, this post-processor works out of the box on JDK 1.6, and requires the + * JSR-250 API jar (and optionally the JAX-WS API jar and/or the EJB 3 API jar) + * to be added to the classpath on JDK 1.5 (when running outside of Java EE 5). + * + *

For default usage, resolving resource names as Spring bean names, + * simply define the following in your application context: + * + *

+ * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
+ * + * For direct JNDI access, resolving resource names as JNDI resource references + * within the Java EE application's "java:comp/env/" namespace, use the following: + * + *
+ * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor">
+ *   <property name="alwaysUseJndiLookup" value="true"/>
+ * </bean>
+ * + * mappedName references will always be resolved in JNDI, + * allowing for global JNDI names (including "java:" prefix) as well. The + * "alwaysUseJndiLookup" flag just affects name references and + * default names (inferred from the field name / property name). + * + *

NOTE: A default CommonAnnotationBeanPostProcessor will be registered + * by the "context:annotation-config" and "context:component-scan" XML tags. + * Remove or turn off the default annotation configuration there if you intend + * to specify a custom CommonAnnotationBeanPostProcessor bean definition! + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setAlwaysUseJndiLookup + * @see #setResourceFactory + * @see org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor + * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + */ +public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor + implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable { + + private static Class webServiceRefClass = null; + + private static Class ejbRefClass = null; + + static { + try { + webServiceRefClass = ClassUtils.forName("javax.xml.ws.WebServiceRef", + CommonAnnotationBeanPostProcessor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + webServiceRefClass = null; + } + try { + ejbRefClass = ClassUtils.forName("javax.ejb.EJB", + CommonAnnotationBeanPostProcessor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + ejbRefClass = null; + } + } + + + private final Set ignoredResourceTypes = new HashSet(1); + + private boolean fallbackToDefaultTypeMatch = true; + + private boolean alwaysUseJndiLookup = false; + + private transient BeanFactory jndiFactory = new SimpleJndiBeanFactory(); + + private transient BeanFactory resourceFactory; + + private transient BeanFactory beanFactory; + + private transient final Map, InjectionMetadata> injectionMetadataCache = + new ConcurrentHashMap, InjectionMetadata>(); + + + /** + * Create a new CommonAnnotationBeanPostProcessor, + * with the init and destroy annotation types set to + * {@link javax.annotation.PostConstruct} and {@link javax.annotation.PreDestroy}, + * respectively. + */ + public CommonAnnotationBeanPostProcessor() { + setOrder(Ordered.LOWEST_PRECEDENCE - 3); + setInitAnnotationType(PostConstruct.class); + setDestroyAnnotationType(PreDestroy.class); + ignoreResourceType("javax.xml.ws.WebServiceContext"); + } + + + /** + * Ignore the given resource type when resolving @Resource + * annotations. + *

By default, the javax.xml.ws.WebServiceContext interface + * will be ignored, since it will be resolved by the JAX-WS runtime. + * @param resourceType the resource type to ignore + */ + public void ignoreResourceType(String resourceType) { + Assert.notNull(resourceType, "Ignored resource type must not be null"); + this.ignoredResourceTypes.add(resourceType); + } + + /** + * Set whether to allow a fallback to a type match if no explicit name has been + * specified. The default name (i.e. the field name or bean property name) will + * still be checked first; if a bean of that name exists, it will be taken. + * However, if no bean of that name exists, a by-type resolution of the + * dependency will be attempted if this flag is "true". + *

Default is "true". Switch this flag to "false" in order to enforce a + * by-name lookup in all cases, throwing an exception in case of no name match. + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency + */ + public void setFallbackToDefaultTypeMatch(boolean fallbackToDefaultTypeMatch) { + this.fallbackToDefaultTypeMatch = fallbackToDefaultTypeMatch; + } + + /** + * Set whether to always use JNDI lookups equivalent to standard Java EE 5 resource + * injection, even for name attributes and default names. + *

Default is "false": Resource names are used for Spring bean lookups in the + * containing BeanFactory; only mappedName attributes point directly + * into JNDI. Switch this flag to "true" for enforcing Java EE style JNDI lookups + * in any case, even for name attributes and default names. + * @see #setJndiFactory + * @see #setResourceFactory + */ + public void setAlwaysUseJndiLookup(boolean alwaysUseJndiLookup) { + this.alwaysUseJndiLookup = alwaysUseJndiLookup; + } + + /** + * Specify the factory for objects to be injected into @Resource / + * @WebServiceRef / @EJB annotated fields and setter methods, + * for mappedName attributes that point directly into JNDI. + * This factory will also be used if "alwaysUseJndiLookup" is set to "true" in order + * to enforce JNDI lookups even for name attributes and default names. + *

The default is a {@link org.springframework.jndi.support.SimpleJndiBeanFactory} + * for JNDI lookup behavior equivalent to standard Java EE 5 resource injection. + * @see #setResourceFactory + * @see #setAlwaysUseJndiLookup + */ + public void setJndiFactory(BeanFactory jndiFactory) { + Assert.notNull(jndiFactory, "BeanFactory must not be null"); + this.jndiFactory = jndiFactory; + } + + /** + * Specify the factory for objects to be injected into @Resource / + * @WebServiceRef / @EJB annotated fields and setter methods, + * for name attributes and default names. + *

The default is the BeanFactory that this post-processor is defined in, + * if any, looking up resource names as Spring bean names. Specify the resource + * factory explicitly for programmatic usage of this post-processor. + *

Specifying Spring's {@link org.springframework.jndi.support.SimpleJndiBeanFactory} + * leads to JNDI lookup behavior equivalent to standard Java EE 5 resource injection, + * even for name attributes and default names. This is the same behavior + * that the "alwaysUseJndiLookup" flag enables. + * @see #setAlwaysUseJndiLookup + */ + public void setResourceFactory(BeanFactory resourceFactory) { + Assert.notNull(resourceFactory, "BeanFactory must not be null"); + this.resourceFactory = resourceFactory; + } + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + if (this.resourceFactory == null) { + this.resourceFactory = beanFactory; + } + } + + + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName); + if (beanType != null) { + InjectionMetadata metadata = findResourceMetadata(beanType); + metadata.checkConfigMembers(beanDefinition); + } + } + + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + return null; + } + + public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + InjectionMetadata metadata = findResourceMetadata(bean.getClass()); + try { + metadata.injectFields(bean, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of resource fields failed", ex); + } + return true; + } + + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { + + InjectionMetadata metadata = findResourceMetadata(bean.getClass()); + try { + metadata.injectMethods(bean, beanName, pvs); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of resource methods failed", ex); + } + return pvs; + } + + + private InjectionMetadata findResourceMetadata(final Class clazz) { + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + final InjectionMetadata newMetadata = new InjectionMetadata(clazz); + ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { + public void doWith(Field field) { + if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@WebServiceRef annotation is not supported on static fields"); + } + newMetadata.addInjectedField(new WebServiceRefElement(field, null)); + } + else if (ejbRefClass != null && field.isAnnotationPresent(ejbRefClass)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@EJB annotation is not supported on static fields"); + } + newMetadata.addInjectedField(new EjbRefElement(field, null)); + } + else if (field.isAnnotationPresent(Resource.class)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@Resource annotation is not supported on static fields"); + } + if (!ignoredResourceTypes.contains(field.getType().getName())) { + newMetadata.addInjectedField(new ResourceElement(field, null)); + } + } + } + }); + ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + if (webServiceRefClass != null && method.isAnnotationPresent(webServiceRefClass) && + method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@WebServiceRef annotation is not supported on static methods"); + } + if (method.getParameterTypes().length != 1) { + throw new IllegalStateException("@WebServiceRef annotation requires a single-arg method: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + newMetadata.addInjectedMethod(new WebServiceRefElement(method, pd)); + } + else if (ejbRefClass != null && method.isAnnotationPresent(ejbRefClass) && + method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@EJB annotation is not supported on static methods"); + } + if (method.getParameterTypes().length != 1) { + throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + newMetadata.addInjectedMethod(new EjbRefElement(method, pd)); + } + else if (method.isAnnotationPresent(Resource.class) && + method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@Resource annotation is not supported on static methods"); + } + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 1) { + throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method); + } + if (!ignoredResourceTypes.contains(paramTypes[0].getName())) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + newMetadata.addInjectedMethod(new ResourceElement(method, pd)); + } + } + } + }); + metadata = newMetadata; + this.injectionMetadataCache.put(clazz, metadata); + } + } + } + return metadata; + } + + /** + * Obtain the resource object for the given name and type. + * @param element the descriptor for the annotated field/method + * @param requestingBeanName the name of the requesting bean + * @return the resource object (never null) + * @throws BeansException if we failed to obtain the target resource + */ + protected Object getResource(LookupElement element, String requestingBeanName) throws BeansException { + if (StringUtils.hasLength(element.mappedName)) { + return this.jndiFactory.getBean(element.mappedName, element.lookupType); + } + if (this.alwaysUseJndiLookup) { + return this.jndiFactory.getBean(element.name, element.lookupType); + } + if (this.resourceFactory == null) { + throw new NoSuchBeanDefinitionException(element.lookupType, + "No resource factory configured - specify the 'resourceFactory' property"); + } + return autowireResource(this.resourceFactory, element, requestingBeanName); + } + + /** + * Obtain a resource object for the given name and type through autowiring + * based on the given factory. + * @param factory the factory to autowire against + * @param element the descriptor for the annotated field/method + * @param requestingBeanName the name of the requesting bean + * @return the resource object (never null) + * @throws BeansException if we failed to obtain the target resource + */ + protected Object autowireResource(BeanFactory factory, LookupElement element, String requestingBeanName) + throws BeansException { + + Object resource = null; + Set autowiredBeanNames = null; + String name = element.name; + + if (this.fallbackToDefaultTypeMatch && element.isDefaultName && + factory instanceof AutowireCapableBeanFactory && !factory.containsBean(name)) { + autowiredBeanNames = new LinkedHashSet(); + resource = ((AutowireCapableBeanFactory) factory).resolveDependency( + element.getDependencyDescriptor(), requestingBeanName, autowiredBeanNames, null); + } + else { + resource = factory.getBean(name, element.lookupType); + autowiredBeanNames = Collections.singleton(name); + } + + if (factory instanceof ConfigurableBeanFactory) { + ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) factory; + for (String autowiredBeanName : autowiredBeanNames) { + beanFactory.registerDependentBean(autowiredBeanName, requestingBeanName); + } + } + + return resource; + } + + + /** + * Class representing generic injection information about an annotated field + * or setter method, supporting @Resource and related annotations. + */ + protected abstract class LookupElement extends InjectionMetadata.InjectedElement { + + protected String name; + + protected boolean isDefaultName = false; + + protected Class lookupType; + + protected String mappedName; + + public LookupElement(Member member, PropertyDescriptor pd) { + super(member, pd); + initAnnotation((AnnotatedElement) member); + } + + protected abstract void initAnnotation(AnnotatedElement ae); + + /** + * Return the resource name for the lookup. + */ + public final String getName() { + return this.name; + } + + /** + * Return the desired type for the lookup. + */ + public final Class getLookupType() { + return this.lookupType; + } + + /** + * Build a DependencyDescriptor for the underlying field/method. + */ + public final DependencyDescriptor getDependencyDescriptor() { + if (this.isField) { + return new LookupDependencyDescriptor((Field) this.member, this.lookupType); + } + else { + return new LookupDependencyDescriptor((Method) this.member, this.lookupType); + } + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @Resource annotation. + */ + private class ResourceElement extends LookupElement { + + protected boolean shareable = true; + + public ResourceElement(Member member, PropertyDescriptor pd) { + super(member, pd); + } + + protected void initAnnotation(AnnotatedElement ae) { + Resource resource = ae.getAnnotation(Resource.class); + String resourceName = resource.name(); + Class resourceType = resource.type(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + if (resourceType != null && !Object.class.equals(resourceType)) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.name = resourceName; + this.lookupType = resourceType; + this.mappedName = resource.mappedName(); + this.shareable = resource.shareable(); + } + + @Override + protected Object getResourceToInject(Object target, String requestingBeanName) { + return getResource(this, requestingBeanName); + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @WebServiceRef annotation. + */ + private class WebServiceRefElement extends LookupElement { + + private Class elementType; + + private String wsdlLocation; + + public WebServiceRefElement(Member member, PropertyDescriptor pd) { + super(member, pd); + } + + protected void initAnnotation(AnnotatedElement ae) { + WebServiceRef resource = ae.getAnnotation(WebServiceRef.class); + String resourceName = resource.name(); + Class resourceType = resource.type(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + if (resourceType != null && !Object.class.equals(resourceType)) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.name = resourceName; + this.elementType = resourceType; + if (Service.class.isAssignableFrom(resourceType)) { + this.lookupType = resourceType; + } + else { + this.lookupType = (!Object.class.equals(resource.value()) ? resource.value() : Service.class); + } + this.mappedName = resource.mappedName(); + this.wsdlLocation = resource.wsdlLocation(); + } + + @Override + protected Object getResourceToInject(Object target, String requestingBeanName) { + Service service = null; + try { + service = (Service) getResource(this, requestingBeanName); + } + catch (NoSuchBeanDefinitionException notFound) { + // Service to be created through generated class. + if (Service.class.equals(this.lookupType)) { + throw new IllegalStateException("No resource with name '" + this.name + "' found in context, " + + "and no specific JAX-WS Service subclass specified. The typical solution is to either specify " + + "a LocalJaxWsServiceFactoryBean with the given name or to specify the (generated) Service " + + "subclass as @WebServiceRef(...) value."); + } + if (StringUtils.hasLength(this.wsdlLocation)) { + try { + Constructor ctor = this.lookupType.getConstructor(new Class[] {URL.class, QName.class}); + WebServiceClient clientAnn = this.lookupType.getAnnotation(WebServiceClient.class); + if (clientAnn == null) { + throw new IllegalStateException("JAX-WS Service class [" + this.lookupType.getName() + + "] does not carry a WebServiceClient annotation"); + } + service = (Service) BeanUtils.instantiateClass(ctor, + new Object[] {new URL(this.wsdlLocation), new QName(clientAnn.targetNamespace(), clientAnn.name())}); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("JAX-WS Service class [" + this.lookupType.getName() + + "] does not have a (URL, QName) constructor. Cannot apply specified WSDL location [" + + this.wsdlLocation + "]."); + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException( + "Specified WSDL location [" + this.wsdlLocation + "] isn't a valid URL"); + } + } + else { + service = (Service) BeanUtils.instantiateClass(this.lookupType); + } + } + return service.getPort(this.elementType); + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @EJB annotation. + */ + private class EjbRefElement extends LookupElement { + + private String beanName; + + public EjbRefElement(Member member, PropertyDescriptor pd) { + super(member, pd); + } + + protected void initAnnotation(AnnotatedElement ae) { + EJB resource = ae.getAnnotation(EJB.class); + String resourceBeanName = resource.beanName(); + String resourceName = resource.name(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + Class resourceType = resource.beanInterface(); + if (resourceType != null && !Object.class.equals(resourceType)) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.beanName = resourceBeanName; + this.name = resourceName; + this.lookupType = resourceType; + this.mappedName = resource.mappedName(); + } + + @Override + protected Object getResourceToInject(Object target, String requestingBeanName) { + if (StringUtils.hasLength(this.beanName)) { + if (beanFactory != null && beanFactory.containsBean(this.beanName)) { + // Local match found for explicitly specified local bean name. + Object bean = beanFactory.getBean(this.beanName, this.lookupType); + if (beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) beanFactory).registerDependentBean(this.beanName, requestingBeanName); + } + return bean; + } + else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { + throw new NoSuchBeanDefinitionException(this.beanName, + "Cannot resolve 'beanName' in local BeanFactory. Consider specifying a general 'name' value instead."); + } + } + // JNDI name lookup - may still go to a local BeanFactory. + return getResource(this, requestingBeanName); + } + } + + + /** + * Extension of the DependencyDescriptor class, + * overriding the dependency type with the specified resource type. + */ + private static class LookupDependencyDescriptor extends DependencyDescriptor { + + private final Class lookupType; + + public LookupDependencyDescriptor(Field field, Class lookupType) { + super(field, true); + this.lookupType = lookupType; + } + + public LookupDependencyDescriptor(Method method, Class lookupType) { + super(new MethodParameter(method, 0), true); + this.lookupType = lookupType; + } + + public Class getDependencyType() { + return this.lookupType; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java new file mode 100644 index 00000000000..de62f4ed0b9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -0,0 +1,279 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Pattern; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.StringUtils; + +/** + * Parser for the <context:component-scan/> element. + * + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ +public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { + + private static final String BASE_PACKAGE_ATTRIBUTE = "base-package"; + + private static final String RESOURCE_PATTERN_ATTRIBUTE = "resource-pattern"; + + private static final String USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters"; + + private static final String ANNOTATION_CONFIG_ATTRIBUTE = "annotation-config"; + + private static final String NAME_GENERATOR_ATTRIBUTE = "name-generator"; + + private static final String SCOPE_RESOLVER_ATTRIBUTE = "scope-resolver"; + + private static final String SCOPED_PROXY_ATTRIBUTE = "scoped-proxy"; + + private static final String EXCLUDE_FILTER_ELEMENT = "exclude-filter"; + + private static final String INCLUDE_FILTER_ELEMENT = "include-filter"; + + private static final String FILTER_TYPE_ATTRIBUTE = "type"; + + private static final String FILTER_EXPRESSION_ATTRIBUTE = "expression"; + + + public BeanDefinition parse(Element element, ParserContext parserContext) { + String[] basePackages = + StringUtils.commaDelimitedListToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE)); + + // Actually scan for bean definitions and register them. + ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); + Set beanDefinitions = scanner.doScan(basePackages); + registerComponents(parserContext.getReaderContext(), beanDefinitions, element); + + return null; + } + + protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) { + XmlReaderContext readerContext = parserContext.getReaderContext(); + + boolean useDefaultFilters = true; + if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { + useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); + } + + // Delegate bean definition registration to scanner class. + ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters); + scanner.setResourceLoader(readerContext.getResourceLoader()); + scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()); + scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()); + + if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) { + scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE)); + } + + try { + parseBeanNameGenerator(element, scanner); + } + catch (Exception ex) { + readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); + } + + try { + parseScope(element, scanner); + } + catch (Exception ex) { + readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); + } + + parseTypeFilters(element, scanner, readerContext); + + return scanner; + } + + protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) { + return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters); + } + + protected void registerComponents( + XmlReaderContext readerContext, Set beanDefinitions, Element element) { + + Object source = readerContext.extractSource(element); + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source); + + for (Iterator it = beanDefinitions.iterator(); it.hasNext();) { + BeanDefinitionHolder beanDefHolder = (BeanDefinitionHolder) it.next(); + compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder)); + } + + // Register annotation config processors, if necessary. + boolean annotationConfig = true; + if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) { + annotationConfig = Boolean.valueOf(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE)); + } + if (annotationConfig) { + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source); + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition)); + } + } + + readerContext.fireComponentRegistered(compositeDef); + } + + protected void parseBeanNameGenerator(Element element, ClassPathBeanDefinitionScanner scanner) { + if (element.hasAttribute(NAME_GENERATOR_ATTRIBUTE)) { + BeanNameGenerator beanNameGenerator = (BeanNameGenerator) instantiateUserDefinedStrategy( + element.getAttribute(NAME_GENERATOR_ATTRIBUTE), BeanNameGenerator.class, + scanner.getResourceLoader().getClassLoader()); + scanner.setBeanNameGenerator(beanNameGenerator); + } + } + + protected void parseScope(Element element, ClassPathBeanDefinitionScanner scanner) { + // Register ScopeMetadataResolver if class name provided. + if (element.hasAttribute(SCOPE_RESOLVER_ATTRIBUTE)) { + if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { + throw new IllegalArgumentException( + "Cannot define both 'scope-resolver' and 'scoped-proxy' on tag"); + } + ScopeMetadataResolver scopeMetadataResolver = (ScopeMetadataResolver) instantiateUserDefinedStrategy( + element.getAttribute(SCOPE_RESOLVER_ATTRIBUTE), ScopeMetadataResolver.class, + scanner.getResourceLoader().getClassLoader()); + scanner.setScopeMetadataResolver(scopeMetadataResolver); + } + + if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { + String mode = element.getAttribute(SCOPED_PROXY_ATTRIBUTE); + if ("targetClass".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); + } + else if ("interfaces".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); + } + else if ("no".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.NO); + } + else { + throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); + } + } + } + + protected void parseTypeFilters( + Element element, ClassPathBeanDefinitionScanner scanner, XmlReaderContext readerContext) { + + // Parse exclude and include filter elements. + ClassLoader classLoader = scanner.getResourceLoader().getClassLoader(); + NodeList nodeList = element.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + String localName = node.getLocalName(); + try { + if (INCLUDE_FILTER_ELEMENT.equals(localName)) { + TypeFilter typeFilter = createTypeFilter((Element) node, classLoader); + scanner.addIncludeFilter(typeFilter); + } + else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { + TypeFilter typeFilter = createTypeFilter((Element) node, classLoader); + scanner.addExcludeFilter(typeFilter); + } + } + catch (Exception ex) { + readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); + } + } + } + } + + @SuppressWarnings("unchecked") + protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader) { + String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); + String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); + try { + if ("annotation".equals(filterType)) { + return new AnnotationTypeFilter((Class) classLoader.loadClass(expression)); + } + else if ("assignable".equals(filterType)) { + return new AssignableTypeFilter(classLoader.loadClass(expression)); + } + else if ("aspectj".equals(filterType)) { + return new AspectJTypeFilter(expression, classLoader); + } + else if ("regex".equals(filterType)) { + return new RegexPatternTypeFilter(Pattern.compile(expression)); + } + else if ("custom".equals(filterType)) { + Class filterClass = classLoader.loadClass(expression); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + throw new IllegalArgumentException( + "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + else { + throw new IllegalArgumentException("Unsupported filter type: " + filterType); + } + } + catch (ClassNotFoundException ex) { + throw new FatalBeanException("Type filter class not found: " + expression, ex); + } + } + + @SuppressWarnings("unchecked") + private Object instantiateUserDefinedStrategy(String className, Class strategyType, ClassLoader classLoader) { + Object result = null; + try { + result = classLoader.loadClass(className).newInstance(); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Class [" + className + "] for strategy [" + + strategyType.getName() + "] not found", ex); + } + catch (Exception ex) { + throw new IllegalArgumentException("Unable to instantiate class [" + className + "] for strategy [" + + strategyType.getName() + "]. A zero-argument constructor is required", ex); + } + + if (!strategyType.isAssignableFrom(result.getClass())) { + throw new IllegalArgumentException("Provided class name must be an implementation of " + strategyType); + } + return result; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FilterType.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FilterType.java new file mode 100644 index 00000000000..6b63b9a0c33 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FilterType.java @@ -0,0 +1,34 @@ +/* + * 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.context.annotation; + +/** + * Enumeration of the valid type filters to be added for annotation-driven configuration. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public enum FilterType { + + ANNOTATION, + ASSIGNABLE_TYPE, + ASPECTJ_PATTERN, + REGEX_PATTERN, + CUSTOM + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java new file mode 100644 index 00000000000..22d918c8870 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java @@ -0,0 +1,61 @@ +/* + * 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.context.annotation; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.util.Assert; + +/** + * Extension of the {@link org.springframework.beans.factory.support.GenericBeanDefinition} + * class, based on an ASM ClassReader, with support for annotation metadata exposed + * through the {@link AnnotatedBeanDefinition} interface. + * + *

This class does not load the bean Class early. + * It rather retrieves all relevant metadata from the ".class" file itself, + * parsed with the ASM ClassReader. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #getMetadata() + * @see #getBeanClassName() + * @see org.springframework.core.type.classreading.MetadataReaderFactory + */ +public class ScannedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition { + + private final AnnotationMetadata metadata; + + + /** + * Create a new ScannedGenericBeanDefinition for the class that the + * given MetadataReader describes. + * @param metadataReader the MetadataReader for the scanned target class + */ + public ScannedGenericBeanDefinition(MetadataReader metadataReader) { + Assert.notNull(metadataReader, "MetadataReader must not be null"); + this.metadata = metadataReader.getAnnotationMetadata(); + setBeanClassName(this.metadata.getClassName()); + } + + + public final AnnotationMetadata getMetadata() { + return this.metadata; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Scope.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Scope.java new file mode 100644 index 00000000000..951ab8b80e6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Scope.java @@ -0,0 +1,47 @@ +/* + * 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.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Indicates the name of a scope to use for instances of the annotated class. + * + *

In this context, scope means the lifecycle of an instance, such as + * 'singleton', 'prototype', and so forth. + * + * @author Mark Fisher + * @since 2.5 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Scope { + + /** + * Specifies the scope to use for instances of the annotated class. + * @return the desired scope + */ + String value() default BeanDefinition.SCOPE_SINGLETON; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java new file mode 100644 index 00000000000..65e8ed0c20c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java @@ -0,0 +1,72 @@ +/* + * 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.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Describes scope characteristics for a Spring-managed bean including the scope + * name and the scoped-proxy behavior. + * + *

The default scope is "singleton", and the default is to not create + * scoped-proxies. + * + * @author Mark Fisher + * @since 2.5 + * @see ScopeMetadataResolver + * @see ScopedProxyMode + */ +public class ScopeMetadata { + + private String scopeName = BeanDefinition.SCOPE_SINGLETON; + + private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO; + + + /** + * Get the name of the scope. + * @return said scope name + */ + public String getScopeName() { + return scopeName; + } + + /** + * Set the name of the scope. + * @param scopeName said scope name + */ + public void setScopeName(String scopeName) { + this.scopeName = scopeName; + } + + /** + * Get the proxy-mode to be applied to the scoped instance. + * @return said scoped-proxy mode + */ + public ScopedProxyMode getScopedProxyMode() { + return scopedProxyMode; + } + + /** + * Set the proxy-mode to be applied to the scoped instance. + * @param scopedProxyMode said scoped-proxy mode + */ + public void setScopedProxyMode(ScopedProxyMode scopedProxyMode) { + this.scopedProxyMode = scopedProxyMode; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java new file mode 100644 index 00000000000..d25609e6581 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java @@ -0,0 +1,44 @@ +/* + * 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.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Strategy interface for resolving the scope of bean definitions. + * + * @author Mark Fisher + * @since 2.5 + * @see Scope + */ +public interface ScopeMetadataResolver { + + /** + * Resolve the {@link ScopeMetadata} appropriate to the supplied + * bean definition. + *

Implementations can of course use any strategy they like to + * determine the scope metadata, but some implementations that spring + * immediately to mind might be to use source level annotations + * present on {@link BeanDefinition#getBeanClassName() the class} of the + * supplied definition, or to use metadata present in the + * {@link BeanDefinition#attributeNames()} of the supplied definition. + * @param definition the target bean definition + * @return the relevant scope metadata; never null + */ + ScopeMetadata resolveScopeMetadata(BeanDefinition definition); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java new file mode 100644 index 00000000000..771026eb478 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java @@ -0,0 +1,52 @@ +/* + * 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.context.annotation; + +/** + * Enumerates the various scoped-proxy options. + * + *

For a fuller discussion of exactly what a scoped-proxy is, see that + * section of the Spring reference documentation entitled 'Scoped beans as + * dependencies'. + * + * @author Mark Fisher + * @since 2.5 + * @see ScopeMetadata + */ +public enum ScopedProxyMode { + + /** + * Do not create a scoped proxy. + *

This proxy-mode is not typically useful when used with a + * non-singleton scoped instance, which should favor the use of the + * {@link #INTERFACES} or {@link #TARGET_CLASS} proxy-modes instead if it + * is to be used as a dependency. + */ + NO, + + /** + * Create a JDK dynamic proxy implementing all interfaces exposed by + * the class of the target object. + */ + INTERFACES, + + /** + * Create a class-based proxy (requires CGLIB). + */ + TARGET_CLASS + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/package.html b/org.springframework.context/src/main/java/org/springframework/context/annotation/package.html new file mode 100644 index 00000000000..4762b9c5807 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/package.html @@ -0,0 +1,8 @@ + + + +Annotation support for context configuration, +including classpath scanning for autowire candidates. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java new file mode 100644 index 00000000000..d18577b017d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java @@ -0,0 +1,52 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; + +/** + * Abstract parser for <context:property-.../> elements. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.5.2 + */ +abstract class AbstractPropertyLoadingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + protected boolean shouldGenerateId() { + return true; + } + + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String location = element.getAttribute("location"); + if (StringUtils.hasLength(location)) { + String[] locations = StringUtils.commaDelimitedListToStringArray(location); + builder.addPropertyValue("locations", locations); + } + String propertiesRef = element.getAttribute("properties-ref"); + if (StringUtils.hasLength(propertiesRef)) { + builder.addPropertyReference("properties", propertiesRef); + } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java b/org.springframework.context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java new file mode 100644 index 00000000000..9a07fd2dc4a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java @@ -0,0 +1,73 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.JdkVersion; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.beans.factory.xml.NamespaceHandler} + * for the 'context' namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class ContextNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); + registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); + registerJava5DependentParser("annotation-config", + "org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser"); + registerJava5DependentParser("component-scan", + "org.springframework.context.annotation.ComponentScanBeanDefinitionParser"); + registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); + registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); + registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); + } + + private void registerJava5DependentParser(final String elementName, final String parserClassName) { + BeanDefinitionParser parser = null; + if (JdkVersion.isAtLeastJava15()) { + try { + Class parserClass = ClassUtils.forName(parserClassName, ContextNamespaceHandler.class.getClassLoader()); + parser = (BeanDefinitionParser) parserClass.newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to create Java 1.5 dependent parser: " + parserClassName, ex); + } + } + else { + parser = new BeanDefinitionParser() { + public BeanDefinition parse(Element element, ParserContext parserContext) { + throw new IllegalStateException("Context namespace element '" + elementName + + "' and its parser class [" + parserClassName + "] are only available on JDK 1.5 and higher"); + } + }; + } + registerBeanDefinitionParser(elementName, parser); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java new file mode 100644 index 00000000000..9d2e861d6db --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java @@ -0,0 +1,95 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * Parser for the <context:load-time-weaver/> element. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class"; + + private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving"; + + private static final String ASPECTJ_AOP_XML_RESOURCE = "META-INF/aop.xml"; + + private static final String DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME = + "org.springframework.context.weaving.DefaultContextLoadTimeWeaver"; + + private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME = + "org.springframework.context.weaving.AspectJWeavingEnabler"; + + + protected String getBeanClassName(Element element) { + if (element.hasAttribute(WEAVER_CLASS_ATTRIBUTE)) { + return element.getAttribute(WEAVER_CLASS_ATTRIBUTE); + } + return DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME; + } + + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + return ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME; + } + + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) { + RootBeanDefinition weavingEnablerDef = new RootBeanDefinition(); + weavingEnablerDef.setBeanClassName(ASPECTJ_WEAVING_ENABLER_CLASS_NAME); + parserContext.getReaderContext().registerWithGeneratedName(weavingEnablerDef); + + if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) { + new SpringConfiguredBeanDefinitionParser().parse(element, parserContext); + } + } + } + + protected boolean isAspectJWeavingEnabled(String value, ParserContext parserContext) { + if ("on".equals(value)) { + return true; + } + else if ("off".equals(value)) { + return false; + } + else { + // Determine default... + ClassLoader cl = parserContext.getReaderContext().getResourceLoader().getClassLoader(); + return (cl.getResource(ASPECTJ_AOP_XML_RESOURCE) != null); + } + } + + protected boolean isBeanConfigurerAspectEnabled(ClassLoader beanClassLoader) { + return ClassUtils.isPresent(SpringConfiguredBeanDefinitionParser.BEAN_CONFIGURER_ASPECT_CLASS_NAME, + beanClassLoader); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java new file mode 100644 index 00000000000..2bc9437302d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java @@ -0,0 +1,100 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.JdkVersion; +import org.springframework.jmx.support.MBeanRegistrationSupport; +import org.springframework.util.StringUtils; + +/** + * Parser for the <context:mbean-export/> element. + * + *

Registers an instance of + * {@link org.springframework.jmx.export.annotation.AnnotationMBeanExporter} + * within the context. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see org.springframework.jmx.export.annotation.AnnotationMBeanExporter + */ +class MBeanExportBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String MBEAN_EXPORTER_BEAN_NAME = "mbeanExporter"; + + private static final String DEFAULT_DOMAIN_ATTRIBUTE = "default-domain"; + + private static final String SERVER_ATTRIBUTE = "server"; + + private static final String REGISTRATION_ATTRIBUTE = "registration"; + + private static final String REGISTRATION_IGNORE_EXISTING = "ignoreExisting"; + + private static final String REGISTRATION_REPLACE_EXISTING = "replaceExisting"; + + + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + return MBEAN_EXPORTER_BEAN_NAME; + } + + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + String beanClassName = (JdkVersion.isAtLeastJava15() ? + "org.springframework.jmx.export.annotation.AnnotationMBeanExporter" : + "org.springframework.jmx.export.MBeanExporter"); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(beanClassName); + + // Mark as infrastructure bean and attach source location. + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + + String defaultDomain = element.getAttribute(DEFAULT_DOMAIN_ATTRIBUTE); + if (StringUtils.hasText(defaultDomain)) { + builder.addPropertyValue("defaultDomain", defaultDomain); + } + + String serverBeanName = element.getAttribute(SERVER_ATTRIBUTE); + if (StringUtils.hasText(serverBeanName)) { + builder.addPropertyReference("server", serverBeanName); + } + else { + AbstractBeanDefinition specialServer = MBeanServerBeanDefinitionParser.findServerForSpecialEnvironment(); + if (specialServer != null) { + builder.addPropertyValue("server", specialServer); + } + } + + String registration = element.getAttribute(REGISTRATION_ATTRIBUTE); + int registrationBehavior = MBeanRegistrationSupport.REGISTRATION_FAIL_ON_EXISTING; + if (REGISTRATION_IGNORE_EXISTING.equals(registration)) { + registrationBehavior = MBeanRegistrationSupport.REGISTRATION_IGNORE_EXISTING; + } + else if (REGISTRATION_REPLACE_EXISTING.equals(registration)) { + registrationBehavior = MBeanRegistrationSupport.REGISTRATION_REPLACE_EXISTING; + } + builder.addPropertyValue("registrationBehavior", new Integer(registrationBehavior)); + + return builder.getBeanDefinition(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.java new file mode 100644 index 00000000000..08585b8910b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jmx.support.MBeanServerFactoryBean; +import org.springframework.jmx.support.WebSphereMBeanServerFactoryBean; +import org.springframework.jndi.JndiObjectFactoryBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the <context:mbean-server/> element. + * + *

Registers an instance of + * {@link org.springframework.jmx.export.annotation.AnnotationMBeanExporter} + * within the context. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.jmx.export.annotation.AnnotationMBeanExporter + */ +class MBeanServerBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String MBEAN_SERVER_BEAN_NAME = "mbeanServer"; + + private static final String AGENT_ID_ATTRIBUTE = "agent-id"; + + + private static final boolean weblogicPresent = ClassUtils.isPresent( + "weblogic.management.Helper", MBeanServerBeanDefinitionParser.class.getClassLoader()); + + private static final boolean webspherePresent = ClassUtils.isPresent( + "com.ibm.websphere.management.AdminServiceFactory", MBeanServerBeanDefinitionParser.class.getClassLoader()); + + + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + String id = element.getAttribute(ID_ATTRIBUTE); + return (StringUtils.hasText(id) ? id : MBEAN_SERVER_BEAN_NAME); + } + + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + String agentId = element.getAttribute(AGENT_ID_ATTRIBUTE); + if (StringUtils.hasText(agentId)) { + RootBeanDefinition bd = new RootBeanDefinition(MBeanServerFactoryBean.class); + bd.getPropertyValues().addPropertyValue("agentId", agentId); + return bd; + } + AbstractBeanDefinition specialServer = findServerForSpecialEnvironment(); + if (specialServer != null) { + return specialServer; + } + RootBeanDefinition bd = new RootBeanDefinition(MBeanServerFactoryBean.class); + bd.getPropertyValues().addPropertyValue("locateExistingServerIfPossible", Boolean.TRUE); + + // Mark as infrastructure bean and attach source location. + bd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + bd.setSource(parserContext.extractSource(element)); + return bd; + } + + static AbstractBeanDefinition findServerForSpecialEnvironment() { + if (weblogicPresent) { + RootBeanDefinition bd = new RootBeanDefinition(JndiObjectFactoryBean.class); + bd.getPropertyValues().addPropertyValue("jndiName", "java:comp/env/jmx/runtime"); + return bd; + } + else if (webspherePresent) { + return new RootBeanDefinition(WebSphereMBeanServerFactoryBean.class); + } + else { + return null; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java new file mode 100644 index 00000000000..0e00fb39343 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java @@ -0,0 +1,35 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.PropertyOverrideConfigurer; + +/** + * Parser for the <context:property-override/> element. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +class PropertyOverrideBeanDefinitionParser extends AbstractPropertyLoadingBeanDefinitionParser { + + protected Class getBeanClass(Element element) { + return PropertyOverrideConfigurer.class; + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java new file mode 100644 index 00000000000..95aff2c8c22 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java @@ -0,0 +1,35 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; + +/** + * Parser for the <context:property-placeholder/> element. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class PropertyPlaceholderBeanDefinitionParser extends AbstractPropertyLoadingBeanDefinitionParser { + + protected Class getBeanClass(Element element) { + return PropertyPlaceholderConfigurer.class; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java new file mode 100644 index 00000000000..3094136710b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java @@ -0,0 +1,60 @@ +/* + * 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.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * {@link BeanDefinitionParser} responsible for parsing the + * <context:spring-configured/> tag. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { + + /** + * The bean name of the internally managed bean configurer aspect. + */ + public static final String BEAN_CONFIGURER_ASPECT_BEAN_NAME = + "org.springframework.context.config.internalBeanConfigurerAspect"; + + static final String BEAN_CONFIGURER_ASPECT_CLASS_NAME = + "org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"; + + + public BeanDefinition parse(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + + // Mark as infrastructure bean and attach source location. + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + def.setSource(parserContext.extractSource(element)); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, BEAN_CONFIGURER_ASPECT_BEAN_NAME)); + } + return null; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/package.html b/org.springframework.context/src/main/java/org/springframework/context/config/package.html new file mode 100644 index 00000000000..9cf9ecee19d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/package.html @@ -0,0 +1,8 @@ + + + +Support package for advanced application context configuration, +with XML schema being the primary configuration format. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/spring-context-2.5.xsd b/org.springframework.context/src/main/java/org/springframework/context/config/spring-context-2.5.xsd new file mode 100644 index 00000000000..0cbb8da9273 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/spring-context-2.5.xsd @@ -0,0 +1,423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tag for that purpose. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/org.springframework.context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java new file mode 100644 index 00000000000..91af674736d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -0,0 +1,112 @@ +/* + * 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.context.event; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.ApplicationListener; +import org.springframework.core.CollectionFactory; + +/** + * Abstract implementation of the {@link ApplicationEventMulticaster} interface, + * providing the basic listener registration facility. + * + *

Doesn't permit multiple instances of the same listener by default, + * as it keeps listeners in a linked Set. The collection class used to hold + * ApplicationListener objects can be overridden through the "collectionClass" + * bean property. + * + *

Implementing ApplicationEventMulticaster's actual {@link #multicastEvent} method + * is left to subclasses. {@link SimpleApplicationEventMulticaster} simply multicasts + * all events to all registered listeners, invoking them in the calling thread. + * Alternative implementations could be more sophisticated in those respects. + * + * @author Juergen Hoeller + * @since 1.2.3 + * @see #setCollectionClass + * @see #getApplicationListeners() + * @see SimpleApplicationEventMulticaster + */ +public abstract class AbstractApplicationEventMulticaster implements ApplicationEventMulticaster { + + /** Collection of ApplicationListeners */ + private Collection applicationListeners = new LinkedHashSet(); + + + /** + * Set whether this multicaster should expect concurrent updates at runtime + * (i.e. after context startup finished). In case of concurrent updates, + * a copy-on-write strategy is applied, keeping iteration (for multicasting) + * without synchronization while still making listener updates thread-safe. + */ + public void setConcurrentUpdates(boolean concurrent) { + Collection newColl = (concurrent ? CollectionFactory.createCopyOnWriteSet() : new LinkedHashSet()); + // Add all previously registered listeners (usually none). + newColl.addAll(this.applicationListeners); + this.applicationListeners = newColl; + } + + /** + * Specify the collection class to use. Can be populated with a fully + * qualified class name when defined in a Spring application context. + *

Default is a linked HashSet, keeping the registration order. + * Note that a Set class specified will not permit multiple instances + * of the same listener, while a List class will allow for registering + * the same listener multiple times. + */ + public void setCollectionClass(Class collectionClass) { + if (collectionClass == null) { + throw new IllegalArgumentException("'collectionClass' must not be null"); + } + if (!Collection.class.isAssignableFrom(collectionClass)) { + throw new IllegalArgumentException("'collectionClass' must implement [java.util.Collection]"); + } + // Create desired collection instance. + Collection newColl = (Collection) BeanUtils.instantiateClass(collectionClass); + // Add all previously registered listeners (usually none). + newColl.addAll(this.applicationListeners); + this.applicationListeners = newColl; + } + + + public void addApplicationListener(ApplicationListener listener) { + this.applicationListeners.add(listener); + } + + public void removeApplicationListener(ApplicationListener listener) { + this.applicationListeners.remove(listener); + } + + public void removeAllListeners() { + this.applicationListeners.clear(); + } + + /** + * Return the current Collection of ApplicationListeners. + *

Note that this is the raw Collection of ApplicationListeners, + * potentially modified when new listeners get registered or + * existing ones get removed. This Collection is not a snapshot copy. + * @return a Collection of ApplicationListeners + * @see org.springframework.context.ApplicationListener + */ + protected Collection getApplicationListeners() { + return this.applicationListeners; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java b/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java new file mode 100644 index 00000000000..bc9dbb3d5f2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -0,0 +1,46 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +/** + * Base class for events raised for an ApplicationContext. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class ApplicationContextEvent extends ApplicationEvent { + + /** + * Create a new ContextStartedEvent. + * @param source the ApplicationContext that the event is raised for + * (must not be null) + */ + public ApplicationContextEvent(ApplicationContext source) { + super(source); + } + + /** + * Get the ApplicationContext that the event was raised for. + */ + public final ApplicationContext getApplicationContext() { + return (ApplicationContext) getSource(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java new file mode 100644 index 00000000000..3998bb33a0b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.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.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * Interface to be implemented by objects that can manage a number + * of ApplicationListeners, and publish events to them. An example + * of such an object is an ApplicationEventPublisher, typically + * the ApplicationContext, which can use an ApplicationEventMulticaster + * as a helper to publish events to listeners. + * + * @author Rod Johnson + */ +public interface ApplicationEventMulticaster { + + /** + * Add a listener to be notified of all events. + * @param listener the listener to add + */ + void addApplicationListener(ApplicationListener listener); + + /** + * Remove a listener from the notification list. + * @param listener the listener to remove + */ + void removeApplicationListener(ApplicationListener listener); + + /** + * Remove all listeners registered with this multicaster. + * It will perform no action on event notification until more + * listeners are registered. + */ + void removeAllListeners(); + + /** + * Multicast the given application event to appropriate listeners. + * @param event the event to multicast + */ + void multicastEvent(ApplicationEvent event); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ContextClosedEvent.java b/org.springframework.context/src/main/java/org/springframework/context/event/ContextClosedEvent.java new file mode 100644 index 00000000000..e53df338969 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -0,0 +1,39 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an ApplicationContext gets closed. + * + * @author Juergen Hoeller + * @since 12.08.2003 + * @see ContextRefreshedEvent + */ +public class ContextClosedEvent extends ApplicationContextEvent { + + /** + * Creates a new ContextClosedEvent. + * @param source the ApplicationContext that has been closed + * (must not be null) + */ + public ContextClosedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/org.springframework.context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java new file mode 100644 index 00000000000..9b448cb3b42 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -0,0 +1,39 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an ApplicationContext gets initialized or refreshed. + * + * @author Juergen Hoeller + * @since 04.03.2003 + * @see ContextClosedEvent + */ +public class ContextRefreshedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextRefreshedEvent. + * @param source the ApplicationContext that has been initialized + * or refreshed (must not be null) + */ + public ContextRefreshedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/org.springframework.context/src/main/java/org/springframework/context/event/ContextStartedEvent.java new file mode 100644 index 00000000000..bc3e7163c13 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -0,0 +1,40 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an ApplicationContext gets started. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see ContextStoppedEvent + */ +public class ContextStartedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextStartedEvent. + * @param source the ApplicationContext that has been started + * (must not be null) + */ + public ContextStartedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/org.springframework.context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java new file mode 100644 index 00000000000..a00f82baea5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -0,0 +1,40 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an ApplicationContext gets stopped. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see ContextStartedEvent + */ +public class ContextStoppedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextStoppedEvent. + * @param source the ApplicationContext that has been stopped + * (must not be null) + */ + public ContextStoppedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java b/org.springframework.context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java new file mode 100644 index 00000000000..98d08dbcf3d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java @@ -0,0 +1,101 @@ +/* + * 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.context.event; + +import java.lang.reflect.Constructor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; + +/** + * {@link MethodInterceptor Interceptor} that publishes an + * ApplicationEvent to all ApplicationListeners + * registered with an ApplicationEventPublisher after each + * successful method invocation. + * + *

Note that this interceptor is only capable of publishing stateless + * events configured via the + * {@link #setApplicationEventClass "applicationEventClass"} property. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Rick Evans + * @see #setApplicationEventClass + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.ApplicationEventPublisher + * @see org.springframework.context.ApplicationContext + */ +public class EventPublicationInterceptor + implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean { + + private Constructor applicationEventClassConstructor; + + private ApplicationEventPublisher applicationEventPublisher; + + + /** + * Set the application event class to publish. + *

The event class must have a constructor with a single + * Object argument for the event source. The interceptor + * will pass in the invoked object. + * @throws IllegalArgumentException if the supplied Class is + * null or if it is not an ApplicationEvent subclass or + * if it does not expose a constructor that takes a single Object argument + */ + public void setApplicationEventClass(Class applicationEventClass) { + if (ApplicationEvent.class.equals(applicationEventClass) || + !ApplicationEvent.class.isAssignableFrom(applicationEventClass)) { + throw new IllegalArgumentException("applicationEventClass needs to extend ApplicationEvent"); + } + try { + this.applicationEventClassConstructor = + applicationEventClass.getConstructor(new Class[] {Object.class}); + } + catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("applicationEventClass [" + + applicationEventClass.getName() + "] does not have the required Object constructor: " + ex); + } + } + + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + public void afterPropertiesSet() throws Exception { + if (this.applicationEventClassConstructor == null) { + throw new IllegalArgumentException("applicationEventClass is required"); + } + } + + + public Object invoke(MethodInvocation invocation) throws Throwable { + Object retVal = invocation.proceed(); + + ApplicationEvent event = (ApplicationEvent) + this.applicationEventClassConstructor.newInstance(new Object[] {invocation.getThis()}); + this.applicationEventPublisher.publishEvent(event); + + return retVal; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/org.springframework.context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java new file mode 100644 index 00000000000..179e0761fac --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -0,0 +1,84 @@ +/* + * 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.context.event; + +import java.util.Iterator; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; + +/** + * Simple implementation of the {@link ApplicationEventMulticaster} interface. + * + *

Multicasts all events to all registered listeners, leaving it up to + * the listeners to ignore events that they are not interested in. + * Listeners will usually perform corresponding instanceof + * checks on the passed-in event object. + * + *

By default, all listeners are invoked in the calling thread. + * This allows the danger of a rogue listener blocking the entire application, + * but adds minimal overhead. Specify an alternative TaskExecutor to have + * listeners executed in different threads, for example from a thread pool. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setTaskExecutor + * @see #setConcurrentUpdates + */ +public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { + + private TaskExecutor taskExecutor = new SyncTaskExecutor(); + + + /** + * Set the TaskExecutor to execute application listeners with. + *

Default is a SyncTaskExecutor, executing the listeners synchronously + * in the calling thread. + *

Consider specifying an asynchronous TaskExecutor here to not block the + * caller until all listeners have been executed. However, note that asynchronous + * execution will not participate in the caller's thread context (class loader, + * transaction association) unless the TaskExecutor explicitly supports this. + * @see org.springframework.core.task.SyncTaskExecutor + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.timer.TimerTaskExecutor + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = (taskExecutor != null ? taskExecutor : new SyncTaskExecutor()); + } + + /** + * Return the current TaskExecutor for this multicaster. + */ + protected TaskExecutor getTaskExecutor() { + return this.taskExecutor; + } + + + public void multicastEvent(final ApplicationEvent event) { + for (Iterator it = getApplicationListeners().iterator(); it.hasNext();) { + final ApplicationListener listener = (ApplicationListener) it.next(); + getTaskExecutor().execute(new Runnable() { + public void run() { + listener.onApplicationEvent(event); + } + }); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/SourceFilteringListener.java b/org.springframework.context/src/main/java/org/springframework/context/event/SourceFilteringListener.java new file mode 100644 index 00000000000..0a4287bd4e4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/SourceFilteringListener.java @@ -0,0 +1,84 @@ +/* + * 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.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * {@link org.springframework.context.ApplicationListener} decorator that filters + * events from a specified event source, invoking its delegate listener for + * matching {@link org.springframework.context.ApplicationEvent} objects only. + * + *

Can also be used as base class, overriding the {@link #onApplicationEventInternal} + * method instead of specifying a delegate listener. + * + * @author Juergen Hoeller + * @since 2.0.5 + */ +public class SourceFilteringListener implements ApplicationListener { + + private final Object source; + + private ApplicationListener delegate; + + + /** + * Create a SourceFilteringListener for the given event source. + * @param source the event source that this listener filters for, + * only processing events from this source + * @param delegate the delegate listener to invoke with event + * from the specified source + */ + public SourceFilteringListener(Object source, ApplicationListener delegate) { + this.source = source; + this.delegate = delegate; + } + + /** + * Create a SourceFilteringListener for the given event source, + * expecting subclasses to override the {@link #onApplicationEventInternal} + * method (instead of specifying a delegate listener). + * @param source the event source that this listener filters for, + * only processing events from this source + */ + protected SourceFilteringListener(Object source) { + this.source = source; + } + + + public void onApplicationEvent(ApplicationEvent event) { + if (event.getSource() == this.source) { + onApplicationEventInternal(event); + } + } + + /** + * Actually process the event, after having filtered according to the + * desired event source already. + *

The default implementation invokes the specified delegate, if any. + * @param event the event to process (matching the specified source) + */ + protected void onApplicationEventInternal(ApplicationEvent event) { + if (this.delegate == null) { + throw new IllegalStateException( + "Must specify a delegate object or override the onApplicationEventInternal method"); + } + this.delegate.onApplicationEvent(event); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/event/package.html b/org.springframework.context/src/main/java/org/springframework/context/event/package.html new file mode 100644 index 00000000000..17301dddbd5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/event/package.html @@ -0,0 +1,8 @@ + + + +Support classes for application events, like standard context events. +To be supported by all major application context implementations. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContext.java b/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContext.java new file mode 100644 index 00000000000..c746151b28b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContext.java @@ -0,0 +1,40 @@ +/* + * 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.context.i18n; + +import java.util.Locale; + +/** + * Strategy interface for determining the current Locale. + * + *

A LocaleContext instance can be associated with a thread + * via the LocaleContextHolder class. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocaleContextHolder + * @see java.util.Locale + */ +public interface LocaleContext { + + /** + * Return the current Locale, which can be fixed or determined dynamically, + * depending on the implementation strategy. + */ + Locale getLocale(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java b/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java new file mode 100644 index 00000000000..2b244f03321 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java @@ -0,0 +1,136 @@ +/* + * 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.context.i18n; + +import java.util.Locale; + +import org.springframework.core.NamedInheritableThreadLocal; +import org.springframework.core.NamedThreadLocal; + +/** + * Simple holder class that associates a LocaleContext instance + * with the current thread. The LocaleContext will be inherited + * by any child threads spawned by the current thread. + * + *

Used as a central holder for the current Locale in Spring, + * wherever necessary: for example, in MessageSourceAccessor. + * DispatcherServlet automatically exposes its current Locale here. + * Other applications can expose theirs too, to make classes like + * MessageSourceAccessor automatically use that Locale. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocaleContext + * @see org.springframework.context.support.MessageSourceAccessor + * @see org.springframework.web.servlet.DispatcherServlet + */ +public abstract class LocaleContextHolder { + + private static final ThreadLocal localeContextHolder = new NamedThreadLocal("Locale context"); + + private static final ThreadLocal inheritableLocaleContextHolder = + new NamedInheritableThreadLocal("Locale context"); + + + /** + * Reset the LocaleContext for the current thread. + */ + public static void resetLocaleContext() { + localeContextHolder.set(null); + inheritableLocaleContextHolder.set(null); + } + + /** + * Associate the given LocaleContext with the current thread, + * not exposing it as inheritable for child threads. + * @param localeContext the current LocaleContext, or null to reset + * the thread-bound context + */ + public static void setLocaleContext(LocaleContext localeContext) { + setLocaleContext(localeContext, false); + } + + /** + * Associate the given LocaleContext with the current thread. + * @param localeContext the current LocaleContext, or null to reset + * the thread-bound context + * @param inheritable whether to expose the LocaleContext as inheritable + * for child threads (using an {@link java.lang.InheritableThreadLocal}) + */ + public static void setLocaleContext(LocaleContext localeContext, boolean inheritable) { + if (inheritable) { + inheritableLocaleContextHolder.set(localeContext); + localeContextHolder.set(null); + } + else { + localeContextHolder.set(localeContext); + inheritableLocaleContextHolder.set(null); + } + } + + /** + * Return the LocaleContext associated with the current thread, if any. + * @return the current LocaleContext, or null if none + */ + public static LocaleContext getLocaleContext() { + LocaleContext localeContext = (LocaleContext) localeContextHolder.get(); + if (localeContext == null) { + localeContext = (LocaleContext) inheritableLocaleContextHolder.get(); + } + return localeContext; + } + + /** + * Associate the given Locale with the current thread. + *

Will implicitly create a LocaleContext for the given Locale, + * not exposing it as inheritable for child threads. + * @param locale the current Locale, or null to reset + * the thread-bound context + * @see SimpleLocaleContext#SimpleLocaleContext(java.util.Locale) + */ + public static void setLocale(Locale locale) { + setLocale(locale, false); + } + + /** + * Associate the given Locale with the current thread. + *

Will implicitly create a LocaleContext for the given Locale. + * @param locale the current Locale, or null to reset + * the thread-bound context + * @param inheritable whether to expose the LocaleContext as inheritable + * for child threads (using an {@link java.lang.InheritableThreadLocal}) + * @see SimpleLocaleContext#SimpleLocaleContext(java.util.Locale) + */ + public static void setLocale(Locale locale, boolean inheritable) { + LocaleContext localeContext = (locale != null ? new SimpleLocaleContext(locale) : null); + setLocaleContext(localeContext, inheritable); + } + + /** + * Return the Locale associated with the current thread, if any, + * or the system default Locale else. + * @return the current Locale, or the system default Locale if no + * specific Locale has been associated with the current thread + * @see LocaleContext#getLocale() + * @see java.util.Locale#getDefault() + */ + public static Locale getLocale() { + LocaleContext localeContext = getLocaleContext(); + return (localeContext != null ? localeContext.getLocale() : Locale.getDefault()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java b/org.springframework.context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java new file mode 100644 index 00000000000..9f0b8c04da9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java @@ -0,0 +1,53 @@ +/* + * 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.context.i18n; + +import java.util.Locale; + +import org.springframework.util.Assert; + +/** + * Simple implementation of the {@link LocaleContext} interface, + * always returning a specified Locale. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleLocaleContext implements LocaleContext { + + private final Locale locale; + + + /** + * Create a new SimpleLocaleContext that exposes the specified Locale. + * Every getLocale() will return this Locale. + * @param locale the Locale to expose + */ + public SimpleLocaleContext(Locale locale) { + Assert.notNull(locale, "Locale must not be null"); + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + public String toString() { + return this.locale.toString(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/i18n/package.html b/org.springframework.context/src/main/java/org/springframework/context/i18n/package.html new file mode 100644 index 00000000000..977d9a45fee --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/i18n/package.html @@ -0,0 +1,8 @@ + + + +Abstraction for determining the current Locale, +plus global holder that exposes a thread-bound Locale. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/package.html b/org.springframework.context/src/main/java/org/springframework/context/package.html new file mode 100644 index 00000000000..a398a9801b5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/package.html @@ -0,0 +1,16 @@ + + + +This package builds on the beans package to add support for +message sources and for the Observer design pattern, and the +ability for application objects to obtain resources using a +consistent API. + +

There is no necessity for Spring applications to depend +on ApplicationContext or even BeanFactory functionality +explicitly. One of the strengths of the Spring architecture +is that application objects can often be configured without +any dependency on Spring-specific APIs. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java new file mode 100644 index 00000000000..411d3e74377 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -0,0 +1,1204 @@ +/* + * 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.context.support; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.support.ResourceEditorRegistrar; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.Lifecycle; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStartedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.core.JdkVersion; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Abstract implementation of the {@link org.springframework.context.ApplicationContext} + * interface. Doesn't mandate the type of storage used for configuration; simply + * implements common context functionality. Uses the Template Method design pattern, + * requiring concrete subclasses to implement abstract methods. + * + *

In contrast to a plain BeanFactory, an ApplicationContext is supposed + * to detect special beans defined in its internal bean factory: + * Therefore, this class automatically registers + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessors}, + * {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessors} + * and {@link org.springframework.context.ApplicationListener ApplicationListeners} + * which are defined as beans in the context. + * + *

A {@link org.springframework.context.MessageSource} may also be supplied + * as a bean in the context, with the name "messageSource"; otherwise, message + * resolution is delegated to the parent context. Furthermore, a multicaster + * for application events can be supplied as "applicationEventMulticaster" bean + * of type {@link org.springframework.context.event.ApplicationEventMulticaster} + * in the context; otherwise, a default multicaster of type + * {@link org.springframework.context.event.SimpleApplicationEventMulticaster} will be used. + * + *

Implements resource loading through extending + * {@link org.springframework.core.io.DefaultResourceLoader}. + * Consequently treats non-URL resource paths as class path resources + * (supporting full class path resource names that include the package path, + * e.g. "mypackage/myresource.dat"), unless the {@link #getResourceByPath} + * method is overwritten in a subclass. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Mark Fisher + * @since January 21, 2001 + * @see #refreshBeanFactory + * @see #getBeanFactory + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.MessageSource + */ +public abstract class AbstractApplicationContext extends DefaultResourceLoader + implements ConfigurableApplicationContext, DisposableBean { + + /** + * Name of the MessageSource bean in the factory. + * If none is supplied, message resolution is delegated to the parent. + * @see MessageSource + */ + public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource"; + + /** + * Name of the ApplicationEventMulticaster bean in the factory. + * If none is supplied, a default SimpleApplicationEventMulticaster is used. + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.SimpleApplicationEventMulticaster + */ + public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + + + static { + // Eagerly load the ContextClosedEvent class to avoid weird classloader issues + // on application shutdown in WebLogic 8.1. (Reported by Dustin Woods.) + ContextClosedEvent.class.getName(); + } + + + /** Logger used by this class. Available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Unique id for this context, if any */ + private String id = ObjectUtils.identityToString(this); + + /** Parent context */ + private ApplicationContext parent; + + /** BeanFactoryPostProcessors to apply on refresh */ + private final List beanFactoryPostProcessors = new ArrayList(); + + /** Display name */ + private String displayName; + + /** System time in milliseconds when this context started */ + private long startupDate; + + /** Flag that indicates whether this context is currently active */ + private boolean active = false; + + /** Synchronization monitor for the "active" flag */ + private final Object activeMonitor = new Object(); + + /** Synchronization monitor for the "refresh" and "destroy" */ + private final Object startupShutdownMonitor = new Object(); + + /** Reference to the JVM shutdown hook, if registered */ + private Thread shutdownHook; + + /** ResourcePatternResolver used by this context */ + private ResourcePatternResolver resourcePatternResolver; + + /** MessageSource we delegate our implementation of this interface to */ + private MessageSource messageSource; + + /** Helper class used in event publishing */ + private ApplicationEventMulticaster applicationEventMulticaster; + + /** Statically specified listeners */ + private List applicationListeners = new ArrayList(); + + + /** + * Create a new AbstractApplicationContext with no parent. + */ + public AbstractApplicationContext() { + this(null); + } + + /** + * Create a new AbstractApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractApplicationContext(ApplicationContext parent) { + this.parent = parent; + this.resourcePatternResolver = getResourcePatternResolver(); + } + + + //--------------------------------------------------------------------- + // Implementation of ApplicationContext interface + //--------------------------------------------------------------------- + + /** + * Set the unique id of this application context. + *

Default is the object id of the context instance, or the name + * of the context bean if the context is itself defined as a bean. + * @param id the unique id of the context + */ + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + /** + * Return the parent context, or null if there is no parent + * (that is, this context is the root of the context hierarchy). + */ + public ApplicationContext getParent() { + return this.parent; + } + + /** + * Return this context's internal bean factory as AutowireCapableBeanFactory, + * if already available. + * @see #getBeanFactory() + */ + public AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException { + return getBeanFactory(); + } + + /** + * Set a friendly name for this context. + * Typically done during initialization of concrete context implementations. + */ + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + /** + * Return a friendly name for this context. + */ + public String getDisplayName() { + return (this.displayName != null ? this.displayName : getId()); + } + + /** + * Return the timestamp (ms) when this context was first loaded. + */ + public long getStartupDate() { + return this.startupDate; + } + + /** + * Publish the given event to all listeners. + *

Note: Listeners get initialized after the MessageSource, to be able + * to access it within listener implementations. Thus, MessageSource + * implementations cannot publish events. + * @param event the event to publish (may be application-specific or a + * standard framework event) + */ + public void publishEvent(ApplicationEvent event) { + Assert.notNull(event, "Event must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Publishing event in context [" + getId() + "]: " + event); + } + getApplicationEventMulticaster().multicastEvent(event); + if (this.parent != null) { + this.parent.publishEvent(event); + } + } + + /** + * Return the internal MessageSource used by the context. + * @return the internal MessageSource (never null) + * @throws IllegalStateException if the context has not been initialized yet + */ + private ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException { + if (this.applicationEventMulticaster == null) { + throw new IllegalStateException("ApplicationEventMulticaster not initialized - " + + "call 'refresh' before multicasting events via the context: " + this); + } + return this.applicationEventMulticaster; + } + + /** + * Return the ResourcePatternResolver to use for resolving location patterns + * into Resource instances. Default is a + * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}, + * supporting Ant-style location patterns. + *

Can be overridden in subclasses, for extended resolution strategies, + * for example in a web environment. + *

Do not call this when needing to resolve a location pattern. + * Call the context's getResources method instead, which + * will delegate to the ResourcePatternResolver. + * @return the ResourcePatternResolver for this context + * @see #getResources + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + */ + protected ResourcePatternResolver getResourcePatternResolver() { + return new PathMatchingResourcePatternResolver(this); + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurableApplicationContext interface + //--------------------------------------------------------------------- + + public void setParent(ApplicationContext parent) { + this.parent = parent; + } + + public void addBeanFactoryPostProcessor(BeanFactoryPostProcessor beanFactoryPostProcessor) { + this.beanFactoryPostProcessors.add(beanFactoryPostProcessor); + } + + /** + * Return the list of BeanFactoryPostProcessors that will get applied + * to the internal BeanFactory. + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + */ + public List getBeanFactoryPostProcessors() { + return this.beanFactoryPostProcessors; + } + + public void addApplicationListener(ApplicationListener listener) { + this.applicationListeners.add(listener); + } + + /** + * Return the list of statically specified ApplicationListeners. + * @see org.springframework.context.ApplicationListener + */ + public List getApplicationListeners() { + return this.applicationListeners; + } + + + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + registerBeanPostProcessors(beanFactory); + + // Initialize message source for this context. + initMessageSource(); + + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + onRefresh(); + + // Check for listener beans and register them. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + finishRefresh(); + } + + catch (BeansException ex) { + // Destroy already created singletons to avoid dangling resources. + beanFactory.destroySingletons(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + } + } + + /** + * Prepare this context for refreshing, setting its startup date and + * active flag. + */ + protected void prepareRefresh() { + this.startupDate = System.currentTimeMillis(); + + synchronized (this.activeMonitor) { + this.active = true; + } + + if (logger.isInfoEnabled()) { + logger.info("Refreshing " + this); + } + } + + /** + * Tell the subclass to refresh the internal bean factory. + * @return the fresh BeanFactory instance + * @see #refreshBeanFactory() + * @see #getBeanFactory() + */ + protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + refreshBeanFactory(); + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + + if (logger.isInfoEnabled()) { + logger.info("Bean factory for application context [" + getId() + "]: " + + ObjectUtils.identityToString(beanFactory)); + } + if (logger.isDebugEnabled()) { + logger.debug(beanFactory.getBeanDefinitionCount() + " beans defined in " + this); + } + + return beanFactory; + } + + /** + * Configure the factory's standard context characteristics, + * such as the context's ClassLoader and post-processors. + * @param beanFactory the BeanFactory to configure + */ + protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { + // Tell the internal bean factory to use the context's class loader. + beanFactory.setBeanClassLoader(getClassLoader()); + + // Populate the bean factory with context-specific resource editors. + beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this)); + + // Configure the bean factory with context callbacks. + beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); + beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); + beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); + beanFactory.ignoreDependencyInterface(MessageSourceAware.class); + beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); + + // BeanFactory interface not registered as resolvable type in a plain factory. + // MessageSource registered (and found for autowiring) as a bean. + beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); + beanFactory.registerResolvableDependency(ResourceLoader.class, this); + beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); + beanFactory.registerResolvableDependency(ApplicationContext.class, this); + + // Detect a LoadTimeWeaver and prepare for weaving, if found. + if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME) && JdkVersion.isAtLeastJava15()) { + // Register the (JDK 1.5 specific) LoadTimeWeaverAwareProcessor. + try { + Class ltwapClass = ClassUtils.forName( + "org.springframework.context.weaving.LoadTimeWeaverAwareProcessor", + AbstractApplicationContext.class.getClassLoader()); + BeanPostProcessor ltwap = (BeanPostProcessor) BeanUtils.instantiateClass(ltwapClass); + ((BeanFactoryAware) ltwap).setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(ltwap); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Spring's LoadTimeWeaverAwareProcessor class is not available"); + } + // Set a temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + } + + /** + * Modify the application context's internal bean factory after its standard + * initialization. All bean definitions will have been loaded, but no beans + * will have been instantiated yet. This allows for registering special + * BeanPostProcessors etc in certain ApplicationContext implementations. + * @param beanFactory the bean factory used by the application context + */ + protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + } + + /** + * Instantiate and invoke all registered BeanFactoryPostProcessor beans, + * respecting explicit order if given. + *

Must be called before singleton instantiation. + */ + protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { + // Invoke factory processors registered with the context instance. + for (Iterator it = getBeanFactoryPostProcessors().iterator(); it.hasNext();) { + BeanFactoryPostProcessor factoryProcessor = (BeanFactoryPostProcessor) it.next(); + factoryProcessor.postProcessBeanFactory(beanFactory); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + String[] postProcessorNames = + beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); + + // Separate between BeanFactoryPostProcessors that implement PriorityOrdered, + // Ordered, and the rest. + List priorityOrderedPostProcessors = new ArrayList(); + List orderedPostProcessorNames = new ArrayList(); + List nonOrderedPostProcessorNames = new ArrayList(); + for (int i = 0; i < postProcessorNames.length; i++) { + if (isTypeMatch(postProcessorNames[i], PriorityOrdered.class)) { + priorityOrderedPostProcessors.add(beanFactory.getBean(postProcessorNames[i])); + } + else if (isTypeMatch(postProcessorNames[i], Ordered.class)) { + orderedPostProcessorNames.add(postProcessorNames[i]); + } + else { + nonOrderedPostProcessorNames.add(postProcessorNames[i]); + } + } + + // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered. + Collections.sort(priorityOrderedPostProcessors, new OrderComparator()); + invokeBeanFactoryPostProcessors(beanFactory, priorityOrderedPostProcessors); + + // Next, invoke the BeanFactoryPostProcessors that implement Ordered. + List orderedPostProcessors = new ArrayList(); + for (Iterator it = orderedPostProcessorNames.iterator(); it.hasNext();) { + String postProcessorName = (String) it.next(); + orderedPostProcessors.add(getBean(postProcessorName)); + } + Collections.sort(orderedPostProcessors, new OrderComparator()); + invokeBeanFactoryPostProcessors(beanFactory, orderedPostProcessors); + + // Finally, invoke all other BeanFactoryPostProcessors. + List nonOrderedPostProcessors = new ArrayList(); + for (Iterator it = nonOrderedPostProcessorNames.iterator(); it.hasNext();) { + String postProcessorName = (String) it.next(); + nonOrderedPostProcessors.add(getBean(postProcessorName)); + } + invokeBeanFactoryPostProcessors(beanFactory, nonOrderedPostProcessors); + } + + /** + * Invoke the given BeanFactoryPostProcessor beans. + */ + private void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List postProcessors) { + for (Iterator it = postProcessors.iterator(); it.hasNext();) { + BeanFactoryPostProcessor postProcessor = (BeanFactoryPostProcessor) it.next(); + postProcessor.postProcessBeanFactory(beanFactory); + } + } + + /** + * Instantiate and invoke all registered BeanPostProcessor beans, + * respecting explicit order if given. + *

Must be called before any instantiation of application beans. + */ + protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); + + // Register BeanPostProcessorChecker that logs an info message when + // a bean is created during BeanPostProcessor instantiation, i.e. when + // a bean is not eligible for getting processed by all BeanPostProcessors. + int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; + beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + + // Separate between BeanPostProcessors that implement PriorityOrdered, + // Ordered, and the rest. + List priorityOrderedPostProcessors = new ArrayList(); + List orderedPostProcessorNames = new ArrayList(); + List nonOrderedPostProcessorNames = new ArrayList(); + for (int i = 0; i < postProcessorNames.length; i++) { + if (isTypeMatch(postProcessorNames[i], PriorityOrdered.class)) { + priorityOrderedPostProcessors.add(beanFactory.getBean(postProcessorNames[i])); + } + else if (isTypeMatch(postProcessorNames[i], Ordered.class)) { + orderedPostProcessorNames.add(postProcessorNames[i]); + } + else { + nonOrderedPostProcessorNames.add(postProcessorNames[i]); + } + } + + // First, register the BeanPostProcessors that implement PriorityOrdered. + Collections.sort(priorityOrderedPostProcessors, new OrderComparator()); + registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); + + // Next, register the BeanPostProcessors that implement Ordered. + List orderedPostProcessors = new ArrayList(); + for (Iterator it = orderedPostProcessorNames.iterator(); it.hasNext();) { + String postProcessorName = (String) it.next(); + orderedPostProcessors.add(getBean(postProcessorName)); + } + Collections.sort(orderedPostProcessors, new OrderComparator()); + registerBeanPostProcessors(beanFactory, orderedPostProcessors); + + // Finally, register all other BeanPostProcessors. + List nonOrderedPostProcessors = new ArrayList(); + for (Iterator it = nonOrderedPostProcessorNames.iterator(); it.hasNext();) { + String postProcessorName = (String) it.next(); + nonOrderedPostProcessors.add(getBean(postProcessorName)); + } + registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); + } + + /** + * Register the given BeanPostProcessor beans. + */ + private void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory, List postProcessors) { + for (Iterator it = postProcessors.iterator(); it.hasNext();) { + BeanPostProcessor postProcessor = (BeanPostProcessor) it.next(); + beanFactory.addBeanPostProcessor(postProcessor); + } + } + + /** + * Initialize the MessageSource. + * Use parent's if none defined in this context. + */ + protected void initMessageSource() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { + this.messageSource = (MessageSource) beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); + // Make MessageSource aware of parent MessageSource. + if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { + HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; + if (hms.getParentMessageSource() == null) { + // Only set parent context as parent MessageSource if no parent MessageSource + // registered already. + hms.setParentMessageSource(getInternalParentMessageSource()); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Using MessageSource [" + this.messageSource + "]"); + } + } + else { + // Use empty MessageSource to be able to accept getMessage calls. + DelegatingMessageSource dms = new DelegatingMessageSource(); + dms.setParentMessageSource(getInternalParentMessageSource()); + this.messageSource = dms; + beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME + + "': using default [" + this.messageSource + "]"); + } + } + } + + /** + * Initialize the ApplicationEventMulticaster. + * Uses SimpleApplicationEventMulticaster if none defined in the context. + * @see org.springframework.context.event.SimpleApplicationEventMulticaster + */ + protected void initApplicationEventMulticaster() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { + this.applicationEventMulticaster = (ApplicationEventMulticaster) + beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); + if (logger.isDebugEnabled()) { + logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]"); + } + } + else { + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(); + beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate ApplicationEventMulticaster with name '" + + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + + "': using default [" + this.applicationEventMulticaster + "]"); + } + } + } + + /** + * Template method which can be overridden to add context-specific refresh work. + * Called on initialization of special beans, before instantiation of singletons. + *

This implementation is empty. + * @throws BeansException in case of errors + * @see #refresh() + */ + protected void onRefresh() throws BeansException { + // For subclasses: do nothing by default. + } + + /** + * Add beans that implement ApplicationListener as listeners. + * Doesn't affect other listeners, which can be added without being beans. + */ + protected void registerListeners() { + // Register statically specified listeners first. + for (Iterator it = getApplicationListeners().iterator(); it.hasNext();) { + addListener((ApplicationListener) it.next()); + } + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let post-processors apply to them! + Collection listenerBeans = getBeansOfType(ApplicationListener.class, true, false).values(); + for (Iterator it = listenerBeans.iterator(); it.hasNext();) { + addListener((ApplicationListener) it.next()); + } + } + + /** + * Subclasses can invoke this method to register a listener. + * Any beans in the context that are listeners are automatically added. + * @param listener the listener to register + */ + protected void addListener(ApplicationListener listener) { + getApplicationEventMulticaster().addApplicationListener(listener); + } + + /** + * Finish the initialization of this context's bean factory, + * initializing all remaining singleton beans. + */ + protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Stop using the temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + beanFactory.preInstantiateSingletons(); + } + + /** + * Finish the refresh of this context, publishing the + * {@link org.springframework.context.event.ContextRefreshedEvent}. + */ + protected void finishRefresh() { + publishEvent(new ContextRefreshedEvent(this)); + } + + /** + * Cancel this context's refresh attempt, resetting the active flag + * after an exception got thrown. + * @param ex the exception that led to the cancellation + */ + protected void cancelRefresh(BeansException ex) { + synchronized (this.activeMonitor) { + this.active = false; + } + } + + + /** + * Register a shutdown hook with the JVM runtime, closing this context + * on JVM shutdown unless it has already been closed at that time. + *

Delegates to doClose() for the actual closing procedure. + * @see java.lang.Runtime#addShutdownHook + * @see #close() + * @see #doClose() + */ + public void registerShutdownHook() { + if (this.shutdownHook == null) { + // No shutdown hook registered yet. + this.shutdownHook = new Thread() { + public void run() { + doClose(); + } + }; + Runtime.getRuntime().addShutdownHook(this.shutdownHook); + } + } + + /** + * DisposableBean callback for destruction of this instance. + * Only called when the ApplicationContext itself is running + * as a bean in another BeanFactory or ApplicationContext, + * which is rather unusual. + *

The close method is the native way to + * shut down an ApplicationContext. + * @see #close() + * @see org.springframework.beans.factory.access.SingletonBeanFactoryLocator + */ + public void destroy() { + close(); + } + + /** + * Close this application context, destroying all beans in its bean factory. + *

Delegates to doClose() for the actual closing procedure. + * Also removes a JVM shutdown hook, if registered, as it's not needed anymore. + * @see #doClose() + * @see #registerShutdownHook() + */ + public void close() { + synchronized (this.startupShutdownMonitor) { + doClose(); + // If we registered a JVM shutdown hook, we don't need it anymore now: + // We've already explicitly closed the context. + if (this.shutdownHook != null) { + Runtime.getRuntime().removeShutdownHook(this.shutdownHook); + } + } + } + + /** + * Actually performs context closing: publishes a ContextClosedEvent and + * destroys the singletons in the bean factory of this application context. + *

Called by both close() and a JVM shutdown hook, if any. + * @see org.springframework.context.event.ContextClosedEvent + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons() + * @see #close() + * @see #registerShutdownHook() + */ + protected void doClose() { + if (isActive()) { + if (logger.isInfoEnabled()) { + logger.info("Closing " + this); + } + try { + // Publish shutdown event. + publishEvent(new ContextClosedEvent(this)); + } + catch (Throwable ex) { + logger.error("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); + } + // Stop all Lifecycle beans, to avoid delays during individual destruction. + Map lifecycleBeans = getLifecycleBeans(); + for (Iterator it = new LinkedHashSet(lifecycleBeans.keySet()).iterator(); it.hasNext();) { + String beanName = (String) it.next(); + doStop(lifecycleBeans, beanName); + } + // Destroy all cached singletons in the context's BeanFactory. + destroyBeans(); + // Close the state of this context itself. + closeBeanFactory(); + onClose(); + synchronized (this.activeMonitor) { + this.active = false; + } + } + } + + /** + * Template method for destroying all beans that this context manages. + * The default implementation destroy all cached singletons in this context, + * invoking DisposableBean.destroy() and/or the specified + * "destroy-method". + *

Can be overridden to add context-specific bean destruction steps + * right before or right after standard singleton destruction, + * while the context's BeanFactory is still active. + * @see #getBeanFactory() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons() + */ + protected void destroyBeans() { + getBeanFactory().destroySingletons(); + } + + /** + * Template method which can be overridden to add context-specific shutdown work. + * The default implementation is empty. + *

Called at the end of {@link #doClose}'s shutdown procedure, after + * this context's BeanFactory has been closed. If custom shutdown logic + * needs to execute while the BeanFactory is still active, override + * the {@link #destroyBeans()} method instead. + */ + protected void onClose() { + // For subclasses: do nothing by default. + } + + public boolean isActive() { + synchronized (this.activeMonitor) { + return this.active; + } + } + + + //--------------------------------------------------------------------- + // Implementation of BeanFactory interface + //--------------------------------------------------------------------- + + public Object getBean(String name) throws BeansException { + return getBeanFactory().getBean(name); + } + + public Object getBean(String name, Class requiredType) throws BeansException { + return getBeanFactory().getBean(name, requiredType); + } + + public Object getBean(String name, Object[] args) throws BeansException { + return getBeanFactory().getBean(name, args); + } + + public boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isPrototype(name); + } + + public boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException { + return getBeanFactory().isTypeMatch(name, targetType); + } + + public Class getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + public String[] getAliases(String name) { + return getBeanFactory().getAliases(name); + } + + + //--------------------------------------------------------------------- + // Implementation of ListableBeanFactory interface + //--------------------------------------------------------------------- + + public boolean containsBeanDefinition(String name) { + return getBeanFactory().containsBeanDefinition(name); + } + + public int getBeanDefinitionCount() { + return getBeanFactory().getBeanDefinitionCount(); + } + + public String[] getBeanDefinitionNames() { + return getBeanFactory().getBeanDefinitionNames(); + } + + public String[] getBeanNamesForType(Class type) { + return getBeanFactory().getBeanNamesForType(type); + } + + public String[] getBeanNamesForType(Class type, boolean includePrototypes, boolean allowEagerInit) { + return getBeanFactory().getBeanNamesForType(type, includePrototypes, allowEagerInit); + } + + public Map getBeansOfType(Class type) throws BeansException { + return getBeanFactory().getBeansOfType(type); + } + + public Map getBeansOfType(Class type, boolean includePrototypes, boolean allowEagerInit) + throws BeansException { + + return getBeanFactory().getBeansOfType(type, includePrototypes, allowEagerInit); + } + + + //--------------------------------------------------------------------- + // Implementation of HierarchicalBeanFactory interface + //--------------------------------------------------------------------- + + public BeanFactory getParentBeanFactory() { + return getParent(); + } + + public boolean containsLocalBean(String name) { + return getBeanFactory().containsLocalBean(name); + } + + /** + * Return the internal bean factory of the parent context if it implements + * ConfigurableApplicationContext; else, return the parent context itself. + * @see org.springframework.context.ConfigurableApplicationContext#getBeanFactory + */ + protected BeanFactory getInternalParentBeanFactory() { + return (getParent() instanceof ConfigurableApplicationContext) ? + ((ConfigurableApplicationContext) getParent()).getBeanFactory() : (BeanFactory) getParent(); + } + + + //--------------------------------------------------------------------- + // Implementation of MessageSource interface + //--------------------------------------------------------------------- + + public String getMessage(String code, Object args[], String defaultMessage, Locale locale) { + return getMessageSource().getMessage(code, args, defaultMessage, locale); + } + + public String getMessage(String code, Object args[], Locale locale) throws NoSuchMessageException { + return getMessageSource().getMessage(code, args, locale); + } + + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + return getMessageSource().getMessage(resolvable, locale); + } + + /** + * Return the internal MessageSource used by the context. + * @return the internal MessageSource (never null) + * @throws IllegalStateException if the context has not been initialized yet + */ + private MessageSource getMessageSource() throws IllegalStateException { + if (this.messageSource == null) { + throw new IllegalStateException("MessageSource not initialized - " + + "call 'refresh' before accessing messages via the context: " + this); + } + return this.messageSource; + } + + /** + * Return the internal message source of the parent context if it is an + * AbstractApplicationContext too; else, return the parent context itself. + */ + protected MessageSource getInternalParentMessageSource() { + return (getParent() instanceof AbstractApplicationContext) ? + ((AbstractApplicationContext) getParent()).messageSource : getParent(); + } + + + //--------------------------------------------------------------------- + // Implementation of ResourcePatternResolver interface + //--------------------------------------------------------------------- + + public Resource[] getResources(String locationPattern) throws IOException { + return this.resourcePatternResolver.getResources(locationPattern); + } + + + //--------------------------------------------------------------------- + // Implementation of Lifecycle interface + //--------------------------------------------------------------------- + + public void start() { + Map lifecycleBeans = getLifecycleBeans(); + for (Iterator it = new LinkedHashSet(lifecycleBeans.keySet()).iterator(); it.hasNext();) { + String beanName = (String) it.next(); + doStart(lifecycleBeans, beanName); + } + publishEvent(new ContextStartedEvent(this)); + } + + public void stop() { + Map lifecycleBeans = getLifecycleBeans(); + for (Iterator it = new LinkedHashSet(lifecycleBeans.keySet()).iterator(); it.hasNext();) { + String beanName = (String) it.next(); + doStop(lifecycleBeans, beanName); + } + publishEvent(new ContextStoppedEvent(this)); + } + + public boolean isRunning() { + Iterator it = getLifecycleBeans().values().iterator(); + while (it.hasNext()) { + Lifecycle lifecycle = (Lifecycle) it.next(); + if (!lifecycle.isRunning()) { + return false; + } + } + return true; + } + + /** + * Return a Map of all singleton beans that implement the + * Lifecycle interface in this context. + * @return Map of Lifecycle beans with bean name as key + */ + private Map getLifecycleBeans() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + String[] beanNames = beanFactory.getSingletonNames(); + Map beans = new LinkedHashMap(); + for (int i = 0; i < beanNames.length; i++) { + Object bean = beanFactory.getSingleton(beanNames[i]); + if (bean instanceof Lifecycle) { + beans.put(beanNames[i], bean); + } + } + return beans; + } + + /** + * Start the specified bean as part of the given set of Lifecycle beans, + * making sure that any beans that it depends on are started first. + * @param lifecycleBeans Map with bean name as key and Lifecycle instance as value + * @param beanName the name of the bean to start + */ + private void doStart(Map lifecycleBeans, String beanName) { + Lifecycle bean = (Lifecycle) lifecycleBeans.get(beanName); + if (bean != null) { + String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName); + for (int i = 0; i < dependenciesForBean.length; i++) { + doStart(lifecycleBeans, dependenciesForBean[i]); + } + if (!bean.isRunning()) { + bean.start(); + } + lifecycleBeans.remove(beanName); + } + } + + /** + * Stop the specified bean as part of the given set of Lifecycle beans, + * making sure that any beans that depends on it are stopped first. + * @param lifecycleBeans Map with bean name as key and Lifecycle instance as value + * @param beanName the name of the bean to stop + */ + private void doStop(Map lifecycleBeans, String beanName) { + Lifecycle bean = (Lifecycle) lifecycleBeans.get(beanName); + if (bean != null) { + String[] dependentBeans = getBeanFactory().getDependentBeans(beanName); + for (int i = 0; i < dependentBeans.length; i++) { + doStop(lifecycleBeans, dependentBeans[i]); + } + if (bean.isRunning()) { + bean.stop(); + } + lifecycleBeans.remove(beanName); + } + } + + + //--------------------------------------------------------------------- + // Abstract methods that must be implemented by subclasses + //--------------------------------------------------------------------- + + /** + * Subclasses must implement this method to perform the actual configuration load. + * The method is invoked by {@link #refresh()} before any other initialization work. + *

A subclass will either create a new bean factory and hold a reference to it, + * or return a single BeanFactory instance that it holds. In the latter case, it will + * usually throw an IllegalStateException if refreshing the context more than once. + * @throws BeansException if initialization of the bean factory failed + * @throws IllegalStateException if already initialized and multiple refresh + * attempts are not supported + */ + protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException; + + /** + * Subclasses must implement this method to release their internal bean factory. + * This method gets invoked by {@link #close()} after all other shutdown work. + *

Should never throw an exception but rather log shutdown failures. + */ + protected abstract void closeBeanFactory(); + + /** + * Subclasses must return their internal bean factory here. They should implement the + * lookup efficiently, so that it can be called repeatedly without a performance penalty. + *

Note: Subclasses should check whether the context is still active before + * returning the internal bean factory. The internal factory should generally be + * considered unavailable once the context has been closed. + * @return this application context's internal bean factory (never null) + * @throws IllegalStateException if the context does not hold an internal bean factory yet + * (usually if {@link #refresh()} has never been called) or if the context has been + * closed already + * @see #refreshBeanFactory() + * @see #closeBeanFactory() + */ + public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + + + /** + * Return information about this context. + */ + public String toString() { + StringBuffer sb = new StringBuffer(getId()); + sb.append(": display name [").append(getDisplayName()); + sb.append("]; startup date [").append(new Date(getStartupDate())); + sb.append("]; "); + ApplicationContext parent = getParent(); + if (parent == null) { + sb.append("root of context hierarchy"); + } + else { + sb.append("parent: ").append(parent.getId()); + } + return sb.toString(); + } + + + /** + * BeanPostProcessor that logs an info message when a bean is created during + * BeanPostProcessor instantiation, i.e. when a bean is not eligible for + * getting processed by all BeanPostProcessors. + */ + private class BeanPostProcessorChecker implements BeanPostProcessor { + + private final ConfigurableListableBeanFactory beanFactory; + + private final int beanPostProcessorTargetCount; + + public BeanPostProcessorChecker(ConfigurableListableBeanFactory beanFactory, int beanPostProcessorTargetCount) { + this.beanFactory = beanFactory; + this.beanPostProcessorTargetCount = beanPostProcessorTargetCount; + } + + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (!(bean instanceof BeanPostProcessor) && + this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) { + if (logger.isInfoEnabled()) { + logger.info("Bean '" + beanName + "' is not eligible for getting processed by all " + + "BeanPostProcessors (for example: not eligible for auto-proxying)"); + } + } + return bean; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractMessageSource.java new file mode 100644 index 00000000000..47dad021c40 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -0,0 +1,348 @@ +/* + * 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.context.support; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.util.ObjectUtils; + +/** + * Abstract implementation of the {@link HierarchicalMessageSource} interface, + * implementing common handling of message variants, making it easy + * to implement a specific strategy for a concrete MessageSource. + * + *

Subclasses must implement the abstract {@link #resolveCode} + * method. For efficient resolution of messages without arguments, the + * {@link #resolveCodeWithoutArguments} method should be overridden + * as well, resolving messages without a MessageFormat being involved. + * + *

Note: By default, message texts are only parsed through + * MessageFormat if arguments have been passed in for the message. In case + * of no arguments, message texts will be returned as-is. As a consequence, + * you should only use MessageFormat escaping for messages with actual + * arguments, and keep all other messages unescaped. If you prefer to + * escape all messages, set the "alwaysUseMessageFormat" flag to "true". + * + *

Supports not only MessageSourceResolvables as primary messages + * but also resolution of message arguments that are in turn + * MessageSourceResolvables themselves. + * + *

This class does not implement caching of messages per code, thus + * subclasses can dynamically change messages over time. Subclasses are + * encouraged to cache their messages in a modification-aware fashion, + * allowing for hot deployment of updated messages. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #resolveCode(String, java.util.Locale) + * @see #resolveCodeWithoutArguments(String, java.util.Locale) + * @see #setAlwaysUseMessageFormat + * @see java.text.MessageFormat + */ +public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { + + private MessageSource parentMessageSource; + + private boolean useCodeAsDefaultMessage = false; + + + public void setParentMessageSource(MessageSource parent) { + this.parentMessageSource = parent; + } + + public MessageSource getParentMessageSource() { + return this.parentMessageSource; + } + + /** + * Set whether to use the message code as default message instead of + * throwing a NoSuchMessageException. Useful for development and debugging. + * Default is "false". + *

Note: In case of a MessageSourceResolvable with multiple codes + * (like a FieldError) and a MessageSource that has a parent MessageSource, + * do not activate "useCodeAsDefaultMessage" in the parent: + * Else, you'll get the first code returned as message by the parent, + * without attempts to check further codes. + *

To be able to work with "useCodeAsDefaultMessage" turned on in the parent, + * AbstractMessageSource and AbstractApplicationContext contain special checks + * to delegate to the internal getMessageInternal method if available. + * In general, it is recommended to just use "useCodeAsDefaultMessage" during + * development and not rely on it in production in the first place, though. + * @see #getMessage(String, Object[], Locale) + * @see #getMessageInternal + * @see org.springframework.validation.FieldError + */ + public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) { + this.useCodeAsDefaultMessage = useCodeAsDefaultMessage; + } + + /** + * Return whether to use the message code as default message instead of + * throwing a NoSuchMessageException. Useful for development and debugging. + * Default is "false". + *

Alternatively, consider overriding the getDefaultMessage + * method to return a custom fallback message for an unresolvable code. + * @see #getDefaultMessage(String) + */ + protected boolean isUseCodeAsDefaultMessage() { + return this.useCodeAsDefaultMessage; + } + + + public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) { + String msg = getMessageInternal(code, args, locale); + if (msg != null) { + return msg; + } + if (defaultMessage == null) { + String fallback = getDefaultMessage(code); + if (fallback != null) { + return fallback; + } + } + return renderDefaultMessage(defaultMessage, args, locale); + } + + public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { + String msg = getMessageInternal(code, args, locale); + if (msg != null) { + return msg; + } + String fallback = getDefaultMessage(code); + if (fallback != null) { + return fallback; + } + throw new NoSuchMessageException(code, locale); + } + + public final String getMessage(MessageSourceResolvable resolvable, Locale locale) + throws NoSuchMessageException { + + String[] codes = resolvable.getCodes(); + if (codes == null) { + codes = new String[0]; + } + for (int i = 0; i < codes.length; i++) { + String msg = getMessageInternal(codes[i], resolvable.getArguments(), locale); + if (msg != null) { + return msg; + } + } + if (resolvable.getDefaultMessage() != null) { + return renderDefaultMessage(resolvable.getDefaultMessage(), resolvable.getArguments(), locale); + } + if (codes.length > 0) { + String fallback = getDefaultMessage(codes[0]); + if (fallback != null) { + return fallback; + } + } + throw new NoSuchMessageException(codes.length > 0 ? codes[codes.length - 1] : null, locale); + } + + + /** + * Resolve the given code and arguments as message in the given Locale, + * returning null if not found. Does not fall back to + * the code as default message. Invoked by getMessage methods. + * @param code the code to lookup up, such as 'calculator.noRateSet' + * @param args array of arguments that will be filled in for params + * within the message + * @param locale the Locale in which to do the lookup + * @return the resolved message, or null if not found + * @see #getMessage(String, Object[], String, Locale) + * @see #getMessage(String, Object[], Locale) + * @see #getMessage(MessageSourceResolvable, Locale) + * @see #setUseCodeAsDefaultMessage + */ + protected String getMessageInternal(String code, Object[] args, Locale locale) { + if (code == null) { + return null; + } + if (locale == null) { + locale = Locale.getDefault(); + } + Object[] argsToUse = args; + + if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { + // Optimized resolution: no arguments to apply, + // therefore no MessageFormat needs to be involved. + // Note that the default implementation still uses MessageFormat; + // this can be overridden in specific subclasses. + String message = resolveCodeWithoutArguments(code, locale); + if (message != null) { + return message; + } + } + + else { + // Resolve arguments eagerly, for the case where the message + // is defined in a parent MessageSource but resolvable arguments + // are defined in the child MessageSource. + argsToUse = resolveArguments(args, locale); + + MessageFormat messageFormat = resolveCode(code, locale); + if (messageFormat != null) { + synchronized (messageFormat) { + return messageFormat.format(argsToUse); + } + } + } + + // Not found -> check parent, if any. + return getMessageFromParent(code, argsToUse, locale); + } + + /** + * Try to retrieve the given message from the parent MessageSource, if any. + * @param code the code to lookup up, such as 'calculator.noRateSet' + * @param args array of arguments that will be filled in for params + * within the message + * @param locale the Locale in which to do the lookup + * @return the resolved message, or null if not found + * @see #getParentMessageSource() + */ + protected String getMessageFromParent(String code, Object[] args, Locale locale) { + MessageSource parent = getParentMessageSource(); + if (parent != null) { + if (parent instanceof AbstractMessageSource) { + // Call internal method to avoid getting the default code back + // in case of "useCodeAsDefaultMessage" being activated. + return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale); + } + else { + // Check parent MessageSource, returning null if not found there. + return parent.getMessage(code, args, null, locale); + } + } + // Not found in parent either. + return null; + } + + /** + * Return a fallback default message for the given code, if any. + *

Default is to return the code itself if "useCodeAsDefaultMessage" + * is activated, or return no fallback else. In case of no fallback, + * the caller will usually receive a NoSuchMessageException from + * getMessage. + * @param code the message code that we couldn't resolve + * and that we didn't receive an explicit default message for + * @return the default message to use, or null if none + * @see #setUseCodeAsDefaultMessage + */ + protected String getDefaultMessage(String code) { + if (isUseCodeAsDefaultMessage()) { + return code; + } + return null; + } + + /** + * Render the given default message String. The default message is + * passed in as specified by the caller and can be rendered into + * a fully formatted default message shown to the user. + *

The default implementation passes the String to formatMessage, + * resolving any argument placeholders found in them. Subclasses may override + * this method to plug in custom processing of default messages. + * @param defaultMessage the passed-in default message String + * @param args array of arguments that will be filled in for params within + * the message, or null if none. + * @param locale the Locale used for formatting + * @return the rendered default message (with resolved arguments) + * @see #formatMessage(String, Object[], java.util.Locale) + */ + protected String renderDefaultMessage(String defaultMessage, Object[] args, Locale locale) { + return formatMessage(defaultMessage, args, locale); + } + + + /** + * Searches through the given array of objects, find any + * MessageSourceResolvable objects and resolve them. + *

Allows for messages to have MessageSourceResolvables as arguments. + * @param args array of arguments for a message + * @param locale the locale to resolve through + * @return an array of arguments with any MessageSourceResolvables resolved + */ + protected Object[] resolveArguments(Object[] args, Locale locale) { + if (args == null) { + return new Object[0]; + } + List resolvedArgs = new ArrayList(args.length); + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof MessageSourceResolvable) { + resolvedArgs.add(getMessage((MessageSourceResolvable) args[i], locale)); + } + else { + resolvedArgs.add(args[i]); + } + } + return resolvedArgs.toArray(new Object[resolvedArgs.size()]); + } + + /** + * Subclasses can override this method to resolve a message without + * arguments in an optimized fashion, that is, to resolve a message + * without involving a MessageFormat. + *

The default implementation does use MessageFormat, + * through delegating to the resolveCode method. + * Subclasses are encouraged to replace this with optimized resolution. + *

Unfortunately, java.text.MessageFormat is not + * implemented in an efficient fashion. In particular, it does not + * detect that a message pattern doesn't contain argument placeholders + * in the first place. Therefore, it's advisable to circumvent + * MessageFormat completely for messages without arguments. + * @param code the code of the message to resolve + * @param locale the Locale to resolve the code for + * (subclasses are encouraged to support internationalization) + * @return the message String, or null if not found + * @see #resolveCode + * @see java.text.MessageFormat + */ + protected String resolveCodeWithoutArguments(String code, Locale locale) { + MessageFormat messageFormat = resolveCode(code, locale); + if (messageFormat != null) { + synchronized (messageFormat) { + return messageFormat.format(new Object[0]); + } + } + return null; + } + + /** + * Subclasses must implement this method to resolve a message. + *

Returns a MessageFormat instance rather than a message String, + * to allow for appropriate caching of MessageFormats in subclasses. + *

Subclasses are encouraged to provide optimized resolution + * for messages without arguments, not involving MessageFormat. + * See resolveCodeWithoutArguments javadoc for details. + * @param code the code of the message to resolve + * @param locale the Locale to resolve the code for + * (subclasses are encouraged to support internationalization) + * @return the MessageFormat for the message, or null if not found + * @see #resolveCodeWithoutArguments(String, java.util.Locale) + */ + protected abstract MessageFormat resolveCode(String code, Locale locale); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java new file mode 100644 index 00000000000..88e56ca179f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -0,0 +1,214 @@ +/* + * 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.context.support; + +import java.io.IOException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; + +/** + * Base class for {@link org.springframework.context.ApplicationContext} + * implementations which are supposed to support multiple refreshs, + * creating a new internal bean factory instance every time. + * Typically (but not necessarily), such a context will be driven by + * a set of config locations to load bean definitions from. + * + *

The only method to be implemented by subclasses is {@link #loadBeanDefinitions}, + * which gets invoked on each refresh. A concrete implementation is supposed to load + * bean definitions into the given + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory}, + * typically delegating to one or more specific bean definition readers. + * + *

Note that there is a similar base class for WebApplicationContexts. + * {@link org.springframework.web.context.support.AbstractRefreshableWebApplicationContext} + * provides the same subclassing strategy, but additionally pre-implements + * all context functionality for web environments. There is also a + * pre-defined way to receive config locations for a web context. + * + *

Concrete standalone subclasses of this base class, reading in a + * specific bean definition format, are {@link ClassPathXmlApplicationContext} + * and {@link FileSystemXmlApplicationContext}, which both derive from the + * common {@link AbstractXmlApplicationContext} base class. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see #loadBeanDefinitions + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.web.context.support.AbstractRefreshableWebApplicationContext + * @see AbstractXmlApplicationContext + * @see ClassPathXmlApplicationContext + * @see FileSystemXmlApplicationContext + */ +public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext { + + private Boolean allowBeanDefinitionOverriding; + + private Boolean allowCircularReferences; + + /** Bean factory for this context */ + private DefaultListableBeanFactory beanFactory; + + /** Synchronization monitor for the internal BeanFactory */ + private final Object beanFactoryMonitor = new Object(); + + + /** + * Create a new AbstractRefreshableApplicationContext with no parent. + */ + public AbstractRefreshableApplicationContext() { + } + + /** + * Create a new AbstractRefreshableApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractRefreshableApplicationContext(ApplicationContext parent) { + super(parent); + } + + + /** + * Set whether it should be allowed to override bean definitions by registering + * a different definition with the same name, automatically replacing the former. + * If not, an exception will be thrown. Default is "true". + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + */ + public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { + this.allowBeanDefinitionOverriding = Boolean.valueOf(allowBeanDefinitionOverriding); + } + + /** + * Set whether to allow circular references between beans - and automatically + * try to resolve them. + *

Default is "true". Turn this off to throw an exception when encountering + * a circular reference, disallowing them completely. + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + */ + public void setAllowCircularReferences(boolean allowCircularReferences) { + this.allowCircularReferences = Boolean.valueOf(allowCircularReferences); + } + + + /** + * This implementation performs an actual refresh of this context's underlying + * bean factory, shutting down the previous bean factory (if any) and + * initializing a fresh bean factory for the next phase of the context's lifecycle. + */ + protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + DefaultListableBeanFactory beanFactory = createBeanFactory(); + customizeBeanFactory(beanFactory); + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException( + "I/O error parsing XML document for application context [" + getDisplayName() + "]", ex); + } + } + + protected final void closeBeanFactory() { + synchronized (this.beanFactoryMonitor) { + this.beanFactory = null; + } + } + + /** + * Determine whether this context currently holds a bean factory, + * i.e. has been refreshed at least once and not been closed yet. + */ + protected final boolean hasBeanFactory() { + synchronized (this.beanFactoryMonitor) { + return (this.beanFactory != null); + } + } + + public final ConfigurableListableBeanFactory getBeanFactory() { + synchronized (this.beanFactoryMonitor) { + if (this.beanFactory == null) { + throw new IllegalStateException("BeanFactory not initialized or already closed - " + + "call 'refresh' before accessing beans via the ApplicationContext"); + } + return this.beanFactory; + } + } + + + /** + * Create an internal bean factory for this context. + * Called for each {@link #refresh()} attempt. + *

The default implementation creates a + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * with the {@link #getInternalParentBeanFactory() internal bean factory} of this + * context's parent as parent bean factory. Can be overridden in subclasses, + * for example to customize DefaultListableBeanFactory's settings. + * @return the bean factory for this context + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowEagerClassLoading + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + */ + protected DefaultListableBeanFactory createBeanFactory() { + return new DefaultListableBeanFactory(getInternalParentBeanFactory()); + } + + /** + * Customize the internal bean factory used by this context. + * Called for each {@link #refresh()} attempt. + *

The default implementation applies this context's + * {@link #setAllowBeanDefinitionOverriding "allowBeanDefinitionOverriding"} + * and {@link #setAllowCircularReferences "allowCircularReferences"} settings, + * if specified. Can be overridden in subclasses to customize any of + * {@link DefaultListableBeanFactory}'s settings. + * @param beanFactory the newly created bean factory for this context + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see DefaultListableBeanFactory#setAllowCircularReferences + * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + * @see DefaultListableBeanFactory#setAllowEagerClassLoading + */ + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { + if (this.allowBeanDefinitionOverriding != null) { + beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding.booleanValue()); + } + if (this.allowCircularReferences != null) { + beanFactory.setAllowCircularReferences(this.allowCircularReferences.booleanValue()); + } + } + + /** + * Load bean definitions into the given bean factory, typically through + * delegating to one or more bean definition readers. + * @param beanFactory the bean factory to load bean definitions into + * @throws IOException if loading of bean definition files failed + * @throws BeansException if parsing of the bean definitions failed + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) + throws IOException, BeansException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java new file mode 100644 index 00000000000..a2ff6dd3c85 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java @@ -0,0 +1,152 @@ +/* + * 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.context.support; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.SystemPropertyUtils; + +/** + * {@link AbstractRefreshableApplicationContext} subclass that adds common handling + * of specified config locations. Serves as base class for XML-based application + * context implementations such as {@link ClassPathXmlApplicationContext} and + * {@link FileSystemXmlApplicationContext}, as well as + * {@link org.springframework.web.context.support.XmlWebApplicationContext} and + * {@link org.springframework.web.portlet.context.XmlPortletApplicationContext}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #setConfigLocation + * @see #setConfigLocations + * @see #getDefaultConfigLocations + */ +public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext + implements BeanNameAware, InitializingBean { + + private String[] configLocations; + + private boolean setIdCalled = false; + + + /** + * Create a new AbstractRefreshableConfigApplicationContext with no parent. + */ + public AbstractRefreshableConfigApplicationContext() { + } + + /** + * Create a new AbstractRefreshableConfigApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractRefreshableConfigApplicationContext(ApplicationContext parent) { + super(parent); + } + + + /** + * Set the config locations for this application context in init-param style, + * i.e. with distinct locations separated by commas, semicolons or whitespace. + *

If not set, the implementation may use a default as appropriate. + */ + public void setConfigLocation(String location) { + setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS)); + } + + /** + * Set the config locations for this application context. + *

If not set, the implementation may use a default as appropriate. + */ + public void setConfigLocations(String[] locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not be null"); + this.configLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } + } + + /** + * Return an array of resource locations, referring to the XML bean definition + * files that this context should be built with. Can also include location + * patterns, which will get resolved via a ResourcePatternResolver. + *

The default implementation returns null. Subclasses can override + * this to provide a set of resource locations to load bean definitions from. + * @return an array of resource locations, or null if none + * @see #getResources + * @see #getResourcePatternResolver + */ + protected String[] getConfigLocations() { + return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations()); + } + + /** + * Return the default config locations to use, for the case where no + * explicit config locations have been specified. + *

The default implementation returns null, + * requiring explicit config locations. + * @return an array of default config locations, if any + * @see #setConfigLocations + */ + protected String[] getDefaultConfigLocations() { + return null; + } + + /** + * Resolve the given path, replacing placeholders with corresponding + * system property values if necessary. Applied to config locations. + * @param path the original file path + * @return the resolved file path + * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders + */ + protected String resolvePath(String path) { + return SystemPropertyUtils.resolvePlaceholders(path); + } + + + public void setId(String id) { + super.setId(id); + this.setIdCalled = true; + } + + /** + * Sets the id of this context to the bean name by default, + * for cases where the context instance is itself defined as a bean. + */ + public void setBeanName(String name) { + if (!this.setIdCalled) { + super.setId(name); + } + } + + /** + * Triggers {@link #refresh()} if not refreshed in the concrete context's + * constructor already. + */ + public void afterPropertiesSet() { + if (!isActive()) { + refresh(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java new file mode 100644 index 00000000000..24c7266e92f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java @@ -0,0 +1,129 @@ +/* + * 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.context.support; + +import java.io.IOException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.ResourceEntityResolver; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; + +/** + * Convenient base class for {@link org.springframework.context.ApplicationContext} + * implementations, drawing configuration from XML documents containing bean definitions + * understood by an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader}. + * + *

Subclasses just have to implement the {@link #getConfigResources} and/or + * the {@link #getConfigLocations} method. Furthermore, they might override + * the {@link #getResourceByPath} hook to interpret relative paths in an + * environment-specific fashion, and/or {@link #getResourcePatternResolver} + * for extended pattern resolution. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConfigResources + * @see #getConfigLocations + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ +public abstract class AbstractXmlApplicationContext extends AbstractRefreshableConfigApplicationContext { + + /** + * Create a new AbstractXmlApplicationContext with no parent. + */ + public AbstractXmlApplicationContext() { + } + + /** + * Create a new AbstractXmlApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractXmlApplicationContext(ApplicationContext parent) { + super(parent); + } + + + /** + * Loads the bean definitions via an XmlBeanDefinitionReader. + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see #initBeanDefinitionReader + * @see #loadBeanDefinitions + */ + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); + } + + /** + * Initialize the bean definition reader used for loading the bean + * definitions of this context. Default implementation is empty. + *

Can be overridden in subclasses, e.g. for turning off XML validation + * or using a different XmlBeanDefinitionParser implementation. + * @param beanDefinitionReader the bean definition reader used by this context + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setDocumentReaderClass + */ + protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) { + } + + /** + * Load the bean definitions with the given XmlBeanDefinitionReader. + *

The lifecycle of the bean factory is handled by the {@link #refreshBeanFactory} + * method; hence this method is just supposed to load and/or register bean definitions. + * @param reader the XmlBeanDefinitionReader to use + * @throws BeansException in case of bean registration errors + * @throws IOException if the required XML document isn't found + * @see #refreshBeanFactory + * @see #getConfigLocations + * @see #getResources + * @see #getResourcePatternResolver + */ + protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } + } + + /** + * Return an array of Resource objects, referring to the XML bean definition + * files that this context should be built with. + *

The default implementation returns null. Subclasses can override + * this to provide pre-built Resource objects rather than location Strings. + * @return an array of Resource objects, or null if none + * @see #getConfigLocations() + */ + protected Resource[] getConfigResources() { + return null; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java new file mode 100644 index 00000000000..e904facbd11 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java @@ -0,0 +1,79 @@ +/* + * 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.context.support; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.ResourceLoaderAware; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * implementation that passes the ApplicationContext to beans that + * implement the {@link ResourceLoaderAware}, {@link MessageSourceAware}, + * {@link ApplicationEventPublisherAware} and/or + * {@link ApplicationContextAware} interfaces. + * If all of them are implemented, they are satisfied in the given order. + * + *

Application contexts will automatically register this with their + * underlying bean factory. Applications do not use this directly. + * + * @author Juergen Hoeller + * @since 10.10.2003 + * @see org.springframework.context.ResourceLoaderAware + * @see org.springframework.context.MessageSourceAware + * @see org.springframework.context.ApplicationEventPublisherAware + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.support.AbstractApplicationContext#refresh() + */ +class ApplicationContextAwareProcessor implements BeanPostProcessor { + + private final ApplicationContext applicationContext; + + + /** + * Create a new ApplicationContextAwareProcessor for the given context. + */ + public ApplicationContextAwareProcessor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware) { + ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware) { + ((MessageSourceAware) bean).setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String name) { + return bean; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java b/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java new file mode 100644 index 00000000000..0a0acb70f7b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java @@ -0,0 +1,159 @@ +/* + * 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.context.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationContextException; + +/** + * Convenient superclass for application objects that want to be aware of + * the application context, e.g. for custom lookup of collaborating beans + * or for context-specific resource access. It saves the application + * context reference and provides an initialization callback method. + * Furthermore, it offers numerous convenience methods for message lookup. + * + *

There is no requirement to subclass this class: It just makes things + * a little easier if you need access to the context, e.g. for access to + * file resources or to the message source. Note that many application + * objects do not need to be aware of the application context at all, + * as they can receive collaborating beans via bean references. + * + *

Many framework classes are derived from this class, particularly + * within the web support. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.context.support.WebApplicationObjectSupport + */ +public abstract class ApplicationObjectSupport implements ApplicationContextAware { + + /** Logger that is available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** ApplicationContext this object runs in */ + private ApplicationContext applicationContext; + + /** MessageSourceAccessor for easy message access */ + private MessageSourceAccessor messageSourceAccessor; + + + public final void setApplicationContext(ApplicationContext context) throws BeansException { + if (context == null && !isContextRequired()) { + // Reset internal context state. + this.applicationContext = null; + this.messageSourceAccessor = null; + } + else if (this.applicationContext == null) { + // Initialize with passed-in context. + if (!requiredContextClass().isInstance(context)) { + throw new ApplicationContextException( + "Invalid application context: needs to be of type [" + requiredContextClass().getName() + "]"); + } + this.applicationContext = context; + this.messageSourceAccessor = new MessageSourceAccessor(context); + initApplicationContext(context); + } + else { + // Ignore reinitialization if same context passed in. + if (this.applicationContext != context) { + throw new ApplicationContextException( + "Cannot reinitialize with different application context: current one is [" + + this.applicationContext + "], passed-in one is [" + context + "]"); + } + } + } + + /** + * Determine whether this application object needs to run in an ApplicationContext. + *

Default is "false". Can be overridden to enforce running in a context + * (i.e. to throw IllegalStateException on accessors if outside a context). + * @see #getApplicationContext + * @see #getMessageSourceAccessor + */ + protected boolean isContextRequired() { + return false; + } + + /** + * Determine the context class that any context passed to + * setApplicationContext must be an instance of. + * Can be overridden in subclasses. + * @see #setApplicationContext + */ + protected Class requiredContextClass() { + return ApplicationContext.class; + } + + /** + * Subclasses can override this for custom initialization behavior. + * Gets called by setApplicationContext after setting the context instance. + *

Note: Does not get called on reinitialization of the context + * but rather just on first initialization of this object's context reference. + *

The default implementation calls the overloaded {@link #initApplicationContext()} + * method without ApplicationContext reference. + * @param context the containing ApplicationContext + * @throws ApplicationContextException in case of initialization errors + * @throws BeansException if thrown by ApplicationContext methods + * @see #setApplicationContext + */ + protected void initApplicationContext(ApplicationContext context) throws BeansException { + initApplicationContext(); + } + + /** + * Subclasses can override this for custom initialization behavior. + *

The default implementation is empty. Called by + * {@link #initApplicationContext(org.springframework.context.ApplicationContext)}. + * @throws ApplicationContextException in case of initialization errors + * @throws BeansException if thrown by ApplicationContext methods + * @see #setApplicationContext + */ + protected void initApplicationContext() throws BeansException { + } + + + /** + * Return the ApplicationContext that this object is associated with. + * @throws IllegalStateException if not running in an ApplicationContext + */ + public final ApplicationContext getApplicationContext() throws IllegalStateException { + if (this.applicationContext == null && isContextRequired()) { + throw new IllegalStateException( + "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); + } + return this.applicationContext; + } + + /** + * Return a MessageSourceAccessor for the application context + * used by this object, for easy message access. + * @throws IllegalStateException if not running in an ApplicationContext + */ + protected final MessageSourceAccessor getMessageSourceAccessor() throws IllegalStateException { + if (this.messageSourceAccessor == null && isContextRequired()) { + throw new IllegalStateException( + "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); + } + return this.messageSourceAccessor; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java new file mode 100644 index 00000000000..d22a74e1e16 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java @@ -0,0 +1,205 @@ +/* + * 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.context.support; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Standalone XML application context, taking the context definition files + * from the class path, interpreting plain paths as class path resource names + * that include the package path (e.g. "mypackage/myresource.txt"). Useful for + * test harnesses as well as for application contexts embedded within JARs. + * + *

The config location defaults can be overridden via {@link #getConfigLocations}, + * Config locations can either denote concrete files like "/myfiles/context.xml" + * or Ant-style patterns like "/myfiles/*-context.xml" (see the + * {@link org.springframework.util.AntPathMatcher} javadoc for pattern details). + * + *

Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

This is a simple, one-stop shop convenience ApplicationContext. + * Consider using the {@link GenericApplicationContext} class in combination + * with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader} + * for more flexible context setup. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getResource + * @see #getResourceByPath + * @see GenericApplicationContext + */ +public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext { + + private Resource[] configResources; + + + /** + * Create a new ClassPathXmlApplicationContext for bean-style configuration. + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public ClassPathXmlApplicationContext() { + } + + /** + * Create a new ClassPathXmlApplicationContext for bean-style configuration. + * @param parent the parent context + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public ClassPathXmlApplicationContext(ApplicationContext parent) { + super(parent); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + * @param configLocation resource location + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param configLocations array of resource locations + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String[] configLocations) throws BeansException { + this(configLocations, true, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param configLocations array of resource locations + * @param parent the parent context + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException { + this(configLocations, true, parent); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files. + * @param configLocations array of resource locations + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @throws BeansException if context creation failed + * @see #refresh() + */ + public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files. + * @param configLocations array of resource locations + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @param parent the parent context + * @throws BeansException if context creation failed + * @see #refresh() + */ + public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } + } + + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + *

This is a convenience method to load class path resources relative to a + * given Class. For full flexibility, consider using a GenericApplicationContext + * with an XmlBeanDefinitionReader and a ClassPathResource argument. + * @param path relative (or absolute) path within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String path, Class clazz) throws BeansException { + this(new String[] {path}, clazz); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param paths array of relative (or absolute) paths within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String[] paths, Class clazz) throws BeansException { + this(paths, clazz, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param paths array of relative (or absolute) paths within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @param parent the parent context + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String[] paths, Class clazz, ApplicationContext parent) + throws BeansException { + + super(parent); + Assert.notNull(paths, "Path array must not be null"); + Assert.notNull(clazz, "Class argument must not be null"); + this.configResources = new Resource[paths.length]; + for (int i = 0; i < paths.length; i++) { + this.configResources[i] = new ClassPathResource(paths[i], clazz); + } + refresh(); + } + + + protected Resource[] getConfigResources() { + return this.configResources; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java b/org.springframework.context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java new file mode 100644 index 00000000000..dd7f3477351 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.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.context.support; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.SmartClassLoader; +import org.springframework.util.ReflectionUtils; + +/** + * Special variant of an overriding ClassLoader, used for temporary type + * matching in {@link AbstractApplicationContext}. Redefines classes from + * a cached byte array for every loadClass call in order to + * pick up recently loaded types in the parent ClassLoader. + * + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractApplicationContext + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setTempClassLoader + */ +class ContextTypeMatchClassLoader extends DecoratingClassLoader implements SmartClassLoader { + + private static Method findLoadedClassMethod; + + static { + try { + findLoadedClassMethod = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] {String.class}); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Invalid [java.lang.ClassLoader] class: no 'findLoadedClass' method defined!"); + } + } + + + /** Cache for byte array per class name */ + private final Map bytesCache = new HashMap(); + + + public ContextTypeMatchClassLoader(ClassLoader parent) { + super(parent); + } + + public Class loadClass(String name) throws ClassNotFoundException { + return new ContextOverridingClassLoader(getParent()).loadClass(name); + } + + public boolean isClassReloadable(Class clazz) { + return (clazz.getClassLoader() instanceof ContextOverridingClassLoader); + } + + + /** + * ClassLoader to be created for each loaded class. + * Caches class file content but redefines class for each call. + */ + private class ContextOverridingClassLoader extends OverridingClassLoader { + + public ContextOverridingClassLoader(ClassLoader parent) { + super(parent); + } + + protected boolean isEligibleForOverriding(String className) { + if (isExcluded(className) || ContextTypeMatchClassLoader.this.isExcluded(className)) { + return false; + } + ReflectionUtils.makeAccessible(findLoadedClassMethod); + ClassLoader parent = getParent(); + while (parent != null) { + if (ReflectionUtils.invokeMethod(findLoadedClassMethod, parent, new Object[] {className}) != null) { + return false; + } + parent = parent.getParent(); + } + return true; + } + + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + byte[] bytes = (byte[]) bytesCache.get(name); + if (bytes == null) { + bytes = loadBytesForClass(name); + if (bytes != null) { + bytesCache.put(name, bytes); + } + else { + return null; + } + } + return defineClass(name, bytes, 0, bytes.length); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/org.springframework.context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java new file mode 100644 index 00000000000..4809dc12215 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -0,0 +1,162 @@ +/* + * 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.context.support; + +import java.io.Serializable; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link MessageSourceResolvable} interface. + * Offers an easy way to store all the necessary values needed to resolve + * a message via a {@link org.springframework.context.MessageSource}. + * + * @author Juergen Hoeller + * @since 13.02.2004 + * @see org.springframework.context.MessageSource#getMessage(MessageSourceResolvable, java.util.Locale) + */ +public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable { + + private final String[] codes; + + private final Object[] arguments; + + private final String defaultMessage; + + + /** + * Create a new DefaultMessageSourceResolvable. + * @param code the code to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String code) { + this(new String[] {code}, null, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes) { + this(codes, null, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes, String defaultMessage) { + this(codes, null, defaultMessage); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes, Object[] arguments) { + this(codes, arguments, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes, Object[] arguments, String defaultMessage) { + this.codes = codes; + this.arguments = arguments; + this.defaultMessage = defaultMessage; + } + + /** + * Copy constructor: Create a new instance from another resolvable. + * @param resolvable the resolvable to copy from + */ + public DefaultMessageSourceResolvable(MessageSourceResolvable resolvable) { + this(resolvable.getCodes(), resolvable.getArguments(), resolvable.getDefaultMessage()); + } + + + public String[] getCodes() { + return this.codes; + } + + /** + * Return the default code of this resolvable, that is, + * the last one in the codes array. + */ + public String getCode() { + return (this.codes != null && this.codes.length > 0) ? this.codes[this.codes.length - 1] : null; + } + + public Object[] getArguments() { + return this.arguments; + } + + public String getDefaultMessage() { + return this.defaultMessage; + } + + + /** + * Build a default String representation for this MessageSourceResolvable: + * including codes, arguments, and default message. + */ + protected final String resolvableToString() { + StringBuffer buf = new StringBuffer(); + buf.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ",")); + buf.append("]; arguments [" + StringUtils.arrayToDelimitedString(this.arguments, ",")); + buf.append("]; default message [").append(this.defaultMessage).append(']'); + return buf.toString(); + } + + /** + * Default implementation exposes the attributes of this MessageSourceResolvable. + * To be overridden in more specific subclasses, potentially including the + * resolvable content through resolvableToString(). + * @see #resolvableToString() + */ + public String toString() { + return getClass().getName() + ": " + resolvableToString(); + } + + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MessageSourceResolvable)) { + return false; + } + MessageSourceResolvable otherResolvable = (MessageSourceResolvable) other; + return ObjectUtils.nullSafeEquals(getCodes(), otherResolvable.getCodes()) && + ObjectUtils.nullSafeEquals(getArguments(), otherResolvable.getArguments()) && + ObjectUtils.nullSafeEquals(getDefaultMessage(), otherResolvable.getDefaultMessage()); + } + + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(getCodes()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArguments()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getDefaultMessage()); + return hashCode; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java new file mode 100644 index 00000000000..c42e38d5eaa --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java @@ -0,0 +1,83 @@ +/* + * 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.context.support; + +import java.util.Locale; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; + +/** + * Empty {@link MessageSource} that delegates all calls to the parent MessageSource. + * If no parent is available, it simply won't resolve any message. + * + *

Used as placeholder by AbstractApplicationContext, if the context doesn't + * define its own MessageSource. Not intended for direct use in applications. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see AbstractApplicationContext + */ +public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { + + private MessageSource parentMessageSource; + + + public void setParentMessageSource(MessageSource parent) { + this.parentMessageSource = parent; + } + + public MessageSource getParentMessageSource() { + return this.parentMessageSource; + } + + + public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(code, args, defaultMessage, locale); + } + else { + return renderDefaultMessage(defaultMessage, args, locale); + } + } + + public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(code, args, locale); + } + else { + throw new NoSuchMessageException(code, locale); + } + } + + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(resolvable, locale); + } + else { + if (resolvable.getDefaultMessage() != null) { + return renderDefaultMessage(resolvable.getDefaultMessage(), resolvable.getArguments(), locale); + } + String[] codes = resolvable.getCodes(); + String code = (codes != null && codes.length > 0 ? codes[0] : null); + throw new NoSuchMessageException(code, locale); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java new file mode 100644 index 00000000000..c09dfdfddc3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java @@ -0,0 +1,161 @@ +/* + * 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.context.support; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * Standalone XML application context, taking the context definition files + * from the file system or from URLs, interpreting plain paths as relative + * file system locations (e.g. "mydir/myfile.txt"). Useful for test harnesses + * as well as for standalone environments. + * + *

NOTE: Plain paths will always be interpreted as relative + * to the current VM working directory, even if they start with a slash. + * (This is consistent with the semantics in a Servlet container.) + * Use an explicit "file:" prefix to enforce an absolute file path. + * + *

The config location defaults can be overridden via {@link #getConfigLocations}, + * Config locations can either denote concrete files like "/myfiles/context.xml" + * or Ant-style patterns like "/myfiles/*-context.xml" (see the + * {@link org.springframework.util.AntPathMatcher} javadoc for pattern details). + * + *

Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

This is a simple, one-stop shop convenience ApplicationContext. + * Consider using the {@link GenericApplicationContext} class in combination + * with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader} + * for more flexible context setup. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getResource + * @see #getResourceByPath + * @see GenericApplicationContext + */ +public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext { + + /** + * Create a new FileSystemXmlApplicationContext for bean-style configuration. + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public FileSystemXmlApplicationContext() { + } + + /** + * Create a new FileSystemXmlApplicationContext for bean-style configuration. + * @param parent the parent context + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public FileSystemXmlApplicationContext(ApplicationContext parent) { + super(parent); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + * @param configLocation file path + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param configLocations array of file paths + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String[] configLocations) throws BeansException { + this(configLocations, true, null); + } + + /** + * Create a new FileSystemXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param configLocations array of file paths + * @param parent the parent context + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException { + this(configLocations, true, parent); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML files. + * @param configLocations array of file paths + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @throws BeansException if context creation failed + * @see #refresh() + */ + public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); + } + + /** + * Create a new FileSystemXmlApplicationContext with the given parent, + * loading the definitions from the given XML files. + * @param configLocations array of file paths + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @param parent the parent context + * @throws BeansException if context creation failed + * @see #refresh() + */ + public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } + } + + + /** + * Resolve resource paths as file system paths. + *

Note: Even if a given path starts with a slash, it will get + * interpreted as relative to the current VM working directory. + * This is consistent with the semantics in a Servlet container. + * @param path path to the resource + * @return Resource handle + * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath + */ + protected Resource getResourceByPath(String path) { + if (path != null && path.startsWith("/")) { + path = path.substring(1); + } + return new FileSystemResource(path); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/GenericApplicationContext.java new file mode 100644 index 00000000000..cc62a89e12f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -0,0 +1,275 @@ +/* + * 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.context.support; + +import java.io.IOException; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.Assert; + +/** + * Generic ApplicationContext implementation that holds a single internal + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * instance and does not assume a specific bean definition format. Implements + * the {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * interface in order to allow for applying any bean definition readers to it. + * + *

Typical usage is to register a variety of bean definitions via the + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * interface and then call {@link #refresh()} to initialize those beans + * with application context semantics (handling + * {@link org.springframework.context.ApplicationContextAware}, auto-detecting + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessors}, + * etc). + * + *

In contrast to other ApplicationContext implementations that create a new + * internal BeanFactory instance for each refresh, the internal BeanFactory of + * this context is available right from the start, to be able to register bean + * definitions on it. {@link #refresh()} may only be called once. + * + *

Usage example: + * + *

+ * GenericApplicationContext ctx = new GenericApplicationContext();
+ * XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
+ * xmlReader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));
+ * PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx);
+ * propReader.loadBeanDefinitions(new ClassPathResource("otherBeans.properties"));
+ * ctx.refresh();
+ *
+ * MyBean myBean = (MyBean) ctx.getBean("myBean");
+ * ...
+ * + * For the typical case of XML bean definitions, simply use + * {@link ClassPathXmlApplicationContext} or {@link FileSystemXmlApplicationContext}, + * which are easier to set up - but less flexible, since you can just use standard + * resource locations for XML bean definitions, rather than mixing arbitrary bean + * definition formats. The equivalent in a web environment is + * {@link org.springframework.web.context.support.XmlWebApplicationContext}. + * + *

For custom application context implementations that are supposed to read + * special bean definition formats in a refreshable manner, consider deriving + * from the {@link AbstractRefreshableApplicationContext} base class. + * + * @author Juergen Hoeller + * @since 1.1.2 + * @see #registerBeanDefinition + * @see #refresh() + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + */ +public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry { + + private final DefaultListableBeanFactory beanFactory; + + private ResourceLoader resourceLoader; + + private boolean refreshed = false; + + + /** + * Create a new GenericApplicationContext. + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext() { + this.beanFactory = new DefaultListableBeanFactory(); + } + + /** + * Create a new GenericApplicationContext with the given DefaultListableBeanFactory. + * @param beanFactory the DefaultListableBeanFactory instance to use for this context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(DefaultListableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + /** + * Create a new GenericApplicationContext with the given parent. + * @param parent the parent application context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(ApplicationContext parent) { + this(); + setParent(parent); + } + + /** + * Create a new GenericApplicationContext with the given DefaultListableBeanFactory. + * @param beanFactory the DefaultListableBeanFactory instance to use for this context + * @param parent the parent application context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(DefaultListableBeanFactory beanFactory, ApplicationContext parent) { + this(beanFactory); + setParent(parent); + } + + + /** + * Set the parent of this application context, also setting + * the parent of the internal BeanFactory accordingly. + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setParentBeanFactory + */ + public void setParent(ApplicationContext parent) { + super.setParent(parent); + this.beanFactory.setParentBeanFactory(getInternalParentBeanFactory()); + } + + /** + * Set a ResourceLoader to use for this context. If set, the context will + * delegate all getResource calls to the given ResourceLoader. + * If not set, default resource loading will apply. + *

The main reason to specify a custom ResourceLoader is to resolve + * resource paths (withour URL prefix) in a specific fashion. + * The default behavior is to resolve such paths as class path locations. + * To resolve resource paths as file system locations, specify a + * FileSystemResourceLoader here. + *

You can also pass in a full ResourcePatternResolver, which will + * be autodetected by the context and used for getResources + * calls as well. Else, default resource pattern matching will apply. + * @see #getResource + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.core.io.FileSystemResourceLoader + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see #getResources + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + + /** + * This implementation delegates to this context's ResourceLoader if set, + * falling back to the default superclass behavior else. + * @see #setResourceLoader + */ + public Resource getResource(String location) { + if (this.resourceLoader != null) { + return this.resourceLoader.getResource(location); + } + return super.getResource(location); + } + + /** + * This implementation delegates to this context's ResourceLoader if it + * implements the ResourcePatternResolver interface, falling back to the + * default superclass behavior else. + * @see #setResourceLoader + */ + public Resource[] getResources(String locationPattern) throws IOException { + if (this.resourceLoader instanceof ResourcePatternResolver) { + return ((ResourcePatternResolver) this.resourceLoader).getResources(locationPattern); + } + return super.getResources(locationPattern); + } + + + //--------------------------------------------------------------------- + // Implementations of AbstractApplicationContext's template methods + //--------------------------------------------------------------------- + + /** + * Do nothing: We hold a single internal BeanFactory and rely on callers + * to register beans through our public methods (or the BeanFactory's). + * @see #registerBeanDefinition + */ + protected final void refreshBeanFactory() throws IllegalStateException { + if (this.refreshed) { + throw new IllegalStateException( + "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once"); + } + this.refreshed = true; + } + + /** + * Do nothing: We hold a single internal BeanFactory that will never + * get released. + */ + protected final void closeBeanFactory() { + } + + /** + * Return the single internal BeanFactory held by this context + * (as ConfigurableListableBeanFactory). + */ + public final ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + /** + * Return the underlying bean factory of this context, + * available for registering bean definitions. + *

NOTE: You need to call {@link #refresh()} to initialize the + * bean factory and its contained beans with application context semantics + * (autodetecting BeanFactoryPostProcessors, etc). + * @return the internal bean factory (as DefaultListableBeanFactory) + */ + public final DefaultListableBeanFactory getDefaultListableBeanFactory() { + return this.beanFactory; + } + + + //--------------------------------------------------------------------- + // Implementation of BeanDefinitionRegistry + //--------------------------------------------------------------------- + + public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + this.beanFactory.registerBeanDefinition(beanName, beanDefinition); + } + + public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + this.beanFactory.removeBeanDefinition(beanName); + } + + public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + return this.beanFactory.getBeanDefinition(beanName); + } + + public boolean isBeanNameInUse(String beanName) { + return this.beanFactory.isBeanNameInUse(beanName); + } + + public void registerAlias(String beanName, String alias) { + this.beanFactory.registerAlias(beanName, alias); + } + + public void removeAlias(String alias) { + this.beanFactory.removeAlias(alias); + } + + public boolean isAlias(String beanName) { + return this.beanFactory.isAlias(beanName); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java new file mode 100644 index 00000000000..2d977350913 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java @@ -0,0 +1,187 @@ +/* + * 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.context.support; + +import java.util.Locale; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; + +/** + * Helper class for easy access to messages from a MessageSource, + * providing various overloaded getMessage methods. + * + *

Available from ApplicationObjectSupport, but also reusable + * as a standalone helper to delegate to in application objects. + * + * @author Juergen Hoeller + * @since 23.10.2003 + * @see ApplicationObjectSupport#getMessageSourceAccessor + */ +public class MessageSourceAccessor { + + private final MessageSource messageSource; + + private final Locale defaultLocale; + + /** + * Create a new MessageSourceAccessor, using LocaleContextHolder's locale + * as default locale. + * @param messageSource the MessageSource to wrap + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ + public MessageSourceAccessor(MessageSource messageSource) { + this.messageSource = messageSource; + this.defaultLocale = null; + } + + /** + * Create a new MessageSourceAccessor, using the given default locale. + * @param messageSource the MessageSource to wrap + * @param defaultLocale the default locale to use for message access + */ + public MessageSourceAccessor(MessageSource messageSource, Locale defaultLocale) { + this.messageSource = messageSource; + this.defaultLocale = defaultLocale; + } + + /** + * Return the default locale to use if no explicit locale has been given. + *

The default implementation returns the default locale passed into the + * corresponding constructor, or LocaleContextHolder's locale as fallback. + * Can be overridden in subclasses. + * @see #MessageSourceAccessor(org.springframework.context.MessageSource, java.util.Locale) + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ + protected Locale getDefaultLocale() { + return (this.defaultLocale != null ? this.defaultLocale : LocaleContextHolder.getLocale()); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code code of the message + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, String defaultMessage) { + return this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code code of the message + * @param defaultMessage String to return if the lookup fails + * @param locale Locale in which to do lookup + * @return the message + */ + public String getMessage(String code, String defaultMessage, Locale locale) { + return this.messageSource.getMessage(code, null, defaultMessage, locale); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage) { + return this.messageSource.getMessage(code, args, defaultMessage, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param defaultMessage String to return if the lookup fails + * @param locale Locale in which to do lookup + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) { + return this.messageSource.getMessage(code, args, defaultMessage, locale); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code code of the message + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code) throws NoSuchMessageException { + return this.messageSource.getMessage(code, null, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code code of the message + * @param locale Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(code, null, locale); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code code of the message + * @param args arguments for the message, or null if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args) throws NoSuchMessageException { + return this.messageSource.getMessage(code, args, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code code of the message + * @param args arguments for the message, or null if none + * @param locale Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(code, args, locale); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance) + * in the default Locale. + * @param resolvable the MessageSourceResolvable + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException { + return this.messageSource.getMessage(resolvable, getDefaultLocale()); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance) + * in the given Locale. + * @param resolvable the MessageSourceResolvable + * @param locale Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(resolvable, locale); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java new file mode 100644 index 00000000000..e8a0e0568a0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.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.context.support; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.util.Assert; + +/** + * Helper class that allows for accessing a Spring + * {@link org.springframework.context.MessageSource} as a {@link java.util.ResourceBundle}. + * Used for example to expose a Spring MessageSource to JSTL web views. + * + * @author Juergen Hoeller + * @since 27.02.2003 + * @see org.springframework.context.MessageSource + * @see java.util.ResourceBundle + * @see org.springframework.web.servlet.support.JstlUtils#exposeLocalizationContext + */ +public class MessageSourceResourceBundle extends ResourceBundle { + + private final MessageSource messageSource; + + private final Locale locale; + + + /** + * Create a new MessageSourceResourceBundle for the given MessageSource and Locale. + * @param source the MessageSource to retrieve messages from + * @param locale the Locale to retrieve messages for + */ + public MessageSourceResourceBundle(MessageSource source, Locale locale) { + Assert.notNull(source, "MessageSource must not be null"); + this.messageSource = source; + this.locale = locale; + } + + /** + * Create a new MessageSourceResourceBundle for the given MessageSource and Locale. + * @param source the MessageSource to retrieve messages from + * @param locale the Locale to retrieve messages for + * @param parent the parent ResourceBundle to delegate to if no local message found + */ + public MessageSourceResourceBundle(MessageSource source, Locale locale, ResourceBundle parent) { + this(source, locale); + setParent(parent); + } + + + /** + * This implementation resolves the code in the MessageSource. + * Returns null if the message could not be resolved. + */ + protected Object handleGetObject(String code) { + try { + return this.messageSource.getMessage(code, null, this.locale); + } + catch (NoSuchMessageException ex) { + return null; + } + } + + /** + * This implementation returns null, as a MessageSource does + * not allow for enumerating the defined message codes. + */ + public Enumeration getKeys() { + return null; + } + + /** + * This implementation exposes the specified Locale for introspection + * through the standard ResourceBundle.getLocale() method. + */ + public Locale getLocale() { + return this.locale; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceSupport.java new file mode 100644 index 00000000000..63b19f0c565 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -0,0 +1,153 @@ +/* + * 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.context.support; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Base class for message source implementations, providing support infrastructure + * such as {@link java.text.MessageFormat} handling but not implementing concrete + * methods defined in the {@link org.springframework.context.MessageSource}. + * + *

{@link AbstractMessageSource} derives from this class, providing concrete + * getMessage implementations that delegate to a central template + * method for message code resolution. + * + * @author Juergen Hoeller + * @since 2.5.5 + */ +public abstract class MessageSourceSupport { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean alwaysUseMessageFormat = false; + + /** + * Cache to hold already generated MessageFormats per message. + * Used for passed-in default messages. MessageFormats for resolved + * codes are cached on a specific basis in subclasses. + */ + private final Map cachedMessageFormats = new HashMap(); + + + /** + * Set whether to always apply the MessageFormat rules, parsing even + * messages without arguments. + *

Default is "false": Messages without arguments are by default + * returned as-is, without parsing them through MessageFormat. + * Set this to "true" to enforce MessageFormat for all messages, + * expecting all message texts to be written with MessageFormat escaping. + *

For example, MessageFormat expects a single quote to be escaped + * as "''". If your message texts are all written with such escaping, + * even when not defining argument placeholders, you need to set this + * flag to "true". Else, only message texts with actual arguments + * are supposed to be written with MessageFormat escaping. + * @see java.text.MessageFormat + */ + public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { + this.alwaysUseMessageFormat = alwaysUseMessageFormat; + } + + /** + * Return whether to always apply the MessageFormat rules, parsing even + * messages without arguments. + */ + protected boolean isAlwaysUseMessageFormat() { + return this.alwaysUseMessageFormat; + } + + + /** + * Format the given message String, using cached MessageFormats. + * By default invoked for passed-in default messages, to resolve + * any argument placeholders found in them. + * @param msg the message to format + * @param args array of arguments that will be filled in for params within + * the message, or null if none. + * @param locale the Locale used for formatting + * @return the formatted message (with resolved arguments) + */ + protected String formatMessage(String msg, Object[] args, Locale locale) { + if (msg == null || (!this.alwaysUseMessageFormat && (args == null || args.length == 0))) { + return msg; + } + MessageFormat messageFormat = null; + synchronized (this.cachedMessageFormats) { + messageFormat = (MessageFormat) this.cachedMessageFormats.get(msg); + if (messageFormat == null) { + messageFormat = createMessageFormat(msg, locale); + this.cachedMessageFormats.put(msg, messageFormat); + } + } + synchronized (messageFormat) { + return messageFormat.format(resolveArguments(args, locale)); + } + } + + /** + * Create a MessageFormat for the given message and Locale. + * @param msg the message to create a MessageFormat for + * @param locale the Locale to create a MessageFormat for + * @return the MessageFormat instance + */ + protected MessageFormat createMessageFormat(String msg, Locale locale) { + if (logger.isDebugEnabled()) { + logger.debug("Creating MessageFormat for pattern [" + msg + "] and locale '" + locale + "'"); + } + return new MessageFormat((msg != null ? msg : ""), locale); + } + + /** + * Template method for resolving argument objects. + *

The default implementation simply returns the given argument + * array as-is. Can be overridden in subclasses in order to resolve + * special argument types. + * @param args the original argument array + * @param locale the Locale to resolve against + * @return the resolved argument array + */ + protected Object[] resolveArguments(Object[] args, Locale locale) { + return args; + } + + + /** + * Render the given default message String. The default message is + * passed in as specified by the caller and can be rendered into + * a fully formatted default message shown to the user. + *

The default implementation passes the String to formatMessage, + * resolving any argument placeholders found in them. Subclasses may override + * this method to plug in custom processing of default messages. + * @param defaultMessage the passed-in default message String + * @param args array of arguments that will be filled in for params within + * the message, or null if none. + * @param locale the Locale used for formatting + * @return the rendered default message (with resolved arguments) + * @see #formatMessage(String, Object[], java.util.Locale) + */ + protected String renderDefaultMessage(String defaultMessage, Object[] args, Locale locale) { + return formatMessage(defaultMessage, args, locale); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java new file mode 100644 index 00000000000..e075a3a853a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -0,0 +1,663 @@ +/* + * 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.context.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.context.MessageSource} implementation that + * accesses resource bundles using specified basenames. This class uses + * {@link java.util.Properties} instances as its custom data structure for + * messages, loading them via a {@link org.springframework.util.PropertiesPersister} + * strategy: The default strategy is capable of loading properties files + * with a specific character encoding, if desired. + * + *

In contrast to {@link ResourceBundleMessageSource}, this class supports + * reloading of properties files through the {@link #setCacheSeconds "cacheSeconds"} + * setting, and also through programmatically clearing the properties cache. + * Since application servers typically cache all files loaded from the classpath, + * it is necessary to store resources somewhere else (for example, in the + * "WEB-INF" directory of a web app). Otherwise changes of files in the + * classpath will not be reflected in the application. + * + *

Note that the base names set as {@link #setBasenames "basenames"} property + * are treated in a slightly different fashion than the "basenames" property of + * {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not + * specifying file extension or language codes, but can refer to any Spring resource + * location (instead of being restricted to classpath resources). With a "classpath:" + * prefix, resources can still be loaded from the classpath, but "cacheSeconds" values + * other than "-1" (caching forever) will not work in this case. + * + *

This MessageSource implementation is usually slightly faster than + * {@link ResourceBundleMessageSource}, which builds on {@link java.util.ResourceBundle} + * - in the default mode, i.e. when caching forever. With "cacheSeconds" set to 1, + * message lookup takes about twice as long - with the benefit that changes in + * individual properties files are detected with a maximum delay of 1 second. + * Higher "cacheSeconds" values usually do not make a significant difference. + * + *

This MessageSource can easily be used outside of an + * {@link org.springframework.context.ApplicationContext}: It will use a + * {@link org.springframework.core.io.DefaultResourceLoader} as default, + * simply getting overridden with the ApplicationContext's resource loader + * if running in a context. It does not have any other specific dependencies. + * + *

Thanks to Thomas Achleitner for providing the initial implementation of + * this message source! + * + * @author Juergen Hoeller + * @see #setCacheSeconds + * @see #setBasenames + * @see #setDefaultEncoding + * @see #setFileEncodings + * @see #setPropertiesPersister + * @see #setResourceLoader + * @see org.springframework.util.DefaultPropertiesPersister + * @see org.springframework.core.io.DefaultResourceLoader + * @see ResourceBundleMessageSource + * @see java.util.ResourceBundle + */ +public class ReloadableResourceBundleMessageSource extends AbstractMessageSource + implements ResourceLoaderAware { + + private static final String PROPERTIES_SUFFIX = ".properties"; + + private static final String XML_SUFFIX = ".xml"; + + + private String[] basenames = new String[0]; + + private String defaultEncoding; + + private Properties fileEncodings; + + private boolean fallbackToSystemLocale = true; + + private long cacheMillis = -1; + + private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + /** Cache to hold filename lists per Locale */ + private final Map cachedFilenames = new HashMap(); + + /** Cache to hold already loaded properties per filename */ + private final Map cachedProperties = new HashMap(); + + /** Cache to hold merged loaded properties per basename */ + private final Map cachedMergedProperties = new HashMap(); + + + /** + * Set a single basename, following the basic ResourceBundle convention of + * not specifying file extension or language codes, but in contrast to + * {@link ResourceBundleMessageSource} referring to a Spring resource location: + * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties", + * "WEB-INF/messages_en.properties", etc. + *

As of Spring 1.2.2, XML properties files are also supported: + * e.g. "WEB-INF/messages" will find and load "WEB-INF/messages.xml", + * "WEB-INF/messages_en.xml", etc as well. Note that this will only + * work on JDK 1.5+. + * @param basename the single basename + * @see #setBasenames + * @see org.springframework.core.io.ResourceEditor + * @see java.util.ResourceBundle + */ + public void setBasename(String basename) { + setBasenames(new String[] {basename}); + } + + /** + * Set an array of basenames, each following the basic ResourceBundle convention + * of not specifying file extension or language codes, but in contrast to + * {@link ResourceBundleMessageSource} referring to a Spring resource location: + * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties", + * "WEB-INF/messages_en.properties", etc. + *

As of Spring 1.2.2, XML properties files are also supported: + * e.g. "WEB-INF/messages" will find and load "WEB-INF/messages.xml", + * "WEB-INF/messages_en.xml", etc as well. Note that this will only + * work on JDK 1.5+. + *

The associated resource bundles will be checked sequentially + * when resolving a message code. Note that message definitions in a + * previous resource bundle will override ones in a later bundle, + * due to the sequential lookup. + * @param basenames an array of basenames + * @see #setBasename + * @see java.util.ResourceBundle + */ + public void setBasenames(String[] basenames) { + if (basenames != null) { + this.basenames = new String[basenames.length]; + for (int i = 0; i < basenames.length; i++) { + String basename = basenames[i]; + Assert.hasText(basename, "Basename must not be empty"); + this.basenames[i] = basename.trim(); + } + } + else { + this.basenames = new String[0]; + } + } + + /** + * Set the default charset to use for parsing properties files. + * Used if no file-specific charset is specified for a file. + *

Default is none, using the java.util.Properties + * default encoding. + *

Only applies to classic properties files, not to XML files. + * @param defaultEncoding the default charset + * @see #setFileEncodings + * @see org.springframework.util.PropertiesPersister#load + */ + public void setDefaultEncoding(String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Set per-file charsets to use for parsing properties files. + *

Only applies to classic properties files, not to XML files. + * @param fileEncodings Properties with filenames as keys and charset + * names as values. Filenames have to match the basename syntax, + * with optional locale-specific appendices: e.g. "WEB-INF/messages" + * or "WEB-INF/messages_en". + * @see #setBasenames + * @see org.springframework.util.PropertiesPersister#load + */ + public void setFileEncodings(Properties fileEncodings) { + this.fileEncodings = fileEncodings; + } + + /** + * Set whether to fall back to the system Locale if no files for a specific + * Locale have been found. Default is "true"; if this is turned off, the only + * fallback will be the default file (e.g. "messages.properties" for + * basename "messages"). + *

Falling back to the system Locale is the default behavior of + * java.util.ResourceBundle. However, this is often not + * desirable in an application server environment, where the system Locale + * is not relevant to the application at all: Set this flag to "false" + * in such a scenario. + */ + public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { + this.fallbackToSystemLocale = fallbackToSystemLocale; + } + + /** + * Set the number of seconds to cache loaded properties files. + *

    + *
  • Default is "-1", indicating to cache forever (just like + * java.util.ResourceBundle). + *
  • A positive number will cache loaded properties files for the given + * number of seconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + *
  • A value of "0" will check the last-modified timestamp of the file on + * every message access. Do not use this in a production environment! + *
+ */ + public void setCacheSeconds(int cacheSeconds) { + this.cacheMillis = (cacheSeconds * 1000); + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + *

The default is a DefaultPropertiesPersister. + * @see org.springframework.util.DefaultPropertiesPersister + */ + public void setPropertiesPersister(PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); + } + + /** + * Set the ResourceLoader to use for loading bundle properties files. + *

The default is a DefaultResourceLoader. Will get overridden by the + * ApplicationContext if running in a context, as it implements the + * ResourceLoaderAware interface. Can be manually overridden when + * running outside of an ApplicationContext. + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.context.ResourceLoaderAware + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + + /** + * Resolves the given message code as key in the retrieved bundle files, + * returning the value found in the bundle as-is (without MessageFormat parsing). + */ + protected String resolveCodeWithoutArguments(String code, Locale locale) { + if (this.cacheMillis < 0) { + PropertiesHolder propHolder = getMergedProperties(locale); + String result = propHolder.getProperty(code); + if (result != null) { + return result; + } + } + else { + for (int i = 0; i < this.basenames.length; i++) { + List filenames = calculateAllFilenames(this.basenames[i], locale); + for (int j = 0; j < filenames.size(); j++) { + String filename = (String) filenames.get(j); + PropertiesHolder propHolder = getProperties(filename); + String result = propHolder.getProperty(code); + if (result != null) { + return result; + } + } + } + } + return null; + } + + /** + * Resolves the given message code as key in the retrieved bundle files, + * using a cached MessageFormat instance per message code. + */ + protected MessageFormat resolveCode(String code, Locale locale) { + if (this.cacheMillis < 0) { + PropertiesHolder propHolder = getMergedProperties(locale); + MessageFormat result = propHolder.getMessageFormat(code, locale); + if (result != null) { + return result; + } + } + else { + for (int i = 0; i < this.basenames.length; i++) { + List filenames = calculateAllFilenames(this.basenames[i], locale); + for (int j = 0; j < filenames.size(); j++) { + String filename = (String) filenames.get(j); + PropertiesHolder propHolder = getProperties(filename); + MessageFormat result = propHolder.getMessageFormat(code, locale); + if (result != null) { + return result; + } + } + } + } + return null; + } + + + /** + * Get a PropertiesHolder that contains the actually visible properties + * for a Locale, after merging all specified resource bundles. + * Either fetches the holder from the cache or freshly loads it. + *

Only used when caching resource bundle contents forever, i.e. + * with cacheSeconds < 0. Therefore, merged properties are always + * cached forever. + */ + protected PropertiesHolder getMergedProperties(Locale locale) { + synchronized (this.cachedMergedProperties) { + PropertiesHolder mergedHolder = (PropertiesHolder) this.cachedMergedProperties.get(locale); + if (mergedHolder != null) { + return mergedHolder; + } + Properties mergedProps = new Properties(); + mergedHolder = new PropertiesHolder(mergedProps, -1); + for (int i = this.basenames.length - 1; i >= 0; i--) { + List filenames = calculateAllFilenames(this.basenames[i], locale); + for (int j = filenames.size() - 1; j >= 0; j--) { + String filename = (String) filenames.get(j); + PropertiesHolder propHolder = getProperties(filename); + if (propHolder.getProperties() != null) { + mergedProps.putAll(propHolder.getProperties()); + } + } + } + this.cachedMergedProperties.put(locale, mergedHolder); + return mergedHolder; + } + } + + /** + * Calculate all filenames for the given bundle basename and Locale. + * Will calculate filenames for the given Locale, the system Locale + * (if applicable), and the default file. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + * @see #setFallbackToSystemLocale + * @see #calculateFilenamesForLocale + */ + protected List calculateAllFilenames(String basename, Locale locale) { + synchronized (this.cachedFilenames) { + Map localeMap = (Map) this.cachedFilenames.get(basename); + if (localeMap != null) { + List filenames = (List) localeMap.get(locale); + if (filenames != null) { + return filenames; + } + } + List filenames = new ArrayList(7); + filenames.addAll(calculateFilenamesForLocale(basename, locale)); + if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) { + List fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault()); + for (Iterator it = fallbackFilenames.iterator(); it.hasNext();) { + String fallbackFilename = (String) it.next(); + if (!filenames.contains(fallbackFilename)) { + // Entry for fallback locale that isn't already in filenames list. + filenames.add(fallbackFilename); + } + } + } + filenames.add(basename); + if (localeMap != null) { + localeMap.put(locale, filenames); + } + else { + localeMap = new HashMap(); + localeMap.put(locale, filenames); + this.cachedFilenames.put(basename, localeMap); + } + return filenames; + } + } + + /** + * Calculate the filenames for the given bundle basename and Locale, + * appending language code, country code, and variant code. + * E.g.: basename "messages", Locale "de_AT_oo" -> "messages_de_AT_OO", + * "messages_de_AT", "messages_de". + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + */ + protected List calculateFilenamesForLocale(String basename, Locale locale) { + List result = new ArrayList(3); + String language = locale.getLanguage(); + String country = locale.getCountry(); + String variant = locale.getVariant(); + StringBuffer temp = new StringBuffer(basename); + + if (language.length() > 0) { + temp.append('_').append(language); + result.add(0, temp.toString()); + } + + if (country.length() > 0) { + temp.append('_').append(country); + result.add(0, temp.toString()); + } + + if (variant.length() > 0) { + temp.append('_').append(variant); + result.add(0, temp.toString()); + } + + return result; + } + + + /** + * Get a PropertiesHolder for the given filename, either from the + * cache or freshly loaded. + * @param filename the bundle filename (basename + Locale) + * @return the current PropertiesHolder for the bundle + */ + protected PropertiesHolder getProperties(String filename) { + synchronized (this.cachedProperties) { + PropertiesHolder propHolder = (PropertiesHolder) this.cachedProperties.get(filename); + if (propHolder != null && + (propHolder.getRefreshTimestamp() < 0 || + propHolder.getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) { + // up to date + return propHolder; + } + return refreshProperties(filename, propHolder); + } + } + + /** + * Refresh the PropertiesHolder for the given bundle filename. + * The holder can be null if not cached before, or a timed-out cache entry + * (potentially getting re-validated against the current last-modified timestamp). + * @param filename the bundle filename (basename + Locale) + * @param propHolder the current PropertiesHolder for the bundle + */ + protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) { + long refreshTimestamp = (this.cacheMillis < 0) ? -1 : System.currentTimeMillis(); + + Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); + if (!resource.exists()) { + resource = this.resourceLoader.getResource(filename + XML_SUFFIX); + } + + if (resource.exists()) { + long fileTimestamp = -1; + if (this.cacheMillis >= 0) { + // Last-modified timestamp of file will just be read if caching with timeout. + try { + fileTimestamp = resource.lastModified(); + if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) { + if (logger.isDebugEnabled()) { + logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified"); + } + propHolder.setRefreshTimestamp(refreshTimestamp); + return propHolder; + } + } + catch (IOException ex) { + // Probably a class path resource: cache it forever. + if (logger.isDebugEnabled()) { + logger.debug( + resource + " could not be resolved in the file system - assuming that is hasn't changed", ex); + } + fileTimestamp = -1; + } + } + try { + Properties props = loadProperties(resource, filename); + propHolder = new PropertiesHolder(props, fileTimestamp); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex); + } + // Empty holder representing "not valid". + propHolder = new PropertiesHolder(); + } + } + + else { + // Resource does not exist. + if (logger.isDebugEnabled()) { + logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML"); + } + // Empty holder representing "not found". + propHolder = new PropertiesHolder(); + } + + propHolder.setRefreshTimestamp(refreshTimestamp); + this.cachedProperties.put(filename, propHolder); + return propHolder; + } + + /** + * Load the properties from the given resource. + * @param resource the resource to load from + * @param filename the original bundle filename (basename + Locale) + * @return the populated Properties instance + * @throws IOException if properties loading failed + */ + protected Properties loadProperties(Resource resource, String filename) throws IOException { + InputStream is = resource.getInputStream(); + Properties props = new Properties(); + try { + if (resource.getFilename().endsWith(XML_SUFFIX)) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.loadFromXml(props, is); + } + else { + String encoding = null; + if (this.fileEncodings != null) { + encoding = this.fileEncodings.getProperty(filename); + } + if (encoding == null) { + encoding = this.defaultEncoding; + } + if (encoding != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); + } + this.propertiesPersister.load(props, new InputStreamReader(is, encoding)); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.load(props, is); + } + } + return props; + } + finally { + is.close(); + } + } + + + /** + * Clear the resource bundle cache. + * Subsequent resolve calls will lead to reloading of the properties files. + */ + public void clearCache() { + logger.debug("Clearing entire resource bundle cache"); + synchronized (this.cachedProperties) { + this.cachedProperties.clear(); + } + synchronized (this.cachedMergedProperties) { + this.cachedMergedProperties.clear(); + } + } + + /** + * Clear the resource bundle caches of this MessageSource and all its ancestors. + * @see #clearCache + */ + public void clearCacheIncludingAncestors() { + clearCache(); + if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) { + ((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors(); + } + } + + + public String toString() { + return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; + } + + + /** + * PropertiesHolder for caching. + * Stores the last-modified timestamp of the source file for efficient + * change detection, and the timestamp of the last refresh attempt + * (updated every time the cache entry gets re-validated). + */ + protected class PropertiesHolder { + + private Properties properties; + + private long fileTimestamp = -1; + + private long refreshTimestamp = -1; + + /** Cache to hold already generated MessageFormats per message code */ + private final Map cachedMessageFormats = new HashMap(); + + public PropertiesHolder(Properties properties, long fileTimestamp) { + this.properties = properties; + this.fileTimestamp = fileTimestamp; + } + + public PropertiesHolder() { + } + + public Properties getProperties() { + return properties; + } + + public long getFileTimestamp() { + return fileTimestamp; + } + + public void setRefreshTimestamp(long refreshTimestamp) { + this.refreshTimestamp = refreshTimestamp; + } + + public long getRefreshTimestamp() { + return refreshTimestamp; + } + + public String getProperty(String code) { + if (this.properties == null) { + return null; + } + return this.properties.getProperty(code); + } + + public MessageFormat getMessageFormat(String code, Locale locale) { + if (this.properties == null) { + return null; + } + synchronized (this.cachedMessageFormats) { + Map localeMap = (Map) this.cachedMessageFormats.get(code); + if (localeMap != null) { + MessageFormat result = (MessageFormat) localeMap.get(locale); + if (result != null) { + return result; + } + } + String msg = this.properties.getProperty(code); + if (msg != null) { + if (localeMap == null) { + localeMap = new HashMap(); + this.cachedMessageFormats.put(code, localeMap); + } + MessageFormat result = createMessageFormat(msg, locale); + localeMap.put(locale, result); + return result; + } + return null; + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java new file mode 100644 index 00000000000..90df82e8f78 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -0,0 +1,306 @@ +/* + * 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.context.support; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.context.MessageSource} implementation that + * accesses resource bundles using specified basenames. This class relies + * on the underlying JDK's {@link java.util.ResourceBundle} implementation, + * in combination with the JDK's standard message parsing provided by + * {@link java.text.MessageFormat}. + * + *

This MessageSource caches both the accessed ResourceBundle instances and + * the generated MessageFormats for each message. It also implements rendering of + * no-arg messages without MessageFormat, as supported by the AbstractMessageSource + * base class. The caching provided by this MessageSource is significantly faster + * than the built-in caching of the java.util.ResourceBundle class. + * + *

Unfortunately, java.util.ResourceBundle caches loaded bundles + * forever: Reloading a bundle during VM execution is not possible. + * As this MessageSource relies on ResourceBundle, it faces the same limitation. + * Consider {@link ReloadableResourceBundleMessageSource} for an alternative + * that is capable of refreshing the underlying bundle files. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setBasenames + * @see ReloadableResourceBundleMessageSource + * @see java.util.ResourceBundle + * @see java.text.MessageFormat + */ +public class ResourceBundleMessageSource extends AbstractMessageSource implements BeanClassLoaderAware { + + private String[] basenames = new String[0]; + + private ClassLoader bundleClassLoader; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** + * Cache to hold loaded ResourceBundles. + * This Map is keyed with the bundle basename, which holds a Map that is + * keyed with the Locale and in turn holds the ResourceBundle instances. + * This allows for very efficient hash lookups, significantly faster + * than the ResourceBundle class's own cache. + */ + private final Map cachedResourceBundles = new HashMap(); + + /** + * Cache to hold already generated MessageFormats. + * This Map is keyed with the ResourceBundle, which holds a Map that is + * keyed with the message code, which in turn holds a Map that is keyed + * with the Locale and holds the MessageFormat values. This allows for + * very efficient hash lookups without concatenated keys. + * @see #getMessageFormat + */ + private final Map cachedBundleMessageFormats = new HashMap(); + + + /** + * Set a single basename, following {@link java.util.ResourceBundle} conventions: + * essentially, a fully-qualified classpath location. If it doesn't contain a + * package qualifier (such as org.mypackage), it will be resolved + * from the classpath root. + *

Messages will normally be held in the "/lib" or "/classes" directory of + * a web application's WAR structure. They can also be held in jar files on + * the class path. + *

Note that ResourceBundle names are effectively classpath locations: As a + * consequence, the JDK's standard ResourceBundle treats dots as package separators. + * This means that "test.theme" is effectively equivalent to "test/theme", + * just like it is for programmatic java.util.ResourceBundle usage. + * @see #setBasenames + * @see java.util.ResourceBundle#getBundle(String) + */ + public void setBasename(String basename) { + setBasenames(new String[] {basename}); + } + + /** + * Set an array of basenames, each following {@link java.util.ResourceBundle} + * conventions: essentially, a fully-qualified classpath location. If it + * doesn't contain a package qualifier (such as org.mypackage), + * it will be resolved from the classpath root. + *

The associated resource bundles will be checked sequentially + * when resolving a message code. Note that message definitions in a + * previous resource bundle will override ones in a later bundle, + * due to the sequential lookup. + *

Note that ResourceBundle names are effectively classpath locations: As a + * consequence, the JDK's standard ResourceBundle treats dots as package separators. + * This means that "test.theme" is effectively equivalent to "test/theme", + * just like it is for programmatic java.util.ResourceBundle usage. + * @see #setBasename + * @see java.util.ResourceBundle#getBundle(String) + */ + public void setBasenames(String[] basenames) { + if (basenames != null) { + this.basenames = new String[basenames.length]; + for (int i = 0; i < basenames.length; i++) { + String basename = basenames[i]; + Assert.hasText(basename, "Basename must not be empty"); + this.basenames[i] = basename.trim(); + } + } + else { + this.basenames = new String[0]; + } + } + + /** + * Set the ClassLoader to load resource bundles with. + *

Default is the containing BeanFactory's + * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader}, + * or the default ClassLoader determined by + * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()} + * if not running within a BeanFactory. + */ + public void setBundleClassLoader(ClassLoader classLoader) { + this.bundleClassLoader = classLoader; + } + + /** + * Return the ClassLoader to load resource bundles with. + *

Default is the containing BeanFactory's bean ClassLoader. + * @see #setBundleClassLoader + */ + protected ClassLoader getBundleClassLoader() { + return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader); + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + + /** + * Resolves the given message code as key in the registered resource bundles, + * returning the value found in the bundle as-is (without MessageFormat parsing). + */ + protected String resolveCodeWithoutArguments(String code, Locale locale) { + String result = null; + for (int i = 0; result == null && i < this.basenames.length; i++) { + ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); + if (bundle != null) { + result = getStringOrNull(bundle, code); + } + } + return result; + } + + /** + * Resolves the given message code as key in the registered resource bundles, + * using a cached MessageFormat instance per message code. + */ + protected MessageFormat resolveCode(String code, Locale locale) { + MessageFormat messageFormat = null; + for (int i = 0; messageFormat == null && i < this.basenames.length; i++) { + ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); + if (bundle != null) { + messageFormat = getMessageFormat(bundle, code, locale); + } + } + return messageFormat; + } + + + /** + * Return a ResourceBundle for the given basename and code, + * fetching already generated MessageFormats from the cache. + * @param basename the basename of the ResourceBundle + * @param locale the Locale to find the ResourceBundle for + * @return the resulting ResourceBundle, or null if none + * found for the given basename and Locale + */ + protected ResourceBundle getResourceBundle(String basename, Locale locale) { + synchronized (this.cachedResourceBundles) { + Map localeMap = (Map) this.cachedResourceBundles.get(basename); + if (localeMap != null) { + ResourceBundle bundle = (ResourceBundle) localeMap.get(locale); + if (bundle != null) { + return bundle; + } + } + try { + ResourceBundle bundle = doGetBundle(basename, locale); + if (localeMap == null) { + localeMap = new HashMap(); + this.cachedResourceBundles.put(basename, localeMap); + } + localeMap.put(locale, bundle); + return bundle; + } + catch (MissingResourceException ex) { + if (logger.isWarnEnabled()) { + logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); + } + // Assume bundle not found + // -> do NOT throw the exception to allow for checking parent message source. + return null; + } + } + } + + /** + * Obtain the resource bundle for the given basename and Locale. + * @param basename the basename to look for + * @param locale the Locale to look for + * @return the corresponding ResourceBundle + * @throws MissingResourceException if no matching bundle could be found + * @see java.util.ResourceBundle#getBundle(String, java.util.Locale, ClassLoader) + * @see #getBundleClassLoader() + */ + protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { + return ResourceBundle.getBundle(basename, locale, getBundleClassLoader()); + } + + /** + * Return a MessageFormat for the given bundle and code, + * fetching already generated MessageFormats from the cache. + * @param bundle the ResourceBundle to work on + * @param code the message code to retrieve + * @param locale the Locale to use to build the MessageFormat + * @return the resulting MessageFormat, or null if no message + * defined for the given code + * @throws MissingResourceException if thrown by the ResourceBundle + */ + protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) + throws MissingResourceException { + + synchronized (this.cachedBundleMessageFormats) { + Map codeMap = (Map) this.cachedBundleMessageFormats.get(bundle); + Map localeMap = null; + if (codeMap != null) { + localeMap = (Map) codeMap.get(code); + if (localeMap != null) { + MessageFormat result = (MessageFormat) localeMap.get(locale); + if (result != null) { + return result; + } + } + } + + String msg = getStringOrNull(bundle, code); + if (msg != null) { + if (codeMap == null) { + codeMap = new HashMap(); + this.cachedBundleMessageFormats.put(bundle, codeMap); + } + if (localeMap == null) { + localeMap = new HashMap(); + codeMap.put(code, localeMap); + } + MessageFormat result = createMessageFormat(msg, locale); + localeMap.put(locale, result); + return result; + } + + return null; + } + } + + private String getStringOrNull(ResourceBundle bundle, String key) { + try { + return bundle.getString(key); + } + catch (MissingResourceException ex) { + // Assume key not found + // -> do NOT throw the exception to allow for checking parent message source. + return null; + } + } + + + /** + * Show the configuration of this MessageSource. + */ + public String toString() { + return getClass().getName() + ": basenames=[" + + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ResourceMapFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/context/support/ResourceMapFactoryBean.java new file mode 100644 index 00000000000..674d8daf6a4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ResourceMapFactoryBean.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.context.support; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * FactoryBean that creates a Map with String keys and Resource values from + * properties, interpreting passed-in String values as resource locations. + * + *

Extends PropertiesFactoryBean to inherit the capability of defining + * local properties and loading from properties files. + * + *

Implements the ResourceLoaderAware interface to automatically use + * the context ResourceLoader if running in an ApplicationContext. + * Uses DefaultResourceLoader else. + * + * @author Juergen Hoeller + * @author Keith Donald + * @since 1.0.2 + * @see org.springframework.core.io.DefaultResourceLoader + */ +public class ResourceMapFactoryBean extends PropertiesFactoryBean implements ResourceLoaderAware { + + private String resourceBasePath = ""; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + + /** + * Set a base path to prepend to each resource location value + * in the properties file. + *

E.g.: resourceBasePath="/images", value="/test.gif" + * -> location="/images/test.gif" + */ + public void setResourceBasePath(String resourceBasePath) { + this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : ""); + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + + public Class getObjectType() { + return Map.class; + } + + /** + * Create the Map instance, populated with keys and Resource values. + */ + protected Object createInstance() throws IOException { + Map resourceMap = new HashMap(); + Properties props = mergeProperties(); + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + String location = props.getProperty(key); + resourceMap.put(key, getResource(location)); + } + return resourceMap; + } + + /** + * Fetch the Resource handle for the given location, + * prepeding the resource base path. + * @param location the resource location + * @return the Resource handle + * @see org.springframework.core.io.ResourceLoader#getResource(String) + */ + protected Resource getResource(String location) { + return this.resourceLoader.getResource(this.resourceBasePath + location); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/StaticApplicationContext.java b/org.springframework.context/src/main/java/org/springframework/context/support/StaticApplicationContext.java new file mode 100644 index 00000000000..c03f3446dc7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/StaticApplicationContext.java @@ -0,0 +1,140 @@ +/* + * 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.context.support; + +import java.util.Locale; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ApplicationContext; + +/** + * {@link org.springframework.context.ApplicationContext} implementation + * which supports programmatic registration of beans and messages, + * rather than reading bean definitions from external configuration sources. + * Mainly useful for testing. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ +public class StaticApplicationContext extends GenericApplicationContext { + + private final StaticMessageSource staticMessageSource; + + + /** + * Create a new StaticApplicationContext. + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ + public StaticApplicationContext() throws BeansException { + this(null); + } + + /** + * Create a new StaticApplicationContext with the given parent. + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ + public StaticApplicationContext(ApplicationContext parent) throws BeansException { + super(parent); + + // Initialize and register a StaticMessageSource. + this.staticMessageSource = new StaticMessageSource(); + getBeanFactory().registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.staticMessageSource); + } + + + /** + * Return the internal StaticMessageSource used by this context. + * Can be used to register messages on it. + * @see #addMessage + */ + public final StaticMessageSource getStaticMessageSource() { + return this.staticMessageSource; + } + + + /** + * Register a singleton bean with the underlying bean factory. + *

For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerSingleton(String name, Class clazz) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(clazz); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a singleton bean with the underlying bean factory. + *

For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerSingleton(String name, Class clazz, MutablePropertyValues pvs) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(clazz); + bd.setPropertyValues(pvs); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a prototype bean with the underlying bean factory. + *

For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerPrototype(String name, Class clazz) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setScope(GenericBeanDefinition.SCOPE_PROTOTYPE); + bd.setBeanClass(clazz); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a prototype bean with the underlying bean factory. + *

For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerPrototype(String name, Class clazz, MutablePropertyValues pvs) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setScope(GenericBeanDefinition.SCOPE_PROTOTYPE); + bd.setBeanClass(clazz); + bd.setPropertyValues(pvs); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Associate the given message with the given code. + * @param code lookup code + * @param locale locale message should be found within + * @param defaultMessage message associated with this lookup code + * @see #getStaticMessageSource + */ + public void addMessage(String code, Locale locale, String defaultMessage) { + getStaticMessageSource().addMessage(code, locale, defaultMessage); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/StaticMessageSource.java b/org.springframework.context/src/main/java/org/springframework/context/support/StaticMessageSource.java new file mode 100644 index 00000000000..7f14893e249 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/StaticMessageSource.java @@ -0,0 +1,82 @@ +/* + * 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.context.support; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Simple implementation of {@link org.springframework.context.MessageSource} + * which allows messages to be registered programmatically. + * This MessageSource supports basic internationalization. + * + *

Intended for testing rather than for use in production systems. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class StaticMessageSource extends AbstractMessageSource { + + /** Map from 'code + locale' keys to message Strings */ + private final Map messages = new HashMap(); + + + protected MessageFormat resolveCode(String code, Locale locale) { + return (MessageFormat) this.messages.get(code + "_" + locale.toString()); + } + + /** + * Associate the given message with the given code. + * @param code the lookup code + * @param locale the locale that the message should be found within + * @param msg the message associated with this lookup code + */ + public void addMessage(String code, Locale locale, String msg) { + Assert.notNull(code, "Code must not be null"); + Assert.notNull(locale, "Locale must not be null"); + Assert.notNull(msg, "Message must not be null"); + this.messages.put(code + "_" + locale.toString(), createMessageFormat(msg, locale)); + if (logger.isDebugEnabled()) { + logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]"); + } + } + + /** + * Associate the given message values with the given keys as codes. + * @param messages the messages to register, with messages codes + * as keys and message texts as values + * @param locale the locale that the messages should be found within + */ + public void addMessages(Map messages, Locale locale) { + Assert.notNull(messages, "Messages Map must not be null"); + for (Iterator it = messages.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + addMessage(entry.getKey().toString(), locale, entry.getValue().toString()); + } + } + + + public String toString() { + return getClass().getName() + ": " + this.messages; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/package.html b/org.springframework.context/src/main/java/org/springframework/context/support/package.html new file mode 100644 index 00000000000..cf7219e3d26 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/support/package.html @@ -0,0 +1,9 @@ + + + +Classes supporting the org.springframework.context package, +such as abstract base classes for ApplicationContext +implementations and a MessageSource implementation. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java b/org.springframework.context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java new file mode 100644 index 00000000000..b302988bf5e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java @@ -0,0 +1,96 @@ +/* + * 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.context.weaving; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; + +/** + * Post-processor that registers AspectJ's + * {@link org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter} + * with the Spring application context's default + * {@link org.springframework.instrument.classloading.LoadTimeWeaver}. + * + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.5 + */ +public class AspectJWeavingEnabler + implements BeanFactoryPostProcessor, BeanClassLoaderAware, LoadTimeWeaverAware, Ordered { + + private ClassLoader beanClassLoader; + + private LoadTimeWeaver loadTimeWeaver; + + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + this.loadTimeWeaver = loadTimeWeaver; + } + + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + LoadTimeWeaver weaverToUse = this.loadTimeWeaver; + if (weaverToUse == null && InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { + weaverToUse = new InstrumentationLoadTimeWeaver(this.beanClassLoader); + } + weaverToUse.addTransformer(new AspectJClassBypassingClassFileTransformer( + new ClassPreProcessorAgentAdapter())); + } + + + /* + * Decorator to suppress processing AspectJ classes, hence avoiding potential LinkageErrors. + * OC4J and Tomcat (in Glassfish) definitely need such bypassing of AspectJ classes. + */ + private static class AspectJClassBypassingClassFileTransformer implements ClassFileTransformer { + + private final ClassFileTransformer delegate; + + public AspectJClassBypassingClassFileTransformer(ClassFileTransformer delegate) { + this.delegate = delegate; + } + + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (className.startsWith("org.aspectj") || className.startsWith("org/aspectj")) { + return classfileBuffer; + } + return this.delegate.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java new file mode 100644 index 00000000000..e67116c484b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java @@ -0,0 +1,136 @@ +/* + * 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.context.weaving; + +import java.lang.instrument.ClassFileTransformer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.instrument.InstrumentationSavingAgent; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver; +import org.springframework.instrument.classloading.glassfish.GlassFishLoadTimeWeaver; +import org.springframework.instrument.classloading.oc4j.OC4JLoadTimeWeaver; +import org.springframework.instrument.classloading.weblogic.WebLogicLoadTimeWeaver; + +/** + * Default {@link LoadTimeWeaver} bean for use in an application context, + * decorating an automatically detected internal LoadTimeWeaver. + * + *

Typically registered for the default bean name + * "loadTimeWeaver"; the most convenient way to achieve this is + * Spring's <context:load-time-weaver> XML tag. + * + *

This class implements a runtime environment check for obtaining the + * appropriate weaver implementation: As of Spring 2.5, it detects Sun's + * GlassFish, Oracle's OC4J, BEA's WebLogic 10, + * {@link InstrumentationSavingAgent Spring's VM agent} and any + * {@link ClassLoader} supported by Spring's {@link ReflectiveLoadTimeWeaver} + * (for example the + * {@link org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader}). + * + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.5 + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public class DefaultContextLoadTimeWeaver implements LoadTimeWeaver, BeanClassLoaderAware, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private LoadTimeWeaver loadTimeWeaver; + + + public void setBeanClassLoader(ClassLoader classLoader) { + LoadTimeWeaver serverSpecificLoadTimeWeaver = createServerSpecificLoadTimeWeaver(classLoader); + if (serverSpecificLoadTimeWeaver != null) { + if (logger.isInfoEnabled()) { + logger.info("Determined server-specific load-time weaver: " + + serverSpecificLoadTimeWeaver.getClass().getName()); + } + this.loadTimeWeaver = serverSpecificLoadTimeWeaver; + } + else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { + logger.info("Found Spring's JVM agent for instrumentation"); + this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(classLoader); + } + else { + try { + this.loadTimeWeaver = new ReflectiveLoadTimeWeaver(classLoader); + logger.info("Using a reflective load-time weaver for class loader: " + + this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName()); + } + catch (IllegalStateException ex) { + throw new IllegalStateException(ex.getMessage() + " Specify a custom LoadTimeWeaver " + + "or start your Java virtual machine with Spring's agent: -javaagent:spring-agent.jar"); + } + } + } + + /* + * This method never fails, allowing to try other possible ways to use an + * server-agnostic weaver. This non-failure logic is required since + * determining a load-time weaver based on the ClassLoader name alone may + * legitimately fail due to other mismatches. Specific case in point: the + * use of WebLogicLoadTimeWeaver works for WLS 10 but fails due to the lack + * of a specific method (addInstanceClassPreProcessor) for any earlier + * versions even though the ClassLoader name is the same. + */ + protected LoadTimeWeaver createServerSpecificLoadTimeWeaver(ClassLoader classLoader) { + try { + if (classLoader.getClass().getName().startsWith("weblogic")) { + return new WebLogicLoadTimeWeaver(classLoader); + } + else if (classLoader.getClass().getName().startsWith("oracle")) { + return new OC4JLoadTimeWeaver(classLoader); + } + else if (classLoader.getClass().getName().startsWith("com.sun.enterprise")) { + return new GlassFishLoadTimeWeaver(classLoader); + } + } + catch (IllegalStateException ex) { + logger.info("Could not obtain server-specific LoadTimeWeaver: " + ex.getMessage()); + } + return null; + } + + public void destroy() { + if (this.loadTimeWeaver instanceof InstrumentationLoadTimeWeaver) { + logger.info("Removing all registered transformers for class loader: " + + this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName()); + ((InstrumentationLoadTimeWeaver) this.loadTimeWeaver).removeTransformers(); + } + } + + + public void addTransformer(ClassFileTransformer transformer) { + this.loadTimeWeaver.addTransformer(transformer); + } + + public ClassLoader getInstrumentableClassLoader() { + return this.loadTimeWeaver.getInstrumentableClassLoader(); + } + + public ClassLoader getThrowawayClassLoader() { + return this.loadTimeWeaver.getThrowawayClassLoader(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java b/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java new file mode 100644 index 00000000000..734251f0ded --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java @@ -0,0 +1,51 @@ +/* + * 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.context.weaving; + +import org.springframework.instrument.classloading.LoadTimeWeaver; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the application context's default {@link LoadTimeWeaver}. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public interface LoadTimeWeaverAware { + + /** + * Set the {@link LoadTimeWeaver} of this object's containing + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + *

Invoked after the population of normal bean properties but before an + * initialization callback like + * {@link org.springframework.beans.factory.InitializingBean InitializingBean's} + * {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet() afterPropertiesSet()} + * or a custom init-method. Invoked after + * {@link org.springframework.context.ApplicationContextAware ApplicationContextAware's} + * {@link org.springframework.context.ApplicationContextAware#setApplicationContext setApplicationContext(..)}. + *

NOTE: This method will only be called if there actually is a + * LoadTimeWeaver available in the application context. If + * there is none, the method will simply not get invoked, assuming that the + * implementing object is able to activate its weaving dependency accordingly. + * @param loadTimeWeaver the LoadTimeWeaver instance (never null) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet + * @see org.springframework.context.ApplicationContextAware#setApplicationContext + */ + void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java new file mode 100644 index 00000000000..aebfb201254 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java @@ -0,0 +1,97 @@ +/* + * 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.context.weaving; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * implementation that passes the context's default {@link LoadTimeWeaver} + * to beans that implement the {@link LoadTimeWeaverAware} interface. + * + *

{@link org.springframework.context.ApplicationContext Application contexts} + * will automatically register this with their underlying + * {@link BeanFactory bean factory}, provided that a default + * LoadTimeWeaver is actually available. + * + *

Applications should not use this class directly. + * + * @author Juergen Hoeller + * @since 2.5 + * @see LoadTimeWeaverAware + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public class LoadTimeWeaverAwareProcessor implements BeanPostProcessor, BeanFactoryAware { + + private LoadTimeWeaver loadTimeWeaver; + + private BeanFactory beanFactory; + + + /** + * Create a new LoadTimeWeaverAwareProcessor that will + * auto-retrieve the {@link LoadTimeWeaver} from the containing + * {@link BeanFactory}, expecting a bean named + * {@link ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME "loadTimeWeaver"}. + */ + public LoadTimeWeaverAwareProcessor() { + } + + /** + * Create a new LoadTimeWeaverAwareProcessor for the given + * {@link LoadTimeWeaver}. + *

If the given loadTimeWeaver is null, then a + * LoadTimeWeaver will be auto-retrieved from the containing + * {@link BeanFactory}, expecting a bean named + * {@link ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME "loadTimeWeaver"}. + * @param loadTimeWeaver the specific LoadTimeWeaver that is to be used; can be null + */ + public LoadTimeWeaverAwareProcessor(LoadTimeWeaver loadTimeWeaver) { + this.loadTimeWeaver = loadTimeWeaver; + } + + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof LoadTimeWeaverAware) { + LoadTimeWeaver ltw = this.loadTimeWeaver; + if (ltw == null) { + Assert.state(this.beanFactory != null, + "BeanFactory required if no LoadTimeWeaver explicitly specified"); + ltw = (LoadTimeWeaver) this.beanFactory.getBean( + ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME, LoadTimeWeaver.class); + } + ((LoadTimeWeaverAware) bean).setLoadTimeWeaver(ltw); + } + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String name) { + return bean; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/weaving/package.html b/org.springframework.context/src/main/java/org/springframework/context/weaving/package.html new file mode 100644 index 00000000000..83020095e52 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/weaving/package.html @@ -0,0 +1,8 @@ + + + +Load-time weaving support for a Spring application context, building on Spring's +{@link org.springframework.instrument.classloading.LoadTimeWeaver} abstraction. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java new file mode 100644 index 00000000000..6a9271f2e2c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java @@ -0,0 +1,245 @@ +/* + * 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.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.RemoteException; + +import javax.ejb.EJBHome; +import javax.ejb.EJBObject; +import javax.naming.NamingException; +import javax.rmi.PortableRemoteObject; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.rmi.RmiClientInterceptorUtils; + +/** + * Base class for interceptors proxying remote Stateless Session Beans. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

Such an interceptor must be the last interceptor in the advice chain. + * In this case, there is no target object. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractRemoteSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor { + + private Class homeInterface; + + private boolean refreshHomeOnConnectFailure = false; + + private volatile boolean homeAsComponent = false; + + + + /** + * Set a home interface that this invoker will narrow to before performing + * the parameterless SLSB create() call that returns the actual + * SLSB proxy. + *

Default is none, which will work on all J2EE servers that are not based + * on CORBA. A plain javax.ejb.EJBHome interface is known to be + * sufficient to make a WebSphere 5.0 Remote SLSB work. On other servers, + * the specific home interface for the target SLSB might be necessary. + */ + public void setHomeInterface(Class homeInterface) { + if (homeInterface != null && !homeInterface.isInterface()) { + throw new IllegalArgumentException( + "Home interface class [" + homeInterface.getClass() + "] is not an interface"); + } + this.homeInterface = homeInterface; + } + + /** + * Set whether to refresh the EJB home on connect failure. + * Default is "false". + *

Can be turned on to allow for hot restart of the EJB server. + * If a cached EJB home throws an RMI exception that indicates a + * remote connect failure, a fresh home will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshHomeOnConnectFailure(boolean refreshHomeOnConnectFailure) { + this.refreshHomeOnConnectFailure = refreshHomeOnConnectFailure; + } + + protected boolean isHomeRefreshable() { + return this.refreshHomeOnConnectFailure; + } + + + /** + * This overridden lookup implementation performs a narrow operation + * after the JNDI lookup, provided that a home interface is specified. + * @see #setHomeInterface + * @see javax.rmi.PortableRemoteObject#narrow + */ + protected Object lookup() throws NamingException { + Object homeObject = super.lookup(); + if (this.homeInterface != null) { + try { + homeObject = PortableRemoteObject.narrow(homeObject, this.homeInterface); + } + catch (ClassCastException ex) { + throw new RemoteLookupFailureException( + "Could not narrow EJB home stub to home interface [" + this.homeInterface.getName() + "]", ex); + } + } + return homeObject; + } + + /** + * Check for EJB3-style home object that serves as EJB component directly. + */ + protected Method getCreateMethod(Object home) throws EjbAccessException { + if (this.homeAsComponent) { + return null; + } + if (!(home instanceof EJBHome)) { + // An EJB3 Session Bean... + this.homeAsComponent = true; + return null; + } + return super.getCreateMethod(home); + } + + + /** + * Fetches an EJB home object and delegates to doInvoke. + *

If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getHome + * @see #doInvoke + * @see #refreshAndRetry + */ + public Object invokeInContext(MethodInvocation invocation) throws Throwable { + try { + return doInvoke(invocation); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

The default implementation delegates to RmiClientInterceptorUtils. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + * @see org.springframework.remoting.rmi.RmiClientInterceptorUtils#isConnectFailure + */ + protected boolean isConnectFailure(RemoteException ex) { + return RmiClientInterceptorUtils.isConnectFailure(ex); + } + + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshHomeOnConnectFailure) { + if (logger.isDebugEnabled()) { + logger.debug("Could not connect to remote EJB [" + getJndiName() + "] - retrying", ex); + } + else if (logger.isWarnEnabled()) { + logger.warn("Could not connect to remote EJB [" + getJndiName() + "] - retrying"); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the EJB home object and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + try { + refreshHome(); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex); + } + return doInvoke(invocation); + } + + + /** + * Perform the given invocation on the current EJB home. + * Template method to be implemented by subclasses. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #getHome + * @see #newSessionBeanInstance + */ + protected abstract Object doInvoke(MethodInvocation invocation) throws Throwable; + + + /** + * Return a new instance of the stateless session bean. + * To be invoked by concrete remote SLSB invoker subclasses. + *

Can be overridden to change the algorithm. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #create + */ + protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException { + if (logger.isDebugEnabled()) { + logger.debug("Trying to create reference to remote EJB"); + } + Object ejbInstance = create(); + if (logger.isDebugEnabled()) { + logger.debug("Obtained reference to remote EJB: " + ejbInstance); + } + return ejbInstance; + } + + /** + * Remove the given EJB instance. + * To be invoked by concrete remote SLSB invoker subclasses. + * @param ejb the EJB instance to remove + * @see javax.ejb.EJBObject#remove + */ + protected void removeSessionBeanInstance(EJBObject ejb) { + if (ejb != null && !this.homeAsComponent) { + try { + ejb.remove(); + } + catch (Throwable ex) { + logger.warn("Could not invoke 'remove' on remote EJB proxy", ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java new file mode 100644 index 00000000000..887f3903253 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java @@ -0,0 +1,230 @@ +/* + * 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.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.jndi.JndiObjectLocator; + +/** + * Base class for AOP interceptors invoking local or remote Stateless Session Beans. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

Such an interceptor must be the last interceptor in the advice chain. + * In this case, there is no direct target object: The call is handled in a + * special way, getting executed on an EJB instance retrieved via an EJB home. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractSlsbInvokerInterceptor extends JndiObjectLocator + implements MethodInterceptor { + + private boolean lookupHomeOnStartup = true; + + private boolean cacheHome = true; + + private boolean exposeAccessContext = false; + + /** + * The EJB's home object, potentially cached. + * The type must be Object as it could be either EJBHome or EJBLocalHome. + */ + private Object cachedHome; + + /** + * The no-arg create() method required on EJB homes, potentially cached. + */ + private Method createMethod; + + private final Object homeMonitor = new Object(); + + + /** + * Set whether to look up the EJB home object on startup. + * Default is "true". + *

Can be turned off to allow for late start of the EJB server. + * In this case, the EJB home object will be fetched on first access. + * @see #setCacheHome + */ + public void setLookupHomeOnStartup(boolean lookupHomeOnStartup) { + this.lookupHomeOnStartup = lookupHomeOnStartup; + } + + /** + * Set whether to cache the EJB home object once it has been located. + * Default is "true". + *

Can be turned off to allow for hot restart of the EJB server. + * In this case, the EJB home object will be fetched for each invocation. + * @see #setLookupHomeOnStartup + */ + public void setCacheHome(boolean cacheHome) { + this.cacheHome = cacheHome; + } + + /** + * Set whether to expose the JNDI environment context for all access to the target + * EJB, i.e. for all method invocations on the exposed object reference. + *

Default is "false", i.e. to only expose the JNDI context for object lookup. + * Switch this flag to "true" in order to expose the JNDI environment (including + * the authorization context) for each EJB invocation, as needed by WebLogic + * for EJBs with authorization requirements. + */ + public void setExposeAccessContext(boolean exposeAccessContext) { + this.exposeAccessContext = exposeAccessContext; + } + + + /** + * Fetches EJB home on startup, if necessary. + * @see #setLookupHomeOnStartup + * @see #refreshHome + */ + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.lookupHomeOnStartup) { + // look up EJB home and create method + refreshHome(); + } + } + + /** + * Refresh the cached home object, if applicable. + * Also caches the create method on the home object. + * @throws NamingException if thrown by the JNDI lookup + * @see #lookup + * @see #getCreateMethod + */ + protected void refreshHome() throws NamingException { + synchronized (this.homeMonitor) { + Object home = lookup(); + if (this.cacheHome) { + this.cachedHome = home; + this.createMethod = getCreateMethod(home); + } + } + } + + /** + * Determine the create method of the given EJB home object. + * @param home the EJB home object + * @return the create method + * @throws EjbAccessException if the method couldn't be retrieved + */ + protected Method getCreateMethod(Object home) throws EjbAccessException { + try { + // Cache the EJB create() method that must be declared on the home interface. + return home.getClass().getMethod("create", (Class[]) null); + } + catch (NoSuchMethodException ex) { + throw new EjbAccessException("EJB home [" + home + "] has no no-arg create() method"); + } + } + + /** + * Return the EJB home object to use. Called for each invocation. + *

Default implementation returns the home created on initialization, + * if any; else, it invokes lookup to get a new proxy for each invocation. + *

Can be overridden in subclasses, for example to cache a home object + * for a given amount of time before recreating it, or to test the home + * object whether it is still alive. + * @return the EJB home object to use for an invocation + * @throws NamingException if proxy creation failed + * @see #lookup + * @see #getCreateMethod + */ + protected Object getHome() throws NamingException { + if (!this.cacheHome || (this.lookupHomeOnStartup && !isHomeRefreshable())) { + return (this.cachedHome != null ? this.cachedHome : lookup()); + } + else { + synchronized (this.homeMonitor) { + if (this.cachedHome == null) { + this.cachedHome = lookup(); + this.createMethod = getCreateMethod(this.cachedHome); + } + return this.cachedHome; + } + } + } + + /** + * Return whether the cached EJB home object is potentially + * subject to on-demand refreshing. Default is "false". + */ + protected boolean isHomeRefreshable() { + return false; + } + + + /** + * Prepares the thread context if necessar, and delegates to + * {@link #invokeInContext}. + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null); + try { + return invokeInContext(invocation); + } + finally { + getJndiTemplate().releaseContext(ctx); + } + } + + /** + * Perform the given invocation on the current EJB home, + * within the thread context being prepared accordingly. + * Template method to be implemented by subclasses. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + protected abstract Object invokeInContext(MethodInvocation invocation) throws Throwable; + + + /** + * Invokes the create() method on the cached EJB home object. + * @return a new EJBObject or EJBLocalObject + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + */ + protected Object create() throws NamingException, InvocationTargetException { + try { + Object home = getHome(); + Method createMethodToUse = this.createMethod; + if (createMethodToUse == null) { + createMethodToUse = getCreateMethod(home); + } + if (createMethodToUse == null) { + return home; + } + // Invoke create() method on EJB home object. + return createMethodToUse.invoke(home, (Object[]) null); + } + catch (IllegalAccessException ex) { + throw new EjbAccessException("Could not access EJB home create() method", ex); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/EjbAccessException.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/EjbAccessException.java new file mode 100644 index 00000000000..26d0a174ad6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/EjbAccessException.java @@ -0,0 +1,46 @@ +/* + * 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.ejb.access; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception that gets thrown when an EJB stub cannot be accessed properly. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class EjbAccessException extends NestedRuntimeException { + + /** + * Constructor for EjbAccessException. + * @param msg the detail message + */ + public EjbAccessException(String msg) { + super(msg); + } + + /** + * Constructor for EjbAccessException. + * @param msg the detail message + * @param cause the root cause + */ + public EjbAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java new file mode 100644 index 00000000000..83adeef066c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java @@ -0,0 +1,174 @@ +/* + * 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.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.ejb.CreateException; +import javax.ejb.EJBLocalHome; +import javax.ejb.EJBLocalObject; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Invoker for a local Stateless Session Bean. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

Caches the home object, since a local EJB home can never go stale. + * See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the lookupHomeOnStartup + * property to false, in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + */ +public class LocalSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor { + + private volatile boolean homeAsComponent = false; + + + /** + * This implementation "creates" a new EJB instance for each invocation. + * Can be overridden for custom invocation strategies. + *

Alternatively, override {@link #getSessionBeanInstance} and + * {@link #releaseSessionBeanInstance} to change EJB instance creation, + * for example to hold a single shared EJB instance. + */ + public Object invokeInContext(MethodInvocation invocation) throws Throwable { + Object ejb = null; + try { + ejb = getSessionBeanInstance(); + Method method = invocation.getMethod(); + if (method.getDeclaringClass().isInstance(ejb)) { + // directly implemented + return method.invoke(ejb, invocation.getArguments()); + } + else { + // not directly implemented + Method ejbMethod = ejb.getClass().getMethod(method.getName(), method.getParameterTypes()); + return ejbMethod.invoke(ejb, invocation.getArguments()); + } + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (logger.isDebugEnabled()) { + logger.debug("Method of local EJB [" + getJndiName() + "] threw exception", targetEx); + } + if (targetEx instanceof CreateException) { + throw new EjbAccessException("Could not create local EJB [" + getJndiName() + "]", targetEx); + } + else { + throw targetEx; + } + } + catch (NamingException ex) { + throw new EjbAccessException("Failed to locate local EJB [" + getJndiName() + "]", ex); + } + catch (IllegalAccessException ex) { + throw new EjbAccessException("Could not access method [" + invocation.getMethod().getName() + + "] of local EJB [" + getJndiName() + "]", ex); + } + finally { + if (ejb instanceof EJBLocalObject) { + releaseSessionBeanInstance((EJBLocalObject) ejb); + } + } + } + + /** + * Check for EJB3-style home object that serves as EJB component directly. + */ + protected Method getCreateMethod(Object home) throws EjbAccessException { + if (this.homeAsComponent) { + return null; + } + if (!(home instanceof EJBLocalHome)) { + // An EJB3 Session Bean... + this.homeAsComponent = true; + return null; + } + return super.getCreateMethod(home); + } + + /** + * Return an EJB instance to delegate the call to. + * Default implementation delegates to newSessionBeanInstance. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #newSessionBeanInstance + */ + protected Object getSessionBeanInstance() throws NamingException, InvocationTargetException { + return newSessionBeanInstance(); + } + + /** + * Release the given EJB instance. + * Default implementation delegates to removeSessionBeanInstance. + * @param ejb the EJB instance to release + * @see #removeSessionBeanInstance + */ + protected void releaseSessionBeanInstance(EJBLocalObject ejb) { + removeSessionBeanInstance(ejb); + } + + /** + * Return a new instance of the stateless session bean. + * Can be overridden to change the algorithm. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #create + */ + protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException { + if (logger.isDebugEnabled()) { + logger.debug("Trying to create reference to local EJB"); + } + Object ejbInstance = create(); + if (logger.isDebugEnabled()) { + logger.debug("Obtained reference to local EJB: " + ejbInstance); + } + return ejbInstance; + } + + /** + * Remove the given EJB instance. + * @param ejb the EJB instance to remove + * @see javax.ejb.EJBLocalObject#remove() + */ + protected void removeSessionBeanInstance(EJBLocalObject ejb) { + if (ejb != null && !this.homeAsComponent) { + try { + ejb.remove(); + } + catch (Throwable ex) { + logger.warn("Could not invoke 'remove' on local EJB proxy", ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java new file mode 100644 index 00000000000..8fb7f64e4e5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java @@ -0,0 +1,105 @@ +/* + * 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.ejb.access; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.ClassUtils; + +/** + * Convenient factory for local Stateless Session Bean (SLSB) proxies. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

If you want control over interceptor chaining, use an AOP ProxyFactoryBean + * with LocalSlsbInvokerInterceptor rather than rely on this class. + * + *

In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the "lookupHomeOnStartup" + * property to "false", in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @since 09.05.2003 + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + */ +public class LocalStatelessSessionProxyFactoryBean extends LocalSlsbInvokerInterceptor + implements FactoryBean, BeanClassLoaderAware { + + /** The business interface of the EJB we're proxying */ + private Class businessInterface; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** EJBLocalObject */ + private Object proxy; + + + /** + * Set the business interface of the EJB we're proxying. + * This will normally be a super-interface of the EJB local component interface. + * Using a business methods interface is a best practice when implementing EJBs. + * @param businessInterface set the business interface of the EJB + */ + public void setBusinessInterface(Class businessInterface) { + this.businessInterface = businessInterface; + } + + /** + * Return the business interface of the EJB we're proxying. + */ + public Class getBusinessInterface() { + return this.businessInterface; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.businessInterface == null) { + throw new IllegalArgumentException("businessInterface is required"); + } + this.proxy = new ProxyFactory(this.businessInterface, this).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.proxy; + } + + public Class getObjectType() { + return this.businessInterface; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java new file mode 100644 index 00000000000..6be3b0b997e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java @@ -0,0 +1,182 @@ +/* + * 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.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.RemoteException; + +import javax.ejb.CreateException; +import javax.ejb.EJBObject; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.rmi.RmiClientInterceptorUtils; + +/** + * Basic invoker for a remote Stateless Session Bean. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

"Creates" a new EJB instance for each invocation, or caches the session + * bean instance for all invocations (see {@link #setCacheSessionBean}). + * See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the "lookupHomeOnStartup" + * property to "false", in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + *

This invoker is typically used with an RMI business interface, which serves + * as super-interface of the EJB component interface. Alternatively, this invoker + * can also proxy a remote SLSB with a matching non-RMI business interface, i.e. an + * interface that mirrors the EJB business methods but does not declare RemoteExceptions. + * In the latter case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's unchecked RemoteAccessException. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 09.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + * @see AbstractRemoteSlsbInvokerInterceptor#setRefreshHomeOnConnectFailure + */ +public class SimpleRemoteSlsbInvokerInterceptor extends AbstractRemoteSlsbInvokerInterceptor + implements DisposableBean { + + private boolean cacheSessionBean = false; + + private Object beanInstance; + + private final Object beanInstanceMonitor = new Object(); + + + /** + * Set whether to cache the actual session bean object. + *

Off by default for standard EJB compliance. Turn this flag + * on to optimize session bean access for servers that are + * known to allow for caching the actual session bean object. + * @see #setCacheHome + */ + public void setCacheSessionBean(boolean cacheSessionBean) { + this.cacheSessionBean = cacheSessionBean; + } + + + /** + * This implementation "creates" a new EJB instance for each invocation. + * Can be overridden for custom invocation strategies. + *

Alternatively, override {@link #getSessionBeanInstance} and + * {@link #releaseSessionBeanInstance} to change EJB instance creation, + * for example to hold a single shared EJB component instance. + */ + protected Object doInvoke(MethodInvocation invocation) throws Throwable { + Object ejb = null; + try { + ejb = getSessionBeanInstance(); + return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, ejb); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + RemoteException rex = (RemoteException) targetEx; + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), rex, isConnectFailure(rex), getJndiName()); + } + else if (targetEx instanceof CreateException) { + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), targetEx, "Could not create remote EJB [" + getJndiName() + "]"); + } + throw targetEx; + } + finally { + if (ejb instanceof EJBObject) { + releaseSessionBeanInstance((EJBObject) ejb); + } + } + } + + /** + * Return an EJB component instance to delegate the call to. + *

The default implementation delegates to {@link #newSessionBeanInstance}. + * @return the EJB component instance + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #newSessionBeanInstance + */ + protected Object getSessionBeanInstance() throws NamingException, InvocationTargetException { + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + if (this.beanInstance == null) { + this.beanInstance = newSessionBeanInstance(); + } + return this.beanInstance; + } + } + else { + return newSessionBeanInstance(); + } + } + + /** + * Release the given EJB instance. + *

The default implementation delegates to {@link #removeSessionBeanInstance}. + * @param ejb the EJB component instance to release + * @see #removeSessionBeanInstance + */ + protected void releaseSessionBeanInstance(EJBObject ejb) { + if (!this.cacheSessionBean) { + removeSessionBeanInstance(ejb); + } + } + + /** + * Reset the cached session bean instance, if necessary. + */ + protected void refreshHome() throws NamingException { + super.refreshHome(); + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + this.beanInstance = null; + } + } + } + + /** + * Remove the cached session bean instance, if necessary. + */ + public void destroy() { + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + if (this.beanInstance instanceof EJBObject) { + removeSessionBeanInstance((EJBObject) this.beanInstance); + } + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java new file mode 100644 index 00000000000..e7f3136011b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java @@ -0,0 +1,119 @@ +/* + * 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.ejb.access; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.ClassUtils; + +/** + * Convenient factory for remote SLSB proxies. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

If you want control over interceptor chaining, use an AOP ProxyFactoryBean + * with SimpleRemoteSlsbInvokerInterceptor rather than rely on this class. + * + *

In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the lookupHomeOnStartup + * property to false, in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + *

This proxy factory is typically used with an RMI business interface, which serves + * as super-interface of the EJB component interface. Alternatively, this factory + * can also proxy a remote SLSB with a matching non-RMI business interface, i.e. an + * interface that mirrors the EJB business methods but does not declare RemoteExceptions. + * In the latter case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's unchecked RemoteAccessException. + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @since 09.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + * @see AbstractRemoteSlsbInvokerInterceptor#setRefreshHomeOnConnectFailure + */ +public class SimpleRemoteStatelessSessionProxyFactoryBean extends SimpleRemoteSlsbInvokerInterceptor + implements FactoryBean, BeanClassLoaderAware { + + /** The business interface of the EJB we're proxying */ + private Class businessInterface; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** EJBObject */ + private Object proxy; + + + /** + * Set the business interface of the EJB we're proxying. + * This will normally be a super-interface of the EJB remote component interface. + * Using a business methods interface is a best practice when implementing EJBs. + *

You can also specify a matching non-RMI business interface, i.e. an interface + * that mirrors the EJB business methods but does not declare RemoteExceptions. + * In this case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's generic RemoteAccessException. + * @param businessInterface the business interface of the EJB + */ + public void setBusinessInterface(Class businessInterface) { + this.businessInterface = businessInterface; + } + + /** + * Return the business interface of the EJB we're proxying. + */ + public Class getBusinessInterface() { + return this.businessInterface; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.businessInterface == null) { + throw new IllegalArgumentException("businessInterface is required"); + } + this.proxy = new ProxyFactory(this.businessInterface, this).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.proxy; + } + + public Class getObjectType() { + return this.businessInterface; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/access/package.html b/org.springframework.context/src/main/java/org/springframework/ejb/access/package.html new file mode 100644 index 00000000000..5bc5538d1d0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/access/package.html @@ -0,0 +1,25 @@ + + + +This package contains classes that allow easy access to EJBs. +The basis are AOP interceptors run before and after the EJB invocation. +In particular, the classes in this package allow transparent access +to stateless session beans (SLSBs) with local interfaces, avoiding +the need for application code using them to use EJB-specific APIs +and JNDI lookups, and work with business interfaces that could be +implemented without using EJB. This provides a valuable decoupling +of client (such as web components) and business objects (which may +or may not be EJBs). This gives us the choice of introducing EJB +into an application (or removing EJB from an application) without +affecting code using business objects. + +

The motivation for the classes in this package are discussed in Chapter 11 of +Expert One-On-One J2EE Design and Development +by Rod Johnson (Wrox, 2002). + +

However, the implementation and naming of classes in this package has changed. +It now uses FactoryBeans and AOP, rather than the custom bean definitions described in +Expert One-on-One J2EE. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java new file mode 100644 index 00000000000..8c96809d668 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java @@ -0,0 +1,64 @@ +/* + * 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.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * Abstract base class for BeanDefinitionParsers which build + * JNDI-locating beans, supporting an optional "jndiEnvironment" + * bean property, populated from an "environment" XML sub-element. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +abstract class AbstractJndiLocatingBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser { + + public static final String ENVIRONMENT = "environment"; + + public static final String ENVIRONMENT_REF = "environment-ref"; + + public static final String JNDI_ENVIRONMENT = "jndiEnvironment"; + + + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && !ENVIRONMENT_REF.equals(attributeName)); + } + + protected void postProcess(BeanDefinitionBuilder definitionBuilder, Element element) { + Object envValue = DomUtils.getChildElementValueByTagName(element, ENVIRONMENT); + if (envValue != null) { + // Specific environment settings defined, overriding any shared properties. + definitionBuilder.addPropertyValue(JNDI_ENVIRONMENT, envValue); + } + else { + // Check whether there is a reference to shared environment properties... + String envRef = element.getAttribute(ENVIRONMENT_REF); + if (StringUtils.hasLength(envRef)) { + definitionBuilder.addPropertyValue(JNDI_ENVIRONMENT, new RuntimeBeanReference(envRef)); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java b/org.springframework.context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java new file mode 100644 index 00000000000..8a8e6d204a2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java @@ -0,0 +1,36 @@ +/* + * 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.ejb.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@link org.springframework.beans.factory.xml.NamespaceHandler} + * for the 'jee' namespace. + * + * @author Rob Harrop + * @since 2.0 + */ +public class JeeNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("jndi-lookup", new JndiLookupBeanDefinitionParser()); + registerBeanDefinitionParser("local-slsb", new LocalStatelessSessionBeanDefinitionParser()); + registerBeanDefinitionParser("remote-slsb", new RemoteStatelessSessionBeanDefinitionParser()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java new file mode 100644 index 00000000000..9c0c78eae19 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java @@ -0,0 +1,71 @@ +/* + * 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.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jndi.JndiObjectFactoryBean; +import org.springframework.util.StringUtils; + +/** + * Simple {@link org.springframework.beans.factory.xml.BeanDefinitionParser} implementation that + * translates jndi-lookup tag into {@link JndiObjectFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see JndiObjectFactoryBean + */ +class JndiLookupBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + public static final String DEFAULT_VALUE = "default-value"; + + public static final String DEFAULT_REF = "default-ref"; + + public static final String DEFAULT_OBJECT = "defaultObject"; + + + protected Class getBeanClass(Element element) { + return JndiObjectFactoryBean.class; + } + + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + !DEFAULT_VALUE.equals(attributeName) && !DEFAULT_REF.equals(attributeName)); + } + + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + String defaultValue = element.getAttribute(DEFAULT_VALUE); + String defaultRef = element.getAttribute(DEFAULT_REF); + if (StringUtils.hasLength(defaultValue)) { + if (StringUtils.hasLength(defaultRef)) { + parserContext.getReaderContext().error(" element is only allowed to contain either " + + "'default-value' attribute OR 'default-ref' attribute, not both", element); + } + builder.addPropertyValue(DEFAULT_OBJECT, defaultValue); + } + else if (StringUtils.hasLength(defaultRef)) { + builder.addPropertyValue(DEFAULT_OBJECT, new RuntimeBeanReference(defaultRef)); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java new file mode 100644 index 00000000000..e921ad41aea --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java @@ -0,0 +1,38 @@ +/* + * 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.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} + * implementation for parsing 'local-slsb' tags and + * creating {@link LocalStatelessSessionProxyFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class LocalStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + protected String getBeanClassName(Element element) { + return "org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java new file mode 100644 index 00000000000..ba73ade9cda --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java @@ -0,0 +1,38 @@ +/* + * 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.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} + * implementation for parsing 'remote-slsb' tags and + * creating {@link SimpleRemoteStatelessSessionProxyFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class RemoteStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + protected String getBeanClassName(Element element) { + return "org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/package.html b/org.springframework.context/src/main/java/org/springframework/ejb/config/package.html new file mode 100644 index 00000000000..a70e503ecaa --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/package.html @@ -0,0 +1,8 @@ + + + +Support package for EJB/J2EE-related configuration, +with XML schema being the primary configuration format. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.0.xsd b/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.0.xsd new file mode 100644 index 00000000000..016bf17a547 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.0.xsd @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.5.xsd b/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.5.xsd new file mode 100644 index 00000000000..09acaf39afe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/config/spring-jee-2.5.xsd @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/SpringBeanAutowiringInterceptor.java b/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/SpringBeanAutowiringInterceptor.java new file mode 100644 index 00000000000..993a3ffc755 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/SpringBeanAutowiringInterceptor.java @@ -0,0 +1,202 @@ +/* + * 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.ejb.interceptor; + +import java.util.Map; +import java.util.WeakHashMap; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.ejb.PostActivate; +import javax.ejb.PrePassivate; +import javax.interceptor.InvocationContext; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.access.BeanFactoryLocator; +import org.springframework.beans.factory.access.BeanFactoryReference; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.access.ContextSingletonBeanFactoryLocator; + +/** + * EJB3-compliant interceptor class that injects Spring beans into + * fields and methods which are annotated with @Autowired. + * Performs injection after construction as well as after activation + * of a passivated bean. + * + *

To be applied through an @Interceptors annotation in + * the EJB Session Bean or Message-Driven Bean class, or through an + * interceptor-binding XML element in the EJB deployment descriptor. + * + *

Delegates to Spring's {@link AutowiredAnnotationBeanPostProcessor} + * underneath, allowing for customization of its specific settings through + * overriding the {@link #configureBeanPostProcessor} template method. + * + *

The actual BeanFactory to obtain Spring beans from is determined + * by the {@link #getBeanFactory} template method. The default implementation + * obtains the Spring {@link ContextSingletonBeanFactoryLocator}, initialized + * from the default resource location classpath*:beanRefContext.xml, + * and obtains the single ApplicationContext defined there. + * + *

NOTE: If you have more than one shared ApplicationContext definition available + * in your EJB class loader, you need to override the {@link #getBeanFactoryLocatorKey} + * method and provide a specific locator key for each autowired EJB. + * Alternatively, override the {@link #getBeanFactory} template method and + * obtain the target factory explicitly. + * + *

WARNING: Do not define the same bean as Spring-managed bean and as + * EJB3 session bean in the same deployment unit. In particular, be + * careful when using the <context:component-scan> feature + * in combination with the deployment of Spring-based EJB3 session beans: + * Make sure that the EJB3 session beans are not autodetected as + * Spring-managed beans as well, using appropriate package restrictions. + * + * @author Juergen Hoeller + * @since 2.5.1 + * @see org.springframework.beans.factory.annotation.Autowired + * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator + * @see #getBeanFactoryLocatorKey + * @see org.springframework.ejb.support.AbstractEnterpriseBean#setBeanFactoryLocator + * @see org.springframework.ejb.support.AbstractEnterpriseBean#setBeanFactoryLocatorKey + */ +public class SpringBeanAutowiringInterceptor { + + /* + * We're keeping the BeanFactoryReference per target object in order to + * allow for using a shared interceptor instance on pooled target beans. + * This is not strictly necessary for EJB3 Session Beans and Message-Driven + * Beans, where interceptor instances get created per target bean instance. + * It simply protects against future usage of the interceptor in a shared scenario. + */ + private final Map beanFactoryReferences = + new WeakHashMap(); + + + /** + * Autowire the target bean after construction as well as after passivation. + * @param invocationContext the EJB3 invocation context + */ + @PostConstruct + @PostActivate + public void autowireBean(InvocationContext invocationContext) throws Exception { + doAutowireBean(invocationContext.getTarget()); + invocationContext.proceed(); + } + + /** + * Actually autowire the target bean after construction/passivation. + * @param target the target bean to autowire + */ + protected void doAutowireBean(Object target) { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + configureBeanPostProcessor(bpp, target); + bpp.setBeanFactory(getBeanFactory(target)); + bpp.processInjection(target); + } + + /** + * Template method for configuring the + * {@link AutowiredAnnotationBeanPostProcessor} used for autowiring. + * @param processor the AutowiredAnnotationBeanPostProcessor to configure + * @param target the target bean to autowire with this processor + */ + protected void configureBeanPostProcessor(AutowiredAnnotationBeanPostProcessor processor, Object target) { + } + + /** + * Determine the BeanFactory for autowiring the given target bean. + * @param target the target bean to autowire + * @return the BeanFactory to use (never null) + * @see #getBeanFactoryReference + */ + protected BeanFactory getBeanFactory(Object target) { + BeanFactory factory = getBeanFactoryReference(target).getFactory(); + if (factory instanceof ApplicationContext) { + factory = ((ApplicationContext) factory).getAutowireCapableBeanFactory(); + } + return factory; + } + + /** + * Determine the BeanFactoryReference for the given target bean. + *

The default implementation delegates to {@link #getBeanFactoryLocator} + * and {@link #getBeanFactoryLocatorKey}. + * @param target the target bean to autowire + * @return the BeanFactoryReference to use (never null) + * @see #getBeanFactoryLocator + * @see #getBeanFactoryLocatorKey + * @see org.springframework.beans.factory.access.BeanFactoryLocator#useBeanFactory(String) + */ + protected BeanFactoryReference getBeanFactoryReference(Object target) { + String key = getBeanFactoryLocatorKey(target); + BeanFactoryReference ref = getBeanFactoryLocator(target).useBeanFactory(key); + this.beanFactoryReferences.put(target, ref); + return ref; + } + + /** + * Determine the BeanFactoryLocator to obtain the BeanFactoryReference from. + *

The default implementation exposes Spring's default + * {@link ContextSingletonBeanFactoryLocator}. + * @param target the target bean to autowire + * @return the BeanFactoryLocator to use (never null) + * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator#getInstance() + */ + protected BeanFactoryLocator getBeanFactoryLocator(Object target) { + return ContextSingletonBeanFactoryLocator.getInstance(); + } + + /** + * Determine the BeanFactoryLocator key to use. This typically indicates + * the bean name of the ApplicationContext definition in + * classpath*:beanRefContext.xml resource files. + *

The default is null, indicating the single + * ApplicationContext defined in the locator. This must be overridden + * if more than one shared ApplicationContext definition is available. + * @param target the target bean to autowire + * @return the BeanFactoryLocator key to use (or null for + * referring to the single ApplicationContext defined in the locator) + */ + protected String getBeanFactoryLocatorKey(Object target) { + return null; + } + + + /** + * Release the factory which has been used for autowiring the target bean. + * @param invocationContext the EJB3 invocation context + */ + @PreDestroy + @PrePassivate + public void releaseBean(InvocationContext invocationContext) throws Exception { + doReleaseBean(invocationContext.getTarget()); + invocationContext.proceed(); + } + + /** + * Actually release the BeanFactoryReference for the given target bean. + * @param target the target bean to release + */ + protected void doReleaseBean(Object target) { + BeanFactoryReference ref = this.beanFactoryReferences.remove(target); + if (ref != null) { + ref.release(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/package.html b/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/package.html new file mode 100644 index 00000000000..f5a46617af6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/interceptor/package.html @@ -0,0 +1,9 @@ + + + +Support classes for EJB 3 Session Beans and Message-Driven Beans, +performing injection of Spring beans through an EJB 3 interceptor +that processes Spring's @Autowired annotation. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractEnterpriseBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractEnterpriseBean.java new file mode 100644 index 00000000000..a81fccf5836 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractEnterpriseBean.java @@ -0,0 +1,192 @@ +/* + * 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.ejb.support; + +import javax.ejb.EnterpriseBean; + +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.access.BeanFactoryLocator; +import org.springframework.beans.factory.access.BeanFactoryReference; +import org.springframework.context.access.ContextJndiBeanFactoryLocator; +import org.springframework.util.WeakReferenceMonitor; + +/** + * Base class for Spring-based EJB 2.x beans. Not intended for direct subclassing: + * Extend {@link AbstractStatelessSessionBean}, {@link AbstractStatefulSessionBean} + * or {@link AbstractMessageDrivenBean} instead. + * + *

Provides a standard way of loading a Spring BeanFactory. Subclasses act as a + * facade, with the business logic deferred to beans in the BeanFactory. Default + * is to use a {@link org.springframework.context.access.ContextJndiBeanFactoryLocator}, + * which will initialize an XML ApplicationContext from the class path (based on a JNDI + * name specified). For a different locator strategy, setBeanFactoryLocator + * may be called (before your EJB's ejbCreate method is invoked, + * e.g. in setSessionContext). For use of a shared ApplicationContext between + * multiple EJBs, where the container class loader setup supports this visibility, you may + * instead use a {@link org.springframework.context.access.ContextSingletonBeanFactoryLocator}. + * Alternatively, {@link #setBeanFactoryLocator} may be called with a custom implementation + * of the {@link org.springframework.beans.factory.access.BeanFactoryLocator} interface. + * + *

Note that we cannot use final for our implementation of EJB lifecycle + * methods, as this would violate the EJB specification. + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @see org.springframework.context.access.ContextJndiBeanFactoryLocator + * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator + */ +public abstract class AbstractEnterpriseBean implements EnterpriseBean { + + public static final String BEAN_FACTORY_PATH_ENVIRONMENT_KEY = "java:comp/env/ejb/BeanFactoryPath"; + + + /** + * Helper strategy that knows how to locate a Spring BeanFactory (or + * ApplicationContext). + */ + private BeanFactoryLocator beanFactoryLocator; + + /** factoryKey to be used with BeanFactoryLocator */ + private String beanFactoryLocatorKey; + + /** Spring BeanFactory that provides the namespace for this EJB */ + private BeanFactoryReference beanFactoryReference; + + + /** + * Set the BeanFactoryLocator to use for this EJB. Default is a + * ContextJndiBeanFactoryLocator. + *

Can be invoked before loadBeanFactory, for example in constructor or + * setSessionContext if you want to override the default locator. + *

Note that the BeanFactory is automatically loaded by the ejbCreate + * implementations of AbstractStatelessSessionBean and + * AbstractMessageDriverBean but needs to be explicitly loaded in custom + * AbstractStatefulSessionBean ejbCreate methods. + * @see AbstractStatelessSessionBean#ejbCreate + * @see AbstractMessageDrivenBean#ejbCreate + * @see AbstractStatefulSessionBean#loadBeanFactory + * @see org.springframework.context.access.ContextJndiBeanFactoryLocator + */ + public void setBeanFactoryLocator(BeanFactoryLocator beanFactoryLocator) { + this.beanFactoryLocator = beanFactoryLocator; + } + + /** + * Set the bean factory locator key. + *

In case of the default BeanFactoryLocator implementation, + * ContextJndiBeanFactoryLocator, this is the JNDI path. The default value + * of this property is "java:comp/env/ejb/BeanFactoryPath". + *

Can be invoked before {@link #loadBeanFactory}, for example in the constructor + * or setSessionContext if you want to override the default locator key. + * @see #BEAN_FACTORY_PATH_ENVIRONMENT_KEY + */ + public void setBeanFactoryLocatorKey(String factoryKey) { + this.beanFactoryLocatorKey = factoryKey; + } + + /** + * Load a Spring BeanFactory namespace. Subclasses must invoke this method. + *

Package-visible as it shouldn't be called directly by user-created + * subclasses. + * @see org.springframework.ejb.support.AbstractStatelessSessionBean#ejbCreate() + */ + void loadBeanFactory() throws BeansException { + if (this.beanFactoryLocator == null) { + this.beanFactoryLocator = new ContextJndiBeanFactoryLocator(); + } + if (this.beanFactoryLocatorKey == null) { + this.beanFactoryLocatorKey = BEAN_FACTORY_PATH_ENVIRONMENT_KEY; + } + + this.beanFactoryReference = this.beanFactoryLocator.useBeanFactory(this.beanFactoryLocatorKey); + + // We cannot rely on the container to call ejbRemove() (it's skipped in + // the case of system exceptions), so ensure the the bean factory + // reference is eventually released. + WeakReferenceMonitor.monitor(this, new BeanFactoryReferenceReleaseListener(this.beanFactoryReference)); + } + + /** + * Unload the Spring BeanFactory instance. The default {@link #ejbRemove()} + * method invokes this method, but subclasses which override ejbRemove() + * must invoke this method themselves. + *

Package-visible as it shouldn't be called directly by user-created + * subclasses. + */ + void unloadBeanFactory() throws FatalBeanException { + // We will not ever get here if the container skips calling ejbRemove(), + // but the WeakReferenceMonitor will still clean up (later) in that case. + if (this.beanFactoryReference != null) { + this.beanFactoryReference.release(); + this.beanFactoryReference = null; + } + } + + /** + * May be called after ejbCreate(). + * @return the bean factory + */ + protected BeanFactory getBeanFactory() { + return this.beanFactoryReference.getFactory(); + } + + /** + * EJB lifecycle method, implemented to invoke onEjbRemove() + * and unload the BeanFactory afterwards. + *

Don't override it (although it can't be made final): code your shutdown + * in {@link #onEjbRemove()}. + */ + public void ejbRemove() { + onEjbRemove(); + unloadBeanFactory(); + } + + /** + * Subclasses must implement this method to do any initialization they would + * otherwise have done in an ejbRemove() method. + * The BeanFactory will be unloaded afterwards. + *

This implementation is empty, to be overridden in subclasses. + * The same restrictions apply to the work of this method as to an + * ejbRemove() method. + */ + protected void onEjbRemove() { + // empty + } + + + /** + * Implementation of WeakReferenceMonitor's ReleaseListener callback interface. + * Release the given BeanFactoryReference if the monitor detects that there + * are no strong references to the handle anymore. + */ + private static class BeanFactoryReferenceReleaseListener implements WeakReferenceMonitor.ReleaseListener { + + private final BeanFactoryReference beanFactoryReference; + + public BeanFactoryReferenceReleaseListener(BeanFactoryReference beanFactoryReference) { + this.beanFactoryReference = beanFactoryReference; + } + + public void released() { + this.beanFactoryReference.release(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractJmsMessageDrivenBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractJmsMessageDrivenBean.java new file mode 100644 index 00000000000..f78bd734c50 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractJmsMessageDrivenBean.java @@ -0,0 +1,32 @@ +/* + * 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.ejb.support; + +import javax.jms.MessageListener; + +/** + * Convenient base class for JMS-based EJB 2.x MDBs. Requires subclasses + * to implement the JMS javax.jms.MessageListener interface. + * + * @author Rod Johnson + */ +public abstract class AbstractJmsMessageDrivenBean extends AbstractMessageDrivenBean implements MessageListener { + + // Empty: The purpose of this class is to ensure + // that subclasses implement javax.jms.MessageListener. + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractMessageDrivenBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractMessageDrivenBean.java new file mode 100644 index 00000000000..e8944c748b1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractMessageDrivenBean.java @@ -0,0 +1,91 @@ +/* + * 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.ejb.support; + +import javax.ejb.MessageDrivenBean; +import javax.ejb.MessageDrivenContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Convenient base class for EJB 2.x MDBs. + * Doesn't require JMS, as EJB 2.1 MDBs are no longer JMS-specific; + * see the {@link AbstractJmsMessageDrivenBean} subclass. + * + *

This class ensures that subclasses have access to the + * MessageDrivenContext provided by the EJB container, and implement + * a no-arg ejbCreate() method as required by the EJB + * specification. This ejbCreate() method loads a BeanFactory, + * before invoking the onEjbCreate() method, which is + * supposed to contain subclass-specific initialization. + * + *

NB: We cannot use final methods to implement EJB API methods, + * as this violates the EJB specification. However, there should be + * no need to override the setMessageDrivenContext or + * ejbCreate() methods. + * + * @author Rod Johnson + */ +public abstract class AbstractMessageDrivenBean extends AbstractEnterpriseBean + implements MessageDrivenBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private MessageDrivenContext messageDrivenContext; + + + /** + * Required lifecycle method. Sets the MessageDriven context. + * @param messageDrivenContext MessageDrivenContext + */ + public void setMessageDrivenContext(MessageDrivenContext messageDrivenContext) { + this.messageDrivenContext = messageDrivenContext; + } + + /** + * Convenience method for subclasses to use. + * @return the MessageDrivenContext passed to this EJB by the EJB container + */ + protected final MessageDrivenContext getMessageDrivenContext() { + return this.messageDrivenContext; + } + + /** + * Lifecycle method required by the EJB specification but not the + * MessageDrivenBean interface. This implementation loads the BeanFactory. + *

Don't override it (although it can't be made final): code initialization + * in onEjbCreate(), which is called when the BeanFactory is available. + *

Unfortunately we can't load the BeanFactory in setSessionContext(), + * as resource manager access isn't permitted and the BeanFactory may require it. + */ + public void ejbCreate() { + loadBeanFactory(); + onEjbCreate(); + } + + /** + * Subclasses must implement this method to do any initialization they would + * otherwise have done in an ejbCreate() method. In contrast + * to ejbCreate(), the BeanFactory will have been loaded here. + *

The same restrictions apply to the work of this method as + * to an ejbCreate() method. + */ + protected abstract void onEjbCreate(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractSessionBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractSessionBean.java new file mode 100644 index 00000000000..6768b713866 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractSessionBean.java @@ -0,0 +1,54 @@ +/* + * 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.ejb.support; + +import javax.ejb.SessionContext; + +/** + * Base class for Spring-based EJB 2.x session beans. Not intended for direct + * subclassing: Extend {@link AbstractStatelessSessionBean} or + * {@link AbstractStatefulSessionBean} instead. + * + *

This class saves the session context provided by the EJB container in an + * instance variable and exposes it through the {@link SmartSessionBean} interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractSessionBean extends AbstractEnterpriseBean implements SmartSessionBean { + + /** The SessionContext passed to this EJB */ + private SessionContext sessionContext; + + + /** + * Set the session context for this EJB. + *

When overriding this method, be sure to invoke this form of it first. + */ + public void setSessionContext(SessionContext sessionContext) { + this.sessionContext = sessionContext; + } + + /** + * Convenience method for subclasses, returning the EJB session context + * saved on initialization ({@link #setSessionContext}). + */ + public final SessionContext getSessionContext() { + return this.sessionContext; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatefulSessionBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatefulSessionBean.java new file mode 100644 index 00000000000..6256180c667 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatefulSessionBean.java @@ -0,0 +1,73 @@ +/* + * 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.ejb.support; + +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; + +/** + * Convenient base class for EJB 2.x stateful session beans (SFSBs). + * SFSBs should extend this class, leaving them to implement the + * ejbActivate() and ejbPassivate() lifecycle + * methods to comply with the requirements of the EJB specification. + * + *

Note: Subclasses should invoke the loadBeanFactory() + * method in their custom ejbCreate() and ejbActivate() + * methods, and should invoke the unloadBeanFactory() method in + * their ejbPassivate method. + * + *

Note: The default BeanFactoryLocator used by this class's superclass + * (ContextJndiBeanFactoryLocator) is not serializable. Therefore, + * when using the default BeanFactoryLocator, or another variant which is + * not serializable, subclasses must call setBeanFactoryLocator(null) + * in ejbPassivate(), with a corresponding call to + * setBeanFactoryLocator(xxx) in ejbActivate() + * unless relying on the default locator. + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @see org.springframework.context.access.ContextJndiBeanFactoryLocator + */ +public abstract class AbstractStatefulSessionBean extends AbstractSessionBean { + + /** + * Load a Spring BeanFactory namespace. Exposed for subclasses + * to load a BeanFactory in their ejbCreate() methods. + * Those callers would normally want to catch BeansException and + * rethrow it as {@link javax.ejb.CreateException}. Unless the + * BeanFactory is known to be serializable, this method must also + * be called from ejbActivate(), to reload a context + * removed via a call to unloadBeanFactory() from + * the ejbPassivate() implementation. + */ + protected void loadBeanFactory() throws BeansException { + super.loadBeanFactory(); + } + + /** + * Unload the Spring BeanFactory instance. The default ejbRemove() + * method invokes this method, but subclasses which override + * ejbRemove() must invoke this method themselves. + *

Unless the BeanFactory is known to be serializable, this method + * must also be called from ejbPassivate(), with a corresponding + * call to loadBeanFactory() from ejbActivate(). + */ + protected void unloadBeanFactory() throws FatalBeanException { + super.unloadBeanFactory(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatelessSessionBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatelessSessionBean.java new file mode 100644 index 00000000000..4f685fb242f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/AbstractStatelessSessionBean.java @@ -0,0 +1,97 @@ +/* + * 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.ejb.support; + +import javax.ejb.CreateException; +import javax.ejb.EJBException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Convenient base class for EJB 2.x stateless session beans (SLSBs), + * minimizing the work involved in implementing an SLSB and preventing + * common errors. Note that SLSBs are the most useful kind of EJB. + * + *

As the ejbActivate() and ejbPassivate() methods cannot be invoked + * on SLSBs, these methods are implemented to throw an exception and should + * not be overriden by subclasses. (Unfortunately the EJB specification + * forbids enforcing this by making EJB lifecycle methods final.) + * + *

There should be no need to override the setSessionContext() + * or ejbCreate() lifecycle methods. + * + *

Subclasses are left to implement the onEjbCreate() method + * to do whatever initialization they wish to do after their BeanFactory has + * already been loaded, and is available from the getBeanFactory() + * method. + * + *

This class provides the no-arg ejbCreate() method required + * by the EJB specification, but not the SessionBean interface, eliminating + * a common cause of EJB deployment failure. + * + * @author Rod Johnson + */ +public abstract class AbstractStatelessSessionBean extends AbstractSessionBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * This implementation loads the BeanFactory. A BeansException thrown by + * loadBeanFactory will simply get propagated, as it is a runtime exception. + *

Don't override it (although it can't be made final): code your own + * initialization in onEjbCreate(), which is called when the BeanFactory + * is available. + *

Unfortunately we can't load the BeanFactory in setSessionContext(), + * as resource manager access isn't permitted there - but the BeanFactory + * may require it. + */ + public void ejbCreate() throws CreateException { + loadBeanFactory(); + onEjbCreate(); + } + + /** + * Subclasses must implement this method to do any initialization + * they would otherwise have done in an ejbCreate() method. + * In contrast to ejbCreate, the BeanFactory will have been loaded here. + *

The same restrictions apply to the work of this method as + * to an ejbCreate() method. + * @throws CreateException + */ + protected abstract void onEjbCreate() throws CreateException; + + + /** + * @see javax.ejb.SessionBean#ejbActivate(). This method always throws an exception, as + * it should not be invoked by the EJB container. + */ + public void ejbActivate() throws EJBException { + throw new IllegalStateException("ejbActivate must not be invoked on a stateless session bean"); + } + + /** + * @see javax.ejb.SessionBean#ejbPassivate(). This method always throws an exception, as + * it should not be invoked by the EJB container. + */ + public void ejbPassivate() throws EJBException { + throw new IllegalStateException("ejbPassivate must not be invoked on a stateless session bean"); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/SmartSessionBean.java b/org.springframework.context/src/main/java/org/springframework/ejb/support/SmartSessionBean.java new file mode 100644 index 00000000000..8d6488b3d89 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/SmartSessionBean.java @@ -0,0 +1,44 @@ +/* + * 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.ejb.support; + +import javax.ejb.SessionBean; +import javax.ejb.SessionContext; + +/** + * Interface to be implemented by Session Beans that want + * to expose important state to cooperating classes. + * + *

Implemented by Spring's AbstractSessionBean class and hence by + * all of Spring's specific Session Bean support classes, such as + * {@link AbstractStatelessSessionBean} and {@link AbstractStatefulSessionBean}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see AbstractStatelessSessionBean + * @see AbstractStatefulSessionBean + */ +public interface SmartSessionBean extends SessionBean { + + /** + * Return the SessionContext that was passed to the Session Bean + * by the EJB container. Can be used by cooperating infrastructure + * code to get access to the user credentials, for example. + */ + SessionContext getSessionContext(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ejb/support/package.html b/org.springframework.context/src/main/java/org/springframework/ejb/support/package.html new file mode 100644 index 00000000000..01235c8e4ea --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ejb/support/package.html @@ -0,0 +1,25 @@ + + + +

Base classes to make implementing EJB 2.x beans simpler and less error-prone, +as well as guaranteeing a Spring BeanFactory is available to such EJBs. +This promotes good practice, with EJB services used for transaction +management, thread management, and (possibly) remoting, while +business logic is implemented in easily testable POJOs.

+ +

In this model, the EJB is a facade, with as many POJO helpers +behind the BeanFactory as required.

+ +

Note that the default behavior is to look for an EJB enviroment variable +with name ejb/BeanFactoryPath that specifies the +location on the classpath of an XML bean factory definition +file (such as /com/mycom/mypackage/mybeans.xml). +If this JNDI key is missing, your EJB subclass won't successfully +initialize in the container.

+ +

Check out the org.springframework.ejb.interceptor +package for equivalent support for the EJB 3 component model, +providing annotation-based autowiring using an EJB 3 interceptor.

+ + + diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java new file mode 100644 index 00000000000..0e6582de46f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java @@ -0,0 +1,190 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.instrument.InstrumentationSavingAgent; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} relying on VM {@link Instrumentation}. + * + *

Start the JVM specifying the Java agent to be used, like as follows: + * + *

-javaagent:path/to/spring-agent.jar + * + *

where spring-agent.jar is a JAR file containing the + * {@link InstrumentationSavingAgent} class. + * + *

In Eclipse, for example, set the "Run configuration"'s JVM args + * to be of the form: + * + *

-javaagent:${project_loc}/lib/spring-agent.jar + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see InstrumentationSavingAgent + */ +public class InstrumentationLoadTimeWeaver implements LoadTimeWeaver { + + private static final boolean AGENT_CLASS_PRESENT = ClassUtils.isPresent( + "org.springframework.instrument.InstrumentationSavingAgent", + InstrumentationLoadTimeWeaver.class.getClassLoader()); + + + private final ClassLoader classLoader; + + private final Instrumentation instrumentation; + + private final List transformers = new ArrayList(4); + + + /** + * Create a new InstrumentationLoadTimeWeaver for the default ClassLoader. + */ + public InstrumentationLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new InstrumentationLoadTimeWeaver for the given ClassLoader. + * @param classLoader the ClassLoader that registered transformers are supposed to apply to + */ + public InstrumentationLoadTimeWeaver(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + this.instrumentation = getInstrumentation(); + } + + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + FilteringClassFileTransformer actualTransformer = + new FilteringClassFileTransformer(transformer, this.classLoader); + synchronized (this.transformers) { + if (this.instrumentation == null) { + throw new IllegalStateException( + "Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation."); + } + this.instrumentation.addTransformer(actualTransformer); + this.transformers.add(actualTransformer); + } + } + + /** + * We have the ability to weave the current class loader when starting the + * JVM in this way, so the instrumentable class loader will always be the + * current loader. + */ + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + /** + * This implementation always returns a {@link SimpleThrowawayClassLoader}. + */ + public ClassLoader getThrowawayClassLoader() { + return new SimpleThrowawayClassLoader(getInstrumentableClassLoader()); + } + + /** + * Remove all registered transformers, in inverse order of registration. + */ + public void removeTransformers() { + synchronized (this.transformers) { + if (!this.transformers.isEmpty()) { + for (int i = this.transformers.size() - 1; i >= 0; i--) { + this.instrumentation.removeTransformer(this.transformers.get(i)); + } + this.transformers.clear(); + } + } + } + + + /** + * Check whether an Instrumentation instance is available for the current VM. + * @see #getInstrumentation() + */ + public static boolean isInstrumentationAvailable() { + return (getInstrumentation() != null); + } + + /** + * Obtain the Instrumentation instance for the current VM, if available. + * @return the Instrumentation instance, or null if none found + * @see #isInstrumentationAvailable() + */ + private static Instrumentation getInstrumentation() { + if (AGENT_CLASS_PRESENT) { + return InstrumentationAccessor.getInstrumentation(); + } + else { + return null; + } + } + + + /** + * Inner class to avoid InstrumentationSavingAgent dependency. + */ + private static class InstrumentationAccessor { + + public static Instrumentation getInstrumentation() { + return InstrumentationSavingAgent.getInstrumentation(); + } + } + + + /** + * Decorator that only applies the given target transformer to a specific ClassLoader. + */ + private static class FilteringClassFileTransformer implements ClassFileTransformer { + + private final ClassFileTransformer targetTransformer; + + private final ClassLoader targetClassLoader; + + public FilteringClassFileTransformer(ClassFileTransformer targetTransformer, ClassLoader targetClassLoader) { + this.targetTransformer = targetTransformer; + this.targetClassLoader = targetClassLoader; + } + + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (!this.targetClassLoader.equals(loader)) { + return null; + } + return this.targetTransformer.transform( + loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } + + public String toString() { + return "FilteringClassFileTransformer for: " + this.targetTransformer.toString(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java new file mode 100644 index 00000000000..9e518009cd4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java @@ -0,0 +1,63 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +/** + * Defines the contract for adding one or more + * {@link ClassFileTransformer ClassFileTransformers} to a {@link ClassLoader}. + * + *

Implementations may operate on the current context ClassLoader + * or expose their own instrumentable ClassLoader. + * + * @author Rod Johnson + * @author Costin Leau + * @since 2.0 + * @see java.lang.instrument.ClassFileTransformer + */ +public interface LoadTimeWeaver { + + /** + * Add a ClassFileTransformer to be applied by this + * LoadTimeWeaver. + * @param transformer the ClassFileTransformer to add + */ + void addTransformer(ClassFileTransformer transformer); + + /** + * Return a ClassLoader that supports instrumentation + * through AspectJ-style load-time weaving based on user-defined + * {@link ClassFileTransformer ClassFileTransformers}. + *

May be the current ClassLoader, or a ClassLoader + * created by this {@link LoadTimeWeaver} instance. + * @return the ClassLoader which will expose + * instrumented classes according to the registered transformers + */ + ClassLoader getInstrumentableClassLoader(); + + /** + * Return a throwaway ClassLoader, enabling classes to be + * loaded and inspected without affecting the parent ClassLoader. + *

Should not return the same instance of the {@link ClassLoader} + * returned from an invocation of {@link #getInstrumentableClassLoader()}. + * @return a temporary throwaway ClassLoader; should return + * a new instance for each call, with no existing state + */ + ClassLoader getThrowawayClassLoader(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java new file mode 100644 index 00000000000..b39bef907e9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java @@ -0,0 +1,139 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link LoadTimeWeaver} which uses reflection to delegate to an underlying ClassLoader + * with well-known transformation hooks. The underlying ClassLoader is expected to + * support the following weaving methods (as defined in the {@link LoadTimeWeaver} + * interface): + *

    + *
  • public void addTransformer(java.lang.instrument.ClassFileTransformer): + * for registering the given ClassFileTransformer on this ClassLoader + *
  • public ClassLoader getThrowawayClassLoader(): + * for obtaining a throwaway class loader for this ClassLoader (optional; + * ReflectiveLoadTimeWeaver will fall back to a SimpleThrowawayClassLoader if + * that method isn't available) + *
+ * + *

Please note that the above methods must reside in a class that is + * publicly accessible, although the class itself does not have to be visible + * to the application's class loader. + * + *

The reflective nature of this LoadTimeWeaver is particularly useful when the + * underlying ClassLoader implementation is loaded in a different class loader itself + * (such as the application server's class loader which is not visible to the + * web application). There is no direct API dependency between this LoadTimeWeaver + * adapter and the underlying ClassLoader, just a 'loose' method contract. + * + *

This is the LoadTimeWeaver to use in combination with Spring's + * {@link org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader} + * for Tomcat 5.0+ as well as with the Resin application server version 3.1+. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see #addTransformer(java.lang.instrument.ClassFileTransformer) + * @see #getThrowawayClassLoader() + * @see SimpleThrowawayClassLoader + * @see org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader + */ +public class ReflectiveLoadTimeWeaver implements LoadTimeWeaver { + + private static final String ADD_TRANSFORMER_METHOD_NAME = "addTransformer"; + + private static final String GET_THROWAWAY_CLASS_LOADER_METHOD_NAME = "getThrowawayClassLoader"; + + private static final Log logger = LogFactory.getLog(ReflectiveLoadTimeWeaver.class); + + + private final ClassLoader classLoader; + + private final Method addTransformerMethod; + + private final Method getThrowawayClassLoaderMethod; + + + /** + * Create a new ReflectiveLoadTimeWeaver for the current context class + * loader, which needs to support the required weaving methods. + */ + public ReflectiveLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new SimpleLoadTimeWeaver for the given class loader. + * @param classLoader the ClassLoader to delegate to for + * weaving (must support the required weaving methods). + * @throws IllegalStateException if the supplied ClassLoader + * does not support the required weaving methods + */ + public ReflectiveLoadTimeWeaver(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + this.addTransformerMethod = ClassUtils.getMethodIfAvailable( + this.classLoader.getClass(), ADD_TRANSFORMER_METHOD_NAME, + new Class [] {ClassFileTransformer.class}); + if (this.addTransformerMethod == null) { + throw new IllegalStateException( + "ClassLoader [" + classLoader.getClass().getName() + "] does NOT provide an " + + "'addTransformer(ClassFileTransformer)' method."); + } + this.getThrowawayClassLoaderMethod = ClassUtils.getMethodIfAvailable( + this.classLoader.getClass(), GET_THROWAWAY_CLASS_LOADER_METHOD_NAME, + new Class[0]); + // getThrowawayClassLoader method is optional + if (this.getThrowawayClassLoaderMethod == null) { + if (logger.isInfoEnabled()) { + logger.info("The ClassLoader [" + classLoader.getClass().getName() + "] does NOT provide a " + + "'getThrowawayClassLoader()' method; SimpleThrowawayClassLoader will be used instead."); + } + } + } + + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + ReflectionUtils.invokeMethod(this.addTransformerMethod, this.classLoader, new Object[] {transformer}); + } + + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + public ClassLoader getThrowawayClassLoader() { + if (this.getThrowawayClassLoaderMethod != null) { + return (ClassLoader) ReflectionUtils.invokeMethod(this.getThrowawayClassLoaderMethod, this.classLoader, + new Object[0]); + } + else { + return new SimpleThrowawayClassLoader(this.classLoader); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java new file mode 100644 index 00000000000..8f8447aef6a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java @@ -0,0 +1,127 @@ +/* + * 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.instrument.classloading; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Subclass of ShadowingClassLoader that overrides attempts to + * locate certain files. + * + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0 + */ +public class ResourceOverridingShadowingClassLoader extends ShadowingClassLoader { + + private static final Enumeration EMPTY_URL_ENUMERATION = new Enumeration() { + public boolean hasMoreElements() { + return false; + } + public URL nextElement() { + throw new UnsupportedOperationException("Should not be called. I am empty."); + } + }; + + + /** + * Key is asked for value: value is actual value + */ + private Map overrides = new HashMap(); + + + /** + * Create a new ResourceOverridingShadowingClassLoader, + * decorating the given ClassLoader. + * @param enclosingClassLoader the ClassLoader to decorate + */ + public ResourceOverridingShadowingClassLoader(ClassLoader enclosingClassLoader) { + super(enclosingClassLoader); + } + + + /** + * Return the resource (if any) at the new path + * on an attempt to locate a resource at the old path. + * @param oldPath the path requested + * @param newPath the actual path to be looked up + */ + public void override(String oldPath, String newPath) { + this.overrides.put(oldPath, newPath); + } + + /** + * Ensure that a resource with the given path is not found. + * @param oldPath the path of the resource to hide even if + * it exists in the parent ClassLoader + */ + public void suppress(String oldPath) { + this.overrides.put(oldPath, null); + } + + /** + * Copy all overrides from the given ClassLoader. + * @param other the other ClassLoader to copy from + */ + public void copyOverrides(ResourceOverridingShadowingClassLoader other) { + Assert.notNull(other, "Other ClassLoader must not be null"); + this.overrides.putAll(other.overrides); + } + + + @Override + public URL getResource(String requestedPath) { + if (this.overrides.containsKey(requestedPath)) { + String overriddenPath = this.overrides.get(requestedPath); + return (overriddenPath != null ? super.getResource(overriddenPath) : null); + } + else { + return super.getResource(requestedPath); + } + } + + @Override + public InputStream getResourceAsStream(String requestedPath) { + if (this.overrides.containsKey(requestedPath)) { + String overriddenPath = this.overrides.get(requestedPath); + return (overriddenPath != null ? super.getResourceAsStream(overriddenPath) : null); + } + else { + return super.getResourceAsStream(requestedPath); + } + } + + @Override + public Enumeration getResources(String requestedPath) throws IOException { + if (this.overrides.containsKey(requestedPath)) { + String overriddenLocation = this.overrides.get(requestedPath); + return (overriddenLocation != null ? + super.getResources(overriddenLocation) : EMPTY_URL_ENUMERATION); + } + else { + return super.getResources(requestedPath); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java new file mode 100644 index 00000000000..0ec1441eb1d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java @@ -0,0 +1,195 @@ +/* + * 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.instrument.classloading; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * ClassLoader decorator that shadows an enclosing ClassLoader, + * applying registered transformers to all affected classes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.0 + * @see #addTransformer + * @see org.springframework.core.OverridingClassLoader + */ +public class ShadowingClassLoader extends DecoratingClassLoader { + + /** Packages that are excluded by default */ + public static final String[] DEFAULT_EXCLUDED_PACKAGES = + new String[] {"java.", "javax.", "sun.", "oracle.", "com.sun.", "com.ibm.", "COM.ibm.", + "org.w3c.", "org.xml.", "org.dom4j.", "org.eclipse", "org.aspectj.", "net.sf.cglib.", + "org.apache.xerces.", "org.apache.commons.logging."}; + + + private final ClassLoader enclosingClassLoader; + + private final List classFileTransformers = new LinkedList(); + + private final Map classCache = new HashMap(); + + + /** + * Create a new ShadowingClassLoader, decorating the given ClassLoader. + * @param enclosingClassLoader the ClassLoader to decorate + */ + public ShadowingClassLoader(ClassLoader enclosingClassLoader) { + Assert.notNull(enclosingClassLoader, "Enclosing ClassLoader must not be null"); + this.enclosingClassLoader = enclosingClassLoader; + for (int i = 0; i < DEFAULT_EXCLUDED_PACKAGES.length; i++) { + excludePackage(DEFAULT_EXCLUDED_PACKAGES[i]); + } + } + + + /** + * Add the given ClassFileTransformer to the list of transformers that this + * ClassLoader will apply. + * @param transformer the ClassFileTransformer + */ + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + this.classFileTransformers.add(transformer); + } + + /** + * Copy all ClassFileTransformers from the given ClassLoader to the list of + * transformers that this ClassLoader will apply. + * @param other the ClassLoader to copy from + */ + public void copyTransformers(ShadowingClassLoader other) { + Assert.notNull(other, "Other ClassLoader must not be null"); + this.classFileTransformers.addAll(other.classFileTransformers); + } + + + public Class loadClass(String name) throws ClassNotFoundException { + if (shouldShadow(name)) { + Class cls = this.classCache.get(name); + if (cls != null) { + return cls; + } + return doLoadClass(name); + } + else { + return this.enclosingClassLoader.loadClass(name); + } + } + + /** + * Determine whether the given class should be excluded from shadowing. + * @param className the name of the class + * @return whether the specified class should be shadowed + */ + private boolean shouldShadow(String className) { + return (!className.equals(getClass().getName()) && !className.endsWith("ShadowingClassLoader") + && isEligibleForShadowing(className) && !isClassNameExcludedFromShadowing(className)); + } + + /** + * Determine whether the specified class is eligible for shadowing + * by this class loader. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #isExcluded + */ + protected boolean isEligibleForShadowing(String className) { + return !isExcluded(className); + } + + /** + * Subclasses can override this method to specify whether or not a + * particular class should be excluded from shadowing. + * @param className the class name to test + * @return whether the specified class is excluded + * @deprecated in favor of {@link #isEligibleForShadowing} + */ + protected boolean isClassNameExcludedFromShadowing(String className) { + return false; + } + + + private Class doLoadClass(String name) throws ClassNotFoundException { + String internalName = StringUtils.replace(name, ".", "/") + ".class"; + InputStream is = this.enclosingClassLoader.getResourceAsStream(internalName); + if (is == null) { + throw new ClassNotFoundException(name); + } + try { + byte[] bytes = FileCopyUtils.copyToByteArray(is); + bytes = applyTransformers(name, bytes); + Class cls = defineClass(name, bytes, 0, bytes.length); + // Additional check for defining the package, if not defined yet. + if (cls.getPackage() == null) { + int packageSeparator = name.lastIndexOf('.'); + if (packageSeparator != -1) { + String packageName = name.substring(0, packageSeparator); + definePackage(packageName, null, null, null, null, null, null, null); + } + } + this.classCache.put(name, cls); + return cls; + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + private byte[] applyTransformers(String name, byte[] bytes) { + String internalName = StringUtils.replace(name, ".", "/"); + try { + for (ClassFileTransformer transformer : this.classFileTransformers) { + byte[] transformed = transformer.transform(this, internalName, null, null, bytes); + bytes = (transformed != null ? transformed : bytes); + } + return bytes; + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException(ex); + } + } + + + public URL getResource(String name) { + return this.enclosingClassLoader.getResource(name); + } + + public InputStream getResourceAsStream(String name) { + return this.enclosingClassLoader.getResourceAsStream(name); + } + + public Enumeration getResources(String name) throws IOException { + return this.enclosingClassLoader.getResources(name); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java new file mode 100644 index 00000000000..191db3f83c2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java @@ -0,0 +1,64 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.core.OverridingClassLoader; + +/** + * Simplistic implementation of an instrumentable ClassLoader. + * + *

Usable in tests and standalone environments. + * + * @author Rod Johnson + * @author Costin Leau + * @since 2.0 + */ +public class SimpleInstrumentableClassLoader extends OverridingClassLoader { + + private final WeavingTransformer weavingTransformer; + + + /** + * Create a new SimpleLoadTimeWeaver for the given + * ClassLoader. + * @param parent the ClassLoader to build a simple + * instrumentable ClassLoader for + */ + public SimpleInstrumentableClassLoader(ClassLoader parent) { + super(parent); + this.weavingTransformer = new WeavingTransformer(parent); + } + + + /** + * Add a ClassFileTransformer to be applied by this + * ClassLoader. + * @param transformer the ClassFileTransformer to register + */ + public void addTransformer(ClassFileTransformer transformer) { + this.weavingTransformer.addTransformer(transformer); + } + + + @Override + protected byte[] transformIfNecessary(String name, byte[] bytes) { + return this.weavingTransformer.transformIfNecessary(name, bytes); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java new file mode 100644 index 00000000000..5393bad255f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java @@ -0,0 +1,80 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * LoadTimeWeaver that builds and exposes a + * {@link SimpleInstrumentableClassLoader}. + * + *

Mainly intended for testing environments, where it is sufficient to + * perform all class transformation on a newly created + * ClassLoader instance. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see #getInstrumentableClassLoader() + * @see SimpleInstrumentableClassLoader + * @see ReflectiveLoadTimeWeaver + */ +public class SimpleLoadTimeWeaver implements LoadTimeWeaver { + + private final SimpleInstrumentableClassLoader classLoader; + + + /** + * Create a new SimpleLoadTimeWeaver for the current context + * ClassLoader. + * @see SimpleInstrumentableClassLoader + */ + public SimpleLoadTimeWeaver() { + this.classLoader = new SimpleInstrumentableClassLoader(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new SimpleLoadTimeWeaver for the given + * ClassLoader. + * @param classLoader the ClassLoader to build a simple + * instrumentable ClassLoader on top of + */ + public SimpleLoadTimeWeaver(SimpleInstrumentableClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + } + + + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(transformer); + } + + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + /** + * This implementation builds a {@link SimpleThrowawayClassLoader}. + */ + public ClassLoader getThrowawayClassLoader() { + return new SimpleThrowawayClassLoader(getInstrumentableClassLoader()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java new file mode 100644 index 00000000000..227a7d25562 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java @@ -0,0 +1,39 @@ +/* + * 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.instrument.classloading; + +import org.springframework.core.OverridingClassLoader; + +/** + * ClassLoader that can be used to load classes without bringing them + * into the parent loader. Intended to support JPA "temp class loader" + * requirement, but not JPA-specific. + * + * @author Rod Johnson + * @since 2.0 + */ +public class SimpleThrowawayClassLoader extends OverridingClassLoader { + + /** + * Create a new SimpleThrowawayClassLoader for the given class loader. + * @param parent the ClassLoader to build a throwaway ClassLoader for + */ + public SimpleThrowawayClassLoader(ClassLoader parent) { + super(parent); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java new file mode 100644 index 00000000000..bcae2c2f5cc --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java @@ -0,0 +1,107 @@ +/* + * 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.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +/** + * ClassFileTransformer-based weaver, allowing for a list of transformers to be + * applied on a class byte array. Normally used inside class loaders. + * + *

Note: This class is deliberately implemented for minimal external dependencies, + * since it is included in weaver jars (to be deployed into application servers). + * + * @author Rod Johnson + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class WeavingTransformer { + + private final ClassLoader classLoader; + + private final List transformers = new ArrayList(); + + + /** + * Create a new WeavingTransformer for the given class loader. + * @param classLoader the ClassLoader to build a transformer for + */ + public WeavingTransformer(ClassLoader classLoader) { + if (classLoader == null) { + throw new IllegalArgumentException("ClassLoader must not be null"); + } + this.classLoader = classLoader; + } + + + /** + * Add a class file transformer to be applied by this weaver. + * @param transformer the class file transformer to register + */ + public void addTransformer(ClassFileTransformer transformer) { + if (transformer == null) { + throw new IllegalArgumentException("Transformer must not be null"); + } + this.transformers.add(transformer); + } + + + /** + * Apply transformation on a given class byte definition. + * The method will always return a non-null byte array (if no transformation has taken place + * the array content will be identical to the original one). + * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) + * @param bytes class byte definition + * @return (possibly transformed) class byte definition + */ + public byte[] transformIfNecessary(String className, byte[] bytes) { + String internalName = className.replace(".", "/"); + return transformIfNecessary(className, internalName, bytes, null); + } + + /** + * Apply transformation on a given class byte definition. + * The method will always return a non-null byte array (if no transformation has taken place + * the array content will be identical to the original one). + * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) + * @param internalName class name internal name in / format (i.e. some/package/SomeClass) + * @param bytes class byte definition + * @param pd protection domain to be used (can be null) + * @return (possibly transformed) class byte definition + */ + public byte[] transformIfNecessary(String className, String internalName, byte[] bytes, ProtectionDomain pd) { + byte[] result = bytes; + for (ClassFileTransformer cft : this.transformers) { + try { + byte[] transformed = cft.transform(this.classLoader, internalName, null, pd, result); + if (transformed != null) { + result = transformed; + } + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException("Class file transformation failed", ex); + } + } + return result; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/ClassTransformerAdapter.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/ClassTransformerAdapter.java new file mode 100644 index 00000000000..98367126d86 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/ClassTransformerAdapter.java @@ -0,0 +1,57 @@ +/* + * 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.instrument.classloading.glassfish; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +import javax.persistence.spi.ClassTransformer; + +/** + * Adapter that implements the JPA ClassTransformer interface (as required by GlassFish) + * based on a given JDK 1.5 ClassFileTransformer. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0.1 + */ +class ClassTransformerAdapter implements ClassTransformer { + + private final ClassFileTransformer classFileTransformer; + + + /** + * Build a new ClassTransformerAdapter for the given ClassFileTransformer. + * @param classFileTransformer the JDK 1.5 ClassFileTransformer to wrap + */ + public ClassTransformerAdapter(ClassFileTransformer classFileTransformer) { + this.classFileTransformer = classFileTransformer; + } + + + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + byte[] result = this.classFileTransformer.transform( + loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + + // If no transformation was done, return null. + return (result == classfileBuffer ? null : result); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java new file mode 100644 index 00000000000..fb83917c6d8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java @@ -0,0 +1,96 @@ +/* + * 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.instrument.classloading.glassfish; + +import java.lang.instrument.ClassFileTransformer; + +import com.sun.enterprise.loader.InstrumentableClassLoader; + +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for GlassFish's + * {@link InstrumentableClassLoader}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0.1 + * @see com.sun.enterprise.loader.InstrumentableClassLoader + */ +public class GlassFishLoadTimeWeaver implements LoadTimeWeaver { + + private final InstrumentableClassLoader classLoader; + + + /** + * Create a new instance of the GlassFishLoadTimeWeaver class + * using the default {@link ClassLoader}. + * @see #GlassFishLoadTimeWeaver(ClassLoader) + */ + public GlassFishLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new instance of the GlassFishLoadTimeWeaver class. + * @param classLoader the specific {@link ClassLoader} to use; must not be null + * @throws IllegalArgumentException if the supplied classLoader is null; + * or if the supplied classLoader is not an {@link InstrumentableClassLoader} + */ + public GlassFishLoadTimeWeaver(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + InstrumentableClassLoader icl = determineClassLoader(classLoader); + if (icl == null) { + throw new IllegalArgumentException(classLoader + " and its parents are not suitable ClassLoaders: " + + "An [" + InstrumentableClassLoader.class.getName() + "] implementation is required."); + } + this.classLoader = icl; + } + + /** + * Determine the GlassFish {@link InstrumentableClassLoader} for the given + * {@link ClassLoader}. + * @param classLoader the ClassLoader to check + * @return the InstrumentableClassLoader, or null if none found + */ + protected InstrumentableClassLoader determineClassLoader(ClassLoader classLoader) { + // Detect transformation-aware ClassLoader by traversing the hierarchy + // (as in GlassFish, Spring can be loaded by the WebappClassLoader). + for (ClassLoader cl = classLoader; cl != null; cl = cl.getParent()) { + if (cl instanceof InstrumentableClassLoader) { + return (InstrumentableClassLoader) cl; + } + } + return null; + } + + + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(new ClassTransformerAdapter(transformer)); + } + + public ClassLoader getInstrumentableClassLoader() { + return (ClassLoader) this.classLoader; + } + + public ClassLoader getThrowawayClassLoader() { + return this.classLoader.copy(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/package.html b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/package.html new file mode 100644 index 00000000000..1b07e7031ba --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/glassfish/package.html @@ -0,0 +1,7 @@ + + + +Support for class instrumentation on GlassFish / Sun Application Server. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JClassPreprocessorAdapter.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JClassPreprocessorAdapter.java new file mode 100644 index 00000000000..4368bd16f11 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JClassPreprocessorAdapter.java @@ -0,0 +1,81 @@ +/* + * 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.instrument.classloading.oc4j; + +import oracle.classloader.util.ClassPreprocessor; +import org.springframework.util.Assert; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +/** + * {@link ClassPreprocessor} adapter for OC4J, delegating to a standard + * JDK {@link ClassFileTransformer} underneath. + * + *

Many thanks to Mike Keith + * for his assistance. + * + * @author Costin Leau + * @since 2.0 + */ +class OC4JClassPreprocessorAdapter implements ClassPreprocessor { + + private final ClassFileTransformer transformer; + + + /** + * Creates a new instance of the {@link OC4JClassPreprocessorAdapter} class. + * @param transformer the {@link ClassFileTransformer} to be adapted (must not be null) + * @throws IllegalArgumentException if the supplied transformer is null + */ + public OC4JClassPreprocessorAdapter(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + this.transformer = transformer; + } + + + public ClassPreprocessor initialize(ClassLoader loader) { + return this; + } + + public byte[] processClass(String className, byte origClassBytes[], int offset, int length, ProtectionDomain pd, + ClassLoader loader) { + try { + byte[] tempArray = new byte[length]; + System.arraycopy(origClassBytes, offset, tempArray, 0, length); + + // NB: OC4J passes className as "." without class while the + // transformer expects a VM, "/" format + byte[] result = this.transformer.transform(loader, className.replace('.', '/'), null, pd, tempArray); + return (result != null ? result : origClassBytes); + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException("Cannot transform because of illegal class format", ex); + } + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getName()); + builder.append(" for transformer: "); + builder.append(this.transformer); + return builder.toString(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JLoadTimeWeaver.java new file mode 100644 index 00000000000..e77f7dd83ce --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/OC4JLoadTimeWeaver.java @@ -0,0 +1,80 @@ +/* + * 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.instrument.classloading.oc4j; + +import java.lang.instrument.ClassFileTransformer; + +import oracle.classloader.util.ClassLoaderUtilities; + +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for OC4J's instrumentable ClassLoader. + * + *

NOTE: Requires Oracle OC4J version 10.1.3.1 or higher. + * + *

Many thanks to Mike Keith + * for his assistance. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class OC4JLoadTimeWeaver implements LoadTimeWeaver { + + private final ClassLoader classLoader; + + + /** + * Creates a new instance of thie {@link OC4JLoadTimeWeaver} class + * using the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public OC4JLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Creates a new instance of the {@link OC4JLoadTimeWeaver} class + * using the supplied {@link ClassLoader}. + * @param classLoader the ClassLoader to delegate to for weaving + */ + public OC4JLoadTimeWeaver(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + } + + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + // Since OC4J 10.1.3's PolicyClassLoader is going to be removed, + // we rely on the ClassLoaderUtilities API instead. + OC4JClassPreprocessorAdapter processor = new OC4JClassPreprocessorAdapter(transformer); + ClassLoaderUtilities.addPreprocessor(this.classLoader, processor); + } + + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + public ClassLoader getThrowawayClassLoader() { + return ClassLoaderUtilities.copy(this.classLoader); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/package.html b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/package.html new file mode 100644 index 00000000000..1b57b01b058 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/oc4j/package.html @@ -0,0 +1,7 @@ + + + +Support for class instrumentation on Oracle OC4J. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/package.html b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/package.html new file mode 100644 index 00000000000..bf7d5e3553c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/package.html @@ -0,0 +1,8 @@ + + + +Support package for load time weaving based on class loaders, +as required by JPA providers (but not JPA-specific). + + + diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoader.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoader.java new file mode 100644 index 00000000000..9a6c3c39e41 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoader.java @@ -0,0 +1,114 @@ +/* + * 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.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.util.Assert; + +/** + * Reflective wrapper around a WebLogic 10 class loader. Used to + * encapsulate the classloader-specific methods (discovered and + * called through reflection) from the load-time weaver. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +class WebLogicClassLoader { + + private static final String GENERIC_CLASS_LOADER_NAME = "weblogic.utils.classloaders.GenericClassLoader"; + + private static final String CLASS_PRE_PROCESSOR_NAME = "weblogic.utils.classloaders.ClassPreProcessor"; + + + private final ClassLoader internalClassLoader; + + private final Class wlPreProcessorClass; + + private final Method addPreProcessorMethod; + + private final Method getClassFinderMethod; + + private final Method getParentMethod; + + private final Constructor wlGenericClassLoaderConstructor; + + + public WebLogicClassLoader(ClassLoader classLoader) { + Class wlGenericClassLoaderClass = null; + try { + wlGenericClassLoaderClass = classLoader.loadClass(GENERIC_CLASS_LOADER_NAME); + this.wlPreProcessorClass = classLoader.loadClass(CLASS_PRE_PROCESSOR_NAME); + this.addPreProcessorMethod = classLoader.getClass().getMethod( + "addInstanceClassPreProcessor", this.wlPreProcessorClass); + this.getClassFinderMethod = classLoader.getClass().getMethod("getClassFinder"); + this.getParentMethod = classLoader.getClass().getMethod("getParent"); + this.wlGenericClassLoaderConstructor = wlGenericClassLoaderClass.getConstructor( + this.getClassFinderMethod.getReturnType(), ClassLoader.class); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize WebLogic ClassLoader because WebLogic 10 API classes are not available", ex); + } + Assert.isInstanceOf(wlGenericClassLoaderClass, classLoader, + "ClassLoader must be instance of [" + wlGenericClassLoaderClass.getName() + "]"); + this.internalClassLoader = classLoader; + } + + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "ClassFileTransformer must not be null"); + try { + InvocationHandler adapter = new WebLogicClassPreProcessorAdapter(transformer, this.internalClassLoader); + Object adapterInstance = Proxy.newProxyInstance(this.wlPreProcessorClass.getClassLoader(), + new Class[] {this.wlPreProcessorClass}, adapter); + this.addPreProcessorMethod.invoke(this.internalClassLoader, adapterInstance); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebLogic addInstanceClassPreProcessor method threw exception", ex.getCause()); + } + catch (Exception ex) { + throw new IllegalStateException("Could not invoke WebLogic addInstanceClassPreProcessor method", ex); + } + } + + public ClassLoader getInternalClassLoader() { + return this.internalClassLoader; + } + + public ClassLoader getThrowawayClassLoader() { + try { + Object classFinder = this.getClassFinderMethod.invoke(this.internalClassLoader); + Object parent = this.getParentMethod.invoke(this.internalClassLoader); + // arguments for 'clone'-like method + return (ClassLoader) this.wlGenericClassLoaderConstructor.newInstance(classFinder, parent); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebLogic GenericClassLoader constructor failed", ex.getCause()); + } + catch (Exception ex) { + throw new IllegalStateException("Could not construct WebLogic GenericClassLoader", ex); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java new file mode 100644 index 00000000000..b6ce745382d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java @@ -0,0 +1,96 @@ +/* + * 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.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Hashtable; + +/** + * Adapter that implements WebLogic ClassPreProcessor interface, delegating to a + * standard JDK {@link ClassFileTransformer} underneath. + * + *

To avoid compile time checks again the vendor API, a dynamic proxy is + * being used. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +class WebLogicClassPreProcessorAdapter implements InvocationHandler { + + private final ClassFileTransformer transformer; + + private final ClassLoader loader; + + + /** + * Creates a new {@link WebLogicClassPreProcessorAdapter}. + * @param transformer the {@link ClassFileTransformer} to be adapted (must + * not be null) + */ + public WebLogicClassPreProcessorAdapter(ClassFileTransformer transformer, ClassLoader loader) { + this.transformer = transformer; + this.loader = loader; + } + + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("equals".equals(method.getName())) { + return (Boolean.valueOf(proxy == args[0])); + } + else if ("hashCode".equals(method.getName())) { + return hashCode(); + } + else if ("initialize".equals(method.getName())) { + initialize((Hashtable) args[0]); + return null; + } + else if ("preProcess".equals(method.getName())) { + return preProcess((String) args[0], (byte[]) args[1]); + } + else { + throw new IllegalArgumentException("Unknown method: " + method); + } + } + + + public void initialize(Hashtable params) { + } + + public byte[] preProcess(String className, byte[] classBytes) { + try { + byte[] result = this.transformer.transform(this.loader, className, null, null, classBytes); + return (result != null ? result : classBytes); + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException("Cannot transform due to illegal class format", ex); + } + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getName()); + builder.append(" for transformer: "); + builder.append(this.transformer); + return builder.toString(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java new file mode 100644 index 00000000000..fffb32f1195 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java @@ -0,0 +1,73 @@ +/* + * 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.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for WebLogic's instrumentable + * ClassLoader. + * + *

NOTE: Requires BEA WebLogic version 10 or higher. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +public class WebLogicLoadTimeWeaver implements LoadTimeWeaver { + + private final WebLogicClassLoader classLoader; + + + /** + * Creates a new instance of the {@link WebLogicLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public WebLogicLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Creates a new instance of the {@link WebLogicLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the ClassLoader to delegate to for + * weaving (must not be null) + */ + public WebLogicLoadTimeWeaver(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = new WebLogicClassLoader(classLoader); + } + + + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(transformer); + } + + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader.getInternalClassLoader(); + } + + public ClassLoader getThrowawayClassLoader() { + return this.classLoader.getThrowawayClassLoader(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/package.html b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/package.html new file mode 100644 index 00000000000..e2d43b821af --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/instrument/classloading/weblogic/package.html @@ -0,0 +1,7 @@ + + + +Support for class instrumentation on BEA WebLogic 10. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/JmxException.java b/org.springframework.context/src/main/java/org/springframework/jmx/JmxException.java new file mode 100644 index 00000000000..b431ca9f621 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/JmxException.java @@ -0,0 +1,47 @@ +/* + * 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.jmx; + +import org.springframework.core.NestedRuntimeException; + +/** + * General base exception to be thrown on JMX errors. + * Unchecked since JMX failures are usually fatal. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class JmxException extends NestedRuntimeException { + + /** + * Constructor for JmxException. + * @param msg the detail message + */ + public JmxException(String msg) { + super(msg); + } + + /** + * Constructor for JmxException. + * @param msg the detail message + * @param cause the root cause (usually a raw JMX API exception) + */ + public JmxException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java b/org.springframework.context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java new file mode 100644 index 00000000000..dce8a8bc11e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java @@ -0,0 +1,49 @@ +/* + * 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.jmx; + +/** + * Exception thrown when we cannot locate an instance of an MBeanServer, + * or when more than one instance is found. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.support.JmxUtils#locateMBeanServer + */ +public class MBeanServerNotFoundException extends JmxException { + + /** + * Create a new MBeanServerNotFoundException with the + * supplied error message. + * @param msg the error message + */ + public MBeanServerNotFoundException(String msg) { + super(msg); + } + + /** + * Create a new MBeanServerNotFoundException with the + * specified error message and root cause. + * @param msg the error message + * @param cause the root cause + */ + public MBeanServerNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java new file mode 100644 index 00000000000..1d2a66d30ac --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java @@ -0,0 +1,88 @@ +/* + * 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.jmx.access; + +import java.io.IOException; +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jmx.support.JmxUtils; + +/** + * Internal helper class for managing a JMX connector. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +class ConnectorDelegate { + + private final static Log logger = LogFactory.getLog(ConnectorDelegate.class); + + private JMXConnector connector; + + + /** + * Connects to the remote MBeanServer using the configured JMXServiceURL: + * to the specified JMX service, or to a local MBeanServer if no service URL specified. + * @param serviceUrl the JMX service URL to connect to (may be null) + * @param environment the JMX environment for the connector (may be null) + * @param agentId the local JMX MBeanServer's agent id (may be null) + */ + public MBeanServerConnection connect(JMXServiceURL serviceUrl, Map environment, String agentId) + throws MBeanServerNotFoundException { + + if (serviceUrl != null) { + if (logger.isDebugEnabled()) { + logger.debug("Connecting to remote MBeanServer at URL [" + serviceUrl + "]"); + } + try { + this.connector = JMXConnectorFactory.connect(serviceUrl, environment); + return this.connector.getMBeanServerConnection(); + } + catch (IOException ex) { + throw new MBeanServerNotFoundException("Could not connect to remote MBeanServer [" + serviceUrl + "]", ex); + } + } + else { + logger.debug("Attempting to locate local MBeanServer"); + return JmxUtils.locateMBeanServer(agentId); + } + } + + /** + * Closes any JMXConnector that may be managed by this interceptor. + */ + public void close() { + if (this.connector != null) { + try { + this.connector.close(); + } + catch (IOException ex) { + logger.debug("Could not close JMX connector", ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java new file mode 100644 index 00000000000..e2bdf799382 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java @@ -0,0 +1,41 @@ +/* + * 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.jmx.access; + +import javax.management.JMRuntimeException; + +/** + * Thrown when trying to invoke an operation on a proxy that is not exposed + * by the proxied MBean resource's management interface. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + */ +public class InvalidInvocationException extends JMRuntimeException { + + /** + * Create a new InvalidInvocationException with the supplied + * error message. + * @param msg the detail message + */ + public InvalidInvocationException(String msg) { + super(msg); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java new file mode 100644 index 00000000000..61a3d8828f9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java @@ -0,0 +1,50 @@ +/* + * 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.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when an invocation on an MBean resource failed with an exception (either + * a reflection exception or an exception thrown by the target method itself). + * + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + */ +public class InvocationFailureException extends JmxException { + + /** + * Create a new InvocationFailureException with the supplied + * error message. + * @param msg the detail message + */ + public InvocationFailureException(String msg) { + super(msg); + } + + /** + * Create a new InvocationFailureException with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public InvocationFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java new file mode 100644 index 00000000000..5a4ebddebd2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java @@ -0,0 +1,600 @@ +/* + * 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.jmx.access; + +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.management.Attribute; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.JMException; +import javax.management.JMX; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanServerConnection; +import javax.management.MBeanServerInvocationHandler; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.ReflectionException; +import javax.management.RuntimeErrorException; +import javax.management.RuntimeMBeanException; +import javax.management.RuntimeOperationsException; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; +import javax.management.remote.JMXServiceURL; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.JdkVersion; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an + * MBean running on the supplied MBeanServerConnection. + * Works for both local and remote MBeanServerConnections. + * + *

By default, the MBeanClientInterceptor will connect to the + * MBeanServer and cache MBean metadata at startup. This can + * be undesirable when running against a remote MBeanServer + * that may not be running when the application starts. Through setting the + * {@link #setConnectOnStartup(boolean) connectOnStartup} property to "false", + * you can defer this process until the first invocation against the proxy. + * + *

Requires JMX 1.2's MBeanServerConnection feature. + * As a consequence, this class will not work on JMX 1.0. + * + *

This functionality is usually used through {@link MBeanProxyFactoryBean}. + * See the javadoc of that class for more information. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanProxyFactoryBean + * @see #setConnectOnStartup + */ +public class MBeanClientInterceptor + implements MethodInterceptor, BeanClassLoaderAware, InitializingBean, DisposableBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private MBeanServerConnection server; + + private JMXServiceURL serviceUrl; + + private Map environment; + + private String agentId; + + private boolean connectOnStartup = true; + + private boolean refreshOnConnectFailure = false; + + private ObjectName objectName; + + private boolean useStrictCasing = true; + + private Class managementInterface; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private final ConnectorDelegate connector = new ConnectorDelegate(); + + private MBeanServerConnection serverToUse; + + private MBeanServerInvocationHandler invocationHandler; + + private Map allowedAttributes; + + private Map allowedOperations; + + private final Map signatureCache = new HashMap(); + + private final Object preparationMonitor = new Object(); + + + /** + * Set the MBeanServerConnection used to connect to the + * MBean which all invocations are routed to. + */ + public void setServer(MBeanServerConnection server) { + this.server = server; + } + + /** + * Set the service URL of the remote MBeanServer. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Specify the environment for the JMX connector. + * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map) + */ + public void setEnvironment(Map environment) { + this.environment = environment; + } + + /** + * Allow Map access to the environment to be set for the connector, + * with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via + * "environment[myKey]". This is particularly useful for + * adding or overriding entries in child bean definitions. + */ + public Map getEnvironment() { + return this.environment; + } + + /** + * Set the agent id of the MBeanServer to locate. + *

Default is none. If specified, this will result in an + * attempt being made to locate the attendant MBeanServer, unless + * the {@link #setServiceUrl "serviceUrl"} property has been set. + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * Set whether or not the proxy should connect to the MBeanServer + * at creation time ("true") or the first time it is invoked ("false"). + * Default is "true". + */ + public void setConnectOnStartup(boolean connectOnStartup) { + this.connectOnStartup = connectOnStartup; + } + + /** + * Set whether to refresh the MBeanServer connection on connect failure. + * Default is "false". + *

Can be turned on to allow for hot restart of the JMX server, + * automatically reconnecting and retrying in case of an IOException. + */ + public void setRefreshOnConnectFailure(boolean refreshOnConnectFailure) { + this.refreshOnConnectFailure = refreshOnConnectFailure; + } + + /** + * Set the ObjectName of the MBean which calls are routed to, + * as ObjectName instance or as String. + */ + public void setObjectName(Object objectName) throws MalformedObjectNameException { + this.objectName = ObjectNameManager.getInstance(objectName); + } + + /** + * Set whether to use strict casing for attributes. Enabled by default. + *

When using strict casing, a JavaBean property with a getter such as + * getFoo() translates to an attribute called Foo. + * With strict casing disabled, getFoo() would translate to just + * foo. + */ + public void setUseStrictCasing(boolean useStrictCasing) { + this.useStrictCasing = useStrictCasing; + } + + /** + * Set the management interface of the target MBean, exposing bean property + * setters and getters for MBean attributes and conventional Java methods + * for MBean operations. + */ + public void setManagementInterface(Class managementInterface) { + this.managementInterface = managementInterface; + } + + /** + * Return the management interface of the target MBean, + * or null if none specified. + */ + protected final Class getManagementInterface() { + return this.managementInterface; + } + + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + /** + * Prepares the MBeanServerConnection if the "connectOnStartup" + * is turned on (which it is by default). + */ + public void afterPropertiesSet() { + if (this.server != null && this.refreshOnConnectFailure) { + throw new IllegalArgumentException("'refreshOnConnectFailure' does not work when setting " + + "a 'server' reference. Prefer 'serviceUrl' etc instead."); + } + if (this.connectOnStartup) { + prepare(); + } + } + + /** + * Ensures that an MBeanServerConnection is configured and attempts + * to detect a local connection if one is not supplied. + */ + public void prepare() { + synchronized (this.preparationMonitor) { + if (this.server != null) { + this.serverToUse = this.server; + } + else { + this.serverToUse = null; + this.serverToUse = this.connector.connect(this.serviceUrl, this.environment, this.agentId); + } + this.invocationHandler = null; + if (this.useStrictCasing) { + // Use the JDK's own MBeanServerInvocationHandler, + // in particular for native MXBean support on Java 6. + if (JdkVersion.isAtLeastJava16()) { + this.invocationHandler = + new MBeanServerInvocationHandler(this.serverToUse, this.objectName, + (this.managementInterface != null && JMX.isMXBeanInterface(this.managementInterface))); + } + else { + this.invocationHandler = new MBeanServerInvocationHandler(this.serverToUse, this.objectName); + } + } + else { + // Non-strict casing can only be achieved through custom + // invocation handling. Only partial MXBean support available! + retrieveMBeanInfo(); + } + } + } + /** + * Loads the management interface info for the configured MBean into the caches. + * This information is used by the proxy when determining whether an invocation matches + * a valid operation or attribute on the management interface of the managed resource. + */ + private void retrieveMBeanInfo() throws MBeanInfoRetrievalException { + try { + MBeanInfo info = this.serverToUse.getMBeanInfo(this.objectName); + + MBeanAttributeInfo[] attributeInfo = info.getAttributes(); + this.allowedAttributes = new HashMap(attributeInfo.length); + for (int x = 0; x < attributeInfo.length; x++) { + this.allowedAttributes.put(attributeInfo[x].getName(), attributeInfo[x]); + } + + MBeanOperationInfo[] operationInfo = info.getOperations(); + this.allowedOperations = new HashMap(operationInfo.length); + for (int x = 0; x < operationInfo.length; x++) { + MBeanOperationInfo opInfo = operationInfo[x]; + Class[] paramTypes = JmxUtils.parameterInfoToTypes(opInfo.getSignature(), this.beanClassLoader); + this.allowedOperations.put(new MethodCacheKey(opInfo.getName(), paramTypes), opInfo); + } + } + catch (ClassNotFoundException ex) { + throw new MBeanInfoRetrievalException("Unable to locate class specified in method signature", ex); + } + catch (IntrospectionException ex) { + throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + "]", ex); + } + catch (InstanceNotFoundException ex) { + // if we are this far this shouldn't happen, but... + throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + + "]: it is likely that this bean was unregistered during the proxy creation process", + ex); + } + catch (ReflectionException ex) { + throw new MBeanInfoRetrievalException("Unable to read MBean info for bean [ " + this.objectName + "]", ex); + } + catch (IOException ex) { + throw new MBeanInfoRetrievalException("An IOException occurred when communicating with the " + + "MBeanServer. It is likely that you are communicating with a remote MBeanServer. " + + "Check the inner exception for exact details.", ex); + } + } + + /** + * Return whether this client interceptor has already been prepared, + * i.e. has already looked up the server and cached all metadata. + */ + protected boolean isPrepared() { + synchronized (this.preparationMonitor) { + return (this.serverToUse != null); + } + } + + + /** + * Route the invocation to the configured managed resource.. + * @param invocation the MethodInvocation to re-route + * @return the value returned as a result of the re-routed invocation + * @throws Throwable an invocation error propagated to the user + * @see #doInvoke + * @see #handleConnectFailure + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + // Lazily connect to MBeanServer if necessary. + synchronized (this.preparationMonitor) { + if (!isPrepared()) { + prepare(); + } + } + try { + return doInvoke(invocation); + } + catch (MBeanConnectFailureException ex) { + return handleConnectFailure(invocation, ex); + } + catch (IOException ex) { + return handleConnectFailure(invocation, ex); + } + } + + /** + * Refresh the connection and retry the MBean invocation if possible. + *

If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, + * if it failed as well + * @see #setRefreshOnConnectFailure + * @see #doInvoke + */ + protected Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshOnConnectFailure) { + String msg = "Could not connect to JMX server - retrying"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex); + } + else if (logger.isWarnEnabled()) { + logger.warn(msg); + } + prepare(); + return doInvoke(invocation); + } + else { + throw ex; + } + } + + /** + * Route the invocation to the configured managed resource. Correctly routes JavaBean property + * access to MBeanServerConnection.get/setAttribute and method invocation to + * MBeanServerConnection.invoke. + * @param invocation the MethodInvocation to re-route + * @return the value returned as a result of the re-routed invocation + * @throws Throwable an invocation error propagated to the user + */ + protected Object doInvoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + try { + Object result = null; + if (this.invocationHandler != null) { + result = this.invocationHandler.invoke(invocation.getThis(), method, invocation.getArguments()); + } + else { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + result = invokeAttribute(pd, invocation); + } + else { + result = invokeOperation(method, invocation.getArguments()); + } + } + return convertResultValueIfNecessary(result, method.getReturnType()); + } + catch (MBeanException ex) { + throw ex.getTargetException(); + } + catch (RuntimeMBeanException ex) { + throw ex.getTargetException(); + } + catch (RuntimeErrorException ex) { + throw ex.getTargetError(); + } + catch (RuntimeOperationsException ex) { + // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code. + RuntimeException rex = ex.getTargetException(); + if (rex instanceof RuntimeMBeanException) { + throw ((RuntimeMBeanException) rex).getTargetException(); + } + else if (rex instanceof RuntimeErrorException) { + throw ((RuntimeErrorException) rex).getTargetError(); + } + else { + throw rex; + } + } + catch (OperationsException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new InvalidInvocationException(ex.getMessage()); + } + } + catch (JMException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new InvocationFailureException("JMX access failed", ex); + } + } + catch (IOException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new MBeanConnectFailureException("I/O failure during JMX access", ex); + } + } + } + + private Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation) + throws JMException, IOException { + + String attributeName = JmxUtils.getAttributeName(pd, this.useStrictCasing); + MBeanAttributeInfo inf = (MBeanAttributeInfo) this.allowedAttributes.get(attributeName); + // If no attribute is returned, we know that it is not defined in the + // management interface. + if (inf == null) { + throw new InvalidInvocationException( + "Attribute '" + pd.getName() + "' is not exposed on the management interface"); + } + if (invocation.getMethod().equals(pd.getReadMethod())) { + if (inf.isReadable()) { + return this.serverToUse.getAttribute(this.objectName, attributeName); + } + else { + throw new InvalidInvocationException("Attribute '" + attributeName + "' is not readable"); + } + } + else if (invocation.getMethod().equals(pd.getWriteMethod())) { + if (inf.isWritable()) { + this.serverToUse.setAttribute(this.objectName, new Attribute(attributeName, invocation.getArguments()[0])); + return null; + } + else { + throw new InvalidInvocationException("Attribute '" + attributeName + "' is not writable"); + } + } + else { + throw new IllegalStateException( + "Method [" + invocation.getMethod() + "] is neither a bean property getter nor a setter"); + } + } + + /** + * Routes a method invocation (not a property get/set) to the corresponding + * operation on the managed resource. + * @param method the method corresponding to operation on the managed resource. + * @param args the invocation arguments + * @return the value returned by the method invocation. + */ + private Object invokeOperation(Method method, Object[] args) throws JMException, IOException { + MethodCacheKey key = new MethodCacheKey(method.getName(), method.getParameterTypes()); + MBeanOperationInfo info = (MBeanOperationInfo) this.allowedOperations.get(key); + if (info == null) { + throw new InvalidInvocationException("Operation '" + method.getName() + + "' is not exposed on the management interface"); + } + String[] signature = null; + synchronized (this.signatureCache) { + signature = (String[]) this.signatureCache.get(method); + if (signature == null) { + signature = JmxUtils.getMethodSignature(method); + this.signatureCache.put(method, signature); + } + } + return this.serverToUse.invoke(this.objectName, method.getName(), args, signature); + } + + /** + * Convert the given result object (from attribute access or operation invocation) + * to the specified target class for returning from the proxy method. + * @param result the result object as returned by the MBeanServer + * @param targetClass the result type of the proxy method that's been invoked + * @return the converted result object, or the passed-in object if no conversion + * is necessary + */ + protected Object convertResultValueIfNecessary(Object result, Class targetClass) { + try { + if (result == null) { + return null; + } + if (ClassUtils.isAssignableValue(targetClass, result)) { + return result; + } + if (result instanceof CompositeData) { + Method fromMethod = targetClass.getMethod("from", new Class[] {CompositeData.class}); + return ReflectionUtils.invokeMethod(fromMethod, null, new Object[] {result}); + } + else if (result instanceof TabularData) { + Method fromMethod = targetClass.getMethod("from", new Class[] {TabularData.class}); + return ReflectionUtils.invokeMethod(fromMethod, null, new Object[] {result}); + } + else { + throw new InvocationFailureException( + "Incompatible result value [" + result + "] for target type [" + targetClass.getName() + "]"); + } + } + catch (NoSuchMethodException ex) { + throw new InvocationFailureException( + "Could not obtain 'find(CompositeData)' / 'find(TabularData)' method on target type [" + + targetClass.getName() + "] for conversion of MXBean data structure [" + result + "]"); + } + } + + public void destroy() { + this.connector.close(); + } + + + /** + * Simple wrapper class around a method name and its signature. + * Used as the key when caching methods. + */ + private static class MethodCacheKey { + + private final String name; + + private final Class[] parameterTypes; + + /** + * Create a new instance of MethodCacheKey with the supplied + * method name and parameter list. + * @param name the name of the method + * @param parameterTypes the arguments in the method signature + */ + public MethodCacheKey(String name, Class[] parameterTypes) { + this.name = name; + this.parameterTypes = (parameterTypes != null ? parameterTypes : new Class[0]); + } + + public boolean equals(Object other) { + if (other == this) { + return true; + } + MethodCacheKey otherKey = (MethodCacheKey) other; + return (this.name.equals(otherKey.name) && Arrays.equals(this.parameterTypes, otherKey.parameterTypes)); + } + + public int hashCode() { + return this.name.hashCode(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java new file mode 100644 index 00000000000..06eb49b9e28 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java @@ -0,0 +1,41 @@ +/* + * 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.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when an invocation failed because of an I/O problem on the + * MBeanServerConnection. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see MBeanClientInterceptor + */ +public class MBeanConnectFailureException extends JmxException { + + /** + * Create a new MBeanConnectFailureException + * with the specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanConnectFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.java new file mode 100644 index 00000000000..2dee72b91df --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.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.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown if an exception is encountered when trying to retrieve + * MBean metadata. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + * @see MBeanProxyFactoryBean + */ +public class MBeanInfoRetrievalException extends JmxException { + + /** + * Create a new MBeanInfoRetrievalException with the + * specified error message. + * @param msg the detail message + */ + public MBeanInfoRetrievalException(String msg) { + super(msg); + } + + /** + * Create a new MBeanInfoRetrievalException with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanInfoRetrievalException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java new file mode 100644 index 00000000000..ec659a31be0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java @@ -0,0 +1,110 @@ +/* + * 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.jmx.access; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.util.ClassUtils; + +/** + * Creates a proxy to a managed resource running either locally or remotely. + * The "proxyInterface" property defines the interface that the generated + * proxy is supposed to implement. This interface should define methods and + * properties that correspond to operations and attributes in the management + * interface of the resource you wish to proxy. + * + *

There is no need for the managed resource to implement the proxy interface, + * although you may find it convenient to do. It is not required that every + * operation and attribute in the management interface is matched by a + * corresponding property or method in the proxy interface. + * + *

Attempting to invoke or access any method or property on the proxy + * interface that does not correspond to the management interface will lead + * to an InvalidInvocationException. + * + *

Requires JMX 1.2's MBeanServerConnection feature. + * As a consequence, this class will not work on JMX 1.0. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + * @see InvalidInvocationException + */ +public class MBeanProxyFactoryBean extends MBeanClientInterceptor + implements FactoryBean, BeanClassLoaderAware, InitializingBean { + + private Class proxyInterface; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Object mbeanProxy; + + + /** + * Set the interface that the generated proxy will implement. + *

This will usually be a management interface that matches the target MBean, + * exposing bean property setters and getters for MBean attributes and + * conventional Java methods for MBean operations. + * @see #setObjectName + */ + public void setProxyInterface(Class proxyInterface) { + this.proxyInterface = proxyInterface; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * Checks that the proxyInterface has been specified and then + * generates the proxy for the target MBean. + */ + public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoRetrievalException { + super.afterPropertiesSet(); + + if (this.proxyInterface == null) { + this.proxyInterface = getManagementInterface(); + if (this.proxyInterface == null) { + throw new IllegalArgumentException("Property 'proxyInterface' or 'managementInterface' is required"); + } + } + else { + if (getManagementInterface() == null) { + setManagementInterface(this.proxyInterface); + } + } + this.mbeanProxy = new ProxyFactory(this.proxyInterface, this).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.mbeanProxy; + } + + public Class getObjectType() { + return this.proxyInterface; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java b/org.springframework.context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java new file mode 100644 index 00000000000..ae7fbcf2e9f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java @@ -0,0 +1,179 @@ +/* + * 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.jmx.access; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXServiceURL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.JmxException; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jmx.support.NotificationListenerHolder; +import org.springframework.util.CollectionUtils; + +/** + * Registrar object that associates a specific {@link javax.management.NotificationListener} + * with one or more MBeans in an {@link javax.management.MBeanServer} + * (typically via a {@link javax.management.MBeanServerConnection}). + * + *

Requires JMX 1.2's MBeanServerConnection feature. + * As a consequence, this class will not work on JMX 1.0. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #setServer + * @see #setMappedObjectNames + * @see #setNotificationListener + */ +public class NotificationListenerRegistrar extends NotificationListenerHolder + implements InitializingBean, DisposableBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private MBeanServerConnection server; + + private JMXServiceURL serviceUrl; + + private Map environment; + + private String agentId; + + private final ConnectorDelegate connector = new ConnectorDelegate(); + + private ObjectName[] actualObjectNames; + + + /** + * Set the MBeanServerConnection used to connect to the + * MBean which all invocations are routed to. + */ + public void setServer(MBeanServerConnection server) { + this.server = server; + } + + /** + * Specify the environment for the JMX connector. + * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map) + */ + public void setEnvironment(Map environment) { + this.environment = environment; + } + + /** + * Allow Map access to the environment to be set for the connector, + * with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via + * "environment[myKey]". This is particularly useful for + * adding or overriding entries in child bean definitions. + */ + public Map getEnvironment() { + return this.environment; + } + + /** + * Set the service URL of the remote MBeanServer. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Set the agent id of the MBeanServer to locate. + *

Default is none. If specified, this will result in an + * attempt being made to locate the attendant MBeanServer, unless + * the {@link #setServiceUrl "serviceUrl"} property has been set. + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + + public void afterPropertiesSet() { + if (getNotificationListener() == null) { + throw new IllegalArgumentException("Property 'notificationListener' is required"); + } + if (CollectionUtils.isEmpty(this.mappedObjectNames)) { + throw new IllegalArgumentException("Property 'mappedObjectName' is required"); + } + prepare(); + } + + /** + * Registers the specified NotificationListener. + *

Ensures that an MBeanServerConnection is configured and attempts + * to detect a local connection if one is not supplied. + */ + public void prepare() { + if (this.server == null) { + this.server = this.connector.connect(this.serviceUrl, this.environment, this.agentId); + } + try { + this.actualObjectNames = getResolvedObjectNames(); + if (logger.isDebugEnabled()) { + logger.debug("Registering NotificationListener for MBeans " + Arrays.asList(this.actualObjectNames)); + } + for (int i = 0; i < this.actualObjectNames.length; i++) { + this.server.addNotificationListener(this.actualObjectNames[i], + getNotificationListener(), getNotificationFilter(), getHandback()); + } + } + catch (IOException ex) { + throw new MBeanServerNotFoundException( + "Could not connect to remote MBeanServer at URL [" + this.serviceUrl + "]", ex); + } + catch (Exception ex) { + throw new JmxException("Unable to register NotificationListener", ex); + } + } + + /** + * Unregisters the specified NotificationListener. + */ + public void destroy() { + try { + if (this.actualObjectNames != null) { + for (int i = 0; i < this.actualObjectNames.length; i++) { + try { + this.server.removeNotificationListener(this.actualObjectNames[i], + getNotificationListener(), getNotificationFilter(), getHandback()); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to unregister NotificationListener", ex); + } + } + } + } + } + finally { + this.connector.close(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/access/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/access/package.html new file mode 100644 index 00000000000..2cf5aabe01e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/access/package.html @@ -0,0 +1,7 @@ + + + +Provides support for accessing remote MBean resources. Requires JMX 1.2 or above. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportException.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportException.java new file mode 100644 index 00000000000..a6e2ec1f4a1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportException.java @@ -0,0 +1,49 @@ +/* + * 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.jmx.export; + +import org.springframework.jmx.JmxException; + +/** + * Exception thrown in case of failure when exporting an MBean. + * + * @author Rob Harrop + * @since 2.0 + * @see MBeanExportOperations + */ +public class MBeanExportException extends JmxException { + + /** + * Create a new MBeanExportException with the + * specified error message. + * @param msg the detail message + */ + public MBeanExportException(String msg) { + super(msg); + } + + /** + * Create a new MBeanExportException with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanExportException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.java new file mode 100644 index 00000000000..f99fa5f657b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.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.jmx.export; + +import javax.management.ObjectName; + +/** + * Interface that defines the set of MBean export operations that are intended to be + * accessed by application developers during application runtime. + * + *

This interface should be used to export application resources to JMX using Spring's + * management interface generation capabilties and, optionally, it's {@link ObjectName} + * generation capabilities. + * + * @author Rob Harrop + * @since 2.0 + * @see MBeanExporter + */ +public interface MBeanExportOperations { + + /** + * Register the supplied resource with JMX. If the resource is not a valid MBean already, + * Spring will generate a management interface for it. The exact interface generated will + * depend on the implementation and its configuration. This call also generates an + * {@link ObjectName} for the managed resource and returns this to the caller. + * @param managedResource the resource to expose via JMX + * @return the {@link ObjectName} under which the resource was exposed + * @throws MBeanExportException if Spring is unable to generate an {@link ObjectName} + * or register the MBean + */ + ObjectName registerManagedResource(Object managedResource) throws MBeanExportException; + + /** + * Register the supplied resource with JMX. If the resource is not a valid MBean already, + * Spring will generate a management interface for it. The exact interface generated will + * depend on the implementation and its configuration. + * @param managedResource the resource to expose via JMX + * @param objectName the {@link ObjectName} under which to expose the resource + * @throws MBeanExportException if Spring is unable to register the MBean + */ + void registerManagedResource(Object managedResource, ObjectName objectName) throws MBeanExportException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporter.java new file mode 100644 index 00000000000..18f6bdb1f58 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -0,0 +1,1091 @@ +/* + * 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.jmx.export; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.DynamicMBean; +import javax.management.JMException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.NotificationListener; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import javax.management.modelmbean.ModelMBean; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.RequiredModelMBean; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.LazyInitTargetSource; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Constants; +import org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler; +import org.springframework.jmx.export.assembler.MBeanInfoAssembler; +import org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler; +import org.springframework.jmx.export.naming.KeyNamingStrategy; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; +import org.springframework.jmx.export.naming.SelfNaming; +import org.springframework.jmx.export.notification.ModelMBeanNotificationPublisher; +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.jmx.support.MBeanRegistrationSupport; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * JMX exporter that allows for exposing any Spring-managed bean + * to a JMX MBeanServer, without the need to define any + * JMX-specific information in the bean classes. + * + *

If the bean implements one of the JMX management interfaces, + * then MBeanExporter can simply register the MBean with the server + * automatically, through its autodetection process. + * + *

If the bean does not implement one of the JMX management interfaces, + * then MBeanExporter will create the management information using the + * supplied {@link MBeanInfoAssembler} implementation. + * + *

A list of {@link MBeanExporterListener MBeanExporterListeners} + * can be registered via the + * {@link #setListeners(MBeanExporterListener[]) listeners} property, + * allowing application code to be notified of MBean registration and + * unregistration events. + * + *

This exporter is compatible with JMX 1.0 or higher for its basic + * functionality. However, for adapting AOP proxies where the target + * bean is a native MBean, JMX 1.2 is required. As of Spring 2.5, + * this class also autodetects and exports JDK 1.6 MXBeans. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @author Mark Fisher + * @since 1.2 + * @see #setBeans + * @see #setAutodetect + * @see #setAssembler + * @see #setListeners + * @see org.springframework.jmx.export.assembler.MBeanInfoAssembler + * @see MBeanExporterListener + */ +public class MBeanExporter extends MBeanRegistrationSupport + implements MBeanExportOperations, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { + + /** + * Autodetection mode indicating that no autodetection should be used. + */ + public static final int AUTODETECT_NONE = 0; + + /** + * Autodetection mode indicating that only valid MBeans should be autodetected. + */ + public static final int AUTODETECT_MBEAN = 1; + + /** + * Autodetection mode indicating that only the {@link MBeanInfoAssembler} should be able + * to autodetect beans. + */ + public static final int AUTODETECT_ASSEMBLER = 2; + + /** + * Autodetection mode indicating that all autodetection mechanisms should be used. + */ + public static final int AUTODETECT_ALL = AUTODETECT_MBEAN | AUTODETECT_ASSEMBLER; + + + /** + * Wildcard used to map a {@link javax.management.NotificationListener} + * to all MBeans registered by the MBeanExporter. + */ + private static final String WILDCARD = "*"; + + /** Constant for the JMX mr_type "ObjectReference" */ + private static final String MR_TYPE_OBJECT_REFERENCE = "ObjectReference"; + + /** Prefix for the autodetect constants defined in this class */ + private static final String CONSTANT_PREFIX_AUTODETECT = "AUTODETECT_"; + + + /** Constants instance for this class */ + private static final Constants constants = new Constants(MBeanExporter.class); + + /** The beans to be exposed as JMX managed resources, with JMX names as keys */ + private Map beans; + + /** The autodetect mode to use for this MBeanExporter */ + private Integer autodetectMode; + + /** Whether to eagerly init candidate beans when autodetecting MBeans */ + private boolean allowEagerInit = false; + + /** Indicates whether Spring should modify generated ObjectNames */ + private boolean ensureUniqueRuntimeObjectNames = true; + + /** Indicates whether Spring should expose the managed resource ClassLoader in the MBean */ + private boolean exposeManagedResourceClassLoader = true; + + /** A set of bean names that should be excluded from autodetection */ + private Set excludedBeans; + + /** The MBeanExporterListeners registered with this exporter. */ + private MBeanExporterListener[] listeners; + + /** The NotificationListeners to register for the MBeans registered by this exporter */ + private NotificationListenerBean[] notificationListeners; + + /** Map of actually registered NotificationListeners */ + private final Map registeredNotificationListeners = new LinkedHashMap(); + + /** Stores the MBeanInfoAssembler to use for this exporter */ + private MBeanInfoAssembler assembler = new SimpleReflectiveMBeanInfoAssembler(); + + /** The strategy to use for creating ObjectNames for an object */ + private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy(); + + /** Stores the ClassLoader to use for generating lazy-init proxies */ + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** Stores the BeanFactory for use in autodetection process */ + private ListableBeanFactory beanFactory; + + + /** + * Supply a Map of beans to be registered with the JMX + * MBeanServer. + *

The String keys are the basis for the creation of JMX object names. + * By default, a JMX ObjectName will be created straight + * from the given key. This can be customized through specifying a + * custom NamingStrategy. + *

Both bean instances and bean names are allowed as values. + * Bean instances are typically linked in through bean references. + * Bean names will be resolved as beans in the current factory, respecting + * lazy-init markers (that is, not triggering initialization of such beans). + * @param beans Map with JMX names as keys and bean instances or bean names + * as values + * @see #setNamingStrategy + * @see org.springframework.jmx.export.naming.KeyNamingStrategy + * @see javax.management.ObjectName#ObjectName(String) + */ + public void setBeans(Map beans) { + this.beans = beans; + } + + /** + * Set whether to autodetect MBeans in the bean factory that this exporter + * runs in. Will also ask an AutodetectCapableMBeanInfoAssembler + * if available. + *

This feature is turned off by default. Explicitly specify + * true here to enable autodetection. + * @see #setAssembler + * @see AutodetectCapableMBeanInfoAssembler + * @see #isMBean + */ + public void setAutodetect(boolean autodetect) { + this.autodetectMode = new Integer(autodetect ? AUTODETECT_ALL : AUTODETECT_NONE); + } + + /** + * Set the autodetection mode to use. + * @exception IllegalArgumentException if the supplied value is not + * one of the AUTODETECT_ constants + * @see #setAutodetectModeName(String) + * @see #AUTODETECT_ALL + * @see #AUTODETECT_ASSEMBLER + * @see #AUTODETECT_MBEAN + * @see #AUTODETECT_NONE + */ + public void setAutodetectMode(int autodetectMode) { + if (!constants.getValues(CONSTANT_PREFIX_AUTODETECT).contains(new Integer(autodetectMode))) { + throw new IllegalArgumentException("Only values of autodetect constants allowed"); + } + this.autodetectMode = new Integer(autodetectMode); + } + + /** + * Set the autodetection mode to use by name. + * @exception IllegalArgumentException if the supplied value is not resolvable + * to one of the AUTODETECT_ constants or is null + * @see #setAutodetectMode(int) + * @see #AUTODETECT_ALL + * @see #AUTODETECT_ASSEMBLER + * @see #AUTODETECT_MBEAN + * @see #AUTODETECT_NONE + */ + public void setAutodetectModeName(String constantName) { + if (constantName == null || !constantName.startsWith(CONSTANT_PREFIX_AUTODETECT)) { + throw new IllegalArgumentException("Only autodetect constants allowed"); + } + this.autodetectMode = (Integer) constants.asNumber(constantName); + } + + /** + * Specify whether to allow eager initialization of candidate beans + * when autodetecting MBeans in the Spring application context. + *

Default is "false", respecting lazy-init flags on bean definitions. + * Switch this to "true" in order to search lazy-init beans as well, + * including FactoryBean-produced objects that haven't been initialized yet. + */ + public void setAllowEagerInit(boolean allowEagerInit) { + this.allowEagerInit = allowEagerInit; + } + + /** + * Set the implementation of the MBeanInfoAssembler interface to use + * for this exporter. Default is a SimpleReflectiveMBeanInfoAssembler. + *

The passed-in assembler can optionally implement the + * AutodetectCapableMBeanInfoAssembler interface, which enables it + * to participate in the exporter's MBean autodetection process. + * @see org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler + * @see org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see #setAutodetect + */ + public void setAssembler(MBeanInfoAssembler assembler) { + this.assembler = assembler; + } + + /** + * Set the implementation of the ObjectNamingStrategy interface + * to use for this exporter. Default is a KeyNamingStrategy. + * @see org.springframework.jmx.export.naming.KeyNamingStrategy + * @see org.springframework.jmx.export.naming.MetadataNamingStrategy + */ + public void setNamingStrategy(ObjectNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + /** + * Set the MBeanExporterListeners that should be notified + * of MBean registration and unregistration events. + * @see MBeanExporterListener + */ + public void setListeners(MBeanExporterListener[] listeners) { + this.listeners = listeners; + } + + /** + * Set the list of names for beans that should be excluded from autodetection. + */ + public void setExcludedBeans(String[] excludedBeans) { + this.excludedBeans = (excludedBeans != null ? new HashSet(Arrays.asList(excludedBeans)) : null); + } + + /** + * Indicates whether Spring should ensure that {@link ObjectName ObjectNames} + * generated by the configured {@link ObjectNamingStrategy} for + * runtime-registered MBeans should be modified to ensure uniqueness + * for every instance of a managed Class. + *

The default value is true. + * @see JmxUtils#appendIdentityToObjectName(javax.management.ObjectName, Object) + */ + public void setEnsureUniqueRuntimeObjectNames(boolean ensureUniqueRuntimeObjectNames) { + this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames; + } + + /** + * Indicates whether or not the managed resource should be exposed on the + * {@link Thread#getContextClassLoader() thread context ClassLoader} before + * allowing any invocations on the MBean to occur. + *

The default value is true, exposing a {@link SpringModelMBean} + * which performs thread context ClassLoader management. Switch this flag off to + * expose a standard JMX {@link javax.management.modelmbean.RequiredModelMBean}. + */ + public void setExposeManagedResourceClassLoader(boolean exposeManagedResourceClassLoader) { + this.exposeManagedResourceClassLoader = exposeManagedResourceClassLoader; + } + + /** + * Set the {@link NotificationListenerBean NotificationListenerBeans} + * containing the + * {@link javax.management.NotificationListener NotificationListeners} + * that will be registered with the {@link MBeanServer}. + * @see #setNotificationListenerMappings(java.util.Map) + * @see NotificationListenerBean + */ + public void setNotificationListeners(NotificationListenerBean[] notificationListeners) { + this.notificationListeners = notificationListeners; + } + + /** + * Set the {@link NotificationListener NotificationListeners} to register + * with the {@link javax.management.MBeanServer}. + *

The key of each entry in the Map is a {@link String} + * representation of the {@link javax.management.ObjectName} or the bean + * name of the MBean the listener should be registered for. Specifying an + * asterisk (*) for a key will cause the listener to be + * associated with all MBeans registered by this class at startup time. + *

The value of each entry is the + * {@link javax.management.NotificationListener} to register. For more + * advanced options such as registering + * {@link javax.management.NotificationFilter NotificationFilters} and + * handback objects see {@link #setNotificationListeners(NotificationListenerBean[])}. + * @throws IllegalArgumentException if the supplied listeners {@link Map} is null. + */ + public void setNotificationListenerMappings(Map listeners) { + Assert.notNull(listeners, "'listeners' must not be null"); + List notificationListeners = new ArrayList(listeners.size()); + + for (Iterator iterator = listeners.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = (Map.Entry) iterator.next(); + + // Get the listener from the map value. + Object value = entry.getValue(); + if (!(value instanceof NotificationListener)) { + throw new IllegalArgumentException( + "Map entry value [" + value + "] is not a NotificationListener"); + } + NotificationListenerBean bean = new NotificationListenerBean((NotificationListener) value); + + // Get the ObjectName from the map key. + Object key = entry.getKey(); + if (key != null && !WILDCARD.equals(key)) { + // This listener is mapped to a specific ObjectName. + bean.setMappedObjectName(entry.getKey()); + } + + notificationListeners.add(bean); + } + + this.notificationListeners = (NotificationListenerBean[]) + notificationListeners.toArray(new NotificationListenerBean[notificationListeners.size()]); + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * This callback is only required for resolution of bean names in the + * {@link #setBeans(java.util.Map) "beans"} {@link Map} and for + * autodetection of MBeans (in the latter case, a + * ListableBeanFactory is required). + * @see #setBeans + * @see #setAutodetect + * @throws IllegalArgumentException if the supplied beanFactory is not a {@link ListableBeanFactory}. + */ + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ListableBeanFactory) { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + else { + logger.info("MBeanExporter not running in a ListableBeanFactory: Autodetection of MBeans not available."); + } + } + + + //--------------------------------------------------------------------- + // Lifecycle in bean factory: automatically register/unregister beans + //--------------------------------------------------------------------- + + /** + * Start bean registration automatically when deployed in an + * ApplicationContext. + * @see #registerBeans() + */ + public void afterPropertiesSet() { + // If no server was provided then try to find one. This is useful in an environment + // such as JDK 1.5, Tomcat or JBoss where there is already an MBeanServer loaded. + if (this.server == null) { + this.server = JmxUtils.locateMBeanServer(); + } + try { + logger.info("Registering beans for JMX exposure on startup"); + registerBeans(); + registerNotificationListeners(); + } + catch (RuntimeException ex) { + // Unregister beans already registered by this exporter. + unregisterNotificationListeners(); + unregisterBeans(); + throw ex; + } + } + + /** + * Unregisters all beans that this exported has exposed via JMX + * when the enclosing ApplicationContext is destroyed. + */ + public void destroy() { + logger.info("Unregistering JMX-exposed beans on shutdown"); + unregisterNotificationListeners(); + unregisterBeans(); + } + + + //--------------------------------------------------------------------- + // Implementation of MBeanExportOperations interface + //--------------------------------------------------------------------- + + public ObjectName registerManagedResource(Object managedResource) throws MBeanExportException { + Assert.notNull(managedResource, "Managed resource must not be null"); + ObjectName objectName = null; + try { + objectName = getObjectName(managedResource, null); + if (this.ensureUniqueRuntimeObjectNames) { + objectName = JmxUtils.appendIdentityToObjectName(objectName, managedResource); + } + } + catch (Exception ex) { + throw new MBeanExportException("Unable to generate ObjectName for MBean [" + managedResource + "]", ex); + } + registerManagedResource(managedResource, objectName); + return objectName; + } + + public void registerManagedResource(Object managedResource, ObjectName objectName) throws MBeanExportException { + Assert.notNull(managedResource, "Managed resource must not be null"); + Assert.notNull(objectName, "ObjectName must not be null"); + try { + if (isMBean(managedResource.getClass())) { + doRegister(managedResource, objectName); + } + else { + ModelMBean mbean = createAndConfigureMBean(managedResource, managedResource.getClass().getName()); + doRegister(mbean, objectName); + injectNotificationPublisherIfNecessary(managedResource, mbean, objectName); + } + } + catch (JMException ex) { + throw new UnableToRegisterMBeanException( + "Unable to register MBean [" + managedResource + "] with object name [" + objectName + "]", ex); + } + } + + + //--------------------------------------------------------------------- + // Exporter implementation + //--------------------------------------------------------------------- + + /** + * Registers the defined beans with the {@link MBeanServer}. + *

Each bean is exposed to the MBeanServer via a + * ModelMBean. The actual implemetation of the + * ModelMBean interface used depends on the implementation of + * the ModelMBeanProvider interface that is configured. By + * default the RequiredModelMBean class that is supplied with + * all JMX implementations is used. + *

The management interface produced for each bean is dependent on the + * MBeanInfoAssembler implementation being used. The + * ObjectName given to each bean is dependent on the + * implementation of the ObjectNamingStrategy interface being used. + */ + protected void registerBeans() { + // The beans property may be null, for example if we are relying solely on autodetection. + if (this.beans == null) { + this.beans = new HashMap(); + // Use AUTODETECT_ALL as default in no beans specified explicitly. + if (this.autodetectMode == null) { + this.autodetectMode = new Integer(AUTODETECT_ALL); + } + } + + // Perform autodetection, if desired. + int mode = (this.autodetectMode != null ? this.autodetectMode.intValue() : AUTODETECT_NONE); + if (mode != AUTODETECT_NONE) { + if (this.beanFactory == null) { + throw new MBeanExportException("Cannot autodetect MBeans if not running in a BeanFactory"); + } + if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) { + // Autodetect any beans that are already MBeans. + this.logger.debug("Autodetecting user-defined JMX MBeans"); + autodetectMBeans(); + } + // Allow the assembler a chance to vote for bean inclusion. + if ((mode == AUTODETECT_ASSEMBLER || mode == AUTODETECT_ALL) && + this.assembler instanceof AutodetectCapableMBeanInfoAssembler) { + autodetectBeans((AutodetectCapableMBeanInfoAssembler) this.assembler); + } + } + + if (!this.beans.isEmpty()) { + for (Iterator it = this.beans.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + Assert.notNull(entry.getKey(), "Beans key must not be null"); + String beanKey = entry.getKey().toString(); + Object value = entry.getValue(); + registerBeanNameOrInstance(value, beanKey); + } + } + } + + /** + * Return whether the specified bean definition should be considered as lazy-init. + * @param beanFactory the bean factory that is supposed to contain the bean definition + * @param beanName the name of the bean to check + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#getBeanDefinition + * @see org.springframework.beans.factory.config.BeanDefinition#isLazyInit + */ + protected boolean isBeanDefinitionLazyInit(ListableBeanFactory beanFactory, String beanName) { + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + return false; + } + try { + BeanDefinition bd = ((ConfigurableListableBeanFactory) beanFactory).getBeanDefinition(beanName); + return bd.isLazyInit(); + } + catch (NoSuchBeanDefinitionException ex) { + // Probably a directly registered singleton. + return false; + } + } + + /** + * Registers an individual bean with the {@link #setServer MBeanServer}. + *

This method is responsible for deciding how a bean + * should be exposed to the MBeanServer. Specifically, if the + * supplied mapValue is the name of a bean that is configured + * for lazy initialization, then a proxy to the resource is registered with + * the MBeanServer so that the the lazy load behavior is + * honored. If the bean is already an MBean then it will be registered + * directly with the MBeanServer without any intervention. For + * all other beans or bean names, the resource itself is registered with + * the MBeanServer directly. + * @param mapValue the value configured for this bean in the beans map; + * may be either the String name of a bean, or the bean itself + * @param beanKey the key associated with this bean in the beans map + * @return the ObjectName under which the resource was registered + * @throws MBeanExportException if the export failed + * @see #setBeans + * @see #registerBeanInstance + * @see #registerLazyInit + */ + protected ObjectName registerBeanNameOrInstance(Object mapValue, String beanKey) throws MBeanExportException { + try { + if (mapValue instanceof String) { + // Bean name pointing to a potentially lazy-init bean in the factory. + if (this.beanFactory == null) { + throw new MBeanExportException("Cannot resolve bean names if not running in a BeanFactory"); + } + String beanName = (String) mapValue; + if (isBeanDefinitionLazyInit(this.beanFactory, beanName)) { + ObjectName objectName = registerLazyInit(beanName, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + else { + Object bean = this.beanFactory.getBean(beanName); + ObjectName objectName = registerBeanInstance(bean, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + } + else { + // Plain bean instance -> register it directly. + if (this.beanFactory != null) { + Map beansOfSameType = this.beanFactory.getBeansOfType(mapValue.getClass(), false, false); + for (Iterator iterator = beansOfSameType.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = (Map.Entry) iterator.next(); + if (entry.getValue() == mapValue) { + String beanName = (String) entry.getKey(); + ObjectName objectName = registerBeanInstance(mapValue, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + } + } + return registerBeanInstance(mapValue, beanKey); + } + } + catch (Exception ex) { + throw new UnableToRegisterMBeanException( + "Unable to register MBean [" + mapValue + "] with key '" + beanKey + "'", ex); + } + } + + /** + * Replaces any bean names used as keys in the NotificationListener + * mappings with their corresponding ObjectName values. + * @param beanName the name of the bean to be registered + * @param objectName the ObjectName under which the bean will be registered + * with the MBeanServer + */ + private void replaceNotificationListenerBeanNameKeysIfNecessary(String beanName, ObjectName objectName) { + if (this.notificationListeners != null) { + for (int i = 0; i < this.notificationListeners.length; i++) { + this.notificationListeners[i].replaceObjectName(beanName, objectName); + } + } + } + + /** + * Registers an existing MBean or an MBean adapter for a plain bean + * with the MBeanServer. + * @param bean the bean to register, either an MBean or a plain bean + * @param beanKey the key associated with this bean in the beans map + * @return the ObjectName under which the bean was registered + * with the MBeanServer + */ + private ObjectName registerBeanInstance(Object bean, String beanKey) throws JMException { + ObjectName objectName = getObjectName(bean, beanKey); + Object mbeanToExpose = null; + if (isMBean(bean.getClass())) { + mbeanToExpose = bean; + } + else { + DynamicMBean adaptedBean = adaptMBeanIfPossible(bean); + if (adaptedBean != null) { + mbeanToExpose = adaptedBean; + } + } + if (mbeanToExpose != null) { + if (logger.isInfoEnabled()) { + logger.info("Located MBean '" + beanKey + "': registering with JMX server as MBean [" + + objectName + "]"); + } + doRegister(mbeanToExpose, objectName); + } + else { + if (logger.isInfoEnabled()) { + logger.info("Located managed bean '" + beanKey + "': registering with JMX server as MBean [" + + objectName + "]"); + } + ModelMBean mbean = createAndConfigureMBean(bean, beanKey); + doRegister(mbean, objectName); + injectNotificationPublisherIfNecessary(bean, mbean, objectName); + } + return objectName; + } + + /** + * Registers beans that are configured for lazy initialization with the + * MBeanServer indirectly through a proxy. + * @param beanName the name of the bean in the BeanFactory + * @param beanKey the key associated with this bean in the beans map + * @return the ObjectName under which the bean was registered + * with the MBeanServer + */ + private ObjectName registerLazyInit(String beanName, String beanKey) throws JMException { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setProxyTargetClass(true); + proxyFactory.setFrozen(true); + + if (isMBean(this.beanFactory.getType(beanName))) { + // A straight MBean... Let's create a simple lazy-init CGLIB proxy for it. + LazyInitTargetSource targetSource = new LazyInitTargetSource(); + targetSource.setTargetBeanName(beanName); + targetSource.setBeanFactory(this.beanFactory); + proxyFactory.setTargetSource(targetSource); + + Object proxy = proxyFactory.getProxy(this.beanClassLoader); + ObjectName objectName = getObjectName(proxy, beanKey); + if (logger.isDebugEnabled()) { + logger.debug("Located MBean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + + objectName + "]"); + } + doRegister(proxy, objectName); + return objectName; + } + + else { + // A simple bean... Let's create a lazy-init ModelMBean proxy with notification support. + NotificationPublisherAwareLazyTargetSource targetSource = new NotificationPublisherAwareLazyTargetSource(); + targetSource.setTargetBeanName(beanName); + targetSource.setBeanFactory(this.beanFactory); + proxyFactory.setTargetSource(targetSource); + + Object proxy = proxyFactory.getProxy(this.beanClassLoader); + ObjectName objectName = getObjectName(proxy, beanKey); + if (logger.isDebugEnabled()) { + logger.debug("Located simple bean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + + objectName + "]"); + } + ModelMBean mbean = createAndConfigureMBean(proxy, beanKey); + targetSource.setModelMBean(mbean); + targetSource.setObjectName(objectName); + doRegister(mbean, objectName); + return objectName; + } + } + + /** + * Retrieve the ObjectName for a bean. + *

If the bean implements the SelfNaming interface, then the + * ObjectName will be retrieved using SelfNaming.getObjectName(). + * Otherwise, the configured ObjectNamingStrategy is used. + * @param bean the name of the bean in the BeanFactory + * @param beanKey the key associated with the bean in the beans map + * @return the ObjectName for the supplied bean + * @throws javax.management.MalformedObjectNameException + * if the retrieved ObjectName is malformed + */ + protected ObjectName getObjectName(Object bean, String beanKey) throws MalformedObjectNameException { + if (bean instanceof SelfNaming) { + return ((SelfNaming) bean).getObjectName(); + } + else { + return this.namingStrategy.getObjectName(bean, beanKey); + } + } + + /** + * Determine whether the given bean class qualifies as an MBean as-is. + *

The default implementation delegates to {@link JmxUtils#isMBean}, + * which checks for {@link javax.management.DynamicMBean} classes as well + * as classes with corresponding "*MBean" interface (Standard MBeans) + * or corresponding "*MXBean" interface (Java 6 MXBeans). + * @param beanClass the bean class to analyze + * @return whether the class qualifies as an MBean + * @see org.springframework.jmx.support.JmxUtils#isMBean(Class) + */ + protected boolean isMBean(Class beanClass) { + return JmxUtils.isMBean(beanClass); + } + + /** + * Build an adapted MBean for the given bean instance, if possible. + *

The default implementation builds a JMX 1.2 StandardMBean + * for the target's MBean/MXBean interface in case of an AOP proxy, + * delegating the interface's management operations to the proxy. + * @param bean the original bean instance + * @return the adapted MBean, or null if not possible + */ + protected DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException { + Class targetClass = AopUtils.getTargetClass(bean); + if (targetClass != bean.getClass()) { + Class ifc = JmxUtils.getMXBeanInterface(targetClass); + if (ifc != null) { + if (!(ifc.isInstance(bean))) { + throw new NotCompliantMBeanException("Managed bean [" + bean + + "] has a target class with an MXBean interface but does not expose it in the proxy"); + } + return new StandardMBean(bean, ifc, true); + } + else { + ifc = JmxUtils.getMBeanInterface(targetClass); + if (ifc != null) { + if (!(ifc.isInstance(bean))) { + throw new NotCompliantMBeanException("Managed bean [" + bean + + "] has a target class with an MBean interface but does not expose it in the proxy"); + } + return new StandardMBean(bean, ifc); + } + } + } + return null; + } + + /** + * Creates an MBean that is configured with the appropriate management + * interface for the supplied managed resource. + * @param managedResource the resource that is to be exported as an MBean + * @param beanKey the key associated with the managed bean + * @see #createModelMBean() + * @see #getMBeanInfo(Object, String) + */ + protected ModelMBean createAndConfigureMBean(Object managedResource, String beanKey) + throws MBeanExportException { + try { + ModelMBean mbean = createModelMBean(); + mbean.setModelMBeanInfo(getMBeanInfo(managedResource, beanKey)); + mbean.setManagedResource(managedResource, MR_TYPE_OBJECT_REFERENCE); + return mbean; + } + catch (Exception ex) { + throw new MBeanExportException("Could not create ModelMBean for managed resource [" + + managedResource + "] with key '" + beanKey + "'", ex); + } + } + + /** + * Create an instance of a class that implements ModelMBean. + *

This method is called to obtain a ModelMBean instance to + * use when registering a bean. This method is called once per bean during the + * registration phase and must return a new instance of ModelMBean + * @return a new instance of a class that implements ModelMBean + * @throws javax.management.MBeanException if creation of the ModelMBean failed + */ + protected ModelMBean createModelMBean() throws MBeanException { + return (this.exposeManagedResourceClassLoader ? new SpringModelMBean() : new RequiredModelMBean()); + } + + /** + * Gets the ModelMBeanInfo for the bean with the supplied key + * and of the supplied type. + */ + private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException { + ModelMBeanInfo info = this.assembler.getMBeanInfo(managedBean, beanKey); + if (logger.isWarnEnabled() && ObjectUtils.isEmpty(info.getAttributes()) && + ObjectUtils.isEmpty(info.getOperations())) { + logger.warn("Bean with key '" + beanKey + + "' has been registered as an MBean but has no exposed attributes or operations"); + } + return info; + } + + + //--------------------------------------------------------------------- + // Autodetection process + //--------------------------------------------------------------------- + + /** + * Invoked when using an AutodetectCapableMBeanInfoAssembler. + * Gives the assembler the opportunity to add additional beans from the + * BeanFactory to the list of beans to be exposed via JMX. + *

This implementation prevents a bean from being added to the list + * automatically if it has already been added manually, and it prevents + * certain internal classes from being registered automatically. + */ + private void autodetectBeans(final AutodetectCapableMBeanInfoAssembler assembler) { + autodetect(new AutodetectCallback() { + public boolean include(Class beanClass, String beanName) { + return assembler.includeBean(beanClass, beanName); + } + }); + } + + /** + * Attempts to detect any beans defined in the ApplicationContext that are + * valid MBeans and registers them automatically with the MBeanServer. + */ + private void autodetectMBeans() { + autodetect(new AutodetectCallback() { + public boolean include(Class beanClass, String beanName) { + return isMBean(beanClass); + } + }); + } + + /** + * Performs the actual autodetection process, delegating to an + * AutodetectCallback instance to vote on the inclusion of a + * given bean. + * @param callback the AutodetectCallback to use when deciding + * whether to include a bean or not + */ + private void autodetect(AutodetectCallback callback) { + String[] beanNames = this.beanFactory.getBeanNamesForType(Object.class, true, this.allowEagerInit); + for (int i = 0; i < beanNames.length; i++) { + String beanName = beanNames[i]; + if (!isExcluded(beanName)) { + Class beanClass = this.beanFactory.getType(beanName); + if (beanClass != null && callback.include(beanClass, beanName)) { + boolean lazyInit = isBeanDefinitionLazyInit(this.beanFactory, beanName); + Object beanInstance = (!lazyInit ? this.beanFactory.getBean(beanName) : null); + if (!this.beans.containsValue(beanName) && + (beanInstance == null || !CollectionUtils.containsInstance(this.beans.values(), beanInstance))) { + // Not already registered for JMX exposure. + this.beans.put(beanName, (beanInstance != null ? beanInstance : beanName)); + if (logger.isInfoEnabled()) { + logger.info("Bean with name '" + beanName + "' has been autodetected for JMX exposure"); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Bean with name '" + beanName + "' is already registered for JMX exposure"); + } + } + } + } + } + } + + /** + * Indicates whether or not a particular bean name is present in the excluded beans list. + */ + private boolean isExcluded(String beanName) { + return (this.excludedBeans != null && this.excludedBeans.contains(beanName)); + } + + + //--------------------------------------------------------------------- + // Notification and listener management + //--------------------------------------------------------------------- + + /** + * If the supplied managed resource implements the {@link NotificationPublisherAware} an instance of + * {@link org.springframework.jmx.export.notification.NotificationPublisher} is injected. + */ + private void injectNotificationPublisherIfNecessary( + Object managedResource, ModelMBean modelMBean, ObjectName objectName) { + + if (managedResource instanceof NotificationPublisherAware) { + ((NotificationPublisherAware) managedResource).setNotificationPublisher( + new ModelMBeanNotificationPublisher(modelMBean, objectName, managedResource)); + } + } + + /** + * Register the configured {@link NotificationListener NotificationListeners} + * with the {@link MBeanServer}. + */ + private void registerNotificationListeners() throws MBeanExportException { + if (this.notificationListeners != null) { + for (int i = 0; i < this.notificationListeners.length; i++) { + NotificationListenerBean bean = this.notificationListeners[i]; + try { + ObjectName[] mappedObjectNames = bean.getResolvedObjectNames(); + if (mappedObjectNames == null) { + // Mapped to all MBeans registered by the MBeanExporter. + mappedObjectNames = getRegisteredObjectNames(); + } + if (this.registeredNotificationListeners.put(bean, mappedObjectNames) == null) { + for (int j = 0; j < mappedObjectNames.length; j++) { + this.server.addNotificationListener(mappedObjectNames[j], + bean.getNotificationListener(), bean.getNotificationFilter(), bean.getHandback()); + } + } + } + catch (Exception ex) { + throw new MBeanExportException("Unable to register NotificationListener", ex); + } + } + } + } + + /** + * Unregister the configured {@link NotificationListener NotificationListeners} + * from the {@link MBeanServer}. + */ + private void unregisterNotificationListeners() { + for (Iterator it = this.registeredNotificationListeners.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + NotificationListenerBean bean = (NotificationListenerBean) entry.getKey(); + ObjectName[] mappedObjectNames = (ObjectName[]) entry.getValue(); + for (int j = 0; j < mappedObjectNames.length; j++) { + try { + this.server.removeNotificationListener(mappedObjectNames[j], + bean.getNotificationListener(), bean.getNotificationFilter(), bean.getHandback()); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to unregister NotificationListener", ex); + } + } + } + } + this.registeredNotificationListeners.clear(); + } + + /** + * Called when an MBean is registered. Notifies all registered + * {@link MBeanExporterListener MBeanExporterListeners} of the registration event. + *

Please note that if an {@link MBeanExporterListener} throws a (runtime) + * exception when notified, this will essentially interrupt the notification process + * and any remaining listeners that have yet to be notified will not (obviously) + * receive the {@link MBeanExporterListener#mbeanRegistered(javax.management.ObjectName)} + * callback. + * @param objectName the ObjectName of the registered MBean + */ + protected void onRegister(ObjectName objectName) { + notifyListenersOfRegistration(objectName); + } + + /** + * Called when an MBean is unregistered. Notifies all registered + * {@link MBeanExporterListener MBeanExporterListeners} of the unregistration event. + *

Please note that if an {@link MBeanExporterListener} throws a (runtime) + * exception when notified, this will essentially interrupt the notification process + * and any remaining listeners that have yet to be notified will not (obviously) + * receive the {@link MBeanExporterListener#mbeanUnregistered(javax.management.ObjectName)} + * callback. + * @param objectName the ObjectName of the unregistered MBean + */ + protected void onUnregister(ObjectName objectName) { + notifyListenersOfUnregistration(objectName); + } + + + /** + * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the + * registration of the MBean identified by the supplied {@link ObjectName}. + */ + private void notifyListenersOfRegistration(ObjectName objectName) { + if (this.listeners != null) { + for (int i = 0; i < this.listeners.length; i++) { + this.listeners[i].mbeanRegistered(objectName); + } + } + } + + /** + * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the + * unregistration of the MBean identified by the supplied {@link ObjectName}. + */ + private void notifyListenersOfUnregistration(ObjectName objectName) { + if (this.listeners != null) { + for (int i = 0; i < this.listeners.length; i++) { + this.listeners[i].mbeanUnregistered(objectName); + } + } + } + + + //--------------------------------------------------------------------- + // Inner classes for internal use + //--------------------------------------------------------------------- + + /** + * Internal callback interface for the autodetection process. + */ + private static interface AutodetectCallback { + + /** + * Called during the autodetection process to decide whether + * or not a bean should be included. + * @param beanClass the class of the bean + * @param beanName the name of the bean + */ + boolean include(Class beanClass, String beanName); + } + + + /** + * Extension of {@link LazyInitTargetSource} that will inject a + * {@link org.springframework.jmx.export.notification.NotificationPublisher} + * into the lazy resource as it is created if required. + */ + private class NotificationPublisherAwareLazyTargetSource extends LazyInitTargetSource { + + private ModelMBean modelMBean; + + private ObjectName objectName; + + public void setModelMBean(ModelMBean modelMBean) { + this.modelMBean = modelMBean; + } + + public void setObjectName(ObjectName objectName) { + this.objectName = objectName; + } + + protected void postProcessTargetObject(Object targetObject) { + injectNotificationPublisherIfNecessary(targetObject, this.modelMBean, this.objectName); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java new file mode 100644 index 00000000000..d586531f29b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java @@ -0,0 +1,45 @@ +/* + * 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.jmx.export; + +import javax.management.ObjectName; + +/** + * A listener that allows application code to be notified when an MBean is + * registered and unregistered via an {@link MBeanExporter}. + * + * @author Rob Harrop + * @since 1.2.2 + * @see org.springframework.jmx.export.MBeanExporter#setListeners + */ +public interface MBeanExporterListener { + + /** + * Called by {@link MBeanExporter} after an MBean has been successfully + * registered with an {@link javax.management.MBeanServer}. + * @param objectName the ObjectName of the registered MBean + */ + void mbeanRegistered(ObjectName objectName); + + /** + * Called by {@link MBeanExporter} after an MBean has been successfully + * unregistered from an {@link javax.management.MBeanServer}. + * @param objectName the ObjectName of the unregistered MBean + */ + void mbeanUnregistered(ObjectName objectName); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java new file mode 100644 index 00000000000..0a482be4362 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java @@ -0,0 +1,75 @@ +/* + * 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.jmx.export; + +import javax.management.NotificationListener; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.support.NotificationListenerHolder; + +/** + * Helper class that aggregates a {@link javax.management.NotificationListener}, + * a {@link javax.management.NotificationFilter}, and an arbitrary handback + * object. + * + *

Also provides support for associating the encapsulated + * {@link javax.management.NotificationListener} with any number of + * MBeans from which it wishes to receive + * {@link javax.management.Notification Notifications} via the + * {@link #setMappedObjectNames mappedObjectNames} property. + * + *

Note: This class supports Spring bean names as + * {@link #setMappedObjectNames "mappedObjectNames"} as well, as alternative + * to specifying JMX object names. Note that only beans exported by the + * same {@link MBeanExporter} are supported for such bean names. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see MBeanExporter#setNotificationListeners + */ +public class NotificationListenerBean extends NotificationListenerHolder implements InitializingBean { + + /** + * Create a new instance of the {@link NotificationListenerBean} class. + */ + public NotificationListenerBean() { + } + + /** + * Create a new instance of the {@link NotificationListenerBean} class. + * @param notificationListener the encapsulated listener + */ + public NotificationListenerBean(NotificationListener notificationListener) { + setNotificationListener(notificationListener); + } + + + public void afterPropertiesSet() { + if (getNotificationListener() == null) { + throw new IllegalArgumentException("Property 'notificationListener' is required"); + } + } + + void replaceObjectName(Object originalName, Object newName) { + if (this.mappedObjectNames != null && this.mappedObjectNames.contains(originalName)) { + this.mappedObjectNames.remove(originalName); + this.mappedObjectNames.add(newName); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java new file mode 100644 index 00000000000..6190116dfb3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java @@ -0,0 +1,163 @@ +/* + * 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.jmx.export; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanException; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; +import javax.management.modelmbean.InvalidTargetObjectTypeException; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.RequiredModelMBean; + +/** + * Extension of the {@link RequiredModelMBean} class that ensures the + * {@link Thread#getContextClassLoader() thread context ClassLoader} is switched + * for the managed resource's {@link ClassLoader} before any invocations occur. + * + * @author Rob Harrop + * @since 2.0 + * @see RequiredModelMBean + */ +public class SpringModelMBean extends RequiredModelMBean { + + /** + * Stores the {@link ClassLoader} to use for invocations. Defaults + * to the current thread {@link ClassLoader}. + */ + private ClassLoader managedResourceClassLoader = Thread.currentThread().getContextClassLoader(); + + + /** + * Construct a new SpringModelMBean instance with an empty {@link ModelMBeanInfo}. + * @see javax.management.modelmbean.RequiredModelMBean#RequiredModelMBean() + */ + public SpringModelMBean() throws MBeanException, RuntimeOperationsException { + super(); + } + + /** + * Construct a new SpringModelMBean instance with the given {@link ModelMBeanInfo}. + * @see javax.management.modelmbean.RequiredModelMBean#RequiredModelMBean(ModelMBeanInfo) + */ + public SpringModelMBean(ModelMBeanInfo mbi) throws MBeanException, RuntimeOperationsException { + super(mbi); + } + + + /** + * Sets managed resource to expose and stores its {@link ClassLoader}. + */ + public void setManagedResource(Object managedResource, String managedResourceType) + throws MBeanException, InstanceNotFoundException, InvalidTargetObjectTypeException { + + this.managedResourceClassLoader = managedResource.getClass().getClassLoader(); + super.setManagedResource(managedResource, managedResourceType); + } + + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#invoke + */ + public Object invoke(String opName, Object[] opArgs, String[] sig) + throws MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.invoke(opName, opArgs, sig); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#getAttribute + */ + public Object getAttribute(String attrName) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.getAttribute(attrName); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#getAttributes + */ + public AttributeList getAttributes(String[] attrNames) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.getAttributes(attrNames); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#setAttribute + */ + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + super.setAttribute(attribute); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#setAttributes + */ + public AttributeList setAttributes(AttributeList attributes) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.setAttributes(attributes); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java new file mode 100644 index 00000000000..d738cf37d37 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java @@ -0,0 +1,47 @@ +/* + * 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.jmx.export; + +/** + * Exception thrown when we are unable to register an MBean, + * for example because of a naming conflict. + * + * @author Rob Harrop + * @since 2.0 + */ +public class UnableToRegisterMBeanException extends MBeanExportException { + + /** + * Create a new UnableToRegisterMBeanException with the + * specified error message. + * @param msg the detail message + */ + public UnableToRegisterMBeanException(String msg) { + super(msg); + } + + /** + * Create a new UnableToRegisterMBeanException with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root caus + */ + public UnableToRegisterMBeanException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java new file mode 100644 index 00000000000..cc9df5fd677 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -0,0 +1,136 @@ +/* + * 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.jmx.export.annotation; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.annotation.AnnotationBeanUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.jmx.export.metadata.InvalidMetadataException; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.export.metadata.ManagedAttribute; +import org.springframework.jmx.export.metadata.ManagedNotification; +import org.springframework.jmx.export.metadata.ManagedOperation; +import org.springframework.jmx.export.metadata.ManagedOperationParameter; +import org.springframework.jmx.export.metadata.ManagedResource; +import org.springframework.util.StringUtils; + +/** + * Implementation of the JmxAttributeSource interface that + * reads JDK 1.5+ annotations and exposes the corresponding attributes. + * + *

This is a direct alternative to AttributesJmxAttributeSource, + * which is able to read in source-level attributes via Commons Attributes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.export.annotation.ManagedResource + * @see org.springframework.jmx.export.annotation.ManagedAttribute + * @see org.springframework.jmx.export.annotation.ManagedOperation + * @see org.springframework.jmx.export.metadata.AttributesJmxAttributeSource + * @see org.springframework.metadata.commons.CommonsAttributes + */ +public class AnnotationJmxAttributeSource implements JmxAttributeSource { + + public ManagedResource getManagedResource(Class beanClass) throws InvalidMetadataException { + org.springframework.jmx.export.annotation.ManagedResource ann = + ((Class) beanClass).getAnnotation(org.springframework.jmx.export.annotation.ManagedResource.class); + if (ann == null) { + return null; + } + ManagedResource managedResource = new ManagedResource(); + AnnotationBeanUtils.copyPropertiesToBean(ann, managedResource); + if (!"".equals(ann.value()) && !StringUtils.hasLength(managedResource.getObjectName())) { + managedResource.setObjectName(ann.value()); + } + return managedResource; + } + + public ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException { + org.springframework.jmx.export.annotation.ManagedAttribute ann = + AnnotationUtils.getAnnotation(method, org.springframework.jmx.export.annotation.ManagedAttribute.class); + if (ann == null) { + return null; + } + ManagedAttribute managedAttribute = new ManagedAttribute(); + AnnotationBeanUtils.copyPropertiesToBean(ann, managedAttribute, "defaultValue"); + if (ann.defaultValue().length() > 0) { + managedAttribute.setDefaultValue(ann.defaultValue()); + } + return managedAttribute; + } + + public ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + throw new InvalidMetadataException( + "The ManagedOperation attribute is not valid for JavaBean properties. Use ManagedAttribute instead."); + } + + Annotation ann = AnnotationUtils.getAnnotation(method, org.springframework.jmx.export.annotation.ManagedOperation.class); + if (ann == null) { + return null; + } + + ManagedOperation op = new ManagedOperation(); + AnnotationBeanUtils.copyPropertiesToBean(ann, op); + return op; + } + + public ManagedOperationParameter[] getManagedOperationParameters(Method method) + throws InvalidMetadataException { + + ManagedOperationParameters params = AnnotationUtils.getAnnotation(method, ManagedOperationParameters.class); + ManagedOperationParameter[] result = null; + if (params == null) { + result = new ManagedOperationParameter[0]; + } + else { + Annotation[] paramData = params.value(); + result = new ManagedOperationParameter[paramData.length]; + for (int i = 0; i < paramData.length; i++) { + Annotation annotation = paramData[i]; + ManagedOperationParameter managedOperationParameter = new ManagedOperationParameter(); + AnnotationBeanUtils.copyPropertiesToBean(annotation, managedOperationParameter); + result[i] = managedOperationParameter; + } + } + return result; + } + + public ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException { + ManagedNotifications notificationsAnn = (ManagedNotifications) clazz.getAnnotation(ManagedNotifications.class); + if(notificationsAnn == null) { + return new ManagedNotification[0]; + } + Annotation[] notifications = notificationsAnn.value(); + ManagedNotification[] result = new ManagedNotification[notifications.length]; + for (int i = 0; i < notifications.length; i++) { + Annotation notification = notifications[i]; + + ManagedNotification managedNotification = new ManagedNotification(); + AnnotationBeanUtils.copyPropertiesToBean(notification, managedNotification); + result[i] = managedNotification; + } + return result; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java new file mode 100644 index 00000000000..a0aaf70c5dc --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java @@ -0,0 +1,66 @@ +/* + * 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.jmx.export.annotation; + +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler; +import org.springframework.jmx.export.naming.MetadataNamingStrategy; + +/** + * Convenient subclass of Spring's standard {@link MBeanExporter}, + * activating Java 5 annotation usage for JMX exposure of Spring beans: + * {@link ManagedResource}, {@link ManagedAttribute}, {@link ManagedOperation}, etc. + * + *

Sets a {@link MetadataNamingStrategy} and a {@link MetadataMBeanInfoAssembler} + * with an {@link AnnotationJmxAttributeSource}, and activates the + * {@link #AUTODETECT_ALL} mode by default. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class AnnotationMBeanExporter extends MBeanExporter { + + private final AnnotationJmxAttributeSource annotationSource = + new AnnotationJmxAttributeSource(); + + private final MetadataNamingStrategy metadataNamingStrategy = + new MetadataNamingStrategy(this.annotationSource); + + private final MetadataMBeanInfoAssembler metadataAssembler = + new MetadataMBeanInfoAssembler(this.annotationSource); + + + public AnnotationMBeanExporter() { + setNamingStrategy(this.metadataNamingStrategy); + setAssembler(this.metadataAssembler); + setAutodetectMode(AUTODETECT_ALL); + } + + + /** + * Specify the default domain to be used for generating ObjectNames + * when no source-level metadata has been specified. + *

The default is to use the domain specified in the bean name + * (if the bean name follows the JMX ObjectName syntax); else, + * the package name of the managed bean class. + * @see MetadataNamingStrategy#setDefaultDomain + */ + public void setDefaultDomain(String defaultDomain) { + this.metadataNamingStrategy.setDefaultDomain(defaultDomain); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java new file mode 100644 index 00000000000..544783d463d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java @@ -0,0 +1,49 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ method-level annotation that indicates to expose a given bean + * property as JMX attribute, corresponding to the ManagedAttribute attribute. + * Only valid when used on a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedAttribute + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedAttribute { + + String defaultValue() default ""; + + String description() default ""; + + int currencyTimeLimit() default -1; + + String persistPolicy() default ""; + + int persistPeriod() default -1; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java new file mode 100644 index 00000000000..02b4ed7bba2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java @@ -0,0 +1,46 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ method-level annotation that indicates a JMX notification + * emitted by a bean. + * + * @author Rob Harrop + * @since 2.0 + * @see org.springframework.jmx.export.metadata.ManagedNotification + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ManagedNotification { + + String name(); + + String description() default ""; + + String[] notificationTypes(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java new file mode 100644 index 00000000000..abffc3e08f4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java @@ -0,0 +1,41 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ method-level annotation that indicates JMX notifications emitted by + * a bean, containing multiple {@link ManagedNotification ManagedNotifications} + * + * @author Rob Harrop + * @since 2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ManagedNotifications { + + ManagedNotification[] value(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java new file mode 100644 index 00000000000..2d813b102c5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java @@ -0,0 +1,43 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ method-level annotation that indicates to expose a given method + * as JMX operation, corresponding to the ManagedOperation attribute. + * Only valid when used on a method that is not a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedOperation + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedOperation { + + String description() default ""; + + int currencyTimeLimit() default -1; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java new file mode 100644 index 00000000000..937bbf2e750 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java @@ -0,0 +1,35 @@ +/* + * 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.jmx.export.annotation; + +/** + * JDK 1.5+ method-level annotation used to provide metadata about operation + * parameters, corresponding to a ManagedOperationParameter attribute. + * Used as part of a ManagedOperationParameters annotation. + * + * @author Rob Harrop + * @since 1.2 + * @see ManagedOperationParameters#value + * @see org.springframework.jmx.export.metadata.ManagedOperationParameter + */ +public @interface ManagedOperationParameter { + + String name(); + + String description(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java new file mode 100644 index 00000000000..23fc2c77dc5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java @@ -0,0 +1,41 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ method-level annotation used to provide metadata about + * operation parameters, corresponding to an array of + * ManagedOperationParameter attributes. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedOperationParameter + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedOperationParameters { + + ManagedOperationParameter[] value() default {}; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java new file mode 100644 index 00000000000..4b7345c5214 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java @@ -0,0 +1,64 @@ +/* + * 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.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * JDK 1.5+ class-level annotation that indicates to register + * instances of a class with a JMX server, corresponding to + * the ManagedResource attribute. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedResource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedResource { + + /** + * The annotation value is equivalent to the objectName + * attribute, for simple default usage. + */ + String value() default ""; + + String objectName() default ""; + + String description() default ""; + + int currencyTimeLimit() default -1; + + boolean log() default false; + + String logFile() default ""; + + String persistPolicy() default ""; + + int persistPeriod() default -1; + + String persistName() default ""; + + String persistLocation() default ""; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/package.html new file mode 100644 index 00000000000..08aa70ca3fe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/annotation/package.html @@ -0,0 +1,9 @@ + + + +JDK 1.5+ annotations for MBean exposure. +Hooked into Spring's JMX export infrastructure +via a special JmxAttributeSource implementation. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java new file mode 100644 index 00000000000..01efdcb537b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java @@ -0,0 +1,107 @@ +/* + * 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.jmx.export.assembler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.jmx.export.metadata.JmxMetadataUtils; +import org.springframework.jmx.export.metadata.ManagedNotification; +import org.springframework.util.StringUtils; + +/** + * Base class for MBeanInfoAssemblers that support configurable + * JMX notification behavior. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractConfigurableMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler { + + private ModelMBeanNotificationInfo[] notificationInfos; + + private final Map notificationInfoMappings = new HashMap(); + + + public void setNotificationInfos(ManagedNotification[] notificationInfos) { + ModelMBeanNotificationInfo[] infos = new ModelMBeanNotificationInfo[notificationInfos.length]; + for (int i = 0; i < notificationInfos.length; i++) { + ManagedNotification notificationInfo = notificationInfos[i]; + infos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(notificationInfo); + } + this.notificationInfos = infos; + } + + public void setNotificationInfoMappings(Map notificationInfoMappings) { + Iterator entries = notificationInfoMappings.entrySet().iterator(); + while (entries.hasNext()) { + Map.Entry entry = (Map.Entry) entries.next(); + if (!(entry.getKey() instanceof String)) { + throw new IllegalArgumentException("Property [notificationInfoMappings] only accepts Strings for Map keys"); + } + this.notificationInfoMappings.put(entry.getKey(), extractNotificationMetadata(entry.getValue())); + } + } + + + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) { + ModelMBeanNotificationInfo[] result = null; + + if (StringUtils.hasText(beanKey)) { + result = (ModelMBeanNotificationInfo[]) this.notificationInfoMappings.get(beanKey); + } + + if (result == null) { + result = this.notificationInfos; + } + + return (result == null) ? new ModelMBeanNotificationInfo[0] : result; + } + + private ModelMBeanNotificationInfo[] extractNotificationMetadata(Object mapValue) { + if (mapValue instanceof ManagedNotification) { + ManagedNotification mn = (ManagedNotification) mapValue; + return new ModelMBeanNotificationInfo[] {JmxMetadataUtils.convertToModelMBeanNotificationInfo(mn)}; + } + else if (mapValue instanceof Collection) { + Collection col = (Collection) mapValue; + List result = new ArrayList(); + for (Iterator iterator = col.iterator(); iterator.hasNext();) { + Object colValue = iterator.next(); + if (!(colValue instanceof ManagedNotification)) { + throw new IllegalArgumentException( + "Property 'notificationInfoMappings' only accepts ManagedNotifications for Map values"); + } + ManagedNotification mn = (ManagedNotification) colValue; + result.add(JmxMetadataUtils.convertToModelMBeanNotificationInfo(mn)); + } + return (ModelMBeanNotificationInfo[]) result.toArray(new ModelMBeanNotificationInfo[result.size()]); + } + else { + throw new IllegalArgumentException( + "Property 'notificationInfoMappings' only accepts ManagedNotifications for Map values"); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.java new file mode 100644 index 00000000000..180eb55b04a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.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.jmx.export.assembler; + +import javax.management.Descriptor; +import javax.management.JMException; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanConstructorInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanInfoSupport; +import javax.management.modelmbean.ModelMBeanNotificationInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.springframework.aop.support.AopUtils; +import org.springframework.jmx.support.JmxUtils; + +/** + * Abstract implementation of the MBeanInfoAssembler interface + * that encapsulates the creation of a ModelMBeanInfo instance + * but delegates the creation of metadata to subclasses. + * + *

This class offers two flavors of Class extraction from a managed bean + * instance: {@link #getTargetClass}, extracting the target class behind + * any kind of AOP proxy, and {@link #getClassToExpose}, returning the + * class or interface that will be searched for annotations and exposed + * to the JMX runtime. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public abstract class AbstractMBeanInfoAssembler implements MBeanInfoAssembler { + + /** + * Create an instance of the ModelMBeanInfoSupport class supplied with all + * JMX implementations and populates the metadata through calls to the subclass. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @param beanKey the key associated with the managed bean + * @return the populated ModelMBeanInfo instance + * @throws JMException in case of errors + * @see #getDescription(Object, String) + * @see #getAttributeInfo(Object, String) + * @see #getConstructorInfo(Object, String) + * @see #getOperationInfo(Object, String) + * @see #getNotificationInfo(Object, String) + * @see #populateMBeanDescriptor(javax.management.Descriptor, Object, String) + */ + public ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException { + checkManagedBean(managedBean); + ModelMBeanInfo info = new ModelMBeanInfoSupport( + getClassName(managedBean, beanKey), getDescription(managedBean, beanKey), + getAttributeInfo(managedBean, beanKey), getConstructorInfo(managedBean, beanKey), + getOperationInfo(managedBean, beanKey), getNotificationInfo(managedBean, beanKey)); + Descriptor desc = info.getMBeanDescriptor(); + populateMBeanDescriptor(desc, managedBean, beanKey); + info.setMBeanDescriptor(desc); + return info; + } + + /** + * Check the given bean instance, throwing an IllegalArgumentException + * if it is not eligible for exposure with this assembler. + *

Default implementation is empty, accepting every bean instance. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @throws IllegalArgumentException the bean is not valid for exposure + */ + protected void checkManagedBean(Object managedBean) throws IllegalArgumentException { + } + + /** + * Return the actual bean class of the given bean instance. + * This is the class exposed to description-style JMX properties. + *

Default implementation returns the target class for an AOP proxy, + * and the plain bean class else. + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see org.springframework.aop.framework.AopProxyUtils#getTargetClass + */ + protected Class getTargetClass(Object managedBean) { + return AopUtils.getTargetClass(managedBean); + } + + /** + * Return the class or interface to expose for the given bean. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see JmxUtils#getClassToExpose(Object) + */ + protected Class getClassToExpose(Object managedBean) { + return JmxUtils.getClassToExpose(managedBean); + } + + /** + * Return the class or interface to expose for the given bean class. + * This is the class that will be searched for attributes and operations + * @param beanClass the bean class (might be an AOP proxy class) + * @return the bean class to expose + * @see JmxUtils#getClassToExpose(Class) + */ + protected Class getClassToExpose(Class beanClass) { + return JmxUtils.getClassToExpose(beanClass); + } + + /** + * Get the class name of the MBean resource. + *

Default implementation returns a simple description for the MBean + * based on the class name. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the MBean description + * @throws JMException in case of errors + */ + protected String getClassName(Object managedBean, String beanKey) throws JMException { + return getTargetClass(managedBean).getName(); + } + + /** + * Get the description of the MBean resource. + *

Default implementation returns a simple description for the MBean + * based on the class name. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @throws JMException in case of errors + */ + protected String getDescription(Object managedBean, String beanKey) throws JMException { + String targetClassName = getTargetClass(managedBean).getName(); + if (AopUtils.isAopProxy(managedBean)) { + return "Proxy for " + targetClassName; + } + return targetClassName; + } + + /** + * Called after the ModelMBeanInfo instance has been constructed but + * before it is passed to the MBeanExporter. + *

Subclasses can implement this method to add additional descriptors to the + * MBean metadata. Default implementation is empty. + * @param descriptor the Descriptor for the MBean resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @throws JMException in case of errors + */ + protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) + throws JMException { + } + + /** + * Get the constructor metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all constructors that should + * be exposed in the management interface for the managed resource. + *

Default implementation returns an empty array of ModelMBeanConstructorInfo. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the constructor metadata + * @throws JMException in case of errors + */ + protected ModelMBeanConstructorInfo[] getConstructorInfo(Object managedBean, String beanKey) + throws JMException { + return new ModelMBeanConstructorInfo[0]; + } + + /** + * Get the notification metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all notifications that should + * be exposed in the management interface for the managed resource. + *

Default implementation returns an empty array of ModelMBeanNotificationInfo. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the notification metadata + * @throws JMException in case of errors + */ + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) + throws JMException { + return new ModelMBeanNotificationInfo[0]; + } + + + /** + * Get the attribute metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all the attributes that should + * be exposed in the management interface for the managed resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the attribute metadata + * @throws JMException in case of errors + */ + protected abstract ModelMBeanAttributeInfo[] getAttributeInfo(Object managedBean, String beanKey) + throws JMException; + + /** + * Get the operation metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all operations that should + * be exposed in the management interface for the managed resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the operation metadata + * @throws JMException in case of errors + */ + protected abstract ModelMBeanOperationInfo[] getOperationInfo(Object managedBean, String beanKey) + throws JMException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java new file mode 100644 index 00000000000..08ea1586863 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java @@ -0,0 +1,554 @@ +/* + * 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.jmx.export.assembler; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.management.Descriptor; +import javax.management.JMException; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.core.JdkVersion; +import org.springframework.jmx.support.JmxUtils; + +/** + * Builds on the {@link AbstractMBeanInfoAssembler} superclass to + * add a basic algorithm for building metadata based on the + * reflective metadata of the MBean class. + * + *

The logic for creating MBean metadata from the reflective metadata + * is contained in this class, but this class makes no decisions as to + * which methods and properties are to be exposed. Instead it gives + * subclasses a chance to 'vote' on each property or method through + * the includeXXX methods. + * + *

Subclasses are also given the opportunity to populate attribute + * and operation metadata with additional descriptors once the metadata + * is assembled through the populateXXXDescriptor methods. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #includeOperation + * @see #includeReadAttribute + * @see #includeWriteAttribute + * @see #populateAttributeDescriptor + * @see #populateOperationDescriptor + */ +public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBeanInfoAssembler { + + /** + * Identifies a getter method in a JMX {@link Descriptor}. + */ + protected static final String FIELD_GET_METHOD = "getMethod"; + + /** + * Identifies a setter method in a JMX {@link Descriptor}. + */ + protected static final String FIELD_SET_METHOD = "setMethod"; + + /** + * Constant identifier for the role field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_ROLE = "role"; + + /** + * Constant identifier for the getter role field value in a JMX {@link Descriptor}. + */ + protected static final String ROLE_GETTER = "getter"; + + /** + * Constant identifier for the setter role field value in a JMX {@link Descriptor}. + */ + protected static final String ROLE_SETTER = "setter"; + + /** + * Identifies an operation (method) in a JMX {@link Descriptor}. + */ + protected static final String ROLE_OPERATION = "operation"; + + /** + * Constant identifier for the visibility field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_VISIBILITY = "visibility"; + + /** + * Lowest visibility, used for operations that correspond to + * accessors or mutators for attributes. + * @see #FIELD_VISIBILITY + */ + protected static final Integer ATTRIBUTE_OPERATION_VISIBILITY = new Integer(4); + + /** + * Constant identifier for the class field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_CLASS = "class"; + /** + * Constant identifier for the log field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_LOG = "log"; + + /** + * Constant identifier for the logfile field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_LOG_FILE = "logFile"; + + /** + * Constant identifier for the currency time limit field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_CURRENCY_TIME_LIMIT = "currencyTimeLimit"; + + /** + * Constant identifier for the default field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_DEFAULT = "default"; + + /** + * Constant identifier for the persistPolicy field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_POLICY = "persistPolicy"; + + /** + * Constant identifier for the persistPeriod field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_PERIOD = "persistPeriod"; + + /** + * Constant identifier for the persistLocation field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_LOCATION = "persistLocation"; + + /** + * Constant identifier for the persistName field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_NAME = "persistName"; + + + /** + * Default value for the JMX field "currencyTimeLimit". + */ + private Integer defaultCurrencyTimeLimit; + + /** + * Indicates whether or not strict casing is being used for attributes. + */ + private boolean useStrictCasing = true; + + private boolean exposeClassDescriptor = false; + + + /** + * Set the default for the JMX field "currencyTimeLimit". + * The default will usually indicate to never cache attribute values. + *

Default is none, not explicitly setting that field, as recommended by the + * JMX 1.2 specification. This should result in "never cache" behavior, always + * reading attribute values freshly (which corresponds to a "currencyTimeLimit" + * of -1 in JMX 1.2). + *

However, some JMX implementations (that do not follow the JMX 1.2 spec + * in that respect) might require an explicit value to be set here to get + * "never cache" behavior: for example, JBoss 3.2.x. + *

Note that the "currencyTimeLimit" value can also be specified on a + * managed attribute or operation. The default value will apply if not + * overridden with a "currencyTimeLimit" value >= 0 there: + * a metadata "currencyTimeLimit" value of -1 indicates + * to use the default; a value of 0 indicates to "always cache" + * and will be translated to Integer.MAX_VALUE; a positive + * value indicates the number of cache seconds. + * @see org.springframework.jmx.export.metadata.AbstractJmxAttribute#setCurrencyTimeLimit + * @see #applyCurrencyTimeLimit(javax.management.Descriptor, int) + */ + public void setDefaultCurrencyTimeLimit(Integer defaultCurrencyTimeLimit) { + this.defaultCurrencyTimeLimit = defaultCurrencyTimeLimit; + } + + /** + * Return default value for the JMX field "currencyTimeLimit", if any. + */ + protected Integer getDefaultCurrencyTimeLimit() { + return this.defaultCurrencyTimeLimit; + } + + /** + * Set whether to use strict casing for attributes. Enabled by default. + *

When using strict casing, a JavaBean property with a getter such as + * getFoo() translates to an attribute called Foo. + * With strict casing disabled, getFoo() would translate to just + * foo. + */ + public void setUseStrictCasing(boolean useStrictCasing) { + this.useStrictCasing = useStrictCasing; + } + + /** + * Return whether strict casing for attributes is enabled. + */ + protected boolean isUseStrictCasing() { + return useStrictCasing; + } + + /** + * Set whether to expose the JMX descriptor field "class" for managed operations. + * Default is "false", letting the JMX implementation determine the actual class + * through reflection. + *

Set this property to true for JMX implementations that + * require the "class" field to be specified, for example WebLogic's. + * In that case, Spring will expose the target class name there, in case of + * a plain bean instance or a CGLIB proxy. When encountering a JDK dynamic + * proxy, the first interface implemented by the proxy will be specified. + *

WARNING: Review your proxy definitions when exposing a JDK dynamic + * proxy through JMX, in particular with this property turned to true: + * the specified interface list should start with your management interface in + * this case, with all other interfaces following. In general, consider exposing + * your target bean directly or a CGLIB proxy for it instead. + * @see #getClassForDescriptor(Object) + */ + public void setExposeClassDescriptor(boolean exposeClassDescriptor) { + this.exposeClassDescriptor = exposeClassDescriptor; + } + + /** + * Return whether to expose the JMX descriptor field "class" for managed operations. + */ + protected boolean isExposeClassDescriptor() { + return exposeClassDescriptor; + } + + + /** + * Iterate through all properties on the MBean class and gives subclasses + * the chance to vote on the inclusion of both the accessor and mutator. + * If a particular accessor or mutator is voted for inclusion, the appropriate + * metadata is assembled and passed to the subclass for descriptor population. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the attribute metadata + * @throws JMException in case of errors + * @see #populateAttributeDescriptor + */ + protected ModelMBeanAttributeInfo[] getAttributeInfo(Object managedBean, String beanKey) throws JMException { + PropertyDescriptor[] props = BeanUtils.getPropertyDescriptors(getClassToExpose(managedBean)); + List infos = new ArrayList(); + + for (int i = 0; i < props.length; i++) { + Method getter = props[i].getReadMethod(); + if (getter != null && getter.getDeclaringClass() == Object.class) { + continue; + } + if (getter != null && !includeReadAttribute(getter, beanKey)) { + getter = null; + } + + Method setter = props[i].getWriteMethod(); + if (setter != null && !includeWriteAttribute(setter, beanKey)) { + setter = null; + } + + if (getter != null || setter != null) { + // If both getter and setter are null, then this does not need exposing. + String attrName = JmxUtils.getAttributeName(props[i], isUseStrictCasing()); + String description = getAttributeDescription(props[i], beanKey); + ModelMBeanAttributeInfo info = new ModelMBeanAttributeInfo(attrName, description, getter, setter); + + Descriptor desc = info.getDescriptor(); + if (getter != null) { + desc.setField(FIELD_GET_METHOD, getter.getName()); + } + if (setter != null) { + desc.setField(FIELD_SET_METHOD, setter.getName()); + } + + populateAttributeDescriptor(desc, getter, setter, beanKey); + info.setDescriptor(desc); + infos.add(info); + } + } + + return (ModelMBeanAttributeInfo[]) infos.toArray(new ModelMBeanAttributeInfo[infos.size()]); + } + + /** + * Iterate through all methods on the MBean class and gives subclasses the chance + * to vote on their inclusion. If a particular method corresponds to the accessor + * or mutator of an attribute that is inclued in the managment interface, then + * the corresponding operation is exposed with the "role" descriptor + * field set to the appropriate value. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the operation metadata + * @see #populateOperationDescriptor + */ + protected ModelMBeanOperationInfo[] getOperationInfo(Object managedBean, String beanKey) { + Method[] methods = getClassToExpose(managedBean).getMethods(); + List infos = new ArrayList(); + + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + if (JdkVersion.isAtLeastJava15() && method.isSynthetic()) { + continue; + } + if (method.getDeclaringClass().equals(Object.class)) { + continue; + } + + ModelMBeanOperationInfo info = null; + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + if ((method.equals(pd.getReadMethod()) && includeReadAttribute(method, beanKey)) || + (method.equals(pd.getWriteMethod()) && includeWriteAttribute(method, beanKey))) { + // Attributes need to have their methods exposed as + // operations to the JMX server as well. + info = createModelMBeanOperationInfo(method, pd.getName(), beanKey); + Descriptor desc = info.getDescriptor(); + if (method.equals(pd.getReadMethod())) { + desc.setField(FIELD_ROLE, ROLE_GETTER); + } + else { + desc.setField(FIELD_ROLE, ROLE_SETTER); + } + desc.setField(FIELD_VISIBILITY, ATTRIBUTE_OPERATION_VISIBILITY); + if (isExposeClassDescriptor()) { + desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName()); + } + info.setDescriptor(desc); + } + } + else if (includeOperation(method, beanKey)) { + info = createModelMBeanOperationInfo(method, method.getName(), beanKey); + Descriptor desc = info.getDescriptor(); + desc.setField(FIELD_ROLE, ROLE_OPERATION); + if (isExposeClassDescriptor()) { + desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName()); + } + populateOperationDescriptor(desc, method, beanKey); + info.setDescriptor(desc); + } + + if (info != null) { + infos.add(info); + } + } + + return (ModelMBeanOperationInfo[]) infos.toArray(new ModelMBeanOperationInfo[infos.size()]); + } + + /** + * Creates an instance of ModelMBeanOperationInfo for the + * given method. Populates the parameter info for the operation. + * @param method the Method to create a ModelMBeanOperationInfo for + * @param name the name for the operation info + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the ModelMBeanOperationInfo + */ + protected ModelMBeanOperationInfo createModelMBeanOperationInfo(Method method, String name, String beanKey) { + MBeanParameterInfo[] params = getOperationParameters(method, beanKey); + if (params.length == 0) { + return new ModelMBeanOperationInfo(getOperationDescription(method, beanKey), method); + } + else { + return new ModelMBeanOperationInfo(name, + getOperationDescription(method, beanKey), + getOperationParameters(method, beanKey), + method.getReturnType().getName(), + MBeanOperationInfo.UNKNOWN); + } + } + + /** + * Return the class to be used for the JMX descriptor field "class". + * Only applied when the "exposeClassDescriptor" property is "true". + *

Default implementation returns the first implemented interface + * for a JDK proxy, and the target class else. + * @param managedBean the bean instance (might be an AOP proxy) + * @return the class to expose in the descriptor field "class" + * @see #setExposeClassDescriptor + * @see #getClassToExpose(Class) + * @see org.springframework.aop.framework.AopProxyUtils#proxiedUserInterfaces(Object) + */ + protected Class getClassForDescriptor(Object managedBean) { + if (AopUtils.isJdkDynamicProxy(managedBean)) { + return AopProxyUtils.proxiedUserInterfaces(managedBean)[0]; + } + return getClassToExpose(managedBean); + } + + + /** + * Allows subclasses to vote on the inclusion of a particular attribute accessor. + * @param method the accessor Method + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return true if the accessor should be included in the management interface, + * otherwise false + */ + protected abstract boolean includeReadAttribute(Method method, String beanKey); + + /** + * Allows subclasses to vote on the inclusion of a particular attribute mutator. + * @param method the mutator Method. + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return true if the mutator should be included in the management interface, + * otherwise false + */ + protected abstract boolean includeWriteAttribute(Method method, String beanKey); + + /** + * Allows subclasses to vote on the inclusion of a particular operation. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return whether the operation should be included in the management interface + */ + protected abstract boolean includeOperation(Method method, String beanKey); + + + /** + * Get the description for a particular attribute. + *

Default implementation returns a description for the operation + * that is the name of corresponding Method. + * @param propertyDescriptor the PropertyDescriptor for the attribute + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the description for the attribute + */ + protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) { + return propertyDescriptor.getDisplayName(); + } + + /** + * Get the description for a particular operation. + *

Default implementation returns a description for the operation + * that is the name of corresponding Method. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the description for the operation + */ + protected String getOperationDescription(Method method, String beanKey) { + return method.getName(); + } + + /** + * Create parameter info for the given method. Default implementation + * returns an empty arry of MBeanParameterInfo. + * @param method the Method to get the parameter information for + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @return the MBeanParameterInfo array + */ + protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { + return new MBeanParameterInfo[0]; + } + + + /** + * Allows subclasses to add extra fields to the Descriptor for an + * MBean. Default implementation sets the currencyTimeLimit field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param descriptor the Descriptor for the MBean resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) { + applyDefaultCurrencyTimeLimit(descriptor); + } + + /** + * Allows subclasses to add extra fields to the Descriptor for a particular + * attribute. Default implementation sets the currencyTimeLimit field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the attribute descriptor + * @param getter the accessor method for the attribute + * @param setter the mutator method for the attribute + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void populateAttributeDescriptor(Descriptor desc, Method getter, Method setter, String beanKey) { + applyDefaultCurrencyTimeLimit(desc); + } + + /** + * Allows subclasses to add extra fields to the Descriptor for a particular + * operation. Default implementation sets the currencyTimeLimit field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the operation descriptor + * @param method the method corresponding to the operation + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) { + applyDefaultCurrencyTimeLimit(desc); + } + + /** + * Set the currencyTimeLimit field to the specified + * "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the JMX attribute or operation descriptor + * @see #setDefaultCurrencyTimeLimit(Integer) + */ + protected final void applyDefaultCurrencyTimeLimit(Descriptor desc) { + if (getDefaultCurrencyTimeLimit() != null) { + desc.setField(FIELD_CURRENCY_TIME_LIMIT, getDefaultCurrencyTimeLimit().toString()); + } + } + + /** + * Apply the given JMX "currencyTimeLimit" value to the given descriptor. + *

Default implementation sets a value >0 as-is (as number of cache seconds), + * turns a value of 0 into Integer.MAX_VALUE ("always cache") + * and sets the "defaultCurrencyTimeLimit" (if any, indicating "never cache") in case of + * a value <0. This follows the recommendation in the JMX 1.2 specification. + * @param desc the JMX attribute or operation descriptor + * @param currencyTimeLimit the "currencyTimeLimit" value to apply + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void applyCurrencyTimeLimit(Descriptor desc, int currencyTimeLimit) { + if (currencyTimeLimit > 0) { + // number of cache seconds + desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(currencyTimeLimit)); + } + else if (currencyTimeLimit == 0) { + // "always cache" + desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(Integer.MAX_VALUE)); + } + else { + // "never cache" + applyDefaultCurrencyTimeLimit(desc); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java new file mode 100644 index 00000000000..e199db653b0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java @@ -0,0 +1,42 @@ +/* + * 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.jmx.export.assembler; + +/** + * Extends the MBeanInfoAssembler to add autodetection logic. + * Implementations of this interface are given the opportunity by the + * MBeanExporter to include additional beans in the registration process. + * + *

The exact mechanism for deciding which beans to include is left to + * implementing classes. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface AutodetectCapableMBeanInfoAssembler extends MBeanInfoAssembler { + + /** + * Indicate whether a particular bean should be included in the registration + * process, if it is not specified in the beans map of the + * MBeanExporter. + * @param beanClass the class of the bean (might be a proxy class) + * @param beanName the name of the bean in the bean factory + */ + boolean includeBean(Class beanClass, String beanName); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java new file mode 100644 index 00000000000..d553706a61b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java @@ -0,0 +1,244 @@ +/* + * 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.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Subclass of AbstractReflectiveMBeanInfoAssembler that allows for + * the management interface of a bean to be defined using arbitrary interfaces. + * Any methods or properties that are defined in those interfaces are exposed + * as MBean operations and attributes. + * + *

By default, this class votes on the inclusion of each operation or attribute + * based on the interfaces implemented by the bean class. However, you can supply an + * array of interfaces via the managedInterfaces property that will be + * used instead. If you have multiple beans and you wish each bean to use a different + * set of interfaces, then you can map bean keys (that is the name used to pass the + * bean to the MBeanExporter) to a list of interface names using the + * interfaceMappings property. + * + *

If you specify values for both interfaceMappings and + * managedInterfaces, Spring will attempt to find interfaces in the + * mappings first. If no interfaces for the bean are found, it will use the + * interfaces defined by managedInterfaces. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setManagedInterfaces + * @see #setInterfaceMappings + * @see MethodNameBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler + implements BeanClassLoaderAware, InitializingBean { + + /** + * Stores the array of interfaces to use for creating the management interface. + */ + private Class[] managedInterfaces; + + /** + * Stores the mappings of bean keys to an array of Classes. + */ + private Properties interfaceMappings; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** + * Stores the mappings of bean keys to an array of Classes. + */ + private Map resolvedInterfaceMappings; + + + /** + * Set the array of interfaces to use for creating the management info. + * These interfaces will be used for a bean if no entry corresponding to + * that bean is found in the interfaceMappings property. + * @param managedInterfaces an array of classes indicating the interfaces to use. + * Each entry MUST be an interface. + * @see #setInterfaceMappings + */ + public void setManagedInterfaces(Class[] managedInterfaces) { + if (managedInterfaces != null) { + for (int x = 0; x < managedInterfaces.length; x++) { + if (!managedInterfaces[x].isInterface()) { + throw new IllegalArgumentException( + "Management interface [" + managedInterfaces[x].getName() + "] is no interface"); + } + } + } + this.managedInterfaces = managedInterfaces; + } + + /** + * Set the mappings of bean keys to a comma-separated list of interface names. + * The property key should match the bean key and the property value should match + * the list of interface names. When searching for interfaces for a bean, Spring + * will check these mappings first. + * @param mappings the mappins of bean keys to interface names + */ + public void setInterfaceMappings(Properties mappings) { + this.interfaceMappings = mappings; + } + + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + public void afterPropertiesSet() { + if (this.interfaceMappings != null) { + this.resolvedInterfaceMappings = resolveInterfaceMappings(this.interfaceMappings); + } + } + + /** + * Resolve the given interface mappings, turning class names into Class objects. + * @param mappings the specified interface mappings + * @return the resolved interface mappings (with Class objects as values) + */ + private Map resolveInterfaceMappings(Properties mappings) { + Map resolvedMappings = new HashMap(mappings.size()); + for (Enumeration en = mappings.propertyNames(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] classNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + Class[] classes = resolveClassNames(classNames, beanKey); + resolvedMappings.put(beanKey, classes); + } + return resolvedMappings; + } + + /** + * Resolve the given class names into Class objects. + * @param classNames the class names to resolve + * @param beanKey the bean key that the class names are associated with + * @return the resolved Class + */ + private Class[] resolveClassNames(String[] classNames, String beanKey) { + Class[] classes = new Class[classNames.length]; + for (int x = 0; x < classes.length; x++) { + Class cls = ClassUtils.resolveClassName(classNames[x].trim(), this.beanClassLoader); + if (!cls.isInterface()) { + throw new IllegalArgumentException( + "Class [" + classNames[x] + "] mapped to bean key [" + beanKey + "] is no interface"); + } + classes[x] = cls; + } + return classes; + } + + + /** + * Check to see if the Method is declared in + * one of the configured interfaces and that it is public. + * @param method the accessor Method. + * @param beanKey the key associated with the MBean in the + * beans Map. + * @return true if the Method is declared in one of the + * configured interfaces, otherwise false. + */ + protected boolean includeReadAttribute(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the Method is declared in + * one of the configured interfaces and that it is public. + * @param method the mutator Method. + * @param beanKey the key associated with the MBean in the + * beans Map. + * @return true if the Method is declared in one of the + * configured interfaces, otherwise false. + */ + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the Method is declared in + * one of the configured interfaces and that it is public. + * @param method the operation Method. + * @param beanKey the key associated with the MBean in the + * beans Map. + * @return true if the Method is declared in one of the + * configured interfaces, otherwise false. + */ + protected boolean includeOperation(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the Method is both public and declared in + * one of the configured interfaces. + * @param method the Method to check. + * @param beanKey the key associated with the MBean in the beans map + * @return true if the Method is declared in one of the + * configured interfaces and is public, otherwise false. + */ + private boolean isPublicInInterface(Method method, String beanKey) { + return ((method.getModifiers() & Modifier.PUBLIC) > 0) && isDeclaredInInterface(method, beanKey); + } + + /** + * Checks to see if the given method is declared in a managed + * interface for the given bean. + */ + private boolean isDeclaredInInterface(Method method, String beanKey) { + Class[] ifaces = null; + + if (this.resolvedInterfaceMappings != null) { + ifaces = (Class[]) this.resolvedInterfaceMappings.get(beanKey); + } + + if (ifaces == null) { + ifaces = this.managedInterfaces; + if (ifaces == null) { + ifaces = ClassUtils.getAllInterfacesForClass(method.getDeclaringClass()); + } + } + + if (ifaces != null) { + for (int i = 0; i < ifaces.length; i++) { + Method[] methods = ifaces[i].getMethods(); + for (int j = 0; j < methods.length; j++) { + Method ifaceMethod = methods[j]; + if (ifaceMethod.getName().equals(method.getName()) && + Arrays.equals(ifaceMethod.getParameterTypes(), method.getParameterTypes())) { + return true; + } + } + } + } + + return false; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java new file mode 100644 index 00000000000..0d0976ea061 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java @@ -0,0 +1,45 @@ +/* + * 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.jmx.export.assembler; + +import javax.management.JMException; +import javax.management.modelmbean.ModelMBeanInfo; + +/** + * Interface to be implemented by all classes that can + * create management interface metadata for a managed resource. + * + *

Used by the MBeanExporter to generate the management + * interface for any bean that is not an MBean. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface MBeanInfoAssembler { + + /** + * Create the ModelMBeanInfo for the given managed resource. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @param beanKey the key associated with the managed bean + * @return the ModelMBeanInfo metadata object + * @throws JMException in case of errors + */ + ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java new file mode 100644 index 00000000000..0a0df23bac4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java @@ -0,0 +1,384 @@ +/* + * 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.jmx.export.assembler; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; + +import javax.management.Descriptor; +import javax.management.MBeanParameterInfo; +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.metadata.InvalidMetadataException; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.export.metadata.JmxMetadataUtils; +import org.springframework.jmx.export.metadata.ManagedAttribute; +import org.springframework.jmx.export.metadata.ManagedNotification; +import org.springframework.jmx.export.metadata.ManagedOperation; +import org.springframework.jmx.export.metadata.ManagedOperationParameter; +import org.springframework.jmx.export.metadata.ManagedResource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of the {@link org.springframework.jmx.export.assembler.MBeanInfoAssembler} + * interface that reads the management interface information from source level metadata. + * + *

Uses the {@link JmxAttributeSource} strategy interface, so that + * metadata can be read using any supported implementation. Out of the box, + * two strategies are included: + *

    + *
  • AttributesJmxAttributeSource, for Commons Attributes + *
  • AnnotationJmxAttributeSource, for JDK 1.5+ annotations + *
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setAttributeSource + * @see org.springframework.jmx.export.metadata.AttributesJmxAttributeSource + * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource + */ +public class MetadataMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler + implements AutodetectCapableMBeanInfoAssembler, InitializingBean { + + private JmxAttributeSource attributeSource; + + + /** + * Create a new MetadataMBeanInfoAssembler which needs to be + * configured through the {@link #setAttributeSource} method. + */ + public MetadataMBeanInfoAssembler() { + } + + /** + * Create a new MetadataMBeanInfoAssembler for the given + * JmxAttributeSource. + * @param attributeSource the JmxAttributeSource to use + */ + public MetadataMBeanInfoAssembler(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + + /** + * Set the JmxAttributeSource implementation to use for + * reading the metadata from the bean class. + * @see org.springframework.jmx.export.metadata.AttributesJmxAttributeSource + * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource + */ + public void setAttributeSource(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + public void afterPropertiesSet() { + if (this.attributeSource == null) { + throw new IllegalArgumentException("Property 'attributeSource' is required"); + } + } + + + /** + * Throws an IllegalArgumentException if it encounters a JDK dynamic proxy. + * Metadata can only be read from target classes and CGLIB proxies! + */ + protected void checkManagedBean(Object managedBean) throws IllegalArgumentException { + if (AopUtils.isJdkDynamicProxy(managedBean)) { + throw new IllegalArgumentException( + "MetadataMBeanInfoAssembler does not support JDK dynamic proxies - " + + "export the target beans directly or use CGLIB proxies instead"); + } + } + + /** + * Used for autodetection of beans. Checks to see if the bean's class has a + * ManagedResource attribute. If so it will add it list of included beans. + * @param beanClass the class of the bean + * @param beanName the name of the bean in the bean factory + */ + public boolean includeBean(Class beanClass, String beanName) { + return (this.attributeSource.getManagedResource(getClassToExpose(beanClass)) != null); + } + + /** + * Vote on the inclusion of an attribute accessor. + * @param method the accessor method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + protected boolean includeReadAttribute(Method method, String beanKey) { + return hasManagedAttribute(method); + } + + /** + * Votes on the inclusion of an attribute mutator. + * @param method the mutator method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + protected boolean includeWriteAttribute(Method method, String beanKey) { + return hasManagedAttribute(method); + } + + /** + * Votes on the inclusion of an operation. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + protected boolean includeOperation(Method method, String beanKey) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + return hasManagedAttribute(method); + } + else { + return hasManagedOperation(method); + } + } + + /** + * Checks to see if the given Method has the ManagedAttribute attribute. + */ + private boolean hasManagedAttribute(Method method) { + return (this.attributeSource.getManagedAttribute(method) != null); + } + + /** + * Checks to see if the given Method has the ManagedOperation attribute. + * @param method the method to check + */ + private boolean hasManagedOperation(Method method) { + return (this.attributeSource.getManagedOperation(method) != null); + } + + + /** + * Reads managed resource description from the source level metadata. + * Returns an empty String if no description can be found. + */ + protected String getDescription(Object managedBean, String beanKey) { + ManagedResource mr = this.attributeSource.getManagedResource(getClassToExpose(managedBean)); + return (mr != null ? mr.getDescription() : ""); + } + + /** + * Creates a description for the attribute corresponding to this property + * descriptor. Attempts to create the description using metadata from either + * the getter or setter attributes, otherwise uses the property name. + */ + protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + + ManagedAttribute getter = + (readMethod != null) ? this.attributeSource.getManagedAttribute(readMethod) : null; + ManagedAttribute setter = + (writeMethod != null) ? this.attributeSource.getManagedAttribute(writeMethod) : null; + + if (getter != null && StringUtils.hasText(getter.getDescription())) { + return getter.getDescription(); + } + else if (setter != null && StringUtils.hasText(setter.getDescription())) { + return setter.getDescription(); + } + return propertyDescriptor.getDisplayName(); + } + + /** + * Retrieves the description for the supplied Method from the + * metadata. Uses the method name is no description is present in the metadata. + */ + protected String getOperationDescription(Method method, String beanKey) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + ManagedAttribute ma = this.attributeSource.getManagedAttribute(method); + if (ma != null && StringUtils.hasText(ma.getDescription())) { + return ma.getDescription(); + } + return method.getName(); + } + else { + ManagedOperation mo = this.attributeSource.getManagedOperation(method); + if (mo != null && StringUtils.hasText(mo.getDescription())) { + return mo.getDescription(); + } + return method.getName(); + } + } + + /** + * Reads MBeanParameterInfo from the ManagedOperationParameter + * attributes attached to a method. Returns an empty array of MBeanParameterInfo + * if no attributes are found. + */ + protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { + ManagedOperationParameter[] params = this.attributeSource.getManagedOperationParameters(method); + if (params == null || params.length == 0) { + return new MBeanParameterInfo[0]; + } + + MBeanParameterInfo[] parameterInfo = new MBeanParameterInfo[params.length]; + Class[] methodParameters = method.getParameterTypes(); + + for (int i = 0; i < params.length; i++) { + ManagedOperationParameter param = params[i]; + parameterInfo[i] = + new MBeanParameterInfo(param.getName(), methodParameters[i].getName(), param.getDescription()); + } + + return parameterInfo; + } + + /** + * Reads the {@link ManagedNotification} metadata from the Class of the managed resource + * and generates and returns the corresponding {@link ModelMBeanNotificationInfo} metadata. + */ + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) { + ManagedNotification[] notificationAttributes = + this.attributeSource.getManagedNotifications(getClassToExpose(managedBean)); + ModelMBeanNotificationInfo[] notificationInfos = + new ModelMBeanNotificationInfo[notificationAttributes.length]; + + for (int i = 0; i < notificationAttributes.length; i++) { + ManagedNotification attribute = notificationAttributes[i]; + notificationInfos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(attribute); + } + + return notificationInfos; + } + + /** + * Adds descriptor fields from the ManagedResource attribute + * to the MBean descriptor. Specifically, adds the currencyTimeLimit, + * persistPolicy, persistPeriod, persistLocation + * and persistName descriptor fields if they are present in the metadata. + */ + protected void populateMBeanDescriptor(Descriptor desc, Object managedBean, String beanKey) { + ManagedResource mr = this.attributeSource.getManagedResource(getClassToExpose(managedBean)); + if (mr == null) { + throw new InvalidMetadataException( + "No ManagedResource attribute found for class: " + getClassToExpose(managedBean)); + } + + applyCurrencyTimeLimit(desc, mr.getCurrencyTimeLimit()); + + if (mr.isLog()) { + desc.setField(FIELD_LOG, "true"); + } + if (StringUtils.hasLength(mr.getLogFile())) { + desc.setField(FIELD_LOG_FILE, mr.getLogFile()); + } + + if (StringUtils.hasLength(mr.getPersistPolicy())) { + desc.setField(FIELD_PERSIST_POLICY, mr.getPersistPolicy()); + } + if (mr.getPersistPeriod() >= 0) { + desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(mr.getPersistPeriod())); + } + if (StringUtils.hasLength(mr.getPersistName())) { + desc.setField(FIELD_PERSIST_NAME, mr.getPersistName()); + } + if (StringUtils.hasLength(mr.getPersistLocation())) { + desc.setField(FIELD_PERSIST_LOCATION, mr.getPersistLocation()); + } + } + + /** + * Adds descriptor fields from the ManagedAttribute attribute + * to the attribute descriptor. Specifically, adds the currencyTimeLimit, + * default, persistPolicy and persistPeriod + * descriptor fields if they are present in the metadata. + */ + protected void populateAttributeDescriptor(Descriptor desc, Method getter, Method setter, String beanKey) { + ManagedAttribute gma = + (getter == null) ? ManagedAttribute.EMPTY : this.attributeSource.getManagedAttribute(getter); + ManagedAttribute sma = + (setter == null) ? ManagedAttribute.EMPTY : this.attributeSource.getManagedAttribute(setter); + + applyCurrencyTimeLimit(desc, resolveIntDescriptor(gma.getCurrencyTimeLimit(), sma.getCurrencyTimeLimit())); + + Object defaultValue = resolveObjectDescriptor(gma.getDefaultValue(), sma.getDefaultValue()); + desc.setField(FIELD_DEFAULT, defaultValue); + + String persistPolicy = resolveStringDescriptor(gma.getPersistPolicy(), sma.getPersistPolicy()); + if (StringUtils.hasLength(persistPolicy)) { + desc.setField(FIELD_PERSIST_POLICY, persistPolicy); + } + int persistPeriod = resolveIntDescriptor(gma.getPersistPeriod(), sma.getPersistPeriod()); + if (persistPeriod >= 0) { + desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(persistPeriod)); + } + } + + /** + * Adds descriptor fields from the ManagedAttribute attribute + * to the attribute descriptor. Specifically, adds the currencyTimeLimit + * descriptor field if it is present in the metadata. + */ + protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) { + ManagedOperation mo = this.attributeSource.getManagedOperation(method); + if (mo != null) { + applyCurrencyTimeLimit(desc, mo.getCurrencyTimeLimit()); + } + } + + /** + * Determines which of two int values should be used as the value + * for an attribute descriptor. In general, only the getter or the setter will + * be have a non-negative value so we use that value. In the event that both values + * are non-negative, we use the greater of the two. This method can be used to + * resolve any int valued descriptor where there are two possible values. + * @param getter the int value associated with the getter for this attribute + * @param setter the int associated with the setter for this attribute + */ + private int resolveIntDescriptor(int getter, int setter) { + return (getter >= setter ? getter : setter); + } + + /** + * Locates the value of a descriptor based on values attached + * to both the getter and setter methods. If both have values + * supplied then the value attached to the getter is preferred. + * @param getter the Object value associated with the get method + * @param setter the Object value associated with the set method + * @return the appropriate Object to use as the value for the descriptor + */ + private Object resolveObjectDescriptor(Object getter, Object setter) { + return (getter != null ? getter : setter); + } + + /** + * Locates the value of a descriptor based on values attached + * to both the getter and setter methods. If both have values + * supplied then the value attached to the getter is preferred. + * The supplied default value is used to check to see if the value + * associated with the getter has changed from the default. + * @param getter the String value associated with the get method + * @param setter the String value associated with the set method + * @return the appropriate String to use as the value for the descriptor + */ + private String resolveStringDescriptor(String getter, String setter) { + return (StringUtils.hasLength(getter) ? getter : setter); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java new file mode 100644 index 00000000000..070052eaf6f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.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.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.util.StringUtils; + +/** + * AbstractReflectiveMBeanInfoAssembler subclass that allows + * method names to be explicitly excluded as MBean operations and attributes. + * + *

Any method not explicitly excluded from the management interface will be exposed to + * JMX. JavaBean getters and setters will automatically be exposed as JMX attributes. + * + *

You can supply an array of method names via the ignoredMethods + * property. If you have multiple beans and you wish each bean to use a different + * set of method names, then you can map bean keys (that is the name used to pass + * the bean to the MBeanExporter) to a list of method names using the + * ignoredMethodMappings property. + * + *

If you specify values for both ignoredMethodMappings and + * ignoredMethods, Spring will attempt to find method names in the + * mappings first. If no method names for the bean are found, it will use the + * method names defined by ignoredMethods. + * + * @author Rob Harrop + * @author Seth Ladd + * @since 1.2.5 + * @see #setIgnoredMethods + * @see #setIgnoredMethodMappings + * @see InterfaceBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see MethodNameBasedMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MethodExclusionMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + private Set ignoredMethods; + + private Map ignoredMethodMappings; + + + /** + * Set the array of method names to be ignored when creating the management info. + *

These method names will be used for a bean if no entry corresponding to + * that bean is found in the ignoredMethodsMappings property. + * @see #setIgnoredMethodMappings(java.util.Properties) + */ + public void setIgnoredMethods(String[] ignoredMethodNames) { + this.ignoredMethods = new HashSet(Arrays.asList(ignoredMethodNames)); + } + + /** + * Set the mappings of bean keys to a comma-separated list of method names. + *

These method names are ignored when creating the management interface. + *

The property key must match the bean key and the property value must match + * the list of method names. When searching for method names to ignore for a bean, + * Spring will check these mappings first. + */ + public void setIgnoredMethodMappings(Properties mappings) { + this.ignoredMethodMappings = new HashMap(); + for (Enumeration en = mappings.keys(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] methodNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + this.ignoredMethodMappings.put(beanKey, new HashSet(Arrays.asList(methodNames))); + } + } + + + protected boolean includeReadAttribute(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + protected boolean includeOperation(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + /** + * Determine whether the given method is supposed to be included, + * that is, not configured as to be ignored. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the MBeanExporter + */ + protected boolean isNotIgnored(Method method, String beanKey) { + if (this.ignoredMethodMappings != null) { + Set methodNames = (Set) this.ignoredMethodMappings.get(beanKey); + if (methodNames != null) { + return !methodNames.contains(method.getName()); + } + } + if (this.ignoredMethods != null) { + return !this.ignoredMethods.contains(method.getName()); + } + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java new file mode 100644 index 00000000000..100ae57285c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java @@ -0,0 +1,118 @@ +/* + * 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.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.util.StringUtils; + +/** + * Subclass of AbstractReflectiveMBeanInfoAssembler that allows + * to specify method names to be exposed as MBean operations and attributes. + * JavaBean getters and setters will automatically be exposed as JMX attributes. + * + *

You can supply an array of method names via the managedMethods + * property. If you have multiple beans and you wish each bean to use a different + * set of method names, then you can map bean keys (that is the name used to pass + * the bean to the MBeanExporter) to a list of method names using the + * methodMappings property. + * + *

If you specify values for both methodMappings and + * managedMethods, Spring will attempt to find method names in the + * mappings first. If no method names for the bean are found, it will use the + * method names defined by managedMethods. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setManagedMethods + * @see #setMethodMappings + * @see InterfaceBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see MethodExclusionMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MethodNameBasedMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + /** + * Stores the set of method names to use for creating the management interface. + */ + private Set managedMethods; + + /** + * Stores the mappings of bean keys to an array of method names. + */ + private Map methodMappings; + + + /** + * Set the array of method names to use for creating the management info. + * These method names will be used for a bean if no entry corresponding to + * that bean is found in the methodMappings property. + * @param methodNames an array of method names indicating the methods to use + * @see #setMethodMappings + */ + public void setManagedMethods(String[] methodNames) { + this.managedMethods = new HashSet(Arrays.asList(methodNames)); + } + + /** + * Set the mappings of bean keys to a comma-separated list of method names. + * The property key should match the bean key and the property value should match + * the list of method names. When searching for method names for a bean, Spring + * will check these mappings first. + * @param mappings the mappins of bean keys to method names + */ + public void setMethodMappings(Properties mappings) { + this.methodMappings = new HashMap(); + for (Enumeration en = mappings.keys(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] methodNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + this.methodMappings.put(beanKey, new HashSet(Arrays.asList(methodNames))); + } + } + + + protected boolean includeReadAttribute(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + protected boolean includeOperation(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + protected boolean isMatch(Method method, String beanKey) { + if (this.methodMappings != null) { + Set methodNames = (Set) this.methodMappings.get(beanKey); + if (methodNames != null) { + return methodNames.contains(method.getName()); + } + } + return (this.managedMethods != null && this.managedMethods.contains(method.getName())); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.java new file mode 100644 index 00000000000..9f97485f48d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.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.jmx.export.assembler; + +import java.lang.reflect.Method; + +/** + * Simple subclass of AbstractReflectiveMBeanInfoAssembler + * that always votes yes for method and property inclusion, effectively exposing + * all public methods and properties as operations and attributes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleReflectiveMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + /** + * Always returns true. + */ + protected boolean includeReadAttribute(Method method, String beanKey) { + return true; + } + + /** + * Always returns true. + */ + protected boolean includeWriteAttribute(Method method, String beanKey) { + return true; + } + + /** + * Always returns true. + */ + protected boolean includeOperation(Method method, String beanKey) { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/package.html new file mode 100644 index 00000000000..3a0e7e35a36 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/assembler/package.html @@ -0,0 +1,8 @@ + + + +Provides a strategy for MBeanInfo assembly. Used by MBeanExporter to +determine the attributes and operations to expose for Spring-managed beans. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java new file mode 100644 index 00000000000..cf2cbd0114e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java @@ -0,0 +1,60 @@ +/* + * 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.jmx.export.metadata; + +/** + * Base class for all JMX metadata classes. + * + * @author Rob Harrop + * @since 1.2 + */ +public class AbstractJmxAttribute { + + private String description = ""; + + private int currencyTimeLimit = -1; + + + /** + * Set a description for this attribute. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Return a description for this attribute. + */ + public String getDescription() { + return this.description; + } + + /** + * Set a currency time limit for this attribute. + */ + public void setCurrencyTimeLimit(int currencyTimeLimit) { + this.currencyTimeLimit = currencyTimeLimit; + } + + /** + * Return a currency time limit for this attribute. + */ + public int getCurrencyTimeLimit() { + return this.currencyTimeLimit; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AttributesJmxAttributeSource.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AttributesJmxAttributeSource.java new file mode 100644 index 00000000000..e5a0e00d448 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/AttributesJmxAttributeSource.java @@ -0,0 +1,203 @@ +/* + * 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.jmx.export.metadata; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.metadata.Attributes; +import org.springframework.util.Assert; + +/** + * Implementation of the JmxAttributeSource interface that + * reads metadata via Spring's Attributes abstraction. + * + *

Typically used for reading in source-level attributes via + * Commons Attributes. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.metadata.Attributes + * @see org.springframework.metadata.commons.CommonsAttributes + */ +public class AttributesJmxAttributeSource implements JmxAttributeSource, InitializingBean { + + /** + * Underlying Attributes implementation that we're using. + */ + private Attributes attributes; + + + /** + * Create a new AttributesJmxAttributeSource. + * @see #setAttributes + */ + public AttributesJmxAttributeSource() { + } + + /** + * Create a new AttributesJmxAttributeSource. + * @param attributes the Attributes implementation to use + * @see org.springframework.metadata.commons.CommonsAttributes + */ + public AttributesJmxAttributeSource(Attributes attributes) { + if (attributes == null) { + throw new IllegalArgumentException("Attributes is required"); + } + this.attributes = attributes; + } + + /** + * Set the Attributes implementation to use. + * @see org.springframework.metadata.commons.CommonsAttributes + */ + public void setAttributes(Attributes attributes) { + this.attributes = attributes; + } + + public void afterPropertiesSet() { + if (this.attributes == null) { + throw new IllegalArgumentException("'attributes' is required"); + } + } + + + /** + * If the specified class has a ManagedResource attribute, + * then it is returned. Otherwise returns null. + * @param clazz the class to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException if more than one attribute exists + */ + public ManagedResource getManagedResource(Class clazz) { + Assert.notNull(this.attributes, "'attributes' is required"); + Collection attrs = this.attributes.getAttributes(clazz, ManagedResource.class); + if (attrs.isEmpty()) { + return null; + } + else if (attrs.size() == 1) { + return (ManagedResource) attrs.iterator().next(); + } + else { + throw new InvalidMetadataException("A Class can have only one ManagedResource attribute"); + } + } + + /** + * If the specified method has a ManagedAttribute attribute, + * then it is returned. Otherwise returns null. + * @param method the method to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException if more than one attribute exists, + * or if the supplied method does not represent a JavaBean property + */ + public ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException { + Assert.notNull(this.attributes, "'attributes' is required"); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd == null) { + throw new InvalidMetadataException( + "The ManagedAttribute attribute is only valid for JavaBean properties: " + + "use ManagedOperation for methods"); + } + Collection attrs = this.attributes.getAttributes(method, ManagedAttribute.class); + if (attrs.isEmpty()) { + return null; + } + else if (attrs.size() == 1) { + return (ManagedAttribute) attrs.iterator().next(); + } + else { + throw new InvalidMetadataException("A Method can have only one ManagedAttribute attribute"); + } + } + + /** + * If the specified method has a ManagedOperation attribute, + * then it is returned. Otherwise return null. + * @param method the method to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException if more than one attribute exists, + * or if the supplied method represents a JavaBean property + */ + public ManagedOperation getManagedOperation(Method method) { + Assert.notNull(this.attributes, "'attributes' is required"); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + throw new InvalidMetadataException( + "The ManagedOperation attribute is not valid for JavaBean properties: " + + "use ManagedAttribute instead"); + } + Collection attrs = this.attributes.getAttributes(method, ManagedOperation.class); + if (attrs.isEmpty()) { + return null; + } + else if (attrs.size() == 1) { + return (ManagedOperation) attrs.iterator().next(); + } + else { + throw new InvalidMetadataException("A Method can have only one ManagedAttribute attribute"); + } + } + + /** + * If the specified method has ManagedOperationParameter attributes, + * then these are returned, otherwise a zero length array is returned. + * @param method the method to get the managed operation parameters for + * @return the array of ManagedOperationParameter objects + * @throws InvalidMetadataException if the number of ManagedOperationParameter + * attributes does not match the number of parameters in the method + */ + public ManagedOperationParameter[] getManagedOperationParameters(Method method) + throws InvalidMetadataException { + + Assert.notNull(this.attributes, "'attributes' is required"); + Collection attrs = this.attributes.getAttributes(method, ManagedOperationParameter.class); + if (attrs.size() == 0) { + return new ManagedOperationParameter[0]; + } + else if (attrs.size() != method.getParameterTypes().length) { + throw new InvalidMetadataException( + "Method [" + method + "] has an incorrect number of ManagedOperationParameters specified"); + } + else { + ManagedOperationParameter[] params = new ManagedOperationParameter[attrs.size()]; + for (Iterator it = attrs.iterator(); it.hasNext();) { + ManagedOperationParameter param = (ManagedOperationParameter) it.next(); + if (param.getIndex() < 0 || param.getIndex() >= params.length) { + throw new InvalidMetadataException( + "ManagedOperationParameter index for [" + param.getName() + "] is out of bounds"); + } + params[param.getIndex()] = param; + } + return params; + } + } + + /** + * If the specified has {@link ManagedNotification} attributes these are returned, otherwise + * a zero-length array is returned. + */ + public ManagedNotification[] getManagedNotifications(Class clazz) { + Assert.notNull(this.attributes, "'attributes' is required"); + Collection attrs = this.attributes.getAttributes(clazz, ManagedNotification.class); + return attrs.isEmpty() ? new ManagedNotification[0] : (ManagedNotification[]) attrs.toArray(new ManagedNotification[attrs.size()]); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java new file mode 100644 index 00000000000..d44dbb2616b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java @@ -0,0 +1,41 @@ +/* + * 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.jmx.export.metadata; + +import org.springframework.jmx.JmxException; + +/** + * Thrown by the JmxAttributeSource when it encounters + * incorrect metadata on a managed resource or one of its methods. + * + * @author Rob Harrop + * @since 1.2 + * @see JmxAttributeSource + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + */ +public class InvalidMetadataException extends JmxException { + + /** + * Create a new InvalidMetadataException with the supplied + * error message. + * @param msg the detail message + */ + public InvalidMetadataException(String msg) { + super(msg); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java new file mode 100644 index 00000000000..b5165454762 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java @@ -0,0 +1,81 @@ +/* + * 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.jmx.export.metadata; + +import java.lang.reflect.Method; + +/** + * Interface used by the MetadataMBeanInfoAssembler to + * read source-level metadata from a managed resource's class. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler#setAttributeSource + * @see org.springframework.jmx.export.MBeanExporter#setAssembler + */ +public interface JmxAttributeSource { + + /** + * Implementations should return an instance of ManagedResource + * if the supplied Class has the appropriate metadata. + * Otherwise should return null. + * @param clazz the class to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + ManagedResource getManagedResource(Class clazz) throws InvalidMetadataException; + + /** + * Implementations should return an instance of ManagedAttribute + * if the supplied Method has the corresponding metadata. + * Otherwise should return null. + * @param method the method to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an instance of ManagedOperation + * if the supplied Method has the corresponding metadata. + * Otherwise should return null. + * @param method the method to read the attribute data from + * @return the attribute, or null if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an array of ManagedOperationParameter + * if the supplied Method has the corresponding metadata. Otherwise + * should return an empty array if no metadata is found. + * @param method the Method to read the metadata from + * @return the parameter information. + * @throws InvalidMetadataException in the case of invalid attributes. + */ + ManagedOperationParameter[] getManagedOperationParameters(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an array of {@link ManagedNotification ManagedNotifications} + * if the supplied the Class has the corresponding metadata. Otherwise + * should return an empty array. + * @param clazz the Class to read the metadata from + * @return the notification information + * @throws InvalidMetadataException in the case of invalid metadata + */ + ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException; +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java new file mode 100644 index 00000000000..3ba7aeeb0ab --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java @@ -0,0 +1,51 @@ +/* + * 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.jmx.export.metadata; + +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.util.StringUtils; + +/** + * Utility methods for converting Spring JMX metadata into their plain JMX equivalents. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class JmxMetadataUtils { + + /** + * Convert the supplied {@link ManagedNotification} into the corresponding + * {@link javax.management.modelmbean.ModelMBeanNotificationInfo}. + */ + public static ModelMBeanNotificationInfo convertToModelMBeanNotificationInfo(ManagedNotification notificationInfo) { + String name = notificationInfo.getName(); + if (!StringUtils.hasText(name)) { + throw new IllegalArgumentException("Must specify notification name"); + } + + String[] notifTypes = notificationInfo.getNotificationTypes(); + if (notifTypes == null || notifTypes.length == 0) { + throw new IllegalArgumentException("Must specify at least one notification type"); + } + + String description = notificationInfo.getDescription(); + return new ModelMBeanNotificationInfo(notifTypes, name, description); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java new file mode 100644 index 00000000000..2831ac92c30 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java @@ -0,0 +1,70 @@ +/* + * 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.jmx.export.metadata; + +/** + * Metadata that indicates to expose a given bean property as JMX attribute. + * Only valid when used on a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedAttribute extends AbstractJmxAttribute { + + public static final ManagedAttribute EMPTY = new ManagedAttribute(); + + + private Object defaultValue; + + private String persistPolicy; + + private int persistPeriod = -1; + + + /** + * Set the default value of this attribute. + */ + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Return the default value of this attribute. + */ + public Object getDefaultValue() { + return this.defaultValue; + } + + public void setPersistPolicy(String persistPolicy) { + this.persistPolicy = persistPolicy; + } + + public String getPersistPolicy() { + return this.persistPolicy; + } + + public void setPersistPeriod(int persistPeriod) { + this.persistPeriod = persistPeriod; + } + + public int getPersistPeriod() { + return this.persistPeriod; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java new file mode 100644 index 00000000000..d60e66363af --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java @@ -0,0 +1,86 @@ +/* + * 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.jmx.export.metadata; + +import org.springframework.util.StringUtils; + +/** + * Metadata that indicates a JMX notification emitted by a bean. + * + * @author Rob Harrop + * @since 2.0 + */ +public class ManagedNotification { + + private String[] notificationTypes; + + private String name; + + private String description; + + + /** + * Set a single notification type, or a list of notification types + * as comma-delimited String. + */ + public void setNotificationType(String notificationType) { + this.notificationTypes = StringUtils.commaDelimitedListToStringArray(notificationType); + } + + /** + * Set a list of notification types. + */ + public void setNotificationTypes(String[] notificationTypes) { + this.notificationTypes = notificationTypes; + } + + /** + * Return the list of notification types. + */ + public String[] getNotificationTypes() { + return this.notificationTypes; + } + + /** + * Set the name of this notification. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return the name of this notification. + */ + public String getName() { + return this.name; + } + + /** + * Set a description for this notification. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Return a description for this notification. + */ + public String getDescription() { + return this.description; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java new file mode 100644 index 00000000000..dfb59db2780 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java @@ -0,0 +1,30 @@ +/* + * 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.jmx.export.metadata; + +/** + * Metadata that indicates to expose a given method as JMX operation. + * Only valid when used on a method that is not a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedOperation extends AbstractJmxAttribute { + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java new file mode 100644 index 00000000000..b9074a51615 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java @@ -0,0 +1,77 @@ +/* + * 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.jmx.export.metadata; + +/** + * Metadata about JMX operation parameters. + * Used in conjunction with a {@link ManagedOperation} attribute. + * + * @author Rob Harrop + * @since 1.2 + */ +public class ManagedOperationParameter { + + private int index = 0; + + private String name = ""; + + private String description = ""; + + + /** + * Set the index of this parameter in the operation signature. + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Return the index of this parameter in the operation signature. + */ + public int getIndex() { + return this.index; + } + + /** + * Set the name of this parameter in the operation signature. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return the name of this parameter in the operation signature. + */ + public String getName() { + return this.name; + } + + /** + * Set a description for this parameter. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Return a description for this parameter. + */ + public String getDescription() { + return this.description; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java new file mode 100644 index 00000000000..4445c197e79 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java @@ -0,0 +1,109 @@ +/* + * 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.jmx.export.metadata; + +/** + * Metadata indicating that instances of an annotated class + * are to be registered with a JMX server. + * Only valid when used on a Class. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.naming.MetadataNamingStrategy + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedResource extends AbstractJmxAttribute { + + private String objectName; + + private boolean log = false; + + private String logFile; + + private String persistPolicy; + + private int persistPeriod = -1; + + private String persistName; + + private String persistLocation; + + + /** + * Set the JMX ObjectName of this managed resource. + */ + public void setObjectName(String objectName) { + this.objectName = objectName; + } + + /** + * Return the JMX ObjectName of this managed resource. + */ + public String getObjectName() { + return this.objectName; + } + + public void setLog(boolean log) { + this.log = log; + } + + public boolean isLog() { + return this.log; + } + + public void setLogFile(String logFile) { + this.logFile = logFile; + } + + public String getLogFile() { + return this.logFile; + } + + public void setPersistPolicy(String persistPolicy) { + this.persistPolicy = persistPolicy; + } + + public String getPersistPolicy() { + return this.persistPolicy; + } + + public void setPersistPeriod(int persistPeriod) { + this.persistPeriod = persistPeriod; + } + + public int getPersistPeriod() { + return this.persistPeriod; + } + + public void setPersistName(String persistName) { + this.persistName = persistName; + } + + public String getPersistName() { + return this.persistName; + } + + public void setPersistLocation(String persistLocation) { + this.persistLocation = persistLocation; + } + + public String getPersistLocation() { + return this.persistLocation; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/package.html new file mode 100644 index 00000000000..4836736670e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/metadata/package.html @@ -0,0 +1,8 @@ + + + +Provides generic JMX metadata classes and basic support for reading +JMX metadata in a provider-agnostic manner. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java new file mode 100644 index 00000000000..1dfa78fd2df --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java @@ -0,0 +1,58 @@ +/* + * 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.jmx.export.naming; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * An implementation of the ObjectNamingStrategy interface that + * creates a name based on the the identity of a given instance. + * + *

The resulting ObjectName will be in the form + * package:class=class name,hashCode=identity hash (in hex) + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public class IdentityNamingStrategy implements ObjectNamingStrategy { + + public static final String TYPE_KEY = "type"; + + public static final String HASH_CODE_KEY = "hashCode"; + + + /** + * Returns an instance of ObjectName based on the identity + * of the managed resource. + */ + public ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException { + String domain = ClassUtils.getPackageName(managedBean.getClass()); + Hashtable keys = new Hashtable(); + keys.put(TYPE_KEY, ClassUtils.getShortName(managedBean.getClass())); + keys.put(HASH_CODE_KEY, ObjectUtils.getIdentityHexString(managedBean)); + return ObjectNameManager.getInstance(domain, keys); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java new file mode 100644 index 00000000000..cd70b6b67e0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.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.jmx.export.naming; + +import java.io.IOException; +import java.util.Properties; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.CollectionUtils; + +/** + * ObjectNamingStrategy implementation that builds + * ObjectName instances from the key used in the + * "beans" map passed to MBeanExporter. + * + *

Can also check object name mappings, given as Properties + * or as mappingLocations of properties files. The key used + * to look up is the key used in MBeanExporter's "beans" map. + * If no mapping is found for a given key, the key itself is used to + * build an ObjectName. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setMappings + * @see #setMappingLocation + * @see #setMappingLocations + * @see org.springframework.jmx.export.MBeanExporter#setBeans + */ +public class KeyNamingStrategy implements ObjectNamingStrategy, InitializingBean { + + /** + * Log instance for this class. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Stores the mappings of bean key to ObjectName. + */ + private Properties mappings; + + /** + * Stores the Resources containing properties that should be loaded + * into the final merged set of Properties used for ObjectName + * resolution. + */ + private Resource[] mappingLocations; + + /** + * Stores the result of merging the mappings Properties + * with the the properties stored in the resources defined by mappingLocations. + */ + private Properties mergedMappings; + + + /** + * Set local properties, containing object name mappings, e.g. via + * the "props" tag in XML bean definitions. These can be considered + * defaults, to be overridden by properties loaded from files. + */ + public void setMappings(Properties mappings) { + this.mappings = mappings; + } + + /** + * Set a location of a properties file to be loaded, + * containing object name mappings. + */ + public void setMappingLocation(Resource location) { + this.mappingLocations = new Resource[]{location}; + } + + /** + * Set location of properties files to be loaded, + * containing object name mappings. + */ + public void setMappingLocations(Resource[] mappingLocations) { + this.mappingLocations = mappingLocations; + } + + + /** + * Merges the Properties configured in the mappings and + * mappingLocations into the final Properties instance + * used for ObjectName resolution. + * @throws IOException + */ + public void afterPropertiesSet() throws IOException { + this.mergedMappings = new Properties(); + + CollectionUtils.mergePropertiesIntoMap(this.mappings, this.mergedMappings); + + if (this.mappingLocations != null) { + for (int i = 0; i < this.mappingLocations.length; i++) { + Resource location = this.mappingLocations[i]; + if (logger.isInfoEnabled()) { + logger.info("Loading JMX object name mappings file from " + location); + } + PropertiesLoaderUtils.fillProperties(this.mergedMappings, location); + } + } + } + + + /** + * Attempts to retrieve the ObjectName via the given key, trying to + * find a mapped value in the mappings first. + */ + public ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException { + String objectName = null; + if (this.mergedMappings != null) { + objectName = this.mergedMappings.getProperty(beanKey); + } + if (objectName == null) { + objectName = beanKey; + } + return ObjectNameManager.getInstance(objectName); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java new file mode 100644 index 00000000000..23d6e048e46 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java @@ -0,0 +1,136 @@ +/* + * 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.jmx.export.naming; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.export.metadata.ManagedResource; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * An implementation of the {@link ObjectNamingStrategy} interface + * that reads the ObjectName from the source-level metadata. + * Falls back to the bean key (bean name) if no ObjectName + * can be found in source-level metadata. + * + *

Uses the {@link JmxAttributeSource} strategy interface, so that + * metadata can be read using any supported implementation. Out of the box, + * two strategies are included: + *

    + *
  • AttributesJmxAttributeSource, for Commons Attributes + *
  • AnnotationJmxAttributeSource, for JDK 1.5+ annotations + *
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see ObjectNamingStrategy + */ +public class MetadataNamingStrategy implements ObjectNamingStrategy, InitializingBean { + + /** + * The JmxAttributeSource implementation to use for reading metadata. + */ + private JmxAttributeSource attributeSource; + + private String defaultDomain; + + + /** + * Create a new MetadataNamingStrategy which needs to be + * configured through the {@link #setAttributeSource} method. + */ + public MetadataNamingStrategy() { + } + + /** + * Create a new MetadataNamingStrategy for the given + * JmxAttributeSource. + * @param attributeSource the JmxAttributeSource to use + */ + public MetadataNamingStrategy(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + + /** + * Set the implementation of the JmxAttributeSource interface to use + * when reading the source-level metadata. + */ + public void setAttributeSource(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + /** + * Specify the default domain to be used for generating ObjectNames + * when no source-level metadata has been specified. + *

The default is to use the domain specified in the bean name + * (if the bean name follows the JMX ObjectName syntax); else, + * the package name of the managed bean class. + */ + public void setDefaultDomain(String defaultDomain) { + this.defaultDomain = defaultDomain; + } + + public void afterPropertiesSet() { + if (this.attributeSource == null) { + throw new IllegalArgumentException("Property 'attributeSource' is required"); + } + } + + + /** + * Reads the ObjectName from the source-level metadata associated + * with the managed resource's Class. + */ + public ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException { + Class managedClass = AopUtils.getTargetClass(managedBean); + ManagedResource mr = this.attributeSource.getManagedResource(managedClass); + + // Check that an object name has been specified. + if (mr != null && StringUtils.hasText(mr.getObjectName())) { + return ObjectNameManager.getInstance(mr.getObjectName()); + } + else { + try { + return ObjectNameManager.getInstance(beanKey); + } + catch (MalformedObjectNameException ex) { + String domain = this.defaultDomain; + if (domain == null) { + domain = ClassUtils.getPackageName(managedClass); + } + Hashtable properties = new Hashtable(); + properties.put("type", ClassUtils.getShortName(managedClass)); + properties.put("name", beanKey); + return ObjectNameManager.getInstance(domain, properties); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java new file mode 100644 index 00000000000..ba0bbdc5fba --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java @@ -0,0 +1,46 @@ +/* + * 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.jmx.export.naming; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +/** + * Strategy interface that encapsulates the creation of ObjectName instances. + * + *

Used by the MBeanExporter to obtain ObjectNames + * when registering beans. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + * @see javax.management.ObjectName + */ +public interface ObjectNamingStrategy { + + /** + * Obtain an ObjectName for the supplied bean. + * @param managedBean the bean that will be exposed under the + * returned ObjectName + * @param beanKey the key associated with this bean in the beans map + * passed to the MBeanExporter + * @return the ObjectName instance + * @throws MalformedObjectNameException if the resulting ObjectName is invalid + */ + ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java new file mode 100644 index 00000000000..4c564ecb808 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java @@ -0,0 +1,43 @@ +/* + * 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.jmx.export.naming; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +/** + * Interface that allows infrastructure components to provide their own + * ObjectNames to the MBeanExporter. + * + *

Note: This interface is mainly intended for internal usage. + * + * @author Rob Harrop + * @since 1.2.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface SelfNaming { + + /** + * Return the ObjectName for the implementing object. + * @throws MalformedObjectNameException if thrown by the ObjectName constructor + * @see javax.management.ObjectName#ObjectName(String) + * @see javax.management.ObjectName#getInstance(String) + * @see org.springframework.jmx.support.ObjectNameManager#getInstance(String) + */ + ObjectName getObjectName() throws MalformedObjectNameException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/package.html new file mode 100644 index 00000000000..2765213c69a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/naming/package.html @@ -0,0 +1,8 @@ + + + +Provides a strategy for ObjectName creation. Used by MBeanExporter +to determine the JMX names to use for exported Spring-managed beans. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java new file mode 100644 index 00000000000..b4c50d237e2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java @@ -0,0 +1,117 @@ +/* + * 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.jmx.export.notification; + +import javax.management.AttributeChangeNotification; +import javax.management.MBeanException; +import javax.management.Notification; +import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBean; +import javax.management.modelmbean.ModelMBeanNotificationBroadcaster; + +import org.springframework.util.Assert; + +/** + * {@link NotificationPublisher} implementation that uses the infrastructure + * provided by the {@link ModelMBean} interface to track + * {@link javax.management.NotificationListener javax.management.NotificationListeners} + * and send {@link Notification Notifications} to those listeners. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + * @see javax.management.modelmbean.ModelMBeanNotificationBroadcaster + * @see NotificationPublisherAware + */ +public class ModelMBeanNotificationPublisher implements NotificationPublisher { + + /** + * The {@link ModelMBean} instance wrapping the managed resource into which this + * NotificationPublisher will be injected. + */ + private final ModelMBeanNotificationBroadcaster modelMBean; + + /** + * The {@link ObjectName} associated with the {@link ModelMBean modelMBean}. + */ + private final ObjectName objectName; + + /** + * The managed resource associated with the {@link ModelMBean modelMBean}. + */ + private final Object managedResource; + + + /** + * Create a new instance of the {@link ModelMBeanNotificationPublisher} class + * that will publish all {@link javax.management.Notification Notifications} + * to the supplied {@link ModelMBean}. + * @param modelMBean the target {@link ModelMBean}; must not be null + * @param objectName the {@link ObjectName} of the source {@link ModelMBean} + * @param managedResource the managed resource exposed by the supplied {@link ModelMBean} + * @throws IllegalArgumentException if any of the parameters is null + */ + public ModelMBeanNotificationPublisher( + ModelMBeanNotificationBroadcaster modelMBean, ObjectName objectName, Object managedResource) { + + Assert.notNull(modelMBean, "'modelMBean' must not be null"); + Assert.notNull(objectName, "'objectName' must not be null"); + Assert.notNull(managedResource, "'managedResource' must not be null"); + this.modelMBean = modelMBean; + this.objectName = objectName; + this.managedResource = managedResource; + } + + + /** + * Send the supplied {@link Notification} using the wrapped + * {@link ModelMBean} instance. + * @param notification the {@link Notification} to be sent + * @throws IllegalArgumentException if the supplied notification is null + * @throws UnableToSendNotificationException if the supplied notification could not be sent + */ + public void sendNotification(Notification notification) { + Assert.notNull(notification, "Notification must not be null"); + replaceNotificationSourceIfNecessary(notification); + try { + if (notification instanceof AttributeChangeNotification) { + this.modelMBean.sendAttributeChangeNotification((AttributeChangeNotification) notification); + } + else { + this.modelMBean.sendNotification(notification); + } + } + catch (MBeanException ex) { + throw new UnableToSendNotificationException("Unable to send notification [" + notification + "]", ex); + } + } + + /** + * From the {@link Notification javadoc}: + *

"It is strongly recommended that notification senders use the object name + * rather than a reference to the MBean object as the source." + * @param notification the {@link Notification} whose + * {@link javax.management.Notification#getSource()} might need massaging + */ + private void replaceNotificationSourceIfNecessary(Notification notification) { + if (notification.getSource() == null || notification.getSource().equals(this.managedResource)) { + notification.setSource(this.objectName); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.java new file mode 100644 index 00000000000..a2fd23186a0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.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.jmx.export.notification; + +import javax.management.Notification; + +/** + * Simple interface allowing Spring-managed MBeans to publish JMX notifications + * without being aware of how those notifications are being transmitted to the + * {@link javax.management.MBeanServer}. + * + *

Managed resources can access a NotificationPublisher by + * implementing the {@link NotificationPublisherAware} interface. After a particular + * managed resource instance is registered with the {@link javax.management.MBeanServer}, + * Spring will inject a NotificationPublisher instance into it if that + * resource implements the {@link NotificationPublisherAware} inteface. + * + *

Each managed resource instance will have a distinct instance of a + * NotificationPublisher implementation. This instance will keep + * track of all the {@link javax.management.NotificationListener NotificationListeners} + * registered for a particular mananaged resource. + * + *

Any existing, user-defined MBeans should use standard JMX APIs for notification + * publication; this interface is intended for use only by Spring-created MBeans. + * + * @author Rob Harrop + * @since 2.0 + * @see NotificationPublisherAware + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface NotificationPublisher { + + /** + * Send the specified {@link javax.management.Notification} to all registered + * {@link javax.management.NotificationListener NotificationListeners}. + * Managed resources are not responsible for managing the list + * of registered {@link javax.management.NotificationListener NotificationListeners}; + * that is performed automatically. + * @param notification the JMX Notification to send + * @throws UnableToSendNotificationException if sending failed + */ + void sendNotification(Notification notification) throws UnableToSendNotificationException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java new file mode 100644 index 00000000000..8971c000426 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java @@ -0,0 +1,46 @@ +/* + * 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.jmx.export.notification; + +/** + * Interface to be implemented by any Spring-managed resource that is to be + * registered with an {@link javax.management.MBeanServer} and wishes to send + * JMX {@link javax.management.Notification javax.management.Notifications}. + * + *

Provides Spring-created managed resources with a {@link NotificationPublisher} + * as soon as they are registered with the {@link javax.management.MBeanServer}. + * + *

NOTE: This interface only applies to simple Spring-managed + * beans which happen to get exported through Spring's + * {@link org.springframework.jmx.export.MBeanExporter}. + * It does not apply to any non-exported beans; neither does it apply + * to standard MBeans exported by Spring. For standard JMX MBeans, + * consider implementing the {@link javax.management.modelmbean.ModelMBeanNotificationBroadcaster} + * interface (or implementing a full {@link javax.management.modelmbean.ModelMBean}). + * + * @author Rob Harrop + * @since 2.0 + * @see NotificationPublisher + */ +public interface NotificationPublisherAware { + + /** + * Set the {@link NotificationPublisher} instance for the current managed resource instance. + */ + void setNotificationPublisher(NotificationPublisher notificationPublisher); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.java b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.java new file mode 100644 index 00000000000..251904c3d49 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.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.jmx.export.notification; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when a JMX {@link javax.management.Notification} is unable to be sent. + * + *

The root cause of just why a particular notification could not be sent + * will typically be available via the {@link #getCause()} property. + * + * @author Rob Harrop + * @since 2.0 + * @see NotificationPublisher + */ +public class UnableToSendNotificationException extends JmxException { + + /** + * Create a new instance of the {@link UnableToSendNotificationException} + * class with the specified error message. + * @param msg the detail message + */ + public UnableToSendNotificationException(String msg) { + super(msg); + } + + /** + * Create a new instance of the {@link UnableToSendNotificationException} + * with the specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public UnableToSendNotificationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/package.html new file mode 100644 index 00000000000..f75cac0809c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/notification/package.html @@ -0,0 +1,8 @@ + + + +Provides supporting infrastructure to allow Spring-created MBeans +to send JMX notifications. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/export/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/export/package.html new file mode 100644 index 00000000000..c9d46710f09 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/export/package.html @@ -0,0 +1,8 @@ + + + +This package provides declarative creation and registration of +Spring-managed beans as JMX MBeans. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/package.html new file mode 100644 index 00000000000..c1dafc87170 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/package.html @@ -0,0 +1,8 @@ + + + +This package contains Spring's JMX support, which includes registration of +Spring-managed beans as JMX MBeans as well as access to remote JMX MBeans. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java new file mode 100644 index 00000000000..62e35043595 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java @@ -0,0 +1,214 @@ +/* + * 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.jmx.support; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.JmxException; + +/** + * FactoryBean that creates a JSR-160 JMXConnectorServer, + * optionally registers it with the MBeanServer and then starts it. + * + *

The JMXConnectorServer can be started in a separate thread by setting the + * threaded property to true. You can configure this thread to be a + * daemon thread by setting the daemon property to true. + * + *

The JMXConnectorServer is correctly shutdown when an instance of this + * class is destroyed on shutdown of the containing ApplicationContext. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see FactoryBean + * @see JMXConnectorServer + * @see MBeanServer + */ +public class ConnectorServerFactoryBean extends MBeanRegistrationSupport + implements FactoryBean, InitializingBean, DisposableBean { + + /** The default service URL */ + public static final String DEFAULT_SERVICE_URL = "service:jmx:jmxmp://localhost:9875"; + + + private String serviceUrl = DEFAULT_SERVICE_URL; + + private Map environment; + + private ObjectName objectName; + + private boolean threaded = false; + + private boolean daemon = false; + + private JMXConnectorServer connectorServer; + + + /** + * Set the service URL for the JMXConnectorServer. + */ + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + /** + * Set the environment properties used to construct the JMXConnectorServer + * as java.util.Properties (String key/value pairs). + */ + public void setEnvironment(Properties environment) { + this.environment = environment; + } + + /** + * Set the environment properties used to construct the JMXConnector + * as a Map of String keys and arbitrary Object values. + */ + public void setEnvironmentMap(Map environment) { + this.environment = environment; + } + + /** + * Set the ObjectName used to register the JMXConnectorServer + * itself with the MBeanServer, as ObjectName instance + * or as String. + * @throws MalformedObjectNameException if the ObjectName is malformed + */ + public void setObjectName(Object objectName) throws MalformedObjectNameException { + this.objectName = ObjectNameManager.getInstance(objectName); + } + + /** + * Set whether the JMXConnectorServer should be started in a separate thread. + */ + public void setThreaded(boolean threaded) { + this.threaded = threaded; + } + + /** + * Set whether any threads started for the JMXConnectorServer should be + * started as daemon threads. + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + + /** + * Start the connector server. If the threaded flag is set to true, + * the JMXConnectorServer will be started in a separate thread. + * If the daemon flag is set to true, that thread will be + * started as a daemon thread. + * @throws JMException if a problem occured when registering the connector server + * with the MBeanServer + * @throws IOException if there is a problem starting the connector server + */ + public void afterPropertiesSet() throws JMException, IOException { + if (this.server == null) { + this.server = JmxUtils.locateMBeanServer(); + } + + // Create the JMX service URL. + JMXServiceURL url = new JMXServiceURL(this.serviceUrl); + + // Create the connector server now. + this.connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, this.environment, this.server); + + // Do we want to register the connector with the MBean server? + if (this.objectName != null) { + doRegister(this.connectorServer, this.objectName); + } + + try { + if (this.threaded) { + // Start the connector server asynchronously (in a separate thread). + Thread connectorThread = new Thread() { + public void run() { + try { + connectorServer.start(); + } + catch (IOException ex) { + throw new JmxException("Could not start JMX connector server after delay", ex); + } + } + }; + + connectorThread.setName("JMX Connector Thread [" + this.serviceUrl + "]"); + connectorThread.setDaemon(this.daemon); + connectorThread.start(); + } + else { + // Start the connector server in the same thread. + this.connectorServer.start(); + } + + if (logger.isInfoEnabled()) { + logger.info("JMX connector server started: " + this.connectorServer); + } + } + + catch (IOException ex) { + // Unregister the connector server if startup failed. + unregisterBeans(); + throw ex; + } + } + + + public Object getObject() { + return this.connectorServer; + } + + public Class getObjectType() { + return (this.connectorServer != null ? this.connectorServer.getClass() : JMXConnectorServer.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Stop the JMXConnectorServer managed by an instance of this class. + * Automatically called on ApplicationContext shutdown. + * @throws IOException if there is an error stopping the connector server + */ + public void destroy() throws IOException { + if (logger.isInfoEnabled()) { + logger.info("Stopping JMX connector server: " + this.connectorServer); + } + try { + this.connectorServer.stop(); + } + finally { + unregisterBeans(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/JmxUtils.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/JmxUtils.java new file mode 100644 index 00000000000..75530f2706f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/JmxUtils.java @@ -0,0 +1,339 @@ +/* + * 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.jmx.support; + +import java.beans.PropertyDescriptor; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.List; + +import javax.management.DynamicMBean; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MXBean; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.JdkVersion; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Collection of generic utility methods to support Spring JMX. + * Includes a convenient method to locate an MBeanServer. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #locateMBeanServer + */ +public abstract class JmxUtils { + + /** + * The key used when extending an existing {@link ObjectName} with the + * identity hash code of its corresponding managed resource. + */ + public static final String IDENTITY_OBJECT_NAME_KEY = "identity"; + + /** + * Suffix used to identify an MBean interface. + */ + private static final String MBEAN_SUFFIX = "MBean"; + + /** + * Suffix used to identify a Java 6 MXBean interface. + */ + private static final String MXBEAN_SUFFIX = "MXBean"; + + private static final String MXBEAN_ANNOTATION_CLASS_NAME = "javax.management.MXBean"; + + + private static final boolean mxBeanAnnotationAvailable = + ClassUtils.isPresent(MXBEAN_ANNOTATION_CLASS_NAME, JmxUtils.class.getClassLoader()); + + private static final Log logger = LogFactory.getLog(JmxUtils.class); + + + /** + * Attempt to find a locally running MBeanServer. Fails if no + * MBeanServer can be found. Logs a warning if more than one + * MBeanServer found, returning the first one from the list. + * @return the MBeanServer if found + * @throws org.springframework.jmx.MBeanServerNotFoundException + * if no MBeanServer could be found + * @see javax.management.MBeanServerFactory#findMBeanServer + */ + public static MBeanServer locateMBeanServer() throws MBeanServerNotFoundException { + return locateMBeanServer(null); + } + + /** + * Attempt to find a locally running MBeanServer. Fails if no + * MBeanServer can be found. Logs a warning if more than one + * MBeanServer found, returning the first one from the list. + * @param agentId the agent identifier of the MBeanServer to retrieve. + * If this parameter is null, all registered MBeanServers are + * considered. + * @return the MBeanServer if found + * @throws org.springframework.jmx.MBeanServerNotFoundException + * if no MBeanServer could be found + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public static MBeanServer locateMBeanServer(String agentId) throws MBeanServerNotFoundException { + List servers = MBeanServerFactory.findMBeanServer(agentId); + + MBeanServer server = null; + if (servers != null && servers.size() > 0) { + // Check to see if an MBeanServer is registered. + if (servers.size() > 1 && logger.isWarnEnabled()) { + logger.warn("Found more than one MBeanServer instance" + + (agentId != null ? " with agent id [" + agentId + "]" : "") + + ". Returning first from list."); + } + server = (MBeanServer) servers.get(0); + } + + if (server == null && agentId == null && JdkVersion.isAtLeastJava15()) { + // Attempt to load the PlatformMBeanServer. + try { + server = ManagementFactory.getPlatformMBeanServer(); + } + catch (SecurityException ex) { + throw new MBeanServerNotFoundException("No specific MBeanServer found, " + + "and not allowed to obtain the Java platform MBeanServer", ex); + } + } + + if (server == null) { + throw new MBeanServerNotFoundException( + "Unable to locate an MBeanServer instance" + + (agentId != null ? " with agent id [" + agentId + "]" : "")); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found MBeanServer: " + server); + } + return server; + } + + /** + * Convert an array of MBeanParameterInfo into an array of + * Class instances corresponding to the parameters. + * @param paramInfo the JMX parameter info + * @return the parameter types as classes + * @throws ClassNotFoundException if a parameter type could not be resolved + */ + public static Class[] parameterInfoToTypes(MBeanParameterInfo[] paramInfo) throws ClassNotFoundException { + return parameterInfoToTypes(paramInfo, ClassUtils.getDefaultClassLoader()); + } + + /** + * Convert an array of MBeanParameterInfo into an array of + * Class instances corresponding to the parameters. + * @param paramInfo the JMX parameter info + * @param classLoader the ClassLoader to use for loading parameter types + * @return the parameter types as classes + * @throws ClassNotFoundException if a parameter type could not be resolved + */ + public static Class[] parameterInfoToTypes(MBeanParameterInfo[] paramInfo, ClassLoader classLoader) + throws ClassNotFoundException { + + Class[] types = null; + if (paramInfo != null && paramInfo.length > 0) { + types = new Class[paramInfo.length]; + for (int x = 0; x < paramInfo.length; x++) { + types[x] = ClassUtils.forName(paramInfo[x].getType(), classLoader); + } + } + return types; + } + + /** + * Create a String[] representing the argument signature of a + * method. Each element in the array is the fully qualified class name + * of the corresponding argument in the methods signature. + * @param method the method to build an argument signature for + * @return the signature as array of argument types + */ + public static String[] getMethodSignature(Method method) { + Class[] types = method.getParameterTypes(); + String[] signature = new String[types.length]; + for (int x = 0; x < types.length; x++) { + signature[x] = types[x].getName(); + } + return signature; + } + + /** + * Return the JMX attribute name to use for the given JavaBeans property. + *

When using strict casing, a JavaBean property with a getter method + * such as getFoo() translates to an attribute called + * Foo. With strict casing disabled, getFoo() + * would translate to just foo. + * @param property the JavaBeans property descriptor + * @param useStrictCasing whether to use strict casing + * @return the JMX attribute name to use + */ + public static String getAttributeName(PropertyDescriptor property, boolean useStrictCasing) { + if (useStrictCasing) { + return StringUtils.capitalize(property.getName()); + } + else { + return property.getName(); + } + } + + /** + * Append an additional key/value pair to an existing {@link ObjectName} with the key being + * the static value identity and the value being the identity hash code of the + * managed resource being exposed on the supplied {@link ObjectName}. This can be used to + * provide a unique {@link ObjectName} for each distinct instance of a particular bean or + * class. Useful when generating {@link ObjectName ObjectNames} at runtime for a set of + * managed resources based on the template value supplied by a + * {@link org.springframework.jmx.export.naming.ObjectNamingStrategy}. + * @param objectName the original JMX ObjectName + * @param managedResource the MBean instance + * @return an ObjectName with the MBean identity added + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see org.springframework.util.ObjectUtils#getIdentityHexString(Object) + */ + public static ObjectName appendIdentityToObjectName(ObjectName objectName, Object managedResource) + throws MalformedObjectNameException { + + Hashtable keyProperties = objectName.getKeyPropertyList(); + keyProperties.put(IDENTITY_OBJECT_NAME_KEY, ObjectUtils.getIdentityHexString(managedResource)); + return ObjectNameManager.getInstance(objectName.getDomain(), keyProperties); + } + + /** + * Return the class or interface to expose for the given bean. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + *

This implementation returns the superclass for a CGLIB proxy and + * the class of the given bean else (for a JDK proxy or a plain bean class). + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see org.springframework.util.ClassUtils#getUserClass(Object) + */ + public static Class getClassToExpose(Object managedBean) { + return ClassUtils.getUserClass(managedBean); + } + + /** + * Return the class or interface to expose for the given bean class. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + *

This implementation returns the superclass for a CGLIB proxy and + * the class of the given bean else (for a JDK proxy or a plain bean class). + * @param beanClass the bean class (might be an AOP proxy class) + * @return the bean class to expose + * @see org.springframework.util.ClassUtils#getUserClass(Class) + */ + public static Class getClassToExpose(Class beanClass) { + return ClassUtils.getUserClass(beanClass); + } + + /** + * Determine whether the given bean class qualifies as an MBean as-is. + *

This implementation checks for {@link javax.management.DynamicMBean} + * classes as well as classes with corresponding "*MBean" interface + * (Standard MBeans) or corresponding "*MXBean" interface (Java 6 MXBeans). + * @param beanClass the bean class to analyze + * @return whether the class qualifies as an MBean + * @see org.springframework.jmx.export.MBeanExporter#isMBean(Class) + */ + public static boolean isMBean(Class beanClass) { + return (beanClass != null && + (DynamicMBean.class.isAssignableFrom(beanClass) || + (getMBeanInterface(beanClass) != null || getMXBeanInterface(beanClass) != null))); + } + + /** + * Return the Standard MBean interface for the given class, if any + * (that is, an interface whose name matches the class name of the + * given class but with suffix "MBean"). + * @param clazz the class to check + * @return the Standard MBean interface for the given class + */ + public static Class getMBeanInterface(Class clazz) { + if (clazz.getSuperclass() == null) { + return null; + } + String mbeanInterfaceName = clazz.getName() + MBEAN_SUFFIX; + Class[] implementedInterfaces = clazz.getInterfaces(); + for (int x = 0; x < implementedInterfaces.length; x++) { + Class iface = implementedInterfaces[x]; + if (iface.getName().equals(mbeanInterfaceName)) { + return iface; + } + } + return getMBeanInterface(clazz.getSuperclass()); + } + + /** + * Return the Java 6 MXBean interface exists for the given class, if any + * (that is, an interface whose name ends with "MXBean" and/or + * carries an appropriate MXBean annotation). + * @param clazz the class to check + * @return whether there is an MXBean interface for the given class + */ + public static Class getMXBeanInterface(Class clazz) { + if (clazz.getSuperclass() == null) { + return null; + } + Class[] implementedInterfaces = clazz.getInterfaces(); + for (int x = 0; x < implementedInterfaces.length; x++) { + Class iface = implementedInterfaces[x]; + boolean isMxBean = iface.getName().endsWith(MXBEAN_SUFFIX); + if (mxBeanAnnotationAvailable) { + Boolean checkResult = MXBeanChecker.hasMXBeanAnnotation(iface); + if (checkResult != null) { + isMxBean = checkResult.booleanValue(); + } + } + if (isMxBean) { + return iface; + } + } + return getMXBeanInterface(clazz.getSuperclass()); + } + + + /** + * Inner class to avoid a Java 6 dependency. + */ + private static class MXBeanChecker { + + public static Boolean hasMXBeanAnnotation(Class iface) { + MXBean mxBean = (MXBean) iface.getAnnotation(MXBean.class); + if (mxBean != null) { + return Boolean.valueOf(mxBean.value()); + } + else { + return null; + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java new file mode 100644 index 00000000000..8f8de2a10c4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.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.jmx.support; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Constants; + +/** + * Provides supporting infrastructure for registering MBeans with an + * {@link javax.management.MBeanServer}. The behavior when encountering + * an existing MBean at a given {@link ObjectName} is fully configurable + * allowing for flexible registration settings. + * + *

All registered MBeans are tracked and can be unregistered by calling + * the #{@link #unregisterBeans()} method. + * + *

Sub-classes can receive notifications when an MBean is registered or + * unregistered by overriding the {@link #onRegister(ObjectName)} and + * {@link #onUnregister(ObjectName)} methods respectively. + * + *

By default, the registration process will fail if attempting to + * register an MBean using a {@link javax.management.ObjectName} that is + * already used. + * + *

By setting the {@link #setRegistrationBehaviorName(String) registrationBehaviorName} + * property to REGISTRATION_IGNORE_EXISTING the registration process + * will simply ignore existing MBeans leaving them registered. This is useful in settings + * where multiple applications want to share a common MBean in a shared {@link MBeanServer}. + * + *

Setting {@link #setRegistrationBehaviorName(String) registrationBehaviorName} property + * to REGISTRATION_REPLACE_EXISTING will cause existing MBeans to be replaced + * during registration if necessary. This is useful in situations where you can't guarantee + * the state of your {@link MBeanServer}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see #setServer + * @see #setRegistrationBehaviorName + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MBeanRegistrationSupport { + + /** + * Constant indicating that registration should fail when + * attempting to register an MBean under a name that already exists. + *

This is the default registration behavior. + */ + public static final int REGISTRATION_FAIL_ON_EXISTING = 0; + + /** + * Constant indicating that registration should ignore the affected MBean + * when attempting to register an MBean under a name that already exists. + */ + public static final int REGISTRATION_IGNORE_EXISTING = 1; + + /** + * Constant indicating that registration should replace the affected MBean + * when attempting to register an MBean under a name that already exists. + */ + public static final int REGISTRATION_REPLACE_EXISTING = 2; + + + /** + * Constants for this class. + */ + private static final Constants constants = new Constants(MBeanRegistrationSupport.class); + + /** + * Log instance for this class. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The MBeanServer instance being used to register beans. + */ + protected MBeanServer server; + + /** + * The beans that have been registered by this exporter. + */ + protected final Set registeredBeans = new LinkedHashSet(); + + /** + * The action take when registering an MBean and finding that it already exists. + * By default an exception is raised. + */ + private int registrationBehavior = REGISTRATION_FAIL_ON_EXISTING; + + + /** + * Specify the MBeanServer instance with which all beans should + * be registered. The MBeanExporter will attempt to locate an + * existing MBeanServer if none is supplied. + */ + public void setServer(MBeanServer server) { + this.server = server; + } + + /** + * Return the MBeanServer that the beans will be registered with. + */ + public final MBeanServer getServer() { + return this.server; + } + + /** + * Set the registration behavior by the name of the corresponding constant, + * e.g. "REGISTRATION_IGNORE_EXISTING". + * @see #setRegistrationBehavior + * @see #REGISTRATION_FAIL_ON_EXISTING + * @see #REGISTRATION_IGNORE_EXISTING + * @see #REGISTRATION_REPLACE_EXISTING + */ + public void setRegistrationBehaviorName(String registrationBehavior) { + setRegistrationBehavior(constants.asNumber(registrationBehavior).intValue()); + } + + /** + * Specify what action should be taken when attempting to register an MBean + * under an {@link javax.management.ObjectName} that already exists. + *

Default is REGISTRATION_FAIL_ON_EXISTING. + * @see #setRegistrationBehaviorName(String) + * @see #REGISTRATION_FAIL_ON_EXISTING + * @see #REGISTRATION_IGNORE_EXISTING + * @see #REGISTRATION_REPLACE_EXISTING + */ + public void setRegistrationBehavior(int registrationBehavior) { + this.registrationBehavior = registrationBehavior; + } + + + /** + * Actually register the MBean with the server. The behavior when encountering + * an existing MBean can be configured using the {@link #setRegistrationBehavior(int)} + * and {@link #setRegistrationBehaviorName(String)} methods. + * @param mbean the MBean instance + * @param objectName the suggested ObjectName for the MBean + * @throws JMException if the registration failed + */ + protected void doRegister(Object mbean, ObjectName objectName) throws JMException { + ObjectInstance registeredBean = null; + try { + registeredBean = this.server.registerMBean(mbean, objectName); + } + catch (InstanceAlreadyExistsException ex) { + if (this.registrationBehavior == REGISTRATION_IGNORE_EXISTING) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring existing MBean at [" + objectName + "]"); + } + } + else if (this.registrationBehavior == REGISTRATION_REPLACE_EXISTING) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Replacing existing MBean at [" + objectName + "]"); + } + this.server.unregisterMBean(objectName); + registeredBean = this.server.registerMBean(mbean, objectName); + } + catch (InstanceNotFoundException ex2) { + logger.error("Unable to replace existing MBean at [" + objectName + "]", ex2); + throw ex; + } + } + else { + throw ex; + } + } + + // Track registration and notify listeners. + ObjectName actualObjectName = (registeredBean != null ? registeredBean.getObjectName() : null); + if (actualObjectName == null) { + actualObjectName = objectName; + } + this.registeredBeans.add(actualObjectName); + onRegister(actualObjectName, mbean); + } + + /** + * Unregisters all beans that have been registered by an instance of this class. + */ + protected void unregisterBeans() { + for (Iterator it = this.registeredBeans.iterator(); it.hasNext();) { + doUnregister((ObjectName) it.next()); + } + this.registeredBeans.clear(); + } + + /** + * Actually unregister the specified MBean from the server. + * @param objectName the suggested ObjectName for the MBean + */ + protected void doUnregister(ObjectName objectName) { + try { + // MBean might already have been unregistered by an external process. + if (this.server.isRegistered(objectName)) { + this.server.unregisterMBean(objectName); + onUnregister(objectName); + } + else { + if (logger.isWarnEnabled()) { + logger.warn("Could not unregister MBean [" + objectName + "] as said MBean " + + "is not registered (perhaps already unregistered by an external process)"); + } + } + } + catch (JMException ex) { + if (logger.isErrorEnabled()) { + logger.error("Could not unregister MBean [" + objectName + "]", ex); + } + } + } + + /** + * Return the {@link ObjectName ObjectNames} of all registered beans. + */ + protected final ObjectName[] getRegisteredObjectNames() { + return (ObjectName[]) this.registeredBeans.toArray(new ObjectName[this.registeredBeans.size()]); + } + + + /** + * Called when an MBean is registered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is registered. + *

The default implementation delegates to {@link #onRegister(ObjectName)}. + * @param objectName the actual {@link ObjectName} that the MBean was registered with + * @param mbean the registered MBean instance + */ + protected void onRegister(ObjectName objectName, Object mbean) { + onRegister(objectName); + } + + /** + * Called when an MBean is registered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is registered. + *

The default implementation is empty. Can be overridden in subclasses. + * @param objectName the actual {@link ObjectName} that the MBean was registered with + */ + protected void onRegister(ObjectName objectName) { + } + + /** + * Called when an MBean is unregistered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is unregistered. + *

The default implementation is empty. Can be overridden in subclasses. + * @param objectName the {@link ObjectName} that the MBean was registered with + */ + protected void onUnregister(ObjectName objectName) { + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java new file mode 100644 index 00000000000..3916315967d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java @@ -0,0 +1,201 @@ +/* + * 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.jmx.support; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Map; +import java.util.Properties; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.AbstractLazyCreationTargetSource; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ClassUtils; + +/** + * FactoryBean that creates a JMX 1.2 MBeanServerConnection + * to a remote MBeanServer exposed via a JMXServerConnector. + * Exposes the MBeanServer for bean references. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanServerFactoryBean + * @see ConnectorServerFactoryBean + * @see org.springframework.jmx.access.MBeanClientInterceptor#setServer + * @see org.springframework.jmx.access.NotificationListenerRegistrar#setServer + */ +public class MBeanServerConnectionFactoryBean + implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean { + + private JMXServiceURL serviceUrl; + + private Map environment; + + private boolean connectOnStartup = true; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private JMXConnector connector; + + private MBeanServerConnection connection; + + private JMXConnectorLazyInitTargetSource connectorTargetSource; + + + /** + * Set the service URL of the remote MBeanServer. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Set the environment properties used to construct the JMXConnector + * as java.util.Properties (String key/value pairs). + */ + public void setEnvironment(Properties environment) { + this.environment = environment; + } + + /** + * Set the environment properties used to construct the JMXConnector + * as a Map of String keys and arbitrary Object values. + */ + public void setEnvironmentMap(Map environment) { + this.environment = environment; + } + + /** + * Set whether to connect to the server on startup. Default is "true". + *

Can be turned off to allow for late start of the JMX server. + * In this case, the JMX connector will be fetched on first access. + */ + public void setConnectOnStartup(boolean connectOnStartup) { + this.connectOnStartup = connectOnStartup; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + /** + * Creates a JMXConnector for the given settings + * and exposes the associated MBeanServerConnection. + */ + public void afterPropertiesSet() throws IOException { + if (this.serviceUrl == null) { + throw new IllegalArgumentException("Property 'serviceUrl' is required"); + } + + if (this.connectOnStartup) { + connect(); + } + else { + createLazyConnection(); + } + } + + /** + * Connects to the remote MBeanServer using the configured service URL and + * environment properties. + */ + private void connect() throws IOException { + this.connector = JMXConnectorFactory.connect(this.serviceUrl, this.environment); + this.connection = this.connector.getMBeanServerConnection(); + } + + /** + * Creates lazy proxies for the JMXConnector and MBeanServerConnection + */ + private void createLazyConnection() { + this.connectorTargetSource = new JMXConnectorLazyInitTargetSource(); + TargetSource connectionTargetSource = new MBeanServerConnectionLazyInitTargetSource(); + + this.connector = (JMXConnector) + new ProxyFactory(JMXConnector.class, this.connectorTargetSource).getProxy(this.beanClassLoader); + this.connection = (MBeanServerConnection) + new ProxyFactory(MBeanServerConnection.class, connectionTargetSource).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.connection; + } + + public Class getObjectType() { + return (this.connection != null ? this.connection.getClass() : MBeanServerConnection.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Closes the underlying JMXConnector. + */ + public void destroy() throws IOException { + if (this.connectorTargetSource == null || this.connectorTargetSource.isInitialized()) { + this.connector.close(); + } + } + + + /** + * Lazily creates a JMXConnector using the configured service URL + * and environment properties. + * @see MBeanServerConnectionFactoryBean#setServiceUrl(String) + * @see MBeanServerConnectionFactoryBean#setEnvironment(java.util.Properties) + */ + private class JMXConnectorLazyInitTargetSource extends AbstractLazyCreationTargetSource { + + protected Object createObject() throws Exception { + return JMXConnectorFactory.connect(serviceUrl, environment); + } + + public Class getTargetClass() { + return JMXConnector.class; + } + } + + + /** + * Lazily creates an MBeanServerConnection. + */ + private class MBeanServerConnectionLazyInitTargetSource extends AbstractLazyCreationTargetSource { + + protected Object createObject() throws Exception { + return connector.getMBeanServerConnection(); + } + + public Class getTargetClass() { + return MBeanServerConnection.class; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java new file mode 100644 index 00000000000..c2cc24569db --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java @@ -0,0 +1,203 @@ +/* + * 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.jmx.support; + +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; + +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.jmx.MBeanServerNotFoundException; + +/** + * FactoryBean that obtains an {@link javax.management.MBeanServer} reference + * through the standard JMX 1.2 {@link javax.management.MBeanServerFactory} + * API (which is available on JDK 1.5 or as part of a JMX 1.2 provider). + * Exposes the MBeanServer for bean references. + * + *

By default, MBeanServerFactoryBean will always create + * a new MBeanServer even if one is already running. To have + * the MBeanServerFactoryBean attempt to locate a running + * MBeanServer first, set the value of the + * "locateExistingServerIfPossible" property to "true". + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setLocateExistingServerIfPossible + * @see #locateMBeanServer + * @see javax.management.MBeanServer + * @see javax.management.MBeanServerFactory#findMBeanServer + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#newMBeanServer + * @see MBeanServerConnectionFactoryBean + * @see ConnectorServerFactoryBean + */ +public class MBeanServerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean locateExistingServerIfPossible = false; + + private String agentId; + + private String defaultDomain; + + private boolean registerWithFactory = true; + + private MBeanServer server; + + private boolean newlyRegistered = false; + + + /** + * Set whether or not the MBeanServerFactoryBean should attempt + * to locate a running MBeanServer before creating one. + *

Default is false. + */ + public void setLocateExistingServerIfPossible(boolean locateExistingServerIfPossible) { + this.locateExistingServerIfPossible = locateExistingServerIfPossible; + } + + /** + * Set the agent id of the MBeanServer to locate. + *

Default is none. If specified, this will result in an + * automatic attempt being made to locate the attendant MBeanServer, + * and (importantly) if said MBeanServer cannot be located no + * attempt will be made to create a new MBeanServer (and an + * MBeanServerNotFoundException will be thrown at resolution time). + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * Set the default domain to be used by the MBeanServer, + * to be passed to MBeanServerFactory.createMBeanServer() + * or MBeanServerFactory.findMBeanServer(). + *

Default is none. + * @see javax.management.MBeanServerFactory#createMBeanServer(String) + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setDefaultDomain(String defaultDomain) { + this.defaultDomain = defaultDomain; + } + + /** + * Set whether to register the MBeanServer with the + * MBeanServerFactory, making it available through + * MBeanServerFactory.findMBeanServer(). + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#findMBeanServer + */ + public void setRegisterWithFactory(boolean registerWithFactory) { + this.registerWithFactory = registerWithFactory; + } + + + /** + * Creates the MBeanServer instance. + */ + public void afterPropertiesSet() throws MBeanServerNotFoundException { + // Try to locate existing MBeanServer, if desired. + if (this.locateExistingServerIfPossible || this.agentId != null) { + try { + this.server = locateMBeanServer(this.agentId); + } + catch (MBeanServerNotFoundException ex) { + // If agentId was specified, we were only supposed to locate that + // specific MBeanServer; so let's bail if we can't find it. + if (this.agentId != null) { + throw ex; + } + logger.info("No existing MBeanServer found - creating new one"); + } + } + + // Create a new MBeanServer and register it, if desired. + if (this.server == null) { + this.server = createMBeanServer(this.defaultDomain, this.registerWithFactory); + this.newlyRegistered = this.registerWithFactory; + } + } + + /** + * Attempt to locate an existing MBeanServer. + * Called if locateExistingServerIfPossible is set to true. + *

The default implementation attempts to find an MBeanServer using + * a standard lookup. Subclasses may override to add additional location logic. + * @param agentId the agent identifier of the MBeanServer to retrieve. + * If this parameter is null, all registered MBeanServers are + * considered. + * @return the MBeanServer if found + * @throws org.springframework.jmx.MBeanServerNotFoundException + * if no MBeanServer could be found + * @see #setLocateExistingServerIfPossible + * @see JmxUtils#locateMBeanServer(String) + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + protected MBeanServer locateMBeanServer(String agentId) throws MBeanServerNotFoundException { + return JmxUtils.locateMBeanServer(agentId); + } + + /** + * Create a new MBeanServer instance and register it with the + * MBeanServerFactory, if desired. + * @param defaultDomain the default domain, or null if none + * @param registerWithFactory whether to register the MBeanServer + * with the MBeanServerFactory + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#newMBeanServer + */ + protected MBeanServer createMBeanServer(String defaultDomain, boolean registerWithFactory) { + if (registerWithFactory) { + return MBeanServerFactory.createMBeanServer(defaultDomain); + } + else { + return MBeanServerFactory.newMBeanServer(defaultDomain); + } + } + + + public Object getObject() { + return this.server; + } + + public Class getObjectType() { + return (this.server != null ? this.server.getClass() : MBeanServer.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Unregisters the MBeanServer instance, if necessary. + */ + public void destroy() { + if (this.newlyRegistered) { + MBeanServerFactory.releaseMBeanServer(this.server); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java new file mode 100644 index 00000000000..6c2d7de7228 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java @@ -0,0 +1,178 @@ +/* + * 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.jmx.support; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.management.MalformedObjectNameException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import org.springframework.util.ObjectUtils; + +/** + * Helper class that aggregates a {@link javax.management.NotificationListener}, + * a {@link javax.management.NotificationFilter}, and an arbitrary handback + * object, as well as the names of MBeans from which the listener wishes + * to receive {@link javax.management.Notification Notifications}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see org.springframework.jmx.export.NotificationListenerBean + * @see org.springframework.jmx.access.NotificationListenerRegistrar + */ +public class NotificationListenerHolder { + + private NotificationListener notificationListener; + + private NotificationFilter notificationFilter; + + private Object handback; + + protected Set mappedObjectNames; + + + /** + * Set the {@link javax.management.NotificationListener}. + */ + public void setNotificationListener(NotificationListener notificationListener) { + this.notificationListener = notificationListener; + } + + /** + * Get the {@link javax.management.NotificationListener}. + */ + public NotificationListener getNotificationListener() { + return this.notificationListener; + } + + /** + * Set the {@link javax.management.NotificationFilter} associated + * with the encapsulated {@link #getNotificationFilter() NotificationFilter}. + *

May be null. + */ + public void setNotificationFilter(NotificationFilter notificationFilter) { + this.notificationFilter = notificationFilter; + } + + /** + * Return the {@link javax.management.NotificationFilter} associated + * with the encapsulated {@link #getNotificationFilter() NotificationFilter}. + *

May be null. + */ + public NotificationFilter getNotificationFilter() { + return this.notificationFilter; + } + + /** + * Set the (arbitrary) object that will be 'handed back' as-is by an + * {@link javax.management.NotificationBroadcaster} when notifying + * any {@link javax.management.NotificationListener}. + * @param handback the handback object (can be null) + * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, Object) + */ + public void setHandback(Object handback) { + this.handback = handback; + } + + /** + * Return the (arbitrary) object that will be 'handed back' as-is by an + * {@link javax.management.NotificationBroadcaster} when notifying + * any {@link javax.management.NotificationListener}. + * @return the handback object (may be null) + * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, Object) + */ + public Object getHandback() { + return this.handback; + } + + /** + * Set the {@link javax.management.ObjectName}-style name of the single MBean + * that the encapsulated {@link #getNotificationFilter() NotificationFilter} + * will be registered with to listen for {@link javax.management.Notification Notifications}. + * Can be specified as ObjectName instance or as String. + * @see #setMappedObjectNames + */ + public void setMappedObjectName(Object mappedObjectName) { + setMappedObjectNames(mappedObjectName != null ? new Object[] {mappedObjectName} : null); + } + + /** + * Set an array of {@link javax.management.ObjectName}-style names of the MBeans + * that the encapsulated {@link #getNotificationFilter() NotificationFilter} + * will be registered with to listen for {@link javax.management.Notification Notifications}. + * Can be specified as ObjectName instances or as Strings. + * @see #setMappedObjectName + */ + public void setMappedObjectNames(Object[] mappedObjectNames) { + if (mappedObjectNames != null) { + this.mappedObjectNames = new LinkedHashSet(mappedObjectNames.length); + for (int i = 0; i < mappedObjectNames.length; i++) { + this.mappedObjectNames.add(mappedObjectNames[i]); + } + } + else { + this.mappedObjectNames = null; + } + } + + /** + * Return the list of {@link javax.management.ObjectName} String representations for + * which the encapsulated {@link #getNotificationFilter() NotificationFilter} will + * be registered as a listener for {@link javax.management.Notification Notifications}. + * @throws MalformedObjectNameException if an ObjectName is malformed + */ + public ObjectName[] getResolvedObjectNames() throws MalformedObjectNameException { + if (this.mappedObjectNames == null) { + return null; + } + ObjectName[] resolved = new ObjectName[this.mappedObjectNames.size()]; + int i = 0; + for (Iterator it = this.mappedObjectNames.iterator(); it.hasNext();) { + resolved[i] = ObjectNameManager.getInstance(it.next()); + i++; + } + return resolved; + } + + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof NotificationListenerHolder)) { + return false; + } + NotificationListenerHolder otherNlh = (NotificationListenerHolder) other; + return (ObjectUtils.nullSafeEquals(this.notificationListener, otherNlh.notificationListener) && + ObjectUtils.nullSafeEquals(this.notificationFilter, otherNlh.notificationFilter) && + ObjectUtils.nullSafeEquals(this.handback, otherNlh.handback) && + ObjectUtils.nullSafeEquals(this.mappedObjectNames, otherNlh.mappedObjectNames)); + } + + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(this.notificationListener); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.notificationFilter); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.handback); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.mappedObjectNames); + return hashCode; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java new file mode 100644 index 00000000000..cd149f60355 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java @@ -0,0 +1,124 @@ +/* + * 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.jmx.support; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.util.ClassUtils; + +/** + * Helper class for the creation of {@link javax.management.ObjectName} instances. + * + *

ObjectName instances will be cached on JMX 1.2, + * whereas they will be recreated for each request on JMX 1.0. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see javax.management.ObjectName#getInstance(String) + */ +public class ObjectNameManager { + + // Determine whether the JMX 1.2 ObjectName.getInstance method is available. + private static final boolean getInstanceAvailable = + ClassUtils.hasMethod(ObjectName.class, "getInstance", new Class[] {String.class}); + + + /** + * Retrieve the ObjectName instance corresponding to the supplied name. + * @param objectName the ObjectName in ObjectName or + * String format + * @return the ObjectName instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String) + * @see ObjectName#getInstance(String) + */ + public static ObjectName getInstance(Object objectName) throws MalformedObjectNameException { + if (objectName instanceof ObjectName) { + return (ObjectName) objectName; + } + if (!(objectName instanceof String)) { + throw new MalformedObjectNameException("Invalid ObjectName value type [" + + objectName.getClass().getName() + "]: only ObjectName and String supported."); + } + return getInstance((String) objectName); + } + + /** + * Retrieve the ObjectName instance corresponding to the supplied name. + * @param objectName the ObjectName in String format + * @return the ObjectName instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String) + * @see ObjectName#getInstance(String) + */ + public static ObjectName getInstance(String objectName) throws MalformedObjectNameException { + if (getInstanceAvailable) { + return ObjectName.getInstance(objectName); + } + else { + return new ObjectName(objectName); + } + } + + /** + * Retrieve an ObjectName instance for the specified domain and a + * single property with the supplied key and value. + * @param domainName the domain name for the ObjectName + * @param key the key for the single property in the ObjectName + * @param value the value for the single property in the ObjectName + * @return the ObjectName instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String, String, String) + * @see ObjectName#getInstance(String, String, String) + */ + public static ObjectName getInstance(String domainName, String key, String value) + throws MalformedObjectNameException { + + if (getInstanceAvailable) { + return ObjectName.getInstance(domainName, key, value); + } + else { + return new ObjectName(domainName, key, value); + } + } + + /** + * Retrieve an ObjectName instance with the specified domain name + * and the supplied key/name properties. + * @param domainName the domain name for the ObjectName + * @param properties the properties for the ObjectName + * @return the ObjectName instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String, java.util.Hashtable) + * @see ObjectName#getInstance(String, java.util.Hashtable) + */ + public static ObjectName getInstance(String domainName, Hashtable properties) + throws MalformedObjectNameException { + + if (getInstanceAvailable) { + return ObjectName.getInstance(domainName, properties); + } + else { + return new ObjectName(domainName, properties); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicJndiMBeanServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicJndiMBeanServerFactoryBean.java new file mode 100644 index 00000000000..56833fc3ec3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicJndiMBeanServerFactoryBean.java @@ -0,0 +1,133 @@ +/* + * 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.jmx.support; + +import java.lang.reflect.InvocationTargetException; + +import javax.management.MBeanServer; +import javax.naming.NamingException; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jndi.JndiLocatorSupport; + +/** + * FactoryBean that obtains a specified WebLogic {@link javax.management.MBeanServer} + * reference through a WebLogic MBeanHome obtained via a JNDI lookup. + * By default, the server's local MBeanHome will be obtained. + * + *

Exposes the MBeanServer for bean references. + * This FactoryBean is a direct alternative to {@link MBeanServerFactoryBean}, + * which uses standard JMX 1.2 API to access the platform's MBeanServer. + * + *

Note: There is also a more general {@link WebLogicMBeanServerFactoryBean} + * for accessing any specified WebLogic MBeanServer, + * potentially a remote one. + * + *

NOTE: This class is only intended for use with WebLogic 8.1. + * On WebLogic 9.x, simply obtain the MBeanServer directly from the JNDI location + * "java:comp/env/jmx/runtime", for example through the following configuration: + * + *

+ * <bean class="org.springframework.jndi.JndiObjectFactoryBean">
+ *   <property name="jndiName" value="java:comp/env/jmx/runtime"/>
+ * </bean>
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2.6 + * @see weblogic.management.MBeanHome#LOCAL_JNDI_NAME + * @see weblogic.management.MBeanHome#getMBeanServer() + * @see javax.management.MBeanServer + * @see MBeanServerFactoryBean + * @see WebLogicMBeanServerFactoryBean + */ +public class WebLogicJndiMBeanServerFactoryBean extends JndiLocatorSupport + implements FactoryBean, InitializingBean { + + private static final String WEBLOGIC_MBEAN_HOME_CLASS = "weblogic.management.MBeanHome"; + + private static final String LOCAL_JNDI_NAME_FIELD = "LOCAL_JNDI_NAME"; + + private static final String GET_MBEAN_SERVER_METHOD = "getMBeanServer"; + + + private String mbeanHomeName; + + private MBeanServer mbeanServer; + + + /** + * Specify the JNDI name of the WebLogic MBeanHome object to use + * for creating the JMX MBeanServer reference. + *

Default is MBeanHome.LOCAL_JNDI_NAME + * @see weblogic.management.MBeanHome#LOCAL_JNDI_NAME + */ + public void setMbeanHomeName(String mbeanHomeName) { + this.mbeanHomeName = mbeanHomeName; + } + + + public void afterPropertiesSet() throws MBeanServerNotFoundException { + try { + String jndiName = this.mbeanHomeName; + if (jndiName == null) { + /* + * jndiName = MBeanHome.LOCAL_JNDI_NAME; + */ + Class mbeanHomeClass = getClass().getClassLoader().loadClass(WEBLOGIC_MBEAN_HOME_CLASS); + jndiName = (String) mbeanHomeClass.getField(LOCAL_JNDI_NAME_FIELD).get(null); + } + Object mbeanHome = lookup(jndiName); + + /* + * this.mbeanServer = mbeanHome.getMBeanServer(); + */ + this.mbeanServer = (MBeanServer) + mbeanHome.getClass().getMethod(GET_MBEAN_SERVER_METHOD, null).invoke(mbeanHome, null); + } + catch (NamingException ex) { + throw new MBeanServerNotFoundException("Could not find WebLogic's MBeanHome object in JNDI", ex); + } + catch (ClassNotFoundException ex) { + throw new MBeanServerNotFoundException("Could not find WebLogic's MBeanHome class", ex); + } + catch (InvocationTargetException ex) { + throw new MBeanServerNotFoundException( + "WebLogic's MBeanHome.getMBeanServer method failed", ex.getTargetException()); + } + catch (Exception ex) { + throw new MBeanServerNotFoundException( + "Could not access WebLogic's MBeanHome/getMBeanServer method", ex); + } + } + + + public Object getObject() { + return this.mbeanServer; + } + + public Class getObjectType() { + return (this.mbeanServer != null ? this.mbeanServer.getClass() : MBeanServer.class); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicMBeanServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicMBeanServerFactoryBean.java new file mode 100644 index 00000000000..f610456130c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebLogicMBeanServerFactoryBean.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.jmx.support; + +import java.lang.reflect.InvocationTargetException; + +import javax.management.MBeanServer; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; + +/** + * FactoryBean that obtains a specified WebLogic {@link javax.management.MBeanServer} + * reference through WebLogic's proprietary Helper / + * MBeanHome API, which is available on WebLogic 6.1 and higher. + * + *

Exposes the MBeanServer for bean references. + * This FactoryBean is a direct alternative to {@link MBeanServerFactoryBean}, + * which uses standard JMX 1.2 API to access the platform's MBeanServer. + * + *

Note: There is also a {@link WebLogicJndiMBeanServerFactoryBean} for + * accessing the WebLogic MBeanServer instance through a WebLogic + * MBeanHome obtained via a JNDI lookup, typical a local one. + * + *

NOTE: This class is only intended for use with WebLogic up to 8.1. + * On WebLogic 9.x, simply obtain the MBeanServer directly from the JNDI location + * "java:comp/env/jmx/runtime", for example through the following configuration: + * + *

+ * <bean class="org.springframework.jndi.JndiObjectFactoryBean">
+ *   <property name="jndiName" value="java:comp/env/jmx/runtime"/>
+ * </bean>
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see weblogic.management.Helper#getMBeanHome(String, String, String, String) + * @see weblogic.management.MBeanHome#getMBeanServer() + * @see javax.management.MBeanServer + * @see MBeanServerFactoryBean + * @see WebLogicJndiMBeanServerFactoryBean + */ +public class WebLogicMBeanServerFactoryBean implements FactoryBean, InitializingBean { + + private static final String WEBLOGIC_JMX_HELPER_CLASS = "weblogic.management.Helper"; + + private static final String GET_MBEAN_HOME_METHOD = "getMBeanHome"; + + private static final String GET_MBEAN_SERVER_METHOD = "getMBeanServer"; + + + private String username = "weblogic"; + + private String password = "weblogic"; + + private String serverUrl = "t3://localhost:7001"; + + private String serverName = "server"; + + private MBeanServer mbeanServer; + + + /** + * Set the username to use for retrieving the WebLogic MBeanServer. + * Default is "weblogic". + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Set the password to use for retrieving the WebLogic MBeanServer. + * Default is "weblogic". + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Set the server URL to use for retrieving the WebLogic MBeanServer. + * Default is "t3://localhost:7001". + */ + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + /** + * Set the server name to use for retrieving the WebLogic MBeanServer. + * Default is "server". + */ + public void setServerName(String serverName) { + this.serverName = serverName; + } + + + public void afterPropertiesSet() throws MBeanServerNotFoundException { + try { + /* + * MBeanHome mbeanHome = Helper.getMBeanHome(this.username, this.password, this.serverUrl, this.serverName); + */ + Class helperClass = getClass().getClassLoader().loadClass(WEBLOGIC_JMX_HELPER_CLASS); + Class[] argTypes = new Class[] {String.class, String.class, String.class, String.class}; + Object[] args = new Object[] {this.username, this.password, this.serverUrl, this.serverName}; + Object mbeanHome = helperClass.getMethod(GET_MBEAN_HOME_METHOD, argTypes).invoke(null, args); + + /* + * this.mbeanServer = mbeanHome.getMBeanServer(); + */ + this.mbeanServer = (MBeanServer) + mbeanHome.getClass().getMethod(GET_MBEAN_SERVER_METHOD, null).invoke(mbeanHome, null); + } + catch (ClassNotFoundException ex) { + throw new MBeanServerNotFoundException("Could not find WebLogic's JMX Helper class", ex); + } + catch (InvocationTargetException ex) { + throw new MBeanServerNotFoundException( + "WebLogic's JMX Helper.getMBeanHome/getMBeanServer method failed", ex.getTargetException()); + } + catch (Exception ex) { + throw new MBeanServerNotFoundException( + "Could not access WebLogic's JMX Helper.getMBeanHome/getMBeanServer method", ex); + } + } + + + public Object getObject() { + return this.mbeanServer; + } + + public Class getObjectType() { + return (this.mbeanServer != null ? this.mbeanServer.getClass() : MBeanServer.class); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java new file mode 100644 index 00000000000..02fbce38bd7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java @@ -0,0 +1,94 @@ +/* + * 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.jmx.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.management.MBeanServer; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; + +/** + * FactoryBean that obtains a WebSphere {@link javax.management.MBeanServer} + * reference through WebSphere's proprietary AdminServiceFactory API, + * available on WebSphere 5.1 and higher. + * + *

Exposes the MBeanServer for bean references. + * This FactoryBean is a direct alternative to {@link MBeanServerFactoryBean}, + * which uses standard JMX 1.2 API to access the platform's MBeanServer. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0.3 + * @see com.ibm.websphere.management.AdminServiceFactory#getMBeanFactory() + * @see com.ibm.websphere.management.MBeanFactory#getMBeanServer() + * @see javax.management.MBeanServer + * @see MBeanServerFactoryBean + */ +public class WebSphereMBeanServerFactoryBean implements FactoryBean, InitializingBean { + + private static final String ADMIN_SERVICE_FACTORY_CLASS = "com.ibm.websphere.management.AdminServiceFactory"; + + private static final String GET_MBEAN_FACTORY_METHOD = "getMBeanFactory"; + + private static final String GET_MBEAN_SERVER_METHOD = "getMBeanServer"; + + + private MBeanServer mbeanServer; + + + public void afterPropertiesSet() throws MBeanServerNotFoundException { + try { + /* + * this.mbeanServer = AdminServiceFactory.getMBeanFactory().getMBeanServer(); + */ + Class adminServiceClass = getClass().getClassLoader().loadClass(ADMIN_SERVICE_FACTORY_CLASS); + Method getMBeanFactoryMethod = adminServiceClass.getMethod(GET_MBEAN_FACTORY_METHOD, new Class[0]); + Object mbeanFactory = getMBeanFactoryMethod.invoke(null, new Object[0]); + Method getMBeanServerMethod = mbeanFactory.getClass().getMethod(GET_MBEAN_SERVER_METHOD, new Class[0]); + this.mbeanServer = (MBeanServer) getMBeanServerMethod.invoke(mbeanFactory, new Object[0]); + } + catch (ClassNotFoundException ex) { + throw new MBeanServerNotFoundException("Could not find WebSphere's AdminServiceFactory class", ex); + } + catch (InvocationTargetException ex) { + throw new MBeanServerNotFoundException( + "WebSphere's AdminServiceFactory.getMBeanFactory/getMBeanServer method failed", ex.getTargetException()); + } + catch (Exception ex) { + throw new MBeanServerNotFoundException( + "Could not access WebSphere's AdminServiceFactory.getMBeanFactory/getMBeanServer method", ex); + } + } + + + public Object getObject() { + return this.mbeanServer; + } + + public Class getObjectType() { + return (this.mbeanServer != null ? this.mbeanServer.getClass() : MBeanServer.class); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jmx/support/package.html b/org.springframework.context/src/main/java/org/springframework/jmx/support/package.html new file mode 100644 index 00000000000..a10df9760a5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jmx/support/package.html @@ -0,0 +1,8 @@ + + + +Contains support classes for connecting to local and remote MBeanServers +and for exposing an MBeanServer to remote clients. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiAccessor.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiAccessor.java new file mode 100644 index 00000000000..8ea3e2c9188 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiAccessor.java @@ -0,0 +1,75 @@ +/* + * 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.jndi; + +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Convenient superclass for JNDI accessors, providing "jndiTemplate" + * and "jndiEnvironment" bean properties. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + */ +public class JndiAccessor { + + /** + * Logger, available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + private JndiTemplate jndiTemplate = new JndiTemplate(); + + + /** + * Set the JNDI template to use for JNDI lookups. + *

You can also specify JNDI environment settings via "jndiEnvironment". + * @see #setJndiEnvironment + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiTemplate = (jndiTemplate != null ? jndiTemplate : new JndiTemplate()); + } + + /** + * Return the JNDI template to use for JNDI lookups. + */ + public JndiTemplate getJndiTemplate() { + return this.jndiTemplate; + } + + /** + * Set the JNDI environment to use for JNDI lookups. + *

Creates a JndiTemplate with the given environment settings. + * @see #setJndiTemplate + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiTemplate = new JndiTemplate(jndiEnvironment); + } + + /** + * Return the JNDI environment to use for JNDI lookups. + */ + public Properties getJndiEnvironment() { + return this.jndiTemplate.getEnvironment(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiCallback.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiCallback.java new file mode 100644 index 00000000000..3d4ee57e75e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiCallback.java @@ -0,0 +1,49 @@ +/* + * 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.jndi; + +import javax.naming.Context; +import javax.naming.NamingException; + +/** + * Callback interface to be implemented by classes that need to perform an + * operation (such as a lookup) in a JNDI context. This callback approach + * is valuable in simplifying error handling, which is performed by the + * JndiTemplate class. This is a similar to JdbcTemplate's approach. + * + *

Note that there is hardly any need to implement this callback + * interface, as JndiTemplate provides all usual JNDI operations via + * convenience methods. + * + * @see JndiTemplate + * @see org.springframework.jdbc.core.JdbcTemplate + * @author Rod Johnson + */ +public interface JndiCallback { + + /** + * Do something with the given JNDI context. + * Implementations don't need to worry about error handling + * or cleanup, as the JndiTemplate class will handle this. + * @param ctx the current JNDI context + * @throws NamingException if thrown by JNDI methods + * @return a result object, or null + */ + Object doInContext(Context ctx) throws NamingException; + +} + diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java new file mode 100644 index 00000000000..3dd50db3b3d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java @@ -0,0 +1,133 @@ +/* + * 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.jndi; + +import javax.naming.NamingException; + +import org.springframework.util.Assert; + +/** + * Convenient superclass for classes that can locate any number of JNDI objects. + * Derives from JndiAccessor to inherit the "jndiTemplate" and "jndiEnvironment" + * bean properties. + * + *

JNDI names may or may not include the "java:comp/env/" prefix expected + * by J2EE applications when accessing a locally mapped (ENC - Environmental + * Naming Context) resource. If it doesn't, the "java:comp/env/" prefix will + * be prepended if the "resourceRef" property is true (the default is + * false) and no other scheme (e.g. "java:") is given. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setResourceRef + */ +public abstract class JndiLocatorSupport extends JndiAccessor { + + /** JNDI prefix used in a J2EE container */ + public static final String CONTAINER_PREFIX = "java:comp/env/"; + + + private boolean resourceRef = false; + + + /** + * Set whether the lookup occurs in a J2EE container, i.e. if the prefix + * "java:comp/env/" needs to be added if the JNDI name doesn't already + * contain it. Default is "false". + *

Note: Will only get applied if no other scheme (e.g. "java:") is given. + */ + public void setResourceRef(boolean resourceRef) { + this.resourceRef = resourceRef; + } + + /** + * Return whether the lookup occurs in a J2EE container. + */ + public boolean isResourceRef() { + return this.resourceRef; + } + + + /** + * Perform an actual JNDI lookup for the given name via the JndiTemplate. + *

If the name doesn't begin with "java:comp/env/", this prefix is added + * if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @return the obtained object + * @throws NamingException if the JNDI lookup failed + * @see #setResourceRef + */ + protected Object lookup(String jndiName) throws NamingException { + return lookup(jndiName, null); + } + + /** + * Perform an actual JNDI lookup for the given name via the JndiTemplate. + *

If the name doesn't begin with "java:comp/env/", this prefix is added + * if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @param requiredType the required type of the object + * @return the obtained object + * @throws NamingException if the JNDI lookup failed + * @see #setResourceRef + */ + protected Object lookup(String jndiName, Class requiredType) throws NamingException { + Assert.notNull(jndiName, "'jndiName' must not be null"); + String convertedName = convertJndiName(jndiName); + Object jndiObject = null; + try { + jndiObject = getJndiTemplate().lookup(convertedName, requiredType); + } + catch (NamingException ex) { + if (!convertedName.equals(jndiName)) { + // Try fallback to originally specified name... + if (logger.isDebugEnabled()) { + logger.debug("Converted JNDI name [" + convertedName + + "] not found - trying original name [" + jndiName + "]. " + ex); + } + jndiObject = getJndiTemplate().lookup(jndiName, requiredType); + } + else { + throw ex; + } + } + if (logger.isDebugEnabled()) { + logger.debug("Located object with JNDI name [" + convertedName + "]"); + } + return jndiObject; + } + + /** + * Convert the given JNDI name into the actual JNDI name to use. + *

The default implementation applies the "java:comp/env/" prefix if + * "resourceRef" is "true" and no other scheme (e.g. "java:") is given. + * @param jndiName the original JNDI name + * @return the JNDI name to use + * @see #CONTAINER_PREFIX + * @see #setResourceRef + */ + protected String convertJndiName(String jndiName) { + // Prepend container prefix if not already specified and no other scheme given. + if (isResourceRef() && !jndiName.startsWith(CONTAINER_PREFIX) && jndiName.indexOf(':') == -1) { + jndiName = CONTAINER_PREFIX + jndiName; + } + return jndiName; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java new file mode 100644 index 00000000000..e4299b0a260 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java @@ -0,0 +1,44 @@ +/* + * 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.jndi; + +import javax.naming.NamingException; + +import org.springframework.core.NestedRuntimeException; + +/** + * RuntimeException to be thrown in case of JNDI lookup failures, + * in particular from code that does not declare JNDI's checked + * {@link javax.naming.NamingException}: for example, from Spring's + * {@link JndiObjectTargetSource}. + * + * @author Juergen Hoeller + * @since 2.0.3 + */ +public class JndiLookupFailureException extends NestedRuntimeException { + + /** + * Construct a new JndiLookupFailureException, + * wrapping the given JNDI NamingException. + * @param msg the detail message + * @param cause the NamingException root cause + */ + public JndiLookupFailureException(String msg, NamingException cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java new file mode 100644 index 00000000000..f4ae0344784 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java @@ -0,0 +1,335 @@ +/* + * 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.jndi; + +import java.lang.reflect.Method; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that looks up a + * JNDI object. Exposes the object found in JNDI for bean references, + * e.g. for data access object's "dataSource" property in case of a + * {@link javax.sql.DataSource}. + * + *

The typical usage will be to register this as singleton factory + * (e.g. for a certain JNDI-bound DataSource) in an application context, + * and give bean references to application services that need it. + * + *

The default behavior is to look up the JNDI object on startup and cache it. + * This can be customized through the "lookupOnStartup" and "cache" properties, + * using a {@link JndiObjectTargetSource} underneath. Note that you need to specify + * a "proxyInterface" in such a scenario, since the actual JNDI object type is not + * known in advance. + * + *

Of course, bean classes in a Spring environment may lookup e.g. a DataSource + * from JNDI themselves. This class simply enables central configuration of the + * JNDI name, and easy switching to non-JNDI alternatives. The latter is + * particularly convenient for test setups, reuse in standalone clients, etc. + * + *

Note that switching to e.g. DriverManagerDataSource is just a matter of + * configuration: Simply replace the definition of this FactoryBean with a + * {@link org.springframework.jdbc.datasource.DriverManagerDataSource} definition! + * + * @author Juergen Hoeller + * @since 22.05.2003 + * @see #setProxyInterface + * @see #setLookupOnStartup + * @see #setCache + * @see JndiObjectTargetSource + */ +public class JndiObjectFactoryBean extends JndiObjectLocator implements FactoryBean, BeanClassLoaderAware { + + private Class[] proxyInterfaces; + + private boolean lookupOnStartup = true; + + private boolean cache = true; + + private boolean exposeAccessContext = false; + + private Object defaultObject; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Object jndiObject; + + + /** + * Specify the proxy interface to use for the JNDI object. + *

Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false. + * Needs to be specified because the actual JNDI object type is not known + * in advance in case of a lazy lookup. + * @see #setProxyInterfaces + * @see #setLookupOnStartup + * @see #setCache + */ + public void setProxyInterface(Class proxyInterface) { + this.proxyInterfaces = new Class[] {proxyInterface}; + } + + /** + * Specify multiple proxy interfaces to use for the JNDI object. + *

Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false. + * Note that proxy interfaces will be autodetected from a specified "expectedType", + * if necessary. + * @see #setExpectedType + * @see #setLookupOnStartup + * @see #setCache + */ + public void setProxyInterfaces(Class[] proxyInterfaces) { + this.proxyInterfaces = proxyInterfaces; + } + + /** + * Set whether to look up the JNDI object on startup. Default is "true". + *

Can be turned off to allow for late availability of the JNDI object. + * In this case, the JNDI object will be fetched on first access. + *

For a lazy lookup, a proxy interface needs to be specified. + * @see #setProxyInterface + * @see #setCache + */ + public void setLookupOnStartup(boolean lookupOnStartup) { + this.lookupOnStartup = lookupOnStartup; + } + + /** + * Set whether to cache the JNDI object once it has been located. + * Default is "true". + *

Can be turned off to allow for hot redeployment of JNDI objects. + * In this case, the JNDI object will be fetched for each invocation. + *

For hot redeployment, a proxy interface needs to be specified. + * @see #setProxyInterface + * @see #setLookupOnStartup + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + /** + * Set whether to expose the JNDI environment context for all access to the target + * object, i.e. for all method invocations on the exposed object reference. + *

Default is "false", i.e. to only expose the JNDI context for object lookup. + * Switch this flag to "true" in order to expose the JNDI environment (including + * the authorization context) for each method invocation, as needed by WebLogic + * for JNDI-obtained factories (e.g. JDBC DataSource, JMS ConnectionFactory) + * with authorization requirements. + */ + public void setExposeAccessContext(boolean exposeAccessContext) { + this.exposeAccessContext = exposeAccessContext; + } + + /** + * Specify a default object to fall back to if the JNDI lookup fails. + * Default is none. + *

This can be an arbitrary bean reference or literal value. + * It is typically used for literal values in scenarios where the JNDI environment + * might define specific config settings but those are not required to be present. + *

Note: This is only supported for lookup on startup. + * @see #setLookupOnStartup + */ + public void setDefaultObject(Object defaultObject) { + this.defaultObject = defaultObject; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + /** + * Look up the JNDI object and store it. + */ + public void afterPropertiesSet() throws IllegalArgumentException, NamingException { + super.afterPropertiesSet(); + + if (this.proxyInterfaces != null || !this.lookupOnStartup || !this.cache || this.exposeAccessContext) { + // We need to create a proxy for this... + if (this.defaultObject != null) { + throw new IllegalArgumentException( + "'defaultObject' is not supported in combination with 'proxyInterface'"); + } + // We need a proxy and a JndiObjectTargetSource. + this.jndiObject = JndiObjectProxyFactory.createJndiObjectProxy(this); + } + else { + if (this.defaultObject != null && getExpectedType() != null && + !getExpectedType().isInstance(this.defaultObject)) { + throw new IllegalArgumentException("Default object [" + this.defaultObject + + "] of type [" + this.defaultObject.getClass().getName() + + "] is not of expected type [" + getExpectedType().getName() + "]"); + } + // Locate specified JNDI object. + this.jndiObject = lookupWithFallback(); + } + } + + /** + * Lookup variant that that returns the specified "defaultObject" + * (if any) in case of lookup failure. + * @return the located object, or the "defaultObject" as fallback + * @throws NamingException in case of lookup failure without fallback + * @see #setDefaultObject + */ + protected Object lookupWithFallback() throws NamingException { + ClassLoader originalClassLoader = ClassUtils.overrideThreadContextClassLoader(this.beanClassLoader); + try { + return lookup(); + } + catch (TypeMismatchNamingException ex) { + // Always let TypeMismatchNamingException through - + // we don't want to fall back to the defaultObject in this case. + throw ex; + } + catch (NamingException ex) { + if (this.defaultObject != null) { + if (logger.isDebugEnabled()) { + logger.debug("JNDI lookup failed - returning specified default object instead", ex); + } + else if (logger.isInfoEnabled()) { + logger.info("JNDI lookup failed - returning specified default object instead: " + ex); + } + return this.defaultObject; + } + throw ex; + } + finally { + if (originalClassLoader != null) { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + } + + + /** + * Return the singleton JNDI object. + */ + public Object getObject() { + return this.jndiObject; + } + + public Class getObjectType() { + if (this.proxyInterfaces != null) { + if (this.proxyInterfaces.length == 1) { + return this.proxyInterfaces[0]; + } + else if (this.proxyInterfaces.length > 1) { + return createCompositeInterface(this.proxyInterfaces); + } + } + if (this.jndiObject != null) { + return this.jndiObject.getClass(); + } + else { + return getExpectedType(); + } + } + + public boolean isSingleton() { + return true; + } + + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

The default implementation builds a JDK proxy class for the + * given interfaces. + * @param interfaces the interfaces to merge + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + protected Class createCompositeInterface(Class[] interfaces) { + return ClassUtils.createCompositeInterface(interfaces, this.beanClassLoader); + } + + + /** + * Inner class to just introduce an AOP dependency when actually creating a proxy. + */ + private static class JndiObjectProxyFactory { + + private static Object createJndiObjectProxy(JndiObjectFactoryBean jof) throws NamingException { + // Create a JndiObjectTargetSource that mirrors the JndiObjectFactoryBean's configuration. + JndiObjectTargetSource targetSource = new JndiObjectTargetSource(); + targetSource.setJndiTemplate(jof.getJndiTemplate()); + targetSource.setJndiName(jof.getJndiName()); + targetSource.setExpectedType(jof.getExpectedType()); + targetSource.setResourceRef(jof.isResourceRef()); + targetSource.setLookupOnStartup(jof.lookupOnStartup); + targetSource.setCache(jof.cache); + targetSource.afterPropertiesSet(); + + // Create a proxy with JndiObjectFactoryBean's proxy interface and the JndiObjectTargetSource. + ProxyFactory proxyFactory = new ProxyFactory(); + if (jof.proxyInterfaces != null) { + proxyFactory.setInterfaces(jof.proxyInterfaces); + } + else { + Class targetClass = targetSource.getTargetClass(); + if (targetClass == null) { + throw new IllegalStateException( + "Cannot deactivate 'lookupOnStartup' without specifying a 'proxyInterface' or 'expectedType'"); + } + proxyFactory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, jof.beanClassLoader)); + } + if (jof.exposeAccessContext) { + proxyFactory.addAdvice(new JndiContextExposingInterceptor(jof.getJndiTemplate())); + } + proxyFactory.setTargetSource(targetSource); + return proxyFactory.getProxy(jof.beanClassLoader); + } + } + + + /** + * Interceptor that exposes the JNDI context for all method invocations, + * according to JndiObjectFactoryBean's "exposeAccessContext" flag. + */ + private static class JndiContextExposingInterceptor implements MethodInterceptor { + + private final JndiTemplate jndiTemplate; + + public JndiContextExposingInterceptor(JndiTemplate jndiTemplate) { + this.jndiTemplate = jndiTemplate; + } + + public Object invoke(MethodInvocation invocation) throws Throwable { + Context ctx = (isEligible(invocation.getMethod()) ? this.jndiTemplate.getContext() : null); + try { + return invocation.proceed(); + } + finally { + this.jndiTemplate.releaseContext(ctx); + } + } + + protected boolean isEligible(Method method) { + return !Object.class.equals(method.getDeclaringClass()); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectLocator.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectLocator.java new file mode 100644 index 00000000000..043d9474da8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectLocator.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.jndi; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.StringUtils; + +/** + * Convenient superclass for JNDI-based service locators, + * providing configurable lookup of a specific JNDI resource. + * + *

Exposes a {@link #setJndiName "jndiName"} property. This may or may not + * include the "java:comp/env/" prefix expected by J2EE applications when + * accessing a locally mapped (Environmental Naming Context) resource. If it + * doesn't, the "java:comp/env/" prefix will be prepended if the "resourceRef" + * property is true (the default is false) and no other scheme + * (e.g. "java:") is given. + * + *

Subclasses may invoke the {@link #lookup()} method whenever it is appropriate. + * Some classes might do this on initialization, while others might do it + * on demand. The latter strategy is more flexible in that it allows for + * initialization of the locator before the JNDI object is available. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiName + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setResourceRef + * @see #lookup() + */ +public abstract class JndiObjectLocator extends JndiLocatorSupport implements InitializingBean { + + private String jndiName; + + private Class expectedType; + + + /** + * Specify the JNDI name to look up. If it doesn't begin with "java:comp/env/" + * this prefix is added automatically if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @see #setResourceRef + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + /** + * Return the JNDI name to look up. + */ + public String getJndiName() { + return this.jndiName; + } + + /** + * Specify the type that the located JNDI object is supposed + * to be assignable to, if any. + */ + public void setExpectedType(Class expectedType) { + this.expectedType = expectedType; + } + + /** + * Return the type that the located JNDI object is supposed + * to be assignable to, if any. + */ + public Class getExpectedType() { + return this.expectedType; + } + + public void afterPropertiesSet() throws IllegalArgumentException, NamingException { + if (!StringUtils.hasLength(getJndiName())) { + throw new IllegalArgumentException("Property 'jndiName' is required"); + } + } + + + /** + * Perform the actual JNDI lookup for this locator's target resource. + * @return the located target object + * @throws NamingException if the JNDI lookup failed or if the + * located JNDI object is not assigable to the expected type + * @see #setJndiName + * @see #setExpectedType + * @see #lookup(String, Class) + */ + protected Object lookup() throws NamingException { + return lookup(getJndiName(), getExpectedType()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java new file mode 100644 index 00000000000..f57040ab655 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java @@ -0,0 +1,145 @@ +/* + * 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.jndi; + +import javax.naming.NamingException; + +import org.springframework.aop.TargetSource; + +/** + * AOP {@link org.springframework.aop.TargetSource} that provides + * configurable JNDI lookups for getTarget() calls. + * + *

Can be used as alternative to {@link JndiObjectFactoryBean}, to allow for + * relocating a JNDI object lazily or for each operation (see "lookupOnStartup" + * and "cache" properties). This is particularly useful during development, as it + * allows for hot restarting of the JNDI server (for example, a remote JMS server). + * + *

Example: + * + *

+ * <bean id="queueConnectionFactoryTarget" class="org.springframework.jndi.JndiObjectTargetSource">
+ *   <property name="jndiName" value="JmsQueueConnectionFactory"/>
+ *   <property name="lookupOnStartup" value="false"/>
+ * </bean>
+ *
+ * <bean id="queueConnectionFactory" class="org.springframework.aop.framework.ProxyFactoryBean">
+ *   <property name="proxyInterfaces" value="javax.jms.QueueConnectionFactory"/>
+ *   <property name="targetSource" ref="queueConnectionFactoryTarget"/>
+ * </bean>
+ * + * A createQueueConnection call on the "queueConnectionFactory" proxy will + * cause a lazy JNDI lookup for "JmsQueueConnectionFactory" and a subsequent delegating + * call to the retrieved QueueConnectionFactory's createQueueConnection. + * + *

Alternatively, use a {@link JndiObjectFactoryBean} with a "proxyInterface". + * "lookupOnStartup" and "cache" can then be specified on the JndiObjectFactoryBean, + * creating a JndiObjectTargetSource underneath (instead of defining separate + * ProxyFactoryBean and JndiObjectTargetSource beans). + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setLookupOnStartup + * @see #setCache + * @see org.springframework.aop.framework.ProxyFactoryBean#setTargetSource + * @see JndiObjectFactoryBean#setProxyInterface + */ +public class JndiObjectTargetSource extends JndiObjectLocator implements TargetSource { + + private boolean lookupOnStartup = true; + + private boolean cache = true; + + private Object cachedObject; + + private Class targetClass; + + + /** + * Set whether to look up the JNDI object on startup. Default is "true". + *

Can be turned off to allow for late availability of the JNDI object. + * In this case, the JNDI object will be fetched on first access. + * @see #setCache + */ + public void setLookupOnStartup(boolean lookupOnStartup) { + this.lookupOnStartup = lookupOnStartup; + } + + /** + * Set whether to cache the JNDI object once it has been located. + * Default is "true". + *

Can be turned off to allow for hot redeployment of JNDI objects. + * In this case, the JNDI object will be fetched for each invocation. + * @see #setLookupOnStartup + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.lookupOnStartup) { + Object object = lookup(); + if (this.cache) { + this.cachedObject = object; + } + else { + this.targetClass = object.getClass(); + } + } + } + + + public Class getTargetClass() { + if (this.cachedObject != null) { + return this.cachedObject.getClass(); + } + else if (this.targetClass != null) { + return this.targetClass; + } + else { + return getExpectedType(); + } + } + + public boolean isStatic() { + return (this.cachedObject != null); + } + + public Object getTarget() { + try { + if (this.lookupOnStartup || !this.cache) { + return (this.cachedObject != null ? this.cachedObject : lookup()); + } + else { + synchronized (this) { + if (this.cachedObject == null) { + this.cachedObject = lookup(); + } + return this.cachedObject; + } + } + } + catch (NamingException ex) { + throw new JndiLookupFailureException("JndiObjectTargetSource failed to obtain new target object", ex); + } + } + + public void releaseTarget(Object target) { + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplate.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplate.java new file mode 100644 index 00000000000..9e6c1b61a1e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplate.java @@ -0,0 +1,240 @@ +/* + * 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.jndi; + +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.CollectionUtils; + +/** + * Helper class that simplifies JNDI operations. It provides methods to lookup and + * bind objects, and allows implementations of the {@link JndiCallback} interface + * to perform any operation they like with a JNDI naming context provided. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JndiCallback + * @see #execute + */ +public class JndiTemplate { + + protected final Log logger = LogFactory.getLog(getClass()); + + private Properties environment; + + + /** + * Create a new JndiTemplate instance. + */ + public JndiTemplate() { + } + + /** + * Create a new JndiTemplate instance, using the given environment. + */ + public JndiTemplate(Properties environment) { + this.environment = environment; + } + + + /** + * Set the environment for the JNDI InitialContext. + */ + public void setEnvironment(Properties environment) { + this.environment = environment; + } + + /** + * Return the environment for the JNDI InitialContext, if any. + */ + public Properties getEnvironment() { + return this.environment; + } + + + /** + * Execute the given JNDI context callback implementation. + * @param contextCallback JndiCallback implementation + * @return a result object returned by the callback, or null + * @throws NamingException thrown by the callback implementation + * @see #createInitialContext + */ + public Object execute(JndiCallback contextCallback) throws NamingException { + Context ctx = getContext(); + try { + return contextCallback.doInContext(ctx); + } + finally { + releaseContext(ctx); + } + } + + /** + * Obtain a JNDI context corresponding to this template's configuration. + * Called by {@link #execute}; may also be called directly. + *

The default implementation delegates to {@link #createInitialContext()}. + * @return the JNDI context (never null) + * @throws NamingException if context retrieval failed + * @see #releaseContext + */ + public Context getContext() throws NamingException { + return createInitialContext(); + } + + /** + * Release a JNDI context as obtained from {@link #getContext()}. + * @param ctx the JNDI context to release (may be null) + * @see #getContext + */ + public void releaseContext(Context ctx) { + if (ctx != null) { + try { + ctx.close(); + } + catch (NamingException ex) { + logger.debug("Could not close JNDI InitialContext", ex); + } + } + } + + /** + * Create a new JNDI initial context. Invoked by {@link #getContext}. + *

The default implementation use this template's environment settings. + * Can be subclassed for custom contexts, e.g. for testing. + * @return the initial Context instance + * @throws NamingException in case of initialization errors + */ + protected Context createInitialContext() throws NamingException { + Hashtable icEnv = null; + Properties env = getEnvironment(); + if (env != null) { + icEnv = new Hashtable(env.size()); + CollectionUtils.mergePropertiesIntoMap(env, icEnv); + } + return new InitialContext(icEnv); + } + + + /** + * Look up the object with the given name in the current JNDI context. + * @param name the JNDI name of the object + * @return object found (cannot be null; if a not so well-behaved + * JNDI implementations returns null, a NamingException gets thrown) + * @throws NamingException if there is no object with the given + * name bound to JNDI + */ + public Object lookup(final String name) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Looking up JNDI object with name [" + name + "]"); + } + return execute(new JndiCallback() { + public Object doInContext(Context ctx) throws NamingException { + Object located = ctx.lookup(name); + if (located == null) { + throw new NameNotFoundException( + "JNDI object with [" + name + "] not found: JNDI implementation returned null"); + } + return located; + } + }); + } + + /** + * Look up the object with the given name in the current JNDI context. + * @param name the JNDI name of the object + * @param requiredType type the JNDI object must match. Can be an interface or + * superclass of the actual class, or null for any match. For example, + * if the value is Object.class, this method will succeed whatever + * the class of the returned instance. + * @return object found (cannot be null; if a not so well-behaved + * JNDI implementations returns null, a NamingException gets thrown) + * @throws NamingException if there is no object with the given + * name bound to JNDI + */ + public Object lookup(String name, Class requiredType) throws NamingException { + Object jndiObject = lookup(name); + if (requiredType != null && !requiredType.isInstance(jndiObject)) { + throw new TypeMismatchNamingException( + name, requiredType, (jndiObject != null ? jndiObject.getClass() : null)); + } + return jndiObject; + } + + /** + * Bind the given object to the current JNDI context, using the given name. + * @param name the JNDI name of the object + * @param object the object to bind + * @throws NamingException thrown by JNDI, mostly name already bound + */ + public void bind(final String name, final Object object) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Binding JNDI object with name [" + name + "]"); + } + execute(new JndiCallback() { + public Object doInContext(Context ctx) throws NamingException { + ctx.bind(name, object); + return null; + } + }); + } + + /** + * Rebind the given object to the current JNDI context, using the given name. + * Overwrites any existing binding. + * @param name the JNDI name of the object + * @param object the object to rebind + * @throws NamingException thrown by JNDI + */ + public void rebind(final String name, final Object object) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Rebinding JNDI object with name [" + name + "]"); + } + execute(new JndiCallback() { + public Object doInContext(Context ctx) throws NamingException { + ctx.rebind(name, object); + return null; + } + }); + } + + /** + * Remove the binding for the given name from the current JNDI context. + * @param name the JNDI name of the object + * @throws NamingException thrown by JNDI, mostly name not found + */ + public void unbind(final String name) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Unbinding JNDI object with name [" + name + "]"); + } + execute(new JndiCallback() { + public Object doInContext(Context ctx) throws NamingException { + ctx.unbind(name); + return null; + } + }); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java b/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java new file mode 100644 index 00000000000..0c8852e2c25 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java @@ -0,0 +1,51 @@ +/* + * 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.jndi; + +import java.beans.PropertyEditorSupport; +import java.util.Properties; + +import org.springframework.beans.propertyeditors.PropertiesEditor; + +/** + * Properties editor for JndiTemplate objects. Allows properties of type + * JndiTemplate to be populated with a properties-format string. + * + * @author Rod Johnson + * @since 09.05.2003 + */ +public class JndiTemplateEditor extends PropertyEditorSupport { + + private final PropertiesEditor propertiesEditor = new PropertiesEditor(); + + public void setAsText(String text) throws IllegalArgumentException { + if (text == null) { + throw new IllegalArgumentException("JndiTemplate cannot be created from null string"); + } + if ("".equals(text)) { + // empty environment + setValue(new JndiTemplate()); + } + else { + // we have a non-empty properties string + this.propertiesEditor.setAsText(text); + Properties props = (Properties) this.propertiesEditor.getValue(); + setValue(new JndiTemplate(props)); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java b/org.springframework.context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java new file mode 100644 index 00000000000..6303b62a138 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java @@ -0,0 +1,73 @@ +/* + * 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.jndi; + +import javax.naming.NamingException; + +/** + * Exception thrown if a type mismatch is encountered for an object + * located in a JNDI environment. Thrown by JndiTemplate. + * + * @author Juergen Hoeller + * @since 1.2.8 + * @see JndiTemplate#lookup(String, Class) + */ +public class TypeMismatchNamingException extends NamingException { + + private Class requiredType; + + private Class actualType; + + + /** + * Construct a new TypeMismatchNamingException, + * building an explanation text from the given arguments. + * @param jndiName the JNDI name + * @param requiredType the required type for the lookup + * @param actualType the actual type that the lookup returned + */ + public TypeMismatchNamingException(String jndiName, Class requiredType, Class actualType) { + super("Object of type [" + actualType + "] available at JNDI location [" + + jndiName + "] is not assignable to [" + requiredType.getName() + "]"); + this.requiredType = requiredType; + this.actualType = actualType; + } + + /** + * Construct a new TypeMismatchNamingException. + * @param explanation the explanation text + */ + public TypeMismatchNamingException(String explanation) { + super(explanation); + } + + + /** + * Return the required type for the lookup, if available. + */ + public final Class getRequiredType() { + return this.requiredType; + } + + /** + * Return the actual type that the lookup returned, if available. + */ + public final Class getActualType() { + return this.actualType; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/package.html b/org.springframework.context/src/main/java/org/springframework/jndi/package.html new file mode 100644 index 00000000000..ec93c5f08e9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/package.html @@ -0,0 +1,13 @@ + + + +The classes in this package make JNDI easier to use, +facilitating the accessing of configuration stored in JNDI, +and provide useful superclasses for JNDI access classes. + +

The classes in this package are discussed in Chapter 11 of +Expert One-On-One J2EE Design and Development +by Rod Johnson (Wrox, 2002). + + + diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java b/org.springframework.context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java new file mode 100644 index 00000000000..fd76d701369 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java @@ -0,0 +1,209 @@ +/* + * 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.jndi.support; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.jndi.TypeMismatchNamingException; + +/** + * Simple JNDI-based implementation of Spring's + * {@link org.springframework.beans.factory.BeanFactory} interface. + * Does not support enumerating bean definitions, hence doesn't implement + * the {@link org.springframework.beans.factory.ListableBeanFactory} interface. + * + *

This factory resolves given bean names as JNDI names within the + * J2EE application's "java:comp/env/" namespace. It caches the resolved + * types for all obtained objects, and optionally also caches shareable + * objects (if they are explicitly marked as + * {@link #addShareableResource shareable resource}. + * + *

The main intent of this factory is usage in combination with Spring's + * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor}, + * configured as "resourceFactory" for resolving @Resource + * annotations as JNDI objects without intermediate bean definitions. + * It may be used for similar lookup scenarios as well, of course, + * in particular if BeanFactory-style type checking is required. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor + */ +public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFactory { + + /** JNDI names of resources that are known to be shareable, i.e. can be cached */ + private final Set shareableResources = new HashSet(); + + /** Cache of shareable singleton objects: bean name --> bean instance */ + private final Map singletonObjects = new HashMap(); + + /** Cache of the types of nonshareable resources: bean name --> bean type */ + private final Map resourceTypes = new HashMap(); + + + public SimpleJndiBeanFactory() { + setResourceRef(true); + } + + + /** + * Set a list of names of shareable JNDI resources, + * which this factory is allowed to cache once obtained. + * @param shareableResources the JNDI names + * (typically within the "java:comp/env/" namespace) + */ + public void setShareableResources(String[] shareableResources) { + this.shareableResources.addAll(Arrays.asList(shareableResources)); + } + + /** + * Add the name of a shareable JNDI resource, + * which this factory is allowed to cache once obtained. + * @param shareableResource the JNDI name + * (typically within the "java:comp/env/" namespace) + */ + public void addShareableResource(String shareableResource) { + this.shareableResources.add(shareableResource); + } + + + public Object getBean(String name) throws BeansException { + return getBean(name, (Class) null); + } + + public Object getBean(String name, Class requiredType) throws BeansException { + try { + if (isSingleton(name)) { + return doGetSingleton(name, requiredType); + } + else { + return lookup(name, requiredType); + } + } + catch (NameNotFoundException ex) { + throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment"); + } + catch (TypeMismatchNamingException ex) { + throw new BeanNotOfRequiredTypeException(name, ex.getRequiredType(), ex.getActualType()); + } + catch (NamingException ex) { + throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", ex); + } + } + + public Object getBean(String name, Object[] args) throws BeansException { + if (args != null) { + throw new UnsupportedOperationException( + "SimpleJndiBeanFactory does not support explicit bean creation arguments)"); + } + return getBean(name); + } + + public boolean containsBean(String name) { + if (this.singletonObjects.containsKey(name) || this.resourceTypes.containsKey(name)) { + return true; + } + try { + doGetType(name); + return true; + } + catch (NamingException ex) { + return false; + } + } + + public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return this.shareableResources.contains(name); + } + + public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { + return !this.shareableResources.contains(name); + } + + public boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException { + Class type = getType(name); + return (targetType == null || (type != null && targetType.isAssignableFrom(type))); + } + + public Class getType(String name) throws NoSuchBeanDefinitionException { + try { + return doGetType(name); + } + catch (NameNotFoundException ex) { + throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment"); + } + catch (NamingException ex) { + return null; + } + } + + public String[] getAliases(String name) { + return new String[0]; + } + + + private Object doGetSingleton(String name, Class requiredType) throws NamingException { + synchronized (this.singletonObjects) { + if (this.singletonObjects.containsKey(name)) { + Object jndiObject = this.singletonObjects.get(name); + if (requiredType != null && !requiredType.isInstance(jndiObject)) { + throw new TypeMismatchNamingException( + convertJndiName(name), requiredType, (jndiObject != null ? jndiObject.getClass() : null)); + } + return jndiObject; + } + Object jndiObject = lookup(name, requiredType); + this.singletonObjects.put(name, jndiObject); + return jndiObject; + } + } + + private Class doGetType(String name) throws NamingException { + if (isSingleton(name)) { + Object jndiObject = doGetSingleton(name, null); + return (jndiObject != null ? jndiObject.getClass() : null); + } + else { + synchronized (this.resourceTypes) { + if (this.resourceTypes.containsKey(name)) { + return (Class) this.resourceTypes.get(name); + } + else { + Object jndiObject = lookup(name, null); + Class type = (jndiObject != null ? jndiObject.getClass() : null); + this.resourceTypes.put(name, type); + return type; + } + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/jndi/support/package.html b/org.springframework.context/src/main/java/org/springframework/jndi/support/package.html new file mode 100644 index 00000000000..204aff78c91 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/jndi/support/package.html @@ -0,0 +1,8 @@ + + + +Support classes for JNDI usage, +including a JNDI-based BeanFactory implementation. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/RemoteAccessException.java b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteAccessException.java new file mode 100644 index 00000000000..260b7de604c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteAccessException.java @@ -0,0 +1,73 @@ +/* + * 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.remoting; + +import org.springframework.core.NestedRuntimeException; + +/** + * Generic remote access exception. A service proxy for any remoting + * protocol should throw this exception or subclasses of it, in order + * to transparently expose a plain Java business interface. + * + *

When using conforming proxies, switching the actual remoting protocol + * e.g. from Hessian to Burlap does not affect client code. Clients work + * with a plain natural Java business interface that the service exposes. + * A client object simply receives an implementation for the interface that + * it needs via a bean reference, like it does for a local bean as well. + * + *

A client may catch RemoteAccessException if it wants to, but as + * remote access errors are typically unrecoverable, it will probably let + * such exceptions propagate to a higher level that handles them generically. + * In this case, the client code doesn't show any signs of being involved in + * remote access, as there aren't any remoting-specific dependencies. + * + *

Even when switching from a remote service proxy to a local implementation + * of the same interface, this amounts to just a matter of configuration. Obviously, + * the client code should be somewhat aware that it might be working + * against a remote service, for example in terms of repeated method calls that + * cause unnecessary roundtrips etc. However, it doesn't have to be aware whether + * it is actually working against a remote service or a local implementation, + * or with which remoting protocol it is working under the hood. + * + * @author Juergen Hoeller + * @since 14.05.2003 + */ +public class RemoteAccessException extends NestedRuntimeException { + + /** Use serialVersionUID from Spring 1.2 for interoperability */ + private static final long serialVersionUID = -4906825139312227864L; + + + /** + * Constructor for RemoteAccessException. + * @param msg the detail message + */ + public RemoteAccessException(String msg) { + super(msg); + } + + /** + * Constructor for RemoteAccessException. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * remoting API such as RMI) + */ + public RemoteAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java new file mode 100644 index 00000000000..cb0b5cb9837 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java @@ -0,0 +1,37 @@ +/* + * 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.remoting; + +/** + * RemoteAccessException subclass to be thrown when no connection + * could be established with a remote service. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class RemoteConnectFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteConnectFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteConnectFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java new file mode 100644 index 00000000000..f7e8afb0a44 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java @@ -0,0 +1,39 @@ +/* + * 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.remoting; + +/** + * RemoteAccessException subclass to be thrown when the execution + * of the target method failed on the server side, for example + * when a method was not found on the target object. + * + * @author Juergen Hoeller + * @since 2.5 + * @see RemoteProxyFailureException + */ +public class RemoteInvocationFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteInvocationFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteInvocationFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.java b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.java new file mode 100644 index 00000000000..0d1b69e26b7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.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.remoting; + +/** + * RemoteAccessException subclass to be thrown in case of a lookup failure, + * typically if the lookup happens on demand for each method invocation. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class RemoteLookupFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteLookupFailureException. + * @param msg the detail message + */ + public RemoteLookupFailureException(String msg) { + super(msg); + } + + /** + * Constructor for RemoteLookupFailureException. + * @param msg message + * @param cause the root cause from the remoting API in use + */ + public RemoteLookupFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java new file mode 100644 index 00000000000..c6d92fac803 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java @@ -0,0 +1,39 @@ +/* + * 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.remoting; + +/** + * RemoteAccessException subclass to be thrown in case of a failure + * within the client-side proxy for a remote service, for example + * when a method was not found on the underlying RMI stub. + * + * @author Juergen Hoeller + * @since 1.2.8 + * @see RemoteInvocationFailureException + */ +public class RemoteProxyFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteProxyFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteProxyFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/package.html b/org.springframework.context/src/main/java/org/springframework/remoting/package.html new file mode 100644 index 00000000000..e99dc3c7e2f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/package.html @@ -0,0 +1,8 @@ + + + +Exception hierarchy for Spring's remoting infrastructure, +independent of any specific remote method invocation system. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java new file mode 100644 index 00000000000..2cab69bcb66 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java @@ -0,0 +1,101 @@ +/* + * 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.remoting.rmi; + +import java.io.IOException; +import java.io.InputStream; +import java.rmi.server.RMIClassLoader; + +import org.springframework.core.ConfigurableObjectInputStream; + +/** + * Special ObjectInputStream subclass that falls back to a specified codebase + * to load classes from if not found locally. In contrast to standard RMI + * conventions for dynamic class download, it is the client that determines + * the codebase URL here, rather than the "java.rmi.server.codebase" system + * property on the server. + * + *

Uses the JDK's RMIClassLoader to load classes from the specified codebase. + * The codebase can consist of multiple URLs, separated by spaces. + * Note that RMIClassLoader requires a SecurityManager to be set, like when + * using dynamic class download with standard RMI! (See the RMI documentation + * for details.) + * + *

Despite residing in the RMI package, this class is not used for + * RmiClientInterceptor, which uses the standard RMI infrastructure instead + * and thus is only able to rely on RMI's standard dynamic class download via + * "java.rmi.server.codebase". CodebaseAwareObjectInputStream is used by + * HttpInvokerClientInterceptor (see the "codebaseUrl" property there). + * + *

Thanks to Lionel Mestre for suggesting the option and providing + * a prototype! + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see java.rmi.server.RMIClassLoader + * @see RemoteInvocationSerializingExporter#createObjectInputStream + * @see org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor#setCodebaseUrl + */ +public class CodebaseAwareObjectInputStream extends ConfigurableObjectInputStream { + + private final String codebaseUrl; + + + /** + * Create a new CodebaseAwareObjectInputStream for the given InputStream and codebase. + * @param in the InputStream to read from + * @param codebaseUrl the codebase URL to load classes from if not found locally + * (can consist of multiple URLs, separated by spaces) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public CodebaseAwareObjectInputStream(InputStream in, String codebaseUrl) throws IOException { + this(in, null, codebaseUrl); + } + + /** + * Create a new CodebaseAwareObjectInputStream for the given InputStream and codebase. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * (may be null to indicate RMI's default ClassLoader) + * @param codebaseUrl the codebase URL to load classes from if not found locally + * (can consist of multiple URLs, separated by spaces) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public CodebaseAwareObjectInputStream( + InputStream in, ClassLoader classLoader, String codebaseUrl) throws IOException { + + super(in, classLoader); + this.codebaseUrl = codebaseUrl; + } + + + protected Class resolveFallbackIfPossible(String className, ClassNotFoundException ex) + throws IOException, ClassNotFoundException { + + // If codebaseUrl is set, try to load the class with the RMIClassLoader. + // Else, propagate the ClassNotFoundException. + if (this.codebaseUrl == null) { + throw ex; + } + return RMIClassLoader.loadClass(this.codebaseUrl, className); + } + + protected ClassLoader getFallbackClassLoader() throws IOException { + return RMIClassLoader.getClassLoader(this.codebaseUrl); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java new file mode 100644 index 00000000000..d49d17b574d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java @@ -0,0 +1,488 @@ +/* + * 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.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.RemoteException; + +import javax.naming.NamingException; +import javax.rmi.PortableRemoteObject; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.omg.CORBA.OBJECT_NOT_EXIST; +import org.omg.CORBA.SystemException; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiObjectLocator; +import org.springframework.remoting.RemoteAccessException; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteInvocationFailureException; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.support.DefaultRemoteInvocationFactory; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationFactory; +import org.springframework.util.ReflectionUtils; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI services from JNDI. + * Typically used for RMI-IIOP (CORBA), but can also be used for EJB home objects + * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup, + * this accessor also performs narrowing through PortableRemoteObject. + * + *

With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

The JNDI environment can be specified as "jndiEnvironment" property, + * or be configured in a jndi.properties file or as system properties. + * For example: + * + *

<property name="jndiEnvironment">
+ * 	 <props>
+ *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
+ *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
+ *	 </props>
+ * </property>
+ * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiServiceExporter + * @see JndiRmiProxyFactoryBean + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + * @see javax.rmi.PortableRemoteObject#narrow + */ +public class JndiRmiClientInterceptor extends JndiObjectLocator implements MethodInterceptor, InitializingBean { + + private Class serviceInterface; + + private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); + + private boolean lookupStubOnStartup = true; + + private boolean cacheStub = true; + + private boolean refreshStubOnConnectFailure = false; + + private Object cachedStub; + + private final Object stubMonitor = new Object(); + + + /** + * Set the interface of the service to access. + * The interface must be suitable for the particular service and remoting tool. + *

Typically required to be able to create a suitable service proxy, + * but can also be optional if the lookup returns a typed stub. + */ + public void setServiceInterface(Class serviceInterface) { + if (serviceInterface != null && !serviceInterface.isInterface()) { + throw new IllegalArgumentException("'serviceInterface' must be an interface"); + } + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to access. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + + /** + * Set the RemoteInvocationFactory to use for this accessor. + * Default is a {@link DefaultRemoteInvocationFactory}. + *

A custom invocation factory can add further context information + * to the invocation, for example user credentials. + */ + public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { + this.remoteInvocationFactory = remoteInvocationFactory; + } + + /** + * Return the RemoteInvocationFactory used by this accessor. + */ + public RemoteInvocationFactory getRemoteInvocationFactory() { + return this.remoteInvocationFactory; + } + + /** + * Set whether to look up the RMI stub on startup. Default is "true". + *

Can be turned off to allow for late start of the RMI server. + * In this case, the RMI stub will be fetched on first access. + * @see #setCacheStub + */ + public void setLookupStubOnStartup(boolean lookupStubOnStartup) { + this.lookupStubOnStartup = lookupStubOnStartup; + } + + /** + * Set whether to cache the RMI stub once it has been located. + * Default is "true". + *

Can be turned off to allow for hot restart of the RMI server. + * In this case, the RMI stub will be fetched for each invocation. + * @see #setLookupStubOnStartup + */ + public void setCacheStub(boolean cacheStub) { + this.cacheStub = cacheStub; + } + + /** + * Set whether to refresh the RMI stub on connect failure. + * Default is "false". + *

Can be turned on to allow for hot restart of the RMI server. + * If a cached RMI stub throws an RMI exception that indicates a + * remote connect failure, a fresh proxy will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { + this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; + } + + + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + prepare(); + } + + /** + * Fetches the RMI stub on startup, if necessary. + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setLookupStubOnStartup + * @see #lookupStub + */ + public void prepare() throws RemoteLookupFailureException { + // Cache RMI stub on initialization? + if (this.lookupStubOnStartup) { + Object remoteObj = lookupStub(); + if (logger.isDebugEnabled()) { + if (remoteObj instanceof RmiInvocationHandler) { + logger.debug("JNDI RMI object [" + getJndiName() + "] is an RMI invoker"); + } + else if (getServiceInterface() != null) { + boolean isImpl = getServiceInterface().isInstance(remoteObj); + logger.debug("Using service interface [" + getServiceInterface().getName() + + "] for JNDI RMI object [" + getJndiName() + "] - " + + (!isImpl ? "not " : "") + "directly implemented"); + } + } + if (this.cacheStub) { + this.cachedStub = remoteObj; + } + } + } + + /** + * Create the RMI stub, typically by looking it up. + *

Called on interceptor initialization if "cacheStub" is "true"; + * else called for each invocation by {@link #getStub()}. + *

The default implementation retrieves the service from the + * JNDI environment. This can be overridden in subclasses. + * @return the RMI stub to store in this interceptor + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setCacheStub + * @see #lookup + */ + protected Object lookupStub() throws RemoteLookupFailureException { + try { + Object stub = lookup(); + if (getServiceInterface() != null && !(stub instanceof RmiInvocationHandler)) { + try { + stub = PortableRemoteObject.narrow(stub, getServiceInterface()); + } + catch (ClassCastException ex) { + throw new RemoteLookupFailureException( + "Could not narrow RMI stub to service interface [" + getServiceInterface().getName() + "]", ex); + } + } + return stub; + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); + } + } + + /** + * Return the RMI stub to use. Called for each invocation. + *

The default implementation returns the stub created on initialization, + * if any. Else, it invokes {@link #lookupStub} to get a new stub for + * each invocation. This can be overridden in subclasses, for example in + * order to cache a stub for a given amount of time before recreating it, + * or to test the stub whether it is still alive. + * @return the RMI stub to use for an invocation + * @throws NamingException if stub creation failed + * @throws RemoteLookupFailureException if RMI stub creation failed + */ + protected Object getStub() throws NamingException, RemoteLookupFailureException { + if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { + return (this.cachedStub != null ? this.cachedStub : lookupStub()); + } + else { + synchronized (this.stubMonitor) { + if (this.cachedStub == null) { + this.cachedStub = lookupStub(); + } + return this.cachedStub; + } + } + } + + + /** + * Fetches an RMI stub and delegates to {@link #doInvoke}. + * If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getStub + * @see #doInvoke + * @see #refreshAndRetry + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + Object stub = null; + try { + stub = getStub(); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); + } + try { + return doInvoke(invocation, stub); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + catch (SystemException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

The default implementation delegates to + * {@link RmiClientInterceptorUtils#isConnectFailure}. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + */ + protected boolean isConnectFailure(RemoteException ex) { + return RmiClientInterceptorUtils.isConnectFailure(ex); + } + + /** + * Determine whether the given CORBA exception indicates a connect failure. + *

The default implementation checks for CORBA's + * {@link org.omg.CORBA.OBJECT_NOT_EXIST} exception. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + */ + protected boolean isConnectFailure(SystemException ex) { + return (ex instanceof OBJECT_NOT_EXIST); + } + + /** + * Refresh the stub and retry the remote invocation if necessary. + *

If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, if failed too. + */ + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshStubOnConnectFailure) { + if (logger.isDebugEnabled()) { + logger.debug("Could not connect to RMI service [" + getJndiName() + "] - retrying", ex); + } + else if (logger.isWarnEnabled()) { + logger.warn("Could not connect to RMI service [" + getJndiName() + "] - retrying"); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the RMI stub and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + Object freshStub = null; + synchronized (this.stubMonitor) { + this.cachedStub = null; + freshStub = lookupStub(); + if (this.cacheStub) { + this.cachedStub = freshStub; + } + } + return doInvoke(invocation, freshStub); + } + + + /** + * Perform the given invocation on the given RMI stub. + * @param invocation the AOP method invocation + * @param stub the RMI stub to invoke + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + protected Object doInvoke(MethodInvocation invocation, Object stub) throws Throwable { + if (stub instanceof RmiInvocationHandler) { + // RMI invoker + try { + return doInvoke(invocation, (RmiInvocationHandler) stub); + } + catch (RemoteException ex) { + throw convertRmiAccessException(ex, invocation.getMethod()); + } + catch (SystemException ex) { + throw convertCorbaAccessException(ex, invocation.getMethod()); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + catch (Throwable ex) { + throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + + "] failed in RMI service [" + getJndiName() + "]", ex); + } + } + else { + // traditional RMI stub + try { + return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + throw convertRmiAccessException((RemoteException) targetEx, invocation.getMethod()); + } + else if (targetEx instanceof SystemException) { + throw convertCorbaAccessException((SystemException) targetEx, invocation.getMethod()); + } + else { + throw targetEx; + } + } + } + } + + /** + * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. + *

The default implementation delegates to {@link #createRemoteInvocation}. + * @param methodInvocation the current AOP method invocation + * @param invocationHandler the RmiInvocationHandler to apply the invocation to + * @return the invocation result + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see org.springframework.remoting.support.RemoteInvocation + */ + protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { + return "RMI invoker proxy for service URL [" + getJndiName() + "]"; + } + + return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); + } + + /** + * Create a new RemoteInvocation object for the given AOP method invocation. + *

The default implementation delegates to the configured + * {@link #setRemoteInvocationFactory RemoteInvocationFactory}. + * This can be overridden in subclasses in order to provide custom RemoteInvocation + * subclasses, containing additional invocation parameters (e.g. user credentials). + *

Note that it is preferable to build a custom RemoteInvocationFactory + * as a reusable strategy, instead of overriding this method. + * @param methodInvocation the current AOP method invocation + * @return the RemoteInvocation object + * @see RemoteInvocationFactory#createRemoteInvocation + */ + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation); + } + + /** + * Convert the given RMI RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not declare + * RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @return the exception to be thrown to the caller + */ + private Exception convertRmiAccessException(RemoteException ex, Method method) { + return RmiClientInterceptorUtils.convertRmiAccessException(method, ex, isConnectFailure(ex), getJndiName()); + } + + /** + * Convert the given CORBA SystemException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not declare + * RemoteException. Else, return the SystemException wrapped in a RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @return the exception to be thrown to the caller + */ + private Exception convertCorbaAccessException(SystemException ex, Method method) { + if (ReflectionUtils.declaresException(method, RemoteException.class)) { + // A traditional RMI service: wrap CORBA exceptions in standard RemoteExceptions. + return new RemoteException("Failed to access CORBA service [" + getJndiName() + "]", ex); + } + else { + if (isConnectFailure(ex)) { + return new RemoteConnectFailureException("Could not connect to CORBA service [" + getJndiName() + "]", ex); + } + else { + return new RemoteAccessException("Could not access CORBA service [" + getJndiName() + "]", ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java new file mode 100644 index 00000000000..8d4fad43bcb --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java @@ -0,0 +1,96 @@ +/* + * 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.remoting.rmi; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.ClassUtils; + +/** + * Factory bean for RMI proxies from JNDI. + * + *

Typically used for RMI-IIOP (CORBA), but can also be used for EJB home objects + * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup, + * this accessor also performs narrowing through {@link javax.rmi.PortableRemoteObject}. + * + *

With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

The JNDI environment can be specified as "jndiEnvironment" property, + * or be configured in a jndi.properties file or as system properties. + * For example: + * + *

<property name="jndiEnvironment">
+ * 	 <props>
+ *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
+ *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
+ *	 </props>
+ * </property>
+ * + * @author Juergen Hoeller + * @since 1.1 + * @see #setServiceInterface + * @see #setJndiName + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiServiceExporter + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + * @see javax.rmi.PortableRemoteObject#narrow + */ +public class JndiRmiProxyFactoryBean extends JndiRmiClientInterceptor implements FactoryBean, BeanClassLoaderAware { + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Object serviceProxy; + + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (getServiceInterface() == null) { + throw new IllegalArgumentException("Property 'serviceInterface' is required"); + } + this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.serviceProxy; + } + + public Class getObjectType() { + return getServiceInterface(); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java new file mode 100644 index 00000000000..d5e9c38fc79 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java @@ -0,0 +1,148 @@ +/* + * 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.remoting.rmi; + +import java.rmi.NoSuchObjectException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Properties; + +import javax.naming.NamingException; +import javax.rmi.PortableRemoteObject; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiTemplate; + +/** + * Service exporter which binds RMI services to JNDI. + * Typically used for RMI-IIOP (CORBA). + * + *

Exports services via the {@link javax.rmi.PortableRemoteObject} class. + * You need to run "rmic" with the "-iiop" option to generate corresponding + * stubs and skeletons for each exported service. + * + *

Also supports exposing any non-RMI service via RMI invokers, to be accessed + * via {@link JndiRmiClientInterceptor} / {@link JndiRmiProxyFactoryBean}'s + * automatic detection of such invokers. + * + *

With an RMI invoker, RMI communication works on the {@link RmiInvocationHandler} + * level, needing only one stub for any service. Service interfaces do not have to + * extend java.rmi.Remote or throw java.rmi.RemoteException + * on all methods, but in and out parameters have to be serializable. + * + *

The JNDI environment can be specified as "jndiEnvironment" bean property, + * or be configured in a jndi.properties file or as system properties. + * For example: + * + *

<property name="jndiEnvironment">
+ * 	 <props>
+ *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
+ *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
+ *	 </props>
+ * </property>
+ * + * @author Juergen Hoeller + * @since 1.1 + * @see #setService + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiClientInterceptor + * @see JndiRmiProxyFactoryBean + * @see javax.rmi.PortableRemoteObject#exportObject + */ +public class JndiRmiServiceExporter extends RmiBasedExporter implements InitializingBean, DisposableBean { + + private JndiTemplate jndiTemplate = new JndiTemplate(); + + private String jndiName; + + private Remote exportedObject; + + + /** + * Set the JNDI template to use for JNDI lookups. + * You can also specify JNDI environment settings via "jndiEnvironment". + * @see #setJndiEnvironment + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiTemplate = (jndiTemplate != null ? jndiTemplate : new JndiTemplate()); + } + + /** + * Set the JNDI environment to use for JNDI lookups. + * Creates a JndiTemplate with the given environment settings. + * @see #setJndiTemplate + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiTemplate = new JndiTemplate(jndiEnvironment); + } + + /** + * Set the JNDI name of the exported RMI service. + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + + public void afterPropertiesSet() throws NamingException, RemoteException { + prepare(); + } + + /** + * Initialize this service exporter, binding the specified service to JNDI. + * @throws NamingException if service binding failed + * @throws RemoteException if service export failed + */ + public void prepare() throws NamingException, RemoteException { + if (this.jndiName == null) { + throw new IllegalArgumentException("Property 'jndiName' is required"); + } + + // Initialize and cache exported object. + this.exportedObject = getObjectToExport(); + PortableRemoteObject.exportObject(this.exportedObject); + + rebind(); + } + + /** + * Rebind the specified service to JNDI, for recovering in case + * of the target registry having been restarted. + * @throws NamingException if service binding failed + */ + public void rebind() throws NamingException { + if (logger.isInfoEnabled()) { + logger.info("Binding RMI service to JNDI location [" + this.jndiName + "]"); + } + this.jndiTemplate.rebind(this.jndiName, this.exportedObject); + } + + /** + * Unbind the RMI service from JNDI on bean factory shutdown. + */ + public void destroy() throws NamingException, NoSuchObjectException { + if (logger.isInfoEnabled()) { + logger.info("Unbinding RMI service from JNDI location [" + this.jndiName + "]"); + } + this.jndiTemplate.unbind(this.jndiName); + PortableRemoteObject.unexportObject(this.exportedObject); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java new file mode 100644 index 00000000000..7f8288b6169 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java @@ -0,0 +1,161 @@ +/* + * 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.remoting.rmi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.rmi.RemoteException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationBasedExporter; +import org.springframework.remoting.support.RemoteInvocationResult; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for remote service exporters that explicitly deserialize + * {@link org.springframework.remoting.support.RemoteInvocation} objects and serialize + * {@link org.springframework.remoting.support.RemoteInvocationResult} objects, + * for example Spring's HTTP invoker. + * + *

Provides template methods for ObjectInputStream and + * ObjectOutputStream handling. + * + * @author Juergen Hoeller + * @since 2.5.1 + * @see java.io.ObjectInputStream + * @see java.io.ObjectOutputStream + * @see #doReadRemoteInvocation + * @see #doWriteRemoteInvocationResult + */ +public abstract class RemoteInvocationSerializingExporter extends RemoteInvocationBasedExporter + implements InitializingBean { + + /** + * Default content type: "application/x-java-serialized-object" + */ + public static final String CONTENT_TYPE_SERIALIZED_OBJECT = "application/x-java-serialized-object"; + + + private String contentType = CONTENT_TYPE_SERIALIZED_OBJECT; + + private Object proxy; + + + /** + * Specify the content type to use for sending remote invocation responses. + *

Default is "application/x-java-serialized-object". + */ + public void setContentType(String contentType) { + Assert.notNull(contentType, "'contentType' must not be null"); + this.contentType = contentType; + } + + /** + * Return the content type to use for sending remote invocation responses. + */ + public String getContentType() { + return this.contentType; + } + + + public void afterPropertiesSet() { + prepare(); + } + + /** + * Initialize this service exporter. + */ + public void prepare() { + this.proxy = getProxyForService(); + } + + protected final Object getProxy() { + Assert.notNull(this.proxy, ClassUtils.getShortName(getClass()) + " has not been initialized"); + return this.proxy; + } + + + /** + * Create an ObjectInputStream for the given InputStream. + *

The default implementation creates a Spring {@link CodebaseAwareObjectInputStream}. + * @param is the InputStream to read from + * @return the new ObjectInputStream instance to use + * @throws java.io.IOException if creation of the ObjectInputStream failed + */ + protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { + return new CodebaseAwareObjectInputStream(is, getBeanClassLoader(), null); + } + + /** + * Perform the actual reading of an invocation result object from the + * given ObjectInputStream. + *

The default implementation simply calls + * {@link java.io.ObjectInputStream#readObject()}. + * Can be overridden for deserialization of a custom wrapper object rather + * than the plain invocation, for example an encryption-aware holder. + * @param ois the ObjectInputStream to read from + * @return the RemoteInvocationResult object + * @throws java.io.IOException in case of I/O failure + * @throws ClassNotFoundException if case of a transferred class not + * being found in the local ClassLoader + */ + protected RemoteInvocation doReadRemoteInvocation(ObjectInputStream ois) + throws IOException, ClassNotFoundException { + + Object obj = ois.readObject(); + if (!(obj instanceof RemoteInvocation)) { + throw new RemoteException("Deserialized object needs to be assignable to type [" + + RemoteInvocation.class.getName() + "]: " + obj); + } + return (RemoteInvocation) obj; + } + + /** + * Create an ObjectOutputStream for the given OutputStream. + *

The default implementation creates a plain + * {@link java.io.ObjectOutputStream}. + * @param os the OutputStream to write to + * @return the new ObjectOutputStream instance to use + * @throws java.io.IOException if creation of the ObjectOutputStream failed + */ + protected ObjectOutputStream createObjectOutputStream(OutputStream os) throws IOException { + return new ObjectOutputStream(os); + } + + /** + * Perform the actual writing of the given invocation result object + * to the given ObjectOutputStream. + *

The default implementation simply calls + * {@link java.io.ObjectOutputStream#writeObject}. + * Can be overridden for serialization of a custom wrapper object rather + * than the plain invocation, for example an encryption-aware holder. + * @param result the RemoteInvocationResult object + * @param oos the ObjectOutputStream to write to + * @throws java.io.IOException if thrown by I/O methods + */ + protected void doWriteRemoteInvocationResult(RemoteInvocationResult result, ObjectOutputStream oos) + throws IOException { + + oos.writeObject(result); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java new file mode 100644 index 00000000000..829dd5f12ac --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java @@ -0,0 +1,75 @@ +/* + * 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.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.Remote; + +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationBasedExporter; + +/** + * Convenient superclass for RMI-based remote exporters. Provides a facility + * to automatically wrap a given plain Java service object with an + * RmiInvocationWrapper, exposing the {@link RmiInvocationHandler} remote interface. + * + *

Using the RMI invoker mechanism, RMI communication operates at the {@link RmiInvocationHandler} + * level, sharing a common invoker stub for any number of services. Service interfaces are not + * required to extend java.rmi.Remote or declare java.rmi.RemoteException + * on all service methods. However, in and out parameters still have to be serializable. + * + * @author Juergen Hoeller + * @since 1.2.5 + * @see RmiServiceExporter + * @see JndiRmiServiceExporter + */ +public abstract class RmiBasedExporter extends RemoteInvocationBasedExporter { + + /** + * Determine the object to export: either the service object itself + * or a RmiInvocationWrapper in case of a non-RMI service object. + * @return the RMI object to export + * @see #setService + * @see #setServiceInterface + */ + protected Remote getObjectToExport() { + // determine remote object + if (getService() instanceof Remote && + (getServiceInterface() == null || Remote.class.isAssignableFrom(getServiceInterface()))) { + // conventional RMI service + return (Remote) getService(); + } + else { + // RMI invoker + if (logger.isDebugEnabled()) { + logger.debug("RMI service [" + getService() + "] is an RMI invoker"); + } + return new RmiInvocationWrapper(getProxyForService(), this); + } + } + + /** + * Redefined here to be visible to RmiInvocationWrapper. + * Simply delegates to the corresponding superclass method. + */ + protected Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + return super.invoke(invocation, targetObject); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java new file mode 100644 index 00000000000..1aeb99b24fe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java @@ -0,0 +1,413 @@ +/* + * 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.remoting.rmi; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteInvocationFailureException; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.support.RemoteInvocationBasedAccessor; +import org.springframework.remoting.support.RemoteInvocationUtils; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} for accessing conventional + * RMI services or RMI invokers. The service URL must be a valid RMI URL + * (e.g. "rmi://localhost:1099/myservice"). + * + *

RMI invokers work at the RmiInvocationHandler level, needing only one stub for + * any service. Service interfaces do not have to extend java.rmi.Remote + * or throw java.rmi.RemoteException. Spring's unchecked + * RemoteAccessException will be thrown on remote invocation failure. + * Of course, in and out parameters have to be serializable. + * + *

With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + * @author Juergen Hoeller + * @since 29.09.2003 + * @see RmiServiceExporter + * @see RmiProxyFactoryBean + * @see RmiInvocationHandler + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + */ +public class RmiClientInterceptor extends RemoteInvocationBasedAccessor + implements MethodInterceptor { + + private boolean lookupStubOnStartup = true; + + private boolean cacheStub = true; + + private boolean refreshStubOnConnectFailure = false; + + private RMIClientSocketFactory registryClientSocketFactory; + + private Remote cachedStub; + + private final Object stubMonitor = new Object(); + + + /** + * Set whether to look up the RMI stub on startup. Default is "true". + *

Can be turned off to allow for late start of the RMI server. + * In this case, the RMI stub will be fetched on first access. + * @see #setCacheStub + */ + public void setLookupStubOnStartup(boolean lookupStubOnStartup) { + this.lookupStubOnStartup = lookupStubOnStartup; + } + + /** + * Set whether to cache the RMI stub once it has been located. + * Default is "true". + *

Can be turned off to allow for hot restart of the RMI server. + * In this case, the RMI stub will be fetched for each invocation. + * @see #setLookupStubOnStartup + */ + public void setCacheStub(boolean cacheStub) { + this.cacheStub = cacheStub; + } + + /** + * Set whether to refresh the RMI stub on connect failure. + * Default is "false". + *

Can be turned on to allow for hot restart of the RMI server. + * If a cached RMI stub throws an RMI exception that indicates a + * remote connect failure, a fresh proxy will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { + this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; + } + + /** + * Set a custom RMI client socket factory to use for accessing the RMI registry. + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.registry.LocateRegistry#getRegistry(String, int, RMIClientSocketFactory) + */ + public void setRegistryClientSocketFactory(RMIClientSocketFactory registryClientSocketFactory) { + this.registryClientSocketFactory = registryClientSocketFactory; + } + + + public void afterPropertiesSet() { + super.afterPropertiesSet(); + prepare(); + } + + /** + * Fetches RMI stub on startup, if necessary. + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setLookupStubOnStartup + * @see #lookupStub + */ + public void prepare() throws RemoteLookupFailureException { + // Cache RMI stub on initialization? + if (this.lookupStubOnStartup) { + Remote remoteObj = lookupStub(); + if (logger.isDebugEnabled()) { + if (remoteObj instanceof RmiInvocationHandler) { + logger.debug("RMI stub [" + getServiceUrl() + "] is an RMI invoker"); + } + else if (getServiceInterface() != null) { + boolean isImpl = getServiceInterface().isInstance(remoteObj); + logger.debug("Using service interface [" + getServiceInterface().getName() + + "] for RMI stub [" + getServiceUrl() + "] - " + + (!isImpl ? "not " : "") + "directly implemented"); + } + } + if (this.cacheStub) { + this.cachedStub = remoteObj; + } + } + } + + /** + * Create the RMI stub, typically by looking it up. + *

Called on interceptor initialization if "cacheStub" is "true"; + * else called for each invocation by {@link #getStub()}. + *

The default implementation looks up the service URL via + * java.rmi.Naming. This can be overridden in subclasses. + * @return the RMI stub to store in this interceptor + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setCacheStub + * @see java.rmi.Naming#lookup + */ + protected Remote lookupStub() throws RemoteLookupFailureException { + try { + Remote stub = null; + if (this.registryClientSocketFactory != null) { + // RMIClientSocketFactory specified for registry access. + // Unfortunately, due to RMI API limitations, this means + // that we need to parse the RMI URL ourselves and perform + // straight LocateRegistry.getRegistry/Registry.lookup calls. + URL url = new URL(null, getServiceUrl(), new DummyURLStreamHandler()); + String protocol = url.getProtocol(); + if (protocol != null && !"rmi".equals(protocol)) { + throw new MalformedURLException("Invalid URL scheme '" + protocol + "'"); + } + String host = url.getHost(); + int port = url.getPort(); + String name = url.getPath(); + if (name != null && name.startsWith("/")) { + name = name.substring(1); + } + Registry registry = LocateRegistry.getRegistry(host, port, this.registryClientSocketFactory); + stub = registry.lookup(name); + } + else { + // Can proceed with standard RMI lookup API... + stub = Naming.lookup(getServiceUrl()); + } + if (logger.isDebugEnabled()) { + logger.debug("Located RMI stub with URL [" + getServiceUrl() + "]"); + } + return stub; + } + catch (MalformedURLException ex) { + throw new RemoteLookupFailureException("Service URL [" + getServiceUrl() + "] is invalid", ex); + } + catch (NotBoundException ex) { + throw new RemoteLookupFailureException( + "Could not find RMI service [" + getServiceUrl() + "] in RMI registry", ex); + } + catch (RemoteException ex) { + throw new RemoteLookupFailureException("Lookup of RMI stub failed", ex); + } + } + + /** + * Return the RMI stub to use. Called for each invocation. + *

The default implementation returns the stub created on initialization, + * if any. Else, it invokes {@link #lookupStub} to get a new stub for + * each invocation. This can be overridden in subclasses, for example in + * order to cache a stub for a given amount of time before recreating it, + * or to test the stub whether it is still alive. + * @return the RMI stub to use for an invocation + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #lookupStub + */ + protected Remote getStub() throws RemoteLookupFailureException { + if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { + return (this.cachedStub != null ? this.cachedStub : lookupStub()); + } + else { + synchronized (this.stubMonitor) { + if (this.cachedStub == null) { + this.cachedStub = lookupStub(); + } + return this.cachedStub; + } + } + } + + + /** + * Fetches an RMI stub and delegates to doInvoke. + * If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getStub + * @see #doInvoke(MethodInvocation, Remote) + * @see #refreshAndRetry + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public Object invoke(MethodInvocation invocation) throws Throwable { + Remote stub = getStub(); + try { + return doInvoke(invocation, stub); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

The default implementation delegates to + * {@link RmiClientInterceptorUtils#isConnectFailure}. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + */ + protected boolean isConnectFailure(RemoteException ex) { + return RmiClientInterceptorUtils.isConnectFailure(ex); + } + + /** + * Refresh the stub and retry the remote invocation if necessary. + *

If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, + * if it failed as well + * @see #setRefreshStubOnConnectFailure + * @see #doInvoke + */ + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshStubOnConnectFailure) { + String msg = "Could not connect to RMI service [" + getServiceUrl() + "] - retrying"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex); + } + else if (logger.isWarnEnabled()) { + logger.warn(msg); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the RMI stub and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + Remote freshStub = null; + synchronized (this.stubMonitor) { + this.cachedStub = null; + freshStub = lookupStub(); + if (this.cacheStub) { + this.cachedStub = freshStub; + } + } + return doInvoke(invocation, freshStub); + } + + /** + * Perform the given invocation on the given RMI stub. + * @param invocation the AOP method invocation + * @param stub the RMI stub to invoke + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + protected Object doInvoke(MethodInvocation invocation, Remote stub) throws Throwable { + if (stub instanceof RmiInvocationHandler) { + // RMI invoker + try { + return doInvoke(invocation, (RmiInvocationHandler) stub); + } + catch (RemoteException ex) { + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), ex, isConnectFailure(ex), getServiceUrl()); + } + catch (InvocationTargetException ex) { + Throwable exToThrow = ex.getTargetException(); + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + catch (Throwable ex) { + throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + + "] failed in RMI service [" + getServiceUrl() + "]", ex); + } + } + else { + // traditional RMI stub + try { + return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + RemoteException rex = (RemoteException) targetEx; + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), rex, isConnectFailure(rex), getServiceUrl()); + } + else { + throw targetEx; + } + } + } + } + + /** + * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. + *

The default implementation delegates to {@link #createRemoteInvocation}. + * @param methodInvocation the current AOP method invocation + * @param invocationHandler the RmiInvocationHandler to apply the invocation to + * @return the invocation result + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see org.springframework.remoting.support.RemoteInvocation + */ + protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { + return "RMI invoker proxy for service URL [" + getServiceUrl() + "]"; + } + + return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); + } + + + /** + * Dummy URLStreamHandler that's just specified to suppress the standard + * java.net.URL URLStreamHandler lookup, to be able to + * use the standard URL class for parsing "rmi:..." URLs. + */ + private static class DummyURLStreamHandler extends URLStreamHandler { + + protected URLConnection openConnection(URL url) throws IOException { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java new file mode 100644 index 00000000000..b26f6e43b64 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java @@ -0,0 +1,230 @@ +/* + * 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.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.ConnectException; +import java.rmi.ConnectIOException; +import java.rmi.NoSuchObjectException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.StubNotFoundException; +import java.rmi.UnknownHostException; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.omg.CORBA.COMM_FAILURE; +import org.omg.CORBA.CompletionStatus; +import org.omg.CORBA.NO_RESPONSE; +import org.omg.CORBA.SystemException; + +import org.springframework.remoting.RemoteAccessException; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteProxyFailureException; +import org.springframework.util.ReflectionUtils; + +/** + * Factored-out methods for performing invocations within an RMI client. + * Can handle both RMI and non-RMI service interfaces working on an RMI stub. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public abstract class RmiClientInterceptorUtils { + + private static final String ORACLE_CONNECTION_EXCEPTION = "com.evermind.server.rmi.RMIConnectionException"; + + private static final Log logger = LogFactory.getLog(RmiClientInterceptorUtils.class); + + + /** + * Apply the given method invocation to the given RMI stub. + *

Delegates to the corresponding method if the RMI stub does not directly + * implement the invoked method. This typically happens when a non-RMI service + * interface is used for an RMI service. The methods of such a service interface + * have to match the RMI stub methods, but they typically don't declare + * java.rmi.RemoteException: A RemoteException thrown by the RMI stub + * will be automatically converted to Spring's RemoteAccessException. + * @deprecated as of Spring 2.5, in favor of {@link #invokeRemoteMethod} + */ + public static Object invoke(MethodInvocation invocation, Remote stub, String serviceName) throws Throwable { + try { + return invokeRemoteMethod(invocation, stub); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + RemoteException rex = (RemoteException) targetEx; + throw convertRmiAccessException(invocation.getMethod(), rex, serviceName); + } + else { + throw targetEx; + } + } + } + + /** + * Perform a raw method invocation on the given RMI stub, + * letting reflection exceptions through as-is. + * @deprecated as of Spring 2.5, in favor of {@link #invokeRemoteMethod} + */ + public static Object doInvoke(MethodInvocation invocation, Remote stub) throws InvocationTargetException { + return invokeRemoteMethod(invocation, stub); + } + + /** + * Perform a raw method invocation on the given RMI stub, + * letting reflection exceptions through as-is. + * @param invocation the AOP MethodInvocation + * @param stub the RMI stub + * @return the invocation result, if any + * @throws InvocationTargetException if thrown by reflection + */ + public static Object invokeRemoteMethod(MethodInvocation invocation, Object stub) + throws InvocationTargetException { + + Method method = invocation.getMethod(); + try { + if (method.getDeclaringClass().isInstance(stub)) { + // directly implemented + return method.invoke(stub, invocation.getArguments()); + } + else { + // not directly implemented + Method stubMethod = stub.getClass().getMethod(method.getName(), method.getParameterTypes()); + return stubMethod.invoke(stub, invocation.getArguments()); + } + } + catch (InvocationTargetException ex) { + throw ex; + } + catch (NoSuchMethodException ex) { + throw new RemoteProxyFailureException("No matching RMI stub method found for: " + method, ex); + } + catch (Throwable ex) { + throw new RemoteProxyFailureException("Invocation of RMI stub method failed: " + method, ex); + } + } + + /** + * Wrap the given arbitrary exception that happened during remote access + * in either a RemoteException or a Spring RemoteAccessException (if the + * method signature does not support RemoteException). + *

Only call this for remote access exceptions, not for exceptions + * thrown by the target service itself! + * @param method the invoked method + * @param ex the exception that happened, to be used as cause for the + * RemoteAccessException or RemoteException + * @param message the message for the RemoteAccessException respectively + * RemoteException + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException(Method method, Throwable ex, String message) { + if (logger.isDebugEnabled()) { + logger.debug(message, ex); + } + if (ReflectionUtils.declaresException(method, RemoteException.class)) { + return new RemoteException(message, ex); + } + else { + return new RemoteAccessException(message, ex); + } + } + + /** + * Convert the given RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not + * support RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @param serviceName the name of the service (for debugging purposes) + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException(Method method, RemoteException ex, String serviceName) { + return convertRmiAccessException(method, ex, isConnectFailure(ex), serviceName); + } + + /** + * Convert the given RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not + * support RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @param isConnectFailure whether the given exception should be considered + * a connect failure + * @param serviceName the name of the service (for debugging purposes) + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException( + Method method, RemoteException ex, boolean isConnectFailure, String serviceName) { + + if (logger.isDebugEnabled()) { + logger.debug("Remote service [" + serviceName + "] threw exception", ex); + } + if (ReflectionUtils.declaresException(method, ex.getClass())) { + return ex; + } + else { + if (isConnectFailure) { + return new RemoteConnectFailureException("Could not connect to remote service [" + serviceName + "]", ex); + } + else { + return new RemoteAccessException("Could not access remote service [" + serviceName + "]", ex); + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

Treats RMI's ConnectException, ConnectIOException, UnknownHostException, + * NoSuchObjectException and StubNotFoundException as connect failure, + * as well as Oracle's OC4J com.evermind.server.rmi.RMIConnectionException + * (which doesn't derive from from any well-known RMI connect exception). + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.UnknownHostException + * @see java.rmi.NoSuchObjectException + * @see java.rmi.StubNotFoundException + */ + public static boolean isConnectFailure(RemoteException ex) { + return (ex instanceof ConnectException || ex instanceof ConnectIOException || + ex instanceof UnknownHostException || ex instanceof NoSuchObjectException || + ex instanceof StubNotFoundException || isCorbaConnectFailure(ex.getCause()) || + ORACLE_CONNECTION_EXCEPTION.equals(ex.getClass().getName())); + } + + /** + * Check whether the given RMI exception root cause indicates a CORBA + * connection failure. + *

This is relevant on the IBM JVM, in particular for WebSphere EJB clients. + *

See the + * IBM website + * for details. + * @param ex the RMI exception to check + */ + private static boolean isCorbaConnectFailure(Throwable ex) { + return ((ex instanceof COMM_FAILURE || ex instanceof NO_RESPONSE) && + ((SystemException) ex).completed == CompletionStatus.COMPLETED_NO); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java new file mode 100644 index 00000000000..0849cd8fe89 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java @@ -0,0 +1,59 @@ +/* + * 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.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.Remote; +import java.rmi.RemoteException; + +import org.springframework.remoting.support.RemoteInvocation; + +/** + * Interface for RMI invocation handlers instances on the server, + * wrapping exported services. A client uses a stub implementing + * this interface to access such a service. + * + *

This is an SPI interface, not to be used directly by applications. + * + * @author Juergen Hoeller + * @since 14.05.2003 + */ +public interface RmiInvocationHandler extends Remote { + + /** + * Return the name of the target interface that this invoker operates on. + * @return the name of the target interface, or null if none + * @throws RemoteException in case of communication errors + * @see RmiServiceExporter#getServiceInterface() + */ + public String getTargetInterfaceName() throws RemoteException; + + /** + * Apply the given invocation to the target object. + *

Called by + * {@link RmiClientInterceptor#doInvoke(org.aopalliance.intercept.MethodInvocation, RmiInvocationHandler)}. + * @param invocation object that encapsulates invocation parameters + * @return the object returned from the invoked method, if any + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + */ + public Object invoke(RemoteInvocation invocation) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java new file mode 100644 index 00000000000..299f5cc6fba --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.RemoteException; + +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.util.Assert; + +/** + * Server-side implementation of {@link RmiInvocationHandler}. An instance + * of this class exists for each remote object. Automatically created + * by {@link RmiServiceExporter} for non-RMI service implementations. + * + *

This is an SPI class, not to be used directly by applications. + * + * @author Juergen Hoeller + * @since 14.05.2003 + * @see RmiServiceExporter + */ +class RmiInvocationWrapper implements RmiInvocationHandler { + + private final Object wrappedObject; + + private final RmiBasedExporter rmiExporter; + + + /** + * Create a new RmiInvocationWrapper for the given object + * @param wrappedObject the object to wrap with an RmiInvocationHandler + * @param rmiExporter the RMI exporter to handle the actual invocation + */ + public RmiInvocationWrapper(Object wrappedObject, RmiBasedExporter rmiExporter) { + Assert.notNull(wrappedObject, "Object to wrap is required"); + Assert.notNull(rmiExporter, "RMI exporter is required"); + this.wrappedObject = wrappedObject; + this.rmiExporter = rmiExporter; + } + + + /** + * Exposes the exporter's service interface, if any, as target interface. + * @see RmiBasedExporter#getServiceInterface() + */ + public String getTargetInterfaceName() { + Class ifc = this.rmiExporter.getServiceInterface(); + return (ifc != null ? ifc.getName() : null); + } + + /** + * Delegates the actual invocation handling to the RMI exporter. + * @see RmiBasedExporter#invoke(org.springframework.remoting.support.RemoteInvocation, Object) + */ + public Object invoke(RemoteInvocation invocation) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + return this.rmiExporter.invoke(invocation, this.wrappedObject); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml new file mode 100644 index 00000000000..e42c2d457f3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java new file mode 100644 index 00000000000..203d083f949 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java @@ -0,0 +1,87 @@ +/* + * 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.remoting.rmi; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; + +/** + * FactoryBean for RMI proxies, supporting both conventional RMI services and + * RMI invokers. Exposes the proxied service for use as a bean reference, + * using the specified service interface. Proxies will throw Spring's unchecked + * RemoteAccessException on remote invocation failure instead of RMI's RemoteException. + * + *

The service URL must be a valid RMI URL like "rmi://localhost:1099/myservice". + * RMI invokers work at the RmiInvocationHandler level, using the same invoker stub + * for any service. Service interfaces do not have to extend java.rmi.Remote + * or throw java.rmi.RemoteException. Of course, in and out parameters + * have to be serializable. + * + *

With conventional RMI services, this proxy factory is typically used with the + * RMI service interface. Alternatively, this factory can also proxy a remote RMI + * service with a matching non-RMI business interface, i.e. an interface that mirrors + * the RMI service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

The major advantage of RMI, compared to Hessian and Burlap, is serialization. + * Effectively, any serializable Java object can be transported without hassle. + * Hessian and Burlap have their own (de-)serialization mechanisms, but are + * HTTP-based and thus much easier to setup than RMI. Alternatively, consider + * Spring's HTTP invoker to combine Java serialization with HTTP-based transport. + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see #setServiceInterface + * @see #setServiceUrl + * @see RmiClientInterceptor + * @see RmiServiceExporter + * @see java.rmi.Remote + * @see java.rmi.RemoteException + * @see org.springframework.remoting.RemoteAccessException + * @see org.springframework.remoting.caucho.HessianProxyFactoryBean + * @see org.springframework.remoting.caucho.BurlapProxyFactoryBean + * @see org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean + */ +public class RmiProxyFactoryBean extends RmiClientInterceptor implements FactoryBean, BeanClassLoaderAware { + + private Object serviceProxy; + + + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (getServiceInterface() == null) { + throw new IllegalArgumentException("Property 'serviceInterface' is required"); + } + this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(getBeanClassLoader()); + } + + + public Object getObject() { + return this.serviceProxy; + } + + public Class getObjectType() { + return getServiceInterface(); + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java new file mode 100644 index 00000000000..e87a08042a0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java @@ -0,0 +1,305 @@ +/* + * 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.remoting.rmi; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.UnicastRemoteObject; + +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; + +/** + * FactoryBean that locates a {@link java.rmi.registry.Registry} and + * exposes it for bean references. Can also create a local RMI registry + * on the fly if none exists already. + * + *

Can be used to set up and pass around the actual Registry object to + * applications objects that need to work with RMI. One example for such an + * object that needs to work with RMI is Spring's {@link RmiServiceExporter}, + * which either works with a passed-in Registry reference or falls back to + * the registry as specified by its local properties and defaults. + * + *

Also useful to enforce creation of a local RMI registry at a given port, + * for example for a JMX connector. If used in conjunction with + * {@link org.springframework.jmx.support.ConnectorServerFactoryBean}, + * it is recommended to mark the connector definition (ConnectorServerFactoryBean) + * as "depends-on" the registry definition (RmiRegistryFactoryBean), + * to guarantee starting up the registry first. + * + *

Note: The implementation of this class mirrors the corresponding logic + * in {@link RmiServiceExporter}, and also offers the same customization hooks. + * RmiServiceExporter implements its own registry lookup as a convenience: + * It is very common to simply rely on the registry defaults. + * + * @author Juergen Hoeller + * @since 1.2.3 + * @see RmiServiceExporter#setRegistry + * @see org.springframework.jmx.support.ConnectorServerFactoryBean + * @see java.rmi.registry.Registry + * @see java.rmi.registry.LocateRegistry + */ +public class RmiRegistryFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private String host; + + private int port = Registry.REGISTRY_PORT; + + private RMIClientSocketFactory clientSocketFactory; + + private RMIServerSocketFactory serverSocketFactory; + + private Registry registry; + + private boolean alwaysCreate = false; + + private boolean created = false; + + + /** + * Set the host of the registry for the exported RMI service, + * i.e. rmi://HOST:port/name + *

Default is localhost. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Return the host of the registry for the exported RMI service. + */ + public String getHost() { + return this.host; + } + + /** + * Set the port of the registry for the exported RMI service, + * i.e. rmi://host:PORT/name + *

Default is Registry.REGISTRY_PORT (1099). + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Return the port of the registry for the exported RMI service. + */ + public int getPort() { + return this.port; + } + + /** + * Set a custom RMI client socket factory to use for the RMI registry. + *

If the given object also implements java.rmi.server.RMIServerSocketFactory, + * it will automatically be registered as server socket factory too. + * @see #setServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see java.rmi.registry.LocateRegistry#getRegistry(String, int, java.rmi.server.RMIClientSocketFactory) + */ + public void setClientSocketFactory(RMIClientSocketFactory clientSocketFactory) { + this.clientSocketFactory = clientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for the RMI registry. + *

Only needs to be specified when the client socket factory does not + * implement java.rmi.server.RMIServerSocketFactory already. + * @see #setClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see java.rmi.registry.LocateRegistry#createRegistry(int, RMIClientSocketFactory, java.rmi.server.RMIServerSocketFactory) + */ + public void setServerSocketFactory(RMIServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + /** + * Set whether to always create the registry in-process, + * not attempting to locate an existing registry at the specified port. + *

Default is "false". Switch this flag to "true" in order to avoid + * the overhead of locating an existing registry when you always + * intend to create a new registry in any case. + */ + public void setAlwaysCreate(boolean alwaysCreate) { + this.alwaysCreate = alwaysCreate; + } + + + public void afterPropertiesSet() throws Exception { + // Check socket factories for registry. + if (this.clientSocketFactory instanceof RMIServerSocketFactory) { + this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory; + } + if ((this.clientSocketFactory != null && this.serverSocketFactory == null) || + (this.clientSocketFactory == null && this.serverSocketFactory != null)) { + throw new IllegalArgumentException( + "Both RMIClientSocketFactory and RMIServerSocketFactory or none required"); + } + + // Fetch RMI registry to expose. + this.registry = getRegistry(this.host, this.port, this.clientSocketFactory, this.serverSocketFactory); + } + + + /** + * Locate or create the RMI registry. + * @param registryHost the registry host to use (if this is specified, + * no implicit creation of a RMI registry will happen) + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws java.rmi.RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(String registryHost, int registryPort, + RMIClientSocketFactory clientSocketFactory, RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (registryHost != null) { + // Host explictly specified: only lookup possible. + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]"); + } + Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + + else { + return getRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + /** + * Locate or create the RMI registry. + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry( + int registryPort, RMIClientSocketFactory clientSocketFactory, RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (clientSocketFactory != null) { + if (this.alwaysCreate) { + logger.info("Creating new RMI registry"); + this.created = true; + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "', using custom socket factory"); + } + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(null, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.debug("RMI registry access threw exception", ex); + logger.info("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + this.created = true; + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + else { + return getRegistry(registryPort); + } + } + + /** + * Locate or create the RMI registry. + * @param registryPort the registry port to use + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort) throws RemoteException { + if (this.alwaysCreate) { + logger.info("Creating new RMI registry"); + this.created = true; + return LocateRegistry.createRegistry(registryPort); + } + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "'"); + } + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(registryPort); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.debug("RMI registry access threw exception", ex); + logger.info("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + this.created = true; + return LocateRegistry.createRegistry(registryPort); + } + } + + /** + * Test the given RMI registry, calling some operation on it to + * check whether it is still active. + *

Default implementation calls Registry.list(). + * @param registry the RMI registry to test + * @throws RemoteException if thrown by registry methods + * @see java.rmi.registry.Registry#list() + */ + protected void testRegistry(Registry registry) throws RemoteException { + registry.list(); + } + + + public Object getObject() throws Exception { + return this.registry; + } + + public Class getObjectType() { + return (this.registry != null ? this.registry.getClass() : Registry.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Unexport the RMI registry on bean factory shutdown, + * provided that this bean actually created a registry. + */ + public void destroy() throws RemoteException { + if (this.created) { + logger.info("Unexporting RMI registry"); + UnicastRemoteObject.unexportObject(this.registry, true); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java new file mode 100644 index 00000000000..ae6e9ba42ad --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java @@ -0,0 +1,451 @@ +/* + * 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.remoting.rmi; + +import java.rmi.AlreadyBoundException; +import java.rmi.NoSuchObjectException; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.UnicastRemoteObject; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * RMI exporter that exposes the specified service as RMI object with the specified name. + * Such services can be accessed via plain RMI or via {@link RmiProxyFactoryBean}. + * Also supports exposing any non-RMI service via RMI invokers, to be accessed via + * {@link RmiClientInterceptor} / {@link RmiProxyFactoryBean}'s automatic detection + * of such invokers. + * + *

With an RMI invoker, RMI communication works on the {@link RmiInvocationHandler} + * level, needing only one stub for any service. Service interfaces do not have to + * extend java.rmi.Remote or throw java.rmi.RemoteException + * on all methods, but in and out parameters have to be serializable. + * + *

The major advantage of RMI, compared to Hessian and Burlap, is serialization. + * Effectively, any serializable Java object can be transported without hassle. + * Hessian and Burlap have their own (de-)serialization mechanisms, but are + * HTTP-based and thus much easier to setup than RMI. Alternatively, consider + * Spring's HTTP invoker to combine Java serialization with HTTP-based transport. + * + *

Note: RMI makes a best-effort attempt to obtain the fully qualified host name. + * If one cannot be determined, it will fall back and use the IP address. Depending + * on your network configuration, in some cases it will resolve the IP to the loopback + * address. To ensure that RMI will use the host name bound to the correct network + * interface, you should pass the java.rmi.server.hostname property to the + * JVM that will export the registry and/or the service using the "-D" JVM argument. + * For example: -Djava.rmi.server.hostname=myserver.com + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see RmiClientInterceptor + * @see RmiProxyFactoryBean + * @see java.rmi.Remote + * @see java.rmi.RemoteException + * @see org.springframework.remoting.caucho.HessianServiceExporter + * @see org.springframework.remoting.caucho.BurlapServiceExporter + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter + */ +public class RmiServiceExporter extends RmiBasedExporter implements InitializingBean, DisposableBean { + + private String serviceName; + + private int servicePort = 0; // anonymous port + + private RMIClientSocketFactory clientSocketFactory; + + private RMIServerSocketFactory serverSocketFactory; + + private Registry registry; + + private String registryHost; + + private int registryPort = Registry.REGISTRY_PORT; + + private RMIClientSocketFactory registryClientSocketFactory; + + private RMIServerSocketFactory registryServerSocketFactory; + + private boolean alwaysCreateRegistry = false; + + private boolean replaceExistingBinding = true; + + private Remote exportedObject; + + + /** + * Set the name of the exported RMI service, + * i.e. rmi://host:port/NAME + */ + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + /** + * Set the port that the exported RMI service will use. + *

Default is 0 (anonymous port). + */ + public void setServicePort(int servicePort) { + this.servicePort = servicePort; + } + + /** + * Set a custom RMI client socket factory to use for exporting the service. + *

If the given object also implements java.rmi.server.RMIServerSocketFactory, + * it will automatically be registered as server socket factory too. + * @see #setServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see UnicastRemoteObject#exportObject(Remote, int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setClientSocketFactory(RMIClientSocketFactory clientSocketFactory) { + this.clientSocketFactory = clientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for exporting the service. + *

Only needs to be specified when the client socket factory does not + * implement java.rmi.server.RMIServerSocketFactory already. + * @see #setClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see UnicastRemoteObject#exportObject(Remote, int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setServerSocketFactory(RMIServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + /** + * Specify the RMI registry to register the exported service with. + * Typically used in combination with RmiRegistryFactoryBean. + *

Alternatively, you can specify all registry properties locally. + * This exporter will then try to locate the specified registry, + * automatically creating a new local one if appropriate. + *

Default is a local registry at the default port (1099), + * created on the fly if necessary. + * @see RmiRegistryFactoryBean + * @see #setRegistryHost + * @see #setRegistryPort + * @see #setRegistryClientSocketFactory + * @see #setRegistryServerSocketFactory + */ + public void setRegistry(Registry registry) { + this.registry = registry; + } + + /** + * Set the host of the registry for the exported RMI service, + * i.e. rmi://HOST:port/name + *

Default is localhost. + */ + public void setRegistryHost(String registryHost) { + this.registryHost = registryHost; + } + + /** + * Set the port of the registry for the exported RMI service, + * i.e. rmi://host:PORT/name + *

Default is Registry.REGISTRY_PORT (1099). + * @see java.rmi.registry.Registry#REGISTRY_PORT + */ + public void setRegistryPort(int registryPort) { + this.registryPort = registryPort; + } + + /** + * Set a custom RMI client socket factory to use for the RMI registry. + *

If the given object also implements java.rmi.server.RMIServerSocketFactory, + * it will automatically be registered as server socket factory too. + * @see #setRegistryServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see LocateRegistry#getRegistry(String, int, RMIClientSocketFactory) + */ + public void setRegistryClientSocketFactory(RMIClientSocketFactory registryClientSocketFactory) { + this.registryClientSocketFactory = registryClientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for the RMI registry. + *

Only needs to be specified when the client socket factory does not + * implement java.rmi.server.RMIServerSocketFactory already. + * @see #setRegistryClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see LocateRegistry#createRegistry(int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setRegistryServerSocketFactory(RMIServerSocketFactory registryServerSocketFactory) { + this.registryServerSocketFactory = registryServerSocketFactory; + } + + /** + * Set whether to always create the registry in-process, + * not attempting to locate an existing registry at the specified port. + *

Default is "false". Switch this flag to "true" in order to avoid + * the overhead of locating an existing registry when you always + * intend to create a new registry in any case. + */ + public void setAlwaysCreateRegistry(boolean alwaysCreateRegistry) { + this.alwaysCreateRegistry = alwaysCreateRegistry; + } + + /** + * Set whether to replace an existing binding in the RMI registry, + * that is, whether to simply override an existing binding with the + * specified service in case of a naming conflict in the registry. + *

Default is "true", assuming that an existing binding for this + * exporter's service name is an accidental leftover from a previous + * execution. Switch this to "false" to make the exporter fail in such + * a scenario, indicating that there was already an RMI object bound. + */ + public void setReplaceExistingBinding(boolean replaceExistingBinding) { + this.replaceExistingBinding = replaceExistingBinding; + } + + + public void afterPropertiesSet() throws RemoteException { + prepare(); + } + + /** + * Initialize this service exporter, registering the service as RMI object. + *

Creates an RMI registry on the specified port if none exists. + * @throws RemoteException if service registration failed + */ + public void prepare() throws RemoteException { + checkService(); + + if (this.serviceName == null) { + throw new IllegalArgumentException("Property 'serviceName' is required"); + } + + // Check socket factories for exported object. + if (this.clientSocketFactory instanceof RMIServerSocketFactory) { + this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory; + } + if ((this.clientSocketFactory != null && this.serverSocketFactory == null) || + (this.clientSocketFactory == null && this.serverSocketFactory != null)) { + throw new IllegalArgumentException( + "Both RMIClientSocketFactory and RMIServerSocketFactory or none required"); + } + + // Check socket factories for RMI registry. + if (this.registryClientSocketFactory instanceof RMIServerSocketFactory) { + this.registryServerSocketFactory = (RMIServerSocketFactory) this.registryClientSocketFactory; + } + if (this.registryClientSocketFactory == null && this.registryServerSocketFactory != null) { + throw new IllegalArgumentException( + "RMIServerSocketFactory without RMIClientSocketFactory for registry not supported"); + } + + // Determine RMI registry to use. + if (this.registry == null) { + this.registry = getRegistry(this.registryHost, this.registryPort, + this.registryClientSocketFactory, this.registryServerSocketFactory); + } + + // Initialize and cache exported object. + this.exportedObject = getObjectToExport(); + + if (logger.isInfoEnabled()) { + logger.info("Binding service '" + this.serviceName + "' to RMI registry: " + this.registry); + } + + // Export RMI object. + if (this.clientSocketFactory != null) { + UnicastRemoteObject.exportObject( + this.exportedObject, this.servicePort, this.clientSocketFactory, this.serverSocketFactory); + } + else { + UnicastRemoteObject.exportObject(this.exportedObject, this.servicePort); + } + + // Bind RMI object to registry. + try { + if (this.replaceExistingBinding) { + this.registry.rebind(this.serviceName, this.exportedObject); + } + else { + this.registry.bind(this.serviceName, this.exportedObject); + } + } + catch (AlreadyBoundException ex) { + // Already an RMI object bound for the specified service name... + unexportObjectSilently(); + throw new IllegalStateException( + "Already an RMI object bound for name '" + this.serviceName + "': " + ex.toString()); + } + catch (RemoteException ex) { + // Registry binding failed: let's unexport the RMI object as well. + unexportObjectSilently(); + throw ex; + } + } + + + /** + * Locate or create the RMI registry for this exporter. + * @param registryHost the registry host to use (if this is specified, + * no implicit creation of a RMI registry will happen) + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(String registryHost, int registryPort, + RMIClientSocketFactory clientSocketFactory, RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (registryHost != null) { + // Host explictly specified: only lookup possible. + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]"); + } + Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + + else { + return getRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + /** + * Locate or create the RMI registry for this exporter. + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry( + int registryPort, RMIClientSocketFactory clientSocketFactory, RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (clientSocketFactory != null) { + if (this.alwaysCreateRegistry) { + logger.info("Creating new RMI registry"); + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "', using custom socket factory"); + } + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(null, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.debug("RMI registry access threw exception", ex); + logger.info("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + else { + return getRegistry(registryPort); + } + } + + /** + * Locate or create the RMI registry for this exporter. + * @param registryPort the registry port to use + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort) throws RemoteException { + if (this.alwaysCreateRegistry) { + logger.info("Creating new RMI registry"); + return LocateRegistry.createRegistry(registryPort); + } + if (logger.isInfoEnabled()) { + logger.info("Looking for RMI registry at port '" + registryPort + "'"); + } + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(registryPort); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.debug("RMI registry access threw exception", ex); + logger.info("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + return LocateRegistry.createRegistry(registryPort); + } + } + + /** + * Test the given RMI registry, calling some operation on it to + * check whether it is still active. + *

Default implementation calls Registry.list(). + * @param registry the RMI registry to test + * @throws RemoteException if thrown by registry methods + * @see java.rmi.registry.Registry#list() + */ + protected void testRegistry(Registry registry) throws RemoteException { + registry.list(); + } + + + /** + * Unbind the RMI service from the registry on bean factory shutdown. + */ + public void destroy() throws RemoteException { + if (logger.isInfoEnabled()) { + logger.info("Unbinding RMI service '" + this.serviceName + + "' from registry at port '" + this.registryPort + "'"); + } + try { + this.registry.unbind(this.serviceName); + } + catch (NotBoundException ex) { + if (logger.isWarnEnabled()) { + logger.warn("RMI service '" + this.serviceName + "' is not bound to registry at port '" + + this.registryPort + "' anymore", ex); + } + } + finally { + unexportObjectSilently(); + } + } + + /** + * Unexport the registered RMI object, logging any exception that arises. + */ + private void unexportObjectSilently() { + try { + UnicastRemoteObject.unexportObject(this.exportedObject, true); + } + catch (NoSuchObjectException ex) { + if (logger.isWarnEnabled()) { + logger.warn("RMI object for service '" + this.serviceName + "' isn't exported anymore", ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/rmi/package.html b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/package.html new file mode 100644 index 00000000000..31f487e729b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/rmi/package.html @@ -0,0 +1,9 @@ + + + +Remoting classes for conventional RMI and transparent remoting via +RMI invokers. Provides a proxy factory for accessing RMI services, +and an exporter for making beans available to RMI clients. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java b/org.springframework.context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java new file mode 100644 index 00000000000..5785c6a1ee5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java @@ -0,0 +1,64 @@ +/* + * 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.remoting.soap; + +import javax.xml.namespace.QName; + +import org.springframework.remoting.RemoteInvocationFailureException; + +/** + * RemoteInvocationFailureException subclass that provides the details + * of a SOAP fault. + * + * @author Juergen Hoeller + * @since 2.5 + * @see javax.xml.rpc.soap.SOAPFaultException + * @see javax.xml.ws.soap.SOAPFaultException + */ +public abstract class SoapFaultException extends RemoteInvocationFailureException { + + /** + * Constructor for SoapFaultException. + * @param msg the detail message + * @param cause the root cause from the SOAP API in use + */ + protected SoapFaultException(String msg, Throwable cause) { + super(msg, cause); + } + + + /** + * Return the SOAP fault code. + */ + public abstract String getFaultCode(); + + /** + * Return the SOAP fault code as a QName object. + */ + public abstract QName getFaultCodeAsQName(); + + /** + * Return the descriptive SOAP fault string. + */ + public abstract String getFaultString(); + + /** + * Return the actor that caused this fault. + */ + public abstract String getFaultActor(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/soap/package.html b/org.springframework.context/src/main/java/org/springframework/remoting/soap/package.html new file mode 100644 index 00000000000..8a93793c33f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/soap/package.html @@ -0,0 +1,7 @@ + + + +SOAP-specific exceptions and support classes for Spring's remoting subsystem. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java new file mode 100644 index 00000000000..303ed12b6f4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java @@ -0,0 +1,41 @@ +/* + * 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.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link RemoteInvocationExecutor} interface. + * Simply delegates to {@link RemoteInvocation}'s invoke method. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocation#invoke + */ +public class DefaultRemoteInvocationExecutor implements RemoteInvocationExecutor { + + public Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException{ + + Assert.notNull(invocation, "RemoteInvocation must not be null"); + Assert.notNull(targetObject, "Target object must not be null"); + return invocation.invoke(targetObject); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java new file mode 100644 index 00000000000..a1baffb853b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java @@ -0,0 +1,34 @@ +/* + * 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.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Default implementation of the {@link RemoteInvocationFactory} interface. + * Simply creates a new standard {@link RemoteInvocation} object. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class DefaultRemoteInvocationFactory implements RemoteInvocationFactory { + + public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return new RemoteInvocation(methodInvocation); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java new file mode 100644 index 00000000000..b69f61cf1d9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java @@ -0,0 +1,62 @@ +/* + * 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.remoting.support; + +/** + * Abstract base class for classes that access a remote service. + * Provides a "serviceInterface" bean property. + * + *

Note that the service interface being used will show some signs of + * remotability, like the granularity of method calls that it offers. + * Furthermore, it has to have serializable arguments etc. + * + *

Accessors are supposed to throw Spring's generic + * {@link org.springframework.remoting.RemoteAccessException} in case + * of remote invocation failure, provided that the service interface + * does not declare java.rmi.RemoteException. + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + */ +public abstract class RemoteAccessor extends RemotingSupport { + + private Class serviceInterface; + + + /** + * Set the interface of the service to access. + * The interface must be suitable for the particular service and remoting strategy. + *

Typically required to be able to create a suitable service proxy, + * but can also be optional if the lookup returns a typed proxy. + */ + public void setServiceInterface(Class serviceInterface) { + if (serviceInterface != null && !serviceInterface.isInterface()) { + throw new IllegalArgumentException("'serviceInterface' must be an interface"); + } + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to access. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteExporter.java new file mode 100644 index 00000000000..c4e64e90d92 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteExporter.java @@ -0,0 +1,185 @@ +/* + * 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.remoting.support; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for classes that export a remote service. + * Provides "service" and "serviceInterface" bean properties. + * + *

Note that the service interface being used will show some signs of + * remotability, like the granularity of method calls that it offers. + * Furthermore, it has to have serializable arguments etc. + * + * @author Juergen Hoeller + * @since 26.12.2003 + */ +public abstract class RemoteExporter extends RemotingSupport { + + private Object service; + + private Class serviceInterface; + + private Boolean registerTraceInterceptor; + + private Object[] interceptors; + + + /** + * Set the service to export. + * Typically populated via a bean reference. + */ + public void setService(Object service) { + this.service = service; + } + + /** + * Return the service to export. + */ + public Object getService() { + return this.service; + } + + /** + * Set the interface of the service to export. + * The interface must be suitable for the particular service and remoting strategy. + */ + public void setServiceInterface(Class serviceInterface) { + if (serviceInterface != null && !serviceInterface.isInterface()) { + throw new IllegalArgumentException("'serviceInterface' must be an interface"); + } + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to export. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + + /** + * Set whether to register a RemoteInvocationTraceInterceptor for exported + * services. Only applied when a subclass uses getProxyForService + * for creating the proxy to expose. + *

Default is "true". RemoteInvocationTraceInterceptor's most important value + * is that it logs exception stacktraces on the server, before propagating an + * exception to the client. Note that RemoteInvocationTraceInterceptor will not + * be registered by default if the "interceptors" property has been specified. + * @see #setInterceptors + * @see #getProxyForService + * @see RemoteInvocationTraceInterceptor + */ + public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) { + this.registerTraceInterceptor = Boolean.valueOf(registerTraceInterceptor); + } + + /** + * Set additional interceptors (or advisors) to be applied before the + * remote endpoint, e.g. a PerformanceMonitorInterceptor. + *

You may specify any AOP Alliance MethodInterceptors or other + * Spring AOP Advices, as well as Spring AOP Advisors. + * @see #getProxyForService + * @see org.springframework.aop.interceptor.PerformanceMonitorInterceptor + */ + public void setInterceptors(Object[] interceptors) { + this.interceptors = interceptors; + } + + + /** + * Check whether the service reference has been set. + * @see #setService + */ + protected void checkService() throws IllegalArgumentException { + if (getService() == null) { + throw new IllegalArgumentException("Property 'service' is required"); + } + } + + /** + * Check whether a service reference has been set, + * and whether it matches the specified service. + * @see #setServiceInterface + * @see #setService + */ + protected void checkServiceInterface() throws IllegalArgumentException { + Class serviceInterface = getServiceInterface(); + Object service = getService(); + if (serviceInterface == null) { + throw new IllegalArgumentException("Property 'serviceInterface' is required"); + } + if (service instanceof String) { + throw new IllegalArgumentException("Service [" + service + "] is a String " + + "rather than an actual service reference: Have you accidentally specified " + + "the service bean name as value instead of as reference?"); + } + if (!serviceInterface.isInstance(service)) { + throw new IllegalArgumentException("Service interface [" + serviceInterface.getName() + + "] needs to be implemented by service [" + service + "] of class [" + + service.getClass().getName() + "]"); + } + } + + /** + * Get a proxy for the given service object, implementing the specified + * service interface. + *

Used to export a proxy that does not expose any internals but just + * a specific interface intended for remote access. Furthermore, a + * {@link RemoteInvocationTraceInterceptor} will be registered (by default). + * @return the proxy + * @see #setServiceInterface + * @see #setRegisterTraceInterceptor + * @see RemoteInvocationTraceInterceptor + */ + protected Object getProxyForService() { + checkService(); + checkServiceInterface(); + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addInterface(getServiceInterface()); + if (this.registerTraceInterceptor != null ? + this.registerTraceInterceptor.booleanValue() : this.interceptors == null) { + proxyFactory.addAdvice(new RemoteInvocationTraceInterceptor(getExporterName())); + } + if (this.interceptors != null) { + AdvisorAdapterRegistry adapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + for (int i = 0; i < this.interceptors.length; i++) { + proxyFactory.addAdvisor(adapterRegistry.wrap(this.interceptors[i])); + } + } + proxyFactory.setTarget(getService()); + return proxyFactory.getProxy(getBeanClassLoader()); + } + + /** + * Return a short name for this exporter. + * Used for tracing of remote invocations. + *

Default is the unqualified class name (without package). + * Can be overridden in subclasses. + * @see #getProxyForService + * @see RemoteInvocationTraceInterceptor + * @see org.springframework.util.ClassUtils#getShortName + */ + protected String getExporterName() { + return ClassUtils.getShortName(getClass()); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java new file mode 100644 index 00000000000..aef851d9907 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java @@ -0,0 +1,214 @@ +/* + * 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.remoting.support; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.util.ClassUtils; + +/** + * Encapsulates a remote invocation, providing core method invocation properties + * in a serializable fashion. Used for RMI and HTTP-based serialization invokers. + * + *

This is an SPI class, typically not used directly by applications. + * Can be subclassed for additional invocation parameters. + * + * @author Juergen Hoeller + * @since 25.02.2004 + * @see RemoteInvocationResult + * @see RemoteInvocationFactory + * @see RemoteInvocationExecutor + * @see org.springframework.remoting.rmi.RmiProxyFactoryBean + * @see org.springframework.remoting.rmi.RmiServiceExporter + * @see org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter + */ +public class RemoteInvocation implements Serializable { + + /** use serialVersionUID from Spring 1.1 for interoperability */ + private static final long serialVersionUID = 6876024250231820554L; + + + private String methodName; + + private Class[] parameterTypes; + + private Object[] arguments; + + private Map attributes; + + + /** + * Create a new RemoteInvocation for use as JavaBean. + */ + public RemoteInvocation() { + } + + /** + * Create a new RemoteInvocation for the given parameters. + * @param methodName the name of the method to invoke + * @param parameterTypes the parameter types of the method + * @param arguments the arguments for the invocation + */ + public RemoteInvocation(String methodName, Class[] parameterTypes, Object[] arguments) { + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.arguments = arguments; + } + + /** + * Create a new RemoteInvocation for the given AOP method invocation. + * @param methodInvocation the AOP invocation to convert + */ + public RemoteInvocation(MethodInvocation methodInvocation) { + this.methodName = methodInvocation.getMethod().getName(); + this.parameterTypes = methodInvocation.getMethod().getParameterTypes(); + this.arguments = methodInvocation.getArguments(); + } + + + /** + * Set the name of the target method. + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + /** + * Return the name of the target method. + */ + public String getMethodName() { + return this.methodName; + } + + /** + * Set the parameter types of the target method. + */ + public void setParameterTypes(Class[] parameterTypes) { + this.parameterTypes = parameterTypes; + } + + /** + * Return the parameter types of the target method. + */ + public Class[] getParameterTypes() { + return this.parameterTypes; + } + + /** + * Set the arguments for the target method call. + */ + public void setArguments(Object[] arguments) { + this.arguments = arguments; + } + + /** + * Return the arguments for the target method call. + */ + public Object[] getArguments() { + return this.arguments; + } + + + /** + * Add an additional invocation attribute. Useful to add additional + * invocation context without having to subclass RemoteInvocation. + *

Attribute keys have to be unique, and no overriding of existing + * attributes is allowed. + *

The implementation avoids to unnecessarily create the attributes + * Map, to minimize serialization size. + * @param key the attribute key + * @param value the attribute value + * @throws IllegalStateException if the key is already bound + */ + public void addAttribute(String key, Serializable value) throws IllegalStateException { + if (this.attributes == null) { + this.attributes = new HashMap(); + } + if (this.attributes.containsKey(key)) { + throw new IllegalStateException("There is already an attribute with key '" + key + "' bound"); + } + this.attributes.put(key, value); + } + + /** + * Retrieve the attribute for the given key, if any. + *

The implementation avoids to unnecessarily create the attributes + * Map, to minimize serialization size. + * @param key the attribute key + * @return the attribute value, or null if not defined + */ + public Serializable getAttribute(String key) { + if (this.attributes == null) { + return null; + } + return (Serializable) this.attributes.get(key); + } + + /** + * Set the attributes Map. Only here for special purposes: + * Preferably, use {@link #addAttribute} and {@link #getAttribute}. + * @param attributes the attributes Map + * @see #addAttribute + * @see #getAttribute + */ + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + /** + * Return the attributes Map. Mainly here for debugging purposes: + * Preferably, use {@link #addAttribute} and {@link #getAttribute}. + * @return the attributes Map, or null if none created + * @see #addAttribute + * @see #getAttribute + */ + public Map getAttributes() { + return this.attributes; + } + + + /** + * Perform this invocation on the given target object. + * Typically called when a RemoteInvocation is received on the server. + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see java.lang.reflect.Method#invoke + */ + public Object invoke(Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + Method method = targetObject.getClass().getMethod(this.methodName, this.parameterTypes); + return method.invoke(targetObject, this.arguments); + } + + + public String toString() { + return "RemoteInvocation: method name '" + this.methodName + "'; parameter types " + + ClassUtils.classNamesToString(this.parameterTypes); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java new file mode 100644 index 00000000000..4e191912260 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java @@ -0,0 +1,88 @@ +/* + * 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.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Abstract base class for remote service accessors that are based + * on serialization of {@link RemoteInvocation} objects. + * + * Provides a "remoteInvocationFactory" property, with a + * {@link DefaultRemoteInvocationFactory} as default strategy. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setRemoteInvocationFactory + * @see RemoteInvocation + * @see RemoteInvocationFactory + * @see DefaultRemoteInvocationFactory + */ +public abstract class RemoteInvocationBasedAccessor extends UrlBasedRemoteAccessor { + + private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); + + + /** + * Set the RemoteInvocationFactory to use for this accessor. + * Default is a {@link DefaultRemoteInvocationFactory}. + *

A custom invocation factory can add further context information + * to the invocation, for example user credentials. + */ + public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { + this.remoteInvocationFactory = + (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory()); + } + + /** + * Return the RemoteInvocationFactory used by this accessor. + */ + public RemoteInvocationFactory getRemoteInvocationFactory() { + return this.remoteInvocationFactory; + } + + /** + * Create a new RemoteInvocation object for the given AOP method invocation. + *

The default implementation delegates to the configured + * {@link #setRemoteInvocationFactory RemoteInvocationFactory}. + * This can be overridden in subclasses in order to provide custom RemoteInvocation + * subclasses, containing additional invocation parameters (e.g. user credentials). + *

Note that it is preferable to build a custom RemoteInvocationFactory + * as a reusable strategy, instead of overriding this method. + * @param methodInvocation the current AOP method invocation + * @return the RemoteInvocation object + * @see RemoteInvocationFactory#createRemoteInvocation + */ + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation); + } + + /** + * Recreate the invocation result contained in the given RemoteInvocationResult object. + *

The default implementation calls the default recreate() method. + * This can be overridden in subclass to provide custom recreation, potentially + * processing the returned result object. + * @param result the RemoteInvocationResult to recreate + * @return a return value if the invocation result is a successful return + * @throws Throwable if the invocation result is an exception + * @see RemoteInvocationResult#recreate() + */ + protected Object recreateRemoteInvocationResult(RemoteInvocationResult result) throws Throwable { + return result.recreate(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java new file mode 100644 index 00000000000..edd3e72eadf --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java @@ -0,0 +1,122 @@ +/* + * 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.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +/** + * Abstract base class for remote service exporters that are based + * on deserialization of {@link RemoteInvocation} objects. + * + *

Provides a "remoteInvocationExecutor" property, with a + * {@link DefaultRemoteInvocationExecutor} as default strategy. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocationExecutor + * @see DefaultRemoteInvocationExecutor + */ +public abstract class RemoteInvocationBasedExporter extends RemoteExporter { + + private RemoteInvocationExecutor remoteInvocationExecutor = new DefaultRemoteInvocationExecutor(); + + + /** + * Set the RemoteInvocationExecutor to use for this exporter. + * Default is a DefaultRemoteInvocationExecutor. + *

A custom invocation executor can extract further context information + * from the invocation, for example user credentials. + */ + public void setRemoteInvocationExecutor(RemoteInvocationExecutor remoteInvocationExecutor) { + this.remoteInvocationExecutor = remoteInvocationExecutor; + } + + /** + * Return the RemoteInvocationExecutor used by this exporter. + */ + public RemoteInvocationExecutor getRemoteInvocationExecutor() { + return this.remoteInvocationExecutor; + } + + + /** + * Apply the given remote invocation to the given target object. + * The default implementation delegates to the RemoteInvocationExecutor. + *

Can be overridden in subclasses for custom invocation behavior, + * possibly for applying additional invocation parameters from a + * custom RemoteInvocation subclass. Note that it is preferable to use + * a custom RemoteInvocationExecutor which is a reusable strategy. + * @param invocation the remote invocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see RemoteInvocationExecutor#invoke + */ + protected Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (logger.isTraceEnabled()) { + logger.trace("Executing " + invocation); + } + try { + return getRemoteInvocationExecutor().invoke(invocation, targetObject); + } + catch (NoSuchMethodException ex) { + if (logger.isDebugEnabled()) { + logger.warn("Could not find target method for " + invocation, ex); + } + throw ex; + } + catch (IllegalAccessException ex) { + if (logger.isDebugEnabled()) { + logger.warn("Could not access target method for " + invocation, ex); + } + throw ex; + } + catch (InvocationTargetException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Target method failed for " + invocation, ex.getTargetException()); + } + throw ex; + } + } + + /** + * Apply the given remote invocation to the given target object, wrapping + * the invocation result in a serializable RemoteInvocationResult object. + * The default implementation creates a plain RemoteInvocationResult. + *

Can be overridden in subclasses for custom invocation behavior, + * for example to return additional context information. Note that this + * is not covered by the RemoteInvocationExecutor strategy! + * @param invocation the remote invocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @see #invoke + */ + protected RemoteInvocationResult invokeAndCreateResult(RemoteInvocation invocation, Object targetObject) { + try { + Object value = invoke(invocation, targetObject); + return new RemoteInvocationResult(value); + } + catch (Throwable ex) { + return new RemoteInvocationResult(ex); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java new file mode 100644 index 00000000000..4a80c47fae1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java @@ -0,0 +1,49 @@ +/* + * 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.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +/** + * Strategy interface for executing a {@link RemoteInvocation} on a target object. + * + *

Used by {@link org.springframework.remoting.rmi.RmiServiceExporter} (for RMI invokers) + * and by {@link org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DefaultRemoteInvocationFactory + * @see org.springframework.remoting.rmi.RmiServiceExporter#setRemoteInvocationExecutor + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#setRemoteInvocationExecutor + */ +public interface RemoteInvocationExecutor { + + /** + * Perform this invocation on the given target object. + * Typically called when a RemoteInvocation is received on the server. + * @param invocation the RemoteInvocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see java.lang.reflect.Method#invoke + */ + Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java new file mode 100644 index 00000000000..80ca75d69ea --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java @@ -0,0 +1,46 @@ +/* + * 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.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Strategy interface for creating a {@link RemoteInvocation} from an AOP Alliance + * {@link org.aopalliance.intercept.MethodInvocation}. + * + *

Used by {@link org.springframework.remoting.rmi.RmiClientInterceptor} (for RMI invokers) + * and by {@link org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DefaultRemoteInvocationFactory + * @see org.springframework.remoting.rmi.RmiClientInterceptor#setRemoteInvocationFactory + * @see org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor#setRemoteInvocationFactory + */ +public interface RemoteInvocationFactory { + + /** + * Create a serializable RemoteInvocation object from the given AOP + * MethodInvocation. + *

Can be implemented to add custom context information to the + * remote invocation, for example user credentials. + * @param methodInvocation the original AOP MethodInvocation object + * @return the RemoteInvocation object + */ + RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java new file mode 100644 index 00000000000..5df1f53c251 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java @@ -0,0 +1,123 @@ +/* + * 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.remoting.support; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; + +/** + * Encapsulates a remote invocation result, holding a result value or an exception. + * Used for HTTP-based serialization invokers. + * + *

This is an SPI class, typically not used directly by applications. + * Can be subclassed for additional invocation parameters. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocation + */ +public class RemoteInvocationResult implements Serializable { + + /** Use serialVersionUID from Spring 1.1 for interoperability */ + private static final long serialVersionUID = 2138555143707773549L; + + + private Object value; + + private Throwable exception; + + + /** + * Create a new RemoteInvocationResult for the given result value. + * @param value the result value returned by a successful invocation + * of the target method + */ + public RemoteInvocationResult(Object value) { + this.value = value; + } + + /** + * Create a new RemoteInvocationResult for the given exception. + * @param exception the exception thrown by an unsuccessful invocation + * of the target method + */ + public RemoteInvocationResult(Throwable exception) { + this.exception = exception; + } + + + /** + * Return the result value returned by a successful invocation + * of the target method, if any. + * @see #hasException + */ + public Object getValue() { + return this.value; + } + + /** + * Return the exception thrown by an unsuccessful invocation + * of the target method, if any. + * @see #hasException + */ + public Throwable getException() { + return this.exception; + } + + /** + * Return whether this invocation result holds an exception. + * If this returns false, the result value applies + * (even if null). + * @see #getValue + * @see #getException + */ + public boolean hasException() { + return (this.exception != null); + } + + /** + * Return whether this invocation result holds an InvocationTargetException, + * thrown by an invocation of the target method itself. + * @see #hasException() + */ + public boolean hasInvocationTargetException() { + return (this.exception instanceof InvocationTargetException); + } + + + /** + * Recreate the invocation result, either returning the result value + * in case of a successful invocation of the target method, or + * rethrowing the exception thrown by the target method. + * @return the result value, if any + * @throws Throwable the exception, if any + */ + public Object recreate() throws Throwable { + if (this.exception != null) { + Throwable exToThrow = this.exception; + if (this.exception instanceof InvocationTargetException) { + exToThrow = ((InvocationTargetException) this.exception).getTargetException(); + } + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + else { + return this.value; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java new file mode 100644 index 00000000000..d66fe1228d7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java @@ -0,0 +1,101 @@ +/* + * 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.remoting.support; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ClassUtils; + +/** + * AOP Alliance MethodInterceptor for tracing remote invocations. + * Automatically applied by RemoteExporter and its subclasses. + * + *

Logs an incoming remote call as well as the finished processing of a remote call + * at DEBUG level. If the processing of a remote call results in a checked exception, + * the exception will get logged at INFO level; if it results in an unchecked + * exception (or error), the exception will get logged at WARN level. + * + *

The logging of exceptions is particularly useful to save the stacktrace + * information on the server-side rather than just propagating the exception + * to the client (who might or might not log it properly). + * + * @author Juergen Hoeller + * @since 1.2 + * @see RemoteExporter#setRegisterTraceInterceptor + * @see RemoteExporter#getProxyForService + */ +public class RemoteInvocationTraceInterceptor implements MethodInterceptor { + + protected static final Log logger = LogFactory.getLog(RemoteInvocationTraceInterceptor.class); + + private final String exporterNameClause; + + + /** + * Create a new RemoteInvocationTraceInterceptor. + */ + public RemoteInvocationTraceInterceptor() { + this.exporterNameClause = ""; + } + + /** + * Create a new RemoteInvocationTraceInterceptor. + * @param exporterName the name of the remote exporter + * (to be used as context information in log messages) + */ + public RemoteInvocationTraceInterceptor(String exporterName) { + this.exporterNameClause = exporterName + " "; + } + + + public Object invoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + if (logger.isDebugEnabled()) { + logger.debug("Incoming " + this.exporterNameClause + "remote call: " + + ClassUtils.getQualifiedMethodName(method)); + } + try { + Object retVal = invocation.proceed(); + if (logger.isDebugEnabled()) { + logger.debug("Finished processing of " + this.exporterNameClause + "remote call: " + + ClassUtils.getQualifiedMethodName(method)); + } + return retVal; + } + catch (Throwable ex) { + if (ex instanceof RuntimeException || ex instanceof Error) { + if (logger.isWarnEnabled()) { + logger.warn("Processing of " + this.exporterNameClause + "remote call resulted in fatal exception: " + + ClassUtils.getQualifiedMethodName(method), ex); + } + } + else { + if (logger.isInfoEnabled()) { + logger.info("Processing of " + this.exporterNameClause + "remote call resulted in exception: " + + ClassUtils.getQualifiedMethodName(method), ex); + } + } + throw ex; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java new file mode 100644 index 00000000000..fadd3d85a17 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java @@ -0,0 +1,62 @@ +/* + * 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.remoting.support; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.JdkVersion; + +/** + * General utilities for handling remote invocations. + * + *

Mainly intended for use within the remoting framework. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class RemoteInvocationUtils { + + /** + * Fill the current client-side stack trace into the given exception. + *

The given exception is typically thrown on the server and serialized + * as-is, with the client wanting it to contain the client-side portion + * of the stack trace as well. What we can do here is to update the + * StackTraceElement array with the current client-side stack + * trace, provided that we run on JDK 1.4+. + * @param ex the exception to update + * @see java.lang.Throwable#getStackTrace() + * @see java.lang.Throwable#setStackTrace(StackTraceElement[]) + */ + public static void fillInClientStackTraceIfPossible(Throwable ex) { + if (ex != null) { + StackTraceElement[] clientStack = new Throwable().getStackTrace(); + Set visitedExceptions = new HashSet(); + Throwable exToUpdate = ex; + while (exToUpdate != null && !visitedExceptions.contains(exToUpdate)) { + StackTraceElement[] serverStack = exToUpdate.getStackTrace(); + StackTraceElement[] combinedStack = new StackTraceElement[serverStack.length + clientStack.length]; + System.arraycopy(serverStack, 0, combinedStack, 0, serverStack.length); + System.arraycopy(clientStack, 0, combinedStack, serverStack.length, clientStack.length); + exToUpdate.setStackTrace(combinedStack); + visitedExceptions.add(exToUpdate); + exToUpdate = exToUpdate.getCause(); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/RemotingSupport.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemotingSupport.java new file mode 100644 index 00000000000..3d727eb4371 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/RemotingSupport.java @@ -0,0 +1,74 @@ +/* + * 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.remoting.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.util.ClassUtils; + +/** + * Generic support base class for remote accessor and exporters, + * providing common bean ClassLoader handling. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public abstract class RemotingSupport implements BeanClassLoaderAware { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * Return the ClassLoader that this accessor operates in, + * to be used for deserializing and for generating proxies. + */ + protected ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @return the original thread context ClassLoader, or null if not overridden + */ + protected ClassLoader overrideThreadContextClassLoader() { + return ClassUtils.overrideThreadContextClassLoader(getBeanClassLoader()); + } + + /** + * Reset the original thread context ClassLoader if necessary. + * @param original the original thread context ClassLoader, + * or null if not overridden (and hence nothing to reset) + */ + protected void resetThreadContextClassLoader(ClassLoader original) { + if (original != null) { + Thread.currentThread().setContextClassLoader(original); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java new file mode 100644 index 00000000000..b1f685fa914 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java @@ -0,0 +1,197 @@ +/* + * 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.remoting.support; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.Filter; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +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.task.TaskExecutor; +import org.springframework.core.task.support.ConcurrentExecutorAdapter; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a simple + * HTTP server, based on the HTTP server that is included in Sun's JRE 1.6. + * Starts the HTTP server on initialization and stops it on destruction. + * Exposes the resulting {@link com.sun.net.httpserver.HttpServer} object. + * + *

Allows for registering {@link com.sun.net.httpserver.HttpHandler HttpHandlers} + * for specific {@link #setContexts context paths}. Alternatively, + * register such context-specific handlers programmatically on the + * {@link com.sun.net.httpserver.HttpServer} itself. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.5.1 + * @see #setPort + * @see #setContexts + */ +public class SimpleHttpServerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private int port = 8080; + + private String hostname; + + private int backlog = -1; + + private int shutdownDelay = 0; + + private Executor executor; + + private Map contexts; + + private List filters; + + private Authenticator authenticator; + + private HttpServer server; + + + /** + * Specify the HTTP server's port. Default is 8080. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Specify the HTTP server's hostname to bind to. Default is localhost; + * can be overridden with a specific network address to bind to. + */ + public void setHostname(String hostname) { + this.hostname = hostname; + } + + /** + * Specify the HTTP server's TCP backlog. Default is -1, + * indicating the system's default value. + */ + public void setBacklog(int backlog) { + this.backlog = backlog; + } + + /** + * Specify the number of seconds to wait until HTTP exchanges have + * completed when shutting down the HTTP server. Default is 0. + */ + public void setShutdownDelay(int shutdownDelay) { + this.shutdownDelay = shutdownDelay; + } + + /** + * Set the JDK concurrent executor to use for dispatching incoming requests. + * @see com.sun.net.httpserver.HttpServer#setExecutor + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + + /** + * Set the Spring TaskExecutor to use for dispatching incoming requests. + * @see com.sun.net.httpserver.HttpServer#setExecutor + */ + public void setTaskExecutor(TaskExecutor executor) { + this.executor = new ConcurrentExecutorAdapter(executor); + } + + /** + * Register {@link com.sun.net.httpserver.HttpHandler HttpHandlers} + * for specific context paths. + * @param contexts a Map with context paths as keys and HttpHandler + * objects as values + * @see org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter + * @see org.springframework.remoting.caucho.SimpleHessianServiceExporter + * @see org.springframework.remoting.caucho.SimpleBurlapServiceExporter + */ + public void setContexts(Map contexts) { + this.contexts = contexts; + } + + /** + * Register common {@link com.sun.net.httpserver.Filter Filters} to be + * applied to all locally registered {@link #setContexts contexts}. + */ + public void setFilters(List filters) { + this.filters = filters; + } + + /** + * Register a common {@link com.sun.net.httpserver.Authenticator} to be + * applied to all locally registered {@link #setContexts contexts}. + */ + public void setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + } + + + public void afterPropertiesSet() throws IOException { + InetSocketAddress address = (this.hostname != null ? + new InetSocketAddress(this.hostname, this.port) : new InetSocketAddress(this.port)); + this.server = HttpServer.create(address, this.backlog); + if (this.executor != null) { + this.server.setExecutor(this.executor); + } + if (this.contexts != null) { + for (String key : this.contexts.keySet()) { + HttpContext httpContext = this.server.createContext(key, this.contexts.get(key)); + if (this.filters != null) { + httpContext.getFilters().addAll(this.filters); + } + if (this.authenticator != null) { + httpContext.setAuthenticator(this.authenticator); + } + } + } + if (this.logger.isInfoEnabled()) { + this.logger.info("Starting HttpServer at address " + address); + } + this.server.start(); + } + + public Object getObject() { + return this.server; + } + + public Class getObjectType() { + return (this.server != null ? this.server.getClass() : HttpServer.class); + } + + public boolean isSingleton() { + return true; + } + + public void destroy() { + logger.info("Stopping HttpServer"); + this.server.stop(this.shutdownDelay); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java b/org.springframework.context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java new file mode 100644 index 00000000000..bec48666187 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java @@ -0,0 +1,55 @@ +/* + * 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.remoting.support; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Abstract base class for classes that access remote services via URLs. + * Provides a "serviceUrl" bean property, which is considered as required. + * + * @author Juergen Hoeller + * @since 15.12.2003 + */ +public abstract class UrlBasedRemoteAccessor extends RemoteAccessor implements InitializingBean { + + private String serviceUrl; + + + /** + * Set the URL of this remote accessor's target service. + * The URL must be compatible with the rules of the particular remoting provider. + */ + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + /** + * Return the URL of this remote accessor's target service. + */ + public String getServiceUrl() { + return this.serviceUrl; + } + + + public void afterPropertiesSet() { + if (getServiceUrl() == null) { + throw new IllegalArgumentException("Property 'serviceUrl' is required"); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/remoting/support/package.html b/org.springframework.context/src/main/java/org/springframework/remoting/support/package.html new file mode 100644 index 00000000000..70d1bec7524 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/remoting/support/package.html @@ -0,0 +1,8 @@ + + + +Generic support classes for remoting implementations. +Provides abstract base classes for remote proxy factories. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java new file mode 100644 index 00000000000..c5db5d903f2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * Extension of the Runnable interface, adding special callbacks + * for long-running operations. + * + *

This interface closely corresponds to the CommonJ Work interface, + * but is kept separate to avoid a required CommonJ dependency. + * + *

Scheduling-capable TaskExecutors are encouraged to check a submitted + * Runnable, detecting whether this interface is implemented and reacting + * as appropriately as they are able to. + * + * @author Juergen Hoeller + * @since 2.0 + * @see commonj.work.Work + * @see org.springframework.core.task.TaskExecutor + * @see SchedulingTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +public interface SchedulingAwareRunnable extends Runnable { + + /** + * Return whether the Runnable's operation is long-lived + * (true) versus short-lived (false). + *

In the former case, the task will not allocate a thread from the thread + * pool (if any) but rather be considered as long-running background thread. + *

This should be considered a hint. Of course TaskExecutor implementations + * are free to ignore this flag and the SchedulingAwareRunnable interface overall. + */ + boolean isLongLived(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingException.java b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingException.java new file mode 100644 index 00000000000..f5a198952f3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingException.java @@ -0,0 +1,49 @@ +/* + * 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; + +import org.springframework.core.NestedRuntimeException; + +/** + * General exception to be thrown on scheduling failures, + * such as the scheduler already having shut down. + * Unchecked since scheduling failures are usually fatal. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class SchedulingException extends NestedRuntimeException { + + /** + * Constructor for SchedulingException. + * @param msg the detail message + */ + public SchedulingException(String msg) { + super(msg); + } + + /** + * Constructor for SchedulingException. + * @param msg the detail message + * @param cause the root cause (usually from using a underlying + * scheduling API such as Quartz) + */ + public SchedulingException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java new file mode 100644 index 00000000000..e87ee9341cb --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java @@ -0,0 +1,55 @@ +/* + * 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; + +import org.springframework.core.task.TaskExecutor; + +/** + * A {@link org.springframework.core.task.TaskExecutor} extension exposing + * scheduling characteristics that are relevant to potential task submitters. + * + *

Scheduling clients are encouraged to submit + * {@link Runnable Runnables} that match the exposed preferences + * of the TaskExecutor implementation in use. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SchedulingAwareRunnable + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +public interface SchedulingTaskExecutor extends TaskExecutor { + + /** + * Does this TaskExecutor prefer short-lived tasks over + * long-lived tasks? + *

A SchedulingTaskExecutor implementation can indicate + * whether it prefers submitted tasks to perform as little work as they + * can within a single task execution. For example, submitted tasks + * might break a repeated loop into individual subtasks which submit a + * follow-up task afterwards (if feasible). + *

This should be considered a hint. Of course TaskExecutor + * clients are free to ignore this flag and hence the + * SchedulingTaskExecutor interface overall. However, thread + * pools will usually indicated a preference for short-lived tasks, to be + * able to perform more fine-grained scheduling. + * @return true if this TaskExecutor prefers + * short-lived tasks + */ + boolean prefersShortLivedTasks(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ConcurrentTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ConcurrentTaskExecutor.java new file mode 100644 index 00000000000..1150d7a0c4d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ConcurrentTaskExecutor.java @@ -0,0 +1,114 @@ +/* + * 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.backportconcurrent; + +import edu.emory.mathcs.backport.java.util.concurrent.Executor; +import edu.emory.mathcs.backport.java.util.concurrent.Executors; +import edu.emory.mathcs.backport.java.util.concurrent.RejectedExecutionException; + +import org.springframework.core.task.TaskRejectedException; +import org.springframework.scheduling.SchedulingTaskExecutor; + +/** + * Adapter that takes a JSR-166 backport + * edu.emory.mathcs.backport.java.util.concurrent.Executor and + * exposes a Spring {@link org.springframework.core.task.TaskExecutor} for it. + * + *

NOTE: This class implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface as well as + * the JSR-166 {@link edu.emory.mathcs.backport.java.util.concurrent.Executor} + * interface, with the former being the primary interface, the other just + * serving as secondary convenience. For this reason, the exception handling + * follows the TaskExecutor contract rather than the Executor contract, in + * particular regarding the {@link org.springframework.core.task.TaskRejectedException}. + * + *

Note that there is a pre-built {@link ThreadPoolTaskExecutor} that allows for + * defining a JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor} in bean + * style, exposing it as a Spring {@link org.springframework.core.task.TaskExecutor} + * directly. This is a convenient alternative to a raw ThreadPoolExecutor + * definition with a separate definition of the present adapter class. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see edu.emory.mathcs.backport.java.util.concurrent.Executor + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor + * @see edu.emory.mathcs.backport.java.util.concurrent.Executors + * @see ThreadPoolTaskExecutor + */ +public class ConcurrentTaskExecutor implements SchedulingTaskExecutor, Executor { + + private Executor concurrentExecutor; + + + /** + * Create a new ConcurrentTaskExecutor, + * using a single thread executor as default. + * @see edu.emory.mathcs.backport.java.util.concurrent.Executors#newSingleThreadExecutor() + */ + public ConcurrentTaskExecutor() { + setConcurrentExecutor(null); + } + + /** + * Create a new ConcurrentTaskExecutor, + * using the given JSR-166 backport concurrent executor. + * @param concurrentExecutor the JSR-166 backport concurrent executor to delegate to + */ + public ConcurrentTaskExecutor(Executor concurrentExecutor) { + setConcurrentExecutor(concurrentExecutor); + } + + /** + * Specify the JSR-166 backport concurrent executor to delegate to. + */ + public void setConcurrentExecutor(Executor concurrentExecutor) { + this.concurrentExecutor = + (concurrentExecutor != null ? concurrentExecutor : Executors.newSingleThreadExecutor()); + } + + /** + * Return the JSR-166 backport concurrent executor that this adapter + * delegates to. + */ + public Executor getConcurrentExecutor() { + return this.concurrentExecutor; + } + + + /** + * Delegates to the specified JSR-166 backport concurrent executor. + * @see edu.emory.mathcs.backport.java.util.concurrent.Executor#execute(Runnable) + */ + public void execute(Runnable task) { + try { + this.concurrentExecutor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + /** + * This task executor prefers short-lived work units. + */ + public boolean prefersShortLivedTasks() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/CustomizableThreadFactory.java b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/CustomizableThreadFactory.java new file mode 100644 index 00000000000..100b06192c1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/CustomizableThreadFactory.java @@ -0,0 +1,58 @@ +/* + * 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.backportconcurrent; + +import edu.emory.mathcs.backport.java.util.concurrent.ThreadFactory; + +import org.springframework.util.CustomizableThreadCreator; + +/** + * Implementation of the JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.ThreadFactory} interface, + * allowing for customizing the created threads (name, priority, etc). + * + *

See the base class {@link org.springframework.util.CustomizableThreadCreator} + * for details on the available configuration options. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #setThreadNamePrefix + * @see #setThreadPriority + */ +public class CustomizableThreadFactory extends CustomizableThreadCreator implements ThreadFactory { + + /** + * Create a new CustomizableThreadFactory with default thread name prefix. + */ + public CustomizableThreadFactory() { + super(); + } + + /** + * Create a new CustomizableThreadFactory with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public CustomizableThreadFactory(String threadNamePrefix) { + super(threadNamePrefix); + } + + + public Thread newThread(Runnable runnable) { + return createThread(runnable); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorFactoryBean.java new file mode 100644 index 00000000000..d1ccfaabf29 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorFactoryBean.java @@ -0,0 +1,285 @@ +/* + * 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.backportconcurrent; + +import edu.emory.mathcs.backport.java.util.concurrent.Executors; +import edu.emory.mathcs.backport.java.util.concurrent.RejectedExecutionHandler; +import edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService; +import edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor; +import edu.emory.mathcs.backport.java.util.concurrent.ThreadFactory; +import edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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.scheduling.support.DelegatingExceptionProofRunnable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that sets up + * a JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService} + * (by default: + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor} + * as implementation) and exposes it for bean references. + * + *

Allows for registration of {@link ScheduledExecutorTask ScheduledExecutorTasks}, + * automatically starting the {@link ScheduledExecutorService} on initialization and + * cancelling it on destruction of the context. In scenarios that just require static + * registration of tasks at startup, there is no need to access the + * {@link ScheduledExecutorService} instance itself in application code. + * + *

Note that + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService} + * uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which instantiates a new Job for each execution. + * + *

WARNING: {@link Runnable Runnables} submitted via a native + * {@link java.util.concurrent.ScheduledExecutorService} are removed from + * the execution schedule once they throw an exception. If you would prefer + * to continue execution after such an exception, switch this FactoryBean's + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * property to "true". + * + *

This class is analogous to the + * {@link org.springframework.scheduling.timer.TimerFactoryBean} + * class for the JDK {@link java.util.Timer} facility. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see ScheduledExecutorTask + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor + * @see org.springframework.scheduling.timer.TimerFactoryBean + */ +public class ScheduledExecutorFactoryBean implements FactoryBean, BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private int poolSize = 1; + + private ThreadFactory threadFactory = Executors.defaultThreadFactory(); + + private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + + private boolean exposeUnconfigurableExecutor = false; + + private ScheduledExecutorTask[] scheduledExecutorTasks; + + private boolean continueScheduledExecutionAfterException = false; + + private boolean waitForTasksToCompleteOnShutdown = false; + + private String beanName; + + private ScheduledExecutorService executor; + + + /** + * Set the ScheduledExecutorService's pool size. + * Default is 1. + */ + public void setPoolSize(int poolSize) { + Assert.isTrue(poolSize > 0, "'poolSize' must be 1 or higher"); + this.poolSize = poolSize; + } + + /** + * Set the ThreadFactory to use for the ThreadPoolExecutor's thread pool. + * Default is the ThreadPoolExecutor's default thread factory. + * @see edu.emory.mathcs.backport.java.util.concurrent.Executors#defaultThreadFactory() + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = (threadFactory != null ? threadFactory : Executors.defaultThreadFactory()); + } + + /** + * Set the RejectedExecutionHandler to use for the ThreadPoolExecutor. + * Default is the ThreadPoolExecutor's default abort policy. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor.AbortPolicy + */ + public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = + (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Specify whether this FactoryBean should expose an unconfigurable + * decorator for the created executor. + *

Default is "false", exposing the raw executor as bean reference. + * Switch this flag to "true" to strictly prevent clients from + * modifying the executor's configuration. + * @see edu.emory.mathcs.backport.java.util.concurrent.Executors#unconfigurableScheduledExecutorService + */ + public void setExposeUnconfigurableExecutor(boolean exposeUnconfigurableExecutor) { + this.exposeUnconfigurableExecutor = exposeUnconfigurableExecutor; + } + + /** + * Register a list of ScheduledExecutorTask objects with the ScheduledExecutorService + * that this FactoryBean creates. Depending on each ScheduledExecutorTask's settings, + * it will be registered via one of ScheduledExecutorService's schedule methods. + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#schedule(java.lang.Runnable, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + */ + public void setScheduledExecutorTasks(ScheduledExecutorTask[] scheduledExecutorTasks) { + this.scheduledExecutorTasks = scheduledExecutorTasks; + } + + /** + * Specify whether to continue the execution of a scheduled task + * after it threw an exception. + *

Default is "false", matching the native behavior of a + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService}. + * Switch this flag to "true" for exception-proof execution of each task, + * continuing scheduled execution as in the case of successful execution. + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate + */ + public void setContinueScheduledExecutionAfterException(boolean continueScheduledExecutionAfterException) { + this.continueScheduledExecutionAfterException = continueScheduledExecutionAfterException; + } + + /** + * Set whether to wait for scheduled tasks to complete on shutdown. + *

Default is "false". Switch this to "true" if you prefer + * fully completed tasks at the expense of a longer shutdown phase. + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#shutdown() + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#shutdownNow() + */ + public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + public void afterPropertiesSet() { + if (logger.isInfoEnabled()) { + logger.info("Initializing ScheduledExecutorService" + + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + ScheduledExecutorService executor = + createExecutor(this.poolSize, this.threadFactory, this.rejectedExecutionHandler); + + // Register specified ScheduledExecutorTasks, if necessary. + if (!ObjectUtils.isEmpty(this.scheduledExecutorTasks)) { + registerTasks(this.scheduledExecutorTasks, executor); + } + + // Wrap executor with an unconfigurable decorator. + this.executor = (this.exposeUnconfigurableExecutor ? + Executors.unconfigurableScheduledExecutorService(executor) : executor); + } + + /** + * Create a new {@link ScheduledExecutorService} instance. + * Called by afterPropertiesSet. + *

The default implementation creates a {@link ScheduledThreadPoolExecutor}. + * Can be overridden in subclasses to provide custom + * {@link ScheduledExecutorService} instances. + * @param poolSize the specified pool size + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ScheduledExecutorService instance + * @see #afterPropertiesSet() + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor + */ + protected ScheduledExecutorService createExecutor( + int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + + /** + * Register the specified {@link ScheduledExecutorTask ScheduledExecutorTasks} + * on the given {@link ScheduledExecutorService}. + * @param tasks the specified ScheduledExecutorTasks (never empty) + * @param executor the ScheduledExecutorService to register the tasks on. + */ + protected void registerTasks(ScheduledExecutorTask[] tasks, ScheduledExecutorService executor) { + for (int i = 0; i < tasks.length; i++) { + ScheduledExecutorTask task = tasks[i]; + Runnable runnable = getRunnableToSchedule(task); + if (task.isOneTimeTask()) { + executor.schedule(runnable, task.getDelay(), task.getTimeUnit()); + } + else { + if (task.isFixedRate()) { + executor.scheduleAtFixedRate(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + else { + executor.scheduleWithFixedDelay(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + } + } + } + + /** + * Determine the actual Runnable to schedule for the given task. + *

Wraps the task's Runnable in a + * {@link org.springframework.scheduling.support.DelegatingExceptionProofRunnable} + * if necessary, according to the + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * flag. + * @param task the ScheduledExecutorTask to schedule + * @return the actual Runnable to schedule (may be a decorator) + */ + protected Runnable getRunnableToSchedule(ScheduledExecutorTask task) { + boolean propagateException = !this.continueScheduledExecutionAfterException; + return new DelegatingExceptionProofRunnable(task.getRunnable(), propagateException); + } + + + public Object getObject() { + return this.executor; + } + + public Class getObjectType() { + return (this.executor != null ? this.executor.getClass() : ScheduledExecutorService.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Cancel the ScheduledExecutorService on bean factory shutdown, + * stopping all scheduled tasks. + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#shutdown() + */ + public void destroy() { + if (logger.isInfoEnabled()) { + logger.info("Shutting down ScheduledExecutorService" + + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (this.waitForTasksToCompleteOnShutdown) { + this.executor.shutdown(); + } + else { + this.executor.shutdownNow(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorTask.java b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorTask.java new file mode 100644 index 00000000000..956fc7970a2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ScheduledExecutorTask.java @@ -0,0 +1,202 @@ +/* + * 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.backportconcurrent; + +import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit; + +/** + * JavaBean that describes a scheduled executor task, consisting of the + * {@link Runnable} and a delay plus period. The period needs to be specified; + * there is no point in a default for it. + * + *

The JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService} + * does not offer more sophisticated scheduling options such as cron expressions. + * Consider using Quartz for such advanced needs. + * + *

Note that the + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService} + * mechanism uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which creates a new Job instance for each execution. + * + *

This class is analogous to the {@link org.springframework.scheduling.timer.ScheduledTimerTask} + * class for the JDK {@link java.util.Timer} facility. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + * @see org.springframework.scheduling.timer.ScheduledTimerTask + */ +public class ScheduledExecutorTask { + + private Runnable runnable; + + private long delay = 0; + + private long period = -1; + + private TimeUnit timeUnit = TimeUnit.MILLISECONDS; + + private boolean fixedRate = false; + + + /** + * Create a new ScheduledExecutorTask, + * to be populated via bean properties. + * @see #setDelay + * @see #setPeriod + * @see #setFixedRate + */ + public ScheduledExecutorTask() { + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution without delay. + * @param executorTask the Runnable to schedule + */ + public ScheduledExecutorTask(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution with the given delay. + * @param executorTask the Runnable to schedule + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledExecutorTask(Runnable executorTask, long delay) { + this.runnable = executorTask; + this.delay = delay; + } + + /** + * Create a new ScheduledExecutorTask. + * @param executorTask the Runnable 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 ScheduledExecutorTask(Runnable executorTask, long delay, long period, boolean fixedRate) { + this.runnable = executorTask; + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + + /** + * Set the Runnable to schedule as executor task. + */ + public void setRunnable(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Return the Runnable to schedule as executor task. + */ + public Runnable getRunnable() { + return this.runnable; + } + + /** + * Set the delay before starting the task for the first time, + * in milliseconds. Default is 0, immediately starting the + * task after successful scheduling. + */ + 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 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 not supported, + * simply because edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService itself + * does not support it. Hence a value of 0 will be treated as one-time execution; + * however, that value should never be specified explicitly in the first place! + * @see #setFixedRate + * @see #isOneTimeTask() + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + */ + 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); + } + + /** + * Specify the time unit for the delay and period values. + * Default is milliseconds (TimeUnit.MILLISECONDS). + * @see edu.emory.mathcs.backport.java.util.concurrent.TimeUnit#MILLISECONDS + * @see edu.emory.mathcs.backport.java.util.concurrent.TimeUnit#SECONDS + */ + public void setTimeUnit(TimeUnit timeUnit) { + this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS); + } + + /** + * Return the time unit for the delay and period values. + */ + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + /** + * Set whether to schedule as fixed-rate execution, rather than + * fixed-delay execution. Default is "false", that is, fixed delay. + *

See ScheduledExecutorService javadoc for details on those execution modes. + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, edu.emory.mathcs.backport.java.util.concurrent.TimeUnit) + */ + 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/src/main/java/org/springframework/scheduling/backportconcurrent/ThreadPoolTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ThreadPoolTaskExecutor.java new file mode 100644 index 00000000000..4b4c506b0a5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/ThreadPoolTaskExecutor.java @@ -0,0 +1,360 @@ +/* + * 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.backportconcurrent; + +import edu.emory.mathcs.backport.java.util.concurrent.BlockingQueue; +import edu.emory.mathcs.backport.java.util.concurrent.Executor; +import edu.emory.mathcs.backport.java.util.concurrent.LinkedBlockingQueue; +import edu.emory.mathcs.backport.java.util.concurrent.RejectedExecutionException; +import edu.emory.mathcs.backport.java.util.concurrent.RejectedExecutionHandler; +import edu.emory.mathcs.backport.java.util.concurrent.SynchronousQueue; +import edu.emory.mathcs.backport.java.util.concurrent.ThreadFactory; +import edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor; +import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; + +/** + * JavaBean that allows for configuring a JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor} in bean + * style (through its "corePoolSize", "maxPoolSize", "keepAliveSeconds", "queueCapacity" + * properties), exposing it as a Spring {@link org.springframework.core.task.TaskExecutor}. + * This is an alternative to configuring a ThreadPoolExecutor instance directly using + * constructor injection, with a separate {@link ConcurrentTaskExecutor} adapter wrapping it. + * + *

For any custom needs, in particular for defining a + * {@link edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor}, + * it is recommended to use a straight definition of the Executor instance or a + * factory method definition that points to the JSR-166 backport + * {@link edu.emory.mathcs.backport.java.util.concurrent.Executors} class. + * To expose such a raw Executor as a Spring {@link org.springframework.core.task.TaskExecutor}, + * simply wrap it with a {@link ConcurrentTaskExecutor} adapter. + * + *

NOTE: This class implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface as well as + * the JSR-166 {@link edu.emory.mathcs.backport.java.util.concurrent.Executor} + * interface, with the former being the primary interface, the other just + * serving as secondary convenience. For this reason, the exception handling + * follows the TaskExecutor contract rather than the Executor contract, in + * particular regarding the {@link org.springframework.core.task.TaskRejectedException}. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see org.springframework.core.task.TaskExecutor + * @see edu.emory.mathcs.backport.java.util.concurrent.Executor + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor + * @see edu.emory.mathcs.backport.java.util.concurrent.ScheduledThreadPoolExecutor + * @see edu.emory.mathcs.backport.java.util.concurrent.Executors + * @see ConcurrentTaskExecutor + */ +public class ThreadPoolTaskExecutor extends CustomizableThreadFactory + implements SchedulingTaskExecutor, Executor, BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Object poolSizeMonitor = new Object(); + + private int corePoolSize = 1; + + private int maxPoolSize = Integer.MAX_VALUE; + + private int keepAliveSeconds = 60; + + private boolean allowCoreThreadTimeOut = false; + + private int queueCapacity = Integer.MAX_VALUE; + + private ThreadFactory threadFactory = this; + + private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + + private boolean waitForTasksToCompleteOnShutdown = false; + + private boolean threadNamePrefixSet = false; + + private String beanName; + + private ThreadPoolExecutor threadPoolExecutor; + + + /** + * Set the ThreadPoolExecutor's core pool size. + * Default is 1. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setCorePoolSize(int corePoolSize) { + synchronized (this.poolSizeMonitor) { + this.corePoolSize = corePoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setCorePoolSize(corePoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's core pool size. + */ + public int getCorePoolSize() { + synchronized (this.poolSizeMonitor) { + return this.corePoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's maximum pool size. + * Default is Integer.MAX_VALUE. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setMaxPoolSize(int maxPoolSize) { + synchronized (this.poolSizeMonitor) { + this.maxPoolSize = maxPoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setMaximumPoolSize(maxPoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's maximum pool size. + */ + public int getMaxPoolSize() { + synchronized (this.poolSizeMonitor) { + return this.maxPoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's keep-alive seconds. + * Default is 60. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setKeepAliveSeconds(int keepAliveSeconds) { + synchronized (this.poolSizeMonitor) { + this.keepAliveSeconds = keepAliveSeconds; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setKeepAliveTime(keepAliveSeconds, TimeUnit.SECONDS); + } + } + } + + /** + * Return the ThreadPoolExecutor's keep-alive seconds. + */ + public int getKeepAliveSeconds() { + synchronized (this.poolSizeMonitor) { + return this.keepAliveSeconds; + } + } + + /** + * Specify whether to allow core threads to time out. This enables dynamic + * growing and shrinking even in combination with a non-zero queue (since + * the max pool size will only grow once the queue is full). + *

Default is "false". Note that this feature is only available on + * backport-concurrent 3.0 or above (based on the code in Java 6). + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#allowCoreThreadTimeOut(boolean) + */ + public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + } + + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is Integer.MAX_VALUE. + *

Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see edu.emory.mathcs.backport.java.util.concurrent.LinkedBlockingQueue + * @see edu.emory.mathcs.backport.java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + /** + * Set the ThreadFactory to use for the ThreadPoolExecutor's thread pool. + *

Default is this executor itself (i.e. the factory that this executor + * inherits from). See {@link org.springframework.util.CustomizableThreadCreator}'s + * javadoc for available bean properties. + * @see #setThreadPriority + * @see #setDaemon + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = (threadFactory != null ? threadFactory : this); + } + + /** + * Set the RejectedExecutionHandler to use for the ThreadPoolExecutor. + * Default is the ThreadPoolExecutor's default abort policy. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor.AbortPolicy + */ + public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = + (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Set whether to wait for scheduled tasks to complete on shutdown. + *

Default is "false". Switch this to "true" if you prefer + * fully completed tasks at the expense of a longer shutdown phase. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#shutdown() + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#shutdownNow() + */ + public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + public void setThreadNamePrefix(String threadNamePrefix) { + super.setThreadNamePrefix(threadNamePrefix); + this.threadNamePrefixSet = true; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + /** + * Calls initialize() after the container applied all property values. + * @see #initialize() + */ + public void afterPropertiesSet() { + initialize(); + } + + /** + * Creates the BlockingQueue and the ThreadPoolExecutor. + * @see #createQueue + */ + public void initialize() { + if (logger.isInfoEnabled()) { + logger.info("Initializing ThreadPoolExecutor" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (!this.threadNamePrefixSet && this.beanName != null) { + setThreadNamePrefix(this.beanName + "-"); + } + BlockingQueue queue = createQueue(this.queueCapacity); + this.threadPoolExecutor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, this.threadFactory, this.rejectedExecutionHandler); + if (this.allowCoreThreadTimeOut) { + this.threadPoolExecutor.allowCoreThreadTimeOut(true); + } + } + + /** + * Create the BlockingQueue to use for the ThreadPoolExecutor. + *

A LinkedBlockingQueue instance will be created for a positive + * capacity value; a SynchronousQueue else. + * @param queueCapacity the specified queue capacity + * @return the BlockingQueue instance + * @see edu.emory.mathcs.backport.java.util.concurrent.LinkedBlockingQueue + * @see edu.emory.mathcs.backport.java.util.concurrent.SynchronousQueue + */ + protected BlockingQueue createQueue(int queueCapacity) { + if (queueCapacity > 0) { + return new LinkedBlockingQueue(queueCapacity); + } + else { + return new SynchronousQueue(); + } + } + + /** + * Return the underlying ThreadPoolExecutor for native access. + * @return the underlying ThreadPoolExecutor (never null) + * @throws IllegalStateException if the ThreadPoolTaskExecutor hasn't been initialized yet + */ + public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException { + Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized"); + return this.threadPoolExecutor; + } + + + /** + * Implementation of both the JSR-166 backport Executor interface and the Spring + * TaskExecutor interface, delegating to the ThreadPoolExecutor instance. + * @see edu.emory.mathcs.backport.java.util.concurrent.Executor#execute(Runnable) + * @see org.springframework.core.task.TaskExecutor#execute(Runnable) + */ + public void execute(Runnable task) { + Executor executor = getThreadPoolExecutor(); + try { + executor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + /** + * This task executor prefers short-lived work units. + */ + public boolean prefersShortLivedTasks() { + return true; + } + + + /** + * Return the current pool size. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#getPoolSize() + */ + public int getPoolSize() { + return getThreadPoolExecutor().getPoolSize(); + } + + /** + * Return the number of currently active threads. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#getActiveCount() + */ + public int getActiveCount() { + return getThreadPoolExecutor().getActiveCount(); + } + + + /** + * Calls shutdown when the BeanFactory destroys + * the task executor instance. + * @see #shutdown() + */ + public void destroy() { + shutdown(); + } + + /** + * Perform a shutdown on the ThreadPoolExecutor. + * @see edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor#shutdown() + */ + public void shutdown() { + if (logger.isInfoEnabled()) { + logger.info("Shutting down ThreadPoolExecutor" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (this.waitForTasksToCompleteOnShutdown) { + this.threadPoolExecutor.shutdown(); + } + else { + this.threadPoolExecutor.shutdownNow(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/package.html b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/package.html new file mode 100644 index 00000000000..22faadd1ef1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/backportconcurrent/package.html @@ -0,0 +1,10 @@ + + + +Scheduling convenience classes for the +JSR-166 backport +Executor mechanism, allowing to set up a ThreadPoolExecutor or +ScheduledThreadPoolExecutor as bean in a Spring context. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java new file mode 100644 index 00000000000..ad07816471b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -0,0 +1,112 @@ +/* + * 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.concurrent; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +import org.springframework.core.task.TaskRejectedException; +import org.springframework.scheduling.SchedulingTaskExecutor; + +/** + * Adapter that takes a JDK 1.5 java.util.concurrent.Executor and + * exposes a Spring {@link org.springframework.core.task.TaskExecutor} for it. + * + *

NOTE: This class implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface as well as the JDK 1.5 + * {@link java.util.concurrent.Executor} interface, with the former being the primary + * interface, the other just serving as secondary convenience. For this reason, the + * exception handling follows the TaskExecutor contract rather than the Executor contract, + * in particular regarding the {@link org.springframework.core.task.TaskRejectedException}. + * + *

Note that there is a pre-built {@link ThreadPoolTaskExecutor} that allows for + * defining a JDK 1.5 {@link java.util.concurrent.ThreadPoolExecutor} in bean style, + * exposing it as a Spring {@link org.springframework.core.task.TaskExecutor} directly. + * This is a convenient alternative to a raw ThreadPoolExecutor definition with + * a separate definition of the present adapter class. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.Executor + * @see java.util.concurrent.ThreadPoolExecutor + * @see java.util.concurrent.Executors + * @see ThreadPoolTaskExecutor + */ +public class ConcurrentTaskExecutor implements SchedulingTaskExecutor, Executor { + + private Executor concurrentExecutor; + + + /** + * Create a new ConcurrentTaskExecutor, + * using a single thread executor as default. + * @see java.util.concurrent.Executors#newSingleThreadExecutor() + */ + public ConcurrentTaskExecutor() { + setConcurrentExecutor(null); + } + + /** + * Create a new ConcurrentTaskExecutor, + * using the given JDK 1.5 concurrent executor. + * @param concurrentExecutor the JDK 1.5 concurrent executor to delegate to + */ + public ConcurrentTaskExecutor(Executor concurrentExecutor) { + setConcurrentExecutor(concurrentExecutor); + } + + + /** + * Specify the JDK 1.5 concurrent executor to delegate to. + */ + public void setConcurrentExecutor(Executor concurrentExecutor) { + this.concurrentExecutor = + (concurrentExecutor != null ? concurrentExecutor : Executors.newSingleThreadExecutor()); + } + + /** + * Return the JDK 1.5 concurrent executor that this adapter + * delegates to. + */ + public Executor getConcurrentExecutor() { + return this.concurrentExecutor; + } + + + /** + * Delegates to the specified JDK 1.5 concurrent executor. + * @see java.util.concurrent.Executor#execute(Runnable) + */ + public void execute(Runnable task) { + try { + this.concurrentExecutor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + /** + * This task executor prefers short-lived work units. + */ + public boolean prefersShortLivedTasks() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java new file mode 100644 index 00000000000..83eb452190c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java @@ -0,0 +1,57 @@ +/* + * 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.concurrent; + +import java.util.concurrent.ThreadFactory; + +import org.springframework.util.CustomizableThreadCreator; + +/** + * Implementation of the JDK 1.5 {@link java.util.concurrent.ThreadFactory} + * interface, allowing for customizing the created threads (name, priority, etc). + * + *

See the base class {@link org.springframework.util.CustomizableThreadCreator} + * for details on the available configuration options. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #setThreadNamePrefix + * @see #setThreadPriority + */ +public class CustomizableThreadFactory extends CustomizableThreadCreator implements ThreadFactory { + + /** + * Create a new CustomizableThreadFactory with default thread name prefix. + */ + public CustomizableThreadFactory() { + super(); + } + + /** + * Create a new CustomizableThreadFactory with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public CustomizableThreadFactory(String threadNamePrefix) { + super(threadNamePrefix); + } + + + public Thread newThread(Runnable runnable) { + return createThread(runnable); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java new file mode 100644 index 00000000000..518ad73ccc0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java @@ -0,0 +1,283 @@ +/* + * 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.concurrent; + +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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.scheduling.support.DelegatingExceptionProofRunnable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that sets up + * a JDK 1.5 {@link java.util.concurrent.ScheduledExecutorService} + * (by default: {@link java.util.concurrent.ScheduledThreadPoolExecutor} + * as implementation) and exposes it for bean references. + * + *

Allows for registration of {@link ScheduledExecutorTask ScheduledExecutorTasks}, + * automatically starting the {@link ScheduledExecutorService} on initialization and + * cancelling it on destruction of the context. In scenarios that just require static + * registration of tasks at startup, there is no need to access the + * {@link ScheduledExecutorService} instance itself in application code. + * + *

Note that {@link java.util.concurrent.ScheduledExecutorService} + * uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which instantiates a new Job for each execution. + * + *

WARNING: {@link Runnable Runnables} submitted via a native + * {@link java.util.concurrent.ScheduledExecutorService} are removed from + * the execution schedule once they throw an exception. If you would prefer + * to continue execution after such an exception, switch this FactoryBean's + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * property to "true". + * + *

This class is analogous to the + * {@link org.springframework.scheduling.timer.TimerFactoryBean} + * class for the JDK 1.3 {@link java.util.Timer} facility. + * + * @author Juergen Hoeller + * @since 2.0 + * @see ScheduledExecutorTask + * @see java.util.concurrent.ScheduledExecutorService + * @see java.util.concurrent.ScheduledThreadPoolExecutor + * @see org.springframework.scheduling.timer.TimerFactoryBean + */ +public class ScheduledExecutorFactoryBean implements FactoryBean, BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private int poolSize = 1; + + private ThreadFactory threadFactory = Executors.defaultThreadFactory(); + + private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + + private boolean exposeUnconfigurableExecutor = false; + + private ScheduledExecutorTask[] scheduledExecutorTasks; + + private boolean continueScheduledExecutionAfterException = false; + + private boolean waitForTasksToCompleteOnShutdown = false; + + private String beanName; + + private ScheduledExecutorService executor; + + + /** + * Set the ScheduledExecutorService's pool size. + * Default is 1. + */ + public void setPoolSize(int poolSize) { + Assert.isTrue(poolSize > 0, "'poolSize' must be 1 or higher"); + this.poolSize = poolSize; + } + + /** + * Set the ThreadFactory to use for the ThreadPoolExecutor's thread pool. + * Default is the ThreadPoolExecutor's default thread factory. + * @see java.util.concurrent.Executors#defaultThreadFactory() + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = (threadFactory != null ? threadFactory : Executors.defaultThreadFactory()); + } + + /** + * Set the RejectedExecutionHandler to use for the ThreadPoolExecutor. + * Default is the ThreadPoolExecutor's default abort policy. + * @see java.util.concurrent.ThreadPoolExecutor.AbortPolicy + */ + public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = + (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Specify whether this FactoryBean should expose an unconfigurable + * decorator for the created executor. + *

Default is "false", exposing the raw executor as bean reference. + * Switch this flag to "true" to strictly prevent clients from + * modifying the executor's configuration. + * @see java.util.concurrent.Executors#unconfigurableScheduledExecutorService + */ + public void setExposeUnconfigurableExecutor(boolean exposeUnconfigurableExecutor) { + this.exposeUnconfigurableExecutor = exposeUnconfigurableExecutor; + } + + /** + * Register a list of ScheduledExecutorTask objects with the ScheduledExecutorService + * that this FactoryBean creates. Depending on each ScheduledExecutorTask's settings, + * it will be registered via one of ScheduledExecutorService's schedule methods. + * @see java.util.concurrent.ScheduledExecutorService#schedule(java.lang.Runnable, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ + public void setScheduledExecutorTasks(ScheduledExecutorTask[] scheduledExecutorTasks) { + this.scheduledExecutorTasks = scheduledExecutorTasks; + } + + /** + * Specify whether to continue the execution of a scheduled task + * after it threw an exception. + *

Default is "false", matching the native behavior of a + * {@link java.util.concurrent.ScheduledExecutorService}. + * Switch this flag to "true" for exception-proof execution of each task, + * continuing scheduled execution as in the case of successful execution. + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate + */ + public void setContinueScheduledExecutionAfterException(boolean continueScheduledExecutionAfterException) { + this.continueScheduledExecutionAfterException = continueScheduledExecutionAfterException; + } + + /** + * Set whether to wait for scheduled tasks to complete on shutdown. + *

Default is "false". Switch this to "true" if you prefer + * fully completed tasks at the expense of a longer shutdown phase. + * @see java.util.concurrent.ScheduledExecutorService#shutdown() + * @see java.util.concurrent.ScheduledExecutorService#shutdownNow() + */ + public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + public void afterPropertiesSet() { + if (logger.isInfoEnabled()) { + logger.info("Initializing ScheduledExecutorService" + + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + ScheduledExecutorService executor = + createExecutor(this.poolSize, this.threadFactory, this.rejectedExecutionHandler); + + // Register specified ScheduledExecutorTasks, if necessary. + if (!ObjectUtils.isEmpty(this.scheduledExecutorTasks)) { + registerTasks(this.scheduledExecutorTasks, executor); + } + + // Wrap executor with an unconfigurable decorator. + this.executor = (this.exposeUnconfigurableExecutor ? + Executors.unconfigurableScheduledExecutorService(executor) : executor); + } + + /** + * Create a new {@link ScheduledExecutorService} instance. + * Called by afterPropertiesSet. + *

The default implementation creates a {@link ScheduledThreadPoolExecutor}. + * Can be overridden in subclasses to provide custom + * {@link ScheduledExecutorService} instances. + * @param poolSize the specified pool size + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ScheduledExecutorService instance + * @see #afterPropertiesSet() + * @see java.util.concurrent.ScheduledThreadPoolExecutor + */ + protected ScheduledExecutorService createExecutor( + int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + + /** + * Register the specified {@link ScheduledExecutorTask ScheduledExecutorTasks} + * on the given {@link ScheduledExecutorService}. + * @param tasks the specified ScheduledExecutorTasks (never empty) + * @param executor the ScheduledExecutorService to register the tasks on. + */ + protected void registerTasks(ScheduledExecutorTask[] tasks, ScheduledExecutorService executor) { + for (int i = 0; i < tasks.length; i++) { + ScheduledExecutorTask task = tasks[i]; + Runnable runnable = getRunnableToSchedule(task); + if (task.isOneTimeTask()) { + executor.schedule(runnable, task.getDelay(), task.getTimeUnit()); + } + else { + if (task.isFixedRate()) { + executor.scheduleAtFixedRate(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + else { + executor.scheduleWithFixedDelay(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + } + } + } + + /** + * Determine the actual Runnable to schedule for the given task. + *

Wraps the task's Runnable in a + * {@link org.springframework.scheduling.support.DelegatingExceptionProofRunnable} + * if necessary, according to the + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * flag. + * @param task the ScheduledExecutorTask to schedule + * @return the actual Runnable to schedule (may be a decorator) + */ + protected Runnable getRunnableToSchedule(ScheduledExecutorTask task) { + boolean propagateException = !this.continueScheduledExecutionAfterException; + return new DelegatingExceptionProofRunnable(task.getRunnable(), propagateException); + } + + + public Object getObject() { + return this.executor; + } + + public Class getObjectType() { + return (this.executor != null ? this.executor.getClass() : ScheduledExecutorService.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Cancel the ScheduledExecutorService on bean factory shutdown, + * stopping all scheduled tasks. + * @see java.util.concurrent.ScheduledExecutorService#shutdown() + */ + public void destroy() { + if (logger.isInfoEnabled()) { + logger.info("Shutting down ScheduledExecutorService" + + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (this.waitForTasksToCompleteOnShutdown) { + this.executor.shutdown(); + } + else { + this.executor.shutdownNow(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java new file mode 100644 index 00000000000..756019ede69 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java @@ -0,0 +1,200 @@ +/* + * 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.concurrent; + +import java.util.concurrent.TimeUnit; + +/** + * JavaBean that describes a scheduled executor task, consisting of the + * {@link Runnable} and a delay plus period. The period needs to be specified; + * there is no point in a default for it. + * + *

The JDK 1.5 {@link java.util.concurrent.ScheduledExecutorService} does + * not offer more sophisticated scheduling options such as cron expressions. + * Consider using Quartz for such advanced needs. + * + *

Note that the {@link java.util.concurrent.ScheduledExecutorService} mechanism + * uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which creates a new Job instance for each execution. + * + *

This class is analogous to the {@link org.springframework.scheduling.timer.ScheduledTimerTask} + * class for the JDK 1.3 {@link java.util.Timer} facility. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see org.springframework.scheduling.timer.ScheduledTimerTask + */ +public class ScheduledExecutorTask { + + private Runnable runnable; + + private long delay = 0; + + private long period = -1; + + private TimeUnit timeUnit = TimeUnit.MILLISECONDS; + + private boolean fixedRate = false; + + + /** + * Create a new ScheduledExecutorTask, + * to be populated via bean properties. + * @see #setDelay + * @see #setPeriod + * @see #setFixedRate + */ + public ScheduledExecutorTask() { + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution without delay. + * @param executorTask the Runnable to schedule + */ + public ScheduledExecutorTask(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution with the given delay. + * @param executorTask the Runnable to schedule + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledExecutorTask(Runnable executorTask, long delay) { + this.runnable = executorTask; + this.delay = delay; + } + + /** + * Create a new ScheduledExecutorTask. + * @param executorTask the Runnable 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 ScheduledExecutorTask(Runnable executorTask, long delay, long period, boolean fixedRate) { + this.runnable = executorTask; + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + + /** + * Set the Runnable to schedule as executor task. + */ + public void setRunnable(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Return the Runnable to schedule as executor task. + */ + public Runnable getRunnable() { + return this.runnable; + } + + /** + * Set the delay before starting the task for the first time, + * in milliseconds. Default is 0, immediately starting the + * task after successful scheduling. + */ + 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 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 not supported, + * simply because java.util.concurrent.ScheduledExecutorService itself + * does not support it. Hence a value of 0 will be treated as one-time execution; + * however, that value should never be specified explicitly in the first place! + * @see #setFixedRate + * @see #isOneTimeTask() + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ + 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); + } + + /** + * Specify the time unit for the delay and period values. + * Default is milliseconds (TimeUnit.MILLISECONDS). + * @see java.util.concurrent.TimeUnit#MILLISECONDS + * @see java.util.concurrent.TimeUnit#SECONDS + */ + public void setTimeUnit(TimeUnit timeUnit) { + this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS); + } + + /** + * Return the time unit for the delay and period values. + */ + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + /** + * Set whether to schedule as fixed-rate execution, rather than + * fixed-delay execution. Default is "false", that is, fixed delay. + *

See ScheduledExecutorService javadoc for details on those execution modes. + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ + 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/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java new file mode 100644 index 00000000000..4ef738a92ab --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -0,0 +1,359 @@ +/* + * 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.concurrent; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; + +/** + * JavaBean that allows for configuring a JDK 1.5 {@link java.util.concurrent.ThreadPoolExecutor} + * in bean style (through its "corePoolSize", "maxPoolSize", "keepAliveSeconds", "queueCapacity" + * properties), exposing it as a Spring {@link org.springframework.core.task.TaskExecutor}. + * This is an alternative to configuring a ThreadPoolExecutor instance directly using + * constructor injection, with a separate {@link ConcurrentTaskExecutor} adapter wrapping it. + * + *

For any custom needs, in particular for defining a + * {@link java.util.concurrent.ScheduledThreadPoolExecutor}, it is recommended to + * use a straight definition of the Executor instance or a factory method definition + * that points to the JDK 1.5 {@link java.util.concurrent.Executors} class. + * To expose such a raw Executor as a Spring {@link org.springframework.core.task.TaskExecutor}, + * simply wrap it with a {@link ConcurrentTaskExecutor} adapter. + * + *

NOTE: This class implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface as well as the JDK 1.5 + * {@link java.util.concurrent.Executor} interface, with the former being the primary + * interface, the other just serving as secondary convenience. For this reason, the + * exception handling follows the TaskExecutor contract rather than the Executor contract, + * in particular regarding the {@link org.springframework.core.task.TaskRejectedException}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.task.TaskExecutor + * @see java.util.concurrent.Executor + * @see java.util.concurrent.ThreadPoolExecutor + * @see java.util.concurrent.ScheduledThreadPoolExecutor + * @see java.util.concurrent.Executors + * @see ConcurrentTaskExecutor + */ +public class ThreadPoolTaskExecutor extends CustomizableThreadFactory + implements SchedulingTaskExecutor, Executor, BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Object poolSizeMonitor = new Object(); + + private int corePoolSize = 1; + + private int maxPoolSize = Integer.MAX_VALUE; + + private int keepAliveSeconds = 60; + + private boolean allowCoreThreadTimeOut = false; + + private int queueCapacity = Integer.MAX_VALUE; + + private ThreadFactory threadFactory = this; + + private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + + private boolean waitForTasksToCompleteOnShutdown = false; + + private boolean threadNamePrefixSet = false; + + private String beanName; + + private ThreadPoolExecutor threadPoolExecutor; + + + /** + * Set the ThreadPoolExecutor's core pool size. + * Default is 1. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setCorePoolSize(int corePoolSize) { + synchronized (this.poolSizeMonitor) { + this.corePoolSize = corePoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setCorePoolSize(corePoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's core pool size. + */ + public int getCorePoolSize() { + synchronized (this.poolSizeMonitor) { + return this.corePoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's maximum pool size. + * Default is Integer.MAX_VALUE. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setMaxPoolSize(int maxPoolSize) { + synchronized (this.poolSizeMonitor) { + this.maxPoolSize = maxPoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setMaximumPoolSize(maxPoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's maximum pool size. + */ + public int getMaxPoolSize() { + synchronized (this.poolSizeMonitor) { + return this.maxPoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's keep-alive seconds. + * Default is 60. + *

This setting can be modified at runtime, for example through JMX. + */ + public void setKeepAliveSeconds(int keepAliveSeconds) { + synchronized (this.poolSizeMonitor) { + this.keepAliveSeconds = keepAliveSeconds; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setKeepAliveTime(keepAliveSeconds, TimeUnit.SECONDS); + } + } + } + + /** + * Return the ThreadPoolExecutor's keep-alive seconds. + */ + public int getKeepAliveSeconds() { + synchronized (this.poolSizeMonitor) { + return this.keepAliveSeconds; + } + } + + /** + * Specify whether to allow core threads to time out. This enables dynamic + * growing and shrinking even in combination with a non-zero queue (since + * the max pool size will only grow once the queue is full). + *

Default is "false". Note that this feature is only available on Java 6 + * or above. On Java 5, consider switching to the backport-concurrent + * version of ThreadPoolTaskExecutor which also supports this feature. + * @see java.util.concurrent.ThreadPoolExecutor#allowCoreThreadTimeOut(boolean) + */ + public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + } + + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is Integer.MAX_VALUE. + *

Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + /** + * Set the ThreadFactory to use for the ThreadPoolExecutor's thread pool. + *

Default is this executor itself (i.e. the factory that this executor + * inherits from). See {@link org.springframework.util.CustomizableThreadCreator}'s + * javadoc for available bean properties. + * @see #setThreadPriority + * @see #setDaemon + */ + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = (threadFactory != null ? threadFactory : this); + } + + /** + * Set the RejectedExecutionHandler to use for the ThreadPoolExecutor. + * Default is the ThreadPoolExecutor's default abort policy. + * @see java.util.concurrent.ThreadPoolExecutor.AbortPolicy + */ + public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = + (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Set whether to wait for scheduled tasks to complete on shutdown. + *

Default is "false". Switch this to "true" if you prefer + * fully completed tasks at the expense of a longer shutdown phase. + * @see java.util.concurrent.ThreadPoolExecutor#shutdown() + * @see java.util.concurrent.ThreadPoolExecutor#shutdownNow() + */ + public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + public void setThreadNamePrefix(String threadNamePrefix) { + super.setThreadNamePrefix(threadNamePrefix); + this.threadNamePrefixSet = true; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + /** + * Calls initialize() after the container applied all property values. + * @see #initialize() + */ + public void afterPropertiesSet() { + initialize(); + } + + /** + * Creates the BlockingQueue and the ThreadPoolExecutor. + * @see #createQueue + */ + public void initialize() { + if (logger.isInfoEnabled()) { + logger.info("Initializing ThreadPoolExecutor" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (!this.threadNamePrefixSet && this.beanName != null) { + setThreadNamePrefix(this.beanName + "-"); + } + BlockingQueue queue = createQueue(this.queueCapacity); + this.threadPoolExecutor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, this.threadFactory, this.rejectedExecutionHandler); + if (this.allowCoreThreadTimeOut) { + this.threadPoolExecutor.allowCoreThreadTimeOut(true); + } + } + + /** + * Create the BlockingQueue to use for the ThreadPoolExecutor. + *

A LinkedBlockingQueue instance will be created for a positive + * capacity value; a SynchronousQueue else. + * @param queueCapacity the specified queue capacity + * @return the BlockingQueue instance + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + protected BlockingQueue createQueue(int queueCapacity) { + if (queueCapacity > 0) { + return new LinkedBlockingQueue(queueCapacity); + } + else { + return new SynchronousQueue(); + } + } + + /** + * Return the underlying ThreadPoolExecutor for native access. + * @return the underlying ThreadPoolExecutor (never null) + * @throws IllegalStateException if the ThreadPoolTaskExecutor hasn't been initialized yet + */ + public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException { + Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized"); + return this.threadPoolExecutor; + } + + + /** + * Implementation of both the JDK 1.5 Executor interface and the Spring + * TaskExecutor interface, delegating to the ThreadPoolExecutor instance. + * @see java.util.concurrent.Executor#execute(Runnable) + * @see org.springframework.core.task.TaskExecutor#execute(Runnable) + */ + public void execute(Runnable task) { + Executor executor = getThreadPoolExecutor(); + try { + executor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + /** + * This task executor prefers short-lived work units. + */ + public boolean prefersShortLivedTasks() { + return true; + } + + + /** + * Return the current pool size. + * @see java.util.concurrent.ThreadPoolExecutor#getPoolSize() + */ + public int getPoolSize() { + return getThreadPoolExecutor().getPoolSize(); + } + + /** + * Return the number of currently active threads. + * @see java.util.concurrent.ThreadPoolExecutor#getActiveCount() + */ + public int getActiveCount() { + return getThreadPoolExecutor().getActiveCount(); + } + + + /** + * Calls shutdown when the BeanFactory destroys + * the task executor instance. + * @see #shutdown() + */ + public void destroy() { + shutdown(); + } + + /** + * Perform a shutdown on the ThreadPoolExecutor. + * @see java.util.concurrent.ThreadPoolExecutor#shutdown() + */ + public void shutdown() { + if (logger.isInfoEnabled()) { + logger.info("Shutting down ThreadPoolExecutor" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (this.waitForTasksToCompleteOnShutdown) { + this.threadPoolExecutor.shutdown(); + } + else { + this.threadPoolExecutor.shutdownNow(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/package.html b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/package.html new file mode 100644 index 00000000000..8592a78dc8e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/concurrent/package.html @@ -0,0 +1,10 @@ + + + +Scheduling convenience classes for the JDK 1.5+ Executor mechanism +in the java.util.concurrent package, allowing to set +up a ThreadPoolExecutor or ScheduledThreadPoolExecutor as bean in +a Spring context. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/package.html b/org.springframework.context/src/main/java/org/springframework/scheduling/package.html new file mode 100644 index 00000000000..387fb1be2ea --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/package.html @@ -0,0 +1,8 @@ + + + +General exceptions for Spring's scheduling support, +independent of any specific scheduling system. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/support/DelegatingExceptionProofRunnable.java b/org.springframework.context/src/main/java/org/springframework/scheduling/support/DelegatingExceptionProofRunnable.java new file mode 100644 index 00000000000..7117890bfc1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/support/DelegatingExceptionProofRunnable.java @@ -0,0 +1,85 @@ +/* + * 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.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Runnable wrapper that catches any exception or error thrown + * from its delegate Runnable. Used for continuing scheduled + * execution even after an exception thrown from a task's Runnable. + * + * @author Juergen Hoeller + * @since 2.0.5 + */ +public class DelegatingExceptionProofRunnable implements Runnable { + + private static final Log logger = LogFactory.getLog(DelegatingExceptionProofRunnable.class); + + private final Runnable delegate; + + private final boolean propagateException; + + + /** + * Create a new DelegatingExceptionProofRunnable that logs the exception + * but isn't propagating it (in order to continue scheduled execution). + * @param delegate the Runnable implementation to delegate to + */ + public DelegatingExceptionProofRunnable(Runnable delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + this.propagateException = false; + } + + /** + * Create a new DelegatingExceptionProofRunnable. + * @param delegate the Runnable implementation to delegate to + * @param propagateException whether to propagate the exception after logging + * (note: this will typically cancel scheduled execution of the runnable) + */ + public DelegatingExceptionProofRunnable(Runnable delegate, boolean propagateException) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + this.propagateException = propagateException; + } + + /** + * Return the wrapped Runnable implementation. + */ + public final Runnable getDelegate() { + return this.delegate; + } + + + public void run() { + try { + this.delegate.run(); + } + catch (Throwable ex) { + logger.error("Unexpected exception thrown from Runnable: " + this.delegate, ex); + if (this.propagateException) { + ReflectionUtils.rethrowRuntimeException(ex); + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java b/org.springframework.context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java new file mode 100644 index 00000000000..e02ff8dd5fa --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.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.support; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.util.ClassUtils; + +/** + * Adapter that implements the {@link Runnable} interface as a configurable + * method invocation based on Spring's MethodInvoker. + * + *

Inherits common configuration properties from + * {@link org.springframework.util.MethodInvoker}. + * + *

Useful to generically encapsulate a method invocation as timer task + * for java.util.Timer, in combination with a + * {@link org.springframework.scheduling.timer.DelegatingTimerTask} adapter. + * Can also be used with JDK 1.5's java.util.concurrent.Executor + * abstraction, which works with plain Runnables. + * + *

Extended by Spring's + * {@link org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean} + * adapter for java.util.TimerTask. Note that you can populate a + * ScheduledTimerTask object with a plain MethodInvokingRunnable instance + * as well, which will automatically get wrapped with a DelegatingTimerTask. + * + * @author Juergen Hoeller + * @since 1.2.4 + * @see org.springframework.scheduling.timer.ScheduledTimerTask#setRunnable(Runnable) + * @see java.util.concurrent.Executor#execute(Runnable) + */ +public class MethodInvokingRunnable extends ArgumentConvertingMethodInvoker + implements Runnable, BeanClassLoaderAware, InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + protected Class resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, this.beanClassLoader); + } + + public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { + prepare(); + } + + + public void run() { + try { + invoke(); + } + catch (InvocationTargetException ex) { + logger.error(getInvocationFailureMessage(), ex.getTargetException()); + // Do not throw exception, else the main loop of the scheduler might stop! + } + catch (Throwable ex) { + logger.error(getInvocationFailureMessage(), ex); + // Do not throw exception, else the main loop of the scheduler might stop! + } + } + + /** + * Build a message for an invocation failure exception. + * @return the error message, including the target method name etc + */ + protected String getInvocationFailureMessage() { + return "Invocation of method '" + getTargetMethod() + + "' on target class [" + getTargetClass() + "] failed"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/support/package.html b/org.springframework.context/src/main/java/org/springframework/scheduling/support/package.html new file mode 100644 index 00000000000..d6c62992b54 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/support/package.html @@ -0,0 +1,8 @@ + + + +Generic support classes for scheduling. +Provides a Runnable adapter for Spring's MethodInvoker. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/timer/DelegatingTimerTask.java b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/DelegatingTimerTask.java new file mode 100644 index 00000000000..2cd03e643a1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/DelegatingTimerTask.java @@ -0,0 +1,74 @@ +/* + * 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.timer; + +import java.util.TimerTask; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * Simple {@link java.util.TimerTask} adapter that delegates to a + * given {@link java.lang.Runnable}. + * + *

This is often preferable to deriving from TimerTask, to be able to + * implement an interface rather than extend an abstract base class. + * + * @author Juergen Hoeller + * @since 1.2.4 + */ +public class DelegatingTimerTask extends TimerTask { + + private static final Log logger = LogFactory.getLog(DelegatingTimerTask.class); + + private final Runnable delegate; + + + /** + * Create a new DelegatingTimerTask. + * @param delegate the Runnable implementation to delegate to + */ + public DelegatingTimerTask(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, catching any exception + * or error thrown in order to continue scheduled execution. + */ + public void run() { + try { + this.delegate.run(); + } + catch (Throwable ex) { + logger.error("Unexpected exception thrown from Runnable: " + this.delegate, ex); + // Do not throw the exception, else the main loop of the Timer might stop! + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/timer/MethodInvokingTimerTaskFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/MethodInvokingTimerTaskFactoryBean.java new file mode 100644 index 00000000000..a0aaf874ce7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/MethodInvokingTimerTaskFactoryBean.java @@ -0,0 +1,69 @@ +/* + * 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.timer; + +import java.util.TimerTask; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.scheduling.support.MethodInvokingRunnable; + +/** + * FactoryBean that exposes a TimerTask object that delegates + * job execution to a specified (static or non-static) method. + * Avoids the need to implement a one-line TimerTask that just + * invokes an existing business method. + * + *

Derives from MethodInvokingRunnable to share common properties + * and behavior, effectively providing a TimerTask adapter for it. + * + *

Often used to populate a ScheduledTimerTask object with a specific + * reflective method invocation. Note that you can alternatively populate + * a ScheduledTimerTask object with a plain MethodInvokingRunnable instance + * as well (as of Spring 1.2.4), without the need for this special FactoryBean. + * + * @author Juergen Hoeller + * @since 19.02.2004 + * @see DelegatingTimerTask + * @see ScheduledTimerTask#setTimerTask + * @see ScheduledTimerTask#setRunnable + * @see org.springframework.scheduling.support.MethodInvokingRunnable + * @see org.springframework.beans.factory.config.MethodInvokingFactoryBean + */ +public class MethodInvokingTimerTaskFactoryBean extends MethodInvokingRunnable implements FactoryBean { + + private TimerTask timerTask; + + + public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { + super.afterPropertiesSet(); + this.timerTask = new DelegatingTimerTask(this); + } + + + public Object getObject() { + return this.timerTask; + } + + public Class getObjectType() { + return TimerTask.class; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/timer/ScheduledTimerTask.java b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/ScheduledTimerTask.java new file mode 100644 index 00000000000..d043fbba1fe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/ScheduledTimerTask.java @@ -0,0 +1,223 @@ +/* + * 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.timer; + +import java.util.TimerTask; + +/** + * JavaBean that describes a scheduled {@link TimerTask}, consisting of the + * {@link TimerTask} itself or a {@link Runnable} to create a {@link TimerTask} + * for and a delay plus period. The period needs to be specified; there is + * no point in a default for it. + * + *

The JDK's {@link java.util.Timer} facility does not offer sophisticated + * scheduling options such as cron expressions. Consider using Quartz for + * such advanced needs. + * + *

Note that the {@link java.util.Timer} mechanism uses a {@link TimerTask} + * instance that is shared between repeated executions, in contrast to Quartz + * which creates a new Job instance for each execution. + * + * @author Juergen Hoeller + * @since 19.02.2004 + * @see java.util.TimerTask + * @see java.util.Timer#schedule(TimerTask, long, long) + * @see java.util.Timer#scheduleAtFixedRate(TimerTask, long, long) + */ +public class ScheduledTimerTask { + + private TimerTask timerTask; + + private long delay = 0; + + private long period = -1; + + private boolean fixedRate = false; + + + /** + * Create a new ScheduledTimerTask, + * to be populated via bean properties. + * @see #setTimerTask + * @see #setDelay + * @see #setPeriod + * @see #setFixedRate + */ + public ScheduledTimerTask() { + } + + /** + * Create a new ScheduledTimerTask, with default + * one-time execution without delay. + * @param timerTask the TimerTask to schedule + */ + public ScheduledTimerTask(TimerTask timerTask) { + this.timerTask = timerTask; + } + + /** + * Create a new ScheduledTimerTask, with default + * one-time execution with the given delay. + * @param timerTask the TimerTask to schedule + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledTimerTask(TimerTask timerTask, long delay) { + this.timerTask = timerTask; + this.delay = delay; + } + + /** + * Create a new ScheduledTimerTask. + * @param timerTask the TimerTask 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 ScheduledTimerTask(TimerTask timerTask, long delay, long period, boolean fixedRate) { + this.timerTask = timerTask; + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + /** + * Create a new ScheduledTimerTask, with default + * one-time execution without delay. + * @param timerTask the Runnable to schedule as TimerTask + */ + public ScheduledTimerTask(Runnable timerTask) { + setRunnable(timerTask); + } + + /** + * Create a new ScheduledTimerTask, with default + * one-time execution with the given delay. + * @param timerTask the Runnable to schedule as TimerTask + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledTimerTask(Runnable timerTask, long delay) { + setRunnable(timerTask); + this.delay = delay; + } + + /** + * Create a new ScheduledTimerTask. + * @param timerTask the Runnable to schedule as TimerTask + * @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 ScheduledTimerTask(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 TimerTask. + * @see DelegatingTimerTask + */ + public void setRunnable(Runnable timerTask) { + this.timerTask = new DelegatingTimerTask(timerTask); + } + + /** + * Set the TimerTask to schedule. + */ + public void setTimerTask(TimerTask timerTask) { + this.timerTask = timerTask; + } + + /** + * Return the TimerTask to schedule. + */ + public TimerTask getTimerTask() { + return this.timerTask; + } + + /** + * Set the delay before starting the task for the first time, + * in milliseconds. Default is 0, immediately starting the + * task after successful scheduling. + */ + 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 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 not + * supported, simply because java.util.Timer itself does not + * support it. Hence a value of 0 will be treated as one-time execution; + * however, that value should never be specified explicitly in the first place! + * @see #setFixedRate + * @see #isOneTimeTask() + * @see java.util.Timer#schedule(TimerTask, 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", that is, fixed delay. + *

See Timer javadoc for details on those execution modes. + * @see java.util.Timer#schedule(TimerTask, long, long) + * @see java.util.Timer#scheduleAtFixedRate(TimerTask, 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/src/main/java/org/springframework/scheduling/timer/TimerFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/TimerFactoryBean.java new file mode 100644 index 00000000000..8a2b3c3e040 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/TimerFactoryBean.java @@ -0,0 +1,184 @@ +/* + * 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.timer; + +import java.util.Timer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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.core.JdkVersion; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * FactoryBean that sets up a {@link java.util.Timer} and exposes it for bean references. + * + *

Allows for registration of {@link ScheduledTimerTask ScheduledTimerTasks}, + * automatically starting the {@link Timer} on initialization and cancelling it + * on destruction of the context. In scenarios that just require static registration + * of tasks at startup, there is no need to access the {@link Timer} instance itself + * in application code at all. + * + *

Note that the {@link Timer} mechanism uses a {@link java.util.TimerTask} + * instance that is shared between repeated executions, in contrast to Quartz + * which creates a new Job instance for each execution. + * + * @author Juergen Hoeller + * @since 19.02.2004 + * @see ScheduledTimerTask + * @see java.util.Timer + * @see java.util.TimerTask + */ +public class TimerFactoryBean implements FactoryBean, BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ScheduledTimerTask[] scheduledTimerTasks; + + private boolean daemon = false; + + private String beanName; + + private Timer timer; + + + /** + * Register a list of ScheduledTimerTask objects with the Timer that + * this FactoryBean creates. Depending on each SchedulerTimerTask's + * settings, it will be registered via one of Timer's schedule methods. + * @see java.util.Timer#schedule(java.util.TimerTask, long) + * @see java.util.Timer#schedule(java.util.TimerTask, long, long) + * @see java.util.Timer#scheduleAtFixedRate(java.util.TimerTask, long, long) + */ + public void setScheduledTimerTasks(ScheduledTimerTask[] scheduledTimerTasks) { + this.scheduledTimerTasks = scheduledTimerTasks; + } + + /** + * Set whether the timer should use a daemon thread, + * just executing as long as the application itself is running. + *

Default is "false": The timer will automatically get cancelled on + * destruction of this FactoryBean. Hence, if the application shuts down, + * tasks will by default finish their execution. Specify "true" for eager + * shutdown of threads that execute tasks. + * @see java.util.Timer#Timer(boolean) + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + + public void afterPropertiesSet() { + logger.info("Initializing Timer"); + this.timer = createTimer(this.beanName, this.daemon); + + // Register specified ScheduledTimerTasks, if necessary. + if (!ObjectUtils.isEmpty(this.scheduledTimerTasks)) { + registerTasks(this.scheduledTimerTasks, this.timer); + } + } + + /** + * Create a new Timer instance. Called by afterPropertiesSet. + * Can be overridden in subclasses to provide custom Timer subclasses. + *

Uses the specified name as Timer thread name on JDK 1.5, + * simply falling back to a default Timer thread on JDK 1.4. + * @param name the desired name of the Timer's associated thread + * (applied on JDK 1.5 and higher; ignored on JDK 1.4) + * @param daemon whether to create a Timer that runs as daemon thread + * @return a new Timer instance + * @see #afterPropertiesSet() + * @see java.util.Timer#Timer(boolean) + */ + protected Timer createTimer(String name, boolean daemon) { + Timer timer = createTimer(daemon); + if (timer != null) { + return timer; + } + if (StringUtils.hasText(name) && JdkVersion.isAtLeastJava15()) { + return new Timer(name, daemon); + } + else { + return new Timer(daemon); + } + } + + /** + * Create a new Timer instance. Called by afterPropertiesSet. + * Can be overridden in subclasses to provide custom Timer subclasses. + * @deprecated as of Spring 2.0.1, in favor of {@link #createTimer(String, boolean)} + */ + protected Timer createTimer(boolean daemon) { + return null; + } + + /** + * Register the specified {@link ScheduledTimerTask ScheduledTimerTasks} + * on the given {@link Timer}. + * @param tasks the specified ScheduledTimerTasks (never empty) + * @param timer the Timer to register the tasks on. + */ + protected void registerTasks(ScheduledTimerTask[] tasks, Timer timer) { + for (int i = 0; i < tasks.length; i++) { + ScheduledTimerTask task = tasks[i]; + if (task.isOneTimeTask()) { + timer.schedule(task.getTimerTask(), task.getDelay()); + } + else { + if (task.isFixedRate()) { + timer.scheduleAtFixedRate(task.getTimerTask(), task.getDelay(), task.getPeriod()); + } + else { + timer.schedule(task.getTimerTask(), task.getDelay(), task.getPeriod()); + } + } + } + } + + + public Object getObject() { + return this.timer; + } + + public Class getObjectType() { + return Timer.class; + } + + public boolean isSingleton() { + return true; + } + + + /** + * Cancel the Timer on bean factory shutdown, stopping all scheduled tasks. + * @see java.util.Timer#cancel() + */ + public void destroy() { + logger.info("Cancelling Timer"); + this.timer.cancel(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/timer/TimerTaskExecutor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/TimerTaskExecutor.java new file mode 100644 index 00000000000..885361b968c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/TimerTaskExecutor.java @@ -0,0 +1,139 @@ +/* + * 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.timer; + +import java.util.Timer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.core.task.TaskExecutor} implementation that uses a + * single {@link Timer} for executing all tasks, effectively resulting in + * serialized asynchronous execution on a single thread. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.Timer + */ +public class TimerTaskExecutor implements SchedulingTaskExecutor, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private Timer timer; + + private int delay = 0; + + private boolean timerInternal = false; + + + /** + * Create a new TimerTaskExecutor that needs to be further configured and initialized. + * @see #setTimer + * @see #afterPropertiesSet + */ + public TimerTaskExecutor() { + } + + /** + * Create a new TimerTaskExecutor for the given {@link Timer}. + * @param timer the {@link Timer} to wrap + */ + public TimerTaskExecutor(Timer timer) { + Assert.notNull(timer, "Timer must not be null"); + this.timer = timer; + } + + + /** + * Set the {@link Timer} to use for this {@link TimerTaskExecutor}, for example + * a shared {@link Timer} instance defined by a {@link TimerFactoryBean}. + *

If not specified, a default internal {@link Timer} instance will be used. + * @param timer the {@link Timer} to use for this {@link TimerTaskExecutor} + * @see TimerFactoryBean + */ + public void setTimer(Timer timer) { + this.timer = timer; + } + + /** + * Set the delay to use for scheduling tasks passed into the + * execute method. Default is 0. + * @param delay the delay in milliseconds before the task is to be executed + */ + public void setDelay(int delay) { + this.delay = delay; + } + + + public void afterPropertiesSet() { + if (this.timer == null) { + logger.info("Initializing Timer"); + this.timer = createTimer(); + this.timerInternal = true; + } + } + + /** + * Create a new {@link Timer} instance. Called by afterPropertiesSet + * if no {@link Timer} has been specified explicitly. + *

The default implementation creates a plain daemon {@link Timer}. + * If overridden, subclasses must take care to ensure that a non-null + * {@link Timer} is returned from the execution of this method. + * @see #afterPropertiesSet + * @see java.util.Timer#Timer(boolean) + */ + protected Timer createTimer() { + return new Timer(true); + } + + + /** + * Schedules the given {@link Runnable} on this executor's {@link Timer} instance, + * wrapping it in a {@link DelegatingTimerTask}. + * @param task the task to be executed + */ + public void execute(Runnable task) { + Assert.notNull(this.timer, "Timer is required"); + this.timer.schedule(new DelegatingTimerTask(task), this.delay); + } + + /** + * This task executor prefers short-lived work units. + */ + public boolean prefersShortLivedTasks() { + return true; + } + + + /** + * Cancel the {@link Timer} on bean factory shutdown, stopping all scheduled tasks. + * @see java.util.Timer#cancel() + */ + public void destroy() { + if (this.timerInternal) { + logger.info("Cancelling Timer"); + this.timer.cancel(); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/timer/package.html b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/package.html new file mode 100644 index 00000000000..5c02ca89ba6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scheduling/timer/package.html @@ -0,0 +1,9 @@ + + + +Scheduling convenience classes for the JDK Timer, +allowing to set up Timers and ScheduledTimerTasks +as beans in a Spring context. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/ScriptCompilationException.java b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptCompilationException.java new file mode 100644 index 00000000000..e6f56a4bafb --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptCompilationException.java @@ -0,0 +1,82 @@ +/* + * 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.scripting; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception to be thrown on script compilation failure. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class ScriptCompilationException extends NestedRuntimeException { + + private ScriptSource scriptSource; + + + /** + * Constructor for ScriptCompilationException. + * @param msg the detail message + */ + public ScriptCompilationException(String msg) { + super(msg); + } + + /** + * Constructor for ScriptCompilationException. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * script compiler API) + */ + public ScriptCompilationException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for ScriptCompilationException. + * @param scriptSource the source for the offending script + * @param cause the root cause (usually from using an underlying + * script compiler API) + */ + public ScriptCompilationException(ScriptSource scriptSource, Throwable cause) { + super("Could not compile script", cause); + this.scriptSource = scriptSource; + } + + /** + * Constructor for ScriptCompilationException. + * @param msg the detail message + * @param scriptSource the source for the offending script + * @param cause the root cause (usually from using an underlying + * script compiler API) + */ + public ScriptCompilationException(ScriptSource scriptSource, String msg, Throwable cause) { + super("Could not compile script [" + scriptSource + "]: " + msg, cause); + this.scriptSource = scriptSource; + } + + + /** + * Return the source for the offending script. + * @return the source, or null if not available + */ + public ScriptSource getScriptSource() { + return this.scriptSource; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/ScriptFactory.java b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptFactory.java new file mode 100644 index 00000000000..e775f3035db --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptFactory.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.scripting; + +import java.io.IOException; + +/** + * Script definition interface, encapsulating the configuration + * of a specific script as well as a factory method for + * creating the actual scripted Java Object. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see #getScriptSourceLocator + * @see #getScriptedObject + */ +public interface ScriptFactory { + + /** + * Return a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + *

Typical supported locators are Spring resource locations + * (such as "file:C:/myScript.bsh" or "classpath:myPackage/myScript.bsh") + * and inline scripts ("inline:myScriptText..."). + * @return the script source locator + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor#convertToScriptSource + * @see org.springframework.core.io.ResourceLoader + */ + String getScriptSourceLocator(); + + /** + * Return the business interfaces that the script is supposed to implement. + *

Can return null if the script itself determines + * its Java interfaces (such as in the case of Groovy). + * @return the interfaces for the script + */ + Class[] getScriptInterfaces(); + + /** + * Return whether the script requires a config interface to be + * generated for it. This is typically the case for scripts that + * do not determine Java signatures themselves, with no appropriate + * config interface specified in getScriptInterfaces(). + * @return whether the script requires a generated config interface + * @see #getScriptInterfaces() + */ + boolean requiresConfigInterface(); + + /** + * Factory method for creating the scripted Java object. + *

Implementations are encouraged to cache script metadata such as + * a generated script class. Note that this method may be invoked + * concurrently and must be implemented in a thread-safe fashion. + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never null) + * @param actualInterfaces the actual interfaces to expose, + * including script interfaces as well as a generated config interface + * (if applicable; may be null) + * @return the scripted Java object + * @throws IOException if script retrieval failed + * @throws ScriptCompilationException if script compilation failed + */ + Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces) + throws IOException, ScriptCompilationException; + + /** + * Determine the type of the scripted Java object. + *

Implementations are encouraged to cache script metadata such as + * a generated script class. Note that this method may be invoked + * concurrently and must be implemented in a thread-safe fashion. + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never null) + * @return the type of the scripted Java object, or null + * if none could be determined + * @throws IOException if script retrieval failed + * @throws ScriptCompilationException if script compilation failed + * @since 2.0.3 + */ + Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException; + + /** + * Determine whether a refresh is required (e.g. through + * ScriptSource's isModified() method). + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never null) + * @return whether a fresh {@link #getScriptedObject} call is required + * @since 2.5.2 + * @see ScriptSource#isModified() + */ + boolean requiresScriptedObjectRefresh(ScriptSource scriptSource); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/ScriptSource.java b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptSource.java new file mode 100644 index 00000000000..2e5d8727d0f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/ScriptSource.java @@ -0,0 +1,52 @@ +/* + * 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.scripting; + +import java.io.IOException; + +/** + * Interface that defines the source of a script. + * Tracks whether the underlying script has been modified. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public interface ScriptSource { + + /** + * Retrieve the current script source text as String. + * @return the script text + * @throws IOException if script retrieval failed + */ + String getScriptAsString() throws IOException; + + /** + * Indicate whether the underlying script data has been modified since + * the last time {@link #getScriptAsString()} was called. + * Returns true if the script has not been read yet. + * @return whether the script data has been modified + */ + boolean isModified(); + + /** + * Determine a class name for the underlying script. + * @return the suggested class name, or null if none available + */ + String suggestedClassName(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java new file mode 100644 index 00000000000..0ba587ccf78 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java @@ -0,0 +1,191 @@ +/* + * 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.scripting.bsh; + +import java.io.IOException; + +import bsh.EvalError; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation + * for a BeanShell script. + * + *

Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc for a configuration example. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see BshScriptUtils + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor + */ +public class BshScriptFactory implements ScriptFactory, BeanClassLoaderAware { + + private final String scriptSourceLocator; + + private final Class[] scriptInterfaces; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Class scriptClass; + + private final Object scriptClassMonitor = new Object(); + + private boolean wasModifiedForTypeCheck = false; + + + /** + * Create a new BshScriptFactory for the given script source. + *

With this BshScriptFactory variant, the script needs to + * declare a full class or return an actual instance of the scripted object. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public BshScriptFactory(String scriptSourceLocator) { + this(scriptSourceLocator, null); + } + + /** + * Create a new BshScriptFactory for the given script source. + *

The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param scriptInterfaces the Java interfaces that the scripted object + * is supposed to implement (may be null) + */ + public BshScriptFactory(String scriptSourceLocator, Class[] scriptInterfaces) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + this.scriptInterfaces = scriptInterfaces; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + public Class[] getScriptInterfaces() { + return this.scriptInterfaces; + } + + /** + * BeanShell scripts do require a config interface. + */ + public boolean requiresConfigInterface() { + return true; + } + + /** + * Load and parse the BeanShell script via {@link BshScriptUtils}. + * @see BshScriptUtils#createBshObject(String, Class[], ClassLoader) + */ + public Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces) + throws IOException, ScriptCompilationException { + + try { + Class clazz = null; + + synchronized (this.scriptClassMonitor) { + boolean requiresScriptEvaluation = (this.wasModifiedForTypeCheck && this.scriptClass == null); + this.wasModifiedForTypeCheck = false; + + if (scriptSource.isModified() || requiresScriptEvaluation) { + // New script content: Let's check whether it evaluates to a Class. + Object result = BshScriptUtils.evaluateBshScript( + scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); + if (result instanceof Class) { + // A Class: We'll cache the Class here and create an instance + // outside of the synchronized block. + this.scriptClass = (Class) result; + } + else { + // Not a Class: OK, we'll simply create BeanShell objects + // through evaluating the script for every call later on. + // For this first-time check, let's simply return the + // already evaluated object. + return result; + } + } + clazz = this.scriptClass; + } + + if (clazz != null) { + // A Class: We need to create an instance for every call. + try { + return clazz.newInstance(); + } + catch (Throwable ex) { + throw new ScriptCompilationException( + scriptSource, "Could not instantiate script class: " + clazz.getName(), ex); + } + } + else { + // Not a Class: We need to evaluate the script for every call. + return BshScriptUtils.createBshObject( + scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); + } + } + catch (EvalError ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + try { + synchronized (this.scriptClassMonitor) { + if (scriptSource.isModified()) { + // New script content: Let's check whether it evaluates to a Class. + this.wasModifiedForTypeCheck = true; + this.scriptClass = BshScriptUtils.determineBshObjectType(scriptSource.getScriptAsString()); + } + return this.scriptClass; + } + } + catch (EvalError ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + synchronized (this.scriptClassMonitor) { + return (scriptSource.isModified() || this.wasModifiedForTypeCheck); + } + } + + + public String toString() { + return "BshScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java new file mode 100644 index 00000000000..37606f50d45 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java @@ -0,0 +1,222 @@ +/* + * 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.scripting.bsh; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import bsh.EvalError; +import bsh.Interpreter; +import bsh.Primitive; +import bsh.XThis; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility methods for handling BeanShell-scripted objects. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class BshScriptUtils { + + /** + * Create a new BeanShell-scripted object from the given script source. + *

With this createBshObject variant, the script needs to + * declare a full class or return an actual instance of the scripted object. + * @param scriptSource the script source text + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + */ + public static Object createBshObject(String scriptSource) throws EvalError { + return createBshObject(scriptSource, null, null); + } + + /** + * Create a new BeanShell-scripted object from the given script source, + * using the default ClassLoader. + *

The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be null or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + * @see #createBshObject(String, Class[], ClassLoader) + */ + public static Object createBshObject(String scriptSource, Class[] scriptInterfaces) throws EvalError { + return createBshObject(scriptSource, scriptInterfaces, ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new BeanShell-scripted object from the given script source. + *

The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be null or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @param classLoader the ClassLoader to create the script proxy with + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + */ + public static Object createBshObject(String scriptSource, Class[] scriptInterfaces, ClassLoader classLoader) + throws EvalError { + + Object result = evaluateBshScript(scriptSource, scriptInterfaces, classLoader); + if (result instanceof Class) { + Class clazz = (Class) result; + try { + return clazz.newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not instantiate script class [" + + clazz.getName() + "]. Root cause is " + ex); + } + } + else { + return result; + } + } + + /** + * Evaluate the specified BeanShell script based on the given script source, + * returning the Class defined by the script. + *

The script may either declare a full class or return an actual instance of + * the scripted object (in which case the Class of the object will be returned). + * In any other case, the returned Class will be null. + * @param scriptSource the script source text + * @return the scripted Java class, or null if none could be determined + * @throws EvalError in case of BeanShell parsing failure + */ + static Class determineBshObjectType(String scriptSource) throws EvalError { + Assert.hasText(scriptSource, "Script source must not be empty"); + Interpreter interpreter = new Interpreter(); + Object result = interpreter.eval(scriptSource); + if (result instanceof Class) { + return (Class) result; + } + else if (result != null) { + return result.getClass(); + } + else { + return null; + } + } + + /** + * Evaluate the specified BeanShell script based on the given script source, + * keeping a returned script Class or script Object as-is. + *

The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be null or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @param classLoader the ClassLoader to create the script proxy with + * @return the scripted Java class or Java object + * @throws EvalError in case of BeanShell parsing failure + */ + static Object evaluateBshScript(String scriptSource, Class[] scriptInterfaces, ClassLoader classLoader) + throws EvalError { + + Assert.hasText(scriptSource, "Script source must not be empty"); + Interpreter interpreter = new Interpreter(); + Object result = interpreter.eval(scriptSource); + if (result != null) { + return result; + } + else { + // Simple BeanShell script: Let's create a proxy for it, implementing the given interfaces. + Assert.notEmpty(scriptInterfaces, + "Given script requires a script proxy: At least one script interface is required."); + XThis xt = (XThis) interpreter.eval("return this"); + return Proxy.newProxyInstance(classLoader, scriptInterfaces, new BshObjectInvocationHandler(xt)); + } + } + + + /** + * InvocationHandler that invokes a BeanShell script method. + */ + private static class BshObjectInvocationHandler implements InvocationHandler { + + private final XThis xt; + + public BshObjectInvocationHandler(XThis xt) { + this.xt = xt; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (ReflectionUtils.isEqualsMethod(method)) { + return (isProxyForSameBshObject(args[0]) ? Boolean.TRUE : Boolean.FALSE); + } + else if (ReflectionUtils.isHashCodeMethod(method)) { + return new Integer(this.xt.hashCode()); + } + else if (ReflectionUtils.isToStringMethod(method)) { + return "BeanShell object [" + this.xt + "]"; + } + try { + Object result = this.xt.invokeMethod(method.getName(), args); + if (result == Primitive.NULL || result == Primitive.VOID) { + return null; + } + if (result instanceof Primitive) { + return ((Primitive) result).getValue(); + } + return result; + } + catch (EvalError ex) { + throw new BshExecutionException(ex); + } + } + + private boolean isProxyForSameBshObject(Object other) { + if (!Proxy.isProxyClass(other.getClass())) { + return false; + } + InvocationHandler ih = Proxy.getInvocationHandler(other); + return (ih instanceof BshObjectInvocationHandler && + this.xt.equals(((BshObjectInvocationHandler) ih).xt)); + } + } + + + /** + * Exception to be thrown on script execution failure. + */ + public static class BshExecutionException extends NestedRuntimeException { + + private BshExecutionException(EvalError ex) { + super("BeanShell script execution failed", ex); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/bsh/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/package.html new file mode 100644 index 00000000000..5ad2dee9a32 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/bsh/package.html @@ -0,0 +1,9 @@ + + + +Package providing integration of +BeanShell +into Spring's scripting infrastructure. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java b/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java new file mode 100644 index 00000000000..6c826fc1412 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java @@ -0,0 +1,53 @@ +/* + * 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.scripting.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * NamespaceHandler that supports the wiring of + * objects backed by dynamic languages such as Groovy, JRuby and + * BeanShell. The following is an example (from the reference + * documentation) that details the wiring of a Groovy backed bean: + * + *

+ * <lang:groovy id="messenger"
+ *     refresh-check-delay="5000"
+ *     script-source="classpath:Messenger.groovy">
+ * <lang:property name="message" value="I Can Do The Frug"/>
+ * </lang:groovy>
+ * 
+ * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +public class LangNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerScriptBeanDefinitionParser("groovy", "org.springframework.scripting.groovy.GroovyScriptFactory"); + registerScriptBeanDefinitionParser("jruby", "org.springframework.scripting.jruby.JRubyScriptFactory"); + registerScriptBeanDefinitionParser("bsh", "org.springframework.scripting.bsh.BshScriptFactory"); + registerBeanDefinitionParser("defaults", new ScriptingDefaultsParser()); + } + + private void registerScriptBeanDefinitionParser(String key, String scriptFactoryClassName) { + registerBeanDefinitionParser(key, new ScriptBeanDefinitionParser(scriptFactoryClassName)); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java b/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java new file mode 100644 index 00000000000..0bb666a7764 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java @@ -0,0 +1,58 @@ +/* + * 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.scripting.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; + +/** + * @author Rob Harrop + * @author Mark Fisher + * @since 2.5 + */ +public abstract class LangNamespaceUtils { + + /** + * The unique name under which the internally managed {@link ScriptFactoryPostProcessor} is + * registered in the {@link BeanDefinitionRegistry}. + */ + private static final String SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME = + "org.springframework.scripting.config.scriptFactoryPostProcessor"; + + + /** + * Register a {@link ScriptFactoryPostProcessor} bean definition in the supplied + * {@link BeanDefinitionRegistry} if the {@link ScriptFactoryPostProcessor} hasn't + * already been registered. + * @param registry the {@link BeanDefinitionRegistry} to register the script processor with + * @return the {@link ScriptFactoryPostProcessor} bean definition (new or already registered) + */ + public static BeanDefinition registerScriptFactoryPostProcessorIfNecessary(BeanDefinitionRegistry registry) { + BeanDefinition beanDefinition = null; + if (registry.containsBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME)) { + beanDefinition = registry.getBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME); + } + else { + beanDefinition = new RootBeanDefinition(ScriptFactoryPostProcessor.class); + registry.registerBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME, beanDefinition); + } + return beanDefinition; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java new file mode 100644 index 00000000000..db184116af5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java @@ -0,0 +1,216 @@ +/* + * 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.scripting.config; + +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * BeanDefinitionParser implementation for the '<lang:groovy/>', + * '<lang:jruby/>' and '<lang:bsh/>' tags. + * Allows for objects written using dynamic languages to be easily exposed with + * the {@link org.springframework.beans.factory.BeanFactory}. + * + *

The script for each object can be specified either as a reference to the Resource + * containing it (using the 'script-source' attribute) or inline in the XML configuration + * itself (using the 'inline-script' attribute. + * + *

By default, dynamic objects created with these tags are not refreshable. + * To enable refreshing, specify the refresh check delay for each object (in milliseconds) using the + * 'refresh-check-delay' attribute. + * + * @author Rob Harrop + * @author Rod Johnson + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +class ScriptBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String SCRIPT_SOURCE_ATTRIBUTE = "script-source"; + + private static final String INLINE_SCRIPT_ELEMENT = "inline-script"; + + private static final String SCOPE_ATTRIBUTE = "scope"; + + private static final String AUTOWIRE_ATTRIBUTE = "autowire"; + + private static final String DEPENDENCY_CHECK_ATTRIBUTE = "dependency-check"; + + private static final String INIT_METHOD_ATTRIBUTE = "init-method"; + + private static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method"; + + private static final String SCRIPT_INTERFACES_ATTRIBUTE = "script-interfaces"; + + private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; + + private static final String CUSTOMIZER_REF_ATTRIBUTE = "customizer-ref"; + + + /** + * The {@link org.springframework.scripting.ScriptFactory} class that this + * parser instance will create bean definitions for. + */ + private final String scriptFactoryClassName; + + + /** + * Create a new instance of this parser, creating bean definitions for the + * supplied {@link org.springframework.scripting.ScriptFactory} class. + * @param scriptFactoryClassName the ScriptFactory class to operate on + */ + public ScriptBeanDefinitionParser(String scriptFactoryClassName) { + this.scriptFactoryClassName = scriptFactoryClassName; + } + + + /** + * Parses the dynamic object element and returns the resulting bean definition. + * Registers a {@link ScriptFactoryPostProcessor} if needed. + */ + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + // Resolve the script source. + String value = resolveScriptSource(element, parserContext.getReaderContext()); + if (value == null) { + return null; + } + + // Set up infrastructure. + LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); + + // Create script factory bean definition. + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClassName(this.scriptFactoryClassName); + bd.setSource(parserContext.extractSource(element)); + + // Determine bean scope. + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + bd.setScope(scope); + } + + // Determine autowire mode. + String autowire = element.getAttribute(AUTOWIRE_ATTRIBUTE); + int autowireMode = parserContext.getDelegate().getAutowireMode(autowire); + // Only "byType" and "byName" supported, but maybe other default inherited... + if (autowireMode == GenericBeanDefinition.AUTOWIRE_AUTODETECT) { + autowireMode = GenericBeanDefinition.AUTOWIRE_BY_TYPE; + } + else if (autowireMode == GenericBeanDefinition.AUTOWIRE_CONSTRUCTOR) { + autowireMode = GenericBeanDefinition.AUTOWIRE_NO; + } + bd.setAutowireMode(autowireMode); + + // Determine dependency check setting. + String dependencyCheck = element.getAttribute(DEPENDENCY_CHECK_ATTRIBUTE); + bd.setDependencyCheck(parserContext.getDelegate().getDependencyCheck(dependencyCheck)); + + // Retrieve the defaults for bean definitions within this parser context + BeanDefinitionDefaults beanDefinitionDefaults = + parserContext.getDelegate().getBeanDefinitionDefaults(); + + // Determine init method and destroy method. + String initMethod = element.getAttribute(INIT_METHOD_ATTRIBUTE); + if (StringUtils.hasLength(initMethod)) { + bd.setInitMethodName(initMethod); + } + else if (beanDefinitionDefaults.getInitMethodName() != null) { + bd.setInitMethodName(beanDefinitionDefaults.getInitMethodName()); + } + + String destroyMethod = element.getAttribute(DESTROY_METHOD_ATTRIBUTE); + if (StringUtils.hasLength(destroyMethod)) { + bd.setDestroyMethodName(destroyMethod); + } + else if (beanDefinitionDefaults.getDestroyMethodName() != null) { + bd.setDestroyMethodName(beanDefinitionDefaults.getDestroyMethodName()); + } + + // Attach any refresh metadata. + String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (StringUtils.hasText(refreshCheckDelay)) { + bd.setAttribute( + ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE, new Long(refreshCheckDelay)); + } + + // Add constructor arguments. + ConstructorArgumentValues cav = bd.getConstructorArgumentValues(); + int constructorArgNum = 0; + cav.addIndexedArgumentValue(constructorArgNum++, value); + if (element.hasAttribute(SCRIPT_INTERFACES_ATTRIBUTE)) { + cav.addIndexedArgumentValue(constructorArgNum++, element.getAttribute(SCRIPT_INTERFACES_ATTRIBUTE)); + } + + // This is used for Groovy. It's a bean reference to a customizer bean. + if (element.hasAttribute(CUSTOMIZER_REF_ATTRIBUTE)) { + String customizerBeanName = element.getAttribute(CUSTOMIZER_REF_ATTRIBUTE); + cav.addIndexedArgumentValue(constructorArgNum++, new RuntimeBeanReference(customizerBeanName)); + } + + // Add any property definitions that need adding. + parserContext.getDelegate().parsePropertyElements(element, bd); + + return bd; + } + + /** + * Resolves the script source from either the 'script-source' attribute or + * the 'inline-script' element. Logs and {@link XmlReaderContext#error} and + * returns null if neither or both of these values are specified. + */ + private String resolveScriptSource(Element element, XmlReaderContext readerContext) { + boolean hasScriptSource = element.hasAttribute(SCRIPT_SOURCE_ATTRIBUTE); + List elements = DomUtils.getChildElementsByTagName(element, INLINE_SCRIPT_ELEMENT); + if (hasScriptSource && !elements.isEmpty()) { + readerContext.error("Only one of 'script-source' and 'inline-script' should be specified.", element); + return null; + } + else if (hasScriptSource) { + return element.getAttribute(SCRIPT_SOURCE_ATTRIBUTE); + } + else if (!elements.isEmpty()) { + Element inlineElement = (Element) elements.get(0); + return "inline:" + DomUtils.getTextValue(inlineElement); + } + else { + readerContext.error("Must specify either 'script-source' or 'inline-script'.", element); + return null; + } + } + + /** + * Scripted beans may be anonymous as well. + */ + protected boolean shouldGenerateIdAsFallback() { + return true; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java b/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java new file mode 100644 index 00000000000..ac59ca7e7f5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java @@ -0,0 +1,45 @@ +/* + * 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.scripting.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * @author Mark Fisher + * @since 2.5 + */ +public class ScriptingDefaultsParser implements BeanDefinitionParser { + + private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; + + + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinition bd = + LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); + String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (StringUtils.hasText(refreshCheckDelay)) { + bd.getPropertyValues().addPropertyValue("defaultRefreshCheckDelay", new Long(refreshCheckDelay)); + } + return null; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/config/package.html new file mode 100644 index 00000000000..b899ea0ce6c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/package.html @@ -0,0 +1,8 @@ + + + +Support package for Spring's dynamic language machinery, +with XML schema being the primary configuration format. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.0.xsd b/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.0.xsd new file mode 100644 index 00000000000..e671dfa5314 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.0.xsd @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.5.xsd b/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.5.xsd new file mode 100644 index 00000000000..b0c6599f3d8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/config/spring-lang-2.5.xsd @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java new file mode 100644 index 00000000000..66fe2e95fa6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java @@ -0,0 +1,43 @@ +/* + * 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.scripting.groovy; + +import groovy.lang.GroovyObject; + +/** + * Strategy used by {@link GroovyScriptFactory} to allow the customization of + * a created {@link GroovyObject}. + * + *

This is useful to allow the authoring of DSLs, the replacement of missing + * methods, and so forth. For example, a custom {@link groovy.lang.MetaClass} + * could be specified. + * + * @author Rod Johnson + * @since 2.0.2 + * @see GroovyScriptFactory + */ +public interface GroovyObjectCustomizer { + + /** + * Customize the supplied {@link GroovyObject}. + *

For example, this can be used to set a custom metaclass to + * handle missing methods. + * @param goo the GroovyObject to customize + */ + void customize(GroovyObject goo); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java new file mode 100644 index 00000000000..1144c05487d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java @@ -0,0 +1,282 @@ +/* + * 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.scripting.groovy; + +import java.io.IOException; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyObject; +import groovy.lang.MetaClass; +import groovy.lang.Script; +import org.codehaus.groovy.control.CompilationFailedException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation + * for a Groovy script. + * + *

Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc} for a configuration example. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @since 2.0 + * @see groovy.lang.GroovyClassLoader + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor + */ +public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware { + + private final String scriptSourceLocator; + + private final GroovyObjectCustomizer groovyObjectCustomizer; + + private GroovyClassLoader groovyClassLoader; + + private Class scriptClass; + + private Class scriptResultClass; + + private CachedResultHolder cachedResult; + + private final Object scriptClassMonitor = new Object(); + + private boolean wasModifiedForTypeCheck = false; + + + /** + * Create a new GroovyScriptFactory for the given script source. + *

We don't need to specify script interfaces here, since + * a Groovy script defines its Java interfaces itself. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public GroovyScriptFactory(String scriptSourceLocator) { + this(scriptSourceLocator, null); + } + + /** + * Create a new GroovyScriptFactory for the given script source, + * specifying a strategy interface that can create a custom MetaClass + * to supply missing methods and otherwise change the behavior of the object. + *

We don't need to specify script interfaces here, since + * a Groovy script defines its Java interfaces itself. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param groovyObjectCustomizer a customizer that can set a custom metaclass + * or make other changes to the GroovyObject created by this factory + * (may be null) + */ + public GroovyScriptFactory(String scriptSourceLocator, GroovyObjectCustomizer groovyObjectCustomizer) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + this.groovyObjectCustomizer = groovyObjectCustomizer; + } + + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableListableBeanFactory) { + ((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class); + } + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.groovyClassLoader = new GroovyClassLoader(classLoader); + } + + /** + * Return the GroovyClassLoader used by this script factory. + */ + public GroovyClassLoader getGroovyClassLoader() { + synchronized (this.scriptClassMonitor) { + if (this.groovyClassLoader == null) { + this.groovyClassLoader = new GroovyClassLoader(ClassUtils.getDefaultClassLoader()); + } + return this.groovyClassLoader; + } + } + + + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + /** + * Groovy scripts determine their interfaces themselves, + * hence we don't need to explicitly expose interfaces here. + * @return null always + */ + public Class[] getScriptInterfaces() { + return null; + } + + /** + * Groovy scripts do not need a config interface, + * since they expose their setters as public methods. + */ + public boolean requiresConfigInterface() { + return false; + } + + + /** + * Loads and parses the Groovy script via the GroovyClassLoader. + * @see groovy.lang.GroovyClassLoader + */ + public Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces) + throws IOException, ScriptCompilationException { + + try { + Class scriptClassToExecute = null; + + synchronized (this.scriptClassMonitor) { + this.wasModifiedForTypeCheck = false; + + if (this.cachedResult != null) { + Object result = this.cachedResult.object; + this.cachedResult = null; + return result; + } + + if (this.scriptClass == null || scriptSource.isModified()) { + // New script content... + this.scriptClass = getGroovyClassLoader().parseClass( + scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); + + if (Script.class.isAssignableFrom(this.scriptClass)) { + // A Groovy script, probably creating an instance: let's execute it. + Object result = executeScript(scriptSource, this.scriptClass); + this.scriptResultClass = (result != null ? result.getClass() : null); + return result; + } + else { + this.scriptResultClass = this.scriptClass; + } + } + scriptClassToExecute = this.scriptClass; + } + + // Process re-execution outside of the synchronized block. + return executeScript(scriptSource, scriptClassToExecute); + } + catch (CompilationFailedException ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + try { + synchronized (this.scriptClassMonitor) { + if (this.scriptClass == null || scriptSource.isModified()) { + // New script content... + this.wasModifiedForTypeCheck = true; + this.scriptClass = getGroovyClassLoader().parseClass( + scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); + + if (Script.class.isAssignableFrom(this.scriptClass)) { + // A Groovy script, probably creating an instance: let's execute it. + Object result = executeScript(scriptSource, this.scriptClass); + this.scriptResultClass = (result != null ? result.getClass() : null); + this.cachedResult = new CachedResultHolder(result); + } + else { + this.scriptResultClass = this.scriptClass; + } + } + return this.scriptResultClass; + } + } + catch (CompilationFailedException ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + synchronized (this.scriptClassMonitor) { + return (scriptSource.isModified() || this.wasModifiedForTypeCheck); + } + } + + + /** + * Instantiate the given Groovy script class and run it if necessary. + * @param scriptSource the source for the underlying script + * @param scriptClass the Groovy script class + * @return the result object (either an instance of the script class + * or the result of running the script instance) + * @throws ScriptCompilationException in case of instantiation failure + */ + protected Object executeScript(ScriptSource scriptSource, Class scriptClass) throws ScriptCompilationException { + try { + GroovyObject goo = (GroovyObject) scriptClass.newInstance(); + + if (this.groovyObjectCustomizer != null) { + // Allow metaclass and other customization. + this.groovyObjectCustomizer.customize(goo); + } + + if (goo instanceof Script) { + // A Groovy script, probably creating an instance: let's execute it. + return ((Script) goo).run(); + } + else { + // An instance of the scripted class: let's return it as-is. + return goo; + } + } + catch (InstantiationException ex) { + throw new ScriptCompilationException( + scriptSource, "Could not instantiate Groovy script class: " + scriptClass.getName(), ex); + } + catch (IllegalAccessException ex) { + throw new ScriptCompilationException( + scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex); + } + } + + + public String toString() { + return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + + + /** + * Wrapper that holds a temporarily cached result object. + */ + private static class CachedResultHolder { + + public final Object object; + + public CachedResultHolder(Object object) { + this.object = object; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/groovy/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/package.html new file mode 100644 index 00000000000..1f3225b300e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/groovy/package.html @@ -0,0 +1,9 @@ + + + +Package providing integration of +Groovy +into Spring's scripting infrastructure. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptFactory.java b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptFactory.java new file mode 100644 index 00000000000..9591ff29a20 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptFactory.java @@ -0,0 +1,126 @@ +/* + * 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.scripting.jruby; + +import java.io.IOException; + +import org.jruby.RubyException; +import org.jruby.exceptions.JumpException; +import org.jruby.exceptions.RaiseException; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation + * for a JRuby script. + * + *

Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc for a configuration example. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see JRubyScriptUtils + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor + */ +public class JRubyScriptFactory implements ScriptFactory, BeanClassLoaderAware { + + private final String scriptSourceLocator; + + private final Class[] scriptInterfaces; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + /** + * Create a new JRubyScriptFactory for the given script source. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param scriptInterfaces the Java interfaces that the scripted object + * is supposed to implement + */ + public JRubyScriptFactory(String scriptSourceLocator, Class[] scriptInterfaces) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + Assert.notEmpty(scriptInterfaces, "'scriptInterfaces' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + this.scriptInterfaces = scriptInterfaces; + } + + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + public Class[] getScriptInterfaces() { + return this.scriptInterfaces; + } + + /** + * JRuby scripts do require a config interface. + */ + public boolean requiresConfigInterface() { + return true; + } + + /** + * Load and parse the JRuby script via JRubyScriptUtils. + * @see JRubyScriptUtils#createJRubyObject(String, Class[], ClassLoader) + */ + public Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces) + throws IOException, ScriptCompilationException { + try { + return JRubyScriptUtils.createJRubyObject( + scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); + } + catch (RaiseException ex) { + RubyException rubyEx = ex.getException(); + String msg = (rubyEx != null && rubyEx.message != null) ? + rubyEx.message.toString() : "Unexpected JRuby error"; + throw new ScriptCompilationException(scriptSource, msg, ex); + } + catch (JumpException ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + return null; + } + + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + return scriptSource.isModified(); + } + + + public String toString() { + return "JRubyScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptUtils.java b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptUtils.java new file mode 100644 index 00000000000..1cce091a749 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/JRubyScriptUtils.java @@ -0,0 +1,262 @@ +/* + * 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.scripting.jruby; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.List; + +import org.jruby.Ruby; +import org.jruby.RubyArray; +import org.jruby.RubyException; +import org.jruby.RubyNil; +import org.jruby.ast.ClassNode; +import org.jruby.ast.Colon2Node; +import org.jruby.ast.NewlineNode; +import org.jruby.ast.Node; +import org.jruby.exceptions.JumpException; +import org.jruby.exceptions.RaiseException; +import org.jruby.javasupport.JavaEmbedUtils; +import org.jruby.runtime.DynamicScope; +import org.jruby.runtime.builtin.IRubyObject; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods for handling JRuby-scripted objects. + * + *

As of Spring 2.5, this class supports JRuby 0.9.9, 0.9.9 and 1.0.x. + * Note that there is no support for JRuby 1.1 at this point! + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + */ +public abstract class JRubyScriptUtils { + + // Determine whether the old JRuby 0.9 parse method is available (incompatible with 1.0) + private final static Method oldParseMethod = ClassUtils.getMethodIfAvailable( + Ruby.class, "parse", new Class[] {String.class, String.class, DynamicScope.class}); + + + /** + * Create a new JRuby-scripted object from the given script source, + * using the default {@link ClassLoader}. + * @param scriptSource the script source text + * @param interfaces the interfaces that the scripted Java object is to implement + * @return the scripted Java object + * @throws JumpException in case of JRuby parsing failure + * @see ClassUtils#getDefaultClassLoader() + */ + public static Object createJRubyObject(String scriptSource, Class[] interfaces) throws JumpException { + return createJRubyObject(scriptSource, interfaces, ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new JRuby-scripted object from the given script source. + * @param scriptSource the script source text + * @param interfaces the interfaces that the scripted Java object is to implement + * @param classLoader the {@link ClassLoader} to create the script proxy with + * @return the scripted Java object + * @throws JumpException in case of JRuby parsing failure + */ + public static Object createJRubyObject(String scriptSource, Class[] interfaces, ClassLoader classLoader) { + Ruby ruby = initializeRuntime(); + + Node scriptRootNode = (oldParseMethod != null ? + (Node) ReflectionUtils.invokeMethod(oldParseMethod, ruby, new Object[] {scriptSource, "", null}) : + ruby.parse(scriptSource, "", null, 0)); + IRubyObject rubyObject = ruby.eval(scriptRootNode); + + if (rubyObject instanceof RubyNil) { + String className = findClassName(scriptRootNode); + rubyObject = ruby.evalScript("\n" + className + ".new"); + } + // still null? + if (rubyObject instanceof RubyNil) { + throw new IllegalStateException("Compilation of JRuby script returned RubyNil: " + rubyObject); + } + + return Proxy.newProxyInstance(classLoader, interfaces, new RubyObjectInvocationHandler(rubyObject, ruby)); + } + + /** + * Initializes an instance of the {@link org.jruby.Ruby} runtime. + */ + private static Ruby initializeRuntime() { + return JavaEmbedUtils.initialize(Collections.EMPTY_LIST); + } + + /** + * Given the root {@link Node} in a JRuby AST will locate the name of the + * class defined by that AST. + * @throws IllegalArgumentException if no class is defined by the supplied AST + */ + private static String findClassName(Node rootNode) { + ClassNode classNode = findClassNode(rootNode); + if (classNode == null) { + throw new IllegalArgumentException("Unable to determine class name for root node '" + rootNode + "'"); + } + Colon2Node node = (Colon2Node) classNode.getCPath(); + return node.getName(); + } + + /** + * Find the first {@link ClassNode} under the supplied {@link Node}. + * @return the found ClassNode, or null + * if no {@link ClassNode} is found + */ + private static ClassNode findClassNode(Node node) { + if (node instanceof ClassNode) { + return (ClassNode) node; + } + List children = node.childNodes(); + for (int i = 0; i < children.size(); i++) { + Node child = (Node) children.get(i); + if (child instanceof ClassNode) { + return (ClassNode) child; + } else if (child instanceof NewlineNode) { + NewlineNode nn = (NewlineNode) child; + Node found = findClassNode(nn.getNextNode()); + if (found instanceof ClassNode) { + return (ClassNode) found; + } + } + } + for (int i = 0; i < children.size(); i++) { + Node child = (Node) children.get(i); + Node found = findClassNode(child); + if (found instanceof ClassNode) { + return (ClassNode) found; + } + } + return null; + } + + + /** + * InvocationHandler that invokes a JRuby script method. + */ + private static class RubyObjectInvocationHandler implements InvocationHandler { + + private final IRubyObject rubyObject; + + private final Ruby ruby; + + public RubyObjectInvocationHandler(IRubyObject rubyObject, Ruby ruby) { + this.rubyObject = rubyObject; + this.ruby = ruby; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (ReflectionUtils.isEqualsMethod(method)) { + return (isProxyForSameRubyObject(args[0]) ? Boolean.TRUE : Boolean.FALSE); + } + else if (ReflectionUtils.isHashCodeMethod(method)) { + return new Integer(this.rubyObject.hashCode()); + } + else if (ReflectionUtils.isToStringMethod(method)) { + String toStringResult = this.rubyObject.toString(); + if (!StringUtils.hasText(toStringResult)) { + toStringResult = ObjectUtils.identityToString(this.rubyObject); + } + return "JRuby object [" + toStringResult + "]"; + } + try { + IRubyObject[] rubyArgs = convertToRuby(args); + IRubyObject rubyResult = + this.rubyObject.callMethod(this.ruby.getCurrentContext(), method.getName(), rubyArgs); + return convertFromRuby(rubyResult, method.getReturnType()); + } + catch (RaiseException ex) { + throw new JRubyExecutionException(ex); + } + } + + private boolean isProxyForSameRubyObject(Object other) { + if (!Proxy.isProxyClass(other.getClass())) { + return false; + } + InvocationHandler ih = Proxy.getInvocationHandler(other); + return (ih instanceof RubyObjectInvocationHandler && + this.rubyObject.equals(((RubyObjectInvocationHandler) ih).rubyObject)); + } + + private IRubyObject[] convertToRuby(Object[] javaArgs) { + if (javaArgs == null || javaArgs.length == 0) { + return new IRubyObject[0]; + } + IRubyObject[] rubyArgs = new IRubyObject[javaArgs.length]; + for (int i = 0; i < javaArgs.length; ++i) { + rubyArgs[i] = JavaEmbedUtils.javaToRuby(this.ruby, javaArgs[i]); + } + return rubyArgs; + } + + private Object convertFromRuby(IRubyObject rubyResult, Class returnType) { + Object result = JavaEmbedUtils.rubyToJava(this.ruby, rubyResult, returnType); + if (result instanceof RubyArray && returnType.isArray()) { + result = convertFromRubyArray(((RubyArray) result).toJavaArray(), returnType); + } + return result; + } + + private Object convertFromRubyArray(IRubyObject[] rubyArray, Class returnType) { + Class targetType = returnType.getComponentType(); + Object javaArray = Array.newInstance(targetType, rubyArray.length); + for (int i = 0; i < rubyArray.length; i++) { + IRubyObject rubyObject = rubyArray[i]; + Array.set(javaArray, i, convertFromRuby(rubyObject, targetType)); + } + return javaArray; + } + } + + + /** + * Exception thrown in response to a JRuby {@link RaiseException} + * being thrown from a JRuby method invocation. + *

Introduced because the RaiseException class does not + * have useful {@link Object#toString()}, {@link Throwable#getMessage()}, + * and {@link Throwable#printStackTrace} implementations. + */ + public static class JRubyExecutionException extends NestedRuntimeException { + + /** + * Create a new JRubyException, + * wrapping the given JRuby RaiseException. + * @param ex the cause (must not be null) + */ + public JRubyExecutionException(RaiseException ex) { + super(buildMessage(ex), ex); + } + + private static String buildMessage(RaiseException ex) { + RubyException rubyEx = ex.getException(); + return (rubyEx != null && rubyEx.message != null) ? rubyEx.message.toString() : "Unexpected JRuby error"; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/jruby/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/package.html new file mode 100644 index 00000000000..694e278011a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/jruby/package.html @@ -0,0 +1,9 @@ + + + +Package providing integration of +JRuby +into Spring's scripting infrastructure. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/package.html new file mode 100644 index 00000000000..cf57311634e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/package.html @@ -0,0 +1,7 @@ + + + +Core interfaces for Spring's scripting support. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java new file mode 100644 index 00000000000..6444660ce6c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java @@ -0,0 +1,81 @@ +/* + * 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.scripting.support; + +import org.springframework.aop.target.dynamic.BeanFactoryRefreshableTargetSource; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; + +/** + * Subclass of {@link BeanFactoryRefreshableTargetSource} that determines whether + * a refresh is required through the given {@link ScriptFactory}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +public class RefreshableScriptTargetSource extends BeanFactoryRefreshableTargetSource { + + private final ScriptFactory scriptFactory; + + private final ScriptSource scriptSource; + + private final boolean isFactoryBean; + + + /** + * Create a new RefreshableScriptTargetSource. + * @param beanFactory the BeanFactory to fetch the scripted bean from + * @param beanName the name of the target bean + * @param scriptFactory the ScriptFactory to delegate to for determining + * whether a refresh is required + * @param scriptSource the ScriptSource for the script definition + * @param isFactoryBean whether the target script defines a FactoryBean + */ + public RefreshableScriptTargetSource(BeanFactory beanFactory, String beanName, + ScriptFactory scriptFactory, ScriptSource scriptSource, boolean isFactoryBean) { + + super(beanFactory, beanName); + Assert.notNull(scriptFactory, "ScriptFactory must not be null"); + Assert.notNull(scriptSource, "ScriptSource must not be null"); + this.scriptFactory = scriptFactory; + this.scriptSource = scriptSource; + this.isFactoryBean = isFactoryBean; + } + + + /** + * Determine whether a refresh is required through calling + * ScriptFactory's requiresScriptedObjectRefresh method. + * @see ScriptFactory#requiresScriptedObjectRefresh(ScriptSource) + */ + protected boolean requiresRefresh() { + return this.scriptFactory.requiresScriptedObjectRefresh(this.scriptSource); + } + + /** + * Obtain a fresh target object, retrieving a FactoryBean if necessary. + */ + protected Object obtainFreshBean(BeanFactory beanFactory, String beanName) { + return super.obtainFreshBean(beanFactory, + (this.isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName)); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java new file mode 100644 index 00000000000..faf56c4b88f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java @@ -0,0 +1,131 @@ +/* + * 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.scripting.support; + +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.scripting.ScriptSource} implementation + * based on Spring's {@link org.springframework.core.io.Resource} + * abstraction. Loads the script text from the underlying Resource's + * {@link org.springframework.core.io.Resource#getFile() File} or + * {@link org.springframework.core.io.Resource#getInputStream() InputStream}, + * and tracks the last-modified timestamp of the file (if possible). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.io.Resource#getInputStream() + * @see org.springframework.core.io.Resource#getFile() + * @see org.springframework.core.io.ResourceLoader + */ +public class ResourceScriptSource implements ScriptSource { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private final Resource resource; + + private long lastModified = -1; + + private final Object lastModifiedMonitor = new Object(); + + + /** + * Create a new ResourceScriptSource for the given resource. + * @param resource the Resource to load the script from + */ + public ResourceScriptSource(Resource resource) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + } + + /** + * Return the {@link org.springframework.core.io.Resource} to load the + * script from. + */ + public final Resource getResource() { + return this.resource; + } + + + public String getScriptAsString() throws IOException { + synchronized (this.lastModifiedMonitor) { + this.lastModified = retrieveLastModifiedTime(); + } + Reader reader = null; + try { + // Try to get a FileReader first: generally more reliable. + reader = new FileReader(getResource().getFile()); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not open FileReader for " + this.resource + + " - falling back to InputStreamReader", ex); + } + } + if (reader == null) { + reader = new InputStreamReader(this.resource.getInputStream()); + } + return FileCopyUtils.copyToString(reader); + } + + public boolean isModified() { + synchronized (this.lastModifiedMonitor) { + return (this.lastModified < 0 || retrieveLastModifiedTime() > this.lastModified); + } + } + + /** + * Retrieve the current last-modified timestamp of the underlying resource. + * @return the current timestamp, or 0 if not determinable + */ + protected long retrieveLastModifiedTime() { + try { + return getResource().lastModified(); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug(getResource() + " could not be resolved in the file system - " + + "current timestamp not available for script modification check", ex); + } + return 0; + } + } + + public String suggestedClassName() { + return StringUtils.stripFilenameExtension(getResource().getFilename()); + } + + + public String toString() { + return this.resource.toString(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java new file mode 100644 index 00000000000..9f5692aa26d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -0,0 +1,547 @@ +/* + * 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.scripting.support; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import net.sf.cglib.asm.Type; +import net.sf.cglib.core.Signature; +import net.sf.cglib.proxy.InterfaceMaker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} that + * handles {@link org.springframework.scripting.ScriptFactory} definitions, + * replacing each factory with the actual scripted Java object generated by it. + * + *

This is similar to the + * {@link org.springframework.beans.factory.FactoryBean} mechanism, but is + * specifically tailored for scripts and not built into Spring's core + * container itself but rather implemented as an extension. + * + *

NOTE: The most important characteristic of this post-processor + * is that constructor arguments are applied to the + * {@link org.springframework.scripting.ScriptFactory} instance + * while bean property values are applied to the generated scripted object. + * Typically, constructor arguments include a script source locator and + * potentially script interfaces, while bean property values include + * references and config values to inject into the scripted object itself. + * + *

The following {@link ScriptFactoryPostProcessor} will automatically + * be applied to the two + * {@link org.springframework.scripting.ScriptFactory} definitions below. + * At runtime, the actual scripted objects will be exposed for + * "bshMessenger" and "groovyMessenger", rather than the + * {@link org.springframework.scripting.ScriptFactory} instances. Both of + * those are supposed to be castable to the example's Messenger + * interfaces here. + * + *

<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>
+ *
+ * <bean id="bshMessenger" class="org.springframework.scripting.bsh.BshScriptFactory">
+ *   <constructor-arg value="classpath:mypackage/Messenger.bsh"/>
+ *   <constructor-arg value="mypackage.Messenger"/>
+ *   <property name="message" value="Hello World!"/>
+ * </bean>
+ *
+ * <bean id="groovyMessenger" class="org.springframework.scripting.bsh.GroovyScriptFactory">
+ *   <constructor-arg value="classpath:mypackage/Messenger.groovy"/>
+ *   <property name="message" value="Hello World!"/>
+ * </bean>
+ * + *

NOTE: Please note that the above excerpt from a Spring + * XML bean definition file uses just the <bean/>-style syntax + * (in an effort to illustrate using the {@link ScriptFactoryPostProcessor} itself). + * In reality, you would never create a <bean/> definition for a + * {@link ScriptFactoryPostProcessor} explicitly; rather you would import the + * tags from the 'lang' namespace and simply create scripted + * beans using the tags in that namespace... as part of doing so, a + * {@link ScriptFactoryPostProcessor} will implicitly be created for you. + * + *

The Spring reference documentation contains numerous examples of using + * tags in the 'lang' namespace; by way of an example, find below + * a Groovy-backed bean defined using the 'lang:groovy' tag. + * + *

+ * <?xml version="1.0" encoding="UTF-8"?>
+ * <beans xmlns="http://www.springframework.org/schema/beans"
+ *     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *     xmlns:lang="http://www.springframework.org/schema/lang">
+ *
+ *   <!-- this is the bean definition for the Groovy-backed Messenger implementation -->
+ *   <lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
+ *     <lang:property name="message" value="I Can Do The Frug" />
+ *   </lang:groovy>
+ *
+ *   <!-- an otherwise normal bean that will be injected by the Groovy-backed Messenger -->
+ *   <bean id="bookingService" class="x.y.DefaultBookingService">
+ *     <property name="messenger" ref="messenger" />
+ *   </bean>
+ *
+ * </beans>
+ * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rick Evans + * @author Mark Fisher + * @since 2.0 + */ +public class ScriptFactoryPostProcessor extends InstantiationAwareBeanPostProcessorAdapter + implements BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware, DisposableBean, Ordered { + + /** + * The {@link org.springframework.core.io.Resource}-style prefix that denotes + * an inline script. + *

An inline script is a script that is defined right there in the (typically XML) + * configuration, as opposed to being defined in an external file. + */ + public static final String INLINE_SCRIPT_PREFIX = "inline:"; + + public static final String REFRESH_CHECK_DELAY_ATTRIBUTE = + Conventions.getQualifiedAttributeName(ScriptFactoryPostProcessor.class, "refreshCheckDelay"); + + private static final String SCRIPT_FACTORY_NAME_PREFIX = "scriptFactory."; + + private static final String SCRIPTED_OBJECT_NAME_PREFIX = "scriptedObject."; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private long defaultRefreshCheckDelay = -1; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private ConfigurableBeanFactory beanFactory; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + final DefaultListableBeanFactory scriptBeanFactory = new DefaultListableBeanFactory(); + + /** Map from bean name String to ScriptSource object */ + private final Map scriptSourceCache = new HashMap(); + + + /** + * Set the delay between refresh checks, in milliseconds. + * Default is -1, indicating no refresh checks at all. + *

Note that an actual refresh will only happen when + * the {@link org.springframework.scripting.ScriptSource} indicates + * that it has been modified. + * @see org.springframework.scripting.ScriptSource#isModified() + */ + public void setDefaultRefreshCheckDelay(long defaultRefreshCheckDelay) { + this.defaultRefreshCheckDelay = defaultRefreshCheckDelay; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableBeanFactory)) { + throw new IllegalStateException("ScriptFactoryPostProcessor doesn't work with a BeanFactory " + + "which does not implement ConfigurableBeanFactory: " + beanFactory.getClass()); + } + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + + // Required so that references (up container hierarchies) are correctly resolved. + this.scriptBeanFactory.setParentBeanFactory(this.beanFactory); + + // Required so that all BeanPostProcessors, Scopes, etc become available. + this.scriptBeanFactory.copyConfigurationFrom(this.beanFactory); + + // Filter out BeanPostProcessors that are part of the AOP infrastructure, + // since those are only meant to apply to beans defined in the original factory. + for (Iterator it = this.scriptBeanFactory.getBeanPostProcessors().iterator(); it.hasNext();) { + BeanPostProcessor postProcessor = (BeanPostProcessor) it.next(); + if (postProcessor instanceof AopInfrastructureBean) { + it.remove(); + } + } + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public int getOrder() { + return Integer.MIN_VALUE; + } + + + public Class predictBeanType(Class beanClass, String beanName) { + // We only apply special treatment to ScriptFactory implementations here. + if (!ScriptFactory.class.isAssignableFrom(beanClass)) { + return null; + } + + BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName); + + try { + String scriptFactoryBeanName = SCRIPT_FACTORY_NAME_PREFIX + beanName; + String scriptedObjectBeanName = SCRIPTED_OBJECT_NAME_PREFIX + beanName; + prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName); + + ScriptFactory scriptFactory = + (ScriptFactory) this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = + getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + Class[] interfaces = scriptFactory.getScriptInterfaces(); + + Class scriptedType = scriptFactory.getScriptedObjectType(scriptSource); + if (scriptedType != null) { + return scriptedType; + } + else if (!ObjectUtils.isEmpty(interfaces)) { + return (interfaces.length == 1 ? interfaces[0] : createCompositeInterface(interfaces)); + } + else { + if (bd.isSingleton()) { + Object bean = this.scriptBeanFactory.getBean(scriptedObjectBeanName); + if (bean != null) { + return bean.getClass(); + } + } + } + } + catch (Exception ex) { + if (ex instanceof BeanCreationException && + ((BeanCreationException) ex).getMostSpecificCause() instanceof BeanCurrentlyInCreationException) { + if (logger.isTraceEnabled()) { + logger.trace("Could not determine scripted object type for bean '" + beanName + "': " + ex.getMessage()); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Could not determine scripted object type for bean '" + beanName + "'", ex); + } + } + } + + return null; + } + + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + // We only apply special treatment to ScriptFactory implementations here. + if (!ScriptFactory.class.isAssignableFrom(beanClass)) { + return null; + } + + BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName); + String scriptFactoryBeanName = SCRIPT_FACTORY_NAME_PREFIX + beanName; + String scriptedObjectBeanName = SCRIPTED_OBJECT_NAME_PREFIX + beanName; + prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName); + + ScriptFactory scriptFactory = + (ScriptFactory) this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = + getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + boolean isFactoryBean = false; + try { + Class scriptedObjectType = scriptFactory.getScriptedObjectType(scriptSource); + // Returned type may be null if the factory is unable to determine the type. + if (scriptedObjectType != null) { + isFactoryBean = FactoryBean.class.isAssignableFrom(scriptedObjectType); + } + } + catch (Exception ex) { + throw new BeanCreationException( + beanName, "Could not determine scripted object type for " + scriptFactory, ex); + } + + long refreshCheckDelay = resolveRefreshCheckDelay(bd); + if (refreshCheckDelay >= 0) { + Class[] interfaces = scriptFactory.getScriptInterfaces(); + RefreshableScriptTargetSource ts = new RefreshableScriptTargetSource( + this.scriptBeanFactory, scriptedObjectBeanName, scriptFactory, scriptSource, isFactoryBean); + ts.setRefreshCheckDelay(refreshCheckDelay); + return createRefreshableProxy(ts, interfaces); + } + + if (isFactoryBean) { + scriptedObjectBeanName = BeanFactory.FACTORY_BEAN_PREFIX + scriptedObjectBeanName; + } + return this.scriptBeanFactory.getBean(scriptedObjectBeanName); + } + + + /** + * Prepare the script beans in the internal BeanFactory that this + * post-processor uses. Each original bean definition will be split + * into a ScriptFactory definition and a scripted object definition. + * @param bd the original bean definition in the main BeanFactory + * @param scriptFactoryBeanName the name of the internal ScriptFactory bean + * @param scriptedObjectBeanName the name of the internal scripted object bean + */ + protected void prepareScriptBeans( + BeanDefinition bd, String scriptFactoryBeanName, String scriptedObjectBeanName) { + + // Avoid recreation of the script bean definition in case of a prototype. + synchronized (this.scriptBeanFactory) { + if (!this.scriptBeanFactory.containsBeanDefinition(scriptedObjectBeanName)) { + + this.scriptBeanFactory.registerBeanDefinition( + scriptFactoryBeanName, createScriptFactoryBeanDefinition(bd)); + ScriptFactory scriptFactory = + (ScriptFactory) this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = + getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + Class[] interfaces = scriptFactory.getScriptInterfaces(); + + Class[] scriptedInterfaces = interfaces; + if (scriptFactory.requiresConfigInterface() && !bd.getPropertyValues().isEmpty()) { + Class configInterface = createConfigInterface(bd, interfaces); + scriptedInterfaces = (Class[]) ObjectUtils.addObjectToArray(interfaces, configInterface); + } + + BeanDefinition objectBd = createScriptedObjectBeanDefinition( + bd, scriptFactoryBeanName, scriptSource, scriptedInterfaces); + long refreshCheckDelay = resolveRefreshCheckDelay(bd); + if (refreshCheckDelay >= 0) { + objectBd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + } + + this.scriptBeanFactory.registerBeanDefinition(scriptedObjectBeanName, objectBd); + } + } + } + + /** + * Get the refresh check delay for the given {@link ScriptFactory} {@link BeanDefinition}. + * If the {@link BeanDefinition} has a + * {@link org.springframework.core.AttributeAccessor metadata attribute} + * under the key {@link #REFRESH_CHECK_DELAY_ATTRIBUTE} which is a valid {@link Number} + * type, then this value is used. Otherwise, the the {@link #defaultRefreshCheckDelay} + * value is used. + * @param beanDefinition the BeanDefinition to check + * @return the refresh check delay + */ + protected long resolveRefreshCheckDelay(BeanDefinition beanDefinition) { + long refreshCheckDelay = this.defaultRefreshCheckDelay; + Object attributeValue = beanDefinition.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (attributeValue instanceof Number) { + refreshCheckDelay = ((Number) attributeValue).longValue(); + } + else if (attributeValue instanceof String) { + refreshCheckDelay = Long.parseLong((String) attributeValue); + } + else if (attributeValue != null) { + throw new BeanDefinitionStoreException( + "Invalid refresh check delay attribute [" + REFRESH_CHECK_DELAY_ATTRIBUTE + + "] with value [" + attributeValue + "]: needs to be of type Number or String"); + } + return refreshCheckDelay; + } + + /** + * Create a ScriptFactory bean definition based on the given script definition, + * extracting only the definition data that is relevant for the ScriptFactory + * (that is, only bean class and constructor arguments). + * @param bd the full script bean definition + * @return the extracted ScriptFactory bean definition + * @see org.springframework.scripting.ScriptFactory + */ + protected BeanDefinition createScriptFactoryBeanDefinition(BeanDefinition bd) { + GenericBeanDefinition scriptBd = new GenericBeanDefinition(); + scriptBd.setBeanClassName(bd.getBeanClassName()); + scriptBd.getConstructorArgumentValues().addArgumentValues(bd.getConstructorArgumentValues()); + return scriptBd; + } + + /** + * Obtain a ScriptSource for the given bean, lazily creating it + * if not cached already. + * @param beanName the name of the scripted bean + * @param scriptSourceLocator the script source locator associated with the bean + * @return the corresponding ScriptSource instance + * @see #convertToScriptSource + */ + protected ScriptSource getScriptSource(String beanName, String scriptSourceLocator) { + synchronized (this.scriptSourceCache) { + ScriptSource scriptSource = (ScriptSource) this.scriptSourceCache.get(beanName); + if (scriptSource == null) { + scriptSource = convertToScriptSource(beanName, scriptSourceLocator, this.resourceLoader); + this.scriptSourceCache.put(beanName, scriptSource); + } + return scriptSource; + } + } + + /** + * Convert the given script source locator to a ScriptSource instance. + *

By default, supported locators are Spring resource locations + * (such as "file:C:/myScript.bsh" or "classpath:myPackage/myScript.bsh") + * and inline scripts ("inline:myScriptText..."). + * @param beanName the name of the scripted bean + * @param scriptSourceLocator the script source locator + * @param resourceLoader the ResourceLoader to use (if necessary) + * @return the ScriptSource instance + */ + protected ScriptSource convertToScriptSource( + String beanName, String scriptSourceLocator, ResourceLoader resourceLoader) { + + if (scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX)) { + return new StaticScriptSource(scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()), beanName); + } + else { + return new ResourceScriptSource(resourceLoader.getResource(scriptSourceLocator)); + } + } + + /** + * Create a config interface for the given bean definition, defining setter + * methods for the defined property values as well as an init method and + * a destroy method (if defined). + *

This implementation creates the interface via CGLIB's InterfaceMaker, + * determining the property types from the given interfaces (as far as possible). + * @param bd the bean definition (property values etc) to create a + * config interface for + * @param interfaces the interfaces to check against (might define + * getters corresponding to the setters we're supposed to generate) + * @return the config interface + * @see net.sf.cglib.proxy.InterfaceMaker + * @see org.springframework.beans.BeanUtils#findPropertyType + */ + protected Class createConfigInterface(BeanDefinition bd, Class[] interfaces) { + InterfaceMaker maker = new InterfaceMaker(); + PropertyValue[] pvs = bd.getPropertyValues().getPropertyValues(); + for (int i = 0; i < pvs.length; i++) { + String propertyName = pvs[i].getName(); + Class propertyType = BeanUtils.findPropertyType(propertyName, interfaces); + String setterName = "set" + StringUtils.capitalize(propertyName); + Signature signature = new Signature(setterName, Type.VOID_TYPE, new Type[] {Type.getType(propertyType)}); + maker.add(signature, new Type[0]); + } + if (bd instanceof AbstractBeanDefinition) { + AbstractBeanDefinition abd = (AbstractBeanDefinition) bd; + if (abd.getInitMethodName() != null) { + Signature signature = new Signature(abd.getInitMethodName(), Type.VOID_TYPE, new Type[0]); + maker.add(signature, new Type[0]); + } + if (abd.getDestroyMethodName() != null) { + Signature signature = new Signature(abd.getDestroyMethodName(), Type.VOID_TYPE, new Type[0]); + maker.add(signature, new Type[0]); + } + } + return maker.create(); + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

The default implementation builds a JDK proxy class + * for the given interfaces. + * @param interfaces the interfaces to merge + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + protected Class createCompositeInterface(Class[] interfaces) { + return ClassUtils.createCompositeInterface(interfaces, this.beanClassLoader); + } + + /** + * Create a bean definition for the scripted object, based on the given script + * definition, extracting the definition data that is relevant for the scripted + * object (that is, everything but bean class and constructor arguments). + * @param bd the full script bean definition + * @param scriptFactoryBeanName the name of the internal ScriptFactory bean + * @param scriptSource the ScriptSource for the scripted bean + * @param interfaces the interfaces that the scripted bean is supposed to implement + * @return the extracted ScriptFactory bean definition + * @see org.springframework.scripting.ScriptFactory#getScriptedObject + */ + protected BeanDefinition createScriptedObjectBeanDefinition( + BeanDefinition bd, String scriptFactoryBeanName, ScriptSource scriptSource, Class[] interfaces) { + + GenericBeanDefinition objectBd = new GenericBeanDefinition(bd); + objectBd.setFactoryBeanName(scriptFactoryBeanName); + objectBd.setFactoryMethodName("getScriptedObject"); + objectBd.getConstructorArgumentValues().clear(); + objectBd.getConstructorArgumentValues().addIndexedArgumentValue(0, scriptSource); + objectBd.getConstructorArgumentValues().addIndexedArgumentValue(1, interfaces); + return objectBd; + } + + /** + * Create a refreshable proxy for the given AOP TargetSource. + * @param ts the refreshable TargetSource + * @param interfaces the proxy interfaces (may be null to + * indicate proxying of all interfaces implemented by the target class) + * @return the generated proxy + * @see RefreshableScriptTargetSource + */ + protected Object createRefreshableProxy(TargetSource ts, Class[] interfaces) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTargetSource(ts); + + if (interfaces == null) { + interfaces = ClassUtils.getAllInterfacesForClass(ts.getTargetClass(), this.beanClassLoader); + } + proxyFactory.setInterfaces(interfaces); + + DelegatingIntroductionInterceptor introduction = new DelegatingIntroductionInterceptor(ts); + introduction.suppressInterface(TargetSource.class); + proxyFactory.addAdvice(introduction); + + return proxyFactory.getProxy(this.beanClassLoader); + } + + + /** + * Destroy the inner bean factory (used for scripts) on shutdown. + */ + public void destroy() { + this.scriptBeanFactory.destroySingletons(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java new file mode 100644 index 00000000000..2ad89e8f5eb --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java @@ -0,0 +1,89 @@ +/* + * 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.scripting.support; + +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; + +/** + * Static implementation of the + * {@link org.springframework.scripting.ScriptSource} interface, + * encapsulating a given String that contains the script source text. + * Supports programmatic updates of the script String. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class StaticScriptSource implements ScriptSource { + + private String script; + + private boolean modified; + + private String className; + + + /** + * Create a new StaticScriptSource for the given script. + * @param script the script String + */ + public StaticScriptSource(String script) { + setScript(script); + } + + /** + * Create a new StaticScriptSource for the given script. + * @param script the script String + * @param className the suggested class name for the script + * (may be null) + */ + public StaticScriptSource(String script, String className) { + setScript(script); + this.className = className; + } + + /** + * Set a fresh script String, overriding the previous script. + * @param script the script String + */ + public synchronized void setScript(String script) { + Assert.hasText(script, "Script must not be empty"); + this.modified = !script.equals(this.script); + this.script = script; + } + + + public synchronized String getScriptAsString() { + this.modified = false; + return this.script; + } + + public synchronized boolean isModified() { + return this.modified; + } + + public String suggestedClassName() { + return this.className; + } + + + public String toString() { + return "static script" + (this.className != null ? " [" + this.className + "]" : ""); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/package.html b/org.springframework.context/src/main/java/org/springframework/scripting/support/package.html new file mode 100644 index 00000000000..78418a477f7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/package.html @@ -0,0 +1,9 @@ + + + +Support classes for Spring's scripting package. +Provides a ScriptFactoryPostProcessor for turning ScriptFactory +definitions into scripted objects. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/stereotype/Component.java b/org.springframework.context/src/main/java/org/springframework/stereotype/Component.java new file mode 100644 index 00000000000..8b3c75dffc5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/stereotype/Component.java @@ -0,0 +1,54 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an annotated class is a "component". + * Such classes are considered as candidates for auto-detection + * when using annotation-based configuration and classpath scanning. + * + *

Other class-level annotations may be considered as identifying + * a component as well, typically a special kind of component: + * e.g. the {@link Repository @Repository} annotation or AspectJ's + * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. + * + * @author Mark Fisher + * @since 2.5 + * @see Repository + * @see Service + * @see Controller + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Component { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + String value() default ""; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/stereotype/Controller.java b/org.springframework.context/src/main/java/org/springframework/stereotype/Controller.java new file mode 100644 index 00000000000..189776d444e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/stereotype/Controller.java @@ -0,0 +1,53 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an annotated class is a "Controller" (e.g. a web controller). + * + *

This annotation serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. + * It is typically used in combination with annotated handler methods based on the + * {@link org.springframework.web.bind.annotation.RequestMapping} annotation. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 2.5 + * @see Component + * @see org.springframework.web.bind.annotation.RequestMapping + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Controller { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + String value() default ""; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/stereotype/Repository.java b/org.springframework.context/src/main/java/org/springframework/stereotype/Repository.java new file mode 100644 index 00000000000..a31069b7346 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/stereotype/Repository.java @@ -0,0 +1,56 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an annotated class is a "Repository" (or "DAO"). + * + *

A class thus annotated is eligible for Spring + * {@link org.springframework.dao.DataAccessException} translation. The + * annotated class is also clarified as to its role in the overall + * application architecture for the purpose of tools, aspects, etc. + * + *

As of Spring 2.5, this annotation also serves as a specialization + * of {@link Component @Component}, allowing for implementation classes + * to be autodetected through classpath scanning. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see Component + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Repository { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + String value() default ""; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/stereotype/Service.java b/org.springframework.context/src/main/java/org/springframework/stereotype/Service.java new file mode 100644 index 00000000000..58c055961db --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/stereotype/Service.java @@ -0,0 +1,49 @@ +/* + * 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.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an annotated class is a "Service" (e.g. a business service facade). + * + *

This annotation serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. + * + * @author Juergen Hoeller + * @since 2.5 + * @see Component + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Service { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + String value() default ""; + +} diff --git a/org.springframework.context/src/main/java/org/springframework/stereotype/package.html b/org.springframework.context/src/main/java/org/springframework/stereotype/package.html new file mode 100644 index 00000000000..bc34c49cc34 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/stereotype/package.html @@ -0,0 +1,10 @@ + + + +Annotations denoting the roles of types or methods in the overall architecture +(at a conceptual, rather than implementation, level). + +

Intended for use by tools and aspects (making an ideal target for pointcuts). + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ui/ExtendedModelMap.java b/org.springframework.context/src/main/java/org/springframework/ui/ExtendedModelMap.java new file mode 100644 index 00000000000..dadb9bc5226 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/ExtendedModelMap.java @@ -0,0 +1,66 @@ +/* + * 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; + +import java.util.Collection; +import java.util.Map; + +/** + * Subclass of {@link ModelMap} that implements the {@link Model} interface. + * Java 5 specific like the Model interface itself. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +@SuppressWarnings("serial") +public class ExtendedModelMap extends ModelMap implements Model { + + @Override + public ExtendedModelMap addAttribute(String attributeName, Object attributeValue) { + super.addAttribute(attributeName, attributeValue); + return this; + } + + @Override + public ExtendedModelMap addAttribute(Object attributeValue) { + super.addAttribute(attributeValue); + return this; + } + + @Override + public ExtendedModelMap addAllAttributes(Collection attributeValues) { + super.addAllAttributes(attributeValues); + return this; + } + + @Override + public ExtendedModelMap addAllAttributes(Map attributes) { + super.addAllAttributes(attributes); + return this; + } + + @Override + public ExtendedModelMap mergeAttributes(Map attributes) { + super.mergeAttributes(attributes); + return this; + } + + @SuppressWarnings("unchecked") + public Map asMap() { + return this; + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/Model.java b/org.springframework.context/src/main/java/org/springframework/ui/Model.java new file mode 100644 index 00000000000..1490d655a85 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/Model.java @@ -0,0 +1,82 @@ +/* + * 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.ui; + +import java.util.Collection; +import java.util.Map; + +/** + * Java-5-specific interface that defines a holder for model attributes. + * Primarily designed for adding attributes to the model. + * Allows for accessing the overall model as a java.util.Map. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public interface Model { + + /** + * Add the supplied attribute under the supplied name. + * @param attributeName the name of the model attribute (never null) + * @param attributeValue the model attribute value (can be null) + */ + Model addAttribute(String attributeName, Object attributeValue); + + /** + * Add the supplied attribute to this Map using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + *

Note: Empty {@link java.util.Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for null rather + * than for empty collections as is already done by JSTL tags. + * @param attributeValue the model attribute value (never null) + */ + Model addAttribute(Object attributeValue); + + /** + * Copy all attributes in the supplied Collection into this + * Map, using attribute name generation for each element. + * @see #addAttribute(Object) + */ + Model addAllAttributes(Collection attributeValues); + + /** + * Copy all attributes in the supplied Map into this Map. + * @see #addAttribute(String, Object) + */ + Model addAllAttributes(Map attributes); + + /** + * Copy all attributes in the supplied Map into this Map, + * with existing objects of the same name taking precedence (i.e. not getting + * replaced). + */ + Model mergeAttributes(Map attributes); + + /** + * Does this model contain an attribute of the given name? + * @param attributeName the name of the model attribute (never null) + * @return whether this model contains a corresponding attribute + */ + boolean containsAttribute(String attributeName); + + /** + * Return the current set of model attributes as a Map. + */ + Map asMap(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/ModelMap.java b/org.springframework.context/src/main/java/org/springframework/ui/ModelMap.java new file mode 100644 index 00000000000..d320cc2ea79 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/ModelMap.java @@ -0,0 +1,178 @@ +/* + * 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.ui; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.Conventions; +import org.springframework.util.Assert; + +/** + * Implementation of {@link java.util.Map} for use when building model data for use + * with UI tools. Supports chained calls and generation of model attribute names. + * + *

This class serves as generic model holder for both Servlet and Portlet MVC, + * but is not tied to either of those. Check out the {@link Model} interface for + * a Java-5-based interface variant that serves the same purpose. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see Conventions#getVariableName + * @see org.springframework.web.servlet.ModelAndView + * @see org.springframework.web.portlet.ModelAndView + */ +public class ModelMap extends LinkedHashMap { + + /** + * Construct a new, empty ModelMap. + */ + public ModelMap() { + } + + /** + * Construct a new ModelMap containing the supplied attribute + * under the supplied name. + * @see #addAttribute(String, Object) + */ + public ModelMap(String attributeName, Object attributeValue) { + addAttribute(attributeName, attributeValue); + } + + /** + * Construct a new ModelMap containing the supplied attribute. + * Uses attribute name generation to generate the key for the supplied model + * object. + * @see #addAttribute(Object) + */ + public ModelMap(Object attributeValue) { + addAttribute(attributeValue); + } + + + /** + * Add the supplied attribute under the supplied name. + * @param attributeName the name of the model attribute (never null) + * @param attributeValue the model attribute value (can be null) + */ + public ModelMap addAttribute(String attributeName, Object attributeValue) { + Assert.notNull(attributeName, "Model attribute name must not be null"); + put(attributeName, attributeValue); + return this; + } + + /** + * Add the supplied attribute to this Map using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + *

Note: Empty {@link Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for null rather + * than for empty collections as is already done by JSTL tags. + * @param attributeValue the model attribute value (never null) + */ + public ModelMap addAttribute(Object attributeValue) { + Assert.notNull(attributeValue, "Model object must not be null"); + if (attributeValue instanceof Collection && ((Collection) attributeValue).isEmpty()) { + return this; + } + return addAttribute(Conventions.getVariableName(attributeValue), attributeValue); + } + + /** + * Copy all attributes in the supplied Collection into this + * Map, using attribute name generation for each element. + * @see #addAttribute(Object) + */ + public ModelMap addAllAttributes(Collection attributeValues) { + if (attributeValues != null) { + for (Iterator it = attributeValues.iterator(); it.hasNext();) { + addAttribute(it.next()); + } + } + return this; + } + + /** + * Copy all attributes in the supplied Map into this Map. + * @see #addAttribute(String, Object) + */ + public ModelMap addAllAttributes(Map attributes) { + if (attributes != null) { + putAll(attributes); + } + return this; + } + + /** + * Copy all attributes in the supplied Map into this Map, + * with existing objects of the same name taking precedence (i.e. not getting + * replaced). + */ + public ModelMap mergeAttributes(Map attributes) { + if (attributes != null) { + for (Iterator it = attributes.keySet().iterator(); it.hasNext();) { + Object key = it.next(); + if (!containsKey(key)) { + put(key, attributes.get(key)); + } + } + } + return this; + } + + /** + * Does this model contain an attribute of the given name? + * @param attributeName the name of the model attribute (never null) + * @return whether this model contains a corresponding attribute + */ + public boolean containsAttribute(String attributeName) { + return containsKey(attributeName); + } + + + /** + * @deprecated as of Spring 2.5, in favor of {@link #addAttribute(String, Object)} + */ + public ModelMap addObject(String modelName, Object modelObject) { + return addAttribute(modelName, modelObject); + } + + /** + * @deprecated as of Spring 2.5, in favor of {@link #addAttribute(Object)} + */ + public ModelMap addObject(Object modelObject) { + return addAttribute(modelObject); + } + + /** + * @deprecated as of Spring 2.5, in favor of {@link #addAllAttributes(Collection)} + */ + public ModelMap addAllObjects(Collection objects) { + return addAllAttributes(objects); + } + + /** + * @deprecated as of Spring 2.5, in favor of {@link #addAllAttributes(Map)} + */ + public ModelMap addAllObjects(Map objects) { + return addAllAttributes(objects); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java b/org.springframework.context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java new file mode 100644 index 00000000000..a67fc7f09a0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java @@ -0,0 +1,42 @@ +/* + * 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.context; + +/** + * Sub-interface of ThemeSource to be implemented by objects that + * can resolve theme messages hierarchically. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public interface HierarchicalThemeSource extends ThemeSource { + + /** + * Set the parent that will be used to try to resolve theme messages + * that this object can't resolve. + * @param parent the parent ThemeSource that will be used to + * resolve messages that this object can't resolve. + * May be null, in which case no further resolution is possible. + */ + void setParentThemeSource(ThemeSource parent); + + /** + * Return the parent of this ThemeSource, or null if none. + */ + ThemeSource getParentThemeSource(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/Theme.java b/org.springframework.context/src/main/java/org/springframework/ui/context/Theme.java new file mode 100644 index 00000000000..dc435264ab1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/Theme.java @@ -0,0 +1,47 @@ +/* + * 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.ui.context; + +import org.springframework.context.MessageSource; + +/** + * A Theme can resolve theme-specific messages, codes, file paths, etcetera + * (e.g. CSS and image files in a web environment). + * The exposed {@link org.springframework.context.MessageSource} supports + * theme-specific parameterization and internationalization. + * + * @author Juergen Hoeller + * @since 17.06.2003 + * @see ThemeSource + * @see org.springframework.web.servlet.ThemeResolver + */ +public interface Theme { + + /** + * Return the name of the theme. + * @return the name of the theme (never null) + */ + String getName(); + + /** + * Return the specific MessageSource that resolves messages + * with respect to this theme. + * @return the theme-specific MessageSource (never null) + */ + MessageSource getMessageSource(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/ThemeSource.java b/org.springframework.context/src/main/java/org/springframework/ui/context/ThemeSource.java new file mode 100644 index 00000000000..b63b444a67d --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/ThemeSource.java @@ -0,0 +1,43 @@ +/* + * 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.ui.context; + +/** + * Interface to be implemented by objects that can resolve {@link Theme Themes}. + * This enables parameterization and internationalization of messages + * for a given 'theme'. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @see Theme + */ +public interface ThemeSource { + + /** + * Return the Theme instance for the given theme name. + *

The returned Theme will resolve theme-specific messages, codes, + * file paths, etc (e.g. CSS and image files in a web environment). + * @param themeName the name of the theme + * @return the corresponding Theme, or null if none defined. + * Note that, by convention, a ThemeSource should at least be able to + * return a default Theme for the default theme name "theme" but may also + * return default Themes for other theme names. + * @see org.springframework.web.servlet.theme.AbstractThemeResolver#ORIGINAL_DEFAULT_THEME_NAME + */ + Theme getTheme(String themeName); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/package.html b/org.springframework.context/src/main/java/org/springframework/ui/context/package.html new file mode 100644 index 00000000000..05af8682090 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/package.html @@ -0,0 +1,31 @@ + + + +Contains classes defining the application context subinterface +for UI applications. The theme feature is added here. + +

    +
  • If no UiApplicationContextUtils.THEME_SOURCE_BEAN_NAME +bean is available in the context or parent context, a default ResourceBundleThemeSource +will be created for requested themes. In this case, the base name of the property file will match +with the theme name.
  • +
  • If the bean is available in the context or parent context, a basenamePrefix can be + set before the theme name for locating the property files like this: +
    + <bean id="themeSource" class="org.springframework.ui.context.support.ResourceBundleThemeSource"> +
    <property name="basenamePrefix"><value>theme.</value></property> +
    </bean> +
    +
    in this case, the themes resource bundles will be named theme.<theme_name>XXX.properties. +
  • +
  • This can be defined at application level and/or at servlet level for web applications.
  • +
  • Normal i18n features of Resource Bundles are available. So a theme message can be dependant + of both theme and locale.
  • +
  • If messages in the resource bundles are in fact paths to resources(css, images, ...), make sure these resources + are directly available for the user and not, for example, under the WEB-INF directory.
  • +
+ +
Web packages add the resolution and the setting of the user current theme. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java b/org.springframework.context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java new file mode 100644 index 00000000000..789e0132cd3 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.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.context.support; + +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; + +/** + * Empty ThemeSource that delegates all calls to the parent ThemeSource. + * If no parent is available, it simply won't resolve any theme. + * + *

Used as placeholder by UiApplicationContextUtils, if a context doesn't + * define its own ThemeSource. Not intended for direct use in applications. + * + * @author Juergen Hoeller + * @since 1.2.4 + * @see UiApplicationContextUtils + */ +public class DelegatingThemeSource implements HierarchicalThemeSource { + + private ThemeSource parentThemeSource; + + + public void setParentThemeSource(ThemeSource parentThemeSource) { + this.parentThemeSource = parentThemeSource; + } + + public ThemeSource getParentThemeSource() { + return parentThemeSource; + } + + + public Theme getTheme(String themeName) { + if (this.parentThemeSource != null) { + return this.parentThemeSource.getTheme(themeName); + } + else { + return null; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java b/org.springframework.context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java new file mode 100644 index 00000000000..ccff153411a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java @@ -0,0 +1,152 @@ +/* + * 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.ui.context.support; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; + +/** + * {@link ThemeSource} implementation that looks up an individual + * {@link java.util.ResourceBundle} per theme. The theme name gets + * interpreted as ResourceBundle basename, supporting a common + * basename prefix for all themes. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @see #setBasenamePrefix + * @see java.util.ResourceBundle + * @see org.springframework.context.support.ResourceBundleMessageSource + */ +public class ResourceBundleThemeSource implements HierarchicalThemeSource { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ThemeSource parentThemeSource; + + private String basenamePrefix = ""; + + /** Map from theme name to Theme instance */ + private final Map themeCache = new HashMap(); + + + public void setParentThemeSource(ThemeSource parent) { + this.parentThemeSource = parent; + + // Update existing Theme objects. + // Usually there shouldn't be any at the time of this call. + synchronized (this.themeCache) { + Iterator it = this.themeCache.values().iterator(); + while (it.hasNext()) { + initParent((Theme) it.next()); + } + } + } + + public ThemeSource getParentThemeSource() { + return this.parentThemeSource; + } + + /** + * Set the prefix that gets applied to the ResourceBundle basenames, + * i.e. the theme names. + * E.g.: basenamePrefix="test.", themeName="theme" -> basename="test.theme". + *

Note that ResourceBundle names are effectively classpath locations: As a + * consequence, the JDK's standard ResourceBundle treats dots as package separators. + * This means that "test.theme" is effectively equivalent to "test/theme", + * just like it is for programmatic java.util.ResourceBundle usage. + * @see java.util.ResourceBundle#getBundle(String) + */ + public void setBasenamePrefix(String basenamePrefix) { + this.basenamePrefix = (basenamePrefix != null ? basenamePrefix : ""); + } + + + /** + * This implementation returns a SimpleTheme instance, holding a + * ResourceBundle-based MessageSource whose basename corresponds to + * the given theme name (prefixed by the configured "basenamePrefix"). + *

SimpleTheme instances are cached per theme name. Use a reloadable + * MessageSource if themes should reflect changes to the underlying files. + * @see #setBasenamePrefix + * @see #createMessageSource + */ + public Theme getTheme(String themeName) { + if (themeName == null) { + return null; + } + synchronized (this.themeCache) { + Theme theme = (Theme) this.themeCache.get(themeName); + if (theme == null) { + String basename = this.basenamePrefix + themeName; + MessageSource messageSource = createMessageSource(basename); + theme = new SimpleTheme(themeName, messageSource); + initParent(theme); + this.themeCache.put(themeName, theme); + if (logger.isDebugEnabled()) { + logger.debug("Theme created: name '" + themeName + "', basename [" + basename + "]"); + } + } + return theme; + } + } + + /** + * Create a MessageSource for the given basename, + * to be used as MessageSource for the corresponding theme. + *

Default implementation creates a ResourceBundleMessageSource. + * for the given basename. A subclass could create a specifically + * configured ReloadableResourceBundleMessageSource, for example. + * @param basename the basename to create a MessageSource for + * @return the MessageSource + * @see org.springframework.context.support.ResourceBundleMessageSource + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + */ + protected MessageSource createMessageSource(String basename) { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(basename); + return messageSource; + } + + /** + * Initialize the MessageSource of the given theme with the + * one from the corresponding parent of this ThemeSource. + * @param theme the Theme to (re-)initialize + */ + protected void initParent(Theme theme) { + if (theme.getMessageSource() instanceof HierarchicalMessageSource) { + HierarchicalMessageSource messageSource = (HierarchicalMessageSource) theme.getMessageSource(); + if (getParentThemeSource() != null && messageSource.getParentMessageSource() == null) { + Theme parentTheme = getParentThemeSource().getTheme(theme.getName()); + if (parentTheme != null) { + messageSource.setParentMessageSource(parentTheme.getMessageSource()); + } + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java b/org.springframework.context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java new file mode 100644 index 00000000000..47910c99955 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java @@ -0,0 +1,58 @@ +/* + * 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.ui.context.support; + +import org.springframework.context.MessageSource; +import org.springframework.ui.context.Theme; +import org.springframework.util.Assert; + +/** + * Default {@link Theme} implementation, wrapping a name and an + * underlying {@link org.springframework.context.MessageSource}. + * + * @author Juergen Hoeller + * @since 17.06.2003 + */ +public class SimpleTheme implements Theme { + + private final String name; + + private final MessageSource messageSource; + + + /** + * Create a SimpleTheme. + * @param name the name of the theme + * @param messageSource the MessageSource that resolves theme messages + */ + public SimpleTheme(String name, MessageSource messageSource) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(messageSource, "MessageSource must not be null"); + this.name = name; + this.messageSource = messageSource; + } + + + public final String getName() { + return this.name; + } + + public final MessageSource getMessageSource() { + return this.messageSource; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java b/org.springframework.context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java new file mode 100644 index 00000000000..d3263d541ac --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java @@ -0,0 +1,92 @@ +/* + * 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.ui.context.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.ThemeSource; + +/** + * Utility class for UI application context implementations. + * Provides support for a special bean named "themeSource", + * of type {@link org.springframework.ui.context.ThemeSource}. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @since 17.06.2003 + */ +public abstract class UiApplicationContextUtils { + + /** + * Name of the ThemeSource bean in the factory. + * If none is supplied, theme resolution is delegated to the parent. + * @see org.springframework.ui.context.ThemeSource + */ + public static final String THEME_SOURCE_BEAN_NAME = "themeSource"; + + + private static final Log logger = LogFactory.getLog(UiApplicationContextUtils.class); + + + /** + * Initialize the ThemeSource for the given application context, + * autodetecting a bean with the name "themeSource". If no such + * bean is found, a default (empty) ThemeSource will be used. + * @param context current application context + * @return the initialized theme source (will never be null) + * @see #THEME_SOURCE_BEAN_NAME + */ + public static ThemeSource initThemeSource(ApplicationContext context) { + if (context.containsLocalBean(THEME_SOURCE_BEAN_NAME)) { + ThemeSource themeSource = (ThemeSource) context.getBean(THEME_SOURCE_BEAN_NAME, ThemeSource.class); + // Make ThemeSource aware of parent ThemeSource. + if (context.getParent() instanceof ThemeSource && themeSource instanceof HierarchicalThemeSource) { + HierarchicalThemeSource hts = (HierarchicalThemeSource) themeSource; + if (hts.getParentThemeSource() == null) { + // Only set parent context as parent ThemeSource if no parent ThemeSource + // registered already. + hts.setParentThemeSource((ThemeSource) context.getParent()); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Using ThemeSource [" + themeSource + "]"); + } + return themeSource; + } + else { + // Use default ThemeSource to be able to accept getTheme calls, either + // delegating to parent context's default or to local ResourceBundleThemeSource. + HierarchicalThemeSource themeSource = null; + if (context.getParent() instanceof ThemeSource) { + themeSource = new DelegatingThemeSource(); + themeSource.setParentThemeSource((ThemeSource) context.getParent()); + } + else { + themeSource = new ResourceBundleThemeSource(); + } + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate ThemeSource with name '" + THEME_SOURCE_BEAN_NAME + + "': using default [" + themeSource + "]"); + } + return themeSource; + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/ui/context/support/package.html b/org.springframework.context/src/main/java/org/springframework/ui/context/support/package.html new file mode 100644 index 00000000000..3ac493c5564 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/context/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.ui.context package. +Provides support classes for specialized UI contexts, e.g. for web UIs. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/ui/package.html b/org.springframework.context/src/main/java/org/springframework/ui/package.html new file mode 100644 index 00000000000..37fb6d0fb5f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/ui/package.html @@ -0,0 +1,8 @@ + + + +Generic support for UI layer concepts. +Provides a generic ModelMap for model holding. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/AbstractBindingResult.java new file mode 100644 index 00000000000..464dd9e23bc --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -0,0 +1,378 @@ +/* + * 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.validation; + +import java.beans.PropertyEditor; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.util.StringUtils; + +/** + * Abstract implementation of the {@link BindingResult} interface and + * its super-interface {@link Errors}. Encapsulates common management of + * {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see Errors + */ +public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult, Serializable { + + private final String objectName; + + private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + + private final List errors = new LinkedList(); + + private final Set suppressedFields = new HashSet(); + + + /** + * Create a new AbstractBindingResult instance. + * @param objectName the name of the target object + * @see DefaultMessageCodesResolver + */ + protected AbstractBindingResult(String objectName) { + this.objectName = objectName; + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Default is DefaultMessageCodesResolver. + * @see DefaultMessageCodesResolver + */ + public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the strategy to use for resolving errors into message codes. + */ + public MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + + //--------------------------------------------------------------------- + // Implementation of the Errors interface + //--------------------------------------------------------------------- + + public String getObjectName() { + return this.objectName; + } + + + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage)); + } + + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + if ("".equals(getNestedPath()) && !StringUtils.hasLength(field)) { + // We're at the top of the nested object hierarchy, + // so the present level is not a field but rather the top object. + // The best we can do is register a global error here... + reject(errorCode, errorArgs, defaultMessage); + return; + } + String fixedField = fixedField(field); + Object newVal = getActualFieldValue(fixedField); + FieldError fe = new FieldError( + getObjectName(), fixedField, newVal, false, + resolveMessageCodes(errorCode, field), errorArgs, defaultMessage); + addError(fe); + } + + public void addError(ObjectError error) { + this.errors.add(error); + } + + public void addAllErrors(Errors errors) { + if (!errors.getObjectName().equals(getObjectName())) { + throw new IllegalArgumentException("Errors object needs to have same object name"); + } + this.errors.addAll(errors.getAllErrors()); + } + + /** + * Resolve the given error code into message codes. + * Calls the MessageCodesResolver with appropriate parameters. + * @param errorCode the error code to resolve into message codes + * @return the resolved message codes + * @see #setMessageCodesResolver + */ + public String[] resolveMessageCodes(String errorCode) { + return getMessageCodesResolver().resolveMessageCodes(errorCode, getObjectName()); + } + + public String[] resolveMessageCodes(String errorCode, String field) { + String fixedField = fixedField(field); + Class fieldType = getFieldType(fixedField); + return getMessageCodesResolver().resolveMessageCodes(errorCode, getObjectName(), fixedField, fieldType); + } + + + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + public int getErrorCount() { + return this.errors.size(); + } + + public List getAllErrors() { + return Collections.unmodifiableList(this.errors); + } + + public List getGlobalErrors() { + List result = new LinkedList(); + for (Iterator it = this.errors.iterator(); it.hasNext();) { + Object error = it.next(); + if (!(error instanceof FieldError)) { + result.add(error); + } + } + return Collections.unmodifiableList(result); + } + + public ObjectError getGlobalError() { + for (Iterator it = this.errors.iterator(); it.hasNext();) { + ObjectError objectError = (ObjectError) it.next(); + if (!(objectError instanceof FieldError)) { + return objectError; + } + } + return null; + } + + public List getFieldErrors() { + List result = new LinkedList(); + for (Iterator it = this.errors.iterator(); it.hasNext();) { + Object error = it.next(); + if (error instanceof FieldError) { + result.add(error); + } + } + return Collections.unmodifiableList(result); + } + + public FieldError getFieldError() { + for (Iterator it = this.errors.iterator(); it.hasNext();) { + Object error = it.next(); + if (error instanceof FieldError) { + return (FieldError) error; + } + } + return null; + } + + public List getFieldErrors(String field) { + List result = new LinkedList(); + String fixedField = fixedField(field); + for (Iterator it = this.errors.iterator(); it.hasNext();) { + Object error = it.next(); + if (error instanceof FieldError && isMatchingFieldError(fixedField, (FieldError) error)) { + result.add(error); + } + } + return Collections.unmodifiableList(result); + } + + public FieldError getFieldError(String field) { + String fixedField = fixedField(field); + for (Iterator it = this.errors.iterator(); it.hasNext();) { + Object error = it.next(); + if (error instanceof FieldError) { + FieldError fe = (FieldError) error; + if (isMatchingFieldError(fixedField, fe)) { + return fe; + } + } + } + return null; + } + + public Object getFieldValue(String field) { + FieldError fe = getFieldError(field); + // Use rejected value in case of error, current bean property value else. + Object value = null; + if (fe != null) { + value = fe.getRejectedValue(); + } + else { + value = getActualFieldValue(fixedField(field)); + } + // Apply formatting, but not on binding failures like type mismatches. + if (fe == null || !fe.isBindingFailure()) { + value = formatFieldValue(field, value); + } + return value; + } + + /** + * This default implementation determines the type based on the actual + * field value, if any. Subclasses should override this to determine + * the type from a descriptor, even for null values. + * @see #getActualFieldValue + */ + public Class getFieldType(String field) { + Object value = getActualFieldValue(fixedField(field)); + if (value != null) { + return value.getClass(); + } + return null; + } + + + //--------------------------------------------------------------------- + // Implementation of BindingResult interface + //--------------------------------------------------------------------- + + /** + * Return a model Map for the obtained state, exposing an Errors + * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName' + * and the object itself. + *

Note that the Map is constructed every time you're calling this method. + * Adding things to the map and then re-calling this method will not work. + *

The attributes in the model Map returned by this method are usually + * included in the ModelAndView for a form view that uses Spring's bind tag, + * which needs access to the Errors instance. Spring's SimpleFormController + * will do this for you when rendering its form or success view. When + * building the ModelAndView yourself, you need to include the attributes + * from the model Map returned by this method yourself. + * @see #getObjectName + * @see #MODEL_KEY_PREFIX + * @see org.springframework.web.servlet.ModelAndView + * @see org.springframework.web.servlet.tags.BindTag + * @see org.springframework.web.servlet.mvc.SimpleFormController + */ + public Map getModel() { + Map model = new HashMap(2); + // Errors instance, even if no errors. + model.put(MODEL_KEY_PREFIX + getObjectName(), this); + // Mapping from name to target object. + model.put(getObjectName(), getTarget()); + return model; + } + + public Object getRawFieldValue(String field) { + return getActualFieldValue(fixedField(field)); + } + + /** + * This implementation delegates to the + * {@link #getPropertyEditorRegistry() PropertyEditorRegistry}'s + * editor lookup facility, if available. + */ + public PropertyEditor findEditor(String field, Class valueType) { + PropertyEditorRegistry editorRegistry = getPropertyEditorRegistry(); + if (editorRegistry != null) { + Class valueTypeToUse = valueType; + if (valueTypeToUse == null) { + valueTypeToUse = getFieldType(field); + } + return editorRegistry.findCustomEditor(valueTypeToUse, fixedField(field)); + } + else { + return null; + } + } + + /** + * This implementation returns null. + */ + public PropertyEditorRegistry getPropertyEditorRegistry() { + return null; + } + + /** + * Mark the specified disallowed field as suppressed. + *

The data binder invokes this for each field value that was + * detected to target a disallowed field. + * @see DataBinder#setAllowedFields + */ + public void recordSuppressedField(String field) { + this.suppressedFields.add(field); + } + + /** + * Return the list of fields that were suppressed during the bind process. + *

Can be used to determine whether any field values were targetting + * disallowed fields. + * @see DataBinder#setAllowedFields + */ + public String[] getSuppressedFields() { + return StringUtils.toStringArray(this.suppressedFields); + } + + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BindingResult)) { + return false; + } + BindingResult otherResult = (BindingResult) other; + return (getObjectName().equals(otherResult.getObjectName()) && + getTarget().equals(otherResult.getTarget()) && + getAllErrors().equals(otherResult.getAllErrors())); + } + + public int hashCode() { + return getObjectName().hashCode() * 29 + getTarget().hashCode(); + } + + + //--------------------------------------------------------------------- + // Template methods to be implemented/overridden by subclasses + //--------------------------------------------------------------------- + + /** + * Return the wrapped target object. + */ + public abstract Object getTarget(); + + /** + * Extract the actual field value for the given field. + * @param field the field to check + * @return the current value of the field + */ + protected abstract Object getActualFieldValue(String field); + + /** + * Format the given value for the specified field. + *

The default implementation simply returns the field value as-is. + * @param field the field to check + * @param value the value of the field (either a rejected value + * other than from a binding error, or an actual field value) + * @return the formatted value + */ + protected Object formatFieldValue(String field, Object value) { + return value; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/AbstractErrors.java b/org.springframework.context/src/main/java/org/springframework/validation/AbstractErrors.java new file mode 100644 index 00000000000..d6acbd552e6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -0,0 +1,223 @@ +/* + * 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.validation; + +import java.io.Serializable; +import java.util.Collections; +import java.util.EmptyStackException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Stack; + +import org.springframework.util.StringUtils; + +/** + * Abstract implementation of the {@link Errors} interface. Provides common + * access to evaluated errors; however, does not define concrete management + * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +public abstract class AbstractErrors implements Errors, Serializable { + + private String nestedPath = ""; + + private final Stack nestedPathStack = new Stack(); + + + public void setNestedPath(String nestedPath) { + doSetNestedPath(nestedPath); + this.nestedPathStack.clear(); + } + + public String getNestedPath() { + return this.nestedPath; + } + + public void pushNestedPath(String subPath) { + this.nestedPathStack.push(getNestedPath()); + doSetNestedPath(getNestedPath() + subPath); + } + + public void popNestedPath() throws IllegalArgumentException { + try { + String formerNestedPath = (String) this.nestedPathStack.pop(); + doSetNestedPath(formerNestedPath); + } + catch (EmptyStackException ex) { + throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); + } + } + + /** + * Actually set the nested path. + * Delegated to by setNestedPath and pushNestedPath. + */ + protected void doSetNestedPath(String nestedPath) { + if (nestedPath == null) { + nestedPath = ""; + } + nestedPath = canonicalFieldName(nestedPath); + if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { + nestedPath += Errors.NESTED_PATH_SEPARATOR; + } + this.nestedPath = nestedPath; + } + + /** + * Transform the given field into its full path, + * regarding the nested path of this instance. + */ + protected String fixedField(String field) { + if (StringUtils.hasLength(field)) { + return getNestedPath() + canonicalFieldName(field); + } + else { + String path = getNestedPath(); + return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? + path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); + } + } + + /** + * Determine the canonical field name for the given field. + *

The default implementation simply returns the field name as-is. + * @param field the original field name + * @return the canonical field name + */ + protected String canonicalFieldName(String field) { + return field; + } + + + public void reject(String errorCode) { + reject(errorCode, null, null); + } + + public void reject(String errorCode, String defaultMessage) { + reject(errorCode, null, defaultMessage); + } + + public void rejectValue(String field, String errorCode) { + rejectValue(field, errorCode, null, null); + } + + public void rejectValue(String field, String errorCode, String defaultMessage) { + rejectValue(field, errorCode, null, defaultMessage); + } + + + public boolean hasErrors() { + return !getAllErrors().isEmpty(); + } + + public int getErrorCount() { + return getAllErrors().size(); + } + + public List getAllErrors() { + List result = new LinkedList(); + result.addAll(getGlobalErrors()); + result.addAll(getFieldErrors()); + return Collections.unmodifiableList(result); + } + + public boolean hasGlobalErrors() { + return (getGlobalErrorCount() > 0); + } + + public int getGlobalErrorCount() { + return getGlobalErrors().size(); + } + + public ObjectError getGlobalError() { + List globalErrors = getGlobalErrors(); + return (!globalErrors.isEmpty() ? (ObjectError) globalErrors.get(0) : null); + } + + public boolean hasFieldErrors() { + return (getFieldErrorCount() > 0); + } + + public int getFieldErrorCount() { + return getFieldErrors().size(); + } + + public FieldError getFieldError() { + List fieldErrors = getFieldErrors(); + return (!fieldErrors.isEmpty() ? (FieldError) fieldErrors.get(0) : null); + } + + public boolean hasFieldErrors(String field) { + return (getFieldErrorCount(field) > 0); + } + + public int getFieldErrorCount(String field) { + return getFieldErrors(field).size(); + } + + public List getFieldErrors(String field) { + List fieldErrors = getFieldErrors(); + List result = new LinkedList(); + String fixedField = fixedField(field); + for (Iterator it = fieldErrors.iterator(); it.hasNext();) { + Object error = it.next(); + if (isMatchingFieldError(fixedField, (FieldError) error)) { + result.add(error); + } + } + return Collections.unmodifiableList(result); + } + + public FieldError getFieldError(String field) { + List fieldErrors = getFieldErrors(field); + return (!fieldErrors.isEmpty() ? (FieldError) fieldErrors.get(0) : null); + } + + + public Class getFieldType(String field) { + Object value = getFieldValue(field); + if (value != null) { + return value.getClass(); + } + return null; + } + /** + * Check whether the given FieldError matches the given field. + * @param field the field that we are looking up FieldErrors for + * @param fieldError the candidate FieldError + * @return whether the FieldError matches the given field + */ + protected boolean isMatchingFieldError(String field, FieldError fieldError) { + return (field.equals(fieldError.getField()) || + (field.endsWith("*") && fieldError.getField().startsWith(field.substring(0, field.length() - 1)))); + } + + + public String toString() { + StringBuffer sb = new StringBuffer(getClass().getName()); + sb.append(": ").append(getErrorCount()).append(" errors"); + Iterator it = getAllErrors().iterator(); + while (it.hasNext()) { + sb.append('\n').append(it.next()); + } + return sb.toString(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java new file mode 100644 index 00000000000..a7773862d6a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java @@ -0,0 +1,127 @@ +/* + * 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.validation; + +import java.beans.PropertyEditor; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorUtils; +import org.springframework.beans.PropertyEditorRegistry; + +/** + * Abstract base class for {@link BindingResult} implementations that work with + * Spring's {@link org.springframework.beans.PropertyAccessor} mechanism. + * Pre-implements field access through delegation to the corresponding + * PropertyAccessor methods. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getPropertyAccessor() + * @see org.springframework.beans.PropertyAccessor + * @see org.springframework.beans.ConfigurablePropertyAccessor + */ +public abstract class AbstractPropertyBindingResult extends AbstractBindingResult { + + /** + * Create a new AbstractPropertyBindingResult instance. + * @param objectName the name of the target object + * @see DefaultMessageCodesResolver + */ + protected AbstractPropertyBindingResult(String objectName) { + super(objectName); + } + + + /** + * Returns the underlying PropertyAccessor. + * @see #getPropertyAccessor() + */ + public PropertyEditorRegistry getPropertyEditorRegistry() { + return getPropertyAccessor(); + } + + /** + * Returns the canonical property name. + * @see org.springframework.beans.PropertyAccessorUtils#canonicalPropertyName + */ + protected String canonicalFieldName(String field) { + return PropertyAccessorUtils.canonicalPropertyName(field); + } + + /** + * Determines the field type from the property type. + * @see #getPropertyAccessor() + */ + public Class getFieldType(String field) { + return getPropertyAccessor().getPropertyType(fixedField(field)); + } + + /** + * Fetches the field value from the PropertyAccessor. + * @see #getPropertyAccessor() + */ + protected Object getActualFieldValue(String field) { + return getPropertyAccessor().getPropertyValue(field); + } + + /** + * Formats the field value based on registered PropertyEditors. + * @see #getCustomEditor + */ + protected Object formatFieldValue(String field, Object value) { + PropertyEditor customEditor = getCustomEditor(field); + if (customEditor != null) { + customEditor.setValue(value); + String textValue = customEditor.getAsText(); + // If the PropertyEditor returned null, there is no appropriate + // text representation for this value: only use it if non-null. + if (textValue != null) { + return textValue; + } + } + return value; + } + + /** + * Retrieve the custom PropertyEditor for the given field, if any. + * @param field the field name + * @return the custom PropertyEditor, or null + */ + protected PropertyEditor getCustomEditor(String field) { + String fixedField = fixedField(field); + Class targetType = getPropertyAccessor().getPropertyType(fixedField); + PropertyEditor editor = getPropertyAccessor().findCustomEditor(targetType, fixedField); + if (editor == null) { + editor = BeanUtils.findEditorByConvention(targetType); + } + return editor; + } + + + /** + * Provide the PropertyAccessor to work with, according to the + * concrete strategy of access. + *

Note that a PropertyAccessor used by a BindingResult should + * always have its "extractOldValueForEditor" flag set to "true" + * by default, since this is typically possible without side effects + * for model objects that serve as data binding target. + * @see ConfigurablePropertyAccessor#setExtractOldValueForEditor + */ + public abstract ConfigurablePropertyAccessor getPropertyAccessor(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java new file mode 100644 index 00000000000..40a34e129b5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java @@ -0,0 +1,87 @@ +/* + * 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.validation; + +import java.io.Serializable; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link Errors} and {@link BindingResult} + * interfaces, for the registration and evaluation of binding errors on + * JavaBean objects. + * + *

Performs standard JavaBean property access, also supporting nested + * properties. Normally, application code will work with the + * Errors interface or the BindingResult interface. + * A {@link DataBinder} returns its BindingResult via + * {@link org.springframework.validation.DataBinder#getBindingResult()}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder#getBindingResult() + * @see DataBinder#initBeanPropertyAccess() + * @see DirectFieldBindingResult + */ +public class BeanPropertyBindingResult extends AbstractPropertyBindingResult implements Serializable { + + private final Object target; + + private transient BeanWrapper beanWrapper; + + + /** + * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * @param target the target bean to bind onto + * @param objectName the name of the target object + */ + public BeanPropertyBindingResult(Object target, String objectName) { + super(objectName); + this.target = target; + } + + + public final Object getTarget() { + return this.target; + } + + /** + * Returns the {@link BeanWrapper} that this instance uses. + * Creates a new one if none existed before. + * @see #createBeanWrapper() + */ + public final ConfigurablePropertyAccessor getPropertyAccessor() { + if (this.beanWrapper == null) { + this.beanWrapper = createBeanWrapper(); + this.beanWrapper.setExtractOldValueForEditor(true); + } + return this.beanWrapper; + } + + /** + * Create a new {@link BeanWrapper} for the underlying target object. + * @see #getTarget() + */ + protected BeanWrapper createBeanWrapper() { + Assert.state(this.target != null, "Cannot access properties on null bean instance '" + getObjectName() + "'!"); + return PropertyAccessorFactory.forBeanPropertyAccess(this.target); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/BindException.java b/org.springframework.context/src/main/java/org/springframework/validation/BindException.java new file mode 100644 index 00000000000..30959acc6a9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/BindException.java @@ -0,0 +1,258 @@ +/* + * 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.validation; + +import java.beans.PropertyEditor; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.util.Assert; + +/** + * Thrown when binding errors are considered fatal. Implements the + * {@link BindingResult} interface (and its super-interface {@link Errors}) + * to allow for the direct analysis of binding errors. + * + *

As of Spring 2.0, this is a special-purpose class. Normally, + * application code will work with the {@link BindingResult} interface, + * or with a {@link DataBinder} that in turn exposes a BindingResult via + * {@link org.springframework.validation.DataBinder#getBindingResult()}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see BindingResult + * @see DataBinder#getBindingResult() + * @see DataBinder#close() + */ +public class BindException extends Exception implements BindingResult { + + /** + * Prefix for the name of the BindException instance in a model, + * followed by the object name. + * @deprecated in favor of BindingResult.MODEL_KEY_PREFIX + * @see BindingResult#MODEL_KEY_PREFIX + */ + public static final String ERROR_KEY_PREFIX = BindException.class.getName() + "."; + + + private final BindingResult bindingResult; + + + /** + * Create a new BindException instance for a BindingResult. + * @param bindingResult the BindingResult instance to wrap + */ + public BindException(BindingResult bindingResult) { + Assert.notNull(bindingResult, "BindingResult must not be null"); + this.bindingResult = bindingResult; + } + + /** + * Create a new BindException instance for a target bean. + * @param target target bean to bind onto + * @param objectName the name of the target object + * @see BeanPropertyBindingResult + */ + public BindException(Object target, String objectName) { + Assert.notNull(target, "Target object must not be null"); + this.bindingResult = new BeanPropertyBindingResult(target, objectName); + } + + + /** + * Return the BindingResult that this BindException wraps. + * Will typically be a BeanPropertyBindingResult. + * @see BeanPropertyBindingResult + */ + public final BindingResult getBindingResult() { + return this.bindingResult; + } + + + public String getObjectName() { + return this.bindingResult.getObjectName(); + } + + public void setNestedPath(String nestedPath) { + this.bindingResult.setNestedPath(nestedPath); + } + + public String getNestedPath() { + return this.bindingResult.getNestedPath(); + } + + public void pushNestedPath(String subPath) { + this.bindingResult.pushNestedPath(subPath); + } + + public void popNestedPath() throws IllegalStateException { + this.bindingResult.popNestedPath(); + } + + + public void reject(String errorCode) { + this.bindingResult.reject(errorCode); + } + + public void reject(String errorCode, String defaultMessage) { + this.bindingResult.reject(errorCode, defaultMessage); + } + + public void reject(String errorCode, Object[] errorArgs, String defaultMessage) { + this.bindingResult.reject(errorCode, errorArgs, defaultMessage); + } + + public void rejectValue(String field, String errorCode) { + this.bindingResult.rejectValue(field, errorCode); + } + + public void rejectValue(String field, String errorCode, String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, defaultMessage); + } + + public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + + public void addAllErrors(Errors errors) { + this.bindingResult.addAllErrors(errors); + } + + + public boolean hasErrors() { + return this.bindingResult.hasErrors(); + } + + public int getErrorCount() { + return this.bindingResult.getErrorCount(); + } + + public List getAllErrors() { + return this.bindingResult.getAllErrors(); + } + + public boolean hasGlobalErrors() { + return this.bindingResult.hasGlobalErrors(); + } + + public int getGlobalErrorCount() { + return this.bindingResult.getGlobalErrorCount(); + } + + public List getGlobalErrors() { + return this.bindingResult.getGlobalErrors(); + } + + public ObjectError getGlobalError() { + return this.bindingResult.getGlobalError(); + } + + public boolean hasFieldErrors() { + return this.bindingResult.hasFieldErrors(); + } + + public int getFieldErrorCount() { + return this.bindingResult.getFieldErrorCount(); + } + + public List getFieldErrors() { + return this.bindingResult.getFieldErrors(); + } + + public FieldError getFieldError() { + return this.bindingResult.getFieldError(); + } + + public boolean hasFieldErrors(String field) { + return this.bindingResult.hasFieldErrors(field); + } + + public int getFieldErrorCount(String field) { + return this.bindingResult.getFieldErrorCount(field); + } + + public List getFieldErrors(String field) { + return this.bindingResult.getFieldErrors(field); + } + + public FieldError getFieldError(String field) { + return this.bindingResult.getFieldError(field); + } + + public Object getFieldValue(String field) { + return this.bindingResult.getFieldValue(field); + } + + public Class getFieldType(String field) { + return this.bindingResult.getFieldType(field); + } + + public Object getTarget() { + return this.bindingResult.getTarget(); + } + + public Map getModel() { + return this.bindingResult.getModel(); + } + + public Object getRawFieldValue(String field) { + return this.bindingResult.getRawFieldValue(field); + } + + public PropertyEditor findEditor(String field, Class valueType) { + return this.bindingResult.findEditor(field, valueType); + } + + public PropertyEditorRegistry getPropertyEditorRegistry() { + return this.bindingResult.getPropertyEditorRegistry(); + } + + public void addError(ObjectError error) { + this.bindingResult.addError(error); + } + + public String[] resolveMessageCodes(String errorCode, String field) { + return this.bindingResult.resolveMessageCodes(errorCode, field); + } + + public void recordSuppressedField(String field) { + this.bindingResult.recordSuppressedField(field); + } + + public String[] getSuppressedFields() { + return this.bindingResult.getSuppressedFields(); + } + + + /** + * Returns diagnostic information about the errors held in this object. + */ + public String getMessage() { + return this.bindingResult.toString(); + } + + public boolean equals(Object other) { + return (this == other || this.bindingResult.equals(other)); + } + + public int hashCode() { + return this.bindingResult.hashCode(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/BindingErrorProcessor.java b/org.springframework.context/src/main/java/org/springframework/validation/BindingErrorProcessor.java new file mode 100644 index 00000000000..2a76abcd2c7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/BindingErrorProcessor.java @@ -0,0 +1,77 @@ +/* + * 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.validation; + +import org.springframework.beans.PropertyAccessException; + +/** + * Strategy for processing DataBinder's missing field errors, + * and for translating a PropertyAccessException to a + * FieldError. + * + *

The error processor is pluggable so you can treat errors differently + * if you want to. A default implementation is provided for typical needs. + * + *

Note: As of Spring 2.0, this interface operates on a given BindingResult, + * to be compatible with any binding strategy (bean property, direct field access, etc). + * It can still receive a BindException as argument (since a BindException implements + * the BindingResult interface as well) but no longer operates on it directly. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @since 1.2 + * @see DataBinder#setBindingErrorProcessor + * @see DefaultBindingErrorProcessor + * @see BindingResult + * @see BindException + */ +public interface BindingErrorProcessor { + + /** + * Apply the missing field error to the given BindException. + *

Usually, a field error is created for a missing required field. + * @param missingField the field that was missing during binding + * @param bindingResult the errors object to add the error(s) to. + * You can add more than just one error or maybe even ignore it. + * The BindingResult object features convenience utils such as + * a resolveMessageCodes method to resolve an error code. + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + */ + void processMissingFieldError(String missingField, BindingResult bindingResult); + + /** + * Translate the given PropertyAccessException to an appropriate + * error registered on the given Errors instance. + *

Note that two error types are available: FieldError and + * ObjectError. Usually, field errors are created, but in certain + * situations one might want to create a global ObjectError instead. + * @param ex the PropertyAccessException to translate + * @param bindingResult the errors object to add the error(s) to. + * You can add more than just one error or maybe even ignore it. + * The BindingResult object features convenience utils such as + * a resolveMessageCodes method to resolve an error code. + * @see Errors + * @see FieldError + * @see ObjectError + * @see MessageCodesResolver + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + */ + void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/BindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/BindingResult.java new file mode 100644 index 00000000000..d67847a1e41 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/BindingResult.java @@ -0,0 +1,140 @@ +/* + * 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.validation; + +import java.beans.PropertyEditor; +import java.util.Map; + +import org.springframework.beans.PropertyEditorRegistry; + +/** + * General interface that represents binding results. Extends the + * {@link Errors interface} for error registration capabilities, + * allowing for a {@link Validator} to be applied, and adds + * binding-specific analysis and model building. + * + *

Serves as result holder for a {@link DataBinder}, obtained via + * the {@link DataBinder#getBindingResult()} method. BindingResult + * implementations can also be used directly, for example to invoke + * a {@link Validator} on it (e.g. as part of a unit test). + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder + * @see Errors + * @see Validator + * @see BeanPropertyBindingResult + * @see DirectFieldBindingResult + * @see MapBindingResult + */ +public interface BindingResult extends Errors { + + /** + * Prefix for the name of the BindingResult instance in a model, + * followed by the object name. + */ + String MODEL_KEY_PREFIX = BindingResult.class.getName() + "."; + + + /** + * Return the wrapped target object, which may be a bean, an object with + * public fields, a Map - depending on the concrete binding strategy. + */ + Object getTarget(); + + /** + * Return a model Map for the obtained state, exposing a BindingResult + * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName' + * and the object itself as 'objectName'. + *

Note that the Map is constructed every time you're calling this method. + * Adding things to the map and then re-calling this method will not work. + *

The attributes in the model Map returned by this method are usually + * included in the {@link org.springframework.web.servlet.ModelAndView} + * for a form view that uses Spring's bind tag in a JSP, + * which needs access to the BindingResult instance. Spring's pre-built + * form controllers will do this for you when rendering a form view. + * When building the ModelAndView instance yourself, you need to include + * the attributes from the model Map returned by this method. + * @see #getObjectName() + * @see #MODEL_KEY_PREFIX + * @see org.springframework.web.servlet.ModelAndView + * @see org.springframework.web.servlet.tags.BindTag + * @see org.springframework.web.servlet.mvc.SimpleFormController + */ + Map getModel(); + + /** + * Extract the raw field value for the given field. + * Typically used for comparison purposes. + * @param field the field to check + * @return the current value of the field in its raw form, + * or null if not known + */ + Object getRawFieldValue(String field); + + /** + * Find a custom property editor for the given type and property. + * @param valueType the type of the property (can be null if a property + * is given but should be specified in any case for consistency checking) + * @param field the path of the property (name or nested path), or + * null if looking for an editor for all properties of the given type + * @return the registered editor, or null if none + */ + PropertyEditor findEditor(String field, Class valueType); + + /** + * Return the underlying PropertyEditorRegistry. + * @return the PropertyEditorRegistry, or null if none + * available for this BindingResult + */ + PropertyEditorRegistry getPropertyEditorRegistry(); + + /** + * Add a custom {@link ObjectError} or {@link FieldError} to the errors list. + *

Intended to be used by cooperating strategies such as {@link BindingErrorProcessor}. + * @see ObjectError + * @see FieldError + * @see BindingErrorProcessor + */ + void addError(ObjectError error); + + /** + * Resolve the given error code into message codes for the given field. + *

Calls the configured {@link MessageCodesResolver} with appropriate parameters. + * @param errorCode the error code to resolve into message codes + * @param field the field to resolve message codes for + * @return the resolved message codes + */ + String[] resolveMessageCodes(String errorCode, String field); + + /** + * Mark the specified disallowed field as suppressed. + *

The data binder invokes this for each field value that was + * detected to target a disallowed field. + * @see DataBinder#setAllowedFields + */ + void recordSuppressedField(String field); + + /** + * Return the list of fields that were suppressed during the bind process. + *

Can be used to determine whether any field values were targeting + * disallowed fields. + * @see DataBinder#setAllowedFields + */ + String[] getSuppressedFields(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/BindingResultUtils.java b/org.springframework.context/src/main/java/org/springframework/validation/BindingResultUtils.java new file mode 100644 index 00000000000..a2d446b3c6e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/BindingResultUtils.java @@ -0,0 +1,65 @@ +/* + * 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.validation; + +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Convenience methods for looking up BindingResults in a model Map. + * + * @author Juergen Hoeller + * @since 2.0 + * @see BindingResult#MODEL_KEY_PREFIX + */ +public abstract class BindingResultUtils { + + /** + * Find the BindingResult for the given name in the given model. + * @param model the model to search + * @param name the name of the target object to find a BindingResult for + * @return the BindingResult, or null if none found + * @throws IllegalStateException if the attribute found is not of type BindingResult + */ + public static BindingResult getBindingResult(Map model, String name) { + Assert.notNull(model, "Model map must not be null"); + Assert.notNull(name, "Name must not be null"); + Object attr = model.get(BindingResult.MODEL_KEY_PREFIX + name); + if (attr != null && !(attr instanceof BindingResult)) { + throw new IllegalStateException("BindingResult attribute is not of type BindingResult: " + attr); + } + return (BindingResult) attr; + } + + /** + * Find a required BindingResult for the given name in the given model. + * @param model the model to search + * @param name the name of the target object to find a BindingResult for + * @return the BindingResult (never null) + * @throws IllegalStateException if no BindingResult found + */ + public static BindingResult getRequiredBindingResult(Map model, String name) { + BindingResult bindingResult = getBindingResult(model, name); + if (bindingResult == null) { + throw new IllegalStateException("No BindingResult attribute found for name '" + name + + "'- have you exposed the correct model?"); + } + return bindingResult; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java new file mode 100644 index 00000000000..15d50032559 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/DataBinder.java @@ -0,0 +1,613 @@ +/* + * 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.validation; + +import java.beans.PropertyEditor; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessException; +import org.springframework.beans.PropertyAccessorUtils; +import org.springframework.beans.PropertyBatchUpdateException; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * Binder that allows for setting property values onto a target object, + * including support for validation and binding result analysis. + * The binding process can be customized through specifying allowed fields, + * required fields, custom editors, etc. + * + *

Note that there are potential security implications in failing to set an array + * of allowed fields. In the case of HTTP form POST data for example, malicious clients + * can attempt to subvert an application by supplying values for fields or properties + * that do not exist on the form. In some cases this could lead to illegal data being + * set on command objects or their nested objects. For this reason, it is + * highly recommended to specify the {@link #setAllowedFields allowedFields} property + * on the DataBinder. + * + *

The binding results can be examined via the {@link BindingResult} interface, + * extending the {@link Errors} interface: see the {@link #getBindingResult()} method. + * Missing fields and property access exceptions will be converted to {@link FieldError FieldErrors}, + * collected in the Errors instance, using the following error codes: + * + *

    + *
  • Missing field error: "required" + *
  • Type mismatch error: "typeMismatch" + *
  • Method invocation error: "methodInvocation" + *
+ * + *

By default, binding errors get resolved through the {@link BindingErrorProcessor} + * strategy, processing for missing fields and property access exceptions: see the + * {@link #setBindingErrorProcessor} method. You can override the default strategy + * if needed, for example to generate different error codes. + * + *

Custom validation errors can be added afterwards. You will typically want to resolve + * such error codes into proper user-visible error messages; this can be achieved through + * resolving each error via a {@link org.springframework.context.MessageSource}, which is + * able to resolve an {@link ObjectError}/{@link FieldError} through its + * {@link org.springframework.context.MessageSource#getMessage(org.springframework.context.MessageSourceResolvable, java.util.Locale)} + * method. The list of message codes can be customized through the {@link MessageCodesResolver} + * strategy: see the {@link #setMessageCodesResolver} method. {@link DefaultMessageCodesResolver}'s + * javadoc states details on the default resolution rules. + * + *

This generic data binder can be used in any kind of environment. + * It is typically used by Spring web MVC controllers, via the web-specific + * subclasses {@link org.springframework.web.bind.ServletRequestDataBinder} + * and {@link org.springframework.web.portlet.bind.PortletRequestDataBinder}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see #setAllowedFields + * @see #setRequiredFields + * @see #registerCustomEditor + * @see #setMessageCodesResolver + * @see #setBindingErrorProcessor + * @see #bind + * @see #getBindingResult + * @see DefaultMessageCodesResolver + * @see DefaultBindingErrorProcessor + * @see org.springframework.context.MessageSource + * @see org.springframework.web.bind.ServletRequestDataBinder + */ +public class DataBinder implements PropertyEditorRegistry, TypeConverter { + + /** Default object name used for binding: "target" */ + public static final String DEFAULT_OBJECT_NAME = "target"; + + + /** + * We'll create a lot of DataBinder instances: Let's use a static logger. + */ + protected static final Log logger = LogFactory.getLog(DataBinder.class); + + private final Object target; + + private final String objectName; + + private AbstractPropertyBindingResult bindingResult; + + private SimpleTypeConverter typeConverter; + + private BindException bindException; + + private boolean ignoreUnknownFields = true; + + private boolean ignoreInvalidFields = false; + + private String[] allowedFields; + + private String[] disallowedFields; + + private String[] requiredFields; + + private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); + + + /** + * Create a new DataBinder instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public DataBinder(Object target) { + this(target, DEFAULT_OBJECT_NAME); + } + + /** + * Create a new DataBinder instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public DataBinder(Object target, String objectName) { + this.target = target; + this.objectName = objectName; + } + + + /** + * Return the wrapped target object. + */ + public Object getTarget() { + return this.target; + } + + /** + * Return the name of the bound object. + */ + public String getObjectName() { + return this.objectName; + } + + /** + * Initialize standard JavaBean property access for this DataBinder. + *

This is the default; an explicit call just leads to eager initialization. + * @see #initDirectFieldAccess() + */ + public void initBeanPropertyAccess() { + Assert.isNull(this.bindingResult, + "DataBinder is already initialized - call initBeanPropertyAccess before any other configuration methods"); + this.bindingResult = new BeanPropertyBindingResult(getTarget(), getObjectName()); + } + + /** + * Initialize direct field access for this DataBinder, + * as alternative to the default bean property access. + * @see #initBeanPropertyAccess() + */ + public void initDirectFieldAccess() { + Assert.isNull(this.bindingResult, + "DataBinder is already initialized - call initDirectFieldAccess before any other configuration methods"); + this.bindingResult = new DirectFieldBindingResult(getTarget(), getObjectName()); + } + + /** + * Return the internal BindingResult held by this DataBinder, + * as AbstractPropertyBindingResult. + */ + protected AbstractPropertyBindingResult getInternalBindingResult() { + if (this.bindingResult == null) { + initBeanPropertyAccess(); + } + return this.bindingResult; + } + + /** + * Return the underlying PropertyAccessor of this binder's BindingResult. + */ + protected ConfigurablePropertyAccessor getPropertyAccessor() { + return getInternalBindingResult().getPropertyAccessor(); + } + + /** + * Return this binder's underlying SimpleTypeConverter. + */ + protected SimpleTypeConverter getSimpleTypeConverter() { + if (this.typeConverter == null) { + this.typeConverter = new SimpleTypeConverter(); + } + return this.typeConverter; + } + + /** + * Return the underlying TypeConverter of this binder's BindingResult. + */ + protected PropertyEditorRegistry getPropertyEditorRegistry() { + if (getTarget() != null) { + return getInternalBindingResult().getPropertyAccessor(); + } + else { + return getSimpleTypeConverter(); + } + } + + /** + * Return the underlying TypeConverter of this binder's BindingResult. + */ + protected TypeConverter getTypeConverter() { + if (getTarget() != null) { + return getInternalBindingResult().getPropertyAccessor(); + } + else { + return getSimpleTypeConverter(); + } + } + + /** + * Return the BindingResult instance created by this DataBinder. + * This allows for convenient access to the binding results after + * a bind operation. + * @return the BindingResult instance, to be treated as BindingResult + * or as Errors instance (Errors is a super-interface of BindingResult) + * @see Errors + * @see #bind + */ + public BindingResult getBindingResult() { + return getInternalBindingResult(); + } + + /** + * Return the Errors instance for this data binder. + * @return the Errors instance, to be treated as Errors or as BindException + * @deprecated in favor of {@link #getBindingResult()}. + * Use the {@link BindException#BindException(BindingResult)} constructor + * to create a BindException instance if still needed. + * @see #getBindingResult() + */ + public BindException getErrors() { + if (this.bindException == null) { + this.bindException = new BindException(getBindingResult()); + } + return this.bindException; + } + + + /** + * Set whether to ignore unknown fields, that is, whether to ignore bind + * parameters that do not have corresponding fields in the target object. + *

Default is "true". Turn this off to enforce that all bind parameters + * must have a matching field in the target object. + *

Note that this setting only applies to binding operations + * on this DataBinder, not to retrieving values via its + * {@link #getBindingResult() BindingResult}. + * @see #bind + */ + public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { + this.ignoreUnknownFields = ignoreUnknownFields; + } + + /** + * Return whether to ignore unknown fields when binding. + */ + public boolean isIgnoreUnknownFields() { + return this.ignoreUnknownFields; + } + + /** + * Set whether to ignore invalid fields, that is, whether to ignore bind + * parameters that have corresponding fields in the target object which are + * not accessible (for example because of null values in the nested path). + *

Default is "false". Turn this on to ignore bind parameters for + * nested objects in non-existing parts of the target object graph. + *

Note that this setting only applies to binding operations + * on this DataBinder, not to retrieving values via its + * {@link #getBindingResult() BindingResult}. + * @see #bind + */ + public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { + this.ignoreInvalidFields = ignoreInvalidFields; + } + + /** + * Return whether to ignore invalid fields when binding. + */ + public boolean isIgnoreInvalidFields() { + return this.ignoreInvalidFields; + } + + /** + * Register fields that should be allowed for binding. Default is all + * fields. Restrict this for example to avoid unwanted modifications + * by malicious users when binding HTTP request parameters. + *

Supports "xxx*", "*xxx" and "*xxx*" patterns. More sophisticated matching + * can be implemented by overriding the isAllowed method. + *

Alternatively, specify a list of disallowed fields. + * @param allowedFields array of field names + * @see #setDisallowedFields + * @see #isAllowed(String) + * @see org.springframework.web.bind.ServletRequestDataBinder + */ + public void setAllowedFields(String[] allowedFields) { + this.allowedFields = PropertyAccessorUtils.canonicalPropertyNames(allowedFields); + } + + /** + * Return the fields that should be allowed for binding. + * @return array of field names + */ + public String[] getAllowedFields() { + return this.allowedFields; + } + + /** + * Register fields that should not be allowed for binding. Default is none. + * Mark fields as disallowed for example to avoid unwanted modifications + * by malicious users when binding HTTP request parameters. + *

Supports "xxx*", "*xxx" and "*xxx*" patterns. More sophisticated matching + * can be implemented by overriding the isAllowed method. + *

Alternatively, specify a list of allowed fields. + * @param disallowedFields array of field names + * @see #setAllowedFields + * @see #isAllowed(String) + * @see org.springframework.web.bind.ServletRequestDataBinder + */ + public void setDisallowedFields(String[] disallowedFields) { + this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields); + } + + /** + * Return the fields that should not be allowed for binding. + * @return array of field names + */ + public String[] getDisallowedFields() { + return this.disallowedFields; + } + + /** + * Register fields that are required for each binding process. + *

If one of the specified fields is not contained in the list of + * incoming property values, a corresponding "missing field" error + * will be created, with error code "required" (by the default + * binding error processor). + * @param requiredFields array of field names + * @see #setBindingErrorProcessor + * @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE + */ + public void setRequiredFields(String[] requiredFields) { + this.requiredFields = PropertyAccessorUtils.canonicalPropertyNames(requiredFields); + if (logger.isDebugEnabled()) { + logger.debug("DataBinder requires binding of required fields [" + + StringUtils.arrayToCommaDelimitedString(requiredFields) + "]"); + } + } + + /** + * Return the fields that are required for each binding process. + * @return array of field names + */ + public String[] getRequiredFields() { + return this.requiredFields; + } + + /** + * Set whether to extract the old field value when applying a + * property editor to a new value for a field. + *

Default is "true", exposing previous field values to custom editors. + * Turn this to "false" to avoid side effects caused by getters. + */ + public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { + getPropertyAccessor().setExtractOldValueForEditor(extractOldValueForEditor); + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Applies the given strategy to the underlying errors holder. + *

Default is a DefaultMessageCodesResolver. + * @see BeanPropertyBindingResult#setMessageCodesResolver + * @see DefaultMessageCodesResolver + */ + public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + getInternalBindingResult().setMessageCodesResolver(messageCodesResolver); + } + + /** + * Set the strategy to use for processing binding errors, that is, + * required field errors and PropertyAccessExceptions. + *

Default is a DefaultBindingErrorProcessor. + * @see DefaultBindingErrorProcessor + */ + public void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) { + this.bindingErrorProcessor = bindingErrorProcessor; + } + + /** + * Return the strategy for processing binding errors. + */ + public BindingErrorProcessor getBindingErrorProcessor() { + return this.bindingErrorProcessor; + } + + + //--------------------------------------------------------------------- + // Implementation of PropertyEditorRegistry/TypeConverter interface + //--------------------------------------------------------------------- + + public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { + getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor); + } + + public void registerCustomEditor(Class requiredType, String field, PropertyEditor propertyEditor) { + getPropertyEditorRegistry().registerCustomEditor(requiredType, field, propertyEditor); + } + + public PropertyEditor findCustomEditor(Class requiredType, String propertyPath) { + return getPropertyEditorRegistry().findCustomEditor(requiredType, propertyPath); + } + + public Object convertIfNecessary(Object value, Class requiredType) throws TypeMismatchException { + return getTypeConverter().convertIfNecessary(value, requiredType); + } + + public Object convertIfNecessary( + Object value, Class requiredType, MethodParameter methodParam) throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); + } + + + /** + * Bind the given property values to this binder's target. + *

This call can create field errors, representing basic binding + * errors like a required field (code "required"), or type mismatch + * between value and bean property (code "typeMismatch"). + *

Note that the given PropertyValues should be a throwaway instance: + * For efficiency, it will be modified to just contain allowed fields if it + * implements the MutablePropertyValues interface; else, an internal mutable + * copy will be created for this purpose. Pass in a copy of the PropertyValues + * if you want your original instance to stay unmodified in any case. + * @param pvs property values to bind + * @see #doBind(org.springframework.beans.MutablePropertyValues) + */ + public void bind(PropertyValues pvs) { + MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ? + (MutablePropertyValues) pvs : new MutablePropertyValues(pvs); + doBind(mpvs); + } + + /** + * Actual implementation of the binding process, working with the + * passed-in MutablePropertyValues instance. + * @param mpvs the property values to bind, + * as MutablePropertyValues instance + * @see #checkAllowedFields + * @see #checkRequiredFields + * @see #applyPropertyValues + */ + protected void doBind(MutablePropertyValues mpvs) { + checkAllowedFields(mpvs); + checkRequiredFields(mpvs); + applyPropertyValues(mpvs); + } + + /** + * Check the given property values against the allowed fields, + * removing values for fields that are not allowed. + * @param mpvs the property values to be bound (can be modified) + * @see #getAllowedFields + * @see #isAllowed(String) + */ + protected void checkAllowedFields(MutablePropertyValues mpvs) { + PropertyValue[] pvs = mpvs.getPropertyValues(); + for (int i = 0; i < pvs.length; i++) { + PropertyValue pv = pvs[i]; + String field = PropertyAccessorUtils.canonicalPropertyName(pv.getName()); + if (!isAllowed(field)) { + mpvs.removePropertyValue(pv); + getBindingResult().recordSuppressedField(field); + if (logger.isDebugEnabled()) { + logger.debug("Field [" + field + "] has been removed from PropertyValues " + + "and will not be bound, because it has not been found in the list of allowed fields"); + } + } + } + } + + /** + * Return if the given field is allowed for binding. + * Invoked for each passed-in property value. + *

The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, + * as well as direct equality, in the specified lists of allowed fields and + * disallowed fields. A field matching a disallowed pattern will not be accepted + * even if it also happens to match a pattern in the allowed list. + *

Can be overridden in subclasses. + * @param field the field to check + * @return if the field is allowed + * @see #setAllowedFields + * @see #setDisallowedFields + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isAllowed(String field) { + String[] allowed = getAllowedFields(); + String[] disallowed = getDisallowedFields(); + return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) && + (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field))); + } + + /** + * Check the given property values against the required fields, + * generating missing field errors where appropriate. + * @param mpvs the property values to be bound (can be modified) + * @see #getRequiredFields + * @see #getBindingErrorProcessor + * @see BindingErrorProcessor#processMissingFieldError + */ + protected void checkRequiredFields(MutablePropertyValues mpvs) { + String[] requiredFields = getRequiredFields(); + if (!ObjectUtils.isEmpty(requiredFields)) { + Map propertyValues = new HashMap(); + PropertyValue[] pvs = mpvs.getPropertyValues(); + for (int i = 0; i < pvs.length; i++) { + PropertyValue pv = pvs[i]; + String canonicalName = PropertyAccessorUtils.canonicalPropertyName(pv.getName()); + propertyValues.put(canonicalName, pv); + } + for (int i = 0; i < requiredFields.length; i++) { + String field = requiredFields[i]; + PropertyValue pv = (PropertyValue) propertyValues.get(field); + if (pv == null || pv.getValue() == null || + (pv.getValue() instanceof String && !StringUtils.hasText((String) pv.getValue()))) { + // Use bind error processor to create FieldError. + getBindingErrorProcessor().processMissingFieldError(field, getInternalBindingResult()); + // Remove property from property values to bind: + // It has already caused a field error with a rejected value. + if (pv != null) { + mpvs.removePropertyValue(pv); + propertyValues.remove(field); + } + } + } + } + } + + /** + * Apply given property values to the target object. + *

Default implementation applies all of the supplied property + * values as bean property values. By default, unknown fields will + * be ignored. + * @param mpvs the property values to be bound (can be modified) + * @see #getTarget + * @see #getPropertyAccessor + * @see #isIgnoreUnknownFields + * @see #getBindingErrorProcessor + * @see BindingErrorProcessor#processPropertyAccessException + */ + protected void applyPropertyValues(MutablePropertyValues mpvs) { + try { + // Bind request parameters onto target object. + getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); + } + catch (PropertyBatchUpdateException ex) { + // Use bind error processor to create FieldErrors. + PropertyAccessException[] exs = ex.getPropertyAccessExceptions(); + for (int i = 0; i < exs.length; i++) { + getBindingErrorProcessor().processPropertyAccessException(exs[i], getInternalBindingResult()); + } + } + } + + + /** + * Close this DataBinder, which may result in throwing + * a BindException if it encountered any errors. + * @return the model Map, containing target object and Errors instance + * @throws BindException if there were any errors in the bind operation + * @see BindingResult#getModel() + */ + public Map close() throws BindException { + if (getBindingResult().hasErrors()) { + throw new BindException(getBindingResult()); + } + return getBindingResult().getModel(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java b/org.springframework.context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java new file mode 100644 index 00000000000..ecda92a9b32 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java @@ -0,0 +1,90 @@ +/* + * 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.validation; + +import org.springframework.beans.PropertyAccessException; +import org.springframework.context.support.DefaultMessageSourceResolvable; + +/** + * Default {@link BindingErrorProcessor} implementation. + * + *

Uses the "required" error code and the field name to resolve message codes + * for a missing field error. + * + *

Creates a FieldError for each PropertyAccessException + * given, using the PropertyAccessException's error code ("typeMismatch", + * "methodInvocation") for resolving message codes. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @since 1.2 + * @see #MISSING_FIELD_ERROR_CODE + * @see DataBinder#setBindingErrorProcessor + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + * @see org.springframework.beans.PropertyAccessException#getErrorCode + * @see org.springframework.beans.TypeMismatchException#ERROR_CODE + * @see org.springframework.beans.MethodInvocationException#ERROR_CODE + */ +public class DefaultBindingErrorProcessor implements BindingErrorProcessor { + + /** + * Error code that a missing field error (i.e. a required field not + * found in the list of property values) will be registered with: + * "required". + */ + public static final String MISSING_FIELD_ERROR_CODE = "required"; + + + public void processMissingFieldError(String missingField, BindingResult bindingResult) { + // Create field error with code "required". + String fixedField = bindingResult.getNestedPath() + missingField; + String[] codes = bindingResult.resolveMessageCodes(MISSING_FIELD_ERROR_CODE, missingField); + Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), fixedField); + bindingResult.addError(new FieldError( + bindingResult.getObjectName(), fixedField, "", true, + codes, arguments, "Field '" + fixedField + "' is required")); + } + + public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) { + // Create field error with the exceptions's code, e.g. "typeMismatch". + String field = ex.getPropertyChangeEvent().getPropertyName(); + Object value = ex.getPropertyChangeEvent().getNewValue(); + String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field); + Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field); + bindingResult.addError(new FieldError( + bindingResult.getObjectName(), field, value, true, + codes, arguments, ex.getLocalizedMessage())); + } + + /** + * Return FieldError arguments for a binding error on the given field. + * Invoked for each missing required fields and each type mismatch. + *

Default implementation returns a DefaultMessageSourceResolvable + * with "objectName.field" and "field" as codes. + * @param field the field that caused the binding error + * @return the Object array that represents the FieldError arguments + * @see org.springframework.validation.FieldError#getArguments + * @see org.springframework.context.support.DefaultMessageSourceResolvable + */ + protected Object[] getArgumentsForBindError(String objectName, String field) { + String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; + String defaultMessage = field; + return new Object[] {new DefaultMessageSourceResolvable(codes, defaultMessage)}; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java b/org.springframework.context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java new file mode 100644 index 00000000000..e7df7aeb0c5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java @@ -0,0 +1,178 @@ +/* + * 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.validation; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link MessageCodesResolver} interface. + * + *

Will create two message codes for an object error, in the following order: + *

    + *
  • 1.: code + "." + object name + *
  • 2.: code + *
+ * + *

Will create four message codes for a field specification, in the following order: + *

    + *
  • 1.: code + "." + object name + "." + field + *
  • 2.: code + "." + field + *
  • 3.: code + "." + field type + *
  • 4.: code + *
+ * + *

For example, in case of code "typeMismatch", object name "user", field "age": + *

    + *
  • 1. try "typeMismatch.user.age" + *
  • 2. try "typeMismatch.age" + *
  • 3. try "typeMismatch.int" + *
  • 4. try "typeMismatch" + *
+ * + *

This resolution algorithm thus can be leveraged for example to show + * specific messages for binding errors like "required" and "typeMismatch": + *

    + *
  • at the object + field level ("age" field, but only on "user"); + *
  • at the field level (all "age" fields, no matter which object name); + *
  • or at the general level (all fields, on any object). + *
+ * + *

In case of array, {@link List} or {@link java.util.Map} properties, + * both codes for specific elements and for the whole collection are + * generated. Assuming a field "name" of an array "groups" in object "user": + *

    + *
  • 1. try "typeMismatch.user.groups[0].name" + *
  • 2. try "typeMismatch.user.groups.name" + *
  • 3. try "typeMismatch.groups[0].name" + *
  • 4. try "typeMismatch.groups.name" + *
  • 5. try "typeMismatch.name" + *
  • 6. try "typeMismatch.java.lang.String" + *
  • 7. try "typeMismatch" + *
+ * + *

In order to group all codes into a specific category within your resource bundles, + * e.g. "validation.typeMismatch.name" instead of the default "typeMismatch.name", + * consider specifying a {@link #setPrefix prefix} to be applied. + * + * @author Juergen Hoeller + * @since 1.0.1 + */ +public class DefaultMessageCodesResolver implements MessageCodesResolver, Serializable { + + /** + * The separator that this implementation uses when resolving message codes. + */ + public static final String CODE_SEPARATOR = "."; + + + private String prefix = ""; + + + /** + * Specify a prefix to be applied to any code built by this resolver. + *

Default is none. Specify, for example, "validation." to get + * error codes like "validation.typeMismatch.name". + */ + public void setPrefix(String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the prefix to be applied to any code built by this resolver. + *

Returns an empty String in case of no prefix. + */ + protected String getPrefix() { + return this.prefix; + } + + + public String[] resolveMessageCodes(String errorCode, String objectName) { + return new String[] { + postProcessMessageCode(errorCode + CODE_SEPARATOR + objectName), + postProcessMessageCode(errorCode)}; + } + + /** + * Build the code list for the given code and field: an + * object/field-specific code, a field-specific code, a plain error code. + *

Arrays, Lists and Maps are resolved both for specific elements and + * the whole collection. + *

See the {@link DefaultMessageCodesResolver class level Javadoc} for + * details on the generated codes. + * @return the list of codes + */ + public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType) { + List codeList = new ArrayList(); + List fieldList = new ArrayList(); + buildFieldList(field, fieldList); + for (Iterator it = fieldList.iterator(); it.hasNext();) { + String fieldInList = (String) it.next(); + codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + objectName + CODE_SEPARATOR + fieldInList)); + } + int dotIndex = field.lastIndexOf('.'); + if (dotIndex != -1) { + buildFieldList(field.substring(dotIndex + 1), fieldList); + } + for (Iterator it = fieldList.iterator(); it.hasNext();) { + String fieldInList = (String) it.next(); + codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + fieldInList)); + } + if (fieldType != null) { + codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + fieldType.getName())); + } + codeList.add(postProcessMessageCode(errorCode)); + return StringUtils.toStringArray(codeList); + } + + /** + * Add both keyed and non-keyed entries for the supplied field + * to the supplied field list. + */ + protected void buildFieldList(String field, List fieldList) { + fieldList.add(field); + String plainField = field; + int keyIndex = plainField.lastIndexOf('['); + while (keyIndex != -1) { + int endKeyIndex = plainField.indexOf(']', keyIndex); + if (endKeyIndex != -1) { + plainField = plainField.substring(0, keyIndex) + plainField.substring(endKeyIndex + 1); + fieldList.add(plainField); + keyIndex = plainField.lastIndexOf('['); + } + else { + keyIndex = -1; + } + } + } + + /** + * Post-process the given message code, built by this resolver. + *

The default implementation applies the specified prefix, if any. + * @param code the message code as built by this resolver + * @return the final message code to be returned + * @see #setPrefix + */ + protected String postProcessMessageCode(String code) { + return getPrefix() + code; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java new file mode 100644 index 00000000000..f4e97ca03e7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java @@ -0,0 +1,81 @@ +/* + * 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.validation; + +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.util.Assert; + +/** + * Special implementation of the Errors and BindingResult interfaces, + * supporting registration and evaluation of binding errors on value objects. + * Performs direct field access instead of going through JavaBean getters. + * + *

This implementation just supports fields in the actual target object. + * It is not able to traverse nested fields. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder#getBindingResult() + * @see DataBinder#initDirectFieldAccess() + * @see BeanPropertyBindingResult + */ +public class DirectFieldBindingResult extends AbstractPropertyBindingResult { + + private final Object target; + + private transient ConfigurablePropertyAccessor directFieldAccessor; + + + /** + * Create a new DirectFieldBindingResult instance. + * @param target the target object to bind onto + * @param objectName the name of the target object + */ + public DirectFieldBindingResult(Object target, String objectName) { + super(objectName); + this.target = target; + } + + + public final Object getTarget() { + return this.target; + } + + /** + * Returns the DirectFieldAccessor that this instance uses. + * Creates a new one if none existed before. + * @see #createDirectFieldAccessor() + */ + public final ConfigurablePropertyAccessor getPropertyAccessor() { + if (this.directFieldAccessor == null) { + this.directFieldAccessor = createDirectFieldAccessor(); + this.directFieldAccessor.setExtractOldValueForEditor(true); + } + return this.directFieldAccessor; + } + + /** + * Create a new DirectFieldAccessor for the underlying target object. + * @see #getTarget() + */ + protected ConfigurablePropertyAccessor createDirectFieldAccessor() { + Assert.state(this.target != null, "Cannot access fields on null target instance '" + getObjectName() + "'!"); + return PropertyAccessorFactory.forDirectFieldAccess(this.target); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/Errors.java b/org.springframework.context/src/main/java/org/springframework/validation/Errors.java new file mode 100644 index 00000000000..5cd482afd0c --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/Errors.java @@ -0,0 +1,301 @@ +/* + * 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.validation; + +import java.util.List; + +import org.springframework.beans.PropertyAccessor; + +/** + * Stores and exposes information about data-binding and validation + * errors for a specific object. + * + *

Field names can be properties of the target object (e.g. "name" + * when binding to a customer object), or nested fields in case of + * subobjects (e.g. "address.street"). Supports subtree navigation + * via {@link #setNestedPath(String)}: for example, an + * AddressValidator validates "address", not being aware + * that this is a subobject of customer. + * + *

Note: Errors objects are single-threaded. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setNestedPath + * @see BindException + * @see DataBinder + * @see ValidationUtils + */ +public interface Errors { + + /** + * The separator between path elements in a nested path, + * for example in "customer.name" or "customer.address.street". + *

"." = same as the + * {@link org.springframework.beans.PropertyAccessor#NESTED_PROPERTY_SEPARATOR nested property separator} + * in the beans package. + */ + String NESTED_PATH_SEPARATOR = PropertyAccessor.NESTED_PROPERTY_SEPARATOR; + + + /** + * Return the name of the bound root object. + */ + String getObjectName(); + + /** + * Allow context to be changed so that standard validators can validate + * subtrees. Reject calls prepend the given path to the field names. + *

For example, an address validator could validate the subobject + * "address" of a customer object. + * @param nestedPath nested path within this object, + * e.g. "address" (defaults to "", null is also acceptable). + * Can end with a dot: both "address" and "address." are valid. + */ + void setNestedPath(String nestedPath); + + /** + * Return the current nested path of this {@link Errors} object. + *

Returns a nested path with a dot, i.e. "address.", for easy + * building of concatenated paths. Default is an empty String. + */ + String getNestedPath(); + + /** + * Push the given sub path onto the nested path stack. + *

A {@link #popNestedPath()} call will reset the original + * nested path before the corresponding + * pushNestedPath(String) call. + *

Using the nested path stack allows to set temporary nested paths + * for subobjects without having to worry about a temporary path holder. + *

For example: current path "spouse.", pushNestedPath("child") -> + * result path "spouse.child."; popNestedPath() -> "spouse." again. + * @param subPath the sub path to push onto the nested path stack + * @see #popNestedPath + */ + void pushNestedPath(String subPath); + + /** + * Pop the former nested path from the nested path stack. + * @throws IllegalStateException if there is no former nested path on the stack + * @see #pushNestedPath + */ + void popNestedPath() throws IllegalStateException; + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + */ + void reject(String errorCode); + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + * @param defaultMessage fallback default message + */ + void reject(String errorCode, String defaultMessage); + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + * @param errorArgs error arguments, for argument binding via MessageFormat + * (can be null) + * @param defaultMessage fallback default message + */ + void reject(String errorCode, Object[] errorArgs, String defaultMessage); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

The field name may be null or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be null or empty String) + * @param errorCode error code, interpretable as a message key + * @see #getNestedPath() + */ + void rejectValue(String field, String errorCode); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

The field name may be null or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be null or empty String) + * @param errorCode error code, interpretable as a message key + * @param defaultMessage fallback default message + * @see #getNestedPath() + */ + void rejectValue(String field, String errorCode, String defaultMessage); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

The field name may be null or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be null or empty String) + * @param errorCode error code, interpretable as a message key + * @param errorArgs error arguments, for argument binding via MessageFormat + * (can be null) + * @param defaultMessage fallback default message + * @see #getNestedPath() + */ + void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage); + + /** + * Add all errors from the given Errors instance to this + * Errors instance. + *

This is a onvenience method to avoid repeated reject(..) + * calls for merging an Errors instance into another + * Errors instance. + *

Note that the passed-in Errors instance is supposed + * to refer to the same target object, or at least contain compatible errors + * that apply to the target object of this Errors instance. + * @param errors the Errors instance to merge in + */ + void addAllErrors(Errors errors); + + /** + * Return if there were any errors. + */ + boolean hasErrors(); + + /** + * Return the total number of errors. + */ + int getErrorCount(); + + /** + * Get all errors, both global and field ones. + * @return List of {@link ObjectError} instances + */ + List getAllErrors(); + + /** + * Are there any global errors? + * @return true if there are any global errors + * @see #hasFieldErrors() + */ + boolean hasGlobalErrors(); + + /** + * Return the number of global errors. + * @return the number of global errors + * @see #getFieldErrorCount() + */ + int getGlobalErrorCount(); + + /** + * Get all global errors. + * @return List of ObjectError instances + */ + List getGlobalErrors(); + + /** + * Get the first global error, if any. + * @return the global error, or null + */ + ObjectError getGlobalError(); + + /** + * Are there any field errors? + * @return true if there are any errors associated with a field + * @see #hasGlobalErrors() + */ + boolean hasFieldErrors(); + + /** + * Return the number of errors associated with a field. + * @return the number of errors associated with a field + * @see #getGlobalErrorCount() + */ + int getFieldErrorCount(); + + /** + * Get all errors associated with a field. + * @return a List of {@link FieldError} instances + */ + List getFieldErrors(); + + /** + * Get the first error associated with a field, if any. + * @return the field-specific error, or null + */ + FieldError getFieldError(); + + /** + * Are there any errors associated with the given field? + * @param field the field name + * @return true if there were any errors associated with the given field + */ + boolean hasFieldErrors(String field); + + /** + * Return the number of errors associated with the given field. + * @param field the field name + * @return the number of errors associated with the given field + */ + int getFieldErrorCount(String field); + + /** + * Get all errors associated with the given field. + *

Implementations should support not only full field names like + * "name" but also pattern matches like "na*" or "address.*". + * @param field the field name + * @return a List of {@link FieldError} instances + */ + List getFieldErrors(String field); + + /** + * Get the first error associated with the given field, if any. + * @param field the field name + * @return the field-specific error, or null + */ + FieldError getFieldError(String field); + + /** + * Return the current value of the given field, either the current + * bean property value or a rejected update from the last binding. + *

Allows for convenient access to user-specified field values, + * even if there were type mismatches. + * @param field the field name + * @return the current value of the given field + */ + Object getFieldValue(String field); + + /** + * Return the type of a given field. + *

Implementations should be able to determine the type even + * when the field value is null, for example from some + * associated descriptor. + * @param field the field name + * @return the type of the field, or null if not determinable + */ + Class getFieldType(String field); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/FieldError.java b/org.springframework.context/src/main/java/org/springframework/validation/FieldError.java new file mode 100644 index 00000000000..be15e0afa53 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/FieldError.java @@ -0,0 +1,125 @@ +/* + * 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.validation; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Encapsulates a field error, that is, a reason for rejecting a specific + * field value. + * + *

See the {@link DefaultMessageCodesResolver} javadoc for details on + * how a message code list is built for a FieldError. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 10.03.2003 + * @see DefaultMessageCodesResolver + */ +public class FieldError extends ObjectError { + + private final String field; + + private final Object rejectedValue; + + private final boolean bindingFailure; + + + /** + * Create a new FieldError instance. + * @param objectName the name of the affected object + * @param field the affected field of the object + * @param defaultMessage the default message to be used to resolve this message + */ + public FieldError(String objectName, String field, String defaultMessage) { + this(objectName, field, null, false, null, null, defaultMessage); + } + + /** + * Create a new FieldError instance. + * @param objectName the name of the affected object + * @param field the affected field of the object + * @param rejectedValue the rejected field value + * @param bindingFailure whether this error represents a binding failure + * (like a type mismatch); else, it is a validation failure + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public FieldError( + String objectName, String field, Object rejectedValue, boolean bindingFailure, + String[] codes, Object[] arguments, String defaultMessage) { + + super(objectName, codes, arguments, defaultMessage); + Assert.notNull(field, "Field must not be null"); + this.field = field; + this.rejectedValue = rejectedValue; + this.bindingFailure = bindingFailure; + } + + + /** + * Return the affected field of the object. + */ + public String getField() { + return this.field; + } + + /** + * Return the rejected field value. + */ + public Object getRejectedValue() { + return this.rejectedValue; + } + + /** + * Return whether this error represents a binding failure + * (like a type mismatch); otherwise it is a validation failure. + */ + public boolean isBindingFailure() { + return this.bindingFailure; + } + + + public String toString() { + return "Field error in object '" + getObjectName() + "' on field '" + this.field + + "': rejected value [" + this.rejectedValue + "]; " + resolvableToString(); + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!super.equals(other)) { + return false; + } + FieldError otherError = (FieldError) other; + return getField().equals(otherError.getField()) && + ObjectUtils.nullSafeEquals(getRejectedValue(), otherError.getRejectedValue()) && + isBindingFailure() == otherError.isBindingFailure(); + } + + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 29 * hashCode + getField().hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getRejectedValue()); + hashCode = 29 * hashCode + (isBindingFailure() ? 1 : 0); + return hashCode; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/MapBindingResult.java b/org.springframework.context/src/main/java/org/springframework/validation/MapBindingResult.java new file mode 100644 index 00000000000..8e5664ba248 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/MapBindingResult.java @@ -0,0 +1,65 @@ +/* + * 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.validation; + +import java.io.Serializable; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Map-based implementation of the BindingResult interface, + * supporting registration and evaluation of binding errors on + * Map attributes. + * + *

Can be used as errors holder for custom binding onto a + * Map, for example when invoking a Validator for a Map object. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.Map + */ +public class MapBindingResult extends AbstractBindingResult implements Serializable { + + private final Map target; + + + /** + * Create a new MapBindingResult instance. + * @param target the target Map to bind onto + * @param objectName the name of the target object + */ + public MapBindingResult(Map target, String objectName) { + super(objectName); + Assert.notNull(target, "Target Map must not be null"); + this.target = target; + } + + + public final Map getTargetMap() { + return this.target; + } + + public final Object getTarget() { + return this.target; + } + + protected Object getActualFieldValue(String field) { + return this.target.get(field); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/MessageCodesResolver.java b/org.springframework.context/src/main/java/org/springframework/validation/MessageCodesResolver.java new file mode 100644 index 00000000000..cd6b4d7188b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/MessageCodesResolver.java @@ -0,0 +1,55 @@ +/* + * 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.validation; + +/** + * Strategy interface for building message codes from validation error codes. + * Used by DataBinder to build the codes list for ObjectErrors and FieldErrors. + * + *

The resulting message codes correspond to the codes of a + * MessageSourceResolvable (as implemented by ObjectError and FieldError). + * + * @author Juergen Hoeller + * @since 1.0.1 + * @see DataBinder#setMessageCodesResolver + * @see ObjectError + * @see FieldError + * @see org.springframework.context.MessageSourceResolvable#getCodes() + */ +public interface MessageCodesResolver { + + /** + * Build message codes for the given error code and object name. + * Used for building the codes list of an ObjectError. + * @param errorCode the error code used for rejecting the object + * @param objectName the name of the object + * @return the message codes to use + */ + String[] resolveMessageCodes(String errorCode, String objectName); + + /** + * Build message codes for the given error code and field specification. + * Used for building the codes list of an FieldError. + * @param errorCode the error code used for rejecting the value + * @param objectName the name of the object + * @param field the field name + * @param fieldType the field type (may be null if not determinable) + * @return the message codes to use + */ + String[] resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/ObjectError.java b/org.springframework.context/src/main/java/org/springframework/validation/ObjectError.java new file mode 100644 index 00000000000..20d74f708ff --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/ObjectError.java @@ -0,0 +1,89 @@ +/* + * 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.validation; + +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.util.Assert; + +/** + * Encapsulates an object error, that is, a global reason for rejecting + * an object. + * + *

See the {@link DefaultMessageCodesResolver} javadoc for details on + * how a message code list is built for an ObjectError. + * + * @author Juergen Hoeller + * @see FieldError + * @see DefaultMessageCodesResolver + * @since 10.03.2003 + */ +public class ObjectError extends DefaultMessageSourceResolvable { + + private final String objectName; + + + /** + * Create a new instance of the ObjectError class. + * @param objectName the name of the affected object + * @param defaultMessage the default message to be used to resolve this message + */ + public ObjectError(String objectName, String defaultMessage) { + this(objectName, null, null, defaultMessage); + } + + /** + * Create a new instance of the ObjectError class. + * @param objectName the name of the affected object + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public ObjectError(String objectName, String[] codes, Object[] arguments, String defaultMessage) { + super(codes, arguments, defaultMessage); + Assert.notNull(objectName, "Object name must not be null"); + this.objectName = objectName; + } + + + /** + * Return the name of the affected object. + */ + public String getObjectName() { + return this.objectName; + } + + + public String toString() { + return "Error in object '" + this.objectName + "': " + resolvableToString(); + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(getClass().equals(other.getClass())) || !super.equals(other)) { + return false; + } + ObjectError otherError = (ObjectError) other; + return getObjectName().equals(otherError.getObjectName()); + } + + public int hashCode() { + return super.hashCode() * 29 + getObjectName().hashCode(); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/ValidationUtils.java b/org.springframework.context/src/main/java/org/springframework/validation/ValidationUtils.java new file mode 100644 index 00000000000..63e4a998601 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/ValidationUtils.java @@ -0,0 +1,230 @@ +/* + * 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.validation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Utility class offering convenient methods for invoking a {@link Validator} + * and for rejecting empty fields. + * + *

Checks for an empty field in Validator implementations can become + * one-liners when using {@link #rejectIfEmpty} or {@link #rejectIfEmptyOrWhitespace}. + * + * @author Juergen Hoeller + * @author Dmitriy Kopylenko + * @since 06.05.2003 + * @see Validator + * @see Errors + */ +public abstract class ValidationUtils { + + private static Log logger = LogFactory.getLog(ValidationUtils.class); + + + /** + * Invoke the given {@link Validator} for the supplied object and + * {@link Errors} instance. + * @param validator the Validator to be invoked (must not be null) + * @param obj the object to bind the parameters to + * @param errors the {@link Errors} instance that should store the errors (must not be null) + * @throws IllegalArgumentException if either of the Validator or Errors arguments is null; + * or if the supplied Validator does not {@link Validator#supports(Class) support} + * the validation of the supplied object's type + */ + public static void invokeValidator(Validator validator, Object obj, Errors errors) { + Assert.notNull(validator, "Validator must not be null"); + Assert.notNull(errors, "Errors object must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Invoking validator [" + validator + "]"); + } + if (obj != null && !validator.supports(obj.getClass())) { + throw new IllegalArgumentException( + "Validator [" + validator.getClass() + "] does not support [" + obj.getClass() + "]"); + } + validator.validate(obj, errors); + if (logger.isDebugEnabled()) { + if (errors.hasErrors()) { + logger.debug("Validator found " + errors.getErrorCount() + " errors"); + } + else { + logger.debug("Validator found no errors"); + } + } + } + + + /** + * Reject the given field with the given error code if the value is empty. + *

An 'empty' value in this context means either null or + * the empty string "". + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode) { + rejectIfEmpty(errors, field, errorCode, null, null); + } + + /** + * Reject the given field with the given error code and default message + * if the value is empty. + *

An 'empty' value in this context means either null or + * the empty string "". + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode error code, interpretable as message key + * @param defaultMessage fallback default message + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode, String defaultMessage) { + rejectIfEmpty(errors, field, errorCode, null, defaultMessage); + } + + /** + * Reject the given field with the given error codea nd error arguments + * if the value is empty. + *

An 'empty' value in this context means either null or + * the empty string "". + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be null) + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs) { + rejectIfEmpty(errors, field, errorCode, errorArgs, null); + } + + /** + * Reject the given field with the given error code, error arguments + * and default message if the value is empty. + *

An 'empty' value in this context means either null or + * the empty string "". + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be null) + * @param defaultMessage fallback default message + */ + public static void rejectIfEmpty( + Errors errors, String field, String errorCode, Object[] errorArgs, String defaultMessage) { + + Assert.notNull(errors, "Errors object must not be null"); + Object value = errors.getFieldValue(field); + if (value == null || !StringUtils.hasLength(value.toString())) { + errors.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + } + + /** + * Reject the given field with the given error code if the value is empty + * or just contains whitespace. + *

An 'empty' value in this context means either null, + * the empty string "", or consisting wholly of whitespace. + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + */ + public static void rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode) { + rejectIfEmptyOrWhitespace(errors, field, errorCode, null, null); + } + + /** + * Reject the given field with the given error code and default message + * if the value is empty or just contains whitespace. + *

An 'empty' value in this context means either null, + * the empty string "", or consisting wholly of whitespace. + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param defaultMessage fallback default message + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, String defaultMessage) { + + rejectIfEmptyOrWhitespace(errors, field, errorCode, null, defaultMessage); + } + + /** + * Reject the given field with the given error code and error arguments + * if the value is empty or just contains whitespace. + *

An 'empty' value in this context means either null, + * the empty string "", or consisting wholly of whitespace. + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be null) + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, Object[] errorArgs) { + + rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null); + } + + /** + * Reject the given field with the given error code, error arguments + * and default message if the value is empty or just contains whitespace. + *

An 'empty' value in this context means either null, + * the empty string "", or consisting wholly of whitespace. + *

The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the Errors instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be null) + * @param defaultMessage fallback default message + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, Object[] errorArgs, String defaultMessage) { + + Assert.notNull(errors, "Errors object must not be null"); + Object value = errors.getFieldValue(field); + if (value == null ||!StringUtils.hasText(value.toString())) { + errors.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/Validator.java b/org.springframework.context/src/main/java/org/springframework/validation/Validator.java new file mode 100644 index 00000000000..563d55e39a4 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/Validator.java @@ -0,0 +1,94 @@ +/* + * 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.validation; + +/** + * A validator for application-specific objects. + * + *

This interface is totally divorced from any infrastructure + * or context; that is to say it is not coupled to validating + * only objects in the web tier, the data-access tier, or the + * whatever-tier. As such it is amenable to being used in any layer + * of an application, and supports the encapsulation of validation + * logic as first-class citizens in their own right. + * + *

Find below a simple but complete Validator + * implementation, which validates that the various {@link String} + * properties of a UserLogin instance are not empty + * (that is they are not null and do not consist + * wholly of whitespace), and that any password that is present is + * at least 'MINIMUM_PASSWORD_LENGTH' characters in length. + * + *

 public class UserLoginValidator implements Validator {
+ * 
+ *    private static final int MINIMUM_PASSWORD_LENGTH = 6;
+ * 
+ *    public boolean supports(Class clazz) {
+ *       return UserLogin.class.isAssignableFrom(clazz);
+ *    }
+ * 
+ *    public void validate(Object target, Errors errors) {
+ *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
+ *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
+ *       UserLogin login = (UserLogin) target;
+ *       if (login.getPassword() != null
+ *             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
+ *          errors.rejectValue("password", "field.min.length",
+ *                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
+ *                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
+ *       }
+ *    }
+ * }
+ * + *

See also the Spring reference manual for a fuller discussion of + * the Validator interface and it's role in a enterprise + * application. + * + * @author Rod Johnson + * @see Errors + * @see ValidationUtils + */ +public interface Validator { + + /** + * Can this {@link Validator} {@link #validate(Object, Errors) validate} + * instances of the supplied clazz? + *

This method is typically implemented like so: + *

return Foo.class.isAssignableFrom(clazz);
+ * (Where Foo is the class (or superclass) of the actual + * object instance that is to be {@link #validate(Object, Errors) validated}.) + * @param clazz the {@link Class} that this {@link Validator} is + * being asked if it can {@link #validate(Object, Errors) validate} + * @return true if this {@link Validator} can indeed + * {@link #validate(Object, Errors) validate} instances of the + * supplied clazz + */ + boolean supports(Class clazz); + + /** + * Validate the supplied target object, which must be + * of a {@link Class} for which the {@link #supports(Class)} method + * typically has (or would) return true. + *

The supplied {@link Errors errors} instance can be used to report + * any resulting validation errors. + * @param target the object that is to be validated (can be null) + * @param errors contextual state about the validation process (never null) + * @see ValidationUtils + */ + void validate(Object target, Errors errors); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/package.html b/org.springframework.context/src/main/java/org/springframework/validation/package.html new file mode 100644 index 00000000000..2d4decc6778 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/package.html @@ -0,0 +1,8 @@ + + + +Provides data binding and validation functionality, +for usage in business and/or UI layers. + + + diff --git a/org.springframework.context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java b/org.springframework.context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java new file mode 100644 index 00000000000..dca2de852a6 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java @@ -0,0 +1,64 @@ +/* + * 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.validation.support; + +import java.util.Map; +import java.util.Set; + +import org.springframework.ui.ExtendedModelMap; +import org.springframework.validation.BindingResult; + +/** + * Subclass of {@link org.springframework.ui.ExtendedModelMap} that + * automatically removes a {@link org.springframework.validation.BindingResult} + * object if the corresponding target attribute gets replaced. + * + *

Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} + * and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see + */ +public class BindingAwareModelMap extends ExtendedModelMap { + + public Object put(Object key, Object value) { + removeBindingResultIfNecessary(key, value); + return super.put(key, value); + } + + public void putAll(Map map) { + for (Map.Entry entry : (Set) map.entrySet()) { + removeBindingResultIfNecessary(entry.getKey(), entry.getValue()); + } + super.putAll(map); + } + + private void removeBindingResultIfNecessary(Object key, Object value) { + if (key instanceof String) { + String attributeName = (String) key; + if (!attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) { + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attributeName; + BindingResult bindingResult = (BindingResult) get(bindingResultKey); + if (bindingResult != null && bindingResult.getTarget() != value) { + remove(bindingResultKey); + } + } + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/validation/support/package.html b/org.springframework.context/src/main/java/org/springframework/validation/support/package.html new file mode 100644 index 00000000000..63de74d7108 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/validation/support/package.html @@ -0,0 +1,7 @@ + + + +Support classes for handling validation results. + + +