05/04/2017 STD29006 – Sistemas Distribuı́dos Laboratório 3: Desenvolvendo Serviços Web em Java e Python Professor: Emerson Ribeiro de Mello i http://docente.ifsc.edu.br/mello/std Nota: O objetivo deste laboratório é mostrar como desenvolver: (1) cliente e Serviços Web SOAP e REST em Java, fazendo uso do Netbeans IDE; e (2) um Serviço Web REST em Python, sendo acessado por meio do aplicativo curl. Requisitos: • Ter o Netbeans IDE 8.2 com Java EE e servidor de aplicação Glassfish. • Ter instalado o ambiente python e o framework Flask (http:// flask.pocoo.org ) 1 SOAP Para o desenvolvimento do cliente e Serviços Web SOAP será feito uso da API Java para Web services XML (JAX-WS) para desenvolvimento de cliente e Serviço Web SOAP. O JAX-WS faz uso de Anotações Java1 visando simplificar o desenvolvimento e implantação de clientes e serviços. O JAX-WS faz uso do JAXB 2.0, que permite mapear classes Java em XML. Ou seja, é possı́vel empacotar objetos Java em documentos XML para que possam ser transmitidos pela rede. Para isto as classes Java são marcadas com Anotações Java, como por exemplo, @XmlRootElement. A Figura 1 ilustra como será o desenvolvimento de cliente e Serviço Web. Figura 1: Comunicação entre cliente e Serviço Web SOAP usando o JAX-WS Neste laboratório serão desenvolvidos: • Serviço Web SOAP (calculadora) com dois métodos: – Somar: public int somar(int a, int b) – Subtrair: public int subtrair(int a, int b) • Cliente Desktop que invoca os métodos da Calculadora 1 https://docs.oracle.com/javase/tutorial/java/annotations 1 IFSC – Campus São José 1.1 Construindo o Serviço Web: Calculadora SOAP 1. Crie um novo projeto dentro da categoria “Java Web” escolha “Aplicação Web”. • Dê o nome LabWebService para o projeto e clique em próximo • Escolha servidor de aplicação Glassfish e Java EE 7. Clique em Finalizar 2. Crie um novo arquivo e dentro da categoria “Web Services” escolha “Web Service” • Dê o nome Calculadora e para o pacote de o nome engtelecom.std. Clique em Finalizar 3. Siga as instruções abaixo para criar os métodos somar e subtrair. Ambos terão 2 inteiros como parâmetro e retornarão um inteiro com o resultado da operação. • Na janela com o código fonte, clique na aba DESIGN. Clique no botão Adicionar Operação • Clique novamente na aba código Fonte e faça a implementação dos métodos somar e subtrair, como apresentado na Listagem 1. 4. Faça a implantação no serviço no servidor de aplicação • Clique com botão direito sobre o nó do projeto e escolha Implantar 5. Faça um teste do serviço com um cliente simples do Netbeans • Abra a pasta Web Services dentro do projeto e clique com botão direito sobre o serviço Calculadora e escolha Testar Web Service Listagem 1: Código da Calculadora SOAP 1 2 3 4 package engtelecom.std; import javax.jws.WebService; import javax.jws.WebMethod; import javax.jws.WebParam; 5 6 7 @WebService(serviceName = "Calculadora") public class Calculadora { 8 @WebMethod(operationName = "somar") public int somar(@WebParam(name = "a") int a, @WebParam(name = "b") int b) { int c = a + b; return c; } 9 10 11 12 13 14 @WebMethod(operationName = "subtrair") public int subtrair(@WebParam(name = "a") int a, @WebParam(name = "b") int b) { int c = a - b; return c; } 15 16 17 18 19 20 } 2 IFSC – Campus São José 1.2 Consumindo o Serviço Web: Calculadora SOAP com cliente desktop 1. Crie um novo projeto dentro da categoria “Java” escolha “Aplicação Java”. • Dê o nome ClienteWebService para o projeto e coloque o nome da classe principal como “engtelecom.std.Principal”. Clique em finalizar 2. Crie um novo arquivo e dentro da categoria “Web Services” escolha “Cliente para Web Service” 3. Na localização do WSDL procure pelo projeto e encontre o serviço Calculadora • Depois de indicar o WSDL, informe engtelecom.std como nome do pacote e clique em Finalizar 4. Abra o arquivo Principal no editor de código e no navegador de projetos abra a pasta “Referência de Web Services” e suas subpastas até visualizar as operações 5. Clique em cima da operação somar e arraste para o código fonte da classe Principal, logo abaixo do método main • Faça o mesmo para o método subtrair • Dentro do método main faça as invocações para somar e subtrair 6. Clique com botão direito no nó do projeto e escolha “Limpar e Construir” e depois execute o projeto Listagem 2: Código fonte do cliente desktop invocando um serviço SOAP 22 23 package engtelecom.std; public class Principal { 24 public static void main(String[] args) { int i = somar(10,2); int j = subtrair(10,2); 25 26 27 28 System.out.println("i: "+i+", j: "+j); 29 } 30 31 private static int somar(int a, int b) { engtelecom.std.Calculadora_Service service = new engtelecom.std.Calculadora_Service(); engtelecom.std.Calculadora port = service.getCalculadoraPort(); return port.somar(a, b); } 32 33 34 35 36 37 private static int subtrair(int a, int b) { engtelecom.std.Calculadora_Service service = new engtelecom.std.Calculadora_Service(); engtelecom.std.Calculadora port = service.getCalculadoraPort(); return port.subtrair(a, b); } 38 39 40 41 42 43 44 } 3 IFSC – Campus São José 2 REST A API Java para RESTful Web Services (JAX-RS) também faz uso de Anotações Java para o desenvolvimento de clientes e serviços REST. O Jersey é a implementação de referência para a especificação JSR 311 e está presente no Netbeans 8.0 com JDK 7 ou superior. JAX-RS provê suporte para criação de mensagens XML e JSON através do JAXB. A Tabela 1 apresenta as Anotações REST do JAX-RS. Tabela 1: Anotações do JAX-RS Descrição Define o caminho base da aplicação Indica que o método abaixo da anotação irá responder pedidos HTTP PUT @GET Indica que o método abaixo da anotação irá responder pedidos HTTP GET @POST Indica que o método abaixo da anotação irá responder pedidos HTTP POST @DELETE Indica que o método abaixo da anotação irá responder pedidos HTTP DELETE @Produces(MediaType. Define o tipo MIME que será entregue por um método TEXT PLAIN[, outros tipos]) que tiver sido anotado com @GET. Outros exemplos: “application/xml”, “application/json” @Consumes(type[, outros tipos]) Define o tipo MIME que será consumido pelo método @PathParam Para obter valores de URL e inserir como parâmetro em um método Anotação @PATH(caminho) @PUT Neste laboratório serão desenvolvidos: • Serviço Web REST com os seguintes recursos: – olamundo ∗ GET – retorna “Olá Mundo” em TEXT PLAIN ∗ GET – retorna “Olá Mundo” em XML ∗ GET – retorna “Olá Mundo” em JSON – olamundo/mensagem ∗ GET – retorna a “mensagem” em XML ∗ POST – consome uma mensagem codificada como parâmetro de formulário HTML e retorna a “mensagem” em XML • Cliente Desktop que consome os recursos dos Serviço REST • Sintaxe de como usar o aplicativo curl para consumir serviço REST 4 IFSC – Campus São José 2.1 Construindo o Serviço Web REST: Ola Mundo 1. Crie um novo projeto dentro da categoria “Java Web” escolha “Aplicação Web”. • Dê o nome LabREST para o projeto e clique em próximo • Deixe o servidor de aplicação Glassfish e a versão do Java EE 7. Clique em Finalizar 2. Crie um novo arquivo e dentro da categoria “Web Services” escolha “Web Service RESTful a partir dos Padrões” • Escolha Recurso Raiz Simples e clique em próximo • Em pacote do recurso dê o nome: engtelecom.recursos • Em caminho dê o nome olamundo • Em nome da Classe deixe OlaMundo • Clique em Finalizar 3. Deixe o código fonte do recurso (classe) OlaMundo conforme listagem abaixo 46 47 48 49 @Path("olamundo") public class OlaMundo { @Context private UriInfo context; 50 public OlaMundo() { } 51 52 53 @GET @Produces(MediaType.TEXT_PLAIN) public String olaTexto() { return "Ola Mundo"; } 54 55 56 57 58 59 @GET @Produces("application/xml") public String olaXML() { return "<?xml version=\"1.0\"?>" + "<mensagem>Ola Mundo" + "</mensagem>"; } 60 61 62 63 64 65 @GET @Produces("application/json") public String olaJSON() { return "{ \"mensagem\": \"Ola Mundo\" }"; 66 67 68 69 70 } @Path("/{nome}") @GET @Produces("application/xml") public String olaOutroXML(@PathParam("nome") String n) { return "<?xml version=\"1.0\"?>" + "<mensagem>"+ n + "</mensagem>"; } @POST @Consumes("application/x-www-form-urlencoded") @Produces("application/xml") public String recebeParametroViaFORM(@FormParam("nome") String nome){ return "<?xml version=\"1.0\"?>" + "<mensagem>"+ nome + "</mensagem>"; } 71 72 73 74 75 76 77 78 79 80 81 82 83 84 } 4. Faça a implantação no serviço no servidor de aplicação • Clique com botão direito sobre o nó do projeto e escolha Implantar 5 IFSC – Campus São José 5. Faça um teste do serviço com um cliente simples do Netbeans • Abra a pasta Web Services dentro do projeto e clique com botão direito sobre o nó do projeto e escolha Testar Web Service RESTful 2.2 Consumindo o Serviço Web RESTful: Cliente desktop 1. Crie um novo projeto dentro da categoria “Java” escolha “Aplicação Java”. • Dê o nome ClienteRESTFul para o projeto e coloque o nome da classe principal como “engtelecom.std.Principal”. Clique em finalizar 2. Crie um novo arquivo e dentro da categoria “Web Services” escolha “Cliente RESTful Java” • Informe ClienteRest como nome da classe • Informe engtelecom.std.ws como nome do pacote • Em “Selecione o recurso REST” clique no botão procurar e escolha o OlaMundo e clique em Finalizar 3. Abra o arquivo Principal no editor de código, crie uma instância da classe ClienteRest e invoque seus métodos. public class Principal { 86 87 public static void main(String[] args) { ClienteRest c = new ClienteRest(); 88 89 90 System.out.println(c.olaJSON()); System.out.println(c.olaTexto()); System.out.println(c.olaXML()); System.out.println(c.olaOutroXML("STD")); c.close(); 91 92 93 94 95 } 96 97 98 2.3 99 } Usando o curl para consumir o serviço RESTful curl -v http://localhost:8080/LabREST/OlaMundo/olamundo/ 100 101 curl -v http://localhost:8080/LabREST/OlaMundo/olamundo/joao 102 103 curl -v -X POST http://localhost:8080/LabREST/OlaMundo/olamundo -d 'nome=joao' 6 IFSC – Campus São José 3 Exercı́cio: Criando uma classe Contato para ser servida via REST 1. Crie uma classe com o nome Contato e com os atributos: nome, telefone e email. • Crie 2 construtores: 1 construtor padrão; 1 construtor que inicia todos os atributos da classe • Crie métodos getters e setters para todos os atributos 2. Coloque a anotação @XmlRootElement uma linha acima da declaração da classe (public class. . . ) 3. Modifique os métodos olaXML, olaJSON e olaOutroXML • olaXML – Modifique o tipo de retorno para Response. Crie um objeto Contato com valores estáticos e retorne este objeto • olaOutroXML – Modifique o tipo de retorno para Response. Crie um objeto Contato com valor para nome recebido via parâmetro e os demais estáticos. Retorne este objeto • olaJSON – Modifique o tipo de retorno para Response. Crie um objeto Contato com valores estáticos e retorne este objeto da seguinte forma: 104 105 Contato c = new Contato(...); return Response.ok(c).build(); 7 IFSC – Campus São José 4 Serviço REST em Python fazendo uso do framework Flask Pacotes necessários: • Se desejar instalar o flask em todo o sistema: sudo apt-get install python-pip (ou sudo easy_install pip) sudo pip install flask sudo pip install flask-httpauth 106 107 108 • Se desejar fazer a instalação no ambiente local do usuário: sudo apt-get install python-virtualenv mkdir projeto-rest cd projeto-rest virtualenv flask flask/bin/pip install flask flask/bin/pip install flask-httpauth 109 110 111 112 113 114 – Para este caso, na 1a. linha do arquivo .py, deve-se colocar: #!flask/bin/python i Nota: Alguns materiais de referência sobre a linguagem python: • https:// www.python.org/ doc/ • https:// developers.google.com/ edu/ python/ • https:// www.codecademy.com/ articles/ glossary-python A Listagem 3 apresenta um serviço REST em Python que permite interagir com uma lista de livros armazenada em memória e também um exemplo de como exigir a autenticação do usuário ao acessar um recurso do Serviço Web. O serviço REST permite: • Listar todos os livros na lista; • Consultar dados de um único livro; • Alterar dados de um livro; • Excluir um livro da lista. Na Listagem 3 acima de cada função, em forma de comentário, são apresentadas sintaxes do aplicativo curl para interagir com cada recurso do Serviço Web. Listagem 3: Aprendendo a usar os verbos HTTP com Flask e Python #!/usr/bin/python from flask import Flask, jsonify from flask import abort from flask import make_response from flask import request from flask import url_for from flask.ext.httpauth import HTTPBasicAuth auth = HTTPBasicAuth() 8 IFSC – Campus São José app = Flask(__name__) livros = [ { 'id' : 1, 'titulo' : 'Linguagem de Programacao C', 'autor' : 'Dennis Ritchie' }, { 'id' : 2, 'titulo' : 'Java como programar', 'autor' : 'Deitel & Deitel' } ] # Como invocar na linha de comando # # curl -i http://localhost:5000/livros # @app.route('/livros', methods=['GET']) def obtem_livros(): return jsonify({'livros': livros}) # Como invocar na linha de comando # # curl -i http://localhost:5000/livros/1 # @app.route('/livros/<int:idLivro>', methods=['GET']) def detalhe_livro(idLivro): resultado = [resultado for resultado in livros if resultado['id'] == idLivro] if len(resultado) == 0: abort(404) return jsonify({'livro': resultado[0]}) # Como invocar na linha de comando # # curl -i -X DELETE http://localhost:5000/livros/2 # @app.route('/livros/<int:idLivro>', methods=['DELETE']) def excluir_livro(idLivro): resultado = [resultado for resultado in livros if resultado['id'] == idLivro] if len(resultado) == 0: abort(404) livros.remove(resultado[0]) return jsonify({'resultado': True}) # Como invocar na linha de comando # # curl -i -H "Content-Type: application/json" -X POST -d '{"titulo":"O livro","autor":"Joao"}' http://localhost:5000/livros # @app.route('/livros', methods=['POST']) def criar_livro(): if not request.json or not 'titulo' in request.json: abort(400) livro = { 'id': livros[-1]['id'] + 1, 'titulo': request.json['titulo'], 'autor': request.json.get('autor', "") } livros.append(livro) return jsonify({'livro': livro}), 201 # Como invocar na linha de comando # 9 IFSC – Campus São José # curl -i -H "Content-Type: application/json" -X PUT -d '{"titulo":"Novo Titulo"}' http:// localhost:5000/livros/2 # @app.route('/livros/<int:idLivro>', methods=['PUT']) def atualizar_livro(idLivro): resultado = [resultado for resultado in livros if resultado['id'] == idLivro] if len(resultado) == 0: abort(404) if not request.json: abort(400) if 'titulo' in request.json and type(request.json['titulo']) != unicode: abort(400) if 'autor' in request.json and type(request.json['autor']) is not unicode: abort(400) resultado[0]['titulo'] = request.json.get('titulo', resultado[0]['titulo']) resultado[0]['autor'] = request.json.get('autor', resultado[0]['autor']) return jsonify({'livro': resultado[0]}) #### Autenticacao simples #### # Como invocar na linha de comando # # curl -u aluno:senha123 -i http://localhost:5000/livrosautenticado # @app.route('/livrosautenticado', methods=['GET']) @auth.login_required def obtem_livros_autenticado(): return jsonify({'livros': livros}) # Autenticacao simples @auth.get_password def get_password(username): if username == 'aluno': return 'senha123' return None @auth.error_handler def unauthorized(): return make_response(jsonify({'erro': 'Acesso Negado'}), 403) ############################## # Para apresentar erro 404 HTTP se tentar acessar um recurso que nao existe @app.errorhandler(404) def not_found(error): return make_response(jsonify({'erro': 'Recurso Nao encontrado'}), 404) if __name__ == "__main__": print "Servidor no ar!" app.run(host='0.0.0.0', debug=True) 10 IFSC – Campus São José Ao listar todos os livros seria interessante que já fosse apresentada a URI do recurso para obter detalhes sobre cada livro. A Listagem 4 ilustra um exemplo de como listar todos os arquivos do diretório corrente, onde está sendo executado o servidor, e apresenta uma solução para apresentar a URI para mais detalhes sobre cada arquivo. Listagem 4: Listando e transferindo arquivos com Flask e Python #!/usr/bin/python from flask import from flask import from flask import from flask import from flask import from flask import Flask, jsonify abort make_response request url_for send_file import os app = Flask(__name__) ### Como listar arquivos em um diretorio e em seus subdiretorios ### # Como invocar na linha de comando # # curl -i http://localhost:5000/listaarquivos # @app.route('/listaarquivos', methods=['GET']) def obtem_arquivos(): lista = [] #https://docs.python.org/2/library/os.html for root, dirs, files in os.walk('.'): for nome in files: linha = {} linha['nome'] = nome linha['tipo'] = 'arquivo' linha['caminho'] = root lista.append(linha) return jsonify({'arquivos': lista}), 201 # Como invocar na linha de comando # # curl -i http://localhost:5000/listaarquivos/{extensao-desejada} # curl -i http://localhost:5000/listaarquivos/pdf # @app.route('/listaarquivos/<extensao>', methods=['GET']) def obtem_arquivos2(extensao): lista = [] #https://docs.python.org/2/library/os.html for root, dirs, files in os.walk('.'): for nome in files: if nome.endswith(extensao): path = os.path.join(root, nome) size = os.stat(path).st_size # in bytes linha = {} linha['tamanho'] = size linha['caminho'] = root linha['nome'] = nome lista.append(linha) return jsonify({'arquivos': lista}), 201 # Como invocar na linha de comando # # curl -i http://localhost:5000/detalhesarquivo/nome.extensao # 11 IFSC – Campus São José @app.route('/detalhesarquivo/<busca>', methods=['GET']) def detalhe_arquivo(busca): lista = [] #https://docs.python.org/2/library/os.html for root, dirs, files in os.walk('.'): for nome in files: path = os.path.join(root, nome) size = os.stat(path).st_size # in bytes linha = {} linha['tamanho'] = size linha['caminho'] = root linha['nome'] = nome lista.append(linha) resultado = [resultado for resultado in lista if resultado['nome'] == busca] if len(resultado) == 0: abort(404) return jsonify({'arquivo': resultado[0]}) # Como invocar na linha de comando # # curl -i http://localhost:5000/detalhesarquivo # @app.route('/detalhesarquivo', methods=['GET']) def lista_arquivos(): lista = [] for root, dirs, files in os.walk('.'): for nome in files: path = os.path.join(root, nome) size = os.stat(path).st_size # in bytes linha = {} linha['tamanho'] = size linha['caminho'] = root linha['nome'] = nome lista.append(linha) return jsonify({'arquivos': [tornar_caminho_navegavel(arquivo) for arquivo in lista]}) # tornando os links navegaveis def tornar_caminho_navegavel(arquivo): novo_arquivo = {} for field in arquivo: if field == 'nome': novo_arquivo['uri'] = url_for('detalhe_arquivo', busca=arquivo['nome'], _external=True) novo_arquivo[field] = arquivo[field] return novo_arquivo # Como invocar (Use seu navegador web, p.e. Firefox) # # http://localhost:5000/obterarquivo/{caminho/nome.extensao} # http://localhost:5000/obterarquivo/foto.png # @app.route('/obterarquivo/<path:filename>') def download_file(filename): if '..' in filename or filename.startswith('/'): abort(404) return send_file(filename,None,False) # True envia como anexo e False como embutido. # Para apresentar erro 404 HTTP se tentar acessar um recurso que nao existe @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404) if __name__ == "__main__": print "Servidor no ar!" app.run(host='0.0.0.0', debug=True) 12 IFSC – Campus São José 5 Como invocar um comando externo com Flask e Python A Listagem 5 mostra como invocar um aplicativo externo fazendo: (1) uma chamada não bloqueante; (2) fazendo uma chamada bloqueante. Listagem 5: Invocando aplicativos do sistema operacional com Flask e Python #!/usr/bin/python from flask import from flask import from flask import from flask import import subprocess import os Flask, jsonify abort make_response request app = Flask(__name__) # Como invocar na linha de comando # Invocando o leitor de PDFs xpdf tem um bug no ubuntu # Editar sudo vi /usr/bin/xpdf +27 e acrescenter -exec no case # -z|-g|-geometry|-remote|-rgb|-papercolor|-eucjp|-t1lib|-ps|-paperw|-paperh|-upw|-exec) # # curl -i http://localhost:5000/xpdf/{arquivo.pdf} # @app.route('/xpdf/<nome>', methods=['GET']) def abre_pdf(nome): # Chamada de processo nao bloqueante - fica em background parametro = '-remote projetor ' + nome p1 = os.spawnlp(os.P_NOWAIT,"xpdf","xpdf",parametro) return jsonify({'resultado': True}), 201 # Como invocar na linha de comando # # curl -i http://localhost:5000/xpdf/{arquivo.pdf}/2 # @app.route('/xpdf/<nome>/<int:pagina>', methods=['GET']) def avanca_paginas(nome,pagina): pagina = 'gotoPage('+str(pagina)+')' # chamada de processo bloqueante, porem o xpdf termina logo apos a execucao subprocess.call(['xpdf','-remote','projetor','-exec',pagina]) return jsonify({'resultado': True}), 201 # Para apresentar erro 404 HTTP se tentar acessar um recurso que nao existe @app.errorhandler(404) def not_found(error): return make_response(jsonify({'erro': 'Recurso Nao encontrado'}), 404) if __name__ == "__main__": print "Servidor no ar!" app.run(host='0.0.0.0', debug=True) Referências 1. http://docs.oracle.com/javaee/6/tutorial/doc/bnayn.html 2. https://netbeans.org/kb/docs/websvc/jax-ws pt BR.html 3. http://docs.oracle.com/javaee/6/tutorial/doc/gijqy.html 4. http://www.vogella.com/tutorials/REST/article.html 5. https://netbeans.org/kb/docs/websvc/rest pt BR.html 6. https://netbeans.org/kb/docs/web/jsf20-crud pt BR.html 13 IFSC – Campus São José 7. http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world 8. http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask 9. http://blog.miguelgrinberg.com/post/designing-a-restful-api-using-flask-restful 10. http://blog.miguelgrinberg.com/post/writing-a-javascript-rest-client 14 IFSC – Campus São José