Exception handler (blatantly stolen from here) :
public final class ApplicationExceptionHandler extends ExceptionHandlerWrapper {
private final ExceptionHandler wrapped;
public ApplicationExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
@Override
public void handle() throws FacesException {
FacesContext facesContext = FacesContext.getCurrentInstance();
for (Iterator<ExceptionQueuedEvent> iter = getUnhandledExceptionQueuedEvents().iterator(); iter.hasNext();) {
Throwable exception = Exceptions.unwrap(iter.next().getContext().getException());
if (Exceptions.is(exception, EntityNotFoundException.class)) {
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_FATAL, "Summary", "Message");
facesContext.addMessage(null, facesMessage);
} else if (Exceptions.is(exception, DatabaseConstraintViolationException.class)) {
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_FATAL, "Summary", "Message");
facesContext.addMessage(null, facesMessage);
} else if (Exceptions.is(exception, DuplicateEntityException.class)) {
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_FATAL, "Summary", "Message");
facesContext.addMessage(null, facesMessage);
} else if (Exceptions.is(exception, EntityAlreadyModifiedException.class)) { // OptimisticLockException
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_FATAL, "Summary", "Message");
facesContext.addMessage(null, facesMessage);
} else if (Exceptions.is(exception, DatabaseException.class)) {
FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_FATAL, "Summary", "Message");
facesContext.addMessage(null, facesMessage);
} else {
// Render a global error page, if any other exceptions occur.
String errorPageLocation = "/WEB-INF/error_pages/GeneralError.xhtml";
facesContext.setViewRoot(facesContext.getApplication().getViewHandler().createView(facesContext, errorPageLocation));
facesContext.getPartialViewContext().setRenderAll(true);
facesContext.renderResponse();
}
}
// getWrapped().handle();
}
@Override
public ExceptionHandler getWrapped() {
return wrapped;
}
}
The factory is registered in faces-config.xml (I guess all other things on the EJB/JPA side are unnecessary here). Exceptions.is() is an OmniFaces utility method.
When any exception as mentioned in the ladder is thrown, it should be registered as validation violation i.e. model values should not be updated nor action(Listener) methods, if involved any, should be executed/triggered as if some conversion/validation failed.
Where this is required :
This is essentially required, when dealing with optimistic locking on the persistence layer. For example, if a row/rows in a <p/h:dataTable> is attempted to delete (by pressing an Ajaxical command button or link) which is already modified by another user in another session behind the back, the javax.persistence.OptimisticLockException should be thrown which happens correctly using this exception handler mechanism.
Once that exception occurs, the system should keep on throwing this exception forever in all subsequent attempts to delete the same row/s until the user explicitly updates the stale values in those rows by triggering another brand new synchronous or asynchronous request (that should not involve deleting the stale rows as obvious).
This would turn out to be true only for the very first attempt to delete the stale row/s. In the following attempt, the row/s with stale values will be liable to be deleted because once this exception is thrown and a message is rendered in the very first attempt, the data table will also have already been updated with the latest updated row version. Therefore, in the immediate following request, the client will be sending rows with the latest updated row version in each row which will not be detected as concurrent modifications by the persistence provider as obvious. This is perfectly legit for the persistence provider to delete those rows. This may give end-users a bad experience - at least not so good as it should.
How can this be achieved using this exception handling mechanism - when any exception as specified by the exception handler as above, a user-friendly message should be rendered (which happens correctly) and neither model values nor action(Listener) methods should be triggered as if a conversion or validation is violated (i.e. the target <p:dataTable> or any other UI component holding rows should not get updated)?
EDIT :
The managed bean (view scoped) :
@Named
@ViewScoped
public class Bean extends LazyDataModel<Entity> implements Serializable {
@Inject
private Service service;
private List<Entity> selectedValues; // Getter & setter.
private static final long serialVersionUID = 1L;
@Override
public List<Entity> load(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, Object> filters) {
setRowCount(service.rowCount());
// Other logic.
return service.getList(first, pageSize, map, filters);
}
public void delete(ActionEvent event) {
if (service.delete(selectedValues)) { // EJB call to delete selected rows in <p:dataTable>.
// FacesMeaage - delete succeeded.
} else {
// FacesMeaage - delete failed.
}
}
// This method is invoked before delete() that
// just warns the user about deletion of rows using <p:confirmDialog>.
public void confirmDelete(ActionEvent event) {
if (selectedValues != null && !selectedValues.isEmpty()) {
// FacesMessage - rows are selected/checked in <p:dataTable>.
} else {
// FacesMessage - rows are not selected/checked in <p:dataTable>.
}
}
}
The data table :
<p:dataTable id="dataTable"
var="row"
value="#{bean}"
lazy="true"
sortMode="multiple"
selection="#{bean.selectedValues}"
rows="10">
<p:column selectionMode="multiple">
<f:facet name="footer">
<p:commandButton oncomplete="PF('confirmDelete').show()"
update=":form:confirmDialog"
process=":form:dataTable"
actionListener="#{bean.confirmDelete}"/> <!---->
</f:facet>
</p:column>
...
...
...
<p:column headerText="Edit">
<p:rowEditor/>
</p:column>
</p:dataTable>
<p:confirmDialog id="confirmDialog"
widgetVar="confirmDelete"
message="Message from bean">
<p:commandButton id="confirmDelete"
value="Yes"
process="@this"
update="dataTable messages"
oncomplete="PF('confirmDelete').hide()"
actionListener="#{bean.delete}"/> <!---->
<p:commandButton id="declineDelete"
value="No"
onclick="PF('confirmDelete').hide()"
type="button"/>
</p:confirmDialog>