blob: f935ec1265929b7349d6bddc592e950e32741a7d [file] [log] [blame]
/*
* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.tiles.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tiles.Attribute;
import org.apache.tiles.AttributeContext;
import org.apache.tiles.Definition;
import org.apache.tiles.TilesApplicationContext;
import org.apache.tiles.TilesContainer;
import org.apache.tiles.TilesException;
import org.apache.tiles.Attribute.AttributeType;
import org.apache.tiles.context.BasicAttributeContext;
import org.apache.tiles.context.TilesContextFactory;
import org.apache.tiles.context.TilesRequestContext;
import org.apache.tiles.definition.DefinitionsFactory;
import org.apache.tiles.definition.DefinitionsFactoryException;
import org.apache.tiles.definition.NoSuchDefinitionException;
import org.apache.tiles.preparer.NoSuchPreparerException;
import org.apache.tiles.preparer.PreparerFactory;
import org.apache.tiles.preparer.ViewPreparer;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Basic implementation of the tiles container interface.
* In most cases, this container will be customized by
* injecting customized services, not necessarily by
* override the container
*
* @since 2.0
* @version $Rev$ $Date$
*/
public class BasicTilesContainer implements TilesContainer {
/**
* Constant representing the configuration parameter
* used to define the tiles definition resources.
*/
public static final String DEFINITIONS_CONFIG = "org.apache.tiles.impl.BasicTilesContainer.DEFINITIONS_CONFIG";
/**
* Compatibility constant.
*
* @deprecated use {@link #DEFINITIONS_CONFIG} to avoid namespace collisions.
*/
private static final String LEGACY_DEFINITIONS_CONFIG = "definitions-config";
/**
* Log instance for all BasicTilesContainer
* instances.
*/
private static final Log LOG =
LogFactory.getLog(BasicTilesContainer.class);
/**
* The Tiles application context object.
*/
private TilesApplicationContext context;
/**
* The definitions factory.
*/
private DefinitionsFactory definitionsFactory;
/**
* The preparer factory.
*/
private PreparerFactory preparerFactory;
/**
* The Tiles context factory.
*/
private TilesContextFactory contextFactory;
/**
* Initialization flag. If set, this container cannot be changed.
*/
private boolean initialized = false;
/**
* Initialize the Container with the given configuration.
*
* @param initParameters application context for this container
* @throws TilesException If something goes wrong during initialization.
*/
public void init(Map<String, String> initParameters) throws TilesException {
checkInit();
initialized = true;
if (LOG.isInfoEnabled()) {
LOG.info("Initializing Tiles2 container. . .");
}
//Everything is now initialized. We will populate
// our definitions
initializeDefinitionsFactory(definitionsFactory, getResourceString(),
initParameters);
}
/** {@inheritDoc} */
public AttributeContext startContext(Object... requestItems) {
TilesRequestContext tilesContext = getRequestContext(requestItems);
return startContext(tilesContext);
}
/** {@inheritDoc} */
public void endContext(Object... requestItems) {
TilesRequestContext tilesContext = getRequestContext(requestItems);
endContext(tilesContext);
}
/**
* Starts an attribute context inside the container.
*
* @param tilesContext The request context to use.
* @return The newly created attribute context.
*/
private AttributeContext startContext(TilesRequestContext tilesContext) {
AttributeContext context = new BasicAttributeContext();
BasicAttributeContext.pushContext(context, tilesContext);
return context;
}
/**
* Releases and removes a previously created attribute context.
*
* @param tilesContext The request context to use.
*/
private void endContext(TilesRequestContext tilesContext) {
BasicAttributeContext.popContext(tilesContext);
}
/**
* Determine whether or not the container has been
* initialized. Utility method used for methods which
* can not be invoked after the container has been
* started.
*
* @throws IllegalStateException if the container has already been initialized.
*/
protected void checkInit() {
if (initialized) {
throw new IllegalStateException("Container allready initialized");
}
}
/**
* Initializes a definitions factory.
*
* @param definitionsFactory The factory to initialize.
* @param resourceString The string containing a comma-separated-list of
* resources.
* @param initParameters A map containing the initialization parameters.
* @throws TilesException If something goes wrong.
*/
protected void initializeDefinitionsFactory(
DefinitionsFactory definitionsFactory, String resourceString,
Map<String, String> initParameters) throws TilesException {
List<String> resources = getResourceNames(resourceString);
definitionsFactory.init(initParameters);
try {
for (String resource : resources) {
URL resourceUrl = context.getResource(resource);
if (resourceUrl != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding resource '" + resourceUrl + "' to definitions factory.");
}
definitionsFactory.addSource(resourceUrl);
} else {
LOG.warn("Unable to find configured definition '" + resource + "'");
}
}
} catch (IOException e) {
throw new DefinitionsFactoryException("Unable to parse definitions from "
+ resourceString, e);
}
if (LOG.isInfoEnabled()) {
LOG.info("Tiles2 container initialization complete.");
}
}
/**
* Returns the Tiles application context used by this container.
*
* @return the application context for this container.
*/
public TilesApplicationContext getApplicationContext() {
return context;
}
/**
* Sets the Tiles application context to use.
*
* @param context The Tiles application context.
*/
public void setApplicationContext(TilesApplicationContext context) {
this.context = context;
}
/** {@inheritDoc} */
public AttributeContext getAttributeContext(Object... requestItems) {
TilesRequestContext tilesContext = getRequestContext(requestItems);
return getAttributeContext(tilesContext);
}
/**
* Returns the current attribute context.
*
* @param tilesContext The request context to use.
* @return The current attribute context.
*/
private AttributeContext getAttributeContext(TilesRequestContext tilesContext) {
AttributeContext context = BasicAttributeContext.getContext(tilesContext);
if (context == null) {
context = new BasicAttributeContext();
BasicAttributeContext.pushContext(context, tilesContext);
}
return context;
}
/**
* Creates a Tiles request context from request items.
*
* @param requestItems The request items.
* @return The created Tiles request context.
*/
private TilesRequestContext getRequestContext(Object... requestItems) {
return getContextFactory().createRequestContext(
getApplicationContext(),
requestItems
);
}
/**
* Returns the context factory.
*
* @return The context factory.
*/
public TilesContextFactory getContextFactory() {
return contextFactory;
}
/**
* Sets the context factory.
*
* @param contextFactory The context factory.
*/
public void setContextFactory(TilesContextFactory contextFactory) {
checkInit();
this.contextFactory = contextFactory;
}
/**
* Returns the definitions factory.
*
* @return The definitions factory used by this container.
*/
public DefinitionsFactory getDefinitionsFactory() {
return definitionsFactory;
}
/**
* Set the definitions factory. This method first ensures
* that the container has not yet been initialized.
*
* @param definitionsFactory the definitions factory for this instance.
*/
public void setDefinitionsFactory(DefinitionsFactory definitionsFactory) {
checkInit();
this.definitionsFactory = definitionsFactory;
}
/**
* Returns the preparer factory used by this container.
*
* @return return the preparerInstance factory used by this container.
*/
public PreparerFactory getPreparerFactory() {
return preparerFactory;
}
/**
* Set the preparerInstance factory. This method first ensures
* that the container has not yet been initialized.
*
* @param preparerFactory the preparerInstance factory for this conainer.
*/
public void setPreparerFactory(PreparerFactory preparerFactory) {
this.preparerFactory = preparerFactory;
}
/** {@inheritDoc} */
public void prepare(String preparer, Object... requestItems)
throws TilesException {
TilesRequestContext requestContext = getContextFactory().createRequestContext(
getApplicationContext(),
requestItems
);
prepare(requestContext, preparer, false);
}
/**
* Execute a preparer.
*
* @param context The request context.
* @param preparerName The name of the preparer.
* @param ignoreMissing If <code>true</code> if the preparer is not found,
* it ignores the problem.
* @throws TilesException If the preparer is not found (and
* <code>ignoreMissing</code> is not set) or if the preparer itself threw an
* exception.
*/
private void prepare(TilesRequestContext context, String preparerName, boolean ignoreMissing)
throws TilesException {
if (LOG.isDebugEnabled()) {
LOG.debug("Prepare request received for '" + preparerName);
}
ViewPreparer preparer = preparerFactory.getPreparer(preparerName, context);
if (preparer == null && ignoreMissing) {
return;
}
if (preparer == null) {
throw new NoSuchPreparerException("Preparer '" + preparerName + " not found");
}
AttributeContext attributeContext = BasicAttributeContext.getContext(context);
preparer.execute(context, attributeContext);
}
/** {@inheritDoc} */
public void render(String definitionName, Object... requestItems)
throws TilesException {
TilesRequestContext requestContext = getContextFactory().createRequestContext(
getApplicationContext(),
requestItems
);
render(requestContext, definitionName);
}
/**
* Renders the specified definition.
*
* @param request The request context.
* @param definitionName The name of the definition to render.
* @throws TilesException If something goes wrong during rendering.
*/
private void render(TilesRequestContext request, String definitionName)
throws TilesException {
if (LOG.isDebugEnabled()) {
LOG.debug("Render request recieved for definition '" + definitionName + "'");
}
Definition definition = getDefinition(definitionName, request);
if (definition == null) {
if (LOG.isWarnEnabled()) {
String message = "Unable to find the definition '" + definitionName + "'";
LOG.warn(message);
}
throw new NoSuchDefinitionException(definitionName);
}
if (!isPermitted(request, definition.getRole())) {
LOG.info("Access to definition '" + definitionName
+ "' denied. User not in role '" + definition.getRole());
return;
}
AttributeContext originalContext = getAttributeContext(request);
BasicAttributeContext subContext = new BasicAttributeContext(originalContext);
subContext.addMissing(definition.getAttributes());
BasicAttributeContext.pushContext(subContext, request);
try {
if (definition.getPreparer() != null) {
prepare(request, definition.getPreparer(), true);
}
String dispatchPath = definition.getTemplate();
if (LOG.isDebugEnabled()) {
LOG.debug("Dispatching to definition path '"
+ definition.getTemplate() + " '");
}
request.dispatch(dispatchPath);
// tiles exception so that it doesn't need to be rethrown.
} catch (TilesException e) {
throw e;
} catch (Exception e) {
LOG.error("Error rendering tile", e);
// TODO it would be nice to make the preparerInstance throw a more specific
throw new TilesException(e.getMessage(), e);
} finally {
BasicAttributeContext.popContext(request);
}
}
/** {@inheritDoc} */
public void render(Attribute attr, Writer writer, Object... requestItems)
throws TilesException, IOException {
TilesRequestContext request = getRequestContext(requestItems);
if (attr == null) {
throw new TilesException("Cannot render a null attribute");
}
AttributeType type = attr.getType();
if (type == null) {
type = calculateType(attr, request);
attr.setType(type);
}
switch (type) {
case OBJECT:
throw new TilesException(
"Cannot insert an attribute of 'object' type");
case STRING:
writer.write(attr.getValue().toString());
break;
case DEFINITION:
render(request, attr.getValue().toString());
break;
case TEMPLATE:
request.dispatch(attr.getValue().toString());
break;
default: // should not happen
throw new TilesException(
"Unrecognized type for attribute value "
+ attr.getValue());
}
}
/**
* Calculates the type of an attribute.
*
* @param attr The attribute to check.
* @param request The request object.
* @return The calculated attribute type.
* @throws TilesException If the type is not recognized.
*/
private AttributeType calculateType(Attribute attr,
TilesRequestContext request) throws TilesException {
AttributeType type;
Object valueContent = attr.getValue();
if (valueContent instanceof String) {
String valueString = (String) valueContent;
if (isValidDefinition(request, valueString)) {
type = AttributeType.DEFINITION;
} else if (valueString.startsWith("/")) {
type = AttributeType.TEMPLATE;
} else {
type = AttributeType.STRING;
}
} else {
type = AttributeType.OBJECT;
}
return type;
}
/**
* Returns a definition specifying its name.
*
* @param definitionName The name of the definition to find.
* @param request The request context.
* @return The definition, if found.
* @throws DefinitionsFactoryException If the definitions factory throws an
* exception.
*/
protected Definition getDefinition(String definitionName,
TilesRequestContext request) throws DefinitionsFactoryException {
Definition definition =
definitionsFactory.getDefinition(definitionName, request);
return definition;
}
/**
* Checks if the current user is in one of the comma-separated roles
* specified in the <code>role</code> parameter.
*
* @param request The request context.
* @param role The comma-separated list of roles.
* @return <code>true</code> if the current user is in one of those roles.
*/
private boolean isPermitted(TilesRequestContext request, String role) {
if (role == null) {
return true;
}
StringTokenizer st = new StringTokenizer(role, ",");
while (st.hasMoreTokens()) {
if (request.isUserInRole(st.nextToken())) {
return true;
}
}
return false;
}
/**
* Derive the resource string from the initialization parameters.
* If no parameter {@link #DEFINITIONS_CONFIG} is available, attempts
* to retrieve {@link #LEGACY_DEFINITIONS_CONFIG}. If niether are
* available, returns "/WEB-INF/tiles.xml".
*
* @return resource string to be parsed.
*/
protected String getResourceString() {
return getResourceString(context.getInitParams());
}
/**
* Derive the resource string from the initialization parameters.
* If no parameter {@link #DEFINITIONS_CONFIG} is available, attempts
* to retrieve {@link #LEGACY_DEFINITIONS_CONFIG}. If niether are
* available, returns "/WEB-INF/tiles.xml".
*
* @param parms The initialization parameters.
* @return resource string to be parsed.
*/
protected String getResourceString(Map<String, String> parms) {
String resourceStr = parms.get(DEFINITIONS_CONFIG);
if (resourceStr == null) {
resourceStr = parms.get(LEGACY_DEFINITIONS_CONFIG);
}
if (resourceStr == null) {
resourceStr = "/WEB-INF/tiles.xml";
}
return resourceStr;
}
/**
* Parse the resourceString into a list of resource paths
* which can be loaded by the application context.
*
* @param resourceString comma seperated resources
* @return parsed resources
*/
protected List<String> getResourceNames(String resourceString) {
StringTokenizer tokenizer = new StringTokenizer(resourceString, ",");
List<String> filenames = new ArrayList<String>(tokenizer.countTokens());
while (tokenizer.hasMoreTokens()) {
filenames.add(tokenizer.nextToken().trim());
}
return filenames;
}
/** {@inheritDoc} */
public boolean isValidDefinition(String definitionName, Object... requestItems) {
return isValidDefinition(getRequestContext(requestItems), definitionName);
}
/**
* Checks if a string is a valid definition name.
*
* @param context The request context.
* @param definitionName The name of the definition to find.
* @return <code>true</code> if <code>definitionName</code> is a valid
* definition name.
*/
private boolean isValidDefinition(TilesRequestContext context, String definitionName) {
try {
Definition definition = getDefinition(definitionName, context);
return definition != null;
} catch (NoSuchDefinitionException nsde) {
return false;
} catch (DefinitionsFactoryException e) {
// TODO, is this the right thing to do?
return false;
}
}
}