JSF中引入jsf.js文件之后,可以拦截jsf.ajax.request请求。一直希望有一种方法可以像jquery的ajax一样,能在js中异步取得服务器端发送的数据。无奈标准JSF并没有提供这样的方法。在一些JSF框架里面提供了这样的方法,比如primefaces的onComplete方法就是返回数据到一个js方法中的。JSF能在bean里面更新视图(非ajax更新),其中的PartialViewContext类就可以做到局部更新UI,在bean里获取到这个UI就可以了。于是在网上翻看了很多开源的JSF框架,无意中发现omnifaces这个开源框架。官网:http://omnifaces.org/。
当然,一个框架的东西会有很多,以个人之力要全部参透会有一些难度。开源框架更是集思广益,不过我所需要的只是其中的一部分而已,即在js中取得bean返回的数据。
根据omnifaces的showcase http://showcase.omnifaces.org/ 看到其中的一个Ajax工具类根据PartialViewContext类做到了js取得返回数据,不论字符串或者对象数据都可以发送,非常方便。于是根据提供的源码,顺藤摸瓜,copy了必须支持的类,整理出来了一个仅Ajax发送数据的jar。
omnifaces做的步骤大概是先重写了PartialViewContextFactory这个类,然后在配置文件中配置该重写的类。
/**
* This partial view context factory takes care that the {@link OmniPartialViewContext} is properly initialized.
*
*/
public class CustomPartialViewContextFactory extends PartialViewContextFactory { // Variables ------------------------------------------------------------------------------------------------------ private PartialViewContextFactory wrapped; // Constructors --------------------------------------------------------------------------------------------------- /**
* Construct a new OmniFaces partial view context factory around the given wrapped factory.
* @param wrapped The wrapped factory.
*/
public CustomPartialViewContextFactory(PartialViewContextFactory wrapped) {
this.wrapped = wrapped;
} // Actions -------------------------------------------------------------------------------------------------------- /**
* Returns a new instance of {@link OmniPartialViewContext} which wraps the original partial view context.
*/
@Override
public PartialViewContext getPartialViewContext(FacesContext context) {
return new CustomPartialViewContext(wrapped.getPartialViewContext(context));
} /**
* Returns the wrapped factory.
*/
@Override
public PartialViewContextFactory getWrapped() {
return wrapped;
} }
该类重写还需要一个PartialViewContext ,此处没有直接继承PartialViewContext ,而是继承了PartialViewContext 的子类PartialViewContextWrapper,并重写了里面的方法:
/**
* <p>
* This OmniFaces partial view context extends and improves the standard partial view context as follows:
* <ul>
* <li>Support for executing callback scripts by {@link PartialResponseWriter#startEval()}.</li>
* <li>Support for adding arguments to an ajax response.</li>
* <li>Any XML tags which Mojarra and MyFaces has left open after an exception in rendering of an already committed
* ajax response, will now be properly closed. This prevents errors about malformed XML.</li>
* <li>Fixes the no-feedback problem when a {@link ViewExpiredException} occurs during an ajax request on a page which
* is restricted by <code>web.xml</code> <code><security-constraint></code>. The enduser will now properly be
* redirected to the login page instead of retrieving an ajax response with only a changed view state (and effectively
* thus no visual feedback at all).</li>
* </ul>
* You can use the {@link Ajax} utility class to easily add callback scripts and arguments.
* <p>
* This partial view context is already registered by OmniFaces' own <code>faces-config.xml</code> and thus gets
* auto-initialized when the OmniFaces JAR is bundled in a web application, so end-users do not need to register this
* partial view context explicitly themselves.
*
* @author Bauke Scholtz
* @since 1.2
* @see OmniPartialViewContextFactory
*/
public class CustomPartialViewContext extends PartialViewContextWrapper { // Constants ------------------------------------------------------------------------------------------------------ private static final String AJAX_DATA = "var Faces=Faces||{};Faces.Ajax={data:%s};";
private static final String ERROR_NO_OMNI_PVC = "There is no current CustomPartialViewContext instance."; // Variables ------------------------------------------------------------------------------------------------------ private PartialViewContext wrapped;
private Map<String, Object> arguments;
private List<String> callbackScripts;
private CustomPartialResponseWriter writer; // Constructors --------------------------------------------------------------------------------------------------- /**
* Construct a new OmniFaces partial view context around the given wrapped partial view context.
* @param wrapped The wrapped partial view context.
*/
public CustomPartialViewContext(PartialViewContext wrapped) {
this.wrapped = wrapped;
setCurrentInstance(this);
} // Actions -------------------------------------------------------------------------------------------------------- @Override
public PartialResponseWriter getPartialResponseWriter() {
if (writer == null) {
writer = new CustomPartialResponseWriter(this, super.getPartialResponseWriter());
} return writer;
} @Override // Necessary because this is missing in PartialViewContextWrapper (will be fixed in JSF 2.2).
public void setPartialRequest(boolean partialRequest) {
getWrapped().setPartialRequest(partialRequest);
} @Override
public PartialViewContext getWrapped() {
return wrapped;
} /**
* Add an argument to the partial response. This is as JSON object available by <code>OmniFaces.Ajax.data</code>.
* For supported argument value types, read {@link Json#encode(Object)}. If a given argument type is not supported,
* then an {@link IllegalArgumentException} will be thrown during end of render response.
* @param name The argument name.
* @param value The argument value.
*/
public void addArgument(String name, Object value) {
if (arguments == null) {
arguments = new HashMap<>();
} arguments.put(name, value);
} /**
* Add a callback script to the partial response. This script will be executed once the partial response is
* successfully retrieved at the client side.
* @param callbackScript The callback script to be added to the partial response.
*/
public void addCallbackScript(String callbackScript) {
if (callbackScripts == null) {
callbackScripts = new ArrayList<>();
} callbackScripts.add(callbackScript);
} /**
* Reset the partial response. This clears any JavaScript arguments and callbacks set any data written to the
* {@link PartialResponseWriter}.
* @see FullAjaxExceptionHandler
*/
public void resetPartialResponse() {
if (writer != null) {
writer.reset();
} arguments = null;
callbackScripts = null;
} /**
* Close the partial response. If the writer is still in update phase, then end the update and the document. This
* fixes the Mojarra problem of incomplete ajax responses caused by exceptions during ajax render response.
* @see FullAjaxExceptionHandler
*/
public void closePartialResponse() {
if (writer != null && writer.updating) {
try {
writer.endUpdate();
writer.endDocument();
}
catch (IOException e) {
throw new FacesException(e);
}
}
} // Static --------------------------------------------------------------------------------------------------------- /**
* Returns the current instance of the OmniFaces partial view context.
* @return The current instance of the OmniFaces partial view context.
* @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can
* happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another
* {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance.
*/
public static CustomPartialViewContext getCurrentInstance() {
return getCurrentInstance(getContext());
} /**
* Returns the current instance of the OmniFaces partial view context from the given faces context.
* @param context The faces context to obtain the current instance of the OmniFaces partial view context from.
* @return The current instance of the OmniFaces partial view context from the given faces context.
* @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can
* happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another
* {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance.
*/
public static CustomPartialViewContext getCurrentInstance(FacesContext context) {
CustomPartialViewContext instance = getContextAttribute(context, CustomPartialViewContext.class.getName()); if (instance != null) {
return instance;
} // Not found. Well, maybe the context attribute map was cleared for some reason. Get it once again.
instance = unwrap(context.getPartialViewContext()); if (instance != null) {
setCurrentInstance(instance);
return instance;
} // Still not found. Well, maybe RichFaces is installed which doesn't use PartialViewContextWrapper.
if (Hacks.isRichFacesInstalled()) {
PartialViewContext pvc = Hacks.getRichFacesWrappedPartialViewContext(); if (pvc != null) {
instance = unwrap(pvc); if (instance != null) {
setCurrentInstance(instance);
return instance;
}
}
} // Still not found. Well, it's end of story.
throw new IllegalStateException(ERROR_NO_OMNI_PVC);
} private static void setCurrentInstance(CustomPartialViewContext instance) {
setContextAttribute(CustomPartialViewContext.class.getName(), instance);
} private static CustomPartialViewContext unwrap(PartialViewContext context) {
PartialViewContext unwrappedContext = context; while (!(unwrappedContext instanceof CustomPartialViewContext) && unwrappedContext instanceof PartialViewContextWrapper) {
unwrappedContext = ((PartialViewContextWrapper) unwrappedContext).getWrapped();
} if (unwrappedContext instanceof CustomPartialViewContext) {
return (CustomPartialViewContext) unwrappedContext;
}
else {
return null;
}
} // Nested classes ------------------------------------------------------------------------------------------------- /**
* This OmniFaces partial response writer adds support for passing arguments to JavaScript context, executing
* oncomplete callback scripts, resetting the ajax response (specifically for {@link FullAjaxExceptionHandler}) and
* fixing incomlete XML response in case of exceptions.
* @author Bauke Scholtz
*/
private static class CustomPartialResponseWriter extends PartialResponseWriter { // Variables -------------------------------------------------------------------------------------------------- private CustomPartialViewContext context;
private PartialResponseWriter wrapped;
private boolean updating; // Constructors ----------------------------------------------------------------------------------------------- public CustomPartialResponseWriter(CustomPartialViewContext context, PartialResponseWriter wrapped) {
super(wrapped);
this.wrapped = wrapped; // We can't rely on getWrapped() due to MyFaces broken PartialResponseWriter.
this.context = context;
} // Overridden actions ----------------------------------------------------------------------------------------- /**
* An override which checks if the web.xml security constraint has been triggered during this ajax request
* (which can happen when the session has been timed out) and if so, then perform a redirect to the originally
* requested page. Otherwise the enduser ends up with an ajax response containing only the new view state
* without any form of visual feedback.
*/
@Override
public void startDocument() throws IOException {
wrapped.startDocument();
String loginURL = WebXml.INSTANCE.getFormLoginPage(); if (loginURL != null) {
FacesContext facesContext = FacesContext.getCurrentInstance();
String loginViewId = normalizeViewId(facesContext, loginURL); if (loginViewId.equals(getViewId(facesContext))) {
String originalURL = getRequestAttribute(facesContext, "javax.servlet.forward.request_uri"); if (originalURL != null) {
redirect(originalURL);
}
}
}
} /**
* An override which remembers if we're updating or not.
* @see #endDocument()
* @see #reset()
*/
@Override
public void startUpdate(String targetId) throws IOException {
updating = true;
wrapped.startUpdate(targetId);
} /**
* An override which remembers if we're updating or not.
* @see #endDocument()
* @see #reset()
*/
@Override
public void endUpdate() throws IOException {
updating = false;
wrapped.endUpdate();
} /**
* An override which writes all {@link OmniPartialViewContext#arguments} as JSON to the extension and all
* {@link OmniPartialViewContext#callbackScripts} to the eval. It also checks if we're still updating, which
* may occur when MyFaces is used and an exception was thrown during rendering the partial response, and then
* gently closes the partial response which MyFaces has left open.
*/
@Override
public void endDocument() throws IOException {
if (updating) {
// If endDocument() method is entered with updating=true, then it means that MyFaces is used and that
// an exception was been thrown during ajax render response. The following calls will gently close the
// partial response which MyFaces has left open.
// Mojarra never enters endDocument() method with updating=true, this is handled in reset() method.
endCDATA();
endUpdate();
}
else {
if (context.arguments != null) {
startEval();
write(String.format(AJAX_DATA, Json.encode(context.arguments)));
endEval();
} if (context.callbackScripts != null) {
for (String callbackScript : context.callbackScripts) {
startEval();
write(callbackScript);
endEval();
}
}
} wrapped.endDocument();
} // Custom actions --------------------------------------------------------------------------------------------- /**
* Reset the partial response writer. It checks if we're still updating, which may occur when Mojarra is used
* and an exception was thrown during rendering the partial response, and then gently closes the partial
* response which Mojarra has left open. This would clear the internal state of the wrapped partial response
* writer and thus make it ready for reuse without risking malformed XML.
*/
public void reset() {
try {
if (updating) {
// If reset() method is entered with updating=true, then it means that Mojarra is used and that
// an exception was been thrown during ajax render response. The following calls will gently close
// the partial response which Mojarra has left open.
// MyFaces never enters reset() method with updating=true, this is handled in endDocument() method.
endCDATA();
endUpdate();
wrapped.endDocument();
}
}
catch (IOException e) {
throw new FacesException(e);
}
finally {
responseReset();
}
} // Delegate actions -------------------------------------------------------------------------------------------
// Due to MyFaces broken PartialResponseWriter, which doesn't delegate to getWrapped() method, but instead to
// the local variable wrapped, we can't use getWrapped() in our own PartialResponseWriter implementations. @Override
public void startError(String errorName) throws IOException {
wrapped.startError(errorName);
} @Override
public void startEval() throws IOException {
wrapped.startEval();
} @Override
public void startExtension(Map<String, String> attributes) throws IOException {
wrapped.startExtension(attributes);
} @Override
public void startInsertAfter(String targetId) throws IOException {
wrapped.startInsertAfter(targetId);
} @Override
public void startInsertBefore(String targetId) throws IOException {
wrapped.startInsertBefore(targetId);
} @Override
public void endError() throws IOException {
wrapped.endError();
} @Override
public void endEval() throws IOException {
wrapped.endEval();
} @Override
public void endExtension() throws IOException {
wrapped.endExtension();
} @Override
public void endInsert() throws IOException {
wrapped.endInsert();
} @Override
public void delete(String targetId) throws IOException {
wrapped.delete(targetId);
} @Override
public void redirect(String url) throws IOException {
wrapped.redirect(url);
} @Override
public void updateAttributes(String targetId, Map<String, String> attributes) throws IOException {
wrapped.updateAttributes(targetId, attributes);
} }
}
在face-config.xml文件添加配置如下:
<factory>
<partial-view-context-factory>com.context.CustomPartialViewContextFactory</partial-view-context-factory>
</factory>
可以看到其中定义了两个常量,一个警告说明,一个名为AJAX_DATA,里面写的是一段js的字符串。这个常量用在了 被重写的endDocument()方法里。这个方法主要做了两件事,第一是写入要传递的数据,并且以json格式打包,第二件事是包含了一个js方法。那么可以这样认为:该方法的作用是用一个js方法取得发送的数据。
首先我们必须清楚,JSF使用f:ajax更新客户端的底层操作是怎样的。JSF更新数据是向页面发送了xml数据的,可以在firefox的 firebug下面的网络-XML面板下看到发送的xml数据,类似于以下的数据:
<partial-response id="j_id1">
<changes>
<update id="j_id1:javax.faces.ViewState:0">-4426271603414575392:-5845678956333562288</update>
</changes>
</partial-response>
里面包含了需要更新的组件和组件状态。
我把omnifaces的Ajax代码整理出来以后,根据官方例子的用法,写了一个小demo测试,其中页面form表单代码如下:
<h:form prependId="false">
<h:outputLabel value="#{test2.radom}" id="outLabel"/>
<h:commandButton action="#{test2.callback}" value="请求服务端js">
<f:ajax/>
</h:commandButton>
<h:commandButton value="获取服务端数据到js">
<f:ajax listener="#{test2.argument}"/>
</h:commandButton> <h:commandButton value="showUser">
<f:ajax listener="#{test2.parseUser}"/>
</h:commandButton>
</h:form>
测试bean代码如下:
@ManagedBean(name="test2")
@ViewScoped
public class Test2 implements Serializable { private static final long serialVersionUID = 8686669721840131192L; public void callback() {
Ajax.oncomplete("alert('Hi, I am the oncomplete callback script!')");
} public void argument() {
Ajax.data("foo", "bar");
Ajax.data("first", "one", "second", "two");
Map<String, Object> data = new HashMap<>();
data.put("bool", true);
data.put("number", 1.2F);
data.put("date", new Date());
data.put("array", new Integer[] { 1, 2, 3, 4, 5 });
data.put("list", Arrays.asList("one", "two", "three"));
Ajax.data(data);
Ajax.oncomplete("showData()");
} public void parseUser(){
Ajax.data("user", new User(1, "bigbang"));
Ajax.oncomplete("showUser()");
} }
其中提供了三个页面响应的方法。当点击第一个button时,页面会弹出一个alert提示框。此时查看firefox下面的数据如下:
<partial-response id="j_id1">
<changes>
<update id="j_id1:javax.faces.ViewState:0">-4426271603414575392:-5845678956333562288</update>
<eval>alert('Hi, I am the oncomplete callback script!')</eval>
</changes>
</partial-response>
点击“获取数据到js”按钮,查看xml数据:
<partial-response id="j_id1">
<changes>
<update id="j_id1:javax.faces.ViewState:0">-3364647386979820288:-1391656100755852530</update>
<eval>var Faces=Faces||{};Faces.Ajax={data:{"second":"two","number":1.2,"list":["one","two","three"],"foo":"bar","bool":true,"date":"Fri, 17 Jul 2015 09:17:50 GMT","first":"one","array":[1,2,3,4,5]}};</eval>
<eval>showData()</eval>
</changes>
</partial-response>
点击“showuser”按钮,查看xml数据:
<partial-response id="j_id1">
<changes>
<update id="j_id1:javax.faces.ViewState:0">-3364647386979820288:-1391656100755852530</update>
<eval>var Faces=Faces||{};Faces.Ajax={data:{"user":{"id":1,"name":"bigbang"}}};</eval>
<eval>showUser()</eval>
</changes>
</partial-response>
可以看出刚才的那个endDocumnt方法和AJAX_DATA常量的用意了,实际是构造了一个js对象,然后传入一个js方法到客户端,客户端会自动调用这个js方法,根据对象取得json数据。那么客户端的js只需要这样写就可以了:
<script>
function showData() {
var data = Faces.Ajax.data;
$.each(data, function(key, value) {
console.log(key+"---"+JSON.stringify(value));
});
} function showUser(){
var data = Faces.Ajax.data;
$.each(data, function(key,value){
var user = JSON.stringify(value);
var u = $.parseJSON(user);
console.log("userName----"+u.name);
}
</script>
控制台显示数据如下:
OK,大功告成了!其中的细节不用细究,无非就是掺杂了各种数据转换为json格式、数据的包装和写入。最后说明一点,omnifaces需要CDI的支持,必须导入CDI的jar包。