/*
 * Copyright 2002-2004 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
 *
 *      http://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.tiles;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.tiles.ComponentContext;
import org.apache.struts.tiles.ComponentDefinition;
import org.apache.struts.tiles.Controller;
import org.apache.struts.tiles.DefinitionsFactory;
import org.apache.struts.tiles.TilesUtilImpl;

import org.springframework.context.ApplicationContextException;
import org.springframework.web.servlet.view.InternalResourceView;

/**
 * View implementation that retrieves a Tiles definition.
 * The "url" property is interpreted as name of a Tiles definition.
 *
 * <p>TilesJstlView with JSTL support is a separate class,
 * mainly to avoid JSTL dependencies in this class.
 *
 * <p>Depends on a Tiles DefinitionsFactory which must be available
 * in the ServletContext. This factory is typically set up via a
 * TilesConfigurer bean definition in the application context.
 *
 * <p>A component controller specified in the Tiles definition will receive
 * a reference to the current Spring ApplicationContext if it implements
 * ApplicationContextAware. The ComponentControllerSupport class provides
 * a convenient base class for such Spring-aware component controllers.
 *
 * @author Alef Arendsen
 * @author Juergen Hoeller
 * @see #setUrl
 * @see TilesJstlView
 * @see TilesConfigurer
 * @see ComponentControllerSupport
 * @see org.springframework.context.ApplicationContextAware
 */
public class TilesView extends InternalResourceView {

	/**
	 * Name of the attribute that will override the path of the layout page
	 * to render. A Tiles component controller can set such an attribute
	 * to dynamically switch the look and feel of a Tiles page.
	 * @see #setPath
	 */
	public static final String PATH_ATTRIBUTE = TilesView.class.getName() + ".PATH";

	/**
	 * Set the path of the layout page to render.
	 * @param request current HTTP request
	 * @param path the path of the layout page
	 * @see #PATH_ATTRIBUTE
	 */
	public static void setPath(HttpServletRequest request, String path) {
		request.setAttribute(PATH_ATTRIBUTE, path);
	}


	private DefinitionsFactory definitionsFactory;

	protected void initApplicationContext() throws ApplicationContextException {
		super.initApplicationContext();

		// get definitions factory
		this.definitionsFactory =
				(DefinitionsFactory) getServletContext().getAttribute(TilesUtilImpl.DEFINITIONS_FACTORY);
		if (this.definitionsFactory == null) {
			throw new ApplicationContextException("Tiles definitions factory not found: TilesConfigurer not defined?");
		}
	}

	/**
	 * Prepare for rendering the Tiles definition: Execute the associated
	 * component controller if any, and determine the request dispatcher path.
	 */
	protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		// get component definition
		ComponentDefinition definition = getComponentDefinition(this.definitionsFactory, request);
		if (definition == null) {
			throw new ServletException("No Tiles definition found for name '" + getUrl() + "'");
		}

		// get current component context
		ComponentContext context = getComponentContext(definition, request);

		// execute component controller associated with definition, if any
		Controller controller = getController(definition, request);
		if (controller != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Executing Tiles controller [" + controller + "]");
			}
			executeController(controller, context, request, response);
		}

		// determine the path of the definition
		String path = getDispatcherPath(definition, request);
		if (path == null) {
			throw new ServletException(
					"Could not determine a path for Tiles definition '" + definition.getName() + "'");
		}

		return path;
	}

	/**
	 * Determine the Tiles component definition for the given Tiles
	 * definitions factory.
	 * @param factory the Tiles definitions factory
	 * @param request current HTTP request
	 * @return the component definition
	 */
	protected ComponentDefinition getComponentDefinition(DefinitionsFactory factory, HttpServletRequest request)
	    throws Exception {
		return factory.getDefinition(getUrl(), request, getServletContext());
	}

	/**
	 * Determine the Tiles component context for the given Tiles definition.
	 * @param definition the Tiles definition to render
	 * @param request current HTTP request
	 * @return the component context
	 * @throws Exception if preparations failed
	 */
	protected ComponentContext getComponentContext(ComponentDefinition definition, HttpServletRequest request)
	    throws Exception {
		ComponentContext context = ComponentContext.getContext(request);
		if (context == null) {
			context = new ComponentContext(definition.getAttributes());
			ComponentContext.setContext(context, request);
		}
		else {
			context.addMissing(definition.getAttributes());
		}
		return context;
	}

	/**
	 * Determine and initialize the Tiles component controller for the
	 * given Tiles definition, if any.
	 * @param definition the Tiles definition to render
	 * @param request current HTTP request
	 * @return the component controller to execute, or null if none
	 * @throws Exception if preparations failed
	 */
	protected Controller getController(ComponentDefinition definition, HttpServletRequest request)
			throws Exception {
		return definition.getOrCreateController();
	}

	/**
	 * Execute the given Tiles controller.
	 * @param controller the component controller to execute
	 * @param context the component context
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception if controller execution failed
	 */
	protected void executeController(
			Controller controller, ComponentContext context, HttpServletRequest request, HttpServletResponse response)
	    throws Exception {
		controller.perform(context, request, response, getServletContext());
	}

	/**
	 * Determine the dispatcher path for the given Tiles definition,
	 * i.e. the request dispatcher path of the layout page.
	 * @param definition the Tiles definition to render
	 * @param request current HTTP request
	 * @return the path of the layout page to render
	 * @throws Exception if preparations failed
	 */
	protected String getDispatcherPath(ComponentDefinition definition, HttpServletRequest request)
	    throws Exception {
		Object pathAttr = request.getAttribute(PATH_ATTRIBUTE);
		return (pathAttr != null ? pathAttr.toString() : definition.getPath());
	}

}
