Arroz con Mango: Seam + Logger & Finder Interceptor + Lucene…

Posteado por Guido Medina en 5 September, 2008

Bueno, tuve unas necesidades en un proyecto, así que comencé por hacer un GenericDAO, luego le agregué un FinderInterceptor el cual permite que cuando hagas findByWhatever te busque un NamedQuery que esté definido en la Entidad de JPA, como tip le agregué un logger el cual loguea cada método conjunto con los parámetros pasados, aqui comienzo a explicar uno por uno:

GenericDAO; interfase que define el contrato del JPA DAO el cual implementa de forma genérica las funcionalidades del mismo; esta interfase está interceptada por un Finder del cual hablaremos más tarde; tuve que usar Lucene ya que el proyecto corre sobre MSSQL Server y el bendito no soporta translate:


package com.viavansi.fdu.persistencia.DAO;
import java.io.Serializable;
import java.util.List;

import org.apache.lucene.queryParser.ParseException;

import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionPersistencia;/**
 * Interface for data access objects.
 *
 * <p>
 * Generic Interface DAO which provides the basic contracted operations for
 * every DAO; an implementation is also provided.
 *
 * @param <T>
 * The persistent class.
 * @param <PK>
 * The class of the primary key of the persistent class.
 */
@FinderExecutor
public interface GenericDao<T, PK extends Serializable> { /**
  * Merge.
  *
  * @param persistentObject
  */
 void update(T persistentObject) throws ExcepcionPersistencia; /**
  * Make the instance persistent.
  *
  * @throws ExcepcionPersistencia
  */
 void create(T newInstance) throws ExcepcionPersistencia; /**
  * Make the object transient.
  *
  * @param persistentObject
  * @throws ExcepcionPersistencia
  */
 void delete(T persistentObject) throws ExcepcionPersistencia; /**
  * Returns a persistent object specified by its key.
  *
  * @throws ExcepcionPersistencia
  */
 T read(PK id) throws ExcepcionPersistencia; /**
  * Returns all persistent entities.
  */
 List<T> findAll() throws ExcepcionPersistencia; /**
  * Resolves and executes a finder. <p/>
  * <p>
  * This implementation uses the short name of the type class, appending a .
  * and the method name so the name of the query to look up becomes
  * Pet1.findByName if the method is findByName and the type Pet1. <p/>
  * <p>
  * An other implementation would be useful as well that does not return a
  * list but a single object.
  */
 List<T> executeFinder(String method, Object[] queryArguments)
  throws ExcepcionPersistencia; List<T> searchByText(String expresion) throws ExcepcionPersistencia,
  ParseException;

Ahora definimos el DAO que contiene injectado el EntityManager:


package com.viavansi.fdu.persistencia.DAO;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.jboss.seam.annotations.In;

import com.viavansi.framework.core.excepciones.CodigoError;
import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionDatosNoEncontrados;
import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionPersistencia;

/**
 * Generic data access object.
 *
 * @param <T>
 * The persistent class.
 * @param <PK>
 * The class of the primary key of the persistent class.
 */
public abstract class GenericJpaDaoFDU<T, PK extends Serializable> implements
  GenericDao<T, PK> {

protected Class<T> type;

@In("fduPersistenceContext")
 protected EntityManager entityManager;

public EntityManager getEntityManager() {
  return entityManager;
 }

public void setEntityManager(EntityManager entityManager) {
  this.entityManager = entityManager;
 }

public void create(T newInstance) throws ExcepcionPersistencia {
  try {
  this.entityManager.persist(newInstance);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

public void delete(T persistentObject) throws ExcepcionPersistencia {
  try {
  this.entityManager.remove(persistentObject);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

public T read(PK id) throws ExcepcionDatosNoEncontrados {
  T t = (T) this.entityManager.find(type, id);
  if (t == null) {
  throw new ExcepcionDatosNoEncontrados(
  CodigoError.ERROR_DATOS_NO_ENCONTRADOS,
  "Datos no encontrados");
  }
  return t;
 }

public void update(T transientObject) throws ExcepcionPersistencia {
  try {
  this.entityManager.merge(transientObject);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

@SuppressWarnings("unchecked")
 public List<T> findAll() throws ExcepcionPersistencia {
  try {
  return entityManager.createQuery(
  "select obj from " + this.type.getName() + " obj")
  .getResultList();
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

/**
  * Resolves and executes a finder. <p/>
  * <p>
  * This implementation uses the short name of the type class, appending a .
  * and the method name so the name of the query to look up becomes
  * Pet1.findByName if the method is findByName and the type Pet1. <p/>
  * <p>
  * An other implementation would be useful as well that does not return a
  * list but a single object.
  *
  * @throws ExcepcionPersistencia
  */
 @SuppressWarnings( { "unchecked" })
 public List<T> executeFinder(String method, Object[] queryArguments)
  throws ExcepcionPersistencia {
  try {
  String queryName = queryNameFromMethod(method);
  Query query = entityManager.createNamedQuery(queryName);
  for (int i = 0; i < queryArguments.length; i++) {
  query.setParameter(i, queryArguments[i]);
  }
  return query.getResultList();
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

/**
  * Resolves the name of the named query.
  *
  * @param finderMethod
  * "findPerson, etc."
  * @return
  */
 protected String queryNameFromMethod(String finderMethod) {
  return type.getSimpleName() + "." + finderMethod;
 }

/*
  * (non-Javadoc)
  *
  * @see
  * com.viavansi.fdu.persistencia.DAO.GenericDao#findWhere(java.lang.String)
  */
 @SuppressWarnings("unchecked")
 public List<T> searchByText(String expression)
  throws ExcepcionPersistencia, ParseException {
  PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance()
  .getPropertyUtils();
  StringBuilder builder = new StringBuilder();
  boolean firstField = true;
  for (PropertyDescriptor descriptor : propertyUtils
  .getPropertyDescriptors(type)) {
  if (firstField) {
  firstField = false;
  } else {
  builder.append(" OR ");
  }
  builder.append(descriptor.getName() + ":" + expression);
  }
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  QueryParser parser = new QueryParser("id", new ISOLatin1Analyzer());
  org.apache.lucene.search.Query luceneQuery = parser.parse(builder
  .toString());
  Query query = fullTextEntityManager.createFullTextQuery(luceneQuery,
  type);
  List result = query.getResultList();
  if (result.size() == 0) {
  throw new ExcepcionDatosNoEncontrados();
  }
  return result;
 }

/**
  *
  * @param <T>
  * @param expression
  * @param entityManager
  * @param type
  * @return
  * @throws ExcepcionPersistencia
  * @throws ParseException
  */
 @SuppressWarnings("unchecked")
 public static <T> List<T> searchByText(String expression,
  EntityManager entityManager, Class<T> type)
  throws ExcepcionPersistencia, ParseException {
  PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance()
  .getPropertyUtils();
  StringBuilder builder = new StringBuilder();
  boolean firstField = true;
  for (PropertyDescriptor descriptor : propertyUtils
  .getPropertyDescriptors(type)) {
  if (firstField) {
  firstField = false;
  } else {
  builder.append(" OR ");
  }
  builder.append(descriptor.getName() + ":" + expression);
  }
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  QueryParser parser = new QueryParser("id", new ISOLatin1Analyzer());
  org.apache.lucene.search.Query luceneQuery = parser.parse(builder
  .toString());
  Query query = fullTextEntityManager.createFullTextQuery(luceneQuery,
  type);
  return query.getResultList();
 }

}

Nuestro 1er DAO para una Entidad; como verán, se apoya en el GenericDAO, y en su implementación:


package com.viavansi.fdu.persistencia.DAO;
import java.util.List;

import org.jboss.seam.annotations.Name;

import com.viavansi.fdu.persistencia.VO.ProcessInfoVO;

/**
 * @author gmedina
 *
 */
@Name("processInfoDAO")
public class ProcessInfoDAO extends GenericJpaDaoFDU<ProcessInfoVO, Long> {

public ProcessInfoDAO() {
  this.type = ProcessInfoVO.class;
 }

public List<ProcessInfoVO> findByProcessName(String name) {
  return null;
 }

public List<ProcessInfoVO> findByJbpmName(String jbpmName) {
  return null;
 }

public List<ProcessInfoVO> findByArea(String area) {
  return null;
 }

}

Vamos a mostrar nuestro Finder interceptor el cual sigue el Interceptor pattern y está soportado por Seam; muy sencillo, si el método se llama findByWhatever el busca un named query llamado findByWhatever, en tu implementación del DAO solo debes retornar null, si retornas algo entonces el finder se anula automáticamente:


package com.viavansi.fdu.interceptor;
import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.intercept.InvocationContext;

import com.viavansi.fdu.persistencia.DAO.GenericDao;

/**
 * @author gmedina
 *
 */
public class FinderInterceptor { /**
  *
  * @param invocation
  * @return
  * @throws Throwable
  */
 @AroundInvoke
 @SuppressWarnings("unchecked")
 public Object executeFinder(InvocationContext invocation) throws Throwable {
  String methodName = invocation.getMethod().getName();
  if (methodName.startsWith("findBy")) {
  GenericDao dao = (GenericDao) invocation.getTarget();
  Object result = invocation.proceed();
  return result == null ? dao.executeFinder(methodName, invocation
  .getParameters()) : result;
  } else
  return invocation.proceed();
 }

Nuestro persistence.xml con la configuración de Lucene que necesita:


<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence
 xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” version=”1.0″
 xsi:schemaLocation=”http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd“>
 <!–
  Conexión por defecto de la aplicación utilizando POOL de conexiones
 –>
 <persistence-unit name=”fduPersistenceUnit”
  transaction-type=”RESOURCE_LOCAL”>
  <provider>org.hibernate.ejb.HibernatePersistence</provider>
  <jta-data-source>java:comp/env/jdbc/fdu</jta-data-source>
  <class>com.viavansi.fdu.persistencia.VO.UsuarioVO</class>
  <class>com.viavansi.fdu.persistencia.VO.MiembroVO</class>
  <class>com.viavansi.fdu.persistencia.VO.RolVO</class>
  <class>com.viavansi.fdu.persistencia.VO.ProcessInfoVO</class>
  <properties>
  <property name=”hibernate.show_sql” value=”true” />
  <property name=”hibernate.dialect” value=”org.hibernate.dialect.SQLServerDialect” />
  <property name=”hibernate.cache.provider_class” value=”org.hibernate.cache.EhCacheProvider” />
  <!–
  Configuración para el soporte de prefijos en Hibernate. Estrategia
  para generación de nombres de tablas asociadas a anotaciones Table
  EJB3.0.
  –>
  <property name=”hibernate.ejb.naming_strategy”
  value=”com.viavansi.framework.persistencia.jpa.NamingStrategy” />
  <property name=”hibernate.search.default.directory_provider”
  value=”org.hibernate.search.store.FSDirectoryProvider” />
  <property name=”hibernate.search.default.indexBase”
  value=”/Java/lucene/fdu/app” />
  </properties>
 </persistence-unit> </persistence>

Finalmente nuestro DAO el cual tiene anotaciones de JPA y Hibernate Search/Lucene


package com.viavansi.fdu.persistencia.VO;
import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.search.annotations.Analyzer;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;

import com.viavansi.fdu.persistencia.DAO.ISOLatin1Analyzer;

/**
 * @author gmedina
 *
 */
@NamedQueries( {
  @NamedQuery(name = "ProcessInfoVO.findByProcessName", query = "from ProcessInfoVO processInfo where processInfo.processName = ?"),
  @NamedQuery(name = "ProcessInfoVO.findByJbpmName", query = "from ProcessInfoVO processInfo where processInfo.jbpmName = ?"),
  @NamedQuery(name = "ProcessInfoVO.findByArea", query = "from ProcessInfoVO processInfo where processInfo.area = ?") })
@Indexed
@Analyzer(impl = ISOLatin1Analyzer.class)
@Entity
@Table(name = "`${PREFIX_FDU}PROCESS_INFO`")
public class ProcessInfoVO implements Serializable {

private static final long serialVersionUID = 1570433106072090149L;

private Long id;
 private String processName;
 private String jbpmName;
 private Date startDate;
 private Date dueDate;
 private boolean active;
 private String area;

/**
  * @return the id
  */
 @Id
 @DocumentId
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 public Long getId() {
  return id;
 }

/**
  * @param id
  * the id to set
  */
 public void setId(Long id) {
  this.id = id;
 }

/**
  * @return the processName
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "PROCESS_NAME", unique = true, nullable = false, insertable = true, updatable = true, length = 255)
 public String getProcessName() {
  return processName;
 }

/**
  * @param processName
  * the processName to set
  */
 public void setProcessName(String processName) {
  this.processName = processName;
 }

/**
  * @return the jbpmName
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "JBPM_NAME", unique = true, nullable = false, insertable = true, updatable = true, length = 255)
 public String getJbpmName() {
  return jbpmName;
 }

/**
  * @param jbpmName
  * the jbpmName to set
  */
 public void setJbpmName(String jbpmName) {
  this.jbpmName = jbpmName;
 }

/**
  * @return the startDate
  */
 @Field(index = Index.UN_TOKENIZED)
 @DateBridge(resolution = Resolution.DAY)
 @Temporal(TemporalType.DATE)
 @Column(name = "START_DATE", unique = false, nullable = true, insertable = true, updatable = true)
 public Date getStartDate() {
  return startDate;
 }

/**
  * @param startDate
  * the startDate to set
  */
 public void setStartDate(Date startDate) {
  this.startDate = startDate;
 }

/**
  * @return the dueDate
  */
 @Field(index = Index.UN_TOKENIZED)
 @DateBridge(resolution = Resolution.DAY)
 @Temporal(TemporalType.DATE)
 @Column(name = "DUE_DATE", unique = false, nullable = true, insertable = true, updatable = true)
 public Date getDueDate() {
  return dueDate;
 }

/**
  * @param dueDate
  * the dueDate to set
  */
 public void setDueDate(Date dueDate) {
  this.dueDate = dueDate;
 }

/**
  * @return the active
  */
 @Column(name = "ACTIVE", unique = false, nullable = false, insertable = true, updatable = true)
 public boolean isActive() {
  return active;
 }

/**
  * @param active
  * the active to set
  */
 public void setActive(boolean active) {
  this.active = active;
 }

/**
  * @return the area
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "AREA", unique = false, nullable = false, insertable = true, updatable = true, length = 100)
 public String getArea() {
  return area;
 }

/**
  * @param area
  * the area to set
  */
 public void setArea(String area) {
  this.area = area;
 }

/*
  * (non-Javadoc)
  *
  * @see java.lang.Object#hashCode()
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((id == null) ? 0 : id.hashCode());
  return result;
 }

/*
  * (non-Javadoc)
  *
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
  return true;
  }
  if (obj == null || !(obj instanceof ProcessInfoVO)) {
  return false;
  }
  ProcessInfoVO other = (ProcessInfoVO) obj;
  if (id == null) {
  if (other.id != null) {
  return false;
  }
  } else if (!id.equals(other.id)) {
  return false;
  }
  return true;
 }

}

Nota: Lucene necesita construir un indice inicial, les pegaré un pedazo de código de como hacer esto:


@SuppressWarnings("unchecked")
 public void reIndexLuceneDB() {
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  Class[] entityClasses = { UsuarioVO.class, RolVO.class,
  ProcessInfoVO.class };
  for (Class entityClass : entityClasses) {
  for (Object object : entityManager.createQuery(
  "select obj from " + entityClass.getName() + " obj")
  .getResultList()) {
  fullTextEntityManager.index(object);
  }
  }
 }

Bueno, he pegado tanto código que explicarlo todo en un solo post es difícil, asi que manden sus preguntas.

Que lo disfruten.

Guido.

www.viafirma.com

Posteado por Félix García Borrego en 3 September, 2008

Debido al gran interés que esta generando Viafirma, nuestra plataforma de Autenticación y Firma Digital en su versión comercial, hemos creado viafirma.com, en la que se recoge toda la información relacionada con las versiones comerciales de la plataforma ( Versión Standard y Advanced).

 ¿Por qué una versión comercial de Viafirma?

En la mayoría de los casos, nuestros clientes desean utilizar Viafirma sobre aplicaciones que no son compatibles con GPL; además, nos exigen soporte técnico, mantenimiento, plazos para nuevos desarrollos, tiempos de respuesta, una documentación completa, y en resumen, unas garantías que no ofrece la versión GPL. Por este motivo y para poder financiar los nuevos desarrollos a los que nos estamos enfrentando, hemos creado el portal  viafirma.com.

¿Implica esto un cambio de filosofía de la versión Software Libre?

No. Tenemos tanto que agradecer a la comunidad que nuestra intención sigue siendo apostar por una plataforma libre . De hecho acabamos de liberar como GPL la nueva versión de Viafirma 1.3.2, si lo desean pueden consultar la lista completa de nuevas funcionalidades en viafirma.org.

 

Nuevo cocktail: Windows Vista con una pizca de iTunes y Quicktime, precio recomendado “Error 46″

Posteado por Manuel Navarro Almuedo en 3 September, 2008

Hola buenas a tod@s,

esta vez os escribo para comentaros un quebradero de cabeza más que me ha dado Windows Vista e iTunes (con Quicktime), un par de problemas:

1) Cada vez que arranco el iTunes, se vuelve a reinstalar.

2) Se produce un error de ActiveX al abrir el Quick Time Player: Error 46

Para el primero de los problemas, la solución fue un poco drástica: eliminar el acceso directo del iTunes que tenemos en el Escritorio y crear un nuevo acceso directo al iTunes.exe que tenemos en nuestra carpeta de “%Program Files%/iTunes”, lo colocamos en nuestro Escritorio y aquí no ha pasado nada :-D

Para solucionar el segundo error ya tenemos que hacer un par de cosillas más.

Primero os comento cuál es la causa de este error:
Parece ser que al instalar QuickTime Player las claves introducidas en el registro  de Windows Vista no adquieren permisos de acceso correctos. Exactamente lo que ocurre es que nuestro usuario (sea Administrador o no) no tiene acceso ni siquiera de lectura a dichas claves.

Esto os lo cuento yo y queda muy bonito ¿verdad?, pero ¿cómo podeis comprobar esto vosotros mismos?
Pues descárguense el programa Process Monitor  de Microsoft. Tras instalarlo y dejar abierta la aplicación, intenten abrir QTPlayer y miren los errores que se muestran en el Proces Monitor producen al intentar abrir el QuickTime Player, seguro les aparecerá “ACCESS DENIED” a distintas claves del registro (podeis filtrar las lineas que se muestran).

Pues bien, parece que tenemos claro ahora cuál es el problema, ¿cómo “repcuperar” los permisos de lectura sobre dichas claves?

Si hacemos lo típico, ejecutar “regedit” e intentar modificar los permisos manualmente veremos que no tenemos autorización para realizar estos cambios en esas claves afectadas. ¡Vaya chasco!

Bien, busquemos otra forma de modificar estos permisos. Usemos ahora otro programa de Microsoft llamado SubInACL: esta es una herramienta de administración, para modificar y consultar distintos parámetros del sistema como claves del registro, servicios, etc., pero vamos al grano.

Con este programa tenemos ahora que modificar los permisos asignados a estas claves que no pudimos modificar con “regedit.exe” .

Una vez descargado e instalado, debemos copiar el script que contiene Script.zip y descomprimir el .cmd en la carpeta”%Program Files%\Windows Resource Kits\Tools“, que es donde se debe haber instalado SubInACL.
Este script es parecido a otros que aparecen en algunos foros, pero este en concreto contiene algunas correcciones realizadas por mi y adaptado a Windows Vista en Español, no en Inglés. Recomiendo hacer una copia de seguridad del registro antes de ejecutar nada, siempre es conveniente trabajar sobre seguro.

A continuación, abramos una terminal de consola (Inicio / Ejecutar / cmd.exe para los no iniciados) y vayamos a la carpeta donde copiamos el script. Ejecutemos en la consola “resetSpanish.cmd” y veamos en el log mostrado que no salgan errores:

Done:        X, Modified        X, Failed        0, Syntax errors        0

lo importante es que “Failed”  y “Syntax errors” salgan a cero.

Este script hay que lanzarlo con un usuario Administrador y recomiendo desactivar el sistema de protección de cuentas de que dispone Windows Vista (Panel de Control / Cuentas de Usuario / Activar o desactivar el control de cuentas de usuario),
una vez lanzado el script podeis volverlo a activar.

Una vez hecho esto, podremos abrir “QuickTime Player” sin obtener el dichoso error 46.

Y recordad: siempre tenemos una alternativa en LINUX …

Namasté.

Previous Articles

Viavansi ya es proveedor oficial de soluciones OpenCms

Posteado por Javier Echeverría Usua en 28 August, 2008

Códigos INE de municipios

Posteado por Javier Echeverría Usua en 29 July, 2008

Eclipse 3.1.2 SDK, un programa ¿de astronomía?

Posteado por Javier Echeverría Usua en 29 July, 2008

naturalmente accesible

Posteado por Beni en 27 July, 2008

¿Software libre? Depende.

Posteado por Jorge Torres Chacón en 26 July, 2008

¿Cuánto pierde Movistar?

Posteado por Javier Echeverría Usua en 17 July, 2008

Caminos de ida y vuelta

Posteado por Javier Echeverría Usua en 10 July, 2008

Bienvenido a Xnoccio

Xnoccio es el blog de Viavansi, una empresa con ganas de hacer las cosas bien. Aquí escribiremos sobre lo que se nos ocurra, pero siempre intentando centrar nuestra verborrea en temas tecnológicos.