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

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.server.EndpointInterceptor;
import org.springframework.xml.transform.TransformerObjectSupport;

/**
 * Abstract base class for {@code EndpointInterceptor} instances that log a part of a
 * {@code WebServiceMessage}. By default, both request and response messages are logged,
 * but this behaviour can be changed using the {@code logRequest} and {@code logResponse}
 * properties.
 *
 * @author Arjen Poutsma
 * @since 1.0.0
 */
public abstract class AbstractLoggingInterceptor extends TransformerObjectSupport implements EndpointInterceptor {

	/**
	 * The default {@code Log} instance used to write trace messages. This instance is
	 * mapped to the implementing {@code Class}.
	 */
	protected transient Log logger = LogFactory.getLog(getClass());

	private boolean logRequest = true;

	private boolean logResponse = true;

	/** Indicates whether the request should be logged. Default is {@code true}. */
	public final void setLogRequest(boolean logRequest) {
		this.logRequest = logRequest;
	}

	/** Indicates whether the response should be logged. Default is {@code true}. */
	public final void setLogResponse(boolean logResponse) {
		this.logResponse = logResponse;
	}

	/**
	 * Set the name of the logger to use. The name will be passed to the underlying logger
	 * implementation through Commons Logging, getting interpreted as log category
	 * according to the logger's configuration.
	 * <p>
	 * This can be specified to not log into the category of a class but rather into a
	 * specific named category.
	 */
	public void setLoggerName(String loggerName) {
		this.logger = LogFactory.getLog(loggerName);
	}

	/**
	 * Logs the request message payload. Logging only occurs if {@code logRequest} is set
	 * to {@code true}, which is the default.
	 * @param messageContext the message context
	 * @return {@code true}
	 * @throws TransformerException when the payload cannot be transformed to a string
	 */
	@Override
	public boolean handleRequest(MessageContext messageContext, Object endpoint) throws TransformerException {
		if (this.logRequest && isLogEnabled()) {
			logMessageSource("Request: ", getSource(messageContext.getRequest()));
		}
		return true;
	}

	/**
	 * Logs the response message payload. Logging only occurs if {@code logResponse} is
	 * set to {@code true}, which is the default.
	 * @param messageContext the message context
	 * @return {@code true}
	 * @throws TransformerException when the payload cannot be transformed to a string
	 */
	@Override
	public boolean handleResponse(MessageContext messageContext, Object endpoint) throws Exception {
		if (this.logResponse && isLogEnabled()) {
			logMessageSource("Response: ", getSource(messageContext.getResponse()));
		}
		return true;
	}

	/** Does nothing by default. Faults are not logged. */
	@Override
	public boolean handleFault(MessageContext messageContext, Object endpoint) throws Exception {
		return true;
	}

	/**
	 * Does nothing by default.
	 */
	@Override
	public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) {
	}

	/**
	 * Determine whether the {@link #logger} field is enabled.
	 * <p>
	 * Default is {@code true} when the "debug" level is enabled. Subclasses can override
	 * this to change the level under which logging occurs.
	 */
	protected boolean isLogEnabled() {
		return this.logger.isDebugEnabled();
	}

	private Transformer createNonIndentingTransformer() throws TransformerConfigurationException {
		Transformer transformer = createTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		transformer.setOutputProperty(OutputKeys.INDENT, "no");
		return transformer;
	}

	/**
	 * Logs the given {@link Source source} to the {@link #logger}, using the message as a
	 * prefix.
	 * <p>
	 * By default, this message creates a string representation of the given source, and
	 * delegates to {@link #logMessage(String)}.
	 * @param logMessage the log message
	 * @param source the source to be logged
	 * @throws TransformerException in case of errors
	 */
	protected void logMessageSource(String logMessage, Source source) throws TransformerException {
		if (source != null) {
			Transformer transformer = createNonIndentingTransformer();
			StringWriter writer = new StringWriter();
			transformer.transform(source, new StreamResult(writer));
			String message = logMessage + writer;
			logMessage(message);
		}
	}

	/**
	 * Logs the given string message.
	 * <p>
	 * By default, this method uses a "debug" level of logging. Subclasses can override
	 * this method to change the level of logging used by the logger.
	 * @param message the message
	 */
	protected void logMessage(String message) {
		this.logger.debug(message);
	}

	/**
	 * Abstract template method that returns the {@code Source} for the given
	 * {@code WebServiceMessage}.
	 * @param message the message
	 * @return the source of the message
	 */
	protected abstract Source getSource(WebServiceMessage message);

}
