/*
 * Copyright 2002-present 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.web.servlet.view.freemarker;

import java.util.Locale;

import freemarker.template.Configuration;
import org.jspecify.annotations.Nullable;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
import org.springframework.web.servlet.view.AbstractUrlBasedView;

/**
 * Convenience subclass of {@link org.springframework.web.servlet.view.UrlBasedViewResolver}
 * that supports {@link FreeMarkerView} (i.e. FreeMarker templates) and custom subclasses of it.
 *
 * <p>The view class for all views generated by this resolver can be specified
 * via the "viewClass" property. See {@code UrlBasedViewResolver} for details.
 *
 * <p><b>Note:</b> To ensure that the correct encoding is used when the rendering
 * the response, set the {@linkplain #setContentType(String) content type} with an
 * appropriate {@code charset} attribute &mdash; for example,
 * {@code "text/html;charset=UTF-8"}; however, as of Spring Framework 6.2, it is
 * no longer strictly necessary to explicitly set the content type in the
 * {@code FreeMarkerViewResolver} if you have set an explicit encoding via either
 * {@link FreeMarkerView#setEncoding(String)},
 * {@link FreeMarkerConfigurer#setDefaultEncoding(String)}, or
 * {@link Configuration#setDefaultEncoding(String)}.
 *
 * <p><b>Note:</b> When chaining ViewResolvers, a {@code FreeMarkerViewResolver} will
 * check for the existence of the specified template resources and only return
 * a non-null {@code View} object if the template was actually found.
 *
 * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
 *
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @since 1.1
 * @see #setViewClass
 * @see #setPrefix
 * @see #setSuffix
 * @see #setContentType
 * @see #setRequestContextAttribute
 * @see #setExposeSpringMacroHelpers
 * @see FreeMarkerView
 */
public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {

	/**
	 * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}:
	 * by default {@link FreeMarkerView}.
	 */
	public FreeMarkerViewResolver() {
		setViewClass(requiredViewClass());
	}

	/**
	 * A convenience constructor that allows for specifying {@link #setPrefix prefix}
	 * and {@link #setSuffix suffix} as constructor arguments.
	 * @param prefix the prefix that gets prepended to view names when building a URL
	 * @param suffix the suffix that gets appended to view names when building a URL
	 * @since 4.3
	 */
	public FreeMarkerViewResolver(String prefix, String suffix) {
		this();
		setPrefix(prefix);
		setSuffix(suffix);
	}


	/**
	 * Requires {@link FreeMarkerView}.
	 */
	@Override
	protected Class<?> requiredViewClass() {
		return FreeMarkerView.class;
	}

	@Override
	protected AbstractUrlBasedView instantiateView() {
		return (getViewClass() == FreeMarkerView.class ? new FreeMarkerView() : super.instantiateView());
	}

	/**
	 * Delegates to {@code super.loadView(viewName, locale)} for standard behavior
	 * and then to {@link #postProcessView(FreeMarkerView)} for customization.
	 * @since 6.2
	 * @see org.springframework.web.servlet.view.UrlBasedViewResolver#loadView(String, Locale)
	 * @see #postProcessView(FreeMarkerView)
	 */
	@Override
	protected @Nullable View loadView(String viewName, Locale locale) throws Exception {
		View view = super.loadView(viewName, locale);
		if (view instanceof FreeMarkerView freeMarkerView) {
			postProcessView(freeMarkerView);
		}
		return view;
	}

	/**
	 * Post process the supplied {@link FreeMarkerView} after it has been {@linkplain
	 * org.springframework.web.servlet.view.UrlBasedViewResolver#loadView(String, Locale)
	 * loaded}.
	 * <p>The default implementation attempts to override the
	 * {@linkplain org.springframework.web.servlet.view.AbstractView#setContentType(String)
	 * content type} of the view with {@code "text/html;charset=<encoding>"},
	 * where {@code <encoding>} is equal to an explicitly configured character
	 * encoding for the underlying FreeMarker template file. If an explicit content
	 * type has been configured for this view resolver or if no explicit character
	 * encoding has been configured for the template file, this method does not
	 * modify the supplied {@code FreeMarkerView}.
	 * @since 6.2
	 * @see #loadView(String, Locale)
	 * @see #setContentType(String)
	 * @see org.springframework.web.servlet.view.AbstractView#setContentType(String)
	 */
	protected void postProcessView(FreeMarkerView freeMarkerView) {
		// If an explicit content type has been configured for all views, it has
		// already been set in the view in UrlBasedViewResolver#buildView(String),
		// and there is no need to override it here.
		if (getContentType() != null) {
			return;
		}

		// Check if the view has an explicit encoding set.
		String encoding = freeMarkerView.getEncoding();
		if (encoding == null) {
			// If an explicit encoding has not been configured for this particular view,
			// use the explicit default encoding for the FreeMarker Configuration, if set.
			Configuration configuration = freeMarkerView.obtainConfiguration();
			if (configuration.isDefaultEncodingExplicitlySet()) {
				encoding = configuration.getDefaultEncoding();
			}
		}
		if (StringUtils.hasText(encoding)) {
			String contentType = "text/html;charset=" + encoding;
			if (logger.isDebugEnabled()) {
				logger.debug("Setting Content-Type for view [%s] to: %s".formatted(freeMarkerView, contentType));
			}
			freeMarkerView.setContentType(contentType);
		}
	}

}
