Al empezar a trabajar con Scala, uno de los retos que nos encontramos es el de la preparación de un entorno que nos facilite la vida y agilice los tiempos de nuestros desarrollo. En este post voy a presentar dos maneras de trabajar con Scala para el desarrollo de de aplicaciones para Apache Spark, una sin IDE y otra con IDE En primer lugar, la forma mas sencilla y directa de crear una primera aplicación con Scala es la de escribirla directamente con un editor de textos. En mi caso, he elegido SublimeText y he creado la siguiente aplicación de ejemplo:
Con guardar el archivo con la extensión .scala tendremos suficiente. Luego deberemos descargar Sbt (Super BuildTool), en mi caso para windows pero también existen versiones para Linux y Mac. Sbt es la herramienta estandard para construir aplicaciones de Scala. Crearemos una estructura de directorios del siguiente tipo:
Y generaremos la aplicación con sbt package esto nos creara un .jar de salida en la ruta ./target Para ejecutar nuestro jar en Spark, simplemente lo haremos con el comando :
Si lo que queremos es un entorno de desarrollo para crear este tipo de aplicaciones, todos aquellos que vengamos del binomio Java + Eclipse agradeceremos tener un IDE basado en eclipse para el desarrollo de aplicaciones Scala, se puede descargar de aquí. Ahora bien, si lo que queremos es construir la aplicación de Scala con Maven desde Eclipse, deberemos instalar un plugin. Aquí os dejo la url del repositorio :
Una vez instalado para crear un nuevo proyecto haremos: New --> Other --> Maven Project y seleccionamos el archetype Scala-archetype-simple Alternativamente, si el método anterior no funciona lo haremos de la siguiente manera: Crearemos un Maven Project Click derecho sobre el proyecto y seleccionamos Configure --> Add Scala Nature En el pom.xml enganchamos
Nos mostrara un error, en la pestaña de Problems --> click derecho sobre el error Quick Fix A continuación, en el archivo pom.xml, importamos las librerías de spark
org.apache.sparkspark-core_2.101.6.1
Ahora se nos mostrará otro error, causado porque el Scala IDE pone como defecto para construir el proyecto Scala 2.11 y el spark-core espera scala 2.10, buildPath y cambiamos la versión de Scala. Ahora cambiamos la carpeta src de java a scala y ya habremos terminado la configuración. Finalmente esta es la pinta que tendrá nuestro proyecto
Ya solo quedará generar el jar haciendo Run as --> Maven build y en mi caso en el parámetro Goal poniendo clean install (por ejemplo).
Básicamente voy a mostrar dos maneras, en el caso en que el WS nos devuelva un JSON y lo queramos tratar como tal en el cliente, este es el bloque de código adecuado
En el caso, altamente útil que ya vimos en el post anterior, que el WS nos devuelva una lista de objetos que nostros hayamos definido previamente, en formato JSON, el código para recibirlo y tratarlo será el siguiente
ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
Client client = Client.create(clientConfig);
WebResource webResource = client.resource("http://localhost:8080/WS_SMSReceiver/SmsService/getSmsMessage");
List list = webResource.accept("application/json").type("application/json").get(new GenericType>(){});
El objetivo de esta entrada es el describir la manera mas elegante de integrar el Web Service REST hecho con jersey en el articulo anterior para la comunicación mediante protocolo JSON con el cliente.
Vamos a mapear el objeto Java para que cada vez que hagamos referencia a el dentro de un contexto de nuestro servlet este se convierta automaticamente a formato JSON, para ello lo primero que haremos es modificar el archivo descriptor (web.xml) y le añadiremos las siguentes lineas.
Ahora crearemos una clase de la siguiente forma, es importante incluir un método sin argumentos y sobreescribir el metodo toString()
public class SendingData {
private String date;
private String valor;
public SendingData() {
}
public SendingData(String date, String valor) {
this.date = date;
this.valor = valor;
}
public String getDate() {
return date;
}
public String getValor() {
return valor;
}
public void setDate(String date) {
this.date = date;
}
public void setValor(String valor) {
this.valor = valor;
}
@Override
public String toString() {
return new StringBuffer(" date : ").append(this.date)
.append(" value : ").append(this.valor).toString();
}
}
A partir de aquí, ya podremos devolver los métodos de nuestro servicio haciendo referencia directamente una instancia del objeto (o una lista de objetos de este tipo), el servicio del artículo anterior queda así :
@Path("/SmsService")
public class SmsService {
@GET
@Path("/ping")
public Response getServerTime() {
System.out.println("RESTful Service 'MessageService' is running ==> ping");
return Response.status(200).entity("received ping on " + new Date().toString()).build();
}
@Path("/smsSend/{message}")
@GET
@Consumes (MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public SendingData smsSend(@PathParam("message") String message) throws ParseException{
SendingData sendingData = new SendingData();
sendingData.setDate(new Date().toString());
sendingData.setValor("valor de ejemplo");
return sendingData;
}
@Path("/smsSendList/{message}")
@GET
@Consumes (MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List smsSendList(@PathParam("message") String message) throws ParseException{
List list = new ArrayList();
SendingData sendingData = new SendingData();
sendingData.setDate(new Date().toString());
sendingData.setValor("valor de ejemplo");
list.add(sendingData);
sendingData = new SendingData();
sendingData.setDate(new Date().toString()+ "//2");
sendingData.setValor("valor de ejemplo 2");
list.add(sendingData);
return list;
}
}
[{"date":"Thu Feb 04 16:51:53 CET 2016","valor":"valor de ejemplo"},{"date":"Thu Feb 04 16:51:53 CET 2016//2","valor":"valor de ejemplo 2"}]
Siguiendo con la mismo filosofía, si queremos hacer la llamada desde un cliente (aplicación Java, por ejemplo) para mapear al objeto como si fuese un JSON, haremos lo siguiente. En primer lugar copiaremos al otro proyecto la clase que representa los datos ( SendingData()).
Crearemos en nuestro servicio del WS (SmsService) un método para recibir el objeto que nos envía el cliente mediante POST (también se podría hacer por GET, pero así variamos un poco). El método quedaría de esta forma :
Ahora implementamos una aplicación Java sencilla, donde en el main implementaremos las siguientes lineas de código (no olvidar importar las librerías de Jersey)
SendingData sendingData = new SendingData();
sendingData.setDate(new Date().toString());
sendingData.setValor("valor de ejemplo cliente");
ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
Client client = Client.create(clientConfig);
WebResource webResource = client.resource("http://localhost:8080/WS_SMSReceiver/SmsService/sendData");
ClientResponse response = webResource.accept("application/json").type("application/json").post(ClientResponse.class, sendingData);
if (response.getStatus() != 200) {
System.out.println("ERROR!!!!!!!!!");
}else{
String output = response.getEntity(String.class);
System.out.println("Output from Server .... "+output);
}
El resultado es que ahora el cliente puede enviar el objeto como si fuese un JSON al WS y este lo recibirá como si fuese el mismo objeto, cosa que nos ahorra estar haciendo parsers o cualquier otra filigrana.
En esta entrada voy a crear un Web Service Rest muy sencillo, que servirá como base a desarrollos mas complejos, con ayuda del framework Jersey. Para este ejemplo he utilizado la version 1.x
En primer lugar, descargamos las librerías necesarias de aqui.
Una vez añadidas al proyecto creamos una clase principal para atender las peticiones, en mi caso SmsService
package com.nextret.smsreceiver.services;
import java.util.Date;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.json.JSONObject;
@Path("/SmsService")
public class SmsService {
@GET
@Path("/ping")
public String getServerTime() {
System.out
.println("RESTful Service 'MessageService' is running ==> ping");
return "received ping on " + new Date().toString();
}
@Path("/smsSend/{message}")
@GET
@Produces("application/json")
public String smsSend(@PathParam("message") String message){
JSONObject json = new JSONObject();
json.put("Clave 1", "Valor 1");
json.put("Clave 2", "Valor 2");
return json.toString();
}
@Path("/tst")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response tst(){
JSONObject json = new JSONObject();
json.put("Clave 1", "Valor 1");
json.put("Clave 2", "Valor 2");
return Response.status(200).entity(json.toString()).build();
}
}
Veremos la salida como : "received ping on Thu Feb 04 14:10:12 CET 2016"
A continuación paso a explicar un poco el código.
El servicio SmsService, que esta definido por @Path("/SmsService"), atiende una serie de peticiones que son cada uno de los métodos implementados en esta clase.
Por un lado tenemos un simple metodo llamado ping, al que se accede mediante la ruta : @Path("/ping"). Tiene como objetivo simplemente devolver la hora para comprobar que el WS se encuentra levantado y escuchando.
Por otro lado, y a modo de ejemplo, se han implementado dos métodos mas. Por un lado el método tst() ( @Path("/tst")) devuelve un objeto JSON al cliente con un codigo 200.
El método smsSend (@Path("/smsSend/{message}")) también devuelve un Json per esta vez prescindiendo del objeto Response, simplemente con un String, en este caso, el método acepta un parámetro para su posterior tratamiento en el mismo
Si nos encontramos con el problema de tener que mapear una tabla con hibernate que no contenga una primary key, tenemos dos opcions: - Intentar inventarnos una pk si tenemos la suerte que en nuestra tabla hay algun atributo que no se repitan - O crear una composite Key, cosa que explicaré a continuación Se trata de algo muy sencillo, simplemente debemos elegir dos atributos de nuestra tabla, que sean de interés para nostros y para los datos que estamos analizando y utilizarlos como claves para las entradas, para ello modificaremos el xml de mapeo de la tabla en cuestión (hbm.xml) y introduciremos el tag .
Importante : La clase .java del mapeo debe implementar Serializable (si es que no lo hacía ya...)
En esta entrada voy a explicar como hacer un listener para las notificaciones que nos lleguen al dispositivo Android y como leer los datos que contiene. La explicación solo pretende ser un ejemplo práctico, no contiene nada de teoría. En primer lugar, vamos a crear la clase NotidicationService que extenderá de la classe abstracta NotificationListenerService, ello nos obligará a sobreescribir tres métodos. Nos centraremos en OnCreate() y onNotidicationPosted(), el primero no tiene misterio así que paso por encima sin explicación. En el segundo caso, el método onNotidicationPosted se activa cuando aparece la notificación en la barra de estado, es en ese momento cuando aprovecharemos el objeto StatusBarNotification para extraer los datos que queremos como el titulo o el texto. Luego montamos un objeto Intent que contendrá toda esa información para que pueda ser utilizada por cualquier otra activity
>Una vez hecho esto, en nuestra Activity principal implementaremos las acciones a realizar cuando salte el evento (El resultado final lo mostramos en un Toast al usuario por comodidad, se puede hacer lo que se tenga en mente con él) y en el manifiesto de android daremos los permisos que se muestran.Otra cosa importante, hay que permitir a nuestra aplicación poder leer las notificaciones, para eso, una vez instalada la aplicación en el dispositivo iremos a configuración --> Sonido y notificaciones --> Acceso a las notificacions --> Activar el check de nuestra aplicación
Vamos a ver el proceso necesario para crear un job de map reduce en hadoop. Son dos los jars necesarios que importaremos en nuestro proyecto de eclipse y que se pueden descargar de : - common - mapreduce-client-core Para implementar un Map Reduce se deben crear tres clases : Map public static class ExampleMapper extends Mapper
Ha de extender de la clase Mapper de hadoop y tendremos que sobreescribir el metodo map, que tiene como atributos key, value y context
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
Aquí se reciben los bloques de datos de información y la deberemos tratar linía a linía, para ello definiremos el comportamiento deseado de nuestro algoritmo y tambien determinaremos los valores de entrada ( key: un apuntador hacía nuestro fichero | value: cada una de las linias) y el formato de salida que se convertirá en la entrada del Reduce (context), por ejemplo :
context.write(new Text(valorTratado1), new DoubleWritable(Double.parseDouble(valorTratado2))); Reduce public static class ExampleReducer extends Reducer{ Al igual que en el caso de map, reduce debe sobreescibir el método reduce : public void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException { Dónde key sera valorTratado1 y values sera un Iterador del tipo de objecto que hayamos definido en el context de map (en nuestro caso DoubleWritable) que tendrá un array con todos aquellos valores que tengan la misma key Aquí se recogen los datos agrupados por clave que nos envia la clase map definida anteriormente y se tratan de la manera deseada. Una vez realizado este tratamiento se prepara la salida mediante el objeto context : context.write(key, new Text(valorTratado3)); Veamos unas imágenes del proceso de map reduce para un algoritmo WordCount ( el "hello world" de hadoop) :
Driver La clase driver es la encargada de ejecutar y configurar el Job de Map Reduce que hayamos creado. Para ello crearemos la clase y extenderemos de , lo que nos obligara a sobreescribir el método run dónde crearemos y configuraremos el job a ejectuar : public class ExampleDriver extends Configured implements Tool{ @Override public int run(String[] arg0) throws Exception { // TODO Auto-generated method stub return 0; } } Aquí definiremos todo lo relacionado a nuestra ejecución o job, para ello crearemos un nuevo job:
final Jobjob = Job.getInstance(getConf(),"NombreDelJob");
job.setReducerClass(ExampleReducer.class); //Sortida del map, definim tipus parametre job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(DoubleWritable.class); //Sortida Reduce, definim tipus parametre job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class); //Definimos los paths de entrada y salida segun los parametros recogidos por linia de comandos FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1]));
job.waitForCompletion(true); return 0; Es una buena práctica crear un método que borre el direcotrio de destino de los datos y ejecutarlo antes de crear la instancia de job ya que hadoop falla si ya existe esa carpeta y tiene contenido : private void deleteOutputFileIfExists(String path) throws IOException { final Path output = new Path(path); FileSystem.get(output.toUri(), getConf()).delete(output, true); } Luego crearemos el punto de entrada con un método main donde, mediante el ToolRunner crearemos una instancia de nuestra clase ExampleDriver pasandole los argumentos que hayamos recogido des de la linia de comandos : public static void main(String[] args) throws Exception { ToolRunner.run(new ExampleDriver(), args); }
Para realizar la ejecución del jar de Map reduce creado ejecutaremos :
hadoop jar MapReduceEx1.jar com.mapreduceex1.ExampleDriver /user/add/archivo.csv /user/add/salida
Donde /user/add/calidad.csvserá la ruta de nuestro hdfs (en hadoop) previamente importado y /user/add/salidaserá el directorio donde se dejará el resultado (en hadoop)
Estos dos parámetros los hemos configurado por código
Una vez realizado el import de la tabla en HDFS y la posterior creación de la misma en Hive, podemos necesitar realizar una acualización de los datos que se han generado desde el momento de nuestra creación hasta ahora, para ello utilizaremos el comando (caso sqlserver):
Un dato interesante es que la importación de una tabla en una BD relacional con sqoop hacia un HDFS se traduce en que el fichero HDFS contiene los datos separados por comas, muy similar al formato de un CSV. Si vemos que los datos se han actualizado en hive pero no en impala, refrescamos la tabla con la instrucción: REFRESH
Recientemente he dado una charla sobre un tema que estoy investigando para el trabajo final del Máster de Software Libre. Concretamente el titulo de la conferéncia ha sido : Detecció de plagi en la creació de circuits digitals en entorn educatiu, desenvolupament d'una eina de Software Lliure
Luego copiaremos el jar que se encuentra en sqljdbc_4.0/enu/sqljdbc4.jar en /var/lib/sqoop ( puede requerir un reinicio)
Con esto ya podremos realizar la conexión
Para comprobar si funciona listaremos las bases de datos ( en el caso de sqlserver, en el resto de la misma manera cambiando la información del conector ):
Si queremos realizar una importación de todas las tablas que contiene una base de datos, podemos hacer : sqoop import-all-tables --connect "jdbc:sqlserver://ip:puerto;databalse=nombreDB;username=user;password=pass" Si alguna de las tablas no tiene primaryKey tenemos dos opciones, definir por que campo diferenciaremos con -split-by o marcar que en vez de ejecutar los 4 maps que arranca por defecto sqoop, lo hagamos con un solo map con la instrucción -m 1, de esta manera no será necesario realizar los splits de la entrada del map y todo se tratara en el mismo map task. También podemos excluir alguna de las tablas que queramos al hacer toda la importación mediante el argumento --exlclude-table nombretaba1,nombretabla2... De esta manera la instrucción para importar todas las tablas de una bd a hive menos una, con alguna que no contenga primarykey, quedaría así : sqoop import-all-tables --connect "jdbc:sqlserver://ip:puerto;databalse=nombreDB;username=user;password=pass" -m 1 --exclude-table nombretabla --hive-import Los hdfs importados se guardarán en hive/warehouse y veremos que solo contienen un part-m-00000 correspondiente a cada una de las tablas que hemos importado Para crear una tabla en hive igual que la que tenemos en la base de datos haremos : sqoop create-hive-table --connect "jdbc:sqlserver://ip:puerto;databalse=nombreDB;username=user;password=pass" --table NombreDeLaTabla La instrucción creará la estructura de la misma forma que en nuestra BD relacional pero no importara los datos que esta contenga. Para cargar los datos des de un hdfs (que puede haber sido cargado previamente con sqoop en hdfs) a una tabla de hive haremos (previamente estaremos en el hive shell ) : hive>LOAD DATA INPATH "nombredelHdfs" INTO TABLE nombreDeLaTabla Si los datos a cargar están de manera local en nuestro filesystem, haremos : hive>LOAD DATA LOCAL INPATH "nombredelarchivo" INTO TABLE nombreDeLaTabla Veamos ahora un contenido un poco mas avanzado Una alternativa si no queremos importar toda una tabla y queremos hacerlo a medida mediante una query propia pasa por introducir el parametro -query en la instrucción, en este caso también deberíamos introducir --split-by, veamos un ejemplo :
sqoop import --connect "jdbc:sqlserver://192.168.7.128:1433;database=Selenium;username=sa;password=P@ssw0rd" --query 'SELECT Resultado.idResultado, Paso.idPaso, Resultado.Fecha, Resultado.TiempoPaso,Test.idTest, Resultado.idGrupTest, Browser.Nombre,Calendario.EveryXMinutes, Resultado.idError, Resultado.errorMsg from Resultado INNER JOIN Paso ON Resultado.idPaso = Paso.idPaso INNER JOIN Test ON Resultado.idTest = Test.idTest INNER JOIN Browser ON Test.Browser = Browser.id INNER JOIN TestCalendario ON Resultado.idTest = TestCalendario.idTest INNER JOIN Calendario ON TestCalendario.idCalendario = Calendario.idCalendario WHERE $CONDITIONS' --split-by Resultado.idTest --incremental append --check-column idResultado --target-dir /dfs/sqlData/Resultado