Integrações entre Sistemas – Parte 3 – File Transfer

Objetivo

Nesta parte veremos uma breve explicação sobre o código em relação ao método de transferência de arquivos. Para isso antes, será necessário explicar a estrutura das classes de acesso a dados, que fazem o trabalho propriamente dito.

Classe de acesso a dados

Como o objetivo desta série não é exercitar patterns, encapsulamento, nada deste tipo, a classe de acesso a dados somente contém a lógica de acesso ao banco, segue o código dela:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IntegrationTests.ServiceClasses.Domain;
using System.Data.SqlClient;
using System.Data;

namespace IntegrationTests.ServiceClasses
{
    public class DAO
    {
        public static ServiceTable GetServiceTable(string ConnString, int ServiceTableID)
        {
            ServiceTable result = new ServiceTable();

            SqlConnection conn = new SqlConnection(ConnString);
            conn.Open();

            SqlCommand cmd = new SqlCommand("select ServiceTableID, DescServiceTable, Value, CreationDate, StringField1, StringField2 " +
                "from ServiceTable where ServiceTableID = @ServiceTableID", conn);

            using (conn)
            {
                SqlParameter p1 = cmd.Parameters.Add("@ServiceTableID", SqlDbType.Int);
                p1.Value = ServiceTableID;

                SqlDataReader rd = cmd.ExecuteReader();
                rd.Read();
                using (rd)
                {
                    result.ServiceTableID = rd.GetInt32(0);
                    result.DescServiceTable = rd.GetString(1);
                    result.Value = (float)rd.GetDouble(2);
                    result.CreationDate = rd.GetDateTime(3);
                    result.StringField1 = rd.GetString(4);
                    result.StringField2 = rd.GetString(5);
                }
            }

            return result;
        }

        public static void ProcessServiceTable(string ConnString, ServiceTable table)
        {
            SqlConnection conn = new SqlConnection(ConnString);
            conn.Open();

            SqlCommand cmd = new SqlCommand("insert into ClientTable (ClientTableID, DescClientTable, Value, CreationDate, StringField1, StringField2)" +
                "values (@ClientTableID, @DescClientTable, @Value, @CreationDate, @StringField1, @StringField2)", conn);

            using (conn)
            {

                SqlParameter p1 = cmd.Parameters.Add("@ClientTableID", SqlDbType.Int);
                SqlParameter p2 = cmd.Parameters.Add("@DescClientTable", SqlDbType.VarChar, 200);
                SqlParameter p3 = cmd.Parameters.Add("@Value", SqlDbType.Float);
                SqlParameter p4 = cmd.Parameters.Add("@CreationDate", SqlDbType.DateTime);
                SqlParameter p5 = cmd.Parameters.Add("@StringField1", SqlDbType.VarChar, 200);
                SqlParameter p6 = cmd.Parameters.Add("@StringField2", SqlDbType.VarChar, 200);

                p1.Value = table.ServiceTableID;
                p2.Value = table.DescServiceTable;
                p3.Value = table.Value;
                p4.Value = table.CreationDate;
                p5.Value = table.StringField1;
                p6.Value = table.StringField2;

                cmd.ExecuteNonQuery();
            }
        }

        public static void ClearClientTable(string ConnString)
        {
            ServiceTable result = new ServiceTable();

            SqlConnection conn = new SqlConnection(ConnString);
            conn.Open();

            SqlCommand cmd = new SqlCommand("delete from ClientTable", conn);

            using (conn)
                cmd.ExecuteNonQuery();
        }
    }
}

Como vemos, a maioria dos métodos são estáticos, somente para simplificar o uso da classe, visto que ela não tem nenhuma propriedade ou dado.

Os métodos GetServiceTable table e ProcessServiceTable simplemente pegam ou inserem um registro da base, sempre utilizando uma representação em classe invés de um dataset (Classe ServiceTable).

A string de conexão sendo passada como parâmetro é uma péssima prática, mas não é nosso objetivo trabalhar nesse tipo de conceito aqui.

Utilitários para trabalhar com Stream

Como a maioria dos métodos de transferência utilizam-se de streams para trafegar os dados, optei por construir esta classe, para encapsular o comportamento de gravar/ler de streams, sempre utilizando um formato XML interno, visto que nosso objetivo é justamente manter o mesmo payload e somente trocar o método de transferência.

Segue o código da classe StreamUtil, para trabalhar com streams:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using IntegrationTests.ServiceClasses.Domain;
using System.Globalization;

namespace IntegrationTests.ServiceClasses
{
    public class StreamUtil
    {
        public void ProcessClientBigRequest(string ConnString, Stream requestStream, Stream responseStream, bool dispose)
        {
            XmlTextReader xr = new XmlTextReader(requestStream);

            XmlTextWriter xw = new XmlTextWriter(responseStream, Encoding.UTF8);

            xw.WriteStartDocument();
            xw.WriteStartElement("table");

            using (xr)
            {
                xr.Read();
                xr.Read();
                xr.ReadStartElement("request");
                while (xr.Name == "id")
                {
                    ServiceTable st = DAO.GetServiceTable(ConnString, Convert.ToInt32(xr.ReadElementString("id")));

                    xw.WriteStartElement("record");
                    xw.WriteElementString("ServiceTableID", st.ServiceTableID.ToString());
                    xw.WriteElementString("DescServiceTable", st.DescServiceTable);
                    xw.WriteElementString("Value", st.Value.ToString("0.00"));
                    xw.WriteElementString("CreationDate", st.CreationDate.ToString("dd/MM/yyyy hh:mm:ss"));
                    xw.WriteElementString("StringField1", st.StringField1);
                    xw.WriteElementString("StringField2", st.StringField2);
                    xw.WriteEndElement();

                }
                xr.ReadEndElement();
            }
            xw.WriteEndElement();
            xw.Flush();
            Console.WriteLine("Finished processing big request");


            if (dispose)
                xw.Close();
        }

        public void GenerateBigRequest(Stream stream, bool dispose, int size)
        {
            XmlTextWriter xw = new XmlTextWriter(stream, Encoding.UTF8);

            xw.WriteStartDocument();
            xw.WriteStartElement("request");
            for (int i = 0; i < size; i++)
            {
                xw.WriteElementString("id", (i + 1).ToString());
            }
            xw.WriteEndElement();
            xw.Flush();

            if (dispose)
                xw.Close();
        }

        public void GenerateBigRequest(Stream stream, bool dispose, int start, int end)
        {
            XmlTextWriter xw = new XmlTextWriter(stream, Encoding.UTF8);

            xw.WriteStartDocument();
            xw.WriteStartElement("request");
            for (int i = start; i <= end; i++)
            {
                xw.WriteElementString("id", (i).ToString());
            }
            xw.WriteEndElement();
            xw.Flush();

            if (dispose)
                xw.Close();
        }

        public XmlTextWriter GenerateBigRequestStart(Stream stream)
        {
            XmlTextWriter xw = new XmlTextWriter(stream, Encoding.UTF8);
            xw.WriteStartDocument();
            xw.WriteStartElement("request");
            return xw;
        }

        public void GenerateBigRequestItem(XmlTextWriter xw, int id)
        {
            xw.WriteElementString("id", id.ToString());
        }

        public void GenerateBigRequestEnd(XmlTextWriter xw)
        {
            xw.WriteEndElement();
        }

        public void ImportarStream(string connString, Stream stream)
        {
            XmlTextReader rd = new XmlTextReader(stream);

            CultureInfo c = new CultureInfo("pt-BR");
            c.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy hh:mm:ss";

            using (rd)
            {
                rd.Read();
                rd.Read();
                rd.ReadStartElement("table");
                while (rd.Name == "record")
                {
                    rd.ReadStartElement("record");
                    ServiceTable st = new ServiceTable();
                    st.ServiceTableID = Convert.ToInt32(rd.ReadElementContentAsString("ServiceTableID", ""));
                    st.DescServiceTable = rd.ReadElementContentAsString("DescServiceTable", "");
                    st.Value = Convert.ToSingle(rd.ReadElementContentAsString("Value", ""));
                    st.CreationDate = Convert.ToDateTime(rd.ReadElementContentAsString("CreationDate", ""), c.DateTimeFormat);
                    st.StringField1 = rd.ReadElementContentAsString("StringField1", "");
                    st.StringField2 = rd.ReadElementContentAsString("StringField2", "");
                    rd.ReadEndElement();

                    DAO.ProcessServiceTable(connString, st);
                }
                rd.ReadEndElement();
            }
        }

    }
}

O método ProcessClientBigRequest recebe um xml com os ID’s que devem ser retornados na requestStream. Ele tem por objetivo ler essa stream e gravar o resultado na ResponseStream. Internamente, a classe DAO é usada para retornar os dados do banco. O parâmetro dispose, indica somente se o XmlWriter usado para escrever o código será liberado no final. Ele foi criado, pois quando estamos utilizando MemoryStream, e liberamos o XmlWriter, a Stream é liberada também. Não desejamos este comportamento em alguns casos.

O método GenerateBigRequest, gera um arquivo de request para o método ClientBigRequest. O inteiro que ele recebe é somente a quantidade de registros que ele vai colocar no request. Ele tem duas variações, uma recebendo o tamanho, outra recebendo o ID de início e de fim. A segunda será usada no futuro. Outra variação são os métodos GenerateBigRequestStart, GenerateBigRequestItem e GenerateBigRequestEnd. Um escreve o cabeçalho, outro os itens de processamento, outro o fim.

Por último, o método ImportarStream. Este método faz o trabalho de receber a stream com a resposta (todos os dados) e inserir na tabela, é quem processa a “resposta” do servidor.

Utilitário para trabalhar com arquivos

A classe abaixo basicamente faz as mesmas coisas que a de cima, só encapsula parâmetros de arquivos, cria FileStreams e joga para a StreamUtil fazer o trabalho sujo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Xml;
using System.Data;
using System.IO;
using System.Globalization;
using IntegrationTests.ServiceClasses.Domain;
using System.Threading;

namespace IntegrationTests.ServiceClasses
{
    public class FileUtil
    {
        public void ExtrairParaArquivo(string ConnString, int IDInicial, int IDFinal, string file)
        {
            SqlConnection conn = new SqlConnection(ConnString);
            conn.Open();

            SqlCommand cmd = new SqlCommand("select ServiceTableID, DescServiceTable, Value, CreationDate, StringField1, StringField2 " +
                "from ServiceTable where ServiceTableID between @IDInicial and @IDFinal", conn);

            XmlTextWriter xw = new XmlTextWriter(file, Encoding.UTF8);

            using (xw)
            {
                xw.WriteStartDocument();
                xw.WriteStartElement("table");

                using (conn)
                {
                    SqlParameter p1 = cmd.Parameters.Add("@IDInicial", SqlDbType.Int);
                    p1.Value = IDInicial;
                    SqlParameter p2 = cmd.Parameters.Add("@IDFinal", SqlDbType.Int);
                    p2.Value = IDFinal;

                    SqlDataReader rd = cmd.ExecuteReader();
                    using (rd)
                    {
                        while (rd.Read())
                        {
                            int ServiceTableID = rd.GetInt32(0);
                            string DescServiceTable = rd.GetString(1);
                            string Value = ((float)rd.GetDouble(2)).ToString("0.00");
                            string CreationDate = rd.GetDateTime(3).ToString("dd/MM/yyyy hh:mm:ss");
                            string StringField1 = rd.GetString(4);
                            string StringField2 = rd.GetString(5);

                            xw.WriteStartElement("record");
                            xw.WriteElementString("ServiceTableID", ServiceTableID.ToString());
                            xw.WriteElementString("DescServiceTable", DescServiceTable);
                            xw.WriteElementString("Value", Value);
                            xw.WriteElementString("CreationDate", CreationDate);
                            xw.WriteElementString("StringField1", StringField1);
                            xw.WriteElementString("StringField2", StringField2);
                            xw.WriteEndElement();
                        }
                    }
                }
                xw.WriteEndElement();
            }
        }

        public void ImportarArquivo(string connString, string file)
        {
            FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None);            
            StreamUtil u = new StreamUtil();
            u.ImportarStream(connString, fs);            
        }

        public int ProcessRequest(string requestFile)
        {
            FileStream fs = new FileStream(requestFile, FileMode.Open, FileAccess.Read, FileShare.None);
            byte[] buffer = new byte[sizeof(int)];
            fs.Read(buffer, 0, sizeof(int));
            return BitConverter.ToInt32(buffer, 0);
        }

        public void ProcessClientRequest(string ConnString, string responseFile, int size)
        {            
            ExtrairParaArquivo(ConnString, 1, size, responseFile);
        }

        public void ProcessClientBigRequest(string ConnString, string requestFile, string responseFile)
        {
            FileStream requestFileStream = new FileStream(requestFile, FileMode.Open);       
            FileStream responseFileStream = new FileStream(responseFile, FileMode.Create);            
            StreamUtil u = new StreamUtil();
            u.ProcessClientBigRequest(ConnString, requestFileStream, responseFileStream, true);
        }

        public static void WaitForUnlock(string file)
        {
            FileStream fs = null;
            bool tryAgain = true;
            while (tryAgain)
            {
                try
                {
                    fs = new FileStream(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                    using (fs)
                        fs.Close();
                    tryAgain = false;
                }
                catch (IOException)
                {
                    tryAgain = true;
                    Thread.Sleep(200);
                }

            }
        }


    }
}

O método ExtrairParaArquivo ainda não foi refatorado. A lógica de acesso a dados deveria estar na DAO e a lógica de escrever na Stream na StreamUtil, porém, mantive ele aqui por ser um brinde. Ele será usado no exemplo de extração sem nenhum request. É interessante para observamos a diferença de tempo entre somente extrair a informação e importar (integração entre sistemas) e a abordagem request/response (reuso de comportamento).

O método ImportarArquivo, somente encapsula ImportarStream, com o objetivo de inserir o resultado na base de dados. ProcessRequest pega somente a “quantidade” de registros a ser gerado. ProcessClientRequest será usado pelo servidor para baseado na quantidade obtida por ProcessRequest e extrair um arquivão com os registros. ProcessClientBigRequest é quem recebe registro a registro o que deve ser extraído e gera a resposta.

WaitForUnlock talvez seja a única coisa diferente aqui. Dado um arquivo, ele fica tentando acessá-lo até que o mesmo esteja disponível. É uma bela gambiarra, mas não percebi outra forma de determinar se um arquivo está em lock ou não (em uso por outro processo, outra thread, etc).

Os exemplos dos métodos um a um não são claros. Quando estudarmos a abordagem dos testes de File Transfer, eles farão mais sentido.

File Transfer – Extração Direta

Essa abordagem traz a seguinte idéia:

  • O cliente manda um arquivo para o servidor, este arquivo possui um inteiro dizendo quantos registros ele espera
  • O servidor recebe este arquivo, pega a quantidade de registros e gera um arquivo com todo o conteúdo (os campos da ServiceTable), de acordo com a quanitdade informada
  • O cliente fica ouvindo um diretório esperando o arquivo chegar. Quando o arquivo chega, ele o processa e insere na base do cliente (ClientTable)

O código começaremos com a classe FileTransferRequest:

        public override bool Execute()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            FileSystemWatcher watcher = new FileSystemWatcher(InputDir);
            watcher.Created += new FileSystemEventHandler(watcher_Created);
            watcher.EnableRaisingEvents = true;

            FileStream fs = new FileStream(OutputDir + "\\request.xml", FileMode.Create);
            Log.LogMessage("Generating " + OutputDir + " with " + Records.ToString());
            using (fs)
                fs.Write(BitConverter.GetBytes(Records), 0, 3);

            Log.LogMessage("Waiting for " + InputDir + "\\response.xml");

            while (!finished)
                Thread.Sleep(250);

            watch.Stop(;)

            Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds");

            return true;
        }

O método acima é executado pelo MSBuild. O stopwatch é utilizado apra calcular os tempos e um FileSystemWatcher é utilizado para “ouvir” o arquivo. Quando chegar o arquivo de resposta, o método watcher_Created será chamado (pendurado no evento Created) do FileSystemWatcher.

Em seguida, abrimos um arquivo e escrevemos a quantidade de registros esperada (recebido em propriedade do MSBuild). Isso deveria estar na FileUtil, mas fica para um refactor futuro.

Uma vez feito isso, fica em loop dormindo até que a resposta seja recebida. O bool finished é atualizado pelo evento watcher_Created.

Agora vamos seguir o fluxo e pensar como servidor. Esse código está na classe FileTransferTestServer:

        public override bool Execute()       
        {
            FileSystemWatcher watcher = new FileSystemWatcher(InputDir);
            watcher.Created += new FileSystemEventHandler(watcher_Created);
            watcher.EnableRaisingEvents = true;

            while (true)
                Thread.Sleep(250);            
        }                   

O servidor também está numa task MSBuild. Obviamente essa não é uma forma adequada de se escrever um servidor, mas para os nossos propósitos, atende. O servidor somente cria o file watcher e dorme. O resto do trabalho é feito no evento watcher_Created do servidor.

            if (e.Name == "request.xml")
            {
                Log.LogMessage("Received request.xml. Writing response.xml");
                FileUtil.WaitForUnlock(InputDir + "\\request.xml");
                _util.ProcessClientRequest(ConnString, InputDir + "\\tempresponse.xml", _util.ProcessRequest(InputDir + "\\request.xml"));
                Log.LogMessage("Response written");
                if (File.Exists(OutputDir + "\\response.xml"))
                    File.Delete(OutputDir + "\\response.xml");
                File.Copy(InputDir + "\\tempresponse.xml", OutputDir + "\\response.xml");
                Log.LogMessage("Response copied to " + InputDir + "\\response.xml");
            }
            else if (e.Name == "bigrequest.xml")
            {
                Log.LogMessage("Received bigrequest.xml");
                Log.LogMessage("Waiting for file to be unlocked");                
                FileUtil.WaitForUnlock(InputDir + "\\bigrequest.xml");
                Log.LogMessage("bigrequest.xml unlocked.");
                _util.ProcessClientBigRequest(ConnString, InputDir + "\\bigrequest.xml", InputDir + "\\tempresponse.xml");
                Log.LogMessage("Starting copy");
                if (File.Exists(OutputDir + "\\response.xml"))
                    File.Delete(OutputDir + "\\response.xml");
                File.Copy(InputDir + "\\tempresponse.xml", OutputDir + "\\response.xml");
                Log.LogMessage("Finished the copy: source " + InputDir + "\\tempresponse.xml, destination: " + OutputDir + "\\response.xml");
            }

Esse é o código do servidor. Este servidor é usado tanto para o teste utilizando o “request” quanto o “bigrequest”. O nosso caso é o primeiro.

Este trecho executa o WaitForUnlock. Isso é necessário porque o FileWatcher nos avisa que um arquivo pingou no diretório, porém, ele nos avisa no momento em que o arquivo é criado e não quando terminaram de escrever nele (o cliente). Essa sincronização é necessária. O WaitForUnlock fica tentando abrir o arquivo e somente deixa o código do servidor continuar quando o cliente parou e escrever no arquivo.

A partir daí o servidor processa o request, gerando em tempresponse.xml o resultado todo (é uma massa razoável de dados). Em seguida, copia de volta para o diretório Output. Nessa cópia é que ocorrerá o custo mais pesado de rede.

Voltamos para o cliente e o código do seu file watcher que está esperando o response.xml:

        private void watcher_Created(object sender, FileSystemEventArgs e)
        {
            if (e.Name == "response.xml")
            {
                Log.LogMessage("Received " + InputDir + "\\response.xml");
                ((FileSystemWatcher)sender).EnableRaisingEvents = false;

                string file = InputDir + "\\response.xml";

                Log.LogMessage("Waiting for file to be unlocked");
                FileUtil.WaitForUnlock(file);
                Log.LogMessage("File unlocked");                

                util.ImportarArquivo(ConnString, file);

                finished = true;
            }
        }

O cliente faz o mesmo trabalho de sincronização que o servidor, ou seja, espera o arquivo estar acessível. Na seqüência importa o mesmo na base de dados.

File Transfer – Request/Response

Esse teste tem a seguinte idéia:

  • O cliente gera um arquivo bigrequest para o servidor. Este arquivo tem ID por ID o que espera do servidor.
  • O servidor processa o bigrequest.xml e gera um response.xml.
  • O cliente processa o response.xml e joga na usa base de dados

Este teste está implementado na task MSBuild FileTransferBigRequest. O código do cliente começa com:

        public override bool Execute()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();

            FileSystemWatcher watcher = new FileSystemWatcher(OutputDir);
            watcher.Created += new FileSystemEventHandler(watcher_Created);
            watcher.EnableRaisingEvents = true;

            Log.LogMessage("Writing big request with " + BigRequestSize.ToString() + " items");

            FileStream fs = new FileStream(OutputDir + "\\bigrequest.xml", FileMode.Create);

            StreamUtil util = new StreamUtil();
            util.GenerateBigRequest(fs, true, BigRequestSize);

            while (!finished)
                Thread.Sleep(250);

            watch.Stop();

            Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds");

            return true;
        }

O cliente cria o seu file watcher para ouvir a resposta e na seqüência gera o “big request”. O big request é um xml, com todos os ID’s desejados, um a um. É um teste mais próximo da realidade quando falamos em reuso de comportamento.

O servidor é o mesmo do teste anterior, e a lógica é a mesma. Muda somente o método que processa o xml, pegando cada um dos ID’s requisitados e gerando a resposta:

            else if (e.Name == "bigrequest.xml")
            {
                Log.LogMessage("Received bigrequest.xml");
                Log.LogMessage("Waiting for file to be unlocked");                
                FileUtil.WaitForUnlock(InputDir + "\\bigrequest.xml");
                Log.LogMessage("bigrequest.xml unlocked.");
                _util.ProcessClientBigRequest(ConnString, InputDir + "\\bigrequest.xml", InputDir + "\\tempresponse.xml");
                Log.LogMessage("Starting copy");
                if (File.Exists(OutputDir + "\\response.xml"))
                    File.Delete(OutputDir + "\\response.xml");
                File.Copy(InputDir + "\\tempresponse.xml", OutputDir + "\\response.xml");
                Log.LogMessage("Finished the copy: source " + InputDir + "\\tempresponse.xml, destination: " + OutputDir + "\\response.xml");
            }

O cliente novamente é muito parecido com o teste anterior:

        private void watcher_Created(object sender, FileSystemEventArgs e)
        {
            FileUtil util = new FileUtil();

            if (e.Name == "response.xml")
            {
                Log.LogMessage("Received " + InputDir + "\\response.xml");
                ((FileSystemWatcher)sender).EnableRaisingEvents = false;

                string file = InputDir + "\\response.xml";

                Log.LogMessage("Waiting for file to be unlocked");
                FileUtil.WaitForUnlock(file);
                Log.LogMessage("File unlocked");

                util.ImportarArquivo(ConnString, file);

                finished = true;
            }
        }

Conclusão

Observamos que apesar de ter um desempenho bom, essa abordagem gera um problema de sincronização de arquivos. Quando envolvemos somente máquinas, isso não é um processo arriscado e difícil, porém, envolvendo pessoas no processo, podemos ter problemas.

Imagine se no momento em que o arquivo está sendo escrito alguém resolve abrí-lo. O servidor ficará eternamente esperando pelo arquivo. Outros casos como duas rotinas configuradas com o mesmo nome de arquivo também podem acontecer.

Além disso, temos a questão que é difícil chamar o servidor de um “serviço”, porque ele estará sempre acoplado a um cliente. É necessário saber onde devolver o arquivo e o formato precisa ser conhecido por ambos.

Apesar dessa abordagem ser muito utilizada, acredito que deve ser evitada.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s