Vamos configurar um projeto simples, que aborde os principais recursos do GraniteDS usando Spring/Hibernate.
Vamos utilizar o projeto do Willian Draï, ele está disponível em http://github.com/wdrai/wineshop-admin é necessário ter o Maven 3.x.
Para baixar o projeto e executar de forma rapida, basta seguir os passos a baixo.
git clone git://github.com/wdrai/wineshop-admin.git cd wineshop-admin mvn clean package cd webapp mvn jetty:run-war
Agora basta acessar o seguinte endereço http://localhost:8080/wineshop-admin/wineshop-admin.swf, os usuários disponíveis são admin/admin e user/user.
Este exemplo é um crud simples que permite criar, editar e pesquisar. O Layout da app é feia, mas seu objetivo é simplesmente demonstrar as seguintes características:
Cada tópico é correspondente a uma tag no GitHub, isso vai servir para que você saiba exatamente o que foi alterado em cada tópico.
Vamos reconstruir o projeto do zero.
Esta é a parte mais facil
mvn archetype:generate -DarchetypeGroupId=org.graniteds.archetypes -DarchetypeArtifactId=org.graniteds-tide-spring-jpa -DarchetypeVersion=1.1.0.GA -DgroupId=com.wineshop -DartifactId=wineshop-admin -Dversion=1.0-SNAPSHOT
Em seguida verifique se o projeto está funcional
cd wineshop-admin mvn clean package cd webapp mvn jetty:run-war
Acesse o seguinte endereço http://localhost:8080/wineshop-admin/wineshop-admin.swf. Você deve ser capaz de ver o aplicativo Hello World.
Este passo será maior, nele iremos construir a maior parte da aplicação. Entidade JPA, Serviço do Spring e um Cliente Flex básico.
Está é nossa entidade, não há nada de especial.
@Entity
public class Vineyard extends AbstractEntity {
private static final long serialVersionUID = 1L;
@Basic
private String name;
@OneToMany(cascade=CascadeType.ALL, mappedBy="vineyard",
orphanRemoval=true)
private Set wines;
//Get's e Set's omitido
}
@Entity
public class Wine extends AbstractEntity {
private static final long serialVersionUID = 1L;
public static enum Type {
RED,
WHITE,
ROSE
}
@ManyToOne
private Vineyard vineyard;
@Basic
private String name;
@Basic
private Integer year;
@Enumerated(EnumType.STRING)
private Type type;
//Get's e Set's omitidos
}
Interface do serviço Spring para trabalhar com este modelo.
@RemoteDestination
@DataEnabled(topic="")
public interface WineshopService {
public void save(Vineyard vineyard);
public void remove(Long vineyardId);
public Map list(Vineyard filter,
int first, int max, String[] sort, boolean[] asc);
}
Como você pode observar temos duas Anotações @RemoteDestination indica que o serviço deve ser exposto ao Flex e um proxy ActionScript3 será gerado para o serviço. @DataEnable indica que o GraniteDS irá acompanhar as atualizações das entidades JPA e envia-las automaticamente para os clientes.
Implementação do Serviço
@Service
public class WineshopServiceImpl implements WineshopService {
@PersistenceContext
private EntityManager entityManager;
@Transactional
public void save(Vineyard vineyard) {
entityManager.merge(vineyard);
entityManager.flush();
}
@Transactional
public void remove(Long vineyardId) {
Vineyard vineyard = entityManager.find(Vineyard.class, vineyardId);
entityManager.remove(vineyard);
entityManager.flush();
}
@Transactional(readOnly=true)
public Map list(Vineyard filter,
int first, int max, String[] sort, boolean[] asc) {
StringBuilder sb = new StringBuilder("from Vineyard vy ");
if (filter.getName() != null)
sb.append("where vy.name like '%' || :name || '%'");
if (sort.length > 0)
sb.append("order by ");
for (int i = 0; i < sort.length; i++)
sb.append(sort[i]).append(" ").append(asc[i] ? " asc" : " desc");
Query qcount = entityManager.createQuery("select count(vy) "
+ sb.toString());
Query qlist = entityManager.createQuery("select vy "
+ sb.toString()).setFirstResult(first).setMaxResults(max);
if (filter.getName() != null) {
qcount.setParameter("name", filter.getName());
qlist.setParameter("name", filter.getName());
}
Map result = new HashMap(4);
result.put("resultCount", (Long)qcount.getSingleResult());
result.put("resultList", qlist.getResultList());
result.put("firstResult", first);
result.put("maxResults", max);
return result;
}
}
Este é um serviço classico do Spring com JPA. Porém temos algumas particularidades.
Mas isto não traz nenhuma dependência com GraniteDS, sendo assim este serviço pode ser usado por qualquer cliente.
Vamos ao cliente Flex:
<?xml version="1.0" encoding="utf-8"?>
<s:VGroup
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:e="com.wineshop.entities.*"
xmlns="*"
width="100%" height="100%">
<fx:Metadata>[Name]</fx:Metadata>
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import org.granite.tide.spring.Spring;
import org.granite.tide.collections.PagedQuery;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.events.TideFaultEvent;
import com.wineshop.entities.Vineyard;
import com.wineshop.entities.Wine;
import com.wineshop.entities.Wine$Type;
import com.wineshop.services.WineshopService;
Spring.getInstance().addComponentWithFactory("vineyards", PagedQuery,
{ filterClass: Vineyard, elementClass: Vineyard, remoteComponentClass: WineshopService, methodName: "list", maxResults: 12 }
);
[In] [Bindable]
public var vineyards:PagedQuery;
[Inject]
public var wineshopService:WineshopService;
private function save():void {
wineshopService.save(vineyard);
}
private function remove():void {
wineshopService.remove(vineyard.id, function(event:TideResultEvent):void {
selectVineyard(null);
});
}
private function selectVineyard(vineyard:Vineyard):void {
this.vineyard = vineyard;
vineyardsList.selectedItem = vineyard;
}
]]>
</fx:Script>
<fx:Declarations>
<e:Vineyard id="vineyard"/>
</fx:Declarations>
<s:VGroup paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10" width="800">
<s:HGroup id="filter">
<s:TextInput id="filterName" text="@{vineyards.filter.name}"/>
<s:Button id="search" label="Search" click="vineyards.refresh()"/>
</s:HGroup>
<s:List id="vineyardsList" labelField="name" width="100%" height="200"
change="selectVineyard(vineyardsList.selectedItem)">
<s:dataProvider><s:AsyncListView list="{vineyards}"/></s:dataProvider>
</s:List>
<s:Button id="newVineyard" label="New" click="selectVineyard(new Vineyard())"/>
</s:VGroup>
<s:VGroup paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10" width="800">
<mx:Form id="formVineyard">
<mx:FormHeading label="{isNaN(vineyard.id) ? 'Create vineyard' : 'Edit vineyard'}"/>
<mx:FormItem label="Name">
<s:Label text="{vineyard.id}"/>
<s:TextInput id="formName" text="@{vineyard.name}"/>
</mx:FormItem>
<mx:FormItem>
<s:HGroup>
<s:Button id="saveVineyard" label="Save"
click="save()"/>
<s:Button id="removeVineyard" label="Remove"
enabled="{!isNaN(vineyard.id)}" click="remove()"/>
</s:HGroup>
</mx:FormItem>
</mx:Form>
</s:VGroup>
</s:VGroup>
Está não é uma parte complicada, mas temos alguns pontos que devem ser observados.
Agora você pode fazer um novo build com mvn package, reniciar o jetty e ver as mudanças.
Aqui não há muito a fazer, basta adicionar um itemrender com um componente de Formulário para permitir a edição dos vinhos da vinha selecionada.
<s:FormItem label="Wines">
<s:HGroup gap="10">
<s:List id="formWines" dataProvider="{vineyard.wines}">
<s:itemRenderer>
<fx:Component>
<s:ItemRenderer>
<s:states><s:State name="normal"/></s:states>
<s:HGroup id="wineEdit">
<s:TextInput text="@{data.name}"/>
<s:TextInput text="@{data.year}"/>
<s:DropDownList
selectedItem="@{data.type}"
requireSelection="true"
dataProvider="{outerDocument.wineTypes}"
labelField="name"/>
</s:HGroup>
</s:ItemRenderer>
</fx:Component>
</s:itemRenderer>
</s:List>
<s:VGroup gap="10">
<s:Button label="+"
click="vineyard.wines.addItem(new Wine(vineyard))"/>
<s:Button label="-"
enabled="{Boolean(formWines.selectedItem)}"
click="vineyard.wines.removeItemAt(formWines.selectedIndex)"/>
</s:VGroup>
</s:HGroup>
</s:FormItem>
Deixamos passar duas coisas: Precisamos mudar o construtor da classe Wine para aceitar como argumento a classe Vineyard. Isto será utilizado pela adição de um novo “Wine”
public function Wine(vineyard:Vineyard = null):void {
this.vineyard = vineyard;
}
E inicializar a coleção de Wines para a nova Vineyard:
public function Vineyard():void {
this.wines = new ArrayCollection();
}
Recrie a aplicação com mvn package e reinicie o jetty.
Como você pode observar isto é puramente código Flex. Deixamos para o “Cascade” salvar nossas alterações no banco de dados.
Mas quando as entidades Vineyards são buscadas as listas de Wines ainda não são carregadas. Quando o usuário seleciona uma Vineyard automaticamente ele irá carrega a lista de wines. Isto é completamente transparente para que você não precise pensar nisso.
Se você ja rodou a aplicação pode ter notado que o uso de bindings bidirecionais leva a um comportamento estranho. Mesmo sem salvar as alterações, os objetos locais são modificados. GrantieDS é capaz de rastrear as todas as modificações feitas nas entidades Gerenciadas e é capaz de restaurar o estado estável dos objetos (normalmente a ultima busca feita no servidor).
Tambem é possivel de forma facil habilitar ou desabilitar o botão “Save”, dependendo do estado do objeto.
Para conseguir isso, precisamos apenas de algumas linhas, quando o usuário seleciona outro elemento na lista principal para restaurar o elemento anterior:
import org.granite.tide.spring.Context;
[Inject] [Bindable]
public var tideContext:Context;
private function selectVineyard(vineyard:Vineyard):void {
Managed.resetEntity(this.vineyard);
tideContext.vineyard = this.vineyard = vineyard;
vineyardsList.selectedItem = vineyard;
}
Então podemos usar a propriedade meta_dirty do contexto Tide para habilitar/desabilitar o botão “Save”
<s:Button id="saveVineyard" label="Save"
enabled="{tideContext.meta_dirty}" click="save()"/>
mvn clean package, jetty, …
Agora ja podemos criar, editar e pesquisar no nosso banco de dados. Agora gostariamos de assegurar que os dados são consistentes. Em vez de definirmos validadores Flex para cada campo, vamos usar a api Bean Validation no servidor e a implementação de GraniteDS no cliente.
Primeiro vamos adicionar algumas anotações de Bean Validation ao nosso modeo.
@Basic
@Size(min=5, max=100,
message="The name must be between {min} and {max} characters")
private String name;
@Basic
@Min(value=1900,
message="The year must be greater than {value}")
@Max(value=2050,
message="The year must be less than {value}")
private Integer year;
@Enumerated(EnumType.STRING)
@NotNull
private Type type;
@Basic
@Size(min=5, max=100,
message="The name must be between {min} and {max} characters")
private String name;
@OneToMany(cascade=CascadeType.ALL,
mappedBy="vineyard", orphanRemoval=true)
@Valid
private Set wines;
Isto vai garantir que não podemos salvar entidades inválidas. No entanto gostariamos de informar ao usuário que a operação falhou. Uma maneira feia de fazer isso seria usar um fault Handler com um Alert. Em vez disso vamos usar o componente FormValidator que irá validar a entidade localmente e interpretar as exceçoes do servidor e propagar as mensagens para o campo correto.
Primeiro precisa registrar o ValidatorExceptionHandler que irá processar os erros de validação provenientes do servidor. Neste exemplo isto não é necessário, pois todas as restrições podem ser processadas no cliente. Mas é util, caso o serviço tenha restrições adicionais. Basta adicionar está linha no método init do main.mxml.
Spring.getInstance().addExceptionHandler(ValidatorExceptionHandler);
Em Home.xml defina o namespace “v” e defina um FormValidator ligado ao formulário de edição e a entidade.
<s:VGroup
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:v="org.granite.validation.*"
xmlns:e="com.wineshop.entities.*"
xmlns="*"
width="100%" height="100%"
initialize="selectVineyard(new Vineyard())">
<fx:Declarations>
<e:Vineyard id="vineyard"/>
<s:ArrayCollection id="wineTypes" source="{Wine$Type.constants}"/>
<v:FormValidator id="formValidator"
entity="{vineyard}"
form="{formVineyard}"/>
</fx:Declarations>
Também podemos definir um FormValidator para o item render:
<s:itemRenderer>
<fx:Component>
<s:ItemRenderer>
<fx:Declarations>
<v:FormValidator id="wineValidator"
form="{wineEdit}" entity="{data}"/>
</fx:Declarations>
<s:states><s:State name="normal"/></s:states>
<s:HGroup id="wineEdit">
<s:TextInput text="@{data.name}"/>
<s:TextInput text="@{data.year}"/>
<s:DropDownList
selectedItem="@{data.type}"
requireSelection="true"
dataProvider="{outerDocument.wineTypes}"
labelField="name"/>
</s:HGroup>
</s:ItemRenderer>
</fx:Component>
</s:itemRenderer>
Estás duas declarações permitirá exibir mensagens de erro no campo durante a edição. O FormValidator utiliza as anotações de validação do bean ActionScript3 para saber quais validações precisam ser aplicadas.
E por fim podemos impedir o usuário de salvar adicionando uma linha.
private function save():void {
if (formValidator.validateEntity())
wineshopService.save(vineyard);
}
mvn package, jetty, …
Habilitar o Data Push é apenas uma questão de configuração. Temos 4 itens a verificar.
<graniteds:messaging-destination id="wineshopTopic" no-local="true" session-selector="true"/>
@RemoteDestination
@DataEnabled(topic="wineshopTopic", publish=PublishMode.ON_SUCCESS)
public interface WineshopService {
Spring.getInstance().addComponent("wineshopTopic", DataObserver);
Spring.getInstance().addEventObserver("org.granite.tide.login",
"wineshopTopic", "subscribe");
Spring.getInstance().addEventObserver("org.granite.tide.logout",
"wineshopTopic", "unsubscribe");
Claro que as três declarações devem usar o mesmo nome do tópico. Mas isto é tudo o que você precisa para permitir o envio de dados.
mvn package, jetty, …
Agora basta abrir vários browsers, e todas as alterações feitas em um navegador devem ser enviadas a todos os outros.
Em qualquer aplicação multiusuário, pode haver vários usuários fazendo alterações simultaneamente na mesma entidade. Usar optimistic look é a forma commun de lidar com estes casos e evitar inconssistência dos dados. O GraniteDS é capaz de lidar com este problema tanto em chamadas normais quanto com chamadas em real-time data push.
Isso é muito simples de configurar, você só precisar registrar um manipuladaro para exceções JPA OptimistickLockException e um event listener no contexto do Tide, que será chamado quando um conflito por modificação concorrente acontecer.
private function init():void {
...
Spring.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);
Spring.getInstance().getSpringContext().addEventListener(
TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);
}
private function conflictsHandler(event:TideDataConflictsEvent):void {
Alert.show("Someone has modified this vineyard at the same time\n. "
+ "Keep your changes ?",
"Conflict", Alert.YES | Alert.NO, null, function(ce:CloseEvent):void {
if (ce.detail == Alert.YES)
event.conflicts.acceptAllClient();
else
event.conflicts.acceptAllServer();
});
}
A parte mais difícil realmente é obter um conflito. Após mvn package, jetty… Abra dois navegadores, crie uma vinha no primeiro, ele irá aparecer no segundo. Edite o registro no segundo navegador altere o nome mas não salve. No primeiro navegador mude para um nome diferente e salve. No segundo navegador um alerta deve aparecer.
Este ultimo passo não é visual mas pode melhorar muito o desempenho da sua aplicação. O Suporte a lazy loading servidor-cliente garante que a quantidade de dados transferidos neste sentido é limitado, mas pode ocorrer um problema no sentido contrário(cliente-servidor). Uma vez que todo o grafo do seu Objeto é carregado para o cliente, o grafo todo será enviado ao servidor, mesmo que você tenha alterado sómente uma propriedade raiz do seu objeto. Com grafos mais profundos e complexos isto pode prejudicar o desenpenho de operações de salvar.
Para resolver isso GraniteDS oferece agora um novo recurso chamado de “reverse lazy loading”.
Isto pode ser configurado da seguinte forma
Spring.getInstance().addComponents([UninitializeArgumentPreprocessor]);
E então basta adicionar no método de update a anotação @Lazy para os argumentos de entrada
public void save(@Lazy Vineyard vineyard);
Agora você é capaz de ver o que o GraniteDS pode oferecer e como ele pode simplificador o seu desenvolvimento, e até mesmo trazer novas possibilidades para sua aplicação