diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractMarshallingPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractMarshallingPayloadEndpoint.java index e164a090..fa36de9a 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractMarshallingPayloadEndpoint.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractMarshallingPayloadEndpoint.java @@ -24,7 +24,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; import org.springframework.util.Assert; -import org.springframework.validation.Validator; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.context.MessageContext; import org.springframework.ws.support.MarshallingUtils; @@ -51,8 +50,6 @@ public abstract class AbstractMarshallingPayloadEndpoint implements MessageEndpo private Unmarshaller unmarshaller; - private Validator[] validators; - /** * Creates a new AbstractMarshallingPayloadEndpoint. The {@link Marshaller} and {@link Unmarshaller} * must be injected using properties. @@ -122,30 +119,6 @@ public abstract class AbstractMarshallingPayloadEndpoint implements MessageEndpo this.unmarshaller = unmarshaller; } - /** - * Set the primary {@link Validator} for this endpoint. The {@link Validator} is must support the specified command - * class. If there are one or more existing validators set already when this method is called, only the specified - * validator will be kept. Use {@link #setValidators(Validator[])} to set multiple validators. - */ - public final void setValidator(Validator validator) { - this.validators = new Validator[]{validator}; - } - - /** Return the primary Validator for this controller. */ - public final Validator getValidator() { - return (this.validators != null && this.validators.length > 0 ? this.validators[0] : null); - } - - /** Set the Validators for this controller. The Validator must support the specified command class. */ - public final void setValidators(Validator[] validators) { - this.validators = validators; - } - - /** Return the Validators for this controller. */ - public final Validator[] getValidators() { - return validators; - } - public final void afterPropertiesSet() throws Exception { Assert.notNull(getMarshaller(), "marshaller is required"); Assert.notNull(getUnmarshaller(), "unmarshaller is required"); @@ -155,10 +128,13 @@ public abstract class AbstractMarshallingPayloadEndpoint implements MessageEndpo public final void invoke(MessageContext messageContext) throws Exception { WebServiceMessage request = messageContext.getRequest(); Object requestObject = unmarshalRequest(request); - Object responseObject = invokeInternal(requestObject); - if (responseObject != null) { - WebServiceMessage response = messageContext.getResponse(); - marshalResponse(responseObject, response); + if (onUnmarshalRequest(messageContext, requestObject)) { + Object responseObject = invokeInternal(requestObject); + if (responseObject != null) { + WebServiceMessage response = messageContext.getResponse(); + marshalResponse(responseObject, response); + onMarshalResponse(messageContext, requestObject, responseObject); + } } } @@ -170,6 +146,20 @@ public abstract class AbstractMarshallingPayloadEndpoint implements MessageEndpo return requestObject; } + /** + * Callback for post-processing in terms of unmarshalling. Called on each message request, after standard + * unmarshalling. + *

+ * Default implementation returns true. + * + * @param messageContext the message context + * @param requestObject the object unmarshalled from the {@link MessageContext#getRequest() request} + * @return true to continue and call {@link #invokeInternal(Object)}; false otherwise + */ + protected boolean onUnmarshalRequest(MessageContext messageContext, Object requestObject) throws Exception { + return true; + } + private void marshalResponse(Object responseObject, WebServiceMessage response) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Marshalling [" + responseObject + "] to response payload"); @@ -177,6 +167,19 @@ public abstract class AbstractMarshallingPayloadEndpoint implements MessageEndpo MarshallingUtils.marshal(getMarshaller(), responseObject, response); } + /** + * Callback for post-processing in terms of marshalling. Called on each message request, after standard marshalling + * of the response. Only invoked when {@link #invokeInternal(Object)} returns an object. + *

+ * Default implementation is empty. + * + * @param messageContext the message context + * @param requestObject the object unmarshalled from the {@link MessageContext#getRequest() request} + * @param responseObject the object marshalled to the {@link MessageContext#getResponse()} request} + */ + protected void onMarshalResponse(MessageContext messageContext, Object requestObject, Object responseObject) { + } + /** * Template method that gets called after the marshaller and unmarshaller have been set. *

diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/AbstractValidatingMarshallingPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractValidatingMarshallingPayloadEndpoint.java new file mode 100644 index 00000000..fcde758a --- /dev/null +++ b/core/src/main/java/org/springframework/ws/server/endpoint/AbstractValidatingMarshallingPayloadEndpoint.java @@ -0,0 +1,99 @@ +/* + * Copyright 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.ws.server.endpoint; + +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.ws.context.MessageContext; + +/** + * Extension of the {@link AbstractMarshallingPayloadEndpoint} which validates the request payload with {@link + * Validator}(s). The desired validators can be set using properties, and must {@link + * Validator#supports(Class) support} the request object. + * + * @author Arjen Poutsma + * @since 1.0.2 + */ +public abstract class AbstractValidatingMarshallingPayloadEndpoint extends AbstractMarshallingPayloadEndpoint { + + /** Default request object name used for validating request objects. */ + public static final String DEFAULT_REQUEST_NAME = "request"; + + private String requestName = DEFAULT_REQUEST_NAME; + + private Validator[] validators; + + /** Return the name of the request object for validation error codes. */ + public final String getRequestName() { + return requestName; + } + + /** Set the name of the request object user for validation errors. */ + public final void setRequestName(String requestName) { + this.requestName = requestName; + } + + /** Return the primary Validator for this controller. */ + public final Validator getValidator() { + return (this.validators != null && this.validators.length > 0 ? this.validators[0] : null); + } + + /** + * Set the primary {@link Validator} for this endpoint. The {@link Validator} is must support the unmarshalled + * class. If there are one or more existing validators set already when this method is called, only the specified + * validator will be kept. Use {@link #setValidators(Validator[])} to set multiple validators. + */ + public final void setValidator(Validator validator) { + this.validators = new Validator[]{validator}; + } + + /** Return the Validators for this controller. */ + public final Validator[] getValidators() { + return validators; + } + + /** Set the Validators for this controller. The Validator must support the specified command class. */ + public final void setValidators(Validator[] validators) { + this.validators = validators; + } + + protected final boolean onUnmarshalRequest(MessageContext messageContext, Object requestObject) throws Exception { + if (validators != null) { + Errors errors = new BeanPropertyBindingResult(requestObject, getRequestName()); + for (int i = 0; i < validators.length; i++) { + ValidationUtils.invokeValidator(validators[i], requestObject, errors); + } + if (errors.hasErrors()) { + return onValidationErrors(messageContext, requestObject, errors); + } + } + return true; + } + + /** + * Callback for post-processing validation errors. Called when validator(s) have been specified, and validation + * fails. + * + * @param messageContext the message context + * @param requestObject the object unmarshalled from the {@link MessageContext#getRequest() request} + * @param errors validation errors holder + * @return true to continue and call {@link #invokeInternal(Object)}; false otherwise + */ + protected abstract boolean onValidationErrors(MessageContext messageContext, Object requestObject, Errors errors); +} diff --git a/core/src/main/java/org/springframework/ws/soap/server/endpoint/AbstractFaultCreatingValidatingMarshallingPayloadEndpoint.java b/core/src/main/java/org/springframework/ws/soap/server/endpoint/AbstractFaultCreatingValidatingMarshallingPayloadEndpoint.java new file mode 100644 index 00000000..3e77ae0e --- /dev/null +++ b/core/src/main/java/org/springframework/ws/soap/server/endpoint/AbstractFaultCreatingValidatingMarshallingPayloadEndpoint.java @@ -0,0 +1,181 @@ +/* + * Copyright 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.ws.soap.server.endpoint; + +import java.util.Iterator; +import java.util.Locale; +import javax.xml.namespace.QName; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.validation.Errors; +import org.springframework.validation.ObjectError; +import org.springframework.validation.Validator; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.server.endpoint.AbstractValidatingMarshallingPayloadEndpoint; +import org.springframework.ws.soap.SoapBody; +import org.springframework.ws.soap.SoapFault; +import org.springframework.ws.soap.SoapFaultDetail; +import org.springframework.ws.soap.SoapFaultDetailElement; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.xml.namespace.QNameUtils; + +/** + * Extension of the {@link AbstractValidatingMarshallingPayloadEndpoint} which validates the request payload with {@link + * Validator}(s), and creates a SOAP Fault whenever the request message cannot be validated. The desired validators can + * be set using properties, and must {@link Validator#supports(Class) support} the request object. + *

+ * The contents of the SOAP Fault can be specified by setting the {@link #setAddValidationErrorDetail(boolean) + * addValidationErrorDetail}, {@link #setFaultStringOrReason(String) faultStringOrReason}, or {@link + * #setDetailElementName(QName) detailElementName} properties. + * + * @author Arjen Poutsma + * @since 1.0.2 + */ +public abstract class AbstractFaultCreatingValidatingMarshallingPayloadEndpoint + extends AbstractValidatingMarshallingPayloadEndpoint implements MessageSourceAware { + + /** + * Default SOAP Fault Detail name used when a global validation error occur on the request. + * + * @see #setDetailElementName(javax.xml.namespace.QName) + */ + public static final QName DEFAULT_DETAIL_ELEMENT_NAME = + QNameUtils.createQName("http://springframework.org/spring-ws", "ValidationError", "spring-ws"); + + /** + * Default SOAP Fault string used when a validation errors occur on the request. + * + * @see #setFaultStringOrReason(String) + */ + public static final String DEFAULT_FAULTSTRING_OR_REASON = "Validation error"; + + private boolean addValidationErrorDetail = true; + + private QName detailElementName = DEFAULT_DETAIL_ELEMENT_NAME; + + private String faultStringOrReason = DEFAULT_FAULTSTRING_OR_REASON; + + private Locale faultStringOrReasonLocale = Locale.ENGLISH; + + private MessageSource messageSource; + + /** + * Returns whether a SOAP Fault detail element should be created when a validation error occurs. This detail element + * will contain the exact validation errors. It is only added when the underlying message is a + * SoapMessage. Defaults to true. + * + * @see org.springframework.ws.soap.SoapFault#addFaultDetail() + */ + public boolean getAddValidationErrorDetail() { + return addValidationErrorDetail; + } + + /** + * Indicates whether a SOAP Fault detail element should be created when a validation error occurs. This detail + * element will contain the exact validation errors. It is only added when the underlying message is a + * SoapMessage. Defaults to true. + * + * @see org.springframework.ws.soap.SoapFault#addFaultDetail() + */ + public void setAddValidationErrorDetail(boolean addValidationErrorDetail) { + this.addValidationErrorDetail = addValidationErrorDetail; + } + + /** Returns the fault detail element name when validation errors occur on the request. */ + public QName getDetailElementName() { + return detailElementName; + } + + /** + * Sets the fault detail element name when validation errors occur on the request. Defaults to + * DEFAULT_DETAIL_ELEMENT_NAME. + * + * @see #DEFAULT_DETAIL_ELEMENT_NAME + */ + public void setDetailElementName(QName detailElementName) { + this.detailElementName = detailElementName; + } + + /** Sets the SOAP faultstring or Reason used when validation errors occur on the request. */ + public String getFaultStringOrReason() { + return faultStringOrReason; + } + + /** + * Sets the SOAP faultstring or Reason used when validation errors occur on the request. + * It is only added when the underlying message is a SoapMessage. Defaults to + * DEFAULT_FAULTSTRING_OR_REASON. + * + * @see #DEFAULT_FAULTSTRING_OR_REASON + */ + public void setFaultStringOrReason(String faultStringOrReason) { + this.faultStringOrReason = faultStringOrReason; + } + + /** Returns the locale for SOAP fault reason and validation message resolution. */ + public Locale getFaultLocale() { + return faultStringOrReasonLocale; + } + + /** + * Sets the locale for SOAP fault reason and validation messages. It is only added when the underlying message is a + * SoapMessage. Defaults to English. + * + * @see java.util.Locale#ENGLISH + */ + public void setFaultStringOrReasonLocale(Locale faultStringOrReasonLocale) { + this.faultStringOrReasonLocale = faultStringOrReasonLocale; + } + + public final void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * This implementation logs all errors, returns false, and creates a {@link + * SoapBody#addClientOrSenderFault(String,Locale) client or sender} {@link SoapFault}, adding a {@link + * SoapFaultDetail} with all errors if the addValidationErrorDetail property is true. + * + * @param messageContext the message context + * @param errors the validation errors + * @return true to continue processing the request, false (the default) otherwise + * @see Errors#getAllErrors() + */ + protected final boolean onValidationErrors(MessageContext messageContext, Object requestObject, Errors errors) { + for (Iterator iterator = errors.getAllErrors().iterator(); iterator.hasNext();) { + ObjectError objectError = (ObjectError) iterator.next(); + String msg = messageSource.getMessage(objectError, getFaultLocale()); + logger.warn("Validation error on request object[" + requestObject + "]: " + msg); + } + if (messageContext.getResponse() instanceof SoapMessage) { + SoapMessage response = (SoapMessage) messageContext.getResponse(); + SoapBody body = response.getSoapBody(); + SoapFault fault = body.addClientOrSenderFault(getFaultStringOrReason(), getFaultLocale()); + if (getAddValidationErrorDetail()) { + SoapFaultDetail detail = fault.addFaultDetail(); + for (Iterator iterator = errors.getAllErrors().iterator(); iterator.hasNext();) { + ObjectError objectError = (ObjectError) iterator.next(); + String msg = messageSource.getMessage(objectError, getFaultLocale()); + SoapFaultDetailElement detailElement = detail.addFaultDetailElement(getDetailElementName()); + detailElement.addText(msg); + } + } + } + return false; + } +} diff --git a/core/src/test/java/org/springframework/ws/soap/server/endpoint/FaultCreatingValidatingMarshallingPayloadEndpointTest.java b/core/src/test/java/org/springframework/ws/soap/server/endpoint/FaultCreatingValidatingMarshallingPayloadEndpointTest.java new file mode 100644 index 00000000..efd6892d --- /dev/null +++ b/core/src/test/java/org/springframework/ws/soap/server/endpoint/FaultCreatingValidatingMarshallingPayloadEndpointTest.java @@ -0,0 +1,191 @@ +/* + * Copyright ${YEAR} the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ws.soap.server.endpoint; + +import java.io.IOException; +import java.util.Iterator; +import javax.xml.namespace.QName; +import javax.xml.soap.Detail; +import javax.xml.soap.DetailEntry; +import javax.xml.soap.MessageFactory; +import javax.xml.soap.SOAPFault; +import javax.xml.soap.SOAPMessage; +import javax.xml.transform.Result; +import javax.xml.transform.Source; + +import junit.framework.TestCase; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.ws.context.DefaultMessageContext; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.soap.saaj.SaajSoapMessage; +import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; + +public class FaultCreatingValidatingMarshallingPayloadEndpointTest extends TestCase { + + private MessageContext messageContext; + + private ResourceBundleMessageSource messageSource; + + protected void setUp() throws Exception { + this.messageSource = new ResourceBundleMessageSource(); + this.messageSource.setBasename("org.springframework.ws.soap.server.endpoint.messages"); + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage request = messageFactory.createMessage(); + request.getSOAPBody().addBodyElement(new QName("http://www.springframework.org/spring-ws", "request")); + messageContext = + new DefaultMessageContext(new SaajSoapMessage(request), new SaajSoapMessageFactory(messageFactory)); + } + + public void testValidationIncorrect() throws Exception { + Person p = new Person("", -1); + PersonMarshaller marshaller = new PersonMarshaller(p); + + AbstractFaultCreatingValidatingMarshallingPayloadEndpoint endpoint = + new AbstractFaultCreatingValidatingMarshallingPayloadEndpoint() { + + protected Object invokeInternal(Object requestObject) throws Exception { + fail("No expected"); + return null; + } + }; + endpoint.setValidator(new PersonValidator()); + endpoint.setMessageSource(messageSource); + endpoint.setMarshaller(marshaller); + endpoint.setUnmarshaller(marshaller); + + endpoint.invoke(messageContext); + + SOAPMessage response = ((SaajSoapMessage) messageContext.getResponse()).getSaajMessage(); + assertTrue("Response has no fault", response.getSOAPBody().hasFault()); + SOAPFault fault = response.getSOAPBody().getFault(); + assertEquals("Invalid fault code", new QName("http://schemas.xmlsoap.org/soap/envelope/", "Client"), + fault.getFaultCodeAsQName()); + assertEquals("Invalid fault string", endpoint.getFaultStringOrReason(), fault.getFaultString()); + Detail detail = fault.getDetail(); + assertNotNull("No detail", detail); + Iterator iterator = detail.getDetailEntries(); + assertTrue("No detail entry", iterator.hasNext()); + DetailEntry detailEntry = (DetailEntry) iterator.next(); + assertEquals("Invalid detail entry name", new QName("http://springframework.org/spring-ws", "ValidationError"), + detailEntry.getElementQName()); + assertEquals("Invalid detail entry text", "Name is required", detailEntry.getTextContent()); + assertTrue("No detail entry", iterator.hasNext()); + detailEntry = (DetailEntry) iterator.next(); + assertEquals("Invalid detail entry name", new QName("http://springframework.org/spring-ws", "ValidationError"), + detailEntry.getElementQName()); + assertEquals("Invalid detail entry text", "Age Cannot be negative", detailEntry.getTextContent()); + assertFalse("Too many detail entries", iterator.hasNext()); + } + + public void testValidationCorrect() throws Exception { + Person p = new Person("John", 42); + PersonMarshaller marshaller = new PersonMarshaller(p); + AbstractFaultCreatingValidatingMarshallingPayloadEndpoint endpoint = + new AbstractFaultCreatingValidatingMarshallingPayloadEndpoint() { + + protected Object invokeInternal(Object requestObject) throws Exception { + return null; + } + }; + endpoint.setValidator(new PersonValidator()); + endpoint.setMessageSource(messageSource); + endpoint.setMarshaller(marshaller); + endpoint.setUnmarshaller(marshaller); + + endpoint.invoke(messageContext); + + SOAPMessage response = ((SaajSoapMessage) messageContext.getResponse()).getSaajMessage(); + assertFalse("Response has fault", response.getSOAPBody().hasFault()); + } + + private static class PersonValidator implements Validator { + + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + public void validate(Object obj, Errors e) { + ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); + Person p = (Person) obj; + if (p.getAge() < 0) { + e.rejectValue("age", "age.negativevalue"); + } + else if (p.getAge() > 110) { + e.rejectValue("age", "too.darn.old"); + } + } + } + + private static class Person { + + private String name; + + private int age; + + private Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String toString() { + return "Person{" + name + "," + age + "}"; + } + } + + private static class PersonMarshaller implements Unmarshaller, Marshaller { + + private final Person person; + + private PersonMarshaller(Person person) { + this.person = person; + } + + public Object unmarshal(Source source) throws XmlMappingException, IOException { + return person; + } + + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + public void marshal(Object graph, Result result) throws XmlMappingException, IOException { + } + } + +} \ No newline at end of file diff --git a/core/src/test/resources/org/springframework/ws/soap/server/endpoint/messages.properties b/core/src/test/resources/org/springframework/ws/soap/server/endpoint/messages.properties new file mode 100644 index 00000000..c5ebafc7 --- /dev/null +++ b/core/src/test/resources/org/springframework/ws/soap/server/endpoint/messages.properties @@ -0,0 +1,2 @@ +name.empty=Name is required +age.negativevalue=Age Cannot be negative \ No newline at end of file