Alura: Imersão Java

Projeto muito interessante realizado na imersão Java da ALURA. Ele tras um grande aprendizado para quem está começando e um grande desafio àqueles que estão no meio do caminho. Eu, estou entre esses dois, e, como parte da minha contribuição, irei descrever, com o pouco que sei, como eu passei pelos desafios.

IMPORTATE!!

As descrições de como resolvi os desafios podem estar diferentes dos código que estão nos fontes do projeto, pois com o caminhar das aulas o código será refatorado.

NOTA

Os fontes da aula 04/05 podem ser encontratos em: FONTES DA API

Espero poder contribuir com aqueles que estão nessa jornada assim como eu.

Onde me encontrar

Linkedin

Aula 01

Tudo começa com a consulta em uma API de filmes, a selecionada foi imdb, e como era esperado ela caiu, com isso utilizamos a themoviedb, e alguns colegas disponibilizaram outras.

Eu tive que utilizar um arquivo estático; estou acompanhando a imersão na empresa e meu eclipse não quer me ajudar, e não quero perder tempo arrumando o problema dele.

Dito isso, vamos ao que interessa. A Chamada via API pode ser vista no arquivo AppStickerFromApi. Realizar a chama é relativamente simples:

Iniciamos criando a URI que iremos utilizar

String url = "https://api.mocki.io/v2/549a5d8b/Top250Movies";
URI uriClient = URI.create(url);

URIS SUJERIDAS

Agora criamos um client que irá chamar essa URI

HttpClient client = HttpClient.newHttpClient();

Preparamos a nossa chamada

HttpRequest request = HttpRequest.newBuilder(uriClient).GET().build();

E a realizamos com e pegamos somente o "corpo" do retorno:

HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
String responseBody = response.body();

O tratamento do retorno foi realizado em um desafio, irei descrevê-lo lá.

Resumo

  1. Criar a URI do serviço que iremos utilizar.
  2. Criar um cliente com HttpClient.
  3. Criar uma requisição com HttpRequest.
  4. Fazer a requisição com client.send(request, BodyHandlers.ofString()).
  5. Pegar o body do retorno.
  6. Tratar o retorno.

Desafios aula 01

Desafio 01

Consumir o endpoint de filmes mais populares da API do IMDB. Procure também, na documentação da API do IMDB, o endpoint que retorna as melhores séries e o que retorna as séries mais populares.

Desafio 02

Usar sua criatividade para deixar a saída dos dados mais bonitinha: usar emojis com código UTF-8, mostrar a nota do filme como estrelinhas, decorar o terminal com cores, negrito e itálico usando códigos ANSI, e mais!

SOLUÇÃO

Tive alguns problemas com essa demanda, meu terminal não reconhecia os caracteres que criam as cores, tive que baixar um plugin para resolver esse problema.

ANSI Escapes.

Resolvendo isso, bastou entrar na documentação sujerida pela Alura

ALURA: Decorando terminal cores emojis.

e "codar".

Na classe AppStickerFromFile criei minhas variáveis que guardarão as cores que utilizarei.

final static String NEGRITO = "\u001B[1m";
final static String RESET = "\u001B[0m";
final static String COR_TITULO = "\u001B[38;2;254;181;0m";
final static String FUNDO_TITULO = "\u001B[48;2;234;214;164m";
final static String COR_LINHA = "\u001B[38;2;178;129;7m";
final static String COR_EMOJI = "\u001B[38;2;164;123;22m";

Agora é só brincar com os prints

System.out.print(COR_LINHA);
System.out.println("-".repeat(100));
System.out.print(RESET);

System.out.print(NEGRITO + COR_TITULO);
System.out.print("Título Original:");
System.out.print(RESET + " ");
System.out.print(jsFilme.get("original_title"));

(...)

teminal-pintado

Desafio 03

Colocar a chave da API do IMDB em algum lugar fora do código como um arquivo de configuração (p. ex, um arquivo .properties) ou uma variável de ambiente.

SOLUÇÃO

Como eu já resolvi isso com alguns projetos do trabalho, foi relativamente fácil. Criei a pasta config com o arquivo config.properties dentro. Neste eu coloquei as minhas chaves

imdb_key = minha-chave-linda
themoviedb_key = minha-chave-linda-dois

A classe a baixo abre o arquivo, itera sobre ele pegando todas as entradas e retorna um Map com as chaves que coloquei no arquivo.

private Map<String, String> getParametrosIntegracao(String nomeArquivoParam) throws Exception {
		if (!nomeArquivoParam.endsWith(".properties")) {
			nomeArquivoParam += ".properties";
		}

		FileInputStream arquivo = new FileInputStream(nomeArquivoParam);
		Properties properties = new Properties();
		properties.load(arquivo);

		Set<Object> keySet = properties.keySet();
		Iterator<Object> iterator = keySet.iterator();
		Map<String, String> parametroMap = new HashMap<String, String>();

		while (iterator.hasNext()) {
			String key = (String) iterator.next();
			parametroMap.put(key, properties.getProperty(key));
		}
		return parametroMap;
	}

Existe uma outra classe nesse arquivo que busca por uma api key especifica

public void setApiKey(String apiKey) {
		
		File classPath = new File(".");
		String configPath = classPath.getAbsolutePath() + "/config/";
						
		Map<String, String> properties;
		try {
			properties = getParametrosIntegracao(configPath + "config");
			this.apiKey = properties.get(apiKey);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	public String getApiKey() {
		return apiKey;
	}

Agora fica simples pegar uma api key que está dentro do meu arquivo que configuração, basta criar uma instância da classe UtilProperties, chamar setApiKey passando no nome da chave, e buscar o resultado com getApiKey

UtilProperties properties = new UtilProperties();
properties.setApiKey("themoviedb_key");
String url = "https://imdb-api.com/en/API/Top250Movies/" + properties.getApiKey();

Desafio 04

Mudar o JsonParser para usar uma biblioteca de parsing de JSON como Jackson ou GSON.

SOLUÇÃO

Usei outra biblioteca a simple parser

Exemplo de utilização da biblioteca

Para resolver esse problema criei uma classe UtilJson que realizara os tratamentos necessário. Lembre-se: Estou utilizando um arquivo .json pois não estou conseguindo chamar apis no meu serviço

Dito isso. Na classe existe um método que é responsavel por abrir o arquivo .json e retornar um Object, dessa forma ficará mais fácil realizar os tratamentos posteriores.

public Object getArquivoJson(String arquivo) {	
		Object obj = new Object();
		String pathJsonFile = this.absPath + File.separator + "base-dados" + File.separator + arquivo + ".json";
		
		try {
			obj = new JSONParser().parse(new FileReader(pathJsonFile));			
			
		} catch (FileNotFoundException e) {			
			throw new RuntimeException(e);
		} catch (IOException e) {			
			throw new RuntimeException(e);
		} catch (ParseException e) {			
			throw new RuntimeException(e);
		}	
		return obj;
	}

O tratamento é feito dependendo da API que estamos chamando, pois cada uma retonar um Json.

Por exemplo temos o extrator para as chamadas no The Movie DB. Veja que nesse caso, primeiro eu transformo o de Object para JSONObject, pego a tag results e depois transformo em JSONArray. Isso tudo para poder conseguir fazer um forEach.

	public List<Conteudo> extraiConteudosTheMovieDb(Object json){
		
		JSONArray jsonArray = (JSONArray) ((JSONObject) json).get("results");
		
		List<Conteudo> conteudos = new ArrayList<>();

		jsonArray.forEach((elemento) -> {
			JSONObject conteudo = (JSONObject) elemento;			
			conteudos.add(
					new Conteudo(conteudo.get("title").toString(), 
							     "https://image.tmdb.org/t/p/w500" + conteudo.get("poster_path"), 
							     conteudo.get("vote_average").toString())								 
				);
			
		});
		
		return conteudos;
		
	}

Desafio 05

Desafio supremo: criar alguma maneira para você dar uma avaliação ao filme, puxando de algum arquivo de configuração OU pedindo a avaliação para o usuário digitar no terminal.

SOLUÇÃO

Fiz algo bem simples, criei a classe AppStickerSalvaVoto, ela pedirá algumas informação ao usuário e chamará o método salvarVoto da classe UtilJson.

O funcionamento desse método é relativamente simmples. Cria um JSONObject insere as informações que foram passadas como paramêtro, cria um PrintWriter, ele será o responsável por salvar as informações em um arquivo.

Como disse é algo bem simples, recebe o voto e salva em um arquivo.

public void salvarVoto(String Usuario, String title, String rating) {
		JSONObject jo = new JSONObject();
		String classPath = UtilJson.class.getClassLoader().getResource("").getPath();
		String votoPath = classPath + "../base-dados/" ;
		
		jo.put("title", title);
		jo.put("usuario", Usuario);
		jo.put("rating", rating);
					
		try {
			PrintWriter pw = new PrintWriter(votoPath + "Filmes.json");
			pw.write(jo.toJSONString());         
	        pw.flush();
	        pw.close();
		} catch (FileNotFoundException e) {	
			throw new RuntimeException(e);
		}
	}

Aula 02

Nessa aula criamos a classe que irá gerar nossa figurinhas. Alguns dos colegas conseguiram fazer coisas incríveis. Eu consegui chegar ao seguinte resultado:

assets

Desafios aula 02

Desafio 01

Ler a documentação da classe abstrata InputStream.

SOLUÇÃO

Lido.

Desafio 02

Centralizar o texto na figurinha.

SOLUÇÃO

A conta é relativamente simples. Pegar a largura total da imagem, subtrair a largura do texto, isso nos dará o espaço que sobra, basta dividir esse valor por dois. E pegar essa possição como a inicial para o texto.

int xInicial = (int) ((largura - textLayout.getAdvance()) / 2);

Caso tenha ficado meio confuso a conta, tente entender esse imagem.

calculo

Desafio 02

Fazer um pacote no Whatsapp e/ou Telegram com as suas próprias figurinhas!

Desafio 04

Criar diretório de saída das imagens, se ainda não existir.

SOLUÇÃO

No meu caso eu criei o diretório a mão, mas a solução é fácil. A biblioteca File ajuda e muito nessa hora. Com ela podemos verificar se um diretório existe, se não basta criá-lo.

if (!new File(caminhoPasta).exists()) {
	new File(caminhoPasta).mkdirs();
}

Desafio 05

Colocar outra fonte como a Comic Sans ou a Impact, a fonte usada em memes.

Desafio 06

Colocar uma imagem de você que está fazendo esse curso sorrindo, fazendo joinha!

Desafio 07

Colocar contorno (outline) no texto da imagem.

SOLUÇÃO

Nesse o Alexandre da Alura me ajudou e muito. Confesso que não entendo muito o que está acontecendo ai; pelo que li, quem faz esse outline é o método setStroke, ele deve receber um objeto do tipo Strock, por isso criamos um BasicStroke,

Para escrever a frase precisamos usar o método draw, que recebe um objeto do tipo Shape que foi criado em Shape outline = textLayout.getOutline(null);

FontRenderContext fontRenderContext = graphics.getFontRenderContext();
			Font font = new Font(Font.SANS_SERIF, Font.BOLD, 60);
			
			TextLayout textLayout = new TextLayout(textoImagem, font, fontRenderContext);

			Shape outline = textLayout.getOutline(null);
			AffineTransform transform = graphics.getTransform();
			
			int xInicial = (int) ((largura - textLayout.getAdvance()) / 2);
			transform.translate(xInicial, novaAltura - 150);
			graphics.setTransform(transform);

			BasicStroke outlineStroke = new BasicStroke(largura * 0.004166f);
			graphics.setStroke(outlineStroke);

			graphics.setColor(Color.DARK_GRAY);
			graphics.draw(outline);
			graphics.setClip(outline);

Desafio 08

Tratar as imagens retornadas pela API do IMDB para pegar uma imagem maior ao invés dos thumbnails. Opções: pegar a URL da imagem e remover o trecho mostrado durante a aula. ou consumir o endpoint de posters da API do IMDB (mais trabalhoso), tratando o JSON retornado.

Desafio 09

Fazer com que o texto da figurinha seja personalizado de acordo com as classificações do IMDB.

SOLUÇÃO

Fácil, ao menos uma é. Na minha classe GeradorDeFigurinhas criei o método textoImagem que recebe o voto, utilizando o operador ternário eu consigo verificar o voto e dar uma mensagem personalizada.

Vale lembrar que estou utilizando um arquio estático como fonte dos dados, e nele só possiu os voltos 8.5, 8.6 e 8.7, por isso que a solução ficou assim

public String textoImagem(String voto) {
		
		return voto.equals("8.5") ? "The Third One" : 
               voto.equals("8.6") ? "The Second One" : 
	           voto.equals("8.7") ? "The Big One" :
		                            "No One";
	}

Desafio 10

Desafio supremo usar alguma biblioteca de manipulação de imagens como OpenCV pra extrair imagem principal e contorná-la.

Aula 03

Nessa aula consumimos a API da nasa, nela conseguimos trazer outras imagens para o nosso projeto.

Também refatoramos o nosso código. Criamos a classe ClienteHttp, esse será a responsável por fazer as chamadas nas APIs.

public String BuscaDados(String url) {				
		
		try {		
			URI uriClient = URI.create(url);		
			HttpClient client = HttpClient.newHttpClient();			
			HttpRequest request = HttpRequest.newBuilder(uriClient).GET().build();	
			
			HttpResponse<String> response;
			response = client.send(request, BodyHandlers.ofString());
			
			return response.body();
		} catch (IOException | InterruptedException e) {	
			throw new TrataExecoes("Algo De Errado Não Está Certo" , e);									
		} 		
	}
}

Na parte do extrator de contéudo eu fiz um pouco diferente. Criei uma unica classe com todos os métodos de extração dentro. A classe é Extratores, e como exemplo de método temos os:

Essa primeira classe trata a extração de informação quando é passado uma String. E a que foi desenvolvida em aula, eu adaptei um pouco, veja que conteudo recebe um parametro a mais.

public List<Conteudo> extraiConteudosNasa(String json){
		
		JsonParser parser = new JsonParser();
		List<Map<String, String>> listaDeAtributos = parser.parse(json);
		
		List<Conteudo> conteudos = new ArrayList<>();
		
		for(Map<String, String> atributos : listaDeAtributos) {
			String titulo = atributos.get("title");
			String urlImage = atributos.get("url");
			
			Conteudo conteudo = new Conteudo(titulo, urlImage, "0.0");
			
			conteudos.add(conteudo);
		}
		
		return conteudos;
		
	}

Para o meu caso, criei uma classe com o mesmo nome, porem recebendo um object

public List<Conteudo> extraiConteudosNasa(Object json){
		
		JSONArray jsonArray = (JSONArray) json;	
		
		List<Conteudo> conteudos = new ArrayList<>();
		
		jsonArray.forEach((elemento) -> {
			JSONObject conteudo = (JSONObject) elemento;
			conteudos.add(
					new Conteudo(conteudo.get("title").toString(), 
							     conteudo.get("url").toString(), 
							     "9.0")
					);
			
		});
		
		return conteudos;
		
	}

Por fim, mas não menos importante. Criamos uma classe Conteudo representando o objeto em que estamos trabalhando

**NOTA: ** Veja que não temos uma classe mas, sim record. Isso porque documentei após a execução dos desafios.

public record Conteudo(
		
		String titulo,
		String urlImagem,
		String voto
	) {}

Desafios aula 03

Desafio 01

Transformar a classe que representa os conteúdos em um Record, disponível a partir do Java 16

SOLUÇÃO

Para realizar essa demanda utilizei como referência esse link.

A solução foi bem simples. Basta alterar o tipo de classe para record, e remover a visibilidade dos atribrutos, dessa forma nosso record ficara:

public record Conteudo(
		
		String titulo,
		String urlImagem,
		String voto
	) {}

Em nossa classe main, basta chamar o atributo pelo nome e não pelo get

InputStream inputStream;	
inputStream = new URL(conteudo.urlImagem()).openStream();
String nomeArquivo = conteudo.titulo().replace(":", " -");
String voto = conteudo.voto();

Desafio 02

Criar as suas próprias exceções e usá-las na classe que implementa o cliente HTTP

SOLUÇÃO

Não cheguei à esse curso na Alura, mas bora tentar alguma coisa. Para isso tomarei a resposta no stakoverflow como base.

Bem, dado a meu baixo conhecimento no assunto, fiz algo bem simples. Creie a classe
TrataExecoes, e coloquei dois métodos dentro dela.

public class TrataExecoes extends RuntimeException{
	
	private static final long serialVersionUID = 1149241039409861914L;
	
	
    public TrataExecoes(String msg){
        super(msg);
    }
    
    public TrataExecoes(String msg, Throwable cause){
        super(msg, cause);
    }

}

E a usei no catch da classe ClienteHttp

} catch (IOException | InterruptedException e) {
	throw new TrataExecoes("Algo De Errado Não Está Certo" , e);
} 	

Desafio 03

Usar recursos do Java 8 e posterior, como Streams e Lambdas, para mapear uma lista em uma outra

SOLUÇÃO

Apliquei essa técnica em várias partes do código. A ideia é simples, pega o objeto que é uma lista, seleciona o método forEach dele, esse método irá percorrer todos os elementos do objeto e aplicar uma função que criamos em tempo de execução. No meu caso essa função adicionrá um conteudo na minha lista.

public List<Conteudo> extraiConteudosNasa(Object json){
		
		JSONArray jsonArray = (JSONArray) json;	
		
		List<Conteudo> conteudos = new ArrayList<>();
		
		jsonArray.forEach((elemento) -> {
			JSONObject conteudo = (JSONObject) elemento;
			conteudos.add(
					new Conteudo(conteudo.get("title").toString(), 
							     conteudo.get("url").toString())
					);
			
		});
		
		return conteudos;
		
	}

Desafio 04

Criar uma Enum que une, como configurações, a URL da API e o extrator utilizado

SOLUÇÃO

Para entender melhor o que foi feito, ler o artigo

Na minha implentação, criei o enun ArquivoJson

public enum ArquivosJson {
	THE_MOVIE_DB("themoviedb-topRated"),
	NASA("nasa-apod");
	
	private String nomeArquivo;
	
	private ArquivosJson(String nomeArquivo) {
		this.nomeArquivo = nomeArquivo;
	}
	
	public String getNomeArquivo() {
		return this.nomeArquivo;
	}

}

E em minha classe AppStickerFromFile o utilizei da seguinte forma:

String arquivosJson = ArquivosJson.THE_MOVIE_DB.getNomeArquivo();
Object json = utilJson.getArquivoJson(arquivosJson);

Desafio 05

Desafio supremo: consumir outras APIs que contém imagens, como a da Marvel, que é bem diferente. Repositório com APIs públicas: clique aqui.

Aula 04

Criar uma api.

Tecnologia utilizada, spring.

Para criar um projeto, podemos ir em start spring io

Banco de dados utilizado, mogo DB

Desafios aula 04

Desafio 01

Finalizar o CRUD (Create, Read, Update e Delete) para que se possa atualizar e excluir uma linguagem cadastrada.

SOLUÇÃO

Esse link pode ser usado como referência.

Create

Esse já foi feito em aula

@PostMapping("/linguagens-repositorio")
	public Linguagem cadastrarLinguagem(@RequestBody Linguagem linguagem) {
		Linguagem linguagemSalva = repositorio.save(linguagem);
		
		return linguagemSalva;
	}

Read

Acho que era para fazer uma busca por ID. Segue a solução

@GetMapping("/linguagens-repositorio/{id}")
	public Linguagem obterLinguagemRepositorio(@PathVariable String id) {
		Optional<Linguagem> linguagemConsultada = repositorio.findById(id);
		
		return linguagemConsultada.get();		
	}

Delete

@DeleteMapping("/linguagens-repositorio/{id}")
    public String deleteBook(@PathVariable String id){
		repositorio.deleteById(id);
        
        return "Deletado Com Sucesso";
    }

Update

Utilizar como referência.

@PutMapping("/linguagens-repositorio/{id}")
	public Linguagem atualizarLinguagem(@PathVariable(value = "id") String id, @RequestBody Linguagem linguagem) {
		Linguagem linguagemNova = repositorio.findById(id).get();
		
		linguagemNova.setTitle(linguagem.getTitle());
		linguagemNova.setImage(linguagem.getImage());		
		linguagemNova.setRanking(linguagem.getRanking());	     
	     
	     final Linguagem linguagemAtualizada = repositorio.save(linguagemNova);
	     return linguagemAtualizada;
	}

Desafio 02

Devolver a listagem ordenada pelo ranking.

SOLUÇÃO

Demorou mais saiu. meio que simples até. Solução encontrada no link

@GetMapping("/linguagens-repositorio")
	public List<Linguagem> obterLinguagensRepositorio(){	
		
		List<Linguagem> linguagensRepositorio = this.repositorio.findAll();
		Collections.sort(linguagensRepositorio, (a, b) -> a.getRanking().compareTo(b.getRanking()));
		
		return linguagensRepositorio;
	}

Desafio 03

Criar na sua API um modelo de entidade com nomes diferentes de title e image e criar seu próprio extrator de informações personalizado OU, manter com o nome title e image e traduzir para que seja retornado como título e imagem através do uso de DTO (Data Transfer Object).

SOLUÇÃO

Não sei ao certo se era isso. Utilizei: link

@PostMapping("/linguagens-repositorio")
	public ResponseEntity<Linguagem> cadastrarLinguagem(@RequestBody PostDto postDto) {
		Linguagem linguagem = new Linguagem(postDto.titulo(), postDto.imagem(), postDto.rank());
		
		Linguagem linguagemSalva = repositorio.save(linguagem);
		
		return ResponseEntity.status(201).body(linguagemSalva);
	}

Desafio 04

Retornar o status 201 quando um recurso (linguagem, no nosso caso) for cadastrado através do POST;

SOLUÇÃO

Material utilizado para a solução.

@PostMapping("/linguagens-repositorio")
	public ResponseEntity<Linguagem> cadastrarLinguagem(@RequestBody Linguagem linguagem) {
		Linguagem linguagemSalva = repositorio.save(linguagem);
		
		return ResponseEntity.status(201).body(linguagemSalva);
	}

Desafio 05

Desafio supremo: Aplicar modificações parciais no recurso através do método PATCH, por exemplo, modificando o número de votos ou de pessoas que utilizam cada linguagem de programação.

SOLUÇÃO

A documentação pode ser encontrada Aqui

@PatchMapping("/linguagens-repositorio/{id}/{ranking}")
	public Linguagem atualizarRankingLinguagem(@PathVariable String id, @PathVariable String ranking) {		
		Linguagem linguagem = repositorio.findById(id).get();
		linguagem.setRanking(ranking);
		
		Linguagem linguagemAtualizada = repositorio.save(linguagem);
		
		return linguagemAtualizada;		
	}

Aula 05

Nessa aula subimos a aplicação para que ele roda-se e cloud, para isso utilizamos o HEROKU. Tive uma pouco de dificuldade. Consegui colocar a aplicação no ar assintindo ao vídeo

Desafios aula 05

Desafio 01

Compartilhe com seus amigos a sua URL do Heroku, para que eles possam consumir a sua API (com o padrão de atributos title e image) e gerar figurinhas do conteúdo que você utilizou (linguagens de programação, filmes, músicas).

SOLUÇÃO

A documentação da API pode ser encontrada no repositório

Desafio 02

Colocar a aplicação no cloud da Oracle;

Link que pode ajudar

https://medium.com/danieldiasjava/executando-sua-aplica%C3%A7%C3%A3o-java-na-oracle-cloud-infrastructure-d8ce55e74d61

Desafio 03

Implementar algum mecanismo de votação na API, para que outros amigos votem nas linguagens preferidas para que se possa fazer um ranking.

SOLUÇÃO

Quero aproveitar o que já foi realizado. Dito isso. Criei o record Voto

record Voto (
	String id,
	String voto
) {}

E uma url com um método post.

@PostMapping("/linguagem-repositorio/votar")
	public ResponseEntity<Linguagem> cadastrarVoto(@RequestBody Voto voto){
		Voto cadastarVoto = new Voto(voto.id(), voto.voto());
		
		Linguagem linguagemEscolhida = repositorio.findById(cadastarVoto.id()).get();
		
		String votacaoAtual = linguagemEscolhida.getRanking();
		int votacaoNova = Integer.parseInt(votacaoAtual) + Integer.parseInt(cadastarVoto.voto());		
		linguagemEscolhida.setRanking("" + votacaoNova);
		
		return ResponseEntity.status(200).body(repositorio.save(linguagemEscolhida));
	}

Desafio 04

Desafio supremo: Evoluir o projeto das três primeiras aula para que ele gere um bundle de stickers, para que se possa fazer o download e já incluir vários stickers no WhatsApp; Usar os conhecimentos aprendidos em alguma imersão React da Alura e fazer uma aplicação front-end para gerar esse bundle, onde possa se passar a URL do conteúdo e já visualizar os stickers;