001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jms.listener.endpoint;
018
019import java.util.Map;
020
021import javax.jms.JMSException;
022import javax.jms.Queue;
023import javax.jms.Session;
024import javax.jms.Topic;
025import javax.resource.spi.ActivationSpec;
026import javax.resource.spi.ResourceAdapter;
027
028import org.springframework.beans.BeanUtils;
029import org.springframework.beans.BeanWrapper;
030import org.springframework.beans.PropertyAccessorFactory;
031import org.springframework.jms.support.destination.DestinationResolutionException;
032import org.springframework.jms.support.destination.DestinationResolver;
033import org.springframework.lang.Nullable;
034
035/**
036 * Standard implementation of the {@link JmsActivationSpecFactory} interface.
037 * Supports the standard JMS properties as defined by the JMS 1.5 specification
038 * (Appendix B); ignores Spring's "maxConcurrency" and "prefetchSize" settings.
039 *
040 * <p>The 'activationSpecClass' property is required, explicitly defining
041 * the fully-qualified class name of the provider's ActivationSpec class
042 * (e.g. "org.apache.activemq.ra.ActiveMQActivationSpec").
043 *
044 * <p>Check out {@link DefaultJmsActivationSpecFactory} for an extended variant
045 * of this class, supporting some further default conventions beyond the plain
046 * JMS 1.5 specification.
047 *
048 * @author Juergen Hoeller
049 * @since 2.5
050 * @see #setActivationSpecClass
051 * @see DefaultJmsActivationSpecFactory
052 */
053public class StandardJmsActivationSpecFactory implements JmsActivationSpecFactory {
054
055        @Nullable
056        private Class<?> activationSpecClass;
057
058        @Nullable
059        private Map<String, String> defaultProperties;
060
061        @Nullable
062        private DestinationResolver destinationResolver;
063
064
065        /**
066         * Specify the fully-qualified ActivationSpec class name for the target
067         * provider (e.g. "org.apache.activemq.ra.ActiveMQActivationSpec").
068         */
069        public void setActivationSpecClass(Class<?> activationSpecClass) {
070                this.activationSpecClass = activationSpecClass;
071        }
072
073        /**
074         * Specify custom default properties, with String keys and String values.
075         * <p>Applied to each ActivationSpec object before it gets populated with
076         * listener-specific settings. Allows for configuring vendor-specific properties
077         * beyond the Spring-defined settings in {@link JmsActivationSpecConfig}.
078         */
079        public void setDefaultProperties(Map<String, String> defaultProperties) {
080                this.defaultProperties = defaultProperties;
081        }
082
083        /**
084         * Set the DestinationResolver to use for resolving destination names
085         * into the JCA 1.5 ActivationSpec "destination" property.
086         * <p>If not specified, destination names will simply be passed in as Strings.
087         * If specified, destination names will be resolved into Destination objects first.
088         * <p>Note that a DestinationResolver for use with this factory must be
089         * able to work <i>without</i> an active JMS Session: e.g.
090         * {@link org.springframework.jms.support.destination.JndiDestinationResolver}
091         * or {@link org.springframework.jms.support.destination.BeanFactoryDestinationResolver}
092         * but not {@link org.springframework.jms.support.destination.DynamicDestinationResolver}.
093         */
094        public void setDestinationResolver(@Nullable DestinationResolver destinationResolver) {
095                this.destinationResolver = destinationResolver;
096        }
097
098        /**
099         * Return the {@link DestinationResolver} to use for resolving destinations names.
100         */
101        @Nullable
102        public DestinationResolver getDestinationResolver() {
103                return this.destinationResolver;
104        }
105
106
107        @Override
108        public ActivationSpec createActivationSpec(ResourceAdapter adapter, JmsActivationSpecConfig config) {
109                Class<?> activationSpecClassToUse = this.activationSpecClass;
110                if (activationSpecClassToUse == null) {
111                        activationSpecClassToUse = determineActivationSpecClass(adapter);
112                        if (activationSpecClassToUse == null) {
113                                throw new IllegalStateException("Property 'activationSpecClass' is required");
114                        }
115                }
116
117                ActivationSpec spec = (ActivationSpec) BeanUtils.instantiateClass(activationSpecClassToUse);
118                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(spec);
119                if (this.defaultProperties != null) {
120                        bw.setPropertyValues(this.defaultProperties);
121                }
122                populateActivationSpecProperties(bw, config);
123                return spec;
124        }
125
126        /**
127         * Determine the ActivationSpec class for the given ResourceAdapter,
128         * if possible. Called if no 'activationSpecClass' has been set explicitly
129         * @param adapter the ResourceAdapter to check
130         * @return the corresponding ActivationSpec class, or {@code null}
131         * if not determinable
132         * @see #setActivationSpecClass
133         */
134        @Nullable
135        protected Class<?> determineActivationSpecClass(ResourceAdapter adapter) {
136                return null;
137        }
138
139        /**
140         * Populate the given ApplicationSpec object with the settings
141         * defined in the given configuration object.
142         * <p>This implementation applies all standard JMS settings, but ignores
143         * "maxConcurrency" and "prefetchSize" - not supported in standard JCA 1.5.
144         * @param bw the BeanWrapper wrapping the ActivationSpec object
145         * @param config the configured object holding common JMS settings
146         */
147        protected void populateActivationSpecProperties(BeanWrapper bw, JmsActivationSpecConfig config) {
148                String destinationName = config.getDestinationName();
149                if (destinationName != null) {
150                        boolean pubSubDomain = config.isPubSubDomain();
151                        Object destination = destinationName;
152                        if (this.destinationResolver != null) {
153                                try {
154                                        destination = this.destinationResolver.resolveDestinationName(null, destinationName, pubSubDomain);
155                                }
156                                catch (JMSException ex) {
157                                        throw new DestinationResolutionException(
158                                                        "Cannot resolve destination name [" + destinationName + "]", ex);
159                                }
160                        }
161                        bw.setPropertyValue("destination", destination);
162                        bw.setPropertyValue("destinationType", pubSubDomain ? Topic.class.getName() : Queue.class.getName());
163                }
164
165                if (bw.isWritableProperty("subscriptionDurability")) {
166                        bw.setPropertyValue("subscriptionDurability", config.isSubscriptionDurable() ? "Durable" : "NonDurable");
167                }
168                else if (config.isSubscriptionDurable()) {
169                        // Standard JCA 1.5 "subscriptionDurability" apparently not supported...
170                        throw new IllegalArgumentException("Durable subscriptions not supported by underlying provider");
171                }
172                if (config.isSubscriptionShared()) {
173                        throw new IllegalArgumentException("Shared subscriptions not supported for JCA-driven endpoints");
174                }
175
176                if (config.getSubscriptionName() != null) {
177                        bw.setPropertyValue("subscriptionName", config.getSubscriptionName());
178                }
179                if (config.getClientId() != null) {
180                        bw.setPropertyValue("clientId", config.getClientId());
181                }
182                if (config.getMessageSelector() != null) {
183                        bw.setPropertyValue("messageSelector", config.getMessageSelector());
184                }
185                applyAcknowledgeMode(bw, config.getAcknowledgeMode());
186        }
187
188        /**
189         * Apply the specified acknowledge mode to the ActivationSpec object.
190         * <p>This implementation applies the standard JCA 1.5 acknowledge modes
191         * "Auto-acknowledge" and "Dups-ok-acknowledge". It throws an exception in
192         * case of {@code CLIENT_ACKNOWLEDGE} or {@code SESSION_TRANSACTED}
193         * having been requested.
194         * @param bw the BeanWrapper wrapping the ActivationSpec object
195         * @param ackMode the configured acknowledge mode
196         * (according to the constants in {@link javax.jms.Session}
197         * @see javax.jms.Session#AUTO_ACKNOWLEDGE
198         * @see javax.jms.Session#DUPS_OK_ACKNOWLEDGE
199         * @see javax.jms.Session#CLIENT_ACKNOWLEDGE
200         * @see javax.jms.Session#SESSION_TRANSACTED
201         */
202        protected void applyAcknowledgeMode(BeanWrapper bw, int ackMode) {
203                if (ackMode == Session.SESSION_TRANSACTED) {
204                        throw new IllegalArgumentException("No support for SESSION_TRANSACTED: Only \"Auto-acknowledge\" " +
205                                        "and \"Dups-ok-acknowledge\" supported in standard JCA 1.5");
206                }
207                else if (ackMode == Session.CLIENT_ACKNOWLEDGE) {
208                        throw new IllegalArgumentException("No support for CLIENT_ACKNOWLEDGE: Only \"Auto-acknowledge\" " +
209                                        "and \"Dups-ok-acknowledge\" supported in standard JCA 1.5");
210                }
211                else if (bw.isWritableProperty("acknowledgeMode")) {
212                        bw.setPropertyValue("acknowledgeMode",
213                                        ackMode == Session.DUPS_OK_ACKNOWLEDGE ? "Dups-ok-acknowledge" : "Auto-acknowledge");
214                }
215                else if (ackMode == Session.DUPS_OK_ACKNOWLEDGE) {
216                        // Standard JCA 1.5 "acknowledgeMode" apparently not supported (e.g. WebSphere MQ 6.0.2.1)
217                        throw new IllegalArgumentException("Dups-ok-acknowledge not supported by underlying provider");
218                }
219        }
220
221}