Turbine是apache 项目中的server-side java技术,位于jakarta子项目,是基于servlet的web应用框架.它提供了很多基础服务:访问控制,页面个性化,服务调度,表单确认,xml-rpc格式的web服务等等.可以做为开发面向服务架构应用的基础,因为turbine很容易开发其它服务,并在其服务管理框架下运行.其下一个版本为2.4,它明确使用亚瑟王神剑项目(该项目实现了IOC模式)来管理服务组件。
本文描述的场景是:turbine2.31+velocity-1.4+torque-3.1。要解决在该场景下提交编码模式为multipart/form-data的表单不能正确处理中文的问题,虽然application/x-www-form-urlencoded编码下可以正确处理中文.
纠正本问题需要提供:Maven,Ant两个工具以及Turbine源码.
首先,看Turbine的入口servlet Turbine.java中的代码片段,是doGet方法:
目的是看原始的web服务器请求对象(request)是如何被传递入Turbine中的,以及Turbine如何处理request对象.
public final void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException
{
// set to true if the request is to be redirected by the page
boolean requestRedirected = false;
// Placeholder for the RunData object.
RunData data = null;
try
{
// Check to make sure that we started up properly.
if (initFailure != null)
{
throw initFailure;
}
// Get general RunData here...
// Perform turbine specific initialization below.
data = rundataService.getRunData(req, res, getServletConfig());
// If this is the first invocation, perform some
// initialization. Certain services need RunData to initialize
// themselves.
if (firstDoGet)
{
init(data);
}
// set the session timeout if specified in turbine's properties
// file if this is a new session
if (data.getSession().isNew())
{
int timeout = configuration.getInt(SESSION_TIMEOUT_KEY,
SESSION_TIMEOUT_DEFAULT);
if (timeout != SESSION_TIMEOUT_DEFAULT)
{
data.getSession().setMaxInactiveInterval(timeout);
}
}
// Fill in the screen and action variables.
data.setScreen(data.getParameters().getString(URIConstants.CGI_SCREEN_PARAM));
data.setAction(data.getParameters().getString(URIConstants.CGI_ACTION_PARAM));
// Special case for login and logout, this must happen before the
// session validator is executed in order either to allow a user to
// even login, or to ensure that the session validator gets to
// mandate its page selection policy for non-logged in users
// after the logout has taken place.
if (data.hasAction())
{
String action = data.getAction();
log.debug("action = " + action);
if (action.equalsIgnoreCase(
configuration.getString(ACTION_LOGIN_KEY,
ACTION_LOGIN_DEFAULT)))
{
loginAction(data);
}
else if (action.equalsIgnoreCase(
configuration.getString(ACTION_LOGOUT_KEY,
ACTION_LOGOUT_DEFAULT)))
{
logoutAction(data);
}
}
// This is where the validation of the Session information
// is performed if the user has not logged in yet, then
// the screen is set to be Login. This also handles the
// case of not having a screen defined by also setting the
// screen to Login. If you want people to go to another
// screen other than Login, you need to change that within
// TurbineResources.properties...screen.homepage; or, you
// can specify your own SessionValidator action.
ActionLoader.getInstance().exec(
data, configuration.getString(ACTION_SESSION_VALIDATOR_KEY,
ACTION_SESSION_VALIDATOR_DEFAULT));
// Put the Access Control List into the RunData object, so
// it is easily available to modules. It is also placed
// into the session for serialization. Modules can null
// out the ACL to force it to be rebuilt based on more
// information.
ActionLoader.getInstance().exec(
data, configuration.getString(ACTION_ACCESS_CONTROLLER_KEY,
ACTION_ACCESS_CONTROLLER_DEFAULT));
// Start the execution phase. DefaultPage will execute the
// appropriate action as well as get the Layout from the
// Screen and then execute that. The Layout is then
// responsible for executing the Navigation and Screen
// modules.
//
// Note that by default, this cannot be overridden from
// parameters passed in via post/query data. This is for
// security purposes. You should really never need more
// than just the default page. If you do, add logic to
// DefaultPage to do what you want.
String defaultPage = (templateService == null)
? null :templateService.getDefaultPageName(data);
if (defaultPage == null)
{
/*
* In this case none of the template services are running.
* The application may be using ECS for views, or a
* decendent of RawScreen is trying to produce output.
* If there is a 'page.default' property in the TR.props
* then use that, otherwise return DefaultPage which will
* handle ECS view scenerios and RawScreen scenerios. The
* app developer can still specify the 'page.default'
* if they wish but the DefaultPage should work in
* most cases.
*/
defaultPage = configuration.getString(PAGE_DEFAULT_KEY,
PAGE_DEFAULT_DEFAULT);
}
PageLoader.getInstance().exec(data, defaultPage);
// If a module has set data.acl = null, remove acl from
// the session.
if (data.getACL() == null)
{
try
{
data.getSession().removeAttribute(
AccessControlList.SESSION_KEY);
}
catch (IllegalStateException ignored)
{
}
}
// handle a redirect request
requestRedirected = ((data.getRedirectURI() != null)
&& (data.getRedirectURI().length() > 0));
if (requestRedirected)
{
if (data.getResponse().isCommitted())
{
requestRedirected = false;
log.warn("redirect requested, response already committed: " +
data.getRedirectURI());
}
else
{
data.getResponse().sendRedirect(data.getRedirectURI());
}
}
if (!requestRedirected)
{
try
{
if (data.isPageSet() == false && data.isOutSet() == false)
{
throw new Exception("Nothing to output");
}
// We are all done! if isPageSet() output that way
// otherwise, data.getOut() has already been written
// to the data.getOut().close() happens below in the
// finally.
if (data.isPageSet() && data.isOutSet() == false)
{
// Modules can override these.
data.getResponse().setLocale(data.getLocale());
data.getResponse().setContentType(
data.getContentType());
// Set the status code.
data.getResponse().setStatus(data.getStatusCode());
// Output the Page.
data.getPage().output(data.getOut());
}
}
catch (Exception e)
{
// The output stream was probably closed by the client
// end of things ie: the client clicked the Stop
// button on the browser, so ignore any errors that
// result.
log.debug("Output stream closed? ", e);
}
}
}
catch (Exception e)
{
handleException(data, res, e);
}
catch (Throwable t)
{
handleException(data, res, t);
}
finally
{
// Return the used RunData to the factory for recycling.
rundataService.putRunData(data);
}
}
以及org.apache.turbine.services.rundata.TurbineRunDataService中的:
public RunData getRunData(HttpServletRequest req,
HttpServletResponse res,
ServletConfig config)
throws TurbineException
{
return getRunData(DEFAULT_CONFIG, req, res, config);
}
/**
* Gets a RunData instance from a specific configuration.
*
* @param key a configuration key.
* @param req a servlet request.
* @param res a servlet response.
* @param config a servlet config.
* @return a new or recycled RunData object.
* @throws TurbineException if the operation fails.
* @throws IllegalArgumentException if any of the parameters are null.
*/
public RunData getRunData(String key,
HttpServletRequest req,
HttpServletResponse res,
ServletConfig config)
throws TurbineException,
IllegalArgumentException
{
// The RunData object caches all the information that is needed for
// the execution lifetime of a single request. A RunData object
// is created/recycled for each and every request and is passed
// to each and every module. Since each thread has its own RunData
// object, it is not necessary to perform syncronization for
// the data within this object.
if ((req == null)
|| (res == null)
|| (config == null))
{
throw new IllegalArgumentException("HttpServletRequest, "
+ "HttpServletResponse or ServletConfig was null.");
}
// Get the specified configuration.
String[] cfg = (String[]) configurations.get(key);
if (cfg == null)
{
throw new TurbineException("RunTime configuration '" + key + "' is undefined");
}
TurbineRunData data;
try
{
data = (TurbineRunData) pool.getInstance(cfg[0]);
data.setParameterParser((ParameterParser) pool.getInstance(cfg[1]));
data.setCookieParser((CookieParser) pool.getInstance(cfg[2]));
}
catch (ClassCastException x)
{
throw new TurbineException("RunData configuration '" + key + "' is illegal", x);
}
// Set the request and response.
data.setRequest(req);
data.setResponse(res);
// Set the servlet configuration.
data.setServletConfig(config);
// Set the ServerData.
data.setServerData(new ServerData(req));
return data;
}
在 org.apache.turbine.services.rundata.DefaultTurbineRunData中的:
/**
* Sets the servlet request.
*
* @param req a request.
*/
public void setRequest(HttpServletRequest req)
{
this.req = req;
}
/**
* Sets the servlet response.
*
* @param res a response.
*/
public void setResponse(HttpServletResponse res)
{
this.res = res;
}
在该类中request对象被不加修改的传递到了ParameterParser:
/**
* Gets the parameters.
*
* @return a parameter parser.
*/
public ParameterParser getParameters()
{
// Parse the parameters first, if not yet done.
if ((this.parameters != null) &&
(this.parameters.getRequest() != this.req))
{
this.parameters.setRequest(this.req);
}
return this.parameters;
}
/**
* Gets the parameter parser without parsing the parameters.
*
* @return the parameter parser.
*/
public ParameterParser getParameterParser()
{
return parameters;
}
/**
* Sets the parameter parser.
*
* @param parser a parameter parser.
*/
public void setParameterParser(ParameterParser parser)
{
parameters = parser;
}
以上类中展示的代码都涉及到了Request对象,从这些代码中看出在Turbine中,实际上并没有对Request对象做任何修改,这样就可以按照通常的方法,加入对Request对象的处理代码.最后, 这里是关键的地方:在类DefaultParameterParser中对Request对象的处理,也就是通过对该处的修改,问题得到了解决:
/**
* Sets the servlet request to be parser. This requires a
* valid HttpServletRequest object. It will attempt to parse out
* the GET/POST/PATH_INFO data and store the data into a Map.
* There are convenience methods for retrieving the data as a
* number of different datatypes. The PATH_INFO data must be a
* URLEncoded() string.
* <p>
* To add name/value pairs to this set of parameters, use the
* <code>add()</code> methods.
*
* @param request An HttpServletRequest.
*/
public void setRequest(HttpServletRequest request)
{
clear();
uploadData = null;
//String enc = request.getCharacterEncoding();
原来是:
enc = request.getCharacterEncoding();
setCharacterEncoding(enc != null ? enc : "US-ASCII");
修改后:
if(enc==null) enc=org.apache.turbine.Turbine.getConfiguration().getString(CharacterEncoding_Key,"US-ASCII");
setCharacterEncoding(enc);
// String object re-use at its best.
String tmp = null;
tmp = request.getHeader("Content-type");
if (uploadServiceIsAvailable
&& uploadService.getAutomatic()
&& tmp != null
&& tmp.startsWith("multipart/form-data"))
{
log.debug("Running the Turbine Upload Service");
try
{
TurbineUpload.parseRequest(request, this);
}
catch (TurbineException e)
{
log.error("File upload failed", e);
}
}
for (Enumeration names = request.getParameterNames();
names.hasMoreElements();)
{
tmp = (String) names.nextElement();
add(convert(tmp),
request.getParameterValues(tmp));
}
// Also cache any pathinfo variables that are passed around as
// if they are query string data.
try
{
StringTokenizer st =
new StringTokenizer(request.getPathInfo(), "/");
boolean isNameTok = true;
String pathPart = null;
while (st.hasMoreTokens())
{
if (isNameTok)
{
tmp = URLDecoder.decode(st.nextToken());
isNameTok = false;
}
else
{
pathPart = URLDecoder.decode(st.nextToken());
if (tmp.length() > 0)
{
add(convert(tmp), pathPart);
}
isNameTok = true;
}
}
}
catch (Exception e)
{
// If anything goes wrong above, don't worry about it.
// Chances are that the path info was wrong anyways and
// things that depend on it being right will fail later
// and should be caught later.
}
this.request = request;
if (log.isDebugEnabled())
{
log.debug("Parameters found in the Request:");
for (Iterator it = keySet().iterator(); it.hasNext();)
{
String key = (String) it.next();
log.debug("Key: " + key + " -> " + getString(key));
}
}
}
同时在配置文件TurbineResources.properties中加入一项:
CharacterEncoding=gb2312.可以根据需要修改该值.
完成以上修改后,用Maven工具重新构建Turbine并得到了新的turbine.jar把它放入你的应用环境中,中文问题即得到正确显示.
注意:以上代码片段来自Turbine源码.黑体字表示要关注的部分,代码中修改部分则给出了说明.