VRaptor + Spring Web Flow

Olá amigos do fórum,

Precisarei integrar o VRaptor com o Spring Web Flow em um projeto…ainda estou na fase de “olhando a documentação”, mas surgiram algumas duvidas:

  1. antes de mais nada é possível integrar os dois sem grandes traumas?senão já nem precisaria estudar a bagaça :lol: … mas pelo que estou lendo dos docs não será muito complicado (espero!)
  2. os objetos que ficam no escopo “flow” e “conversation”, podem ser injetados nos objetos do VRaptor anotados com @Component ou @Resource?
  3. nos exemplos da documentação do Spring, nos arquivos de definição de fluxo eles usam muito a instrução:
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="..." />

Ou seja, em determinado momento do fluxo, será invocado esse método “createBooking” do bean “bookingService”, com os parametros tais aí. Seria possível fazer isso com os objetos do VRaptor? Creio eu que sim, já que são gerenciados pelo Spring…correto?

E aproveitando o gancho, pra quem manja do Web Flow…esse fluxo que preciso mexer hoje já está funcionando, em uma aplicação JSF que utiliza o JBoss Seam (que também tem um excelente suporte pra page flow). Como já estava acostumado com o jeitao do Seam fazer a bagaça surgiu uma duvida que ainda nao consegui sanar na documentação

pra iniciar e finalizar um fluxo o Seam tem as anotações @Begin e @End, pra serem usadas nos métodos dos managed-beans do JSF. Ou seja, quando os metodos marcados com tais anotações forem invocados um fluxo (e uma conversação) serão iniciados ou finalizados. Pelo que entendi do Spring Web Flow o inicio do fluxo começa com uma invocação de uma url específica que faz referencia a um fluxo. é isso mesmo? (por exemplo o fluxo “meufluxo-flow.xml” será iniciado quando eu invocar “/meufluxo”)

Obrigado amigos!

não conheço muito de spring web flow, mas parece ser bem possível essa implementação sim…

todos os componentes do vraptor são registrados como beans do spring, então tudo deve funcionar sem problemas. Só coloque o applicationContext.xml no classpath, daí fica tudo certo.

Legal, Lucas, valeu. Minha impressão a partir da documentação foi a mesma, como os componentes do VRaptor são beans do Spring tudo deve funcionar sem maiores transtornos. Até mesmo os escopos específicos (flow, conversation), são gerenciados dentro da execução do fluxo e não é necessário que os beans sejam declarados explicitamente no código com esses escopos. Então também deve funcionar pros componentes do VRaptor. Que beleza!

Valeu fessor, brigadão.

Amigos do fórum,

Agora estou começando a botar a mao na massa com essa integração VRaptor + Spring WebFlow…e tá dificil :lol: .

Ainda não compreendi como algumas questões refer. ao WebFlow vão se encaixar no VRaptor…

  1. quando o fluxo é iniciado, o SWF gera uma url do tipo “nomedofluxo?e1s1”. Essa string no final é para o controle interno do SWF e é irrelevante, o ponto aqui é: como é que vou mapear essa url “nomedofluxo” para um Controller do VRaptor??? Acho que nao é possível pela convenção das urls do VRaptor…eu criei um interceptor para validar se aquela url especifica faz parte de um fluxo e entao delega a execução do WebFlow, mas…deve ter um jeito melhor…

  2. Bom eu queria direcionar os passos do fluxo para os controllers do VRaptor mas dei de barato que nao vai rolar. Depois percebi que o SWF descarta o uso do controller, mesmo que eu estivesse usando o Spring MVC! O que ele faz a cada passo é apenas exibir o jsp relacionado ao passo, e os atributos de requisição, que normalmente seriam preenchidos na logica do controller, são preenchidos no proprio fluxo (input direto de variaveis no escopo de request, flow, conversation, etc), ou usando classes de “action”. Ou seja, adeus usar o Result do VRaptor no controller…procede isso? E essas classes de Action hein, meu receio é afastar muito o codigo do VRaptor, parece que a coisa vai terminar como um “mix” de VRaptor e Spring MVC (eca)

  3. Uma das funcionalidades que preciso implementar nesse fluxo envolve um upload de arquivos (com ajax…). Só com o VRaptor seria simples de implementar, até já fiz, mas…como espetar isso no WebFlow, se o Controller do VRaptor não faz parte da execução do fluxo? O upload teria que ser feito no raio da tal classe “action” (na propria documentação tem um exemplo disso, http://static.springsource.org/spring-webflow/docs/2.3.x/reference/htmlsingle/spring-webflow-reference.html#file-upload), mas o upload terá que ser feito via ajax, preciso de uma url pra direcionar o upload…e usando a tal classe action, nao faço ideia de qual seria hehe

Alguem por ai por favor que já tenha trabalhado com o Spring WebFlow…por favor socorro! :cry: .

Obrigado amigos.

  1. @Path(“nomedofluxo”) deveria funcionar

  2. o result do vraptor adiciona atributos no request, então provavelmente vai funcionar do jeito que vc quer

  3. não faz diferença do lado do servidor se o upload é ajax ou normal.

[quote=Lucas Cavalcanti]1. @Path(“nomedofluxo”) deveria funcionar

  1. o result do vraptor adiciona atributos no request, então provavelmente vai funcionar do jeito que vc quer

  2. não faz diferença do lado do servidor se o upload é ajax ou normal.[/quote]

entao lucas mas essa url “nomedofluxo?parametros” se repete em todos os passos do fluxo, ou seja. Se eu tiver um controller do VRaptor com @Path(“nomedofluxo”) vai bater lá em todos os passos e nao é bem o que eu queria.

O meu fluxo tem seis passos, e a minha ideia era ter o Passo1Controller, Passo2Controller, etc, cada controller seria responsável por um trecho especifico do fluxo de paginas. Mas com o SpringWebFlow esse conceito muda um pouco, tudo o que é necessario pro fluxo de paginas é declarado no proprio xml e para ações especificas, sao invocados métodos de beans comuns, mas esses metodos não são invocados a partir de uma url (como é a forma que usamos os controllers no vraptor).

Ou seja, a meu ver (e é o que eles dizem nos docs tambem) com o Spring WebFlow nao é preciso ter controllers específicos para as urls; ELE faz tudo. Mas no VRaptor tudo o que fazemos começa no controller…daí a minha pontual dificuldade hehe.

  1. O Result do VRaptor faz o que precisaria, de fato, o problema é fazer a execução chegar no @Resource. Estou tentando bolar um jeito de interferir na execução do WebFlow e fazer o controller (do VRaptor) responder a cada passo, mas não é assim que o framework (o WebFlow) funciona.

  2. De fato nao faz diferença com relação ao upload. O problema é que com o WebFlow…eu nao tenho uma url pra enviar o upload (e se estivesse atuando apenas com o controller do VRaptor, teria).

Essa integração não tá muito transparente, ou tô fazendo algo de errado…

(aliás ainda vou mexer no probleminha que lhe perguntei do linkTo, mas infelizmente esse caso aqui é mais prioritário hehe)

Valeu lucas, obrigado.

aliás…me parece que, quando o fluxo estiver sendo executado (quando a url for “nomedofluxo?algumacoisa”), o VRaptor NAO deve tratar essa request (não deve passar pelo fluxo normal de interceptors até o controller)

aê, consegui fazer funcionar! assim que puder vou postar aqui o que fiz…até pra alguem, se possível, ajudar a melhorar :lol:

valeu amigos

Bom, pra quem precisar integrar o VRaptor com o Spring WebFlow deve servir como um ponto de partida. Ou pra quem já fez isso de um jeito melhor e queira colaborar com o tópico será muito bem-vindo :stuck_out_tongue: . Achei que a minha integração ficou meio intrusiva, mas ainda nao vejo um jeito melhor de fazer… :oops:

Bom, o WebFlow gera uma url com o nome do fluxo uma vez que este é iniciado, e segue com essa url até o fim do fluxo por todas as paginas. Isso implica que não podemos usar os controllers do VRaptor, uma vez que teremos a mesma url para várias páginas. Assim sendo, as requisições ref. ao fluxo devem ser tratadas pelo WebFlow e não devem seguir o fluxo normal do VRaptor (cadeia de interceptors até o controller). Isso eu fiz com este interceptor aqui:

public class WebFlowInterceptor implements Interceptor {

	private final HttpServletRequest request;
	private final HttpServletResponse response;
	
	private final FlowHandlerMapping flowHandlerMapping;
	private final FlowHandlerAdapter flowHandlerAdapter;
	
	private static final Logger log = LoggerFactory.getLogger(WebFlowInterceptor.class);
	
	public WebFlowInterceptor(HttpServletRequest request, HttpServletResponse response, FlowHandlerMapping flowHandlerMapping, FlowHandlerAdapter flowHandlerAdapter) {
		this.request = request;
		this.response = response;
		this.flowHandlerMapping = flowHandlerMapping;
		this.flowHandlerAdapter = flowHandlerAdapter;
	}

	@Override
	public void intercept(InterceptorStack stack, ResourceMethod method,Object resourceInstance) throws InterceptionException {
	
		try {
			HandlerExecutionChain mappedHandler = flowHandlerMapping.getHandler(request);
			
			if (mappedHandler == null){ //se for uma requisição que nao pertence a um flow...o VRaptor segue como conhecemos e amamos
				stack.next(method, resourceInstance);	

			} else {
				
                                //se a requisiçao pertencer a um flow - nao segue o ciclo de vida normal do VRaptor
				Object handler = mappedHandler.getHandler();
				
				if (flowHandlerAdapter.supports(handler))
					flowHandlerAdapter.handle(request, response, handler);
			}
		
		} catch (Exception e) {
			
			log.error("exception in web flow handler...", e);
			
			stack.next(method, resourceInstance);
		}
	}

	@Override
	public boolean accepts(ResourceMethod method) {
		return true;
	}

}

Para encontrar os jsps, o WebFlow por padrao procura no mesmo diretorio onde está o flow. Eu queria manter as paginas junto das outras usadas pelo VRaptor, que colocamos em /WEB-INF-/jsp. Entao defini alguns beans do Spring para tanto

   <!-- esses beans sao do webflow -->
 <webflow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows">
		<webflow:flow-location-pattern value="/**/*.xml" />
	</webflow:flow-registry>
	 
	<webflow:flow-executor id="flowExecutor">
	</webflow:flow-executor>
	
	<beans:bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
		<beans:property name="flowRegistry" ref="flowRegistry" />
	</beans:bean>
	
	<beans:bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
		<beans:property name="flowExecutor" ref="flowExecutor" />
	</beans:bean>
	
  <!-- para o WebFlow conseguir resolver os jsps dentro da pasta padrao do VRaptor -->
	<beans:bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/jsp/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	<beans:bean id="webFlowViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
		<beans:property name="viewResolvers" ref="jspViewResolver" />
		<beans:property name="useSpringBeanBinding" value="true" />
	</beans:bean>

Existe um outro bean que pode ser definido no xml que é o

<webflow:flow-builder-services/>

Esse FlowBuilderServices cria as instancias que colaboram com a execução dos flows, e no xml é possível definir o atributo “view-factory-creator”, que é um carinha que resolve as views que são renderizadas pelo fluxo (no caso seria o “webFlowViewFactoryCreator” que definimos acima). Tentei definir tudo dentro do xml mesmo, mas…quando o contexto do Spring era carregado, o FlowBuilderServices ficava ok, com a instancia do “webFlowViewFactoryCreator” definida no xml… mas após o VRaptor fazer o “refresh” no contexto do Spring, o valor da propriedade era perdido, nao entendi o motivo mas pareceu ser mais problema do Spring que do VRaptor…já viu isso acontecer Lucas? (espero ter explicado direito :lol: )

Bom como workaround pra esse probleminha eu criei um ApplicationListener pra redefinir o FlowBuilderServices após o refresh do contexto, com o viewFactoryCreator que declarei no xml

public class WebFlowBuilderServicesSetup implements ApplicationListener<ContextRefreshedEvent>{

	private final MvcViewFactoryCreator viewFactoryCreator;
	
	public WebFlowBuilderServicesSetup(MvcViewFactoryCreator viewFactoryCreator){
		this.viewFactoryCreator = viewFactoryCreator;
	}
	
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		
		ApplicationContext context = event.getApplicationContext();
		
		FlowBuilderServices flowBuilderServices = context.getBean(FlowBuilderServices.class);
		
		flowBuilderServices.setViewFactoryCreator(viewFactoryCreator);
	}
}

E é isso. A principal diferença aqui foi fazer as coisas sem o controller…realmente, no modelo do WebFlow, nao é necessario um controller convencional.

Valeu pela ajuda lucas! (agora vou tentar arrumar aquele probleminha do linkTo, hehe. valeu mano)

se vc está com a última versão do VRaptor e usar o applicationContext.xml no classpath, o VRaptor não dá o refresh…

vc não precisa colocar o listener no web.xml na última versão (3.4.1)

[quote=Lucas Cavalcanti]se vc está com a última versão do VRaptor e usar o applicationContext.xml no classpath, o VRaptor não dá o refresh…

vc não precisa colocar o listener no web.xml na última versão (3.4.1)[/quote]

opa que beleza, nao sabia disso, embora o xml do spring esteja no classpath estou usando o listener do web.xml. Pra funcionar da forma que voce falou o arquivo precisa ter exatamente esse nome “applicationContext.xml”?

Valeu!

precisa sim.

Amigos,

Houve mais uma situação que acho útil citar aqui. Como tinha dito não é necessario ter um controller responsavel por cada url/pagina, como normalmente fariamos no VRaptor, pois o Spring WebFlow vai encontrar os jsps, invocar os beans que venham a ser necessários antes da renderizacao da pagina, etc. Mas precisei criar um controller que era invocado via ajax, e nesse controller precisava acessar os dados no escopo “flow”.

Esse escopo é gerenciado pelo WebFlow durante a execução do fluxo e é o que permite manter o estado de objetos durante varias requisicoes, e o framework também se encarrega de limpar esse escopo ao fim do fluxo. Os beans incluidos nesse escopo são gerenciados a parte pelo WebFlow e não são necessariamente “beans gerenciados” pelo Spring; na verdade são gerenciados pelo WebFlow meio “por fora” do IOC-Container e não é possível injeta-los em ontros beans.

Enfim, eu precisava desse objeto do escopo “flow” dentro do controller, e fiz da seguinte forma:

Uma anotação de parametro:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Flow {

}

Um interceptor:

@Intercepts(before=ParametersInstantiatorInterceptor.class)
@Lazy
public class WebFlowParametersInstantiatorInterceptor implements Interceptor {

	private final WebFlowParameterResolver webFlowParameterResolver;
	
	public WebFlowParametersInstantiatorInterceptor(WebFlowParameterResolver webFlowParameterResolver){
		this.webFlowParameterResolver = webFlowParameterResolver;
	}
	
	@Override
	public void intercept(InterceptorStack stack, ResourceMethod method, Object resourceInstance) throws InterceptionException {
		
		webFlowParameterResolver.resolve(method.getMethod());
		
		stack.next(method, resourceInstance);
	}		

	@Override
	public boolean accepts(ResourceMethod method) {
		return hasFlowParameters(method.getMethod());
	}

	private boolean hasFlowParameters(Method method){
		
		if (method.getParameterTypes().length == 0)
			return false;
		
		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
		
		for (Annotation[] annotations : parameterAnnotations){
			for (Annotation annotation : annotations){
				if (annotation instanceof Flow) return true;
			}
		}
		
		return false;
	}
}

E um componente que lê o escopo “flow” para buscar os beans

@Component @RequestScoped
public class WebFlowParameterResolver {

	private final FlowExecutor flowExecutor;
	
	private final HttpServletRequest request;
	private final HttpServletResponse response;
	private final ServletContext servletContext;
	
	private final FlowUrlHandler flowUrlHandler = new DefaultFlowUrlHandler();
	
	public WebFlowParameterResolver(FlowExecutor flowExecutor, HttpServletRequest request, HttpServletResponse response, ServletContext servletContext){
		this.flowExecutor = flowExecutor;
		
		this.request = request;
		this.response = response;
		this.servletContext = servletContext;
	}
	
	private FlowExecutionRepository flowRepository(){
		return ((FlowExecutorImpl) flowExecutor).getExecutionRepository();
	}
	
	private FlowExecution currentFlowExecution(){
		
		String key = flowUrlHandler.getFlowExecutionKey(request);
		
		if (key == null)
			throw new IllegalArgumentException("No flow execution key found in request");
        
		FlowExecutionRepository repository = flowRepository();
		
        FlowExecutionKey executionKey = repository.parseFlowExecutionKey(key);
        
        return repository.getFlowExecution(executionKey);
	}
	
	private Map<?, ?> flowScope(){
		
		FlowExecution flowExecution = currentFlowExecution();
		
		return flowExecution.getActiveSession().getScope().asMap();
	}
	
	private void resolveParameters(Method method){

		Map<?, ?> flowScope = flowScope();
		
		Class<?>[] parameterTypes = method.getParameterTypes();
		
		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
		
		int i = 0;
		
		for (Annotation[] annotations : parameterAnnotations){
			
			Class<?> parameterType = parameterTypes[i++];
			
			for (Annotation annotation : annotations){
				if (annotation instanceof Flow){
					
					resolveParameter(parameterType, flowScope);
					
				}
			}
		}

	}
	
	private void resolveParameter(Class<?> parameterType, Map<?, ?> flowScope){
		
		Map<?, ?> values = filterValues(flowScope, instanceOf(parameterType));
		
		if (values.size() != 0){
			for (Entry<?, ?> entry : values.entrySet())
				request.setAttribute(entry.getKey().toString(), entry.getValue());
		}
	}
	
	private void createExternalContext(){
		ExternalContextHolder.setExternalContext(new MvcExternalContext(servletContext, request, response, flowUrlHandler));
	}
	
	private void destroyExternalContext(){
		ExternalContextHolder.setExternalContext(null);
	}
	
	public void resolve(Method method)  {
		
		createExternalContext();
		
		resolveParameters(method);
		
		destroyExternalContext();
	}
}

Dessa forma o controller poderia ser dessa forma

@Resource
public class NomeController {

	public void teste(@Flow TipoQueEstaNoFlow xpto){
	}
}

O interceptor vai ser executado quando o metodo que atende a url tiver ao menos um parametro anotado com @Flow
O componente mais acima vai buscar no escopo flow o objeto do mesmo tipo do parametro do metodo, e inclui o cara como atributo da request; depois o VRaptor vai tratar de injetar isso no parametro (estou assumindo que o parametro tem o mesmo nome que o nome do bean no flow scope).

Não curti muito essa injeção pelo metodo, mas me pareceu meio forçado fazer um ComponentFactory nesse cenario, uma vez que os objetos que vao pro flow scope, via de regra, são os objetos temporarios que duram a execução do fluxo. São objetos de representação do modelo, não necessariamente “componentes”.

Bom, espero que possa ajudar pra quem mais precisar integrar o WebFlow com o VRaptor.

Valeu amigos.

Legal!

põe lá no github! =)

Opa lucas, vou colocar lá sim, vai que até ajuda alguem né :lol: .

Ainda tem uma situação que quero corrigir, com relação aos converters: da maneira como eu fiz, é como se nem estivesse usando VRaptor nas paginas que pertencem ao fluxo, pois tudo é gerenciado pelo Spring WebFlow; inclusive o bind dos campos do jsp para o modelo…ele segue o funcionamento do Spring MVC, e penso que o funcionamento do VRaptor é mais esperto que o deles, hehe.

É possível criar conversores personalizados para o bind jsp->modelo, assim como no VRaptor, mas tive que criar alguns para converter dados de acordo com o Locale, coisa que o VRaptor já tem para tipos como integer, double, BigDecimal…e o modelo do VRaptor é mais esperto, lê os converters no momento do scanning, já para o Spring MVC é preciso registra-los manualmente, e criá-los não é tão facil quanto colocar um @Converter na classe. Quero criar um adapter para usar os converters do VRaptor no Spring WebFlow. Ia melhorar bastante…como disse minha “solução” está um pouco intrusiva, a coisa ficou muito acoplada com o Spring WebFlow. Mas funciona 100% hehe, então por enquanto tá bom.

Olha só lucas, quando estava fazendo esse negocio ai encima, a principio eu tinha feito todo esse codigo no interceptor. Pra facilitar o teste unitário do metodo accepts eu havia criado um construtor sem parametros, com visibilidade default, para não ter que passar as dependências. Mas quando rodei a aplicação, o VRaptor usou esse construtor (o de visibilidade default, não o publico que recebe as dependencias) para criar a instancia “lazy” e a instancia “real” do interceptor…isso não deveria ocorrer né?

Valeu!

o ideal eh não ter dois construtores… se tiver vc tem que marcá-lo com @Inject, mas mesmo assim pode dar problemas…

pra testar com as dependencias, remova o construtor sem argumentos e tente usar uma lib de mocks, tipo o mockito, pra passar as dependencias…

para os adapters do ScreenFlow, vc pode criar um só que converte filhos de Object, e usar o componente Converters do vraptor, que deixa vc fazer o lookup de um converter qualquer =)

[quote=Lucas Cavalcanti]o ideal eh não ter dois construtores… se tiver vc tem que marcá-lo com @Inject, mas mesmo assim pode dar problemas…

pra testar com as dependencias, remova o construtor sem argumentos e tente usar uma lib de mocks, tipo o mockito, pra passar as dependencias…

para os adapters do ScreenFlow, vc pode criar um só que converte filhos de Object, e usar o componente Converters do vraptor, que deixa vc fazer o lookup de um converter qualquer =)[/quote]

Então, o caso era que eu criei o construtor default para não passar as dependencias, uma vez que elas nao eram relevantes para o método accepts do interceptor. Era só pra evitar, na classe de teste, fazer new ClasseSendoTestada(null, null, null…). Eu costumo fazer isso e nunca tive problema, por isso achei meio bizarro ele usar o construtor não-publico pra instanciar a classe. Bom, é de menos hehe.

Sobre os converters, a sua sugestao seria criar um converter “genérico”, e uma vez dentro dele definir o converter mais relevante para o tipo? Mas nesse caso como ficaria a anotação, @Converts(Object.class)?

Valeu novamente, Lucas…obrigado.

eh pra fazer isso do lado do Spring Web-Flow, nao do vraptor…
ou seja, criar um converter generico do spring, que recebe o Converters (componente de todos os converters) do vraptor

[quote=Lucas Cavalcanti]eh pra fazer isso do lado do Spring Web-Flow, nao do vraptor…
ou seja, criar um converter generico do spring, que recebe o Converters (componente de todos os converters) do vraptor[/quote]

Entendi, valeu pela dica lucas ia ficar bem legal mesmo, vou implementar essa bagaça dessa forma pq tá um saco ter que registrar os converters manualmente (via xml ou no codigo), sempre esqueço algum e dá xabu no jsp hehe. Vai ajudar demais.

Valeu lucas, obrigado!