Design - Flash

Custom Events + AMF + AS3 + ColdFusion – Casamento que dá certo!

Neste artigo, explico como criar uma aplicação CRUD (Create, Read, Update, Delete) de Comentários simples no Flash CS4 utilizando ActionScript 3.0, ColdFusion e para intercalar os dois o poder do protocolo AMF.

por Jhonatan Rosa Jacinto



Download

Salve, salve... Comunidade Flash!

Estou eu, Jou aqui no portal Linha de Código novamente (depois de um bom tempo desaparecido hibernando) para falar mais sobre essa poderosa tecnologia chamada Flash!

O assunto que tratarei aqui nada mais é do que a criação de uma aplicação Flash baseada nos princípios da arquitetura Orientada a Objeto usando Custom Events (eventos que você desenvolvedor cria para tratar ações específicas da sua aplicação flash) e a bênção do AMF – Adobe Action Message Format – para manipular objetos server-side diretamente (neste caso acessaremos um ColdFusion Component).

Estudo de Caso:

A app que criaremos aqui é um simples sistema de CRUD (Create, Read, Update and Delete) de Comentários. O banco de dados que utilizei nesse tutorial é o Microsoft Access 2003.

ServicoComentarioExemplo.cfc : ColdFusion Component que vai realizar as operações com o banco de dados para a nossa app Flash.

Comentario.as: Classe que vai modelar os dados da base de dados e organizados como objetos desse tipo (Comentario).

ComentarioEvento.as: Evento customizado para avisar a app quais operações foram realizadas (inserção, atualização, etc).

DB-Comentarios.mdb: Banco de dados Access da App.

TesteComentario.fla: App em si. É onde utilizaremos as classes que criamos para a aplicação.

Vamos botar a mão na massa então:

Missão 1: Criando o CFC e o Banco de dados.

Se vc já está com o ColdFusion Server Instalado no seu PC, vá em C:\ColdFusion9\wwwroot\ e crie a seguinte estrutura de pastas:

/jou/servico/comentarios: para guardar o arquivo CFC;
/jou/servico/db : para manter o banco de dados .mdb;

Obs.: Se você optar por mudar o nome do diretório ou diminuir o número de pastas na estrutura, certifique-se de colocar a mesma estrutura de pastas no código ActionScript 3 que vc criará para a sua app flash, hein!

Vá ao Dreamweaver ou qualquer outro editor HTML e digite o código ColdFusion abaixo:

<!--

      Este é o CFC que vai manipular as operações com o Banco de Dados da nossa aplicação

    As operações de inserção, exclusão, atualização, exclusão e seleção estão

    no corpo do cfcomponent -->

<cfcomponent>

<!-- Operação de inserção de dados. Método "inserirCmt()" -->

      <cffunction name="inserirCmt" access="remote" returntype="string">

            <!-- argumentos da função -->

            <cfargument name="titulo" type="string" required="yes">

            <cfargument name="texto" type="string" required="yes">

            <cfargument name="autor" type="string" required="yes">

      <cftry>

            <!-- mensagem padrão que deve ser enviada ao flash em caso de execução bem sucedida da função -->

                  <cfset mensagem = "Comentário inserido com sucesso!">           

            <!-- criamos a conexão com o datasource e executamos o SQL de inserção

                  com os dados enviados pelos argumentos da função -->

            <cfquery datasource="dsComentarios" username="" password="">

            insert into Comentarios( titulo, texto, autor )

                values( <cfqueryparam value="#arguments.titulo#">,

                      <cfqueryparam value="#arguments.texto#">,

                        <cfqueryparam value="#arguments.autor#"> )

            </cfquery>

           

            <cfcatch type="any">

                  <!-- caso ocorra algum erro, passamos a mensagem do erro para a variável de retorno "mensagem" -->

                  <cfset mensagem = cfcatch.Message>

            </cfcatch>

        </cftry>

        <!-- retornamos a mensagem para o flash -->

        <cfreturn mensagem>

      </cffunction>

<!-- fim da função inserirCmt() -->

<!-- Operação de atualização de dados. Método "atualizarCmt()" -->

      <cffunction name="atualizarCmt" access="remote" returntype="string">

<!-- argumentos da função -->

            <cfargument name="id" type="numeric" required="yes">

            <cfargument name="titulo" type="string" required="yes">

            <cfargument name="texto" type="string" required="yes">

            <cfargument name="autor" type="string" required="yes">

      <cftry>

            <!-- mensagem padrão que deve ser enviada ao flash em caso de execução bem sucedida da função -->

                  <cfset mensagem = "Comentário atualizado com sucesso!">

            <!-- criamos a conexão com o datasource e executamos o SQL de atualização

                  com os dados enviados pelos argumentos da função -->

            <cfquery datasource="dsComentarios" username="" password="">

            update Comentarios

            set titulo=<cfqueryparam value="#arguments.titulo#">,

            texto=<cfqueryparam value="#arguments.texto#">,

            autor=<cfqueryparam value="#arguments.autor#">

            where id=<cfqueryparam value="#arguments.id#">

            </cfquery>

           

            <cfcatch type="any">

                  <!-- caso ocorra algum erro, passamos a mensagem do erro para a variável de retorno "mensagem" -->

                  <cfset mensagem = cfcatch.Message>

            </cfcatch>

        </cftry>

        <!-- retornamos a mensagem para o flash -->

        <cfreturn mensagem>

      </cffunction>

<!-- fim da função atualizarCmt() -->

<!-- Operação de exclusão de dados. Método "excluirCmt()" -->

      <cffunction name="excluirCmt" access="remote" returntype="string">

<!-- argumentos da função -->

     <cfargument name="id" type="numeric" required="yes">

      <cftry>

            <!-- mensagem padrão que deve ser enviada ao flash em caso de execução bem sucedida da função -->

                  <cfset mensagem = "Comentário excluído com sucesso!">          

            <!-- criamos a conexão com o datasource e executamos o SQL de exclusão

                  com os dados enviados pelos argumentos da função -->

            <cfquery datasource="dsComentarios" username="" password="">

                  delete from Comentarios where id=<cfqueryparam value=”#arguments.id#”>

            </cfquery>

           

            <cfcatch type="any">

                  <!-- caso ocorra algum erro, passamos a mensagem do erro para a variável de retorno "mensagem" -->

                  <cfset mensagem = cfcatch.Message>

            </cfcatch>

        </cftry>

        <!-- retornamos a mensagem para o flash -->

        <cfreturn mensagem>

      </cffunction>

<!-- fim da função excluirCmt() -->

<!-- Operação de seleção de dados. Método "selecionarTodos()" -->

      <cffunction name="selecionarTodos" access="remote" returntype="query">

        <!-- criamos a conexão com o datasource e executamos o SQL de seleção -->

            <cfquery name="listaComentarios" datasource="dsComentarios" username="" password="">

                  select * from Comentarios order by id desc;

            </cfquery>

        <!-- retornamos a lista de Comentários para o flash -->

        <cfreturn listaComentarios>

      </cffunction>

<!-- fim da função selecionarTodos() -->

   

</cfcomponent>

Salve o arquivo .cfc como ServicoComentarioExemplo.cfc na estrutura de pastas mencionada para o CFC anteriormente.

Vamos ao banco de dados agora.

Abra o Microsoft Access crie um novo banco de dados e salve-o como DB-Comentarios.mdb na estrutura de pastas que mencionei no ColdFusion Server.

Crie uma tabela chamada Comentarios e crie os seguintes campos:

id (Numeração Automática);
titulo (texto);
texto (memorando);
autor (texto).

Se quiser inserir 2 ou 3 registros no banco de dados para que o flash já os carregue na primeira vez, tudo bem... mas se quiser deixá-los em branco tudo bem tmb. Não tem importância.

Vamos configurar agora o datasource para que o ColdFusion Component ache a conexão com o banco de dados.

Vá ao ColdFusion Administrator e digite seu usuário e senha de Administrador para acessar o painel de controle do servidor CF.

Após logar, vá ao menu à esquerda e procure por Data Sources, na aba Data & Services:

Uma página de configuração irá aparecer. Forneça as informações abaixo:

Clique em Add. Forneça o caminho do Banco de dados no Campo DataBase File:

No meu caso, o caminho é C:\ColdFusion9\wwwroot\jou\servico\db\DB-Comentarios.mdb

Clique sobre o botão Submit para finalizar. Se a mensagem a seguir aparecer...

O data source foi criado com sucesso. Clique em Logout para sair do ColdFusion Administrator.

O server-side já está configurado.
Vamos ao Flash agora.


Missão 2: Classes AS.

Criaremos agora as classes ComentarioEvento.as e Comentario.as. Como tenho várias classes criadas em ActionScript 3, tive que organiza-las na minha pasta pessoal da seguinte forma.

/jou/exemplo/events : onde ficará a classe ComentarioEvento.as;

/jou/exemplo/model: onde ficará a classe Comentario.as.

Sendo assim, os packages para ambas as classes ficaria sendo:

package jou.exemplo.events = ComentarioEvento.as e...
package jou.exemplo.model = Comentario.as.

É apenas uma forma de organizar que encontrei aqui no meu PC. Porém se vc quiser mudar a estrutura fique à vontade.

Vamos aos códigos. Começando pelo ComentarioEvento.as:

/*

*     O pacote jou.exemplo.events vai conter as classes de manipulação de eventos,

*     ou seja, nossas classes customizadas de eventos que extendem a classe Event do

*     ActionScript 3

**/

package jou.exemplo.events

{

     

      /*importamos a classe base Events */

      import flash.events.Event;

      /* o pacote jou.exemplo.model contém as classes de Modelagem de Dados. É onde está a classe

      Comentario **/

      import jou.exemplo.model.*;

     

      /* Declaramos a classe ComentarioEvento que controla os eventos relacionados

      *  às operações com os comentários da nossa app flash, que neste caso serão: seleção

      de comentários, exclusão, atualização e inserção **/

     

      public class ComentarioEvento extends Event

      {

            /* Criamos um campo chamado "listaComentarios" que representa um array do tipo

            Comentario que será passado junto com o evento disparado */

            public var listaComentarios:Array;

            /* criamos um campo chamado "mensagem" que vai receber a mensagem vinda

            da operação efetuada no servidor CF **/

            public var mensagem:String = null;

            /* criamos algumas constantes que avisam qual tipo de evento foi disparado, ou seja,

            se foi uma inserção, uma exclusão, etc. */

            //ON_EXCLUIR_COMENTARIO: quando houver uma exclusão

            public static const ON_EXCLUIR_COMENTARIO:String = "onExcluirComentario";

            //ON_ATUALIZAR_COMENTARIO: quando houver uma atualização

            public static const ON_ATUALIZAR_COMENTARIO:String = "onAtualizarComentario";

            //ON_COMENTARIOS_CARREGADOS: quando a busca de comentários no Server-Side for completada e carregada

            //na aplicação flash.

            public static const ON_COMENTARIOS_CARREGADOS:String = "onComentariosCarregados";

            //ON_INSERIR_COMENTARIO: quando houver uma inserção.

            public static const ON_INSERIR_COMENTARIO:String = "onInserirComentario";

           

            public function ComentarioEvento(type:String, bubbles:Boolean=false, cancelable:Boolean=false):void

            {

                  //fazemos um override do Construtor da Classe e passamos os mesmos parametros para a super classe Event

                  super(type, bubbles, cancelable);

            }

      }

}

Após salvar o arquivo na estrutura de pastas recomendada, crie um novo arquivo ActionScript 3.0 no Flash. Vamos criar a classe Comentario.as agora:

/*

*     O pacote jou.exemplo.model contem classes de Modelagem de Dados,

*     ou seja, classes que descrevem as características dos objetos usados na aplicação.

*     Neste caso específico, nossa classe de Modelagem é a classe Comentario.

**/

package jou.exemplo.model

{

      /* importamos as classes que utilizaremos na nossa app */

      import flash.display.Sprite;

      import flash.events.*;

      //A classe NetConnection vai realizar a conexão com o ColdFusion Component

      import flash.net.NetConnection;

      /* A classe Responder vai receber a resposta gerada no servidor pelo ColdFusion Component */

      import flash.net.Responder;

      /* importamos nosso pacote de eventos para utilizarmos a classe

            ComentarioEvento */

      import jou.exemplo.events.*;

     

      public class Comentario extends Sprite

      {

            //criamos os campos privados da nossa classe

            private var _id:int = 0;

            private var _titulo:String = null;

            private var _texto:String = null;

            private var _autor:String = null;

            //criamos o objeto nc do tipo NetConnection que realizará as operações Remotas

            private var nc:NetConnection;

            //criamos o responder também;

            private var res:Responder;

            //gateway de conexão com o ColdFusion Server

            private var urlgateway:String = "http://localhost:8500/flashservices/gateway";

           

            /*

            *     Declaramos o construtor da Classe. */

            public function Comentario():void

            {     }

           

            //criamos os getters e setters das propriedades da nossa classe

            public function set id( _pid:int ):void { this._id = _pid; }

            public function get id():int { return this._id; }

            public function set titulo( _ptitulo:String ):void { this._titulo = _ptitulo; }

            public function get titulo():String { return this._titulo; }

            public function set texto( _ptexto:String ):void { this._texto = _ptexto; }

            public function get texto():String { return this._texto; }

            public function set autor( _pautor:String ):void { this._autor = _pautor; }

            public function get autor():String { return this._autor; }

           

            /*

            *     @Método "inserir()": como o próprio nome já diz, ele insere um

            *     novo Comentário no Banco de dados. **/

            public function inserir():void

            {

                  //criamos um novo objeto NetConnection();

                  nc = new NetConnection();

                  //criamos um responder para encapsular os dados retornados do servidor

                  res = new Responder( resInserirComentario );

                  //conectamos ao gateway do servidor ColdFusion

                  nc.connect( this.urlgateway );

                  /* chamamos o método inserirCmt criado no ColdFusion Component "ServicoComentarioExemplo"

                    com o auxilio do método call() da classe NetConnection. Passamos o Responder "res"

                    para guardar a mensagem retornada pelo servidor CF e em seguida, passamos

                    os parametros da função de inserção com os dados do comentário em questão que são

                    titulo, texto e autor */

                  nc.call( "jou.servico.comentarios.ServicoComentarioExemplo.inserirCmt", res, this.titulo, this.texto, this.autor );

            }

           

            /*

            *     E o "chama-chama" de métodos continua.

            *     Aqui ocorre o mesmo que no inserir(). O que muda é que o método que estamos chamando

            é o atualizarCmt() que faz um update dos dados no banco de dados, baseado nos

            argumentos passados para a função **/

            public function atualizar():void

            {

                  nc = new NetConnection();

                  res = new Responder( resAtualizarComentario );

                  nc.connect( this.urlgateway );

                  /* passamos os mesmos parametros que usamos na função inserir(), porém adicionamos

                  o parametro "id" para que o ColdFusion saiba qual comentário atualizar **/

                  nc.call( "jou.servico.comentarios.ServicoComentarioExemplo.atualizarCmt", res, this.id, this.titulo, this.texto, this.autor );

            }

           

            public function excluir():void

            {

                  nc = new NetConnection();

                  res = new Responder( resExcluirComentario );

                  nc.connect( this.urlgateway );

                  /* aqui passamos apenas o id para a função excluirCmt pois é só disso

                  que ela precisa para saber qual comentário excluir **/

                  nc.call( "jou.servico.comentarios.ServicoComentarioExemplo.excluirCmt", res, this.id );

            }

           

            /* Aqui a coisa muda um pouco!

            carregarTodos() faz uma seleção dos comentários presentes no banco de dados. Como a função

            no CFC não requer nenhum argumento específico (parametro), apenas indicamos o método

            a ser chamado e o objeto responder que vai receber os dados retornados no método call(); **/

            public function carregarTodos():void

            {

                  nc = new NetConnection();

                  res = new Responder( resBuscarDados );

                  nc.connect( this.urlgateway );

                  nc.call( "jou.servico.comentarios.ServicoComentarioExemplo.selecionarTodos", res );

            }

           

            /*

            *     @Função "resInserirComentario()" é a função que é executada quando o responder

            *     é criado e retornado para o flash com os dados do servidor. Neste caso, quando a operação

            *     de inserção, especificamente, é chamada no CFC **/

            private function resInserirComentario( retorno:Object ):void

            {

                  /* É ai que entra o nosso Evento Customizado!!!

                  *     criamos um novo evento Customizado do tipo ComentarioEvento avisando-o

                  * no parametro "type" do construtor ( new ComentarioEvento()) o tipo de evento

                  *     que está sendo disparado. Neste caso, é o evento "onInserirComentario". **/

                  var insEvento:ComentarioEvento = new ComentarioEvento( "onInserirComentario" );

                  /* passamos para o campo "mensagem" a mensagem retornada pelo método inserirCmt() do

                  CF Component **/

                  insEvento.mensagem = retorno.toString();

                  /* despachamos o evento notificando a aplicação inteira de que um evento de inserção

                  aconteceu **/

                  dispatchEvent( insEvento );

                  //fechamos a conexão com o servidor.

                  nc.close();

            }

           

            private function resAtualizarComentario( retorno:Object ):void

            {

                  /*

                  *     Quando a atualização é feita e o responder é retornado do servidor

                  * a função resAtualizarComentario é chamada e disparamos um evento do tipo

                  *     onAtualizarComentario **/

                  var atualizarEvento:ComentarioEvento = new ComentarioEvento( "onAtualizarComentario" );

                  atualizarEvento.mensagem = retorno.toString();

                  dispatchEvent( atualizarEvento );

                  nc.close();

            }

           

            private function resExcluirComentario( retorno:Object ):void

            {

                  /*

                  *     Mesma coisa. Quando o evento de exclusão é chamado a função resExcluirComentario

                  é chamada e disparamos um evento do tipo onExcluirComentario **/

                  var excluirEvento:ComentarioEvento = new ComentarioEvento( "onExcluirComentario" );

                  excluirEvento.mensagem = retorno.toString();

                  dispatchEvent( excluirEvento );

                  nc.close();

            }

           

            private function resBuscarDados( retorno:Object ):void

            {

                  /*

                  *     Quando a seleção de dados é completada, criamos um novo evento do tipo

                  onComentariosCarregados para avisarmos a app que os dados já estão disponíveis **/

                  var carregarEvento:ComentarioEvento = new ComentarioEvento( "onComentariosCarregados" );

                  //criamos um Array para organizar todos os comentários retornados numa lista.

                  var listaC:Array = new Array();

                 

                  /*

                  *     serverInfo.initialData contém um array de objetos que tem as informações retornadas

                  *     do servidor. Sendo que [0][0] representa a primeira linha e a primeira coluna (id) do

                  *     banco de dados respectivamente. Assim, [0][0] é a coluna id da primeira linha,

                  *     [0][1] é a coluna titulo da primeira linha, etc **/

                  for ( var i:int = 0; i < retorno.serverInfo.initialData.length; i++ )

                  {

                        //no loop criamos um novo Objeto Comentario para cada dado retornado.

                        var c:Comentario = new Comentario();

                        //passamos as informações para as propriedades do objeto Comentario

                        c.id = retorno.serverInfo.initialData[i][0];

                        c.titulo = retorno.serverInfo.initialData[i][1];

                        c.texto = retorno.serverInfo.initialData[i][2];

                        c.autor = retorno.serverInfo.initialData[i][3];

                        //e adicionamos a lista.

                        listaC.push( c );

                  }

                 

                  //passamos a lista para a propriedade "listaComentarios" do evento

                  carregarEvento.listaComentarios = listaC;

                  //despachamos a bagaça

                  dispatchEvent( carregarEvento );

                  //e fechamos a conexão...

                  nc.close();

            }

      }

}

Salve a classe no diretório especificado pelo package da mesma.

Missão 3: A App Final hehehe!!!.

Depois dessa porrada de códigos... mais códigos! RS!
Vamos ao nosso arquivo flash. Crie um novo .fla – ActionScript 3.0 e salve-o no mesmo diretório em que está os pacotes jou.exemplo.model e jou.exemplo.events.

Exemplo:

Se você criou sua estrutura de pacotes em Meus Documentos - .../Meus Documentos/jou/exemplo/model/*.as -, salve o fla em Meus Documentos. Assim, quando importarmos as classes no .fla, o flash vai conseguir achar os pacotes sem problema pois tanto o fla quando a estrutura de pacotes está no mesmo diretório.

Arraste para o Stage os seguintes componentes:

DataGrid = dgComentarios.

Label = lblId;

TextInput = txtAutor, txtTitulo;

TextArea = txtTexto.

Button = btnInserir, btnAtualizar, btnExcluir.

Veja como ficou a minha interface.

Digite o código abaixo no Actions-Frame:

import jou.exemplo.model.*;

import jou.exemplo.events.*;

import fl.data.DataProvider;

import fl.controls.dataGridClasses.DataGridColumn;

import fl.controls.*;

var dp:DataProvider;

dgComentarios.addEventListener( Event.CHANGE, selecionarEvento );

dgComentarios.columns = [new DataGridColumn("Id"), new DataGridColumn("Titulo"), new DataGridColumn("Autor"), new DataGridColumn("Comentario")];

btnInserir.addEventListener( MouseEvent.CLICK, eventoMouse );

btnAtualizar.addEventListener( MouseEvent.CLICK, eventoMouse );

btnExcluir.addEventListener( MouseEvent.CLICK, eventoMouse );

carregarDados();

function carregarDados():void

{

      var c:Comentario = new Comentario();

      c.addEventListener( ComentarioEvento.ON_COMENTARIOS_CARREGADOS, eventoComentariosCarregados );

      c.carregarTodos();

}

function eventoComentariosCarregados( event:ComentarioEvento ):void {

      if ( event.listaComentarios.length > 0 ) {

            dp = new DataProvider();

            for( var i:int = 0; i < event.listaComentarios.length; i++ ) {

                  dp.addItem({ Id:event.listaComentarios[i].id,

                                   Titulo:event.listaComentarios[i].titulo,

                                   Autor:event.listaComentarios[i].autor,

                                   Comentario:event.listaComentarios[i].texto });

            }

            dgComentarios.dataProvider = dp;

      } else {

            trace("Não há registro de Comentários");

      }

}

function selecionarEvento(event:Event):void {

      lblId.text = event.target.selectedItem.Id;

      txtTexto.text = event.target.selectedItem.Comentario;

      txtAutor.text = event.target.selectedItem.Autor;

      txtTitulo.text = event.target.selectedItem.Titulo;

}

function eventoMouse(event:MouseEvent):void {

      var c:Comentario = new Comentario();

      if ( event.target.label == "Inserir" ) {

            c.addEventListener( ComentarioEvento.ON_INSERIR_COMENTARIO, eventoInsComt );

            c.titulo = txtTitulo.text;

            c.autor = txtAutor.text;

            c.texto = txtTexto.text;

            c.inserir();

      } else if ( event.target.label == "Atualizar" ) {

            if ( lblId.text != "" ) {

                  c.addEventListener( ComentarioEvento.ON_ATUALIZAR_COMENTARIO, eventoAtuaComt );

                  c.id = int(lblId.text);

                  c.titulo = txtTitulo.text;

                  c.autor = txtAutor.text;

                  c.texto = txtTexto.text;

                  c.atualizar();

            }

      } else if ( event.target.label == "Excluir" ) {

            if ( lblId.text != "" ) {

                  c.addEventListener( ComentarioEvento.ON_EXCLUIR_COMENTARIO, eventoExComt );

                  c.id = int(lblId.text);

                  c.excluir();

            }

      }

     

      limparCaixas();

}

function eventoExComt(event:ComentarioEvento):void {

      trace( event.mensagem );

      carregarDados();

}

function eventoAtuaComt(event:ComentarioEvento):void {

      trace( event.mensagem );

      carregarDados();

}

function eventoInsComt(event:ComentarioEvento):void {

      trace( event.mensagem );

      carregarDados();

}

function limparCaixas():void {

      txtTitulo.text = "";

      txtAutor.text = "";

      lblId.text = "";

      txtTexto.text = "";

}

Testandooo.............

Dê CTRL+Enter e vejamos o que acontece:

Primeiramente os dados são carregados e mostrados no dgComentarios:

No meu caso, tinha registrado de antes 3 registros no meu DB.
Preencha os campos do formulário e clique em inserir.

O dado é inserido:

E a mensagem mostrada.

Ao clicar em algum item do DataGrid, o formulário é preenchido e então podemos alterar dados ou excluí-los:

Ao clicar em atualizar...

O dado é alterado e a mensagem mostrada na tela output:

E finalmente a exclusão:

Item excluído...

Mensagem de exclusão

É isso ae gente.

Espero que tenham gostado do artigo e que o teste de vocês aí em casa tenha funcionado.

De qualquer forma estou a disposição para esclarecer qualquer dúvida sobre o artigo, basta me add no MSN jhonatansaopaulino@hotmail.com e me envie sua dúvida.

É tudo por enquanto! No próximo artigo, vamos criar uma galeria de fotos utilizando os mesmos princípios!!!

Jhonatan Rosa Jacinto

Jhonatan Rosa Jacinto