/*
 * 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.transport.http;

import java.util.HashMap;
import java.util.Map;

import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.w3c.dom.Document;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.ws.wsdl.WsdlDefinition;
import org.springframework.xml.xpath.XPathExpression;
import org.springframework.xml.xpath.XPathExpressionFactory;

/**
 * Adapter to use the {@code WsdlDefinition} interface with the generic
 * {@code DispatcherServlet}.
 * <p>
 * Reads the source from the mapped {@code WsdlDefinition} implementation, and writes that
 * as the result to the {@code HttpServletResponse}.
 * <p>
 * If the property {@code transformLocations} is set to {@code true}, this adapter will
 * change {@code location} attributes in the WSDL definition to reflect the URL of the
 * incoming request. If the location field in the original WSDL is an absolute path, the
 * scheme, hostname, and port will be changed. If the location is a relative path, the
 * scheme, hostname, port, and context path will be prepended. This behavior can be
 * customized by overriding the {@code transformLocation()} method.
 * <p>
 * For instance, if the location attribute defined in the WSDL is
 * {@code http://localhost:8080/context/services/myService}, and the request URI for the
 * WSDL is {@code http://example.com/context/myService.wsdl}, the location will be changed
 * to {@code http://example.com/context/services/myService}.
 * <p>
 * If the location attribute defined in the WSDL is {@code /services/myService}, and the
 * request URI for the WSDL is {@code http://example.com:8080/context/myService.wsdl}, the
 * location will be changed to {@code http://example.com:8080/context/services/myService}.
 * <p>
 * When {@code transformLocations} is enabled, all {@code location} attributes found in
 * the WSDL definition are changed by default. This behavior can be customized by changing
 * the {@code locationExpression} property, which is an XPath expression that matches the
 * attributes to change.
 *
 * @author Arjen Poutsma
 * @since 1.0.0
 * @see WsdlDefinition
 * @see #setTransformLocations(boolean)
 * @see #setLocationExpression(String)
 * @see #transformLocation(String,jakarta.servlet.http.HttpServletRequest)
 */
public class WsdlDefinitionHandlerAdapter extends LocationTransformerObjectSupport
		implements HandlerAdapter, InitializingBean {

	/**
	 * Default XPath expression used for extracting all {@code location} attributes from
	 * the WSDL definition.
	 */
	public static final String DEFAULT_LOCATION_EXPRESSION = "//@location";

	/**
	 * Default XPath expression used for extracting all {@code schemaLocation} attributes
	 * from the WSDL definition.
	 */
	public static final String DEFAULT_SCHEMA_LOCATION_EXPRESSION = "//@schemaLocation";

	private static final String CONTENT_TYPE = "text/xml";

	private final Map<String, String> expressionNamespaces = new HashMap<>();

	private String locationExpression = DEFAULT_LOCATION_EXPRESSION;

	private String schemaLocationExpression = DEFAULT_SCHEMA_LOCATION_EXPRESSION;

	private XPathExpression locationXPathExpression;

	private XPathExpression schemaLocationXPathExpression;

	private boolean transformLocations = false;

	private boolean transformSchemaLocations = false;

	/**
	 * Sets the XPath expression used for extracting the {@code location} attributes from
	 * the WSDL 1.1 definition.
	 * <p>
	 * Defaults to {@code DEFAULT_LOCATION_EXPRESSION}.
	 */
	public void setLocationExpression(String locationExpression) {
		this.locationExpression = locationExpression;
	}

	/**
	 * Sets the XPath expression used for extracting the {@code schemaLocation} attributes
	 * from the WSDL 1.1 definition.
	 * <p>
	 * Defaults to {@code DEFAULT_SCHEMA_LOCATION_EXPRESSION}.
	 */
	public void setSchemaLocationExpression(String schemaLocationExpression) {
		this.schemaLocationExpression = schemaLocationExpression;
	}

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

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

	@Override
	@Deprecated
	public long getLastModified(HttpServletRequest request, Object handler) {
		Source definitionSource = ((WsdlDefinition) handler).getSource();
		return LastModifiedHelper.getLastModified(definitionSource);
	}

	@Override
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		if (HttpTransportConstants.METHOD_GET.equals(request.getMethod())) {
			WsdlDefinition definition = (WsdlDefinition) handler;

			Transformer transformer = createTransformer();
			Source definitionSource = definition.getSource();

			if (this.transformLocations || this.transformSchemaLocations) {
				DOMResult domResult = new DOMResult();
				transformer.transform(definitionSource, domResult);
				Document definitionDocument = (Document) domResult.getNode();
				if (this.transformLocations) {
					transformLocations(definitionDocument, request);
				}
				if (this.transformSchemaLocations) {
					transformSchemaLocations(definitionDocument, request);
				}
				definitionSource = new DOMSource(definitionDocument);
			}

			response.setContentType(CONTENT_TYPE);
			StreamResult responseResult = new StreamResult(response.getOutputStream());
			transformer.transform(definitionSource, responseResult);
		}
		else {
			response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
		}
		return null;
	}

	@Override
	public boolean supports(Object handler) {
		return handler instanceof WsdlDefinition;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		this.locationXPathExpression = XPathExpressionFactory.createXPathExpression(this.locationExpression,
				this.expressionNamespaces);
		this.schemaLocationXPathExpression = XPathExpressionFactory.createXPathExpression(this.schemaLocationExpression,
				this.expressionNamespaces);
	}

	/**
	 * Transforms all {@code location} attributes to reflect the server name given
	 * {@code HttpServletRequest}. Determines the suitable attributes by evaluating the
	 * defined XPath expression, and delegates to {@code transformLocation} to do the
	 * transformation for all attributes that match.
	 * <p>
	 * This method is only called when the {@code transformLocations} property is true.
	 * @see #setLocationExpression(String)
	 * @see #setTransformLocations(boolean)
	 * @see #transformLocation(String,jakarta.servlet.http.HttpServletRequest)
	 */
	protected void transformLocations(Document definitionDocument, HttpServletRequest request) throws Exception {
		transformLocations(this.locationXPathExpression, definitionDocument, request);
	}

	/**
	 * Transforms all {@code schemaLocation} attributes to reflect the server name given
	 * {@code HttpServletRequest}. Determines the suitable attributes by evaluating the
	 * defined XPath expression, and delegates to {@code transformLocation} to do the
	 * transformation for all attributes that match.
	 * <p>
	 * This method is only called when the {@code transformSchemaLocations} property is
	 * true.
	 * @see #setSchemaLocationExpression(String)
	 * @see #setTransformSchemaLocations(boolean)
	 * @see #transformLocation(String,jakarta.servlet.http.HttpServletRequest)
	 */
	protected void transformSchemaLocations(Document definitionDocument, HttpServletRequest request) throws Exception {
		transformLocations(this.schemaLocationXPathExpression, definitionDocument, request);
	}

}
