/*

    Copyright (C) 2022 AGNITAS AG (https://www.agnitas.org)

    This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
    This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.

*/

package org.agnitas.emm.springws;

import java.util.Map;

import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FrameworkServlet;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.server.EndpointAdapter;
import org.springframework.ws.server.EndpointExceptionResolver;
import org.springframework.ws.server.EndpointMapping;
import org.springframework.ws.server.MessageDispatcher;
import org.springframework.ws.support.DefaultStrategiesHelper;
import org.springframework.ws.support.WebUtils;
import org.springframework.ws.transport.WebServiceMessageReceiver;
import org.springframework.ws.transport.http.HttpTransportConstants;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter;
import org.springframework.ws.transport.http.WsdlDefinitionHandlerAdapter;
import org.springframework.ws.transport.http.XsdSchemaHandlerAdapter;
import org.springframework.ws.wsdl.WsdlDefinition;
import org.springframework.xml.xsd.XsdSchema;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * Servlet for simplified dispatching of Web service messages.
 *
 * <p>This servlet is a convenient alternative to the standard Spring-MVC {@link DispatcherServlet} with separate {@link
 * WebServiceMessageReceiverHandlerAdapter}, {@link MessageDispatcher}, and {@link WsdlDefinitionHandlerAdapter}
 * instances.
 *
 * <p>This servlet automatically detects {@link EndpointAdapter EndpointAdapters}, {@link EndpointMapping
 * EndpointMappings}, and {@link EndpointExceptionResolver EndpointExceptionResolvers} <i>by type</i>.
 *
 * <p>This servlet also automatically detects any {@link WsdlDefinition} defined in its application context. This WSDL is
 * exposed under the bean name: for example, a {@code WsdlDefinition} bean named '{@code echo}' will be
 * exposed as {@code echo.wsdl} in this servlet's context: {@code http://localhost:8080/spring-ws/echo.wsdl}.
 * When the {@code transformWsdlLocations} init-param is set to {@code true} in this servlet's configuration
 * in {@code web.xml}, all {@code location} attributes in the WSDL definitions will reflect the URL of the
 * incoming request.
 *
 * @author Arjen Poutsma
 * @see org.springframework.web.servlet.DispatcherServlet
 * @see org.springframework.ws.server.MessageDispatcher
 * @see org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter
 * @since 1.0.0
 */
public class AgnitasMessageDispatcherServlet extends FrameworkServlet {
	private static final long serialVersionUID = -5637150405242158516L;

	/** Well-known name for the {@link WebServiceMessageFactory} bean in the bean factory for this namespace. */
	public static final String DEFAULT_MESSAGE_FACTORY_BEAN_NAME = "messageFactory";

	/** Well-known name for the {@link WebServiceMessageReceiver} object in the bean factory for this namespace. */
	public static final String DEFAULT_MESSAGE_RECEIVER_BEAN_NAME = "messageReceiver";

	/**
	 * Well-known name for the {@link WebServiceMessageReceiverHandlerAdapter} object in the bean factory for this
	 * namespace.
	 */
	public static final String DEFAULT_MESSAGE_RECEIVER_HANDLER_ADAPTER_BEAN_NAME = "messageReceiverHandlerAdapter";

	/** Well-known name for the {@link WsdlDefinitionHandlerAdapter} object in the bean factory for this namespace. */
	public static final String DEFAULT_WSDL_DEFINITION_HANDLER_ADAPTER_BEAN_NAME = "wsdlDefinitionHandlerAdapter";

	/** Well-known name for the {@link XsdSchemaHandlerAdapter} object in the bean factory for this namespace. */
	public static final String DEFAULT_XSD_SCHEMA_HANDLER_ADAPTER_BEAN_NAME = "xsdSchemaHandlerAdapter";

	/** Suffix of a WSDL request uri. */
	private static final String WSDL_SUFFIX_NAME = ".wsdl";

	/** Suffix of a XSD request uri. */
	private static final String XSD_SUFFIX_NAME = ".xsd";

	private final DefaultStrategiesHelper defaultStrategiesHelper;

	private String messageFactoryBeanName = DEFAULT_MESSAGE_FACTORY_BEAN_NAME;

	private String messageReceiverHandlerAdapterBeanName = DEFAULT_MESSAGE_RECEIVER_HANDLER_ADAPTER_BEAN_NAME;

	/** The {@link WebServiceMessageReceiverHandlerAdapter} used by this servlet. */
	private WebServiceMessageReceiverHandlerAdapter messageReceiverHandlerAdapter;

	private String wsdlDefinitionHandlerAdapterBeanName = DEFAULT_WSDL_DEFINITION_HANDLER_ADAPTER_BEAN_NAME;

	/** The {@link WsdlDefinitionHandlerAdapter} used by this servlet. */
	private WsdlDefinitionHandlerAdapter wsdlDefinitionHandlerAdapter;

	private String xsdSchemaHandlerAdapterBeanName = DEFAULT_XSD_SCHEMA_HANDLER_ADAPTER_BEAN_NAME;

	/** The {@link XsdSchemaHandlerAdapter} used by this servlet. */
	private XsdSchemaHandlerAdapter xsdSchemaHandlerAdapter;

	private String messageReceiverBeanName = DEFAULT_MESSAGE_RECEIVER_BEAN_NAME;

	/** The {@link WebServiceMessageReceiver} used by this servlet. */
	private WebServiceMessageReceiver messageReceiver;

	/** Keys are bean names, values are {@link WsdlDefinition WsdlDefinitions}. */
	private Map<String, WsdlDefinition> wsdlDefinitions;

	private Map<String, XsdSchema> xsdSchemas;

	private boolean transformWsdlLocations = false;

	private boolean transformSchemaLocations = false;

	/**
	 * Public constructor, necessary for some Web application servers.
	 */
	public AgnitasMessageDispatcherServlet() {
		this(null);
	}

	/**
	 * Constructor to support programmatic configuration of the Servlet with the specified
	 * web application context. This constructor is useful in Servlet 3.0+ environments
	 * where instance-based registration of servlets is possible through the
	 * {@code ServletContext#addServlet} API.
	 * <p>Using this constructor indicates that the following properties / init-params
	 * will be ignored:
	 * <ul>
	 * <li>{@link #setContextClass(Class)} / 'contextClass'</li>
	 * <li>{@link #setContextConfigLocation(String)} / 'contextConfigLocation'</li>
	 * <li>{@link #setContextAttribute(String)} / 'contextAttribute'</li>
	 * <li>{@link #setNamespace(String)} / 'namespace'</li>
	 * </ul>
	 * <p>The given web application context may or may not yet be {@linkplain
	 * org.springframework.web.context.ConfigurableWebApplicationContext#refresh() refreshed}.
	 * If it has <strong>not</strong> already been refreshed (the recommended approach), then
	 * the following will occur:
	 * <ul>
	 * <li>If the given context does not already have a {@linkplain
	 * org.springframework.web.context.ConfigurableWebApplicationContext#setParent parent},
	 * the root application context will be set as the parent.</li>
	 * <li>If the given context has not already been assigned an {@linkplain
	 * org.springframework.web.context.ConfigurableWebApplicationContext#setId id}, one
	 * will be assigned to it</li>
	 * <li>{@code ServletContext} and {@code ServletConfig} objects will be delegated to
	 * the application context</li>
	 * <li>{@link #postProcessWebApplicationContext} will be called</li>
	 * <li>Any {@code ApplicationContextInitializer}s specified through the
	 * "contextInitializerClasses" init-param or through the {@link
	 * #setContextInitializers} property will be applied.</li>
	 * <li>{@link org.springframework.web.context.ConfigurableWebApplicationContext#refresh refresh()}
	 * will be called if the context implements
	 * {@link org.springframework.web.context.ConfigurableWebApplicationContext}</li>
	 * </ul>
	 * If the context has already been refreshed, none of the above will occur, under the
	 * assumption that the user has performed these actions (or not) per their specific
	 * needs.
	 * <p>See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
	 *
	 * @param webApplicationContext the context to use
	 * @see FrameworkServlet#FrameworkServlet(WebApplicationContext)
	 * @see org.springframework.web.WebApplicationInitializer
	 * @see #initWebApplicationContext()
	 * @see #configureAndRefreshWebApplicationContext(org.springframework.web.context.ConfigurableWebApplicationContext)
	 */
	public AgnitasMessageDispatcherServlet(WebApplicationContext webApplicationContext) {
		super(webApplicationContext);
		defaultStrategiesHelper = new DefaultStrategiesHelper(MessageDispatcherServlet.class);
	}

	/** Returns the bean name used to lookup a {@link WebServiceMessageFactory}. */
	public String getMessageFactoryBeanName() {
		return messageFactoryBeanName;
	}

	/**
	 * Sets the bean name used to lookup a {@link WebServiceMessageFactory}. Defaults to {@link
	 * #DEFAULT_MESSAGE_FACTORY_BEAN_NAME}.
	 */
	public void setMessageFactoryBeanName(String messageFactoryBeanName) {
		this.messageFactoryBeanName = messageFactoryBeanName;
	}

	/** Returns the bean name used to lookup a {@link WebServiceMessageReceiver}. */
	public String getMessageReceiverBeanName() {
		return messageReceiverBeanName;
	}

	/**
	 * Sets the bean name used to lookup a {@link WebServiceMessageReceiver}. Defaults to {@link
	 * #DEFAULT_MESSAGE_RECEIVER_BEAN_NAME}.
	 */
	public void setMessageReceiverBeanName(String messageReceiverBeanName) {
		this.messageReceiverBeanName = messageReceiverBeanName;
	}

	/**
	 * Indicates whether relative address locations in the WSDL are to be transformed using the request URI of the
	 * incoming {@link HttpServletRequest}.
	 */
	public boolean isTransformWsdlLocations() {
		return transformWsdlLocations;
	}

	/**
	 * Sets whether relative address locations in the WSDL are to be transformed using the request URI of the incoming
	 * {@link HttpServletRequest}. Defaults to {@code false}.
	 */
	public void setTransformWsdlLocations(boolean transformWsdlLocations) {
		this.transformWsdlLocations = transformWsdlLocations;
	}

	/**
	 * Indicates whether relative address locations in the XSD are to be transformed using the request URI of the
	 * incoming {@link HttpServletRequest}.
	 */
	public boolean isTransformSchemaLocations() {
		return transformSchemaLocations;
	}

	/**
	 * Sets whether relative address locations in the XSD are to be transformed using the request URI of the incoming
	 * {@link HttpServletRequest}. Defaults to {@code false}.
	 */
	public void setTransformSchemaLocations(boolean transformSchemaLocations) {
		this.transformSchemaLocations = transformSchemaLocations;
	}

	/** Returns the bean name used to lookup a {@link WebServiceMessageReceiverHandlerAdapter}. */
	public String getMessageReceiverHandlerAdapterBeanName() {
		return messageReceiverHandlerAdapterBeanName;
	}

	/**
	 * Sets the bean name used to lookup a {@link WebServiceMessageReceiverHandlerAdapter}. Defaults to {@link
	 * #DEFAULT_MESSAGE_RECEIVER_HANDLER_ADAPTER_BEAN_NAME}.
	 */
	public void setMessageReceiverHandlerAdapterBeanName(String messageReceiverHandlerAdapterBeanName) {
		this.messageReceiverHandlerAdapterBeanName = messageReceiverHandlerAdapterBeanName;
	}

	/** Returns the bean name used to lookup a {@link WsdlDefinitionHandlerAdapter}. */
	public String getWsdlDefinitionHandlerAdapterBeanName() {
		return wsdlDefinitionHandlerAdapterBeanName;
	}

	/**
	 * Sets the bean name used to lookup a {@link WsdlDefinitionHandlerAdapter}. Defaults to {@link
	 * #DEFAULT_WSDL_DEFINITION_HANDLER_ADAPTER_BEAN_NAME}.
	 */
	public void setWsdlDefinitionHandlerAdapterBeanName(String wsdlDefinitionHandlerAdapterBeanName) {
		this.wsdlDefinitionHandlerAdapterBeanName = wsdlDefinitionHandlerAdapterBeanName;
	}

	/** Returns the bean name used to lookup a {@link XsdSchemaHandlerAdapter}. */
	public String getXsdSchemaHandlerAdapterBeanName() {
		return xsdSchemaHandlerAdapterBeanName;
	}

	/**
	 * Sets the bean name used to lookup a {@link XsdSchemaHandlerAdapter}. Defaults to {@link
	 * #DEFAULT_XSD_SCHEMA_HANDLER_ADAPTER_BEAN_NAME}.
	 */
	public void setXsdSchemaHandlerAdapterBeanName(String xsdSchemaHandlerAdapterBeanName) {
		this.xsdSchemaHandlerAdapterBeanName = xsdSchemaHandlerAdapterBeanName;
	}


	@Override
	protected void doService(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
			throws Exception {
		WsdlDefinition definition = getWsdlDefinition(httpServletRequest);
		if (definition != null) {
			wsdlDefinitionHandlerAdapter.handle(httpServletRequest, httpServletResponse, definition);
			httpServletResponse.setStatus(200);
			return;
		}
		XsdSchema schema = getXsdSchema(httpServletRequest);
		if (schema != null) {
			xsdSchemaHandlerAdapter.handle(httpServletRequest, httpServletResponse, schema);
			return;
		}
		messageReceiverHandlerAdapter.handle(httpServletRequest, httpServletResponse, messageReceiver);
	}

	/**
	 * This implementation calls {@link #initStrategies}.
	 */
	@Override
	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}

	@Override
	protected long getLastModified(HttpServletRequest httpServletRequest) {
		WsdlDefinition definition = getWsdlDefinition(httpServletRequest);
		if (definition != null) {
			return wsdlDefinitionHandlerAdapter.getLastModified(httpServletRequest, definition);
		}
		XsdSchema schema = getXsdSchema(httpServletRequest);
		if (schema != null) {
			return xsdSchemaHandlerAdapter.getLastModified(httpServletRequest, schema);
		}
		return messageReceiverHandlerAdapter.getLastModified(httpServletRequest, messageReceiver);
	}

	/** Returns the {@link WebServiceMessageReceiver} used by this servlet. */
	protected WebServiceMessageReceiver getMessageReceiver() {
		return messageReceiver;
	}

	/**
	 * Determines the {@link WsdlDefinition} for a given request, or {@code null} if none is found.
	 *
	 * <p>Default implementation checks whether the request method is {@code GET}, whether the request uri ends with
	 * {@code ".wsdl"}, and if there is a {@code WsdlDefinition} with the same name as the filename in the
	 * request uri.
	 *
	 * @param request the {@code HttpServletRequest}
	 * @return a definition, or {@code null}
	 */
	protected WsdlDefinition getWsdlDefinition(HttpServletRequest request) {
		if ((HttpTransportConstants.METHOD_GET.equals(request.getMethod()) || "HEAD".equals(request.getMethod())) &&
				request.getRequestURI().endsWith(WSDL_SUFFIX_NAME)) {
			String fileName = WebUtils.extractFilenameFromUrlPath(request.getRequestURI());
			return wsdlDefinitions.get(fileName);
		}
		else {
			return null;
		}
	}

	/**
	 * Determines the {@link XsdSchema} for a given request, or {@code null} if none is found.
	 *
	 * <p>Default implementation checks whether the request method is {@code GET}, whether the request uri ends with
	 * {@code ".xsd"}, and if there is a {@code XsdSchema} with the same name as the filename in the request
	 * uri.
	 *
	 * @param request the {@code HttpServletRequest}
	 * @return a schema, or {@code null}
	 */
	protected XsdSchema getXsdSchema(HttpServletRequest request) {
		if (HttpTransportConstants.METHOD_GET.equals(request.getMethod()) &&
				request.getRequestURI().endsWith(XSD_SUFFIX_NAME)) {
			String fileName = WebUtils.extractFilenameFromUrlPath(request.getRequestURI());
			return xsdSchemas.get(fileName);
		}
		else {
			return null;
		}
	}

	/**
	 * Initialize the strategy objects that this servlet uses.
	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
	 */
	protected void initStrategies(ApplicationContext context) {
		initMessageReceiverHandlerAdapter(context);
		initWsdlDefinitionHandlerAdapter(context);
		initXsdSchemaHandlerAdapter(context);
		initMessageReceiver(context);
		initWsdlDefinitions(context);
		initXsdSchemas(context);
	}


	private void initMessageReceiverHandlerAdapter(ApplicationContext context) {
		try {
			try {
				messageReceiverHandlerAdapter = context.getBean(getMessageReceiverHandlerAdapterBeanName(),
						WebServiceMessageReceiverHandlerAdapter.class);
			}
			catch (NoSuchBeanDefinitionException ignored) {
				messageReceiverHandlerAdapter = new WebServiceMessageReceiverHandlerAdapter();
			}
			initWebServiceMessageFactory(context);
			messageReceiverHandlerAdapter.afterPropertiesSet();
		}
		catch (Exception ex) {
			throw new BeanInitializationException("Could not initialize WebServiceMessageReceiverHandlerAdapter", ex);
		}
	}

	private void initWebServiceMessageFactory(ApplicationContext context) {
		WebServiceMessageFactory messageFactory;
		try {
			messageFactory = context.getBean(getMessageFactoryBeanName(), WebServiceMessageFactory.class);
		}
		catch (NoSuchBeanDefinitionException ignored) {
			messageFactory = defaultStrategiesHelper
					.getDefaultStrategy(WebServiceMessageFactory.class, context);
			if (logger.isDebugEnabled()) {
				logger.debug("No WebServiceMessageFactory found in servlet '" + getServletName() + "': using default");
			}
		}
		messageReceiverHandlerAdapter.setMessageFactory(messageFactory);
	}

	private void initWsdlDefinitionHandlerAdapter(ApplicationContext context) {
		try {
			try {
				wsdlDefinitionHandlerAdapter =
						context.getBean(getWsdlDefinitionHandlerAdapterBeanName(), WsdlDefinitionHandlerAdapter.class);

			}
			catch (NoSuchBeanDefinitionException ignored) {
				wsdlDefinitionHandlerAdapter = new WsdlDefinitionHandlerAdapter();
			}
			wsdlDefinitionHandlerAdapter.setTransformLocations(isTransformWsdlLocations());
			wsdlDefinitionHandlerAdapter.setTransformSchemaLocations(isTransformSchemaLocations());
			wsdlDefinitionHandlerAdapter.afterPropertiesSet();
		}
		catch (Exception ex) {
			throw new BeanInitializationException("Could not initialize WsdlDefinitionHandlerAdapter", ex);
		}
	}

	private void initXsdSchemaHandlerAdapter(ApplicationContext context) {
		try {
			try {
				xsdSchemaHandlerAdapter = context
						.getBean(getXsdSchemaHandlerAdapterBeanName(), XsdSchemaHandlerAdapter.class);

			}
			catch (NoSuchBeanDefinitionException ignored) {
				xsdSchemaHandlerAdapter = new XsdSchemaHandlerAdapter();
			}
			xsdSchemaHandlerAdapter.setTransformSchemaLocations(isTransformSchemaLocations());
			xsdSchemaHandlerAdapter.afterPropertiesSet();
		}
		catch (Exception ex) {
			throw new BeanInitializationException("Could not initialize XsdSchemaHandlerAdapter", ex);
		}
	}

	private void initMessageReceiver(ApplicationContext context) {
		try {
			messageReceiver = context.getBean(getMessageReceiverBeanName(), WebServiceMessageReceiver.class);
		}
		catch (NoSuchBeanDefinitionException ex) {
			messageReceiver = defaultStrategiesHelper
					.getDefaultStrategy(WebServiceMessageReceiver.class, context);
			if (messageReceiver instanceof BeanNameAware) {
				((BeanNameAware) messageReceiver).setBeanName(getServletName());
			}
			if (logger.isDebugEnabled()) {
				logger.debug("No MessageDispatcher found in servlet '" + getServletName() + "': using default");
			}
		}
	}

	private void initWsdlDefinitions(ApplicationContext context) {
		wsdlDefinitions = BeanFactoryUtils
				.beansOfTypeIncludingAncestors(context, WsdlDefinition.class, true, false);
		if (logger.isDebugEnabled()) {
			for (Map.Entry<String, WsdlDefinition> entry : wsdlDefinitions.entrySet()) {
				String beanName = entry.getKey();
				WsdlDefinition definition = entry.getValue();
				logger.debug("Published [" + definition + "] as " + beanName + WSDL_SUFFIX_NAME);
			}
		}
	}

	private void initXsdSchemas(ApplicationContext context) {
		xsdSchemas = BeanFactoryUtils
				.beansOfTypeIncludingAncestors(context, XsdSchema.class, true, false);
		if (logger.isDebugEnabled()) {
			for (Map.Entry<String, XsdSchema> entry : xsdSchemas.entrySet()) {
				String beanName = entry.getKey();
				XsdSchema schema = entry.getValue();
				logger.debug("Published [" + schema + "] as " + beanName + XSD_SUFFIX_NAME);
			}
		}
	}
}
