image05 image06 image07

300x250 AD TOP

adv1

Formulário de contato

Feature Label Area

28 de abril de 2020

Tags: , ,

Primeiros Passos com Google Closure Compiler

Esse post foi originalmente traduzido do meu post no blog da LogRocket: Using Google Closure Compiler to deliver better JavaScript.

Code review é o processo de análise de código por meio de um processo (teoricamente) rigoroso de leitura e revisão crítica de seu conteúdo feita por, no mínimo, duas pessoas. Antes de enviar o código para revisão, os programadores geralmente o "limpam" com uma série de ferramentas automatizadas, dependendo da linguagem e plataforma a serem usados.

No mundo JavaScript, simplesmente por causa da natureza da linguagem, muitos desenvolvedores, especialmente os iniciantes, não conseguem ver as armadilhas e erros que cometem ao programar. Pode ser qualquer coisa, desde o uso de variáveis ​​não declaradas a exceções de ponteiro nulo devido a verificações nulas inconsistentes, até mesmo o uso indevido — ou nenhum uso — do que uma função retorna.

O Google Closure Compiler faz exatamente isso: ele compila código JavaScript para um JavaScript melhorado, analisando, minimizando e reescrevendo-o. E, é claro, também nos alerta sobre as mesmas armadilhas que mencionamos acima. Ele remove o que é desnecessário, verifica a sintaxe, etc.

Neste artigo, apresentaremos alguns problemas comuns que os desenvolvedores de front-end enfrentam, bem como entender melhor como o Closure Compiler pode nos ajudar a verificar rapidamente o código que estamos escrevendo para garantir o melhor código possível.

Começando simples

Você pode executar o Closure Compiler na linha de comando da sua aplicação (por exemplo, em Node.js) ou por meio de um serviço web disponível gratuitamente.



Basicamente, ele expõe uma página web onde você pode compilar seu código por meio de um arquivo JavaScript linkado ou simplesmente colando o código. A ferramenta exibe os resultados no lado direito da tela.

Esses resultados, por sua vez, mostram a diferença de tamanho entre o código-fonte original e a versão compilada (compactada e descompactada) e um link gerado automaticamente para você fazer o download do arquivo JavaScript final.

Mais importante, você verá uma tabela com quatro guias, exibindo:

  • O código compilado final;
  • Uma lista de avisos e erros, indicando quando eles aconteceram, a linha, o tipo de erro/aviso e uma descrição do que estava errado;
  • Os dados do POST que foram usados ​​para fazer o request ao web service REST do Closure Compiler.

Em relação às otimizações, você pode selecionar entre as opções Simple e Advanced (não consideraremos o Whitespace only, pois não faz muita coisa).

O Simple transpilará e minificará seu código JS, além de alertar sobre a sintaxe e os erros mais perigosos (ainda que óbvios) que normalmente cometemos.

O avançado, por outro lado, é muito mais agressivo quando se trata de remover código, reorganizando toda a estrutura da sua implementação original.

Veja a imagem anterior do exemplo padrão “hello, world” na página do web service do Closure: ele reduziu o código e o simplificou, mas perdeu a função hello(), o que significa que referências externas a ele seriam interrompidas. Mas não se preocupe, exploraremos como corrigir isso.


Vamos dar outro exemplo um pouco mais complexo extraído dos tutoriais oficiais do Google:

// Copyright 2009 Google Inc. All Rights Reserved.

/**
 * Creates the DOM structure for the note and adds it to the document.
 */
function makeNoteDom(noteTitle, noteContent, noteContainer) {
  // Create DOM structure to represent the note.
  var headerElement = document.createElement('div');
  var headerText = document.createTextNode(noteTitle);
  headerElement.appendChild(headerText);
  
  var contentElement = document.createElement('div');
  var contentText = document.createTextNode(noteContent);
  contentElement.appendChild(contentText);

  var newNote = document.createElement('div');
  newNote.appendChild(headerElement);
  newNote.appendChild(contentElement);

  // Add the note's DOM structure to the document.
  noteContainer.appendChild(newNote);
}

/**
 * Iterates over a list of note data objects and creates a DOM
 */
function makeNotes(data, noteContainer) {
  for (var i = 0; i < data.length; i++) {
    makeNoteDom(data[i].title, data[i].content, noteContainer);
  }
}

function main() {
  var noteData = [
      {title: 'Note 1', content: 'Content of Note 1'},
      {title: 'Note 2', content: 'Content of Note 2'}];
  var noteListElement = document.getElementById('notes');
  makeNotes(noteData, noteListElement);
}

main();

Aqui, basicamente criamos uma estrutura de dados de notas, cada uma com atributos string de título e conteúdo. O restante é composto por funções utilitárias para iterar a lista de notas e colocá-las todas no documento por meio de cada função de criação. O mesmo código ficará assim depois de compilado pelo Closure Compiler:

for (var a = [{title:"Note 1", content:"Content of Note 1"}, {title:"Note 2", content:"Content of Note 2"}], b = document.getElementById("notes"), c = 0; c < a.length; c++) { var d = a[c].content, e = b, f = document.createElement("div"); f.appendChild(document.createTextNode(a[c].title)); var g = document.createElement("div"); g.appendChild(document.createTextNode(d)); var h = document.createElement("div"); h.appendChild(f); h.appendChild(g); e.appendChild(h); } ;

Observe que toda a lista de variáveis noteData foi alterada para uma declaração de objeto inline, que vem dentro do loop. As variáveis foram renomeadas para caracteres aleatórios do alfabeto. E você não pode reutilizar as funções anteriores em outros lugares; O Closure Compiler provavelmente teria colado a lista duas vezes se estivesse sendo chamado de outro lugar.

No entanto, a legibilidade e a compreensão do código não são boas (o que, é claro, não poderia ser usado em um ambiente de desenvolvimento).

Variáveis não utilizadas

Existem muitos cenários nos quais o Closure Compiler pode atuar — ou seja, problemas comuns ao nosso dia a dia como desenvolvedores JavaScript. Vejamos esse exemplo:

'use strict';
const helperModule = require('./helper.js');
var notUsed;

O que aconteceria com o código de saída gerado quando usamos o modo 'use strict'? Ou uma variável não utilizada, mesmo que você defina um valor mais tarde?

É comum criarmos muitas estruturas (não apenas variáveis, mas constantes, funções, classes, etc.) a serem removidas posteriormente que são facilmente esquecíveis — ainda mais se você estiver lidando com uma enorme quantidade de arquivos de código-fonte. Dependendo da complexidade de seus modelos ou de como você expõe seus objetos ao mundo externo, isso pode levar a situações indesejadas.

Bem, esse é o resultado:

var a = require(“./helper.js”);

As estruturas que não foram utilizadas foram identificadas e removidas automaticamente pelo Closure Compiler. Além disso, variáveis locais (let) e constantes (const) são substituídas por declarações var.

Fluxos condicionais

E quanto a um cenário em que um fluxo depende de outro fluxo condicional? Digamos que você tenha uma função, check(), que depende de outra, getRandomInt(), para gerar um número aleatório entre 0 e 1, e retornará true se for 1.

Com base nesse fluxo, não sabemos o que vai acontecer porque a função é aleatória — ou seja, somente em tempo de execução veremos se o código entra no if ou não:

let abc = 1;
if (check()) {
   abc = "abc";
}
console.info(`abc length: ` + abc.length);
function check() { return getRandomInt(2) == 1; }

function getRandomInt(max) {
   return Math.floor(Math.random() * Math.floor(max));
}

Eis o código compilado:

var b = 1;
1 == Math.floor(2 * Math.random()) && (b = "abc");
console.info("abc length: " + b.length);

O fluxo condicional foi analisado e reprogramado para uma única linha. Observe como o Closure Compiler verifica a primeira condição precedida por um operador &&. Este operador diz que somente se a primeira condição for verdadeira a segunda será executada. Caso contrário, se nosso número aleatório não for igual a 1, b nunca receberá "abc" como valor.

Que tal um if multi-condicional?

if(document == null || document == undefined || document == ‘’)
   console.info('Is not valid');

Eis o resultado:

null != document && void 0 != document && “” != document || console.info(“Is not valid”);

Os ifs condicionais foram aninhados. Esse é o comportamento padrão do Closure Compiler: ele sempre tenta diminuir o máximo possível, mantendo o código pequeno e ainda assim executável.

Referências externas e anotações

Pode ser potencialmente perigoso e improdutivo sempre revisar o código que foi compilado. Além disso, o que acontece se você desejar manter a função check() disponível globalmente para outros arquivos JavaScript? Existem alguns truques aqui, como a sugestão do Google de anexar a função ao objeto global window:

window.check = c;

Teríamos, portanto, o seguinte output:

var a = require("./helper.js"), b = 1;
c() && (b = "abc");
console.info("abc length: " + b.length);
null != document && void 0 != document && "" != document || console.info("Is not valid");
function c() {
return 1 == Math.floor(2 * Math.random());
}
window.check = c;

O sistema de tipos que o Closure Compiler checa é o coração de tudo. Você pode anotar seu código para ser mais "tipificado", o que significa que o Compiler verificará usos incorretos com base em suas anotações.

Vamos dar o exemplo da função getRandomInt(). Você precisa que seu parâmetro seja um número em todos os casos; logo, pode solicitar ao Closure Compiler para verificar se um caller está passando algo diferente:

function getRandomInt(/** number */ max) {
return Math.floor(Math.random() * Math.floor(max));
}
window['getRandomInt'] = getRandomInt;
getRandomInt('a');

Isso retornaria:


Mesmo que o arquivo seja sempre compilado para "warnings", você pode ter uma ideia do que está acontecendo com seu código, principalmente para códigos atualizados por muitas pessoas.

Outro recurso interessante são as definições de exportação. Se você decidir que não deseja que algo seja renomeado por meio da opção Advanced, também pode anotar seu código com:

/** @export */

Dessa forma, o código de saída não sofrerá mudanças.

Conclusão

Existem muitos cenários diferentes que você pode usar para testar o poder dessa ferramenta. Vá em frente, pegue seus próprios códigos JavaScript e experimente-os com o Closure Compiler. Faça anotações sobre o que ele gera e as diferenças entre cada opção de otimização. Você também pode pegar qualquer arquivo JavaScript externo e importá-lo no web service para fins de teste.

Lembre-se, também é possível executá-lo a partir da linha de comando ou mesmo do seu sistema de build, como Gulp ou Webpack. O Google também fornece uma pasta com exemplos no repositório oficial do GitHub, onde você pode testar mais recursos. Ótimos estudos!

15 de janeiro de 2016

Tags: , , ,

Como configurar a flag allow-file-access-from-files no Google Chrome

Se você já tentou desenvolver ou testar alguma aplicação no Google Chrome usando apenas páginas HTML estáticas sem hospedá-las em um servidor local qualquer, provavelmente já passou pelo impasse de não conseguir que seus códigos JavaScript, jQuery ou Ajax funcionem.


Vários tipos de implementação enfrentam esse tipo de problema: tentar enviar um request via API Ajax do jQuery, acessar uma página dentro de um iframe/embed, dentre outras.

O que acontece é que o Chrome não carrega arquivos localmente por padrão em função de restrições de segurança internas. Essa é uma discussão de muito tempo, mas já temos soluções para solucioná-la.

Através da opção de flag --allow-file-access-from-files você consegue forçar o browser a reconhecer esse recurso como antes. Vejamos os passos necessários:
  1. Acesse o prompt cmd e acesse o diretório de instalação do seu Chrome (atente que para cada tipo de instalação - usuário ou global - temos um diretório diferente). Ex: cd C:\Program Files (x86)\Google\Chrome\Application
  2. Execute o chrome.exe a partir da linha de comando com o parâmetro adicionar "--allow-file-access-from-files": chrome.exe --allow-file-access-from-files (tal como na Figura 1).
  3. Para evitar ter de refazer estes passos toda vez, basta criar uma cópia do arquivo e alterar suas propriedades, tal como vemos na Figura 2.
Figura 1. Comandos para abrir Chrome com nova flag

Figura 2. Criando atalho com a configuração.

10 de abril de 2015

Tags: , , , , , , ,

Integrando Android e PayPal com Java e MySQL - Parte 2

Na primeira parte deste artigo, nós cobrimos toda a configuração inicial do ambiente, envolvendo principalmente o projeto Java Web com Restful, as bibliotecas e ferramentas envolvidas, bem como suas respectivas versões.


Criamos também toda a comunicação com o banco de dados, o modelo Entidade-Relacionamento, o SQL gerado e as configurações da API do PayPal SDK para Java.

Nessa parte, focaremos em finalizar a aplicação de modelo, principalmente no lado Android, testando tudo de forma local.

Você poderá efetuar o download do código fonte diretamente do meu GitHub, no botão abaixo:

DOWNLOAD CÓDIGO
Na Figura 1 abaixo você pode visualizar como ficarão nossas telas ao final da implementação:

Configurando o projeto Android

Antes de criar o projeto, precisamos efetuar o download do PayPal Android SDK. Extraia os arquivos em uma pasta de preferência.

Mais uma vez, você pode se sentir à vontade para usar o Eclipse Android Bundle ou o AndroidStudio para programar a parte Android, desde que tenha boa experiência em ambas as ferramentas.

Crie um novo projeto Android no Eclipse através das opções File > New > Android Application Project e preencha todas as informações, tais como pacote e nome do projeto.

Copie todo o conteúdo da pasta libs do PayPal Android SDK que você baixou anteriormente e cole na pasta libs do seu projeto recém criado, que deverá ser semelhante à estrutura mostrada na Figura 1.


Figura 1. Criando App PayPal

Abra agora o arquivo colors.xml do seu projeto (ou crie um se não tiver) e adicione o conteúdo da listagem abaixo:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="categoria">#666666</color>
    <color name="valor">#888888</color>
    <color name="list_divisor">#d9d9d9</color>
    <color name="linha_cor_inicio">#ffffff</color>
    <color name="linha_cor_fim">#ffffff</color>
    <color name="linha_cor_inicio_hover">#ebeef0</color>
    <color name="linha_cor_fim_hover">#ebeef0</color>
</resources>

Essas cores serão usadas posteriormente para outras finalidades. Atualize o seu arquivo de strings.xml com algumas chaves que iremos usar nos layouts:

<string name="paypal_client">Cliente PayPal</string>
<string name="checkout">Checkout</string>
<string name="add_carrinho">Add ao Carrinho</string>

Note que na Figura 1 nós estamos fazendo uso de algumas outras bibliotecas como a do Volley, para fazer chamadas a serviços na rede. Baixe a biblioteca e add ao seu classpath.

Crie a classe BitMapCache que será responsável por cachear as requisições via Volley no que se refere às imagens da nossa listagem:

package br.edu.ecommerce.custom.util;

import com.android.volley.toolbox.ImageLoader.ImageCache;

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

/**
 * Classe responsável por manter o cache das informações de imagens em disco.
 */
public class BitMapCache extends LruCache<String, Bitmap> implements ImageCache {

 public BitMapCache() {
  this(getDefaultCacheSize());
 }

 public BitMapCache(int maxSize) {
  super(maxSize);
 }

 public static int getDefaultCacheSize() {
  final int memoriaMax = (int) (Runtime.getRuntime().maxMemory() / 1024);
  final int tamanhoCache = memoriaMax / 8;

  return tamanhoCache;
 }

 @Override
 protected int sizeOf(String key, Bitmap value) {
  return value.getRowBytes() * value.getHeight() / 1024;
 }

 @Override
 public Bitmap getBitmap(String url) {
  return get(url);
 }

 @Override
 public void putBitmap(String url, Bitmap valor) {
  put(url, valor);
 }

}

Após isso, criaremos também uma classe chamada AppController que será responsável por estender de Application e gerenciar os objetos volley há cada requisição:

package br.edu.ecommerce.custom.app;

import br.edu.ecommerce.custom.util.BitMapCache;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;

import android.app.Application;
import android.text.TextUtils;

public class AppController extends Application {

 private static final String TAG = AppController.class.getSimpleName();

 private static AppController appController;

 private RequestQueue requestQueue;
 private ImageLoader imageLoader;

 @Override
 public void onCreate() {
  super.onCreate();
  appController = this;
 }

 public static synchronized AppController getInstance() {
  return appController;
 }

 public RequestQueue getRequestQueue() {
  if (requestQueue == null) {
   requestQueue = Volley.newRequestQueue(getApplicationContext());
  }
  return requestQueue;
 }

 public ImageLoader getImageLoader() {
  getRequestQueue();
  if (imageLoader == null) {
   imageLoader = new ImageLoader(requestQueue, new BitMapCache());
  }
  return imageLoader;
 }

 public <T> void addToRequestQueue(Request<T> request, String tag) {
  request.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
  getRequestQueue().add(request);
 }

 public <T> void addToRequestQueue(Request<T> request) {
  request.setTag(TAG);
  getRequestQueue().add(request);
 }

 public void cancelarRequestPending(Object tag) {
  if (requestQueue != null) {
   requestQueue.cancelAll(tag);
  }
 }

}

Essas são classes utilitárias, logo você não precisa se preocupar em modificá-las no futuro, mas sim reusá-las quando implementações semelhantes surgirem.

Vamos editar o AndroidManifest.xml agora, com algumas configurações de permissão à APi do PayPal.

<!-- para o card.io e card scanning -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.VIBRATE" />

    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera.autofocus"
        android:required="false" />

    <!-- para card.io e paypal -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Dentro da tag application -->
    <service
            android:name="com.paypal.android.sdk.payments.PayPalService"
            android:exported="false" />

    <activity android:name="br.edu.ecommerce.ListaProdutosActivity" />
        <activity android:name="com.paypal.android.sdk.payments.PaymentActivity" />
    <activity android:name="com.paypal.android.sdk.payments.LoginActivity" />
    <activity android:name="com.paypal.android.sdk.payments.PaymentMethodActivity" />
    <activity android:name="com.paypal.android.sdk.payments.PaymentConfirmActivity" />
    <activity
         android:name="io.card.payment.CardIOActivity"
         android:configChanges="keyboardHidden|orientation" />
    <activity android:name="io.card.payment.DataEntryActivity" />

Após isso, nós também precisaremos salvar as informações relativas ao PayPal Client ID e Secret no lado Android. Crie a classe Config abaixo que conterá algumas outras constantes importantes:

package br.edu.ecommerce.paypal;

import com.paypal.android.sdk.payments.PayPalConfiguration;

public class Config {

 // PayPal app configuration
 public static final String PAYPAL_CLIENT_ID = "AbkVNdXzruEZ6FEGaJB6TBfB_-4qmlqLPAQj5qin4FI8cS3v0Vs2pjldttP4MgmF487ZyVZ2y34j4xPE";
 public static final String PAYPAL_CLIENT_SECRET = "EDYpA14ZmKc8pXqFe9CHWYJkkqu-6z1NxduRpeIHsr_O37QIgZK7vYd3R6ElsKpsPbQ55VbVo8ps3KuE";

 public static final String PAYPAL_ENVIRONMENT = PayPalConfiguration.ENVIRONMENT_SANDBOX;
 public static final String PAYMENT_INTENT = PayPalPayment.PAYMENT_INTENT_SALE;
 public static final String DEFAULT_CURRENCY = "BRL";

 // PayPal server urls
 public static final String URL_PRODUTOS = "http://192.168.0.103:8080/eCommerce-web/produto/produtos";
 public static final String URL_VER_PAGTOS = "http://192.168.0.103:8080/eCommerce-web/produto/checkPagto";

}

Veja que estamos salvando os mesmos dados que fizemos no lado web, dentro do arquivo de propriedades. Não esqueça de modificar o IP das duas URLs para o da sua máquina, bem como o client id e secret. E nada de tentar usar localhost, não vai funcionar.

Para esse exemplo funcionar, você precisará também duplicar as mesmas classes de entidades que criamos no projeto Java. Não mostraremos esse passo aqui, mas você poderá copiar as classes direto do arquivo de download do código fonte.

Vamos criar agora o arquivo xml de layout da lista de produtos. Para isso, crie o arquivo abaixo dentro da pasta res/layout com o nome de item_lista.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/linha_lista_seletor"
    android:padding="8dp" >

    <!-- Componente de imagem do produto -->

    <com.android.volley.toolbox.NetworkImageView
        android:id="@+id/imgProduto"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_alignParentLeft="true"
        android:layout_marginRight="8dp" >
    </com.android.volley.toolbox.NetworkImageView>

    <!-- Título do Produto -->

    <TextView
        android:id="@+id/txtTitulo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/imgProduto"
        android:layout_toRightOf="@id/imgProduto"
        android:textSize="@dimen/titulo"
        android:textStyle="bold" />

    <!-- Qtde de produtos vendidos -->

    <TextView
        android:id="@+id/txtQtde"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtTitulo"
        android:layout_marginTop="1dip"
        android:layout_toRightOf="@id/imgProduto"
        android:textSize="@dimen/qtde" />

    <!-- Categorias do produto -->

    <TextView
        android:id="@+id/txtCategoria"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtQtde"
        android:layout_marginTop="5dp"
        android:layout_toRightOf="@id/imgProduto"
        android:textColor="@color/categoria"
        android:textSize="@dimen/categoria" />

    <!-- Valor do produto -->

    <TextView
        android:id="@+id/txtValor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:textColor="@color/valor"
        android:textSize="@dimen/valor" />

    <Button
        android:id="@+id/btnAddToCart"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_below="@id/txtCategoria"
        android:layout_margin="5dp"
        android:layout_toRightOf="@id/imgProduto"
        android:background="#64d048"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:text="@string/add_carrinho"
        android:textColor="#ffffff" />

</RelativeLayout>

Como estamos lidando com uma listview customizada, precisamos criar uma classe adapter da mesma. Cria a classe CustomListAdapter abaixo e adicione o conteúdo:

package br.edu.ecommerce.custom.adapter;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;
import br.edu.ecommerce.loja_virtual.R;
import br.edu.ecommerce.custom.app.AppController;
import br.edu.ecommerce.custom.model.Produto;

import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;

public class CustomListAdapter extends BaseAdapter {

 private Activity activity;
 private List produtos = new ArrayList();

 private LayoutInflater inflater;

 ImageLoader imageLoader = AppController.getInstance().getImageLoader();
 
 private ProductListAdapterListener listener;

 public CustomListAdapter(Activity activity, List produtos, ProductListAdapterListener listener) {
  this.activity = activity;
  this.produtos = produtos;
  this.listener = listener;
 }

 @Override
 public int getCount() {
  return produtos.size();
 }

 @Override
 public Object getItem(int position) {
  return produtos.get(position);
 }

 @Override
 public long getItemId(int position) {
  return position;
 }

 @SuppressLint("InflateParams")
 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  if (inflater == null) {
   inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  }

  if (convertView == null) {
   convertView = inflater.inflate(R.layout.linha_lista, null);
  }

  if (imageLoader == null) {
   imageLoader = AppController.getInstance().getImageLoader();
  }

  NetworkImageView imageProd = (NetworkImageView) convertView.findViewById(R.id.imgProduto);
  TextView txtTitulo = (TextView) convertView.findViewById(R.id.txtTitulo);
  TextView txtQtde = (TextView) convertView.findViewById(R.id.txtQtde);
  TextView txtCategoria = (TextView) convertView.findViewById(R.id.txtCategoria);
  TextView txtValor = (TextView) convertView.findViewById(R.id.txtValor);

  // Recuperando o produto atual da iteração
  final Produto prod = produtos.get(position);

  imageProd.setImageUrl(prod.getUrlImg(), imageLoader);

  txtTitulo.setText(prod.getTitulo());
  txtQtde.setText("Qtde Vendidos: " + String.valueOf(prod.getQtde()));
  NumberFormat format = NumberFormat.getCurrencyInstance(new Locale("pt"));
  txtValor.setText("R$ " + String.valueOf(format.format(prod.getValor())));

  String concat = "";
  for (String cat : prod.getCategorias()) {
   concat += cat + ", ";
  }
  concat = concat.length() > 0 ? concat.substring(0, concat.length() - 2) : concat;

  txtCategoria.setText(concat);

  Button btnAddToCart = (Button) convertView.findViewById(R.id.btnAddToCart);

  btnAddToCart.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    listener.onAddCarrrinhoPressed(prod);
   }
  });

  return convertView;
 }

 public interface ProductListAdapterListener {
  public void onAddCarrrinhoPressed(Produto produto);
 }

}

Após isso, nós também precisamos criar o xml que iterará todos os itens customizados dos produtos. Para isso, crie o arquivo listagem_produtos.xml também dentro da pasta res/layout com o seguinite conteúdo:



    

    

Estamos quase lá. Agora precisamos criar a Activity que conterá os métodos de fachada para lidar com a comunicação com a API do PayPal. Veja o código abaixo:

package br.edu.ecommerce;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.Toast;
import br.edu.devmedia.loja_virtual.R;
import br.edu.ecommerce.custom.UTF8ParseJson;
import br.edu.ecommerce.custom.adapter.CustomListAdapter;
import br.edu.ecommerce.custom.adapter.CustomListAdapter.ProductListAdapterListener;
import br.edu.ecommerce.custom.app.AppController;
import br.edu.ecommerce.custom.model.Produto;
import br.edu.ecommerce.paypal.Config;

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.Request.Method;
import com.android.volley.Response;
import com.android.volley.Response.ErrorListener;
import com.android.volley.Response.Listener;
import com.android.volley.RetryPolicy;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.StringRequest;
import com.google.gson.Gson;
import com.paypal.android.sdk.payments.PayPalConfiguration;
import com.paypal.android.sdk.payments.PayPalItem;
import com.paypal.android.sdk.payments.PayPalPayment;
import com.paypal.android.sdk.payments.PayPalPaymentDetails;
import com.paypal.android.sdk.payments.PayPalService;
import com.paypal.android.sdk.payments.PaymentActivity;
import com.paypal.android.sdk.payments.PaymentConfirmation;

public class ListaProdutosActivity extends Activity implements SearchView.OnQueryTextListener, ProductListAdapterListener {

 private static final String TAG = ListaProdutosActivity.class.getSimpleName();

 private ListView lstProd;
 private List<Produto> produtos = new ArrayList<Produto>();
 private CustomListAdapter adapter;

 private ProgressDialog progressDialog;

 // To store the products those are added to cart
 private List<PayPalItem> produtosCarrinho = new ArrayList<PayPalItem>();

 private Button btnCheckout;

 private static final int CODIGO_PAGTO = 1;

 // PayPal configuration
 private static PayPalConfiguration paypalConfig = 
   new PayPalConfiguration().environment(Config.PAYPAL_ENVIRONMENT).clientId(Config.PAYPAL_CLIENT_ID).languageOrLocale("pt_BR");

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.listagem_produtos);

  lstProd = (ListView) findViewById(R.id.listaProd);
  btnCheckout = (Button) findViewById(R.id.btnCheckout);

  adapter = new CustomListAdapter(this, produtos, this);
  lstProd.setAdapter(adapter);

  progressDialog = new ProgressDialog(this);
  progressDialog.setCancelable(false);
  progressDialog.setMessage("Carregando...");
  progressDialog.show();

  getActionBar().setBackgroundDrawable(new ColorDrawable(Color.parseColor("#1b1b1b")));

  // Starting PayPal service
  Intent intent = new Intent(this, PayPalService.class);
  intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, paypalConfig);
  startService(intent);

  // Checkout button click listener
  btnCheckout.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    // Check for empty cart
    if (produtosCarrinho.size() > 0) {
     executarPagtoPayPal();
    } else {
     Toast.makeText(getApplicationContext(), "Carrinho vazio! Favor adicionar produtos!", Toast.LENGTH_SHORT).show();
    }

   }
  });

  // Fetching products from server
  executarRequestProdutos(null);
 }

 private void executarRequestProdutos(String param) {
  produtos.clear();
  if (param != null) {
   try {
    param = URLEncoder.encode(param, "UTF-8");
   } catch (UnsupportedEncodingException e) {
    e.printStackTrace();
   }
  }
  JsonArrayRequest requestProds = new UTF8ParseJson(param != null ? Config.URL_PRODUTOS + "?str=" + param : Config.URL_PRODUTOS, new Listener<JSONArray>() {
   @Override
   public void onResponse(JSONArray jsonArray) {
    esconderDialog();

    for (int i = 0; i < jsonArray.length(); i++) {
     try {
      JSONObject jsonObj = jsonArray.getJSONObject(i);

      Gson gson = new Gson();
      Produto produto = gson.fromJson(jsonObj.toString(), Produto.class);

      produtos.add(produto);
     } catch (JSONException e) {
      e.printStackTrace();
     }
    }
    adapter.notifyDataSetChanged();
   }
  }, 
  new ErrorListener() {
   @Override
   public void onErrorResponse(VolleyError error) {
    VolleyLog.d(TAG, "Erro: " + error.getMessage());
    esconderDialog();
   }
  });
  AppController.getInstance().addToRequestQueue(requestProds);
 }

 private void esconderDialog() {
  if (progressDialog != null) {
   progressDialog.dismiss();
   progressDialog = null;
  }
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  esconderDialog();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.produtos, menu);

  return true;
 }

 @Override
 public boolean onQueryTextSubmit(String query) {
  return false;
 }

 @Override
 public boolean onQueryTextChange(String newText) {
  if (newText != null) {
   executarRequestProdutos(newText);
  }
  return false;
 }

 /**
  * Verifying the mobile payment on the server to avoid fraudulent payment
  * */
 private void verificarPagtoServidor(final String idPagto, final String jsonClientePagto) {
  // Showing progress dialog before making request
  progressDialog = new ProgressDialog(this);
  progressDialog.setMessage("Verificando pagamento...");
  progressDialog.show();

  StringRequest verifyReq = new StringRequest(Method.POST, Config.URL_VER_PAGTOS, new Response.Listener<String>() {
   @Override
   public void onResponse(String response) {
    Log.d(TAG, "verify payment: " + response.toString());

    try {
     JSONObject res = new JSONObject(response);
     boolean erro = res.getBoolean("erro");
     String msg = res.getString("msg");

     // user error boolean flag to check for errors
     Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();

     if (!erro) {
      // empty the cart
      produtosCarrinho.clear();
     }

    } catch (JSONException e) {
     e.printStackTrace();
    }

    // hiding the progress dialog
    esconderDialog();
   }
  }, new Response.ErrorListener() {
   @Override
   public void onErrorResponse(VolleyError error) {
    Log.e(TAG, "Erro de verificação: " + error.getMessage());
    Toast.makeText(getApplicationContext(), error.getMessage(), Toast.LENGTH_SHORT).show();
    // hiding the progress dialog
    esconderDialog();
   }
  }) {
   @Override
   protected Map<String, String> getParams() {
    SharedPreferences preferences = getPreferences(MODE_PRIVATE);
    
    Integer idUsuario = preferences.getInt("usuario", 0);
    
    Map<String, String> params = new HashMap<String, String>();
    params.put("idPagto", idPagto);
    params.put("idUsuario", idUsuario != 0 ? String.valueOf(idUsuario) : "1");
    params.put("jsonClientePagto", jsonClientePagto);

    return params;
   }
  };

  // Setting timeout to volley request as verification request takes sometime
  int socketTimeout = 60000;
  RetryPolicy policy = new DefaultRetryPolicy(socketTimeout,
    DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
    DefaultRetryPolicy.DEFAULT_BACKOFF_MULT);
  verifyReq.setRetryPolicy(policy);

  // Adding request to request queue
  AppController.getInstance().addToRequestQueue(verifyReq);
 }

 /**
  * Preparing final cart amount that needs to be sent to PayPal for payment
  * */
 private PayPalPayment prepararCarrinhoFinal() {
  PayPalItem[] itens = new PayPalItem[produtosCarrinho.size()];
  itens = produtosCarrinho.toArray(itens);

  // Total amount
  BigDecimal subtotal = PayPalItem.getItemTotal(itens);

  // If you have frete cost, add it here
  BigDecimal frete = new BigDecimal("0.0");

  // If you have tax, add it here
  BigDecimal taxa = new BigDecimal("0.0");

  PayPalPaymentDetails detalhesPagto = new PayPalPaymentDetails(frete, subtotal, taxa);

  BigDecimal quantia = subtotal.add(frete).add(taxa);

  PayPalPayment pagto = new PayPalPayment(
    quantia,
    Config.DEFAULT_CURRENCY,
    "Transação de compra na Loja DevMedia sendo processada...",
    Config.PAYMENT_INTENT);

  pagto.items(itens).paymentDetails(detalhesPagto);

  // Custom field like invoice_number etc.,
  pagto.custom("Texto a ser associado com o pagto que a app vai usar.");

  return pagto;
 }

 /**
  * Launching PalPay payment activity to complete the payment
  * */
 private void executarPagtoPayPal() {
  PayPalPayment coisasPraComprar = prepararCarrinhoFinal();

  Intent intent = new Intent(ListaProdutosActivity.this, PaymentActivity.class);

  intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, paypalConfig);

  intent.putExtra(PaymentActivity.EXTRA_PAYMENT, coisasPraComprar);

  startActivityForResult(intent, CODIGO_PAGTO);
 }

 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == CODIGO_PAGTO) {
   if (resultCode == Activity.RESULT_OK) {
    PaymentConfirmation confirm = data.getParcelableExtra(PaymentActivity.EXTRA_RESULT_CONFIRMATION);
    if (confirm != null) {
     try {
      Log.e(TAG, confirm.toJSONObject().toString(4));
      Log.e(TAG, confirm.getPayment().toJSONObject().toString(4));

      String pagtoId = confirm.toJSONObject().getJSONObject("response").getString("id");

      String jsonClientePagto = confirm.getPayment().toJSONObject().toString();

      Log.e(TAG, "pagtoId: " + pagtoId + ", jsonClientePagto: " + jsonClientePagto);

      // Now verify the payment on the server side
      verificarPagtoServidor(pagtoId, jsonClientePagto);
     } catch (JSONException e) {
      Log.e(TAG, "Um erro ocorreu: ", e);
     }
    }
   } else if (resultCode == Activity.RESULT_CANCELED) {
    Log.e(TAG, "O usuário cancelou a operação.");
   } else if (resultCode == PaymentActivity.RESULT_EXTRAS_INVALID) {
    Log.e(TAG, "Um pagamento inválido foi submetido.");
   }
  }
 }

 @Override
 public void onAddCarrrinhoPressed(Produto produto) {
  PayPalItem item = new PayPalItem(produto.getTitulo(), 1,
    new BigDecimal(produto.getValor()).setScale(2, RoundingMode.CEILING), Config.DEFAULT_CURRENCY, produto.getSku());

  produtosCarrinho.add(item);

  Toast.makeText(getApplicationContext(), item.getName() + " adicionado ao carrinho!", Toast.LENGTH_SHORT).show();
 }
}

Agora é só executar ambos os aplicativos (Restful e Android) e testar o projeto. O resultado deverá ser semelhante à Figura 2.


Figura 2. Criando App PayPal

O fluxo de testes é simples, e pode ser bem explicado pelas imagens acima.

Então, é isso pessoal. Se tiverem alguma dúvida ou problemas com a implementação é só comentar que venho ajudar vocês! Bons estudos! :)

-->

2 de abril de 2015

Tags: , , , , , , ,

Integrando Android e PayPal com Java e MySQL - Parte 1

Uma das funcionalidades mais importantes da maioria dos apps mobile de hoje em dia é a possibilidade de se integrar com plataformas de pagamento online, tais como PagSeguro, MercadoPago ou a mais famosa de todas (a nível internacional): PayPal.


Na maioria dos apps de eCommerce, que se caracterizam principalmente por compras e vendas online, não basta somente ter uma boa interface e lógica de negócio implementadas, é também preciso gerenciar tudo isso de forma segura, e para isso precisamos fazer uso de Web Services, bancos de dados, aplicações e outros tipos de recursos e operações no lado do servidor.

Com o objetivo de cobrir uma implementação pouco vista em português, esse artigo, dividido em duas partes, visa ensinar como construir uma aplicação básica em Android, usando a biblioteca SDK do PayPal, uma integração server side com um projeto em Java Web, que fará uso de requisições HTTP via Web Services Restful (implementação Jersey) e salvará os dados em um schema MySQL.

Você poderá efetuar o download do código fonte diretamente do meu GitHub, no botão abaixo:

DOWNLOAD CÓDIGO
Na Figura 1 abaixo você pode visualizar como ficarão nossas telas ao final da implementação:


Figura 1. Telas finais do aplicativo

Resumo

O PayPal disponibiliza três formas distintas para efetuar pagamentos via sua API, a saber:
No nosso caso, usaremos a primeira opção dada a sua facilidade de uso e adequação ao nosso caso de uso.

Veja no vídeo abaixo como se dará o fluxo de execução da nossa implementação:


Criando App no PayPal

Para fazer uso dos serviços do PayPal mostrados no vídeo, nós precisaremos antes criar uma conta no site developer.paypal.com, em alusão ao que já ocorre com o padrão oAuth em empresas como Google, Facebook, etc.

Acesse a página, logue ou crie uma nova conta se não for registrado ainda, e em seguida acesse a página de Apps.

Após isso, crie uma nova aplicação, dê um nome, selecione a sua conta de desenvolvedor e finaliza, tal como exibido na Figura 2.


Figura 2. Criando App PayPal

Uma vez criada, a app será redirecionada para a página com os dados da mesma, dentre os quais se destacam as informações de Client ID (Identificador do cliente) e Secret (Senha) que deverão ser guardadas para uso nas chamadas futuras que faremos com a API (Figura 3).


Figura 3. Dados de Client ID e Secret da APP PayPal

Conta de Testes: PayPal Sandbox

O PayPal fornece um ambiente de teste integrado chamado sandbox. Ele será muito útil pois vem com contas configuradas automaticamente com quantias em dinheiro para teste, o que nos poupa o trabalho de usar algo real.

Para fazer uso real desse ambiente, basta ir na opção "Sandbox > Accounts" na mesma página exibida na Figura 2 e selecionar o email com o sufixo "-buyer" (que corresponde ao email da conta de teste). Clique no link "Profile" e modifique a senha na aba "Profile" (já selecionada). A Figura 4 mostra a opção na mesma aba e uma representação da aba Funding, onde podemos ver o saldo disponível para testes na conta em questão.


Figura 4. Detalhes da conta de teste

Downloads e Configurações

Como estamos usando tecnologias distintas (mobile, server e bd), é interessante que você selecione as IDEs, banco, SGBD e emuladores que se sinta mais confortável. Neste artigo, estarei usando as seguintes ferramentas:

Mais uma vez, a escolha de outras ferramentas que você tenha mais familiaridade não implica em consequência alguma para o desenvolver deste post, como o AndroidStudio, SOAP, etc.

Já em relação às tecnologias usadas, temos a seguinte relação:
Para implementar o nosso projeto, precisaremos ainda efetuar o download de dois pacotes do PayPal:
  • A API do PayPal para Java, que fornece os mesmos recursos do SDK para fazer a comunicação com os serviços PayPal. Essa API também pode ser usada normalmente em um projeto Java Web qualquer que precise dos mesmos serviços.
  • E o SDK do PayPal para Android.
Se estiver usando o Maven como gerenciador de bibliotecas no seu projeto, poderá referenciar os artefatos no pom.xml através das informações fornecidas nos links oficiais dos referidos projetos no Github. No nosso exemplo, utilizaremos as bibliotecas e suas dependências manualmente, efetuando o download das respectivas libs. Para isso, acesse os dois links abaixo e clique na opção "Download (JAR)":

Criando o Banco de Dados

O nosso modelo de base de dados (Figura 5) será composto por seis tabelas distintas. A tabela tb_usuario salvará os dados básicos do usuário, como login, senha, etc. tb_pagamento se encarregará de guardar um histórico com todos os dados de quaisquer pagamentos que tenham sido feitos, com ou sem sucesso. tb_venda guardará as vendas finalizadas e aprovadas e se comunicará com tb_produto que terá a listagem de todos os produtos disponíveis. Cada produto tem uma ou mais categorias e vice versa, por isso temos uma tabela intermediária para representar o relacionamento muitos-pra-muitos.


Figura 5. Modelo de entidades do BD

O SQL de geração do modelo apresentado pode ser encontrado na listagem abaixo. Rode o mesmo script no seu banco MySQL.

CREATE SCHEMA IF NOT EXISTS `ecommerce-teste` DEFAULT CHARACTER SET utf8 ;
USE `ecommerce-teste` ;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_categoria`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_categoria` (
  `id_categoria` INT(11) NOT NULL AUTO_INCREMENT,
  `descricao` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id_categoria`))
ENGINE = InnoDB
AUTO_INCREMENT = 5
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_produto`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_produto` (
  `id_produto` INT(11) NOT NULL AUTO_INCREMENT,
  `titulo` VARCHAR(45) NOT NULL,
  `qtde` INT(11) NOT NULL,
  `valor` DOUBLE NOT NULL,
  `img` VARCHAR(1500) NOT NULL,
  `sku` TEXT NOT NULL,
  PRIMARY KEY (`id_produto`))
ENGINE = InnoDB
AUTO_INCREMENT = 5
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_produto_categoria`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_produto_categoria` (
  `id_produto` INT(11) NOT NULL,
  `id_categoria` INT(11) NOT NULL,
  PRIMARY KEY (`id_produto`, `id_categoria`),
  INDEX `fk_tb_produto_has_tb_categoria_tb_categoria1_idx` (`id_categoria` ASC),
  INDEX `fk_tb_produto_has_tb_categoria_tb_produto_idx` (`id_produto` ASC),
  CONSTRAINT `fk_tb_produto_has_tb_categoria_tb_categoria1`
    FOREIGN KEY (`id_categoria`)
    REFERENCES `ecommerce-teste`.`tb_categoria` (`id_categoria`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_tb_produto_has_tb_categoria_tb_produto`
    FOREIGN KEY (`id_produto`)
    REFERENCES `ecommerce-teste`.`tb_produto` (`id_produto`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_usuario`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_usuario` (
  `id_usuario` INT(11) NOT NULL AUTO_INCREMENT,
  `login` VARCHAR(45) NOT NULL,
  `senha` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id_usuario`))
ENGINE = InnoDB
AUTO_INCREMENT = 6
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_pagamento`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_pagamento` (
  `id_pagamento` INT NOT NULL AUTO_INCREMENT,
  `pagtoPaypalId` TEXT NOT NULL,
  `estado` VARCHAR(15) NOT NULL,
  `valor` DECIMAL(6,2) NOT NULL,
  `moeda` VARCHAR(3) NOT NULL,
  `idUsuario` INT(11) NOT NULL,
  PRIMARY KEY (`id_pagamento`),
  INDEX `fk_tb_pagamento_tb_usuario1_idx` (`idUsuario` ASC),
  CONSTRAINT `fk_tb_pagamento_tb_usuario1`
    FOREIGN KEY (`idUsuario`)
    REFERENCES `ecommerce-teste`.`tb_usuario` (`id_usuario`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

-- -----------------------------------------------------
-- Table `ecommerce-teste`.`tb_venda`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `ecommerce-teste`.`tb_venda` (
  `id_venda` INT NOT NULL AUTO_INCREMENT,
  `estado` VARCHAR(45) NOT NULL,
  `preco` DECIMAL(6,2) NOT NULL,
  `qtde` INT(4) NOT NULL,
  `produtoId` INT(11) NOT NULL,
  `pagtoId` INT NOT NULL,
  PRIMARY KEY (`id_venda`),
  INDEX `fk_tb_venda_tb_produto1_idx` (`produtoId` ASC),
  INDEX `fk_tb_venda_tb_pagamento1_idx` (`pagtoId` ASC),
  CONSTRAINT `fk_tb_venda_tb_produto1`
    FOREIGN KEY (`produtoId`)
    REFERENCES `ecommerce-teste`.`tb_produto` (`id_produto`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_tb_venda_tb_pagamento1`
    FOREIGN KEY (`pagtoId`)
    REFERENCES `ecommerce-teste`.`tb_pagamento` (`id_pagamento`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;
As tabelas de produtos e usuários são as únicas que você precisará popular diretamente na base. Você pode preencher com seus próprios dados, ou pode utilizar a referência abaixo:


O código sku é um código usado pelo PayPal para gerenciar os produtos. Você pode criar os seus próprios, porém no padrão mostrado.

Criando o Projeto Java Web

No Eclipse, com o Tomcat já configurado e funcional, selecione a perspectiva Java EE no canto superior direito, e vá até o menu "File > New > Dynamic Web Project". Clique em "Next" até a tela de "Web Module" e marque a opção de geração do web.xml descrita na Figura 6, e clique em finalizar.


Figura 6. Opção de geração do arquivo web.xml no projeto.

Após isso, adicione as libs baixadas do Jersey, Gson e MySQL Connector J na pasta WEB-INF/lib do projeto igual às da Figura 7 na pasta src.


Figura 7. Estrutura de diretórios do projeto.

Após isso, a primeira configuração importante é a do Servlet do Jersey no web.xml. Adicione o conteúdo da listagem abaixo a esse arquivo, de modo a termos os Web Services rest prontos para serem usados:
<servlet>
    <servlet-name>Jersey Rest</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>jersey.config.server.provider.packages</param-name>
      <param-value>br.edu.ecommerce.rest</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey Rest</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
Após isso, precisamos criar uma classe de comunicação direta com o banco MySQL, para fornecer objetos Connection há cada nova ação de persistência. Para isso, crie a classe da listagem abaixo dentro do pacote br.edu.ecommerce.db:
package br.edu.ecommerce.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtil {

 public static Connection getConnexao() throws ClassNotFoundException, SQLException {
  Class.forName("com.mysql.jdbc.Driver");
  return DriverManager.getConnection("jdbc:mysql://localhost:3306/ecommerce-teste", "root", "root");
 }
 
}
Modifique os valores de usuário e senha de acordo com os configurados para o seu banco em específico.

Em seguida precisamos criar as entidades que conterão os dados como POJOS Java comuns para serem usados depois. Veja na listagem abaixo toda as quatro entidades que devem ser criados dentro do pacote br.edu.ecommerce.entidades:
public class Usuario {

 private int id;

 private String login;

 private String senha;
 
 private boolean logado;
 
 // get's e set's
 
}

public class Pagamento {

 private int id;

 private String pagtoPaypalId;

 private String estado;

 private String moeda;

 private double valor;

 private Usuario usuario;
 
 // get's e set's
 
}

public class Venda {

 private int id;

 private String estado;

 private double preco;

 private int qtde;

 private Pagamento pagamento;

 private Produto produto;
 
 // get's e set's
 
}

public class Produto {

 private int id;
 
 private String titulo;

 @SerializedName("imagem")
 private String img;

 private int qtde;

 private double valor;

 private String sku;
 
 private List<String> categoria;
 
 private List<Venda> vendas;
 
 // get's e set's
 
}
Perceba que a classe Produto tem seu atributo img anotado com a anotação @SerializeName pertencente ao Gson, e que define o nome com o qual esse valor será serializado quando transformado para JSON.

Agora é hora de configurar as nossas classes DAO para lidar com a persistência das informações relacionadas aos produtos. Veja na listagem abaixo todos os métodos que usaremos no projeto, portanto, adicione-os à uma nova classe no pacote br.edu.ecommerce.dao.
package br.edu.ecommerce.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import br.edu.ecommerce.db.DBUtil;
import br.edu.ecommerce.entidades.Pagamento;
import br.edu.ecommerce.entidades.Produto;
import br.edu.ecommerce.entidades.Venda;

public class ProdutoDAO {

 public List<Produto> listarProdutos() {
  List<Produto> resultado = new ArrayList<Produto>();

  Connection con = null;
  try {
   con = DBUtil.getConnexao();

   String sql = "SELECT * FROM TB_PRODUTO";

   PreparedStatement st = con.prepareStatement(sql);

   ResultSet rs = st.executeQuery();
   while (rs.next()) {
    Produto produto = new Produto();
    
    int idProduto = rs.getInt("id_produto");
    
    produto.setId(idProduto);
    produto.setTitulo(rs.getString("titulo"));
    produto.setQtde(rs.getInt("qtde"));
    produto.setSku(rs.getString("sku"));
    produto.setValor(rs.getDouble("valor"));
    produto.setImg(rs.getString("img"));
    produto.setCategoria(consultarCategoriasPorId(idProduto));

    resultado.add(produto);
   }
  } catch (ClassNotFoundException | SQLException e) {
   e.printStackTrace();
  } finally {
   if (con != null) {
    try {
     con.close();
    } catch (SQLException e) {
     e.printStackTrace();
    }
   }
  }

  return resultado;
 }
 
 public Produto consultarPorSku(String sku) {
  Produto produto = new Produto();
  
  Connection con = null;
  try {
   con = DBUtil.getConnexao();
   
   String sql = "SELECT * FROM TB_PRODUTO";
   if (sku != null) {
    sql += " WHERE SKU = ?";
   }
   
   PreparedStatement st = con.prepareStatement(sql);
   if (sku != null) {
    st.setString(1, sku);
   }
   
   ResultSet rs = st.executeQuery();
   if (rs.next()) {
    int idProduto = rs.getInt("id_produto");
    
    produto.setId(idProduto);
    produto.setTitulo(rs.getString("titulo"));
    produto.setQtde(rs.getInt("qtde"));
    produto.setSku(rs.getString("sku"));
    produto.setValor(rs.getDouble("valor"));
    produto.setImg(rs.getString("img"));
    produto.setCategoria(consultarCategoriasPorId(idProduto));
   }
  } catch (ClassNotFoundException | SQLException e) {
   e.printStackTrace();
  } finally {
   if (con != null) {
    try {
     con.close();
    } catch (SQLException e) {
     e.printStackTrace();
    }
   }
  }
  
  return produto;
 }


 public List<String> consultarCategoriasPorId(int idProduto) {
  List<String> resultado = new ArrayList<String>();

  Connection con = null;
  try {
   con = DBUtil.getConnexao();

   StringBuilder sql = new StringBuilder();
   sql.append("SELECT ");
   sql.append("    cat.descricao");
   sql.append(" FROM");
   sql.append("    tb_categoria cat");
   sql.append("        INNER JOIN");
   sql.append("    tb_produto_categoria pc ON cat.id_categoria = pc.id_categoria");
   sql.append(" WHERE");
   sql.append("    pc.id_produto = ?");

   PreparedStatement st = con.prepareStatement(sql.toString());
   st.setInt(1, idProduto);
   
   ResultSet rs = st.executeQuery();
   while (rs.next()) {
    resultado.add(rs.getString("descricao"));
   }
  } catch (ClassNotFoundException | SQLException e) {
   e.printStackTrace();
  } finally {
   if (con != null) {
    try {
     con.close();
    } catch (SQLException e) {
     e.printStackTrace();
    }
   }
  }

  return resultado;
 }
 
 public int salvarPagto(Pagamento pagamento) {
  int chaveGerada = 0;
  
  Connection con = null;
  try {
   con = DBUtil.getConnexao();
   
   String sql = "INSERT INTO TB_PAGAMENTO(PAGTOPAYPALID, ESTADO, VALOR, MOEDA, IDUSUARIO) VALUES (?, ?, ?, ?, ?)";
   
   PreparedStatement st = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
   st.setString(1, pagamento.getPagtoPaypalId());
   st.setString(2, pagamento.getEstado());
   st.setDouble(3, pagamento.getValor());
   st.setString(4, pagamento.getMoeda());
   st.setInt(5, pagamento.getUsuario().getId());
   
   st.execute();
   ResultSet rs = st.getGeneratedKeys();
   if(rs.next()) {
    chaveGerada = rs.getInt(1);
   }
  } catch (ClassNotFoundException | SQLException e) {
   e.printStackTrace();
  } finally {
   if (con != null) {
    try {
     con.close();
    } catch (SQLException e) {
     e.printStackTrace();
    }
   }
  }
  
  return chaveGerada;
 }
 
 public List<Produto> salvarVenda(Venda venda) {
  List<Produto> resultado = new ArrayList<Produto>();
  
  Connection con = null;
  try {
   con = DBUtil.getConnexao();
   
   String sql = "INSERT INTO TB_VENDA(ESTADO, PRECO, QTDE, PRODUTOID, PAGTOID) VALUES (?, ?, ?, ?, ?)";
   
   PreparedStatement st = con.prepareStatement(sql);
   st.setString(1, venda.getEstado());
   st.setDouble(2, venda.getPreco());
   st.setInt(3, venda.getQtde());
   st.setInt(4, venda.getProduto().getId());
   st.setInt(5, venda.getPagamento().getId());
   
   st.execute();
  } catch (ClassNotFoundException | SQLException e) {
   e.printStackTrace();
  } finally {
   if (con != null) {
    try {
     con.close();
    } catch (SQLException e) {
     e.printStackTrace();
    }
   }
  }
  
  return resultado;
 }
}
Na mesma listagem, temos os seguintes métodos e suas funções:
  • listarProdutos: Faz uma listagem simples dos produtos na base preenchendo os valores e chamando o método consultarCategoriasPorId() para buscar as categorias de cada produto.
  • consultarPorSku: Método que busca os valores pelo código sku já falado antes.
  • salvarPagto: Método que efetua o salvamento do pagamento. Note que ele deverá ser chamado sempre após qualquer tentativa de fazer um pagamento. Repare também que estamos usando o recurso RETURN_GENERATED_KEYS do Statement JDBC, isso serve para nos retornar o id autogerado de cada produto, uma vez que iremos precisar dele para salvar o relacionamento com a venda.
  • salvarVenda: Método que salva a venda final.
Vamos criar agora mais três classes que serão auxiliares nesse processo todo. Veja-as abaixo e observe que o pacote de cada uma está referenciado logo antes da declaração:
package br.edu.ecommerce.util;

public class Util {
 public static StringBuilder converterJSONUTF8(List<Produto> produtos) {
  Gson gson = new Gson();
  StringBuilder produtosJson = new StringBuilder();
  String string;
  try {
   InputStream inputStream = new ByteArrayInputStream(gson.toJson(produtos).getBytes(StandardCharsets.UTF_8));
   if (inputStream != null) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    while (null != (string = reader.readLine())) {
     produtosJson.append(string);
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
  return produtosJson;
 }
}

package br.edu.ecommerce.rest;

public class Response {

 private String msg;

 private boolean erro;
 
 public String getMsg() {
  return msg;
 }

 public void setMsg(String msg) {
  this.msg = msg;
 }

 public boolean isErro() {
  return erro;
 }

 public void setErro(boolean erro) {
  this.erro = erro;
 }
 
}

package br.edu.ecommerce.entidades;

import com.google.gson.annotations.SerializedName;

public class PagamentoCliente {

 @SerializedName("amount")
 private double quantia;

 @SerializedName("currency_code")
 private String moeda;

 public double getQuantia() {
  return quantia;
 }

 public void setQuantia(double quantia) {
  this.quantia = quantia;
 }

 public String getMoeda() {
  return moeda;
 }

 public void setMoeda(String moeda) {
  this.moeda = moeda;
 }

}
A primeira classe é um utilitário que nos ajudará a fazer a converter todo o nosso conteúdo JSON para UTF-8, uma vez que lidaremos com valores de URLs, acentos e caracteres especiais via HTTP. A segunda classe é um POJO simples para lidar com as respostas que enviaremos ao app Android e a terceira lidará com o salvamento das informações de cada pagamento que virão serializadas também do app Android.

Agora é hora de criar os métodos do nosso Web Service Restful que irá atuar como fachada de recebimento das requisições provindas do app Android. Veja na listagem abaixo o código que precisaremos criar na nova classe do pacote br.edu.ecommerce.rest:
package br.edu.ecommerce.rest;

// Imports omitidos. Use Ctrl + Shift + O para organizá-los no Eclipse.

@Path("produto")
public class ProdutosRest {
 
 private ProdutoDAO produtoDAO = new ProdutoDAO();

 @Path("produtos")
 @GET
 @Produces(MediaType.APPLICATION_JSON)
 public String getProdutos(@QueryParam("str") String str) {
  return Util.converterJSONUTF8(produtoDAO.consultarPorTitulo(str)).toString();
 }
 
 @Path("checkPagto")
 @POST
 @Produces(MediaType.APPLICATION_JSON)
 public Response verificarPagto(@FormParam("idPagto") String idPagto, @FormParam("jsonClientePagto") String jsonClientePagto, @FormParam("idUsuario") String idUsuario) {
  br.edu.ecommerce.rest.Response r = new br.edu.ecommerce.rest.Response();
  try {
   OAuthTokenCredential tokenCredential = Payment.initConfig(getClass().getClassLoader().getResourceAsStream("sdk_config.properties"));
   String accessToken = tokenCredential.getAccessToken();
   APIContext apiContext = new APIContext(accessToken);
   Payment pagto = Payment.get(apiContext, idPagto);
   
   if (!pagto.getState().equals("approved")) {
    r.setErro(true);
    r.setMsg("Pagamento não verificado. Status: ");
    return Response.ok(new Gson().toJson(r), MediaType.APPLICATION_JSON).build();
   }
   
   Gson gson = new Gson();
   PagamentoCliente pagtoParam = gson.fromJson(jsonClientePagto, PagamentoCliente.class);
   double valorCliente = pagtoParam.getQuantia();
   String moedaCliente = pagtoParam.getMoeda();
   
   Transaction trans = pagto.getTransactions().get(0);
   String valorServidor = trans.getAmount().getTotal();
   String moedaServidor = trans.getAmount().getCurrency();
   
   String estadoVenda = trans.getRelatedResources().get(0).getSale().getState();
   
   Pagamento pagtoFinal = new Pagamento();
   pagtoFinal.setEstado(pagto.getState());
   pagtoFinal.setPagtoPaypalId(pagto.getId());
   
   Usuario usuario = new Usuario();
   usuario.setId(idUsuario != null ? Integer.parseInt(idUsuario) : 1);
   pagtoFinal.setUsuario(usuario);
   
   pagtoFinal.setMoeda(moedaCliente);
   pagtoFinal.setValor(Double.parseDouble(valorServidor));
   
   int idPagtoBD = produtoDAO.salvarPagto(pagtoFinal);
   pagtoFinal.setId(idPagtoBD);
   
   if (Double.parseDouble(valorServidor) != valorCliente) {
    r.setErro(true);
    r.setMsg("Quantias de pagamento não conferem!");
    return Response.ok(new Gson().toJson(r), MediaType.APPLICATION_JSON).build();
   }
   
   if (!moedaServidor.equals(moedaCliente)) {
    r.setErro(true);
    r.setMsg("Moedas de pagamento não conferem!");
    return Response.ok(new Gson().toJson(r), MediaType.APPLICATION_JSON).build();
   }
   
   if (!estadoVenda.equals("completed")) {
    r.setErro(true);
    r.setMsg("Venda não completada!");
    return Response.ok(new Gson().toJson(r), MediaType.APPLICATION_JSON).build();
   }
   
   inserirItensVendas(pagtoFinal, trans, estadoVenda);
   
  } catch (PayPalRESTException ex) {
   ex.printStackTrace();
   return Response.serverError().entity("Erro na comunicação com o PayPal. Favor tentar novamente!").build();
  }
  
  r.setErro(false);
  r.setMsg("Pagamento verificado com sucesso!");
  
  return Response.ok(new Gson().toJson(r), MediaType.APPLICATION_JSON).build();
 }
 
 private void inserirItensVendas(Pagamento pagtoFinal, Transaction trans, String estadoVenda) {
  ItemList listaItens = trans.getItemList();
  
  for (Item i : listaItens.getItems()) {
   Venda venda = new Venda();
   venda.setQtde(Integer.parseInt(i.getQuantity()));
   venda.setPreco(Double.parseDouble(i.getPrice()));
   venda.setPagamento(pagtoFinal);
   venda.setEstado(estadoVenda);
   
   Produto produto = produtoDAO.consultarPorSku(i.getSku());
   venda.setProduto(produto);
   
   produtoDAO.salvarVenda(venda);
  }
 }
}
Essa classe usa essencialmente Rest e anotações da API do Jersey. Se não conhece bem como funciona esse tipo de WS e/ou tecnologia, dê uma pesquisada na documentação oficial do projeto.

O primeiro método lista todos os produtos vindos da base de acordo com o parâmetro de filtro enviado. O segundo é o método de verificação do pagamento, para nos assegurar de que o mesmo está correto e aprovado. Perceba que logo no início carregamos o objeto do tipo OAuthTokenCredential a partir de um arquivo "sdk_config.properties" que deve estar na raiz da pasta src do projeto. O conteúdo desse arquivo está listado na próxima listagem abaixo. No final do método, salvamos a informação do pagamento e, se estiver tudo ok, salvamos a venda em seguida através do último método da classe. Nesse momento, o código sku também se faz importante.

# Connection Information
http.ConnectionTimeOut=5000
http.Retry=1
http.ReadTimeOut=30000
http.MaxConnection=100

# HTTP Proxy configuration
# If you are using proxy set http.UseProxy to true and replace the following values with your proxy parameters
http.ProxyPort=8080
http.ProxyHost=127.0.0.1
http.UseProxy=false
http.ProxyUserName=null
http.ProxyPassword=null

#Set this property to true if you are using the PayPal SDK within a Google App Engine java app
http.GoogleAppEngine = false

# Service Configuration
service.EndPoint=https://api.sandbox.paypal.com
# Live EndPoint
# service.EndPoint=https://api.paypal.com

# Credentials
clientId=AbkVNdXzruEZ6FEGaJB6TBfB_-4qmlqLPAQj5qin4FI8cS3v0Vs2pjldttP4MgmF487ZyVZ2y34j4xPE
clientSecret=EDYpA14ZmKc8pXqFe9CHWYJkkqu-6z1NxduRpeIHsr_O37QIgZK7vYd3R6ElsKpsPbQ55VbVo8ps3KuE
Esse arquivo pode ser encontrado dentro do pacote de download do projeto PayPal-Java-SDK no Github, bastando simplesmente alterar as informações de clientId e clientSecret para as do seu app.

Concluindo

Com isso, nós finalizamos os ajustes na parte web e de bando de dados do nosso projeto. Na segunda parte do artigo, iremos construir o projeto Android, configurar a exibição dos produtos e fazer as requisições para os métodos de serviço que criamos hoje. Até já! :)

-->

14 de fevereiro de 2014

Tags: , , , , ,

[SoapUi] Removendo comentários "Optional" em Web Services

O SoapUI é uma ferramenta de geração de testes client criada para facilitar a vida tanto de desenvolvedores quanto dos testers de Web Services. Através dela, o usuário tem a possibilidade de gerar projetos a partir da URL do WSDL do serviço. Através disso uma requisição de ação (request action) será gerada com os parâmetros do método requerido já carregados inicialmente.


Independente da tecnologia WS usada, o SoapUI tem o poder de reconhecer os parâmetros, em vista da universalidade do xml usada na especificação. Além disso, é possível observar também a presença do comentários que vem precedidos de cada campo. Como mostrado na figura abaixo.

Figura 1. Código exemplo de request action no SoapUi

Esse tipo de comentário existe para o caso em que o número mínimo de ocorrências para o parâmetro é zero. Se o elemento for obrigatório o SoapUi não exibe nada acima. E é ainda onde entra a questão... Como fazer?
Suponha que você tenha um WSDL semelhante ao exibido na listagem abaixo:

<xsd:element name="inserirPessoa">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:element name="id" type="xsd:int" minOccurs="1" maxOccurs="1" />
      <xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="1" />
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

efinição dos atributos minOccurs e maxOccurs pode ser justificada da seguinte maneira:
minOccurs="0" maxOccurs="1" -> significa opcional;
minOccurs="1" maxOccurs="1" -> significa obrigatório.

A utilização do maxOccurs não é obrigatória, logo, você pode usar apenas o atributo "minOccurs" setado com o valor 1 e tudo vai funcionar perfeitamente também.

O resultado na geração do XML de entrada no SoapUi será algo semelhante a:

<soapenv:Body>
  <xsd:inserirPessoa>
    <xsd:id>?</xsd:id>
    <!--Optional:-->
    <xsd:email>?</xsd:email>
  </xsd:inserirPessoa>
</soapenv:Body>

O situação muda um pouco de cor quando lidamos com a geração de WS utilizando annotations java da especificação JEE atreladas aos famosos EJBs. Neste caso, a anotação @WebService permite que você mapeie um POJO simples com todos os atributos sendo automaticamente associados aos parâmetros do serviço. Por exemplo, considere a classe simples abaixo:

public class Aluno {
    private int id;
    private String nome;
    
    // get's e set's
}

Você poderá ter o mesmo efeito empregado usando uma anotação também do pacote do JaxB:
public class Aluno {
    private int id;
    @XmlElement(required = true, nillable = false)
    private String nome;
    
    // get's e set's
}

O atributo required diz que o valor é obrigatório e o atributo nillable diz que o valor não poderá ser nulo.

Conclusão

As abordagens são relativamente simples, mas compensam o uso e conhecimento por facilitar a vida de quem irá testar ou receber o Web Service. A anotação dá a impressão de que o campo é opcional quando não é.

Em algumas versões das api's de WS o exemplo com annotations pode não funcionar. Se isso acontecer então solicite a atualização das bibliotecas. Alguns erros do tipo acontecem porque a versão não contempla os respectivos fixes. :)

13 de setembro de 2013

Tags: , , , ,

Erro CrystalReports: "java.lang.NoClassDefFoundError: CrystalConfigurationImpl"

Olá galera, post rápido sobre alguns problemas que venho me defrontando com o IBM e bibliotecas afins.

Como muitos sabem a documentação oficial da IBM para ajudar a resolver os problemas decorrentes de suas ferramentas e api's é bem escassa e vaga. Logo, pretendo fazer um acervo de problemas que eu for tendo ao longo do usar das mesmas tecnologias.

O problema acima acontece quando tentamos usar as bibliotecas de geração de relatórios CrystalReports junto com o Java EE.

Neste caso especificamente, a ausência de algumas bibliotecas pode ocasionar determinados problemas de classes não encontradas seja em tempo de compilação ou de execução...

A lib em questão é a "commons-configuration.jar", da Apache. Ela é responsável por associar as configurações do report ao java.

É isso. Adicione ela ao seu classpath e corre para o abraço!