/*
 * Copyright 2005-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.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.ws.soap.addressing.server;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.transform.TransformerException;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.server.EndpointInterceptor;
import org.springframework.ws.server.EndpointInvocationChain;
import org.springframework.ws.server.EndpointMapping;
import org.springframework.ws.server.SmartEndpointInterceptor;
import org.springframework.ws.soap.SoapHeader;
import org.springframework.ws.soap.SoapHeaderElement;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.addressing.core.MessageAddressingProperties;
import org.springframework.ws.soap.addressing.messageid.MessageIdStrategy;
import org.springframework.ws.soap.addressing.messageid.UuidMessageIdStrategy;
import org.springframework.ws.soap.addressing.version.Addressing10;
import org.springframework.ws.soap.addressing.version.Addressing200408;
import org.springframework.ws.soap.addressing.version.AddressingVersion;
import org.springframework.ws.soap.server.SoapEndpointInvocationChain;
import org.springframework.ws.soap.server.SoapEndpointMapping;
import org.springframework.ws.transport.WebServiceMessageSender;
import org.springframework.xml.transform.TransformerObjectSupport;

/**
 * Abstract base class for {@link EndpointMapping} implementations that handle
 * WS-Addressing. Besides the normal {@link SoapEndpointMapping} properties, this mapping
 * has a
 * {@link #setVersions(org.springframework.ws.soap.addressing.version.AddressingVersion[])
 * versions} property, which defines the WS-Addressing specifications supported. By
 * default, these are
 * {@link org.springframework.ws.soap.addressing.version.Addressing200408} and
 * {@link org.springframework.ws.soap.addressing.version.Addressing10}.
 * <p>
 * The {@link #setMessageIdStrategy(MessageIdStrategy) messageIdStrategy} property defines
 * the strategy to use for creating reply {@code MessageIDs}. By default, this is the
 * {@link UuidMessageIdStrategy}.
 * <p>
 * The {@link #setMessageSenders(WebServiceMessageSender[]) messageSenders} are used to
 * send out-of-band reply messages. If a request messages defines a non-anonymous reply
 * address, these senders will be used to send the message.
 * <p>
 * This mapping (and all subclasses) uses an implicit WS-Addressing
 * {@link EndpointInterceptor}, which is added in every {@link EndpointInvocationChain}
 * produced. As such, this mapping does not have the standard {@code interceptors}
 * property, but rather a {@link #setPreInterceptors(EndpointInterceptor[])
 * preInterceptors} and {@link #setPostInterceptors(EndpointInterceptor[])
 * postInterceptors} property, which are added before and after the implicit WS-Addressing
 * interceptor, respectively.
 *
 * @author Arjen Poutsma
 * @author Nate Stoddard
 * @since 1.5.0
 */
public abstract class AbstractAddressingEndpointMapping extends TransformerObjectSupport
		implements SoapEndpointMapping, ApplicationContextAware, InitializingBean, Ordered {

	private String[] actorsOrRoles;

	private boolean isUltimateReceiver = true;

	private MessageIdStrategy messageIdStrategy;

	private WebServiceMessageSender[] messageSenders = new WebServiceMessageSender[0];

	private AddressingVersion[] versions;

	private EndpointInterceptor[] preInterceptors = new EndpointInterceptor[0];

	private EndpointInterceptor[] postInterceptors = new EndpointInterceptor[0];

	private SmartEndpointInterceptor[] smartInterceptors = new SmartEndpointInterceptor[0];

	private ApplicationContext applicationContext;

	private int order = Integer.MAX_VALUE; // default: same as non-Ordered

	/** Protected constructor. Initializes the default settings. */
	protected AbstractAddressingEndpointMapping() {
		initDefaultStrategies();
	}

	/**
	 * Initializes the default implementation for this mapping's strategies: the
	 * {@link org.springframework.ws.soap.addressing.version.Addressing200408} and
	 * {@link org.springframework.ws.soap.addressing.version.Addressing10} versions of the
	 * specification, and the {@link UuidMessageIdStrategy}.
	 */
	protected void initDefaultStrategies() {
		this.versions = new AddressingVersion[] { new Addressing200408(), new Addressing10() };
		this.messageIdStrategy = new UuidMessageIdStrategy();
	}

	@Override
	public final void setActorOrRole(String actorOrRole) {
		Assert.notNull(actorOrRole, "actorOrRole must not be null");
		this.actorsOrRoles = new String[] { actorOrRole };
	}

	@Override
	public final void setActorsOrRoles(String[] actorsOrRoles) {
		Assert.notEmpty(actorsOrRoles, "actorsOrRoles must not be empty");
		this.actorsOrRoles = actorsOrRoles;
	}

	@Override
	public final void setUltimateReceiver(boolean ultimateReceiver) {
		this.isUltimateReceiver = ultimateReceiver;
	}

	public ApplicationContext getApplicationContext() {
		return this.applicationContext;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public final int getOrder() {
		return this.order;
	}

	/**
	 * Specify the order value for this mapping.
	 * <p>
	 * Default value is {@link Integer#MAX_VALUE}, meaning that it's non-ordered.
	 * @see org.springframework.core.Ordered#getOrder()
	 */
	public final void setOrder(int order) {
		this.order = order;
	}

	/**
	 * Set additional interceptors to be applied before the implicit WS-Addressing
	 * interceptor, e.g. {@code XwsSecurityInterceptor}.
	 */
	public final void setPreInterceptors(EndpointInterceptor[] preInterceptors) {
		Assert.notNull(preInterceptors, "'preInterceptors' must not be null");
		this.preInterceptors = preInterceptors;
	}

	/**
	 * Set additional interceptors to be applied after the implicit WS-Addressing
	 * interceptor, e.g. {@code PayloadLoggingInterceptor}.
	 */
	public final void setPostInterceptors(EndpointInterceptor[] postInterceptors) {
		Assert.notNull(postInterceptors, "'postInterceptors' must not be null");
		this.postInterceptors = postInterceptors;
	}

	/**
	 * Sets the message id strategy used for creating WS-Addressing MessageIds.
	 * <p>
	 * By default, the {@link UuidMessageIdStrategy} is used.
	 */
	public final void setMessageIdStrategy(MessageIdStrategy messageIdStrategy) {
		Assert.notNull(messageIdStrategy, "'messageIdStrategy' must not be null");
		this.messageIdStrategy = messageIdStrategy;
	}

	/**
	 * Returns the message id strategy used for creating WS-Addressing MessageIds.
	 */
	public MessageIdStrategy getMessageIdStrategy() {
		return this.messageIdStrategy;
	}

	/**
	 * Sets a single message senders, which is used to send out-of-band reply messages. If
	 * a request messages defines a non-anonymous reply address, this senders will be used
	 * to send the message.
	 * @param messageSender the message sender
	 */
	public final void setMessageSender(WebServiceMessageSender messageSender) {
		Assert.notNull(messageSender, "'messageSender' must not be null");
		setMessageSenders(new WebServiceMessageSender[] { messageSender });
	}

	/**
	 * Sets the message senders, which are used to send out-of-band reply messages. If a
	 * request messages defines a non-anonymous reply address, these senders will be used
	 * to send the message.
	 * @param messageSenders the message senders
	 */
	public final void setMessageSenders(WebServiceMessageSender[] messageSenders) {
		Assert.notNull(messageSenders, "'messageSenders' must not be null");
		this.messageSenders = messageSenders;
	}

	/**
	 * Returns the message senders, which are used to send out-of-band reply messages.
	 * @return the message sender
	 */
	public final WebServiceMessageSender[] getMessageSenders() {
		return this.messageSenders;
	}

	/**
	 * Sets the WS-Addressing versions to be supported by this mapping.
	 * <p>
	 * By default, this array is set to support
	 * {@link org.springframework.ws.soap.addressing.version.Addressing200408 the August
	 * 2004} and the {@link org.springframework.ws.soap.addressing.version.Addressing10
	 * May 2006} versions of the specification.
	 */
	public final void setVersions(AddressingVersion[] versions) {
		this.versions = versions;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (this.logger.isInfoEnabled()) {
			this.logger.info("Supporting " + Arrays.asList(this.versions));
		}
		if (getApplicationContext() != null) {
			Map<String, SmartEndpointInterceptor> smartInterceptors = BeanFactoryUtils
				.beansOfTypeIncludingAncestors(getApplicationContext(), SmartEndpointInterceptor.class, true, false);
			if (!smartInterceptors.isEmpty()) {
				this.smartInterceptors = smartInterceptors.values().toArray(new SmartEndpointInterceptor[0]);
			}
		}
	}

	@Override
	public final EndpointInvocationChain getEndpoint(MessageContext messageContext) throws TransformerException {
		Assert.isInstanceOf(SoapMessage.class, messageContext.getRequest());
		SoapMessage request = (SoapMessage) messageContext.getRequest();
		for (AddressingVersion version : this.versions) {
			if (supports(version, request)) {
				if (this.logger.isDebugEnabled()) {
					this.logger.debug("Request [" + request + "] uses [" + version + "]");
				}
				MessageAddressingProperties requestMap = version.getMessageAddressingProperties(request);
				if (requestMap == null) {
					return null;
				}
				Object endpoint = getEndpointInternal(requestMap);
				if (endpoint == null) {
					return null;
				}
				return getEndpointInvocationChain(endpoint, version, requestMap, messageContext);
			}
		}
		return null;
	}

	/**
	 * Creates a {@link SoapEndpointInvocationChain} based on the given endpoint and
	 * {@link org.springframework.ws.soap.addressing.version.AddressingVersion}.
	 */
	private EndpointInvocationChain getEndpointInvocationChain(Object endpoint, AddressingVersion version,
			MessageAddressingProperties requestMap, MessageContext messageContext) {
		URI responseAction = getResponseAction(endpoint, requestMap);
		URI faultAction = getFaultAction(endpoint, requestMap);

		WebServiceMessageSender[] messageSenders = getMessageSenders(endpoint);
		MessageIdStrategy messageIdStrategy = getMessageIdStrategy(endpoint);

		List<EndpointInterceptor> interceptors = new ArrayList<>(Arrays.asList(this.preInterceptors));

		AddressingEndpointInterceptor addressingInterceptor = new AddressingEndpointInterceptor(version,
				messageIdStrategy, messageSenders, responseAction, faultAction);
		interceptors.add(addressingInterceptor);
		interceptors.addAll(Arrays.asList(this.postInterceptors));

		if (this.smartInterceptors != null) {
			for (SmartEndpointInterceptor smartInterceptor : this.smartInterceptors) {
				if (smartInterceptor.shouldIntercept(messageContext, endpoint)) {
					interceptors.add(smartInterceptor);
				}
			}
		}

		return new SoapEndpointInvocationChain(endpoint, interceptors.toArray(new EndpointInterceptor[0]),
				this.actorsOrRoles, this.isUltimateReceiver);
	}

	private boolean supports(AddressingVersion version, SoapMessage request) {
		SoapHeader header = request.getSoapHeader();
		if (header != null) {
			for (Iterator<SoapHeaderElement> iterator = header.examineAllHeaderElements(); iterator.hasNext();) {
				SoapHeaderElement headerElement = iterator.next();
				if (version.understands(headerElement)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns the message senders for the given endpoint. Default implementation returns
	 * {@link #getMessageSenders()}
	 * @param endpoint the endpoint
	 * @return the message senders for the given endpoint
	 */
	protected WebServiceMessageSender[] getMessageSenders(Object endpoint) {
		return getMessageSenders();
	}

	/**
	 * Returns the message ID strategy for the given endpoint. Default implementation
	 * returns {@link #getMessageIdStrategy()}
	 * @param endpoint the endpoint
	 * @return the message ID strategy for the given endpoint
	 */
	protected MessageIdStrategy getMessageIdStrategy(Object endpoint) {
		return getMessageIdStrategy();
	}

	/**
	 * Lookup an endpoint for the given {@link MessageAddressingProperties}, returning
	 * {@code null} if no specific one is found. This template method is called by
	 * {@link #getEndpoint(MessageContext)}.
	 * @param map the message addressing properties
	 * @return the endpoint, or {@code null}
	 */
	protected abstract Object getEndpointInternal(MessageAddressingProperties map);

	/**
	 * Provides the WS-Addressing Action for response messages, given the endpoint, and
	 * request Message Addressing Properties.
	 * @param endpoint the mapped endpoint
	 * @param requestMap the MAP for the request
	 * @return the response Action
	 */
	protected abstract URI getResponseAction(Object endpoint, MessageAddressingProperties requestMap);

	/**
	 * Provides the WS-Addressing Action for response fault messages, given the endpoint,
	 * and request Message Addressing Properties.
	 * @param endpoint the mapped endpoint
	 * @param requestMap the MAP for the request
	 * @return the response Action
	 */
	protected abstract URI getFaultAction(Object endpoint, MessageAddressingProperties requestMap);

}
