/*
 * 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.server.endpoint;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Locale;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.NodeFactory;
import nu.xom.ParentNode;
import nu.xom.ParsingException;
import nu.xom.Serializer;
import nu.xom.converters.DOMConverter;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import org.springframework.core.NestedRuntimeException;
import org.springframework.xml.namespace.QNameUtils;
import org.springframework.xml.transform.TransformerObjectSupport;
import org.springframework.xml.transform.TraxUtils;

/**
 * Abstract base class for endpoints that handle the message payload as XOM elements.
 * Offers the message payload as a XOM {@code Element}, and allows subclasses to create a
 * response by returning an {@code Element}.
 * <p>
 * An {@code AbstractXomPayloadEndpoint} only accept one payload element. Multiple payload
 * elements are not in accordance with WS-I.
 *
 * @author Arjen Poutsma
 * @since 1.0.0
 * @see Element
 * @deprecated as of Spring Web Services 2.0, in favor of annotated endpoints
 */
@Deprecated
@SuppressWarnings("Since15")
public abstract class AbstractXomPayloadEndpoint extends TransformerObjectSupport implements PayloadEndpoint {

	@Override
	public final Source invoke(Source request) throws Exception {
		Element requestElement = null;
		if (request != null) {
			XomSourceCallback sourceCallback = new XomSourceCallback();
			try {
				TraxUtils.doWithSource(request, sourceCallback);
			}
			catch (XomParsingException ex) {
				throw (ParsingException) ex.getCause();
			}
			requestElement = sourceCallback.element;
		}
		Element responseElement = invokeInternal(requestElement);
		return (responseElement != null) ? convertResponse(responseElement) : null;
	}

	private Source convertResponse(Element responseElement) throws IOException {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		Serializer serializer = createSerializer(os);
		Document document = responseElement.getDocument();
		if (document == null) {
			document = new Document(responseElement);
		}
		serializer.write(document);
		byte[] bytes = os.toByteArray();
		return new StreamSource(new ByteArrayInputStream(bytes));
	}

	/**
	 * Creates a {@link Serializer} to be used for writing the response to.
	 * <p>
	 * Default implementation uses the UTF-8 encoding and does not set any options, but
	 * this may be changed in subclasses.
	 * @param outputStream the output stream to serialize to
	 * @return the serializer
	 */
	protected Serializer createSerializer(OutputStream outputStream) {
		return new Serializer(outputStream);
	}

	/**
	 * Template method. Subclasses must implement this. Offers the request payload as a
	 * XOM {@code Element}, and allows subclasses to return a response {@code Element}.
	 * @param requestElement the contents of the SOAP message as XOM element
	 * @return the response element. Can be {@code null} to specify no response.
	 */
	protected abstract Element invokeInternal(Element requestElement) throws Exception;

	private static final class XomSourceCallback implements TraxUtils.SourceCallback {

		private Element element;

		@Override
		public void domSource(Node node) {
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				this.element = DOMConverter.convert((org.w3c.dom.Element) node);
			}
			else if (node.getNodeType() == Node.DOCUMENT_NODE) {
				Document document = DOMConverter.convert((org.w3c.dom.Document) node);
				this.element = document.getRootElement();
			}
			else {
				throw new IllegalArgumentException("DOMSource contains neither Document nor Element");
			}
		}

		@Override
		public void saxSource(XMLReader reader, InputSource inputSource) throws IOException, SAXException {
			try {
				Builder builder = new Builder(reader);
				Document document;
				if (inputSource.getByteStream() != null) {
					document = builder.build(inputSource.getByteStream());
				}
				else if (inputSource.getCharacterStream() != null) {
					document = builder.build(inputSource.getCharacterStream());
				}
				else {
					throw new IllegalArgumentException(
							"InputSource in SAXSource contains neither byte stream nor character stream");
				}
				this.element = document.getRootElement();
			}
			catch (ParsingException ex) {
				throw new XomParsingException(ex);
			}
		}

		@Override
		public void staxSource(XMLEventReader eventReader) throws XMLStreamException {
			throw new IllegalArgumentException("XMLEventReader not supported");
		}

		@Override
		public void staxSource(XMLStreamReader streamReader) throws XMLStreamException {
			Document document = StaxStreamConverter.convert(streamReader);
			this.element = document.getRootElement();
		}

		@Override
		public void streamSource(InputStream inputStream) throws IOException {
			try {
				Builder builder = new Builder();
				Document document = builder.build(inputStream);
				this.element = document.getRootElement();
			}
			catch (ParsingException ex) {
				throw new XomParsingException(ex);
			}
		}

		@Override
		public void streamSource(Reader reader) throws IOException {
			try {
				Builder builder = new Builder();
				Document document = builder.build(reader);
				this.element = document.getRootElement();
			}
			catch (ParsingException ex) {
				throw new XomParsingException(ex);
			}
		}

		@Override
		public void source(String systemId) throws Exception {
			try {
				Builder builder = new Builder();
				Document document = builder.build(systemId);
				this.element = document.getRootElement();
			}
			catch (ParsingException ex) {
				throw new XomParsingException(ex);
			}
		}

	}

	@SuppressWarnings("serial")
	private static final class XomParsingException extends NestedRuntimeException {

		private XomParsingException(ParsingException ex) {
			super(ex.getMessage(), ex);
		}

	}

	private static final class StaxStreamConverter {

		private static Document convert(XMLStreamReader streamReader) throws XMLStreamException {
			NodeFactory nodeFactory = new NodeFactory();
			Document document = null;
			Element element = null;
			ParentNode parent = null;
			boolean documentFinished = false;
			while (streamReader.hasNext()) {
				int event = streamReader.next();
				switch (event) {
					case XMLStreamConstants.START_DOCUMENT:
						document = nodeFactory.startMakingDocument();
						parent = document;
						break;
					case XMLStreamConstants.END_DOCUMENT:
						nodeFactory.finishMakingDocument(document);
						documentFinished = true;
						break;
					case XMLStreamConstants.START_ELEMENT:
						if (document == null) {
							document = nodeFactory.startMakingDocument();
							parent = document;
						}
						String name = QNameUtils.toQualifiedName(streamReader.getName());
						if (element == null) {
							element = nodeFactory.makeRootElement(name, streamReader.getNamespaceURI());
							document.setRootElement(element);
						}
						else {
							element = nodeFactory.startMakingElement(name, streamReader.getNamespaceURI());
							parent.appendChild(element);
						}
						convertNamespaces(streamReader, element);
						convertAttributes(streamReader, nodeFactory);
						parent = element;
						break;
					case XMLStreamConstants.END_ELEMENT:
						nodeFactory.finishMakingElement(element);
						parent = parent.getParent();
						break;
					case XMLStreamConstants.ATTRIBUTE:
						convertAttributes(streamReader, nodeFactory);
						break;
					case XMLStreamConstants.CHARACTERS:
						nodeFactory.makeText(streamReader.getText());
						break;
					case XMLStreamConstants.COMMENT:
						nodeFactory.makeComment(streamReader.getText());
						break;
					default:
						break;
				}
			}
			if (!documentFinished) {
				nodeFactory.finishMakingDocument(document);
			}
			return document;
		}

		private static void convertNamespaces(XMLStreamReader streamReader, Element element) {
			for (int i = 0; i < streamReader.getNamespaceCount(); i++) {
				String uri = streamReader.getNamespaceURI(i);
				String prefix = streamReader.getNamespacePrefix(i);

				element.addNamespaceDeclaration(prefix, uri);
			}

		}

		private static void convertAttributes(XMLStreamReader streamReader, NodeFactory nodeFactory) {
			for (int i = 0; i < streamReader.getAttributeCount(); i++) {
				String name = QNameUtils.toQualifiedName(streamReader.getAttributeName(i));
				String uri = streamReader.getAttributeNamespace(i);
				String value = streamReader.getAttributeValue(i);
				Attribute.Type type = convertAttributeType(streamReader.getAttributeType(i));

				nodeFactory.makeAttribute(name, uri, value, type);
			}
		}

		private static Attribute.Type convertAttributeType(String type) {
			type = type.toUpperCase(Locale.ENGLISH);
			return switch (type) {
				case "CDATA" -> Attribute.Type.CDATA;
				case "ENTITIES" -> Attribute.Type.ENTITIES;
				case "ENTITY" -> Attribute.Type.ENTITY;
				case "ENUMERATION" -> Attribute.Type.ENUMERATION;
				case "ID" -> Attribute.Type.ID;
				case "IDREF" -> Attribute.Type.IDREF;
				case "IDREFS" -> Attribute.Type.IDREFS;
				case "NMTOKEN" -> Attribute.Type.NMTOKEN;
				case "NMTOKENS" -> Attribute.Type.NMTOKENS;
				case "NOTATION" -> Attribute.Type.NOTATION;
				default -> Attribute.Type.UNDECLARED;
			};
		}

	}

}
