Exception handler no vraptor3

Boa noite pessoal.

Eu estava trabalhando em um exception handler para minha aplicação usando vraptor. Porém um pequeno problema pessoal me deixou um pouco offline nesses ultimos dias, e acabei tendo pouquíssimo tempo para isso.

Com esse pouco tempo o que eu fiz foi apenas criar um interceptor com um try and catch. Caso cair no catch ele verifica o tipo da exception e imprime na tela as mensagens. Pouca coisa, não?

Meu objetivo é fazer algo semelhante com o que eu tinha no struts 1x, onde eu verificava o tipo de exception e a partir dela eu poderia fazer a decisão de para onde ir. Normalmente eu ia para a tela anterior com os dados do form para erros de negócio ou erros que eu possa recuperar. Caso ocorrer erros irrecuperáveis (banco fora do ar, etc) eu apenas exibia uma tela com as mensagens.

Então preciso de uma opinião de vocês sobre como seria a melhor forma de indicar ao exception handlers para onde ele deve ir em caso de exception (de inicio pensei em uma annotation @OnError ou um results.onException).

Abraços

pelo que eu entendi, as exceções não recuperáveis vão sempre pro mesmo lugar, certo?

quanto às exceções recuperáveis, não é possível prevení-las com validações?

tipo, um NullPointer é facilmente evitado com uma verificação… isso meio que acontece com
todos os outros erros “recuperáveis”…

onde as exceções seriam lançadas? na sua lógica? no seu modelo?

[]'s

Sim, para uma tela que apenas imprime a mensagem de erro.

Na verdade no meu controller tenho as validaçoes de campos obrigatórios apenas, depois chamo os EJBs e redireciono para view. Esses erros que quero tratar com um exception-handler seriam os erros de negócio mesmo.

Minha idéia é não ficar fazendo try and catch a cada vez que chamo os EJBs para tratar esses erros. Penso em algo automatizado.

Atualmente eu tenho isso aqui:

[code]public void delete(final Long id) {
// validate required fields
validator.checking(new Validations() {
{
that(id == null, “erro”, “blah”);
}
});

validator.onErrorUse(Results.page()).of(getClass()).list();

// call the ejb method
try {
	coreService.deleteDegree(id);
} catch (Exception e) {
	validator.add(new ValidationMessage(e.getMessage(), "error"));
}

validator.onErrorUse(Results.page()).of(getClass()).list();

// forward if operation success
result.use(Results.logic()).redirectTo(getClass()).list();

}[/code]

Acho esse código do try and catch para tratar os erros lançados pelo EJB um pouco exaustivas. Exatamente para esses códigos que eu pensei no exception handler.

[code]public void delete(final Long id) {
// validate required fields
validator.checking(new Validations() {
{
that(id == null, “erro”, “blah”);
}
});

validator.onErrorUse(Results.page()).of(getClass()).list();

// call the ejb method - if an error occurs the exception handler can be catch
coreService.deleteDegree(id);

// forward if operation success
result.use(Results.logic()).redirectTo(getClass()).list();

}[/code]

bom… o que você vai ter que mudar pra isso é o ExecuteMethodInterceptor…

dentro dele tem um try…catch…

você vai ter que adicionar essa sua lógica de erros dentro dele… (na verdade ele coloca a exceção que deu dentro de uma
InterceptionException, ou algo do tipo, vc pode capturá-la em outro lugar também…)

daí vc pode fazer um interceptor que o accepts dele são os métodos anotados com @OnError, e o
intercepts dele faz um try…catch e em caso de erro vai para a página especificada… só que daí não dá pra fazer uma
interface fluente… no máximo a classe e o nome do método pra onde vc quer ir…

Para exceptions nao recuperaveis (sqlexceptio etc) use o proprio <error-page> no web.xml

[quote=lucascs]daí vc pode fazer um interceptor que o accepts dele são os métodos anotados com @OnError, e o
intercepts dele faz um try…catch e em caso de erro vai para a página especificada… só que daí não dá pra fazer uma
interface fluente… no máximo a classe e o nome do método pra onde vc quer ir…[/quote]

Lucas, até aí foi o que eu pensei. Para o handler eu não vou conseguir fazer uma fluent interface. Por isso pensei no @OnError. Porém o máximo que consigo seria colocar uma String para indicar para onde ir quando der erro, já que annotations somente aceita constantes.

Vou fazer mais uns testes aqui com tua sugestão. Obrigado.

:thumbup:

então… você pode criar uma classe

@Component
public class MyResult {
      public class MyResult(Result result) {
            this.delegate = result;
      }

      //delega todos os métodos que vc usa
      public <T> T onErrorUse(Class<T> controller) {
            return proxifier.proxify(controller, new MethodInvocation() {
                public void ....(Object proxy, Method method,  Object[] args...) {
                         MyResult.this.method = method;
                         MyResult.this.args = args;
                         return null;
                 }
           }
      }

      public Method getErrorMethod() {
            return this.method;
      }
      public Object[] getErrorArgs() {
            return this.args;
      }
}

vc tem que receber nas suas lógicas que vão usar isso esse MyResult ao invés do Result, e chamar no começo da lógica:

  myResult.onErrorGoto(BlahController.class).blahMethod();

daí no seu interceptor é só usar o myResult.getErrorMethod() e redirecionar pra ele em caso de erro:

   Object instance = myResult.use(logic()).redirectTo(errorMethod.getDeclaringClass());
    errorMethod.invoke(instance, errorArgs);

isso deve funcionar, corrigindo alguns erros de compilação que eu deixei :wink:

que acha?

Lucas, essa classe MyResult estende ou implementa alguém?

Não precisa… mas se você quiser pode extender DefaultResult, ou implementar Result, e receber um DefaultResult no construtor…

mas onde você precisar usar esse onError, tem que receber explicitamente um MyResult

Lucas, funcionou tudo bem mesmo. Adicionei um if para evitar NullPointerException caso eu não especificar o onError. Porém como faço agora para reinjetar os dados que o usuário digitou na tela anterior, e que estavam injetados no meu bean?

Ahh, alterei o myResult.use(logic()).redirectTo(errorMethod.getDeclaringClass()); para FORWARD.

Abraços

isso de reinjetar os dados digitados já está implementado, no repositório…

mas só funciona diretamente pra erros de validação, você vai ter que colocar no seu interceptor de erro:

outjector.outjectRequestMap();

e receber um br.com.caelum.vraptor.validator.Outjector no construtor.

baixa o source mais novo e gera o jar, que isso vai funcionar

[]'s

Lucas, estou usando a última versão do repositório.

Fiz a alteração e funcionou perfeitamente. Vou fazer mais uns ajustes e otimizações e logo publico aqui o componente.

Abraços

Lucas, meu problema agora é que as mensagens de erro não estão sendo enviadas para a tela:

[code]Method errorMethod = result.getErrorMethod();
Object[] args = result.getErrorArgs();
Object target = result.use(Results.logic()).forwardTo(errorMethod.getDeclaringClass());

validator.add(new ValidationMessage(ExceptionUtils.getRootCauseMessage(e), “error”));
outjector.outjectRequestMap();[/code]

[code]<c:if test="${not empty errors}">



    <c:forEach items="${errors}" var=“e”>
  • ${e.category}: ${e.message}

  • </c:forEach>

<br clear="all"/>

</c:if>[/code]

desculpe, esqueci de falar… não dá pra mexer muito bem com o validator de dentro de um interceptor…
(a validação só acontece de fato na chamada validation.onErrorUse(…), que joga exceção se tiver erro… isso não funciona num interceptor)

você pode fazer o seguinte:

 result.include("errors", Arrays.asList(new ValidationMessage(...)));

funciona do mesmo jeito, e como você não vai chamar o stack.next, não tem problema…

outra coisa, coloque o outjector.outjectRequestMap() antes da chamada result.use(…)

[]'s

Lucas, pois é, depois que postei a mensagem pensei: me parece errado jogar mensagens de exceptions dentro de um validator. Então fiz de forma muito parecido com o que você fez.

Abraços, e obrigado.

Lucas, tive um outro problema agora. Quando salvo um objeto e o EJB lança uma exception de negócio, estou com problemas ao buscar os dados na tela.

Nesse caso no controller roda tudo bem. Quando dá um erro no método store, o método edit é chamado. Porém nesse caso como eu estava na inserção ele entra no método edit passando null como parametro. Sendo assim o objeto Year não é carregado (está certo esse procedimento). Então no JSP tenho o form com alguns campos de input. Quando ele encontra inputs que não são String dá esse erro. Notei que se eu removo os campos id (long), startDate (Date) tudo funciona. Será que há algo no outjector.outjectRequestMap()?

Aqui como ficou meu CustomExecuteMethodInterceptor

[code]final List messages = new LinkedList();
messages.add(new ValidationMessage(ExceptionUtils.getRootCauseMessage(e), “error”));
result.include(“errors”, messages);

outjector.outjectRequestMap();

Method errorMethod = result.getErrorMethod();
Object[] args = result.getErrorArgs();
Object target = result.use(Results.logic()).forwardTo(errorMethod.getDeclaringClass());

try {
errorMethod.invoke(target, args);
} catch (Exception e0) {
throw new InterceptionException(e0);
}
[/code]

[code]public void edit(Integer id) {
if (id != null) {
result.include(“year”, yearService.findByPK(id));
}
}

public void store(YearDTO year) {
result.onErrorUse(getClass()).edit(year == null ? null : year.getId());

yearService.store(year);
result.use(Results.logic()).redirectTo(getClass()).list();

}[/code]

O JSP de edição:

<w:text name="year.id" class="number" size="4" maxlength="4" /> <w:date name="year.startDate" pattern="dd/MM/yyyy" size="10" class="date" />

Caused by: java.lang.IllegalArgumentException: Cannot format given Object as a Number at java.text.DecimalFormat.format(DecimalFormat.java:487) at java.text.Format.format(Format.java:140) at org.apache.taglibs.standard.tag.common.fmt.FormatNumberSupport.doEndTag(FormatNumberSupport.java:230) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx._jspx_meth_fmt_formatNumber_0(year_edit_jspx.java from :209) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx._jspx_meth_fmt_param_0(year_edit_jspx.java from :181) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx._jspx_meth_fmt_message_0(year_edit_jspx.java from :148) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx.access$000(year_edit_jspx.java from :7) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx$year_edit_jspxHelper.invoke0(year_edit_jspx.java from :839) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx$year_edit_jspxHelper.invoke(year_edit_jspx.java from :858) at org.apache.jsp.tag.web.default_tag.doTag(default_tag.java from :180) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx._jspx_meth_tags_default_0(year_edit_jspx.java from :127) at org.apache.jsp.WEB_002dINF.jspx.management.year_edit_jspx._jspService(year_edit_jspx.java from :102)

Fiz agora mais um debug, e pelo que noto está sendo colocado o objeto outjectMap no request, ao invés do tipo correto.

Caused by: java.lang.IllegalArgumentException: Cannot convert true of type class br.com.caelum.vraptor.view.RequestOutjectMap to class java.lang.Boolean at com.sun.el.lang.ELSupport.coerceToBoolean(ELSupport.java:180) at com.sun.el.lang.ELSupport.coerceToType(ELSupport.java:360) at com.sun.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:209) ... 84 more

Se eu coloco os campos na tela como String funciona tudo bem. Quando coloco nas taglibs que precisam fazer evaluate para Date, Boolean, Double dá esse erro.

Pelo que notei o vraptor usa como atributo do request um objeto RequestOutjectMap. Não consegui ainda entender por qual razão que as telas funcionam bem, porém apenas quando há uma exception capturada pelo exception handler o RequestOutjectMap resolve a encrencar com o evaluate as ELs do glassfish.

O mais estranho é que mesmo que eu tenha os parametros corretos esse objeto está sempre retornando null em RequestOutjectMap.getAsString().

Galera, muito obrigado pelo help. Consegui resolver o problema. Não sei se sou eu que não soube usar corretamente o RequestOutjectMap ou se há realmente um problema no componente. Notei que ele existe desde a versão 3.0.2 (estou usando o snapshot dessa versão). Mas para contornar ao invés de usar ele estou reinjetando os parametros manualmente no meu exception handler. Desculpem o código muito feio, mas foi o que consegui fazer na correria. Logo mais a noite acho que consigo tempo de mexer com calma nisso.

[code]Enumeration params = request.getParameterNames();
while (params.hasMoreElements()) {
String name = params.nextElement();
Object value = request.getParameter(name);

if (name.contains(".")) {
	final int iname = name.indexOf(".");
	String oname = name.substring(0, iname); // the attribute name
	Map<String, Object> map = (Map<String, Object>) request.getAttribute(oname);
	if (map == null) {
		map = Maps.newLinkedHashMap();
	}

	map.put(name.substring(iname + 1), value); // the property

	request.setAttribute(oname, map); // set again for cluster propagation
} else {
	request.setAttribute(name, value); // this is a single property
}

}[/code]

Olá garcia,

acabei deixando passar esse tópico do guj, desculpe =(

esse código que você fez funciona no seu caso?

se funcionar a gente pode substituir a implementação do nosso RequestOutejctMap (que é talvez um pouco gambiarra demais)

[]'s