/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.integration.ws;

import java.io.IOException;
import java.util.Iterator;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Document;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.core.SourceExtractor;
import org.springframework.ws.client.core.WebServiceMessageCallback;
import org.springframework.ws.client.support.destination.DestinationProvider;
import org.springframework.ws.mime.Attachment;
import org.springframework.ws.mime.MimeMessage;
import org.springframework.xml.transform.StringResult;
import org.springframework.xml.transform.StringSource;
import org.springframework.xml.transform.TransformerObjectSupport;

/**
 * An outbound Messaging Gateway for invoking a Web Service.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Artem Bilan
 * @author Gary Russell
 */
public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundGateway {

	private final SourceExtractor<?> sourceExtractor;

	private volatile boolean extractPayload = true;

	public SimpleWebServiceOutboundGateway(DestinationProvider destinationProvider) {
		this(destinationProvider, null, null);
	}

	public SimpleWebServiceOutboundGateway(DestinationProvider destinationProvider,
			SourceExtractor<?> sourceExtractor) {
		this(destinationProvider, sourceExtractor, null);
	}

	public SimpleWebServiceOutboundGateway(DestinationProvider destinationProvider, SourceExtractor<?> sourceExtractor,
			WebServiceMessageFactory messageFactory) {
		super(destinationProvider, messageFactory);
		this.sourceExtractor = (sourceExtractor != null) ? sourceExtractor : new DefaultSourceExtractor();
	}

	public SimpleWebServiceOutboundGateway(String uri) {
		this(uri, null, null);
	}

	public SimpleWebServiceOutboundGateway(String uri, SourceExtractor<?> sourceExtractor) {
		this(uri, sourceExtractor, null);
	}

	public SimpleWebServiceOutboundGateway(String uri, SourceExtractor<?> sourceExtractor,
			WebServiceMessageFactory messageFactory) {
		super(uri, messageFactory);
		this.sourceExtractor = (sourceExtractor != null) ? sourceExtractor : new DefaultSourceExtractor();
	}

	/**
	 * A flag to return the whole {@link WebServiceMessage} or build
	 * {@code payload} based on {@link WebServiceMessage}
	 * and populated headers according {@code headerMapper} configuration.
	 * Defaults to extract payload.
	 * @param extractPayload build payload or return a whole {@link WebServiceMessage}
	 * @since 5.0
	 */
	public void setExtractPayload(boolean extractPayload) {
		this.extractPayload = extractPayload;
	}

	@Override
	public String getComponentType() {
		return "ws:outbound-gateway(simple)";
	}

	@Override
	protected Object doHandle(String uri, final Message<?> requestMessage,
			final WebServiceMessageCallback requestCallback) {
		Object requestPayload = requestMessage.getPayload();
		Result responseResultInstance = null;
		if (requestPayload instanceof String) {
			responseResultInstance = new StringResult();
		}
		else if (requestPayload instanceof Document) {
			responseResultInstance = new DOMResult();
		}
		return getWebServiceTemplate()
				.sendAndReceive(uri,
						new SimpleRequestMessageCallback(requestCallback, requestMessage),
						new SimpleResponseMessageExtractor(responseResultInstance));
	}

	private final class SimpleRequestMessageCallback extends RequestMessageCallback {

		SimpleRequestMessageCallback(WebServiceMessageCallback requestCallback, Message<?> requestMessage) {
			super(requestCallback, requestMessage);
		}

		@Override
		public void doWithMessageInternal(WebServiceMessage message, Object payload)
				throws IOException, TransformerException {
			Source source = this.extractSource(payload);
			transform(source, message.getPayloadResult());
			if (message instanceof MimeMessage && payload instanceof MimeMessage) {
				copyAttachments((MimeMessage) payload, (MimeMessage) message);
			}
		}

		private Source extractSource(Object requestPayload) throws IOException, TransformerException {
			Source source = null;

			if (requestPayload instanceof Source) {
				source = (Source) requestPayload;
				Object o = SimpleWebServiceOutboundGateway.this.sourceExtractor.extractData(source);
				Assert.isInstanceOf(Source.class, o);
				source = (Source) o;
			}
			else if (requestPayload instanceof String) {
				source = new StringSource((String) requestPayload);
			}
			else if (requestPayload instanceof Document) {
				source = new DOMSource((Document) requestPayload);
			}
			else if (requestPayload instanceof WebServiceMessage) {
				source = ((WebServiceMessage) requestPayload).getPayloadSource();
			}
			else {
				throw new MessagingException("Unsupported payload type '" + requestPayload.getClass() +
						"'. " + this.getClass().getName() + " only supports 'java.lang.String', '" +
						Source.class.getName() +
						"', '" + Document.class.getName() + "' and '" + WebServiceMessage.class.getName() + "'. " +
						"Consider either using the '"
						+ MarshallingWebServiceOutboundGateway.class.getName() + "' or a Message Transformer.");
			}

			return source;
		}

		private void copyAttachments(MimeMessage source, MimeMessage target) {
			for (Iterator<Attachment> attachments = source.getAttachments(); attachments.hasNext(); ) {
				Attachment attachment = attachments.next();
				target.addAttachment(attachment.getContentId(), attachment.getDataHandler());
			}
		}

	}

	private final class SimpleResponseMessageExtractor extends ResponseMessageExtractor {

		private final Result result;

		SimpleResponseMessageExtractor(Result result) {
			super();
			this.result = result;
		}

		@Override
		public Object doExtractData(WebServiceMessage message) throws IOException, TransformerException {
			if (!SimpleWebServiceOutboundGateway.this.extractPayload) {
				return message;
			}
			else {
				Source payloadSource = message.getPayloadSource();

				if (payloadSource != null && this.result != null) {
					this.transform(payloadSource, this.result);
					if (this.result instanceof StringResult) {
						return this.result.toString();
					}
					else if (this.result instanceof DOMResult) {
						return ((DOMResult) this.result).getNode();
					}
					else {
						return this.result;
					}
				}

				return payloadSource;
			}
		}

	}


	private static class DefaultSourceExtractor extends TransformerObjectSupport implements SourceExtractor<DOMSource> {

		DefaultSourceExtractor() {
			super();
		}

		@Override
		public DOMSource extractData(Source source) throws IOException, TransformerException {
			if (source instanceof DOMSource) {
				return (DOMSource) source;
			}
			DOMResult result = new DOMResult();
			this.transform(source, result);
			return new DOMSource(result.getNode());
		}

	}

}
