martes, 18 de diciembre de 2012

Cálculo de días vividos desde fecha de nacimiento

Cómo calcular el total de días transcurridos desde la fecha de nacimiento a la fecha actual.


import java.util.Calendar;
import java.util.GregorianCalendar;

public class TotalLifeTimeDays {
 
 public int getLifeTimeDays(int birthYear, int birthMonth, int birthDay) {
  
  Calendar birthDayCal = new GregorianCalendar();
     Calendar currentDayCal = Calendar.getInstance();

  birthDayCal.set(birthYear, birthMonth, birthDay);    
  return (int)((currentDayCal.getTime().getTime() - birthDayCal.getTime().getTime())
     / (1000 * 60 * 60 * 24));  
 }
 
 public static void main(String[] args) {
  TotalLifeTimeDays totalLifeTimeDays = new TotalLifeTimeDays();
  int totalDays = totalLifeTimeDays.getLifeTimeDays(1982, 11, 20);
  System.out.println("Total days lived: " + totalDays);
 }
}

Android: Juego de Matemáticas

Como práctica hice este pequeño programa que intenta ser un juego de matemáticas. Operaciones básicas de suma, resta, multiplicación y división, son generadas al azar con la respuesta válida y dos respuestas incorrectas. Cada respuesta correcta acumula un punto.


import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;

import com.example.mytoolbox.utilities.NumberUtilities;

public class MathRallyActivity extends Activity implements OnClickListener {
 
 public enum Operation {
  SUM, DIVISION, MULTIPLICATION, SUBSTRACT  
 }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_math_rally);
        
        Button buttonOption1 = (Button) findViewById(R.id.buttonMath1);
        Button buttonOption2 = (Button) findViewById(R.id.buttonMath2);
        Button buttonOption3 = (Button) findViewById(R.id.buttonMath3);
        ImageButton buttonHome = (ImageButton) findViewById(R.id.imageButtonHome);
        
        buttonOption1.setOnClickListener(this);
        buttonOption2.setOnClickListener(this);
        buttonOption3.setOnClickListener(this);
        buttonHome.setOnClickListener(this);
        
        createOperation();        
    }
    
    private void createOperation() {
     Operation operation = generateRandOperation();
     
     int operand1 = NumberUtilities.generateRandNumber(2, 100);
     int operand2 = NumberUtilities.generateRandNumber(2, 100);
     
     String operationText = String.valueOf(operand1) + " " +
       getOperationString(operation) + " " + String.valueOf(operand2) + "?";
     
     TextView textViewOperation = (TextView) findViewById(R.id.textViewOperation);
     textViewOperation.setText(operationText);
     float rightValue = calculateRightValue(operation, operand1, operand2);
     
     rightBox = NumberUtilities.generateRandNumber(1, 3);
     float randWrongValue1 = NumberUtilities.generateRandNumber(2);
     float randWrongValue2 = NumberUtilities.generateRandNumber(2);
     
     Button buttonOption1 = (Button) findViewById(R.id.buttonMath1);
        Button buttonOption2 = (Button) findViewById(R.id.buttonMath2);
        Button buttonOption3 = (Button) findViewById(R.id.buttonMath3);
     
     switch (rightBox) {
      case 0:
       buttonOption1.setText(String.valueOf(rightValue));
       buttonOption2.setText(String.valueOf(randWrongValue1));
       buttonOption3.setText(String.valueOf(randWrongValue2));
       break;
      case 1:
       buttonOption2.setText(String.valueOf(rightValue));
       buttonOption1.setText(String.valueOf(randWrongValue1));
       buttonOption3.setText(String.valueOf(randWrongValue2));
       break;
      case 2:
       buttonOption3.setText(String.valueOf(rightValue));
       buttonOption1.setText(String.valueOf(randWrongValue1));
       buttonOption2.setText(String.valueOf(randWrongValue2));
       break;
     }     
    }
    
    private float calculateRightValue(Operation oper, int operand1, int operand2) {
     float calculation = 0;
     
     if (oper == Operation.SUM) {
      calculation = operand1 + operand2;
     } else if (oper == Operation.MULTIPLICATION) {
      calculation = operand1 * operand2;
     } else if (oper == Operation.SUBSTRACT) {
      calculation = operand1 - operand2;
     } else if (oper == Operation.DIVISION) {
      calculation = operand1 / operand2;
     }
     
     return calculation;
    }
    
    private String getOperationString(Operation oper) {
     String operationText = "";
     if (oper == Operation.SUM) {
      operationText = "+";
     } else if (oper == Operation.MULTIPLICATION) {
      operationText = "*";
     } else if (oper == Operation.SUBSTRACT) {
      operationText = "-";
     } else if (oper == Operation.DIVISION) {
      operationText = "/";
     }
     return operationText;
    }
    
    private Operation generateRandOperation() {
     int rand = NumberUtilities.generateRandNumber(1, 4);
     Operation operation = null;
     
     switch(rand) {
      case 0:
       operation = Operation.SUM;
       break;
      case 1:
       operation = Operation.DIVISION;
       break;
      case 2:
       operation = Operation.MULTIPLICATION;
       break;
      case 3:
       operation = Operation.SUBSTRACT;
       break;
     }
     return operation;
    }
  
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_math_rally, menu);
        return true;
    }

 @Override
 public void onClick(View view) {
  boolean correct = false;
  boolean play = true;
  
  switch(view.getId()) {
   case R.id.buttonMath1:
    if (rightBox == 0) {
     correct = true;
    }
    break;
   case R.id.buttonMath2:
    if (rightBox == 1) {
     correct = true;
    }
    break;
   case R.id.buttonMath3:
    if (rightBox == 2) {
     correct = true;
    }
    break;
   case R.id.imageButtonHome:
    startActivity(new Intent(MathRallyActivity.this, ChooseActivity.class));
    play = false;
    break;
  }
  if (correct) {
   points++;
   updateScore();
  } if(play) {
   createOperation();
  }
 }
 
 private void updateScore() {
  EditText editText = (EditText) findViewById(R.id.editTextPoints);
  editText.setText(String.valueOf(points));
 }

 private int rightBox;
 private int points;
}

miércoles, 5 de diciembre de 2012

JSP: Obtener la fecha actual

Dejo este pequeño código mostrando como se puede obtener la fecha actual usando código JSP. En ocasiones es preferible utilizar código del lado del servidor, en lugar de usar JavaScript, para no depender de que un usuario tiene bien configurada la fecha en su ordenador, o si hay diferencias de huso horario.

<%@ page import="java.util.*" %>
<%@ page import="java.text.SimpleDateFormat"%>
 
<%
   Date dNow = new Date();
   SimpleDateFormat ft = 
   new SimpleDateFormat ("MM/dd/yyyy");
   String currentDate = ft.format(dNow);
%>

<p>The current date is: <%=currentDate%></p>

viernes, 23 de noviembre de 2012

Android: Chequear conectividad con WiFi

Para poder chequear la conectividad con WiFi primero hay que agregar los permisos respectivos en el manifiesto de Android:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mytoolbox"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="9"
        android:targetSdkVersion="15" />

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
 ...
 
</manifest>

Aquí dejo un código que verifica la conectividad. Si no existe entonces despliega un mensaje indicando al usuario que debe haber conectividad para poder usar la aplicación. Seguidamente termina la aplicación.

private void checkWiFiConnectivity() {
  ConnectivityManager connMan =  (ConnectivityManager) 
       getSystemService(Context.CONNECTIVITY_SERVICE);
  boolean wifiIsOn = connMan.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
        .isConnectedOrConnecting();
  Log.d("GABO", "WiFi is : " + wifiIsOn);
  
  if(!wifiIsOn) {
   finishApplication();
  }  
}

private void finishApplication() {
  AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
  builder.setMessage("You need WiFi connection to access this App!");
  builder.setCancelable(false);
     
  builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
   
   @Override
   public void onClick(DialogInterface dialog, int which) {
       MainActivity.this.finish();    
   }
  });
     
  AlertDialog alert = builder.create();
  alert.show();  
}

miércoles, 21 de noviembre de 2012

MongoDB bajo contexto


Un par de semanas atrás asistí como es de costumbre cuando se da la oportunidad, a una de las charlas del Grupo de Usuarios de Java de Costa Rica (CRJUG). En esta ocasión organizado por los amigos de BodyBuilding.com. En esta ocasión nos dieron una charla introductiva al mundo de MongoDB, un motor no-sql de base de datos que usan en su sitio web. Disfruté mucho el estilo relajado de las presentaciones. Algo así como “Por programadores para programadores”. Nos mostraron las principales diferencias entre este nuevo paradigma en comparación con el tradicional paradigma relacional. Creo que desde un principio fueron bastante transparentes afirmando que no habían venido en una “misión evangelística” de la tecnología. Ellos saben al igual que muchos que el uso de este nuevo paradigma es para determinados propósitos. No es para substituir las bases de datos relacionales con sus características típicas: seguridad, transaccionabilidad, integridad referencial, etc. Como en muchos otros ámbitos en ciencias de la computación donde siempre existe una tensión constante en tratar de balancear todos los elementos claves de un sistema: seguridad, robusticidad, velocidad, confiabilidad, etc,  las bases de datos no escapan de esta batalla y por tanto han comenzado a aparecer nuevos métodos para inclinar la balanza sobre un grupo de características deseables en ciertos contextos, como lo es la escalabilidad.

Estoy enfatizando este punto por algo interesante que observé durante la presentación. Debido al sistema informal de la presentación, siempre hubo apertura para hacer preguntas y comentarios en todo momento y entoces algunos participantes en varias ocasiones insistieron en comparar esta solución con la típica solución de base de datos relacional. Esto en principio está bien para señalar el por qué de las diferencias, pero no obstante lo que encontré interesante fue lo que percibí la manera en que se hacían los aportes. Como con cierta duda y escepticismo, como “no puedo creer que las reglas de toda una vida se estén violando”. Por supuesto que nadie dijo esto de manera literal, pero solo describo mi percepción de la forma defensiva en que se hacía. Por ejemplo un participante comentó que Oracle atacaba un problema de una manera distinta sin abandonar el paradigma relacional. En otra ocasión hubo una breve argumentación de por qué MongoDB no continuó con un lenguaje de consulta similar el tipo SQL, porque después de todo, toda la comunidad de desarrolladores ya está acostumbrada a usar este lenguaje.

Esta reacción de una parte del público es interesante, pero no del todo sorprendente. Las bases de datos relacionales han estado con nosotros por más de 40 años. Son más viejas que muchos de nosotros. Es por ellos que a muchos les cuesta creer que sea necesario reinventar el agua tibia. Es similar a que apareciera un nuevo movimiento defendiendo las virtudes de un lenguaje de programación que abandona la programación orientada a objetos. Sería completamente normal para muchos mirar esto con ojos totalmente escépticos. En mi humilde opinión considero que la experimentación en nuevos paradigmas es completamente necesario para desarrollar innovación en cualquier ámbito científico y tecnológico. Y también creo por otro lado que no debemos conducirnos de una manera “hipsteriana”, intentando probar y hasta siendo promotores de las últimas tecnologías solo por lucir “cool”, llegando al punto inclusive de etiquetar a otros como anticuados por no usar lo más reciente en frameworks, librerías, metodologías, etc. Las bases de datos relacionales están a décadas creo yo de entrar en una fase de retiro, pero igual debemos comenzar a considerar las nuevas opciones que ya están comenzando aparecer en el escenario desde hace algunos años atrás.

En este video bastante gracioso (la verdad cada vez que lo veo me causa risa de nuevo), se señala de manera cómica este último punto que menciono, donde tenemos personas bastante creídas y apresuradas para descartar soluciones sólidas y probadas solo porque hay algo nuevo de moda. Jugando como dije al hipster pero con tecnologías de desarrollo. No creo que este video sea una crítica en específico a MongoDB, sino más bien una crítica a la gente que no entiende el contexto apropiado de ciertas tecnologías y creen que es una zapato que le queda bien a todo mundo. Si uno tiene una aplicación sencilla sin grandes problemas de concurrencia por el momento, uno no debería estar preocupándose muy al principio por escalabilidad. Sí dejar un diseño no acoplado solamente a una base de datos, pero no entrar en pánicos innecesarios. Sobre arquitecturar una aplicación puede causar más problemas de los que puede solucionar. Pero ese es otro tema que comentaré después a su debido tiempo.


viernes, 16 de noviembre de 2012

Evitar modo IE7 Standards

Estabamos trabajando un sitio, donde por razones fuera de nuestro control, no teníamos suficiente tiempo para pulir el sitio para soportar Internet Explorer 7 (que es como el nuevo IE6 de nuestros tiempos). El sitio funcionaba bien en los navegadores serios como Chrome, FF y IE8, pero por alguna razón siempre el navegador, a pesar de ser versión 8, cargaba el modo de IE7 Standards.


Un poquito de "googlismo" y encontré que hay un meta tag para forzar al navegador usar la versión más alta:

<meta http-equiv="X-UA-Compatible" content="IE=edge">

lunes, 5 de noviembre de 2012

CakePHP: Salvar y Mostrar

En este pequeño código muestro como guardar los datos de un formulario, en este caso correspondientes a un anuncio, e inmediatamente redirigir a la página para ver los datos guardados.

<?php

//...

class AnunciosController extends AppController {
  
 public function add() {  
        if ($this->request->is('post')) {
         if ($this->Anuncio->save($this->request->data)) {
              $this->Session->setFlash('El anuncio fue guardado: ');
              $inserted_id = $this->Anuncio->id;              
              $this->redirect(array('action' => 'view', $inserted_id ));
         } else {
             $this->Session->setFlash('No se pudo guardar el anuncio');
         }
  }
  // ...
    }
    
 // ...
 
 public function view($id) {
        $this->Anuncio->id = $id;
        $this->set('anuncio', $this->Anuncio->read());
    }    
}

La línea clave es la que hace el redireccionamiento. Aquí tocó quebrarme bastante la cabeza para determinar como pasaba el id del recién insertado anuncio.

Android: "Piedra, papel y tijeras"

Aquí dando mis primeros gateos en el mundo de desarrollo de Android. En esta simple aplicación ejemplifico el uso de:
  • Buttons
  • Click events
  • Image Views
  • Alert dialogs
La aplicación es un simple juego de"Piedra, Papel y Tijeras". El usuario hace la selección entre las clásicas tres opciones, y luego Android hace una selección aleatoria. De acuerdo a las básicas reglas universales de este conocido juego, el resultado es determinado y mostrado en un mensaje de alerta.

Este es el layout básico del juego. Las dos imágenes de signo de interrogación son para desplegar las dos selecciones. Las otras tres imágenes son para que el usuario haga la selección.



















XML del layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:padding="@dimen/padding_medium"
        android:text="Rock, Paper, Scissors!"
        tools:context=".RockPaperScissors" />

    <Button
        android:id="@+id/buttonRock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="147dp"
        android:layout_marginRight="20dp"
        android:layout_toLeftOf="@+id/textView1"
        android:background="@drawable/rock_small" />

    <Button
        android:id="@+id/buttonPaper"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/buttonRock"
        android:layout_alignBottom="@+id/buttonRock"
        android:layout_centerHorizontal="true"
        android:background="@drawable/paper_small" />

    <Button
        android:id="@+id/buttonScissors"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/buttonPaper"
        android:layout_alignBottom="@+id/buttonPaper"
        android:layout_marginLeft="23dp"
        android:layout_toRightOf="@+id/textView1"
        android:background="@drawable/scissors_small" />

    <ImageView
        android:id="@+id/imageViewAnswerUser"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView1"
        android:layout_marginTop="26dp"
        android:layout_toLeftOf="@+id/buttonPaper"
        android:src="@drawable/question" />

    <ImageView
        android:id="@+id/ImageViewAnswerAndroid"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@+id/buttonScissors"
        android:layout_alignTop="@+id/imageViewAnswerUser"
        android:src="@drawable/question" />

    <ImageButton
        android:id="@+id/imageButtonHome"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@+id/buttonPaper"
        android:layout_below="@+id/buttonPaper"
        android:layout_marginTop="38dp"
        android:src="@drawable/home" />

</RelativeLayout>

código de la actividad:
package com.example.mytoolbox;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;

public class RockPaperScissors extends Activity implements OnClickListener {
 
 public enum Option {
  ROCK, PAPER, SCISSORS
 }
 
 public enum Result {
  WIN, LOSS, DRAW  
 }
 
 private Option userSelection;
 private Result gameResult;
 

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_rock_paper_scissors);
        
        Button buttonRock = (Button) findViewById(R.id.buttonRock);
        Button buttonpaper = (Button) findViewById(R.id.buttonPaper);
        Button buttonScissors = (Button) findViewById(R.id.buttonScissors);
        ImageButton buttonHome = (ImageButton) findViewById(R.id.imageButtonHome);
        
        // Set click listening event for all buttons.
        buttonRock.setOnClickListener(this);
        buttonpaper.setOnClickListener(this);
        buttonScissors.setOnClickListener(this);
        buttonHome.setOnClickListener(this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_rock_paper_scissors, menu);
        return true;
    }

 @Override
 public void onClick(View v) {
  
  ImageView imageView = (ImageView) findViewById(R.id.imageViewAnswerUser);
  boolean play = true;
  
  switch (v.getId()) {
   case R.id.buttonRock:
    userSelection = Option.ROCK;
    imageView.setImageResource(R.drawable.rock);
    break; 
   case R.id.buttonPaper:
    userSelection = Option.PAPER;
    imageView.setImageResource(R.drawable.paper);
    break;
   case R.id.buttonScissors:
    userSelection = Option.SCISSORS;
    imageView.setImageResource(R.drawable.scissors);
    break;
   case R.id.imageButtonHome:
    startActivity(new Intent(RockPaperScissors.this, ChooseActivity.class)); // To go home.
    play = false;
    break;
  }
  
  if(play) {
   play();
   showResults();
  }
 }

 private void showResults() {
  AlertDialog.Builder builder = new AlertDialog.Builder(RockPaperScissors.this);     
     builder.setCancelable(false);     
     builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {   
   @Override
   public void onClick(DialogInterface dialog, int which) {
    // Do nothing   
   }
  });
     
     // Sets the right message according to result.
     if(gameResult == Result.LOSS) {
      builder.setMessage("You Loose!");
     } else if(gameResult == Result.WIN) {
      builder.setMessage("You Win!");
     } else if(gameResult == Result.DRAW) {
      builder.setMessage("It's a draw!");
     } 
     
     AlertDialog alert = builder.create();
     alert.show();  
    } 

 private void play() {
  // Generates a random play.
  int rand =  ((int)(Math.random() * 10)) % 3;
  Option androidSelection = null;
  ImageView imageView = (ImageView) findViewById(R.id.ImageViewAnswerAndroid);
  
  // Sets the right image according to random selection.
  switch (rand) {
   case 0:
    androidSelection = Option.ROCK;
    imageView.setImageResource(R.drawable.rock);
    break;
   case 1:
    androidSelection = Option.PAPER;
    imageView.setImageResource(R.drawable.paper);
    break;
   case 2:
    androidSelection = Option.SCISSORS;
    imageView.setImageResource(R.drawable.scissors);
    break;
  }
  // Determine game result according to user selection and Android selection.
  if(androidSelection == userSelection) {
   gameResult = Result.DRAW;
  }
  else if(androidSelection == Option.ROCK && userSelection == Option.SCISSORS) {
   gameResult = Result.LOSS;
  }
  else if(androidSelection == Option.PAPER && userSelection == Option.ROCK) {
   gameResult = Result.LOSS;
  } 
  else if(androidSelection == Option.SCISSORS && userSelection == Option.PAPER) {
   gameResult = Result.LOSS;
  } else {
   gameResult = Result.WIN;
  }  
 }    
}

App corriendo:


miércoles, 31 de octubre de 2012

JavaScript: Copiar y Resaltar (Copy & Highlight)

Como parte de mis disciplina de documentar funcionalidades que me sacan canas, esta vez dejo a disposición un pequeño código que ejemplifica como hacer un copiado/pegado de un elemento del DOM y a su vez dejarlo seleccionado/resaltado.

Para el copy/paste utilizo una librería popular en la web que utiliza un archivo Flash como intermediario de la operación: ZeroClipboard. Para la selección encontré un código que funciona adecuadamente en los principales browsers (IE, FF y Chrome). En el código de abajo son las funciones "getTextNodesIn" y "setSelectionRange". Cuando se carga este código en el browser, el usuario puede hacer clic en el texto y automáticamente se copia mostrando a su vez un mensaje de alerta, para después dejarlo seleccionado.

<html>
<head>
<script type="text/javascript" src="ZeroClipboard.min.js"></script>  
<script type="text/javascript">
 function getTextNodesIn(node) {
  var textNodes = [];
  if (node.nodeType == 3) {
   textNodes.push(node);
  } else {
   var children = node.childNodes;
   for (var i = 0, len = children.length; i < len; ++i) {
    textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
   }
  }
  return textNodes;
 }

 function setSelectionRange(el, start, end) {
  if (document.createRange && window.getSelection) {
   var range = document.createRange();
   range.selectNodeContents(el);
   var textNodes = getTextNodesIn(el);
   var foundStart = false;
   var charCount = 0, endCharCount;

   for (var i = 0, textNode; textNode = textNodes[i++]; ) {
    endCharCount = charCount + textNode.length;
    if (!foundStart && start >= charCount
      && (start < endCharCount ||
      (start == endCharCount && i < textNodes.length))) {
     range.setStart(textNode, start - charCount);
     foundStart = true;
    }
    if (foundStart && end <= endCharCount) {
     range.setEnd(textNode, end - charCount);
     break;
    }
    charCount = endCharCount;
   }

   var sel = window.getSelection();
   sel.removeAllRanges();
   sel.addRange(range);
  } else if (document.selection && document.body.createTextRange) {
   var textRange = document.body.createTextRange();
   textRange.moveToElementText(el);
   textRange.collapse(true);
   textRange.moveEnd("character", end);
   textRange.moveStart("character", start);
   textRange.select();
  }
 }

 function highlight() {
  var element = document.getElementById("span_id");
  setSelectionRange(element, 0, element.innerHTML.length); 
  alert("copied!");
 }

</script>
</head>
<body>
 <span id="span_id">Copy Me!</span>
 <script>
  ZeroClipboard.setMoviePath( 'ZeroClipboard.swf' );
  var clip = new ZeroClipboard.Client();
  var element = document.getElementById("span_id");
  clip.setText( element.innerHTML )
  clip.glue( 'span_id' ); 
  clip.addEventListener( 'onMouseUp', highlight );
 </script>
</html>
</body>


lunes, 29 de octubre de 2012

Hacer que un elemento DIV se comporte como Link

Si se desea que un elemento DIV pueda ser "cliqueable" en toda su área de manera que se comporte como una ancla (etiqueta "A"), simplemente se cambia el estilo para que el cursor sea "pointer", y se agrega el evento de onclick:

<div style="cursor:pointer;" onclick="window.location='new_page.html'>
...
</div>

miércoles, 10 de octubre de 2012

DOS: Listar todos los archivos de un tipo de manera recursiva

Hace poco quería listar de manera recursiva todos los .html que tenía en una carpeta. Pensé que tenía que crear un script de Windows pero resulta que con un simple comando de DOS resulta suficiente:

dir /s/b *.html > results.txt

domingo, 7 de octubre de 2012

Android: Nombres de recursos

Si obtienes un error diciendo lo siguiente:

"Invalid file name: must contain only [a-z0-9_.]"

Es porque alguno de los recursos que colocaste en la carpeta "res" no sigue las reglas de nombres de archivos apropiada.

Recientemente, en mis primeras incursiones en el desarrollo Android, nombre un archivo como "the-beatles-band". Según yo estaba usando caracteres válidos, pero no había notado que el guion que sale en la regla es para describir una expresión regular. "a-z" significa letras de la "a" a la "z" en minúscula. De igual forma "0-9" significa los números del 0 al 9. Así que tuve que reemplazar el guion por la raya abajo ("_").

miércoles, 3 de octubre de 2012

JSTL: Ruta actual de una página

Para obtener por medio de JSTL el path actual de una página incluyendo el query string, si lo tiene:

<c:set var="url"><c:out value="${pageContext.request.scheme}://${pageContext.request.serverName}${pageContext.request.contextPath}${pageContext.request.servletPath}"/><c:if test="${not empty pageContext.request.queryString}"><c:out value="?${pageContext.request.queryString}"/></c:if>
</c:set>

lunes, 1 de octubre de 2012

Prevención de XSS (Cross-site scripting)

Dejo a disposición un pequeño ejemplo de como evitar del lado del cliente, haciendo la aclaración de que no debemos omitir la validación del lado del servidor, un ataque de seguridad usando el Cross-site scripting (XSS).

Esta vulnerabilidad se da cuando una aplicación web toma datos que el usuario suministra y los despliega en una página HTML. Si el usuario suministra datos que contiene caracteres que son interpretados como parte de un elemento HTML en lugar de texto literal, entonces un atacante puede modificar el HTML que es recibido por el navegador de la victima.

Para poder explotar esta vulnerabilidad, un usuario malicioso debe engañar a una victima para que visite un URL con la carga de XSS. Una carga de XSS puede consistir de HTML, JavaScript u otro tipo de contenido que pueda ser interpretado en el navegador. 

Este tipo de ataque apunta a usuarios de una aplicación web, en lugar de la aplicación web misma. Puede llevar al robo de credenciales de usuario e información financiera personal.

En el siguiente script recorro todos los elementos del formulario para reemplazar los paréntesis angulares ("<>") por sus códigos HTML equivalentes. Repito que este filtro debe aplicarse también del lado del servidor, pues un atacante podría crear un formulario propio sin estas validaciones y redirigir a una victima a esta versión maliciosa del formulario.

<html>
<head>
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
 <script type="text/javascript">
 $(document).ready(function() {
   $('#formId').submit(function() {    
  var $inputs = $('#formId :input');    
  var values = {};
   $inputs.each(function() {
    var newValue=$(this).val().replace("<", "&lt;").replace(">", "&gt;");  
    $(this).val(newValue);
   });
  });
 });
 </script>
 </head>
<body>
 <form id="formId" action="sucess.html" method="get">
  user:<input type="text" name="username" />
  Password:<input type="password" name="password" />
  <input type="submit" />
 </form>
</body>
</html>

jueves, 27 de septiembre de 2012

Android: Nombres código de los Sistemas Operativos



Quizás muchos estén familiarizados ya con los nombres tan llamativos que se le designan a las versiones de los distintos sistemas operativos de Android. Pero si son tan despistados como quien escribe en este blog, tal vez no habrán notado el patrón que siguen estos particulares nombres código.

Para los que sí ponen atención, sabrán que las distintas versiones de Android son nombradas a partir de nombres de bocadillos dulces: Cupcake, Donut, Éclair, Froyo, Gingerbread, Honeycomb, Ice Cream Sandwich, Jelly Bean. Para los aún más observadores, ya se dieron cuenta que cada uno de los postres lleva   un orden alfabético que comienza a partir de la letra C.

Android 1.5 - Cupcake
Android 1.6 - Donut
Android 2.0 - Éclair
Android 2.1 - Éclair
Android 2.2 - Froyo
Android 2.3 - Gingerbread
Android 3.0 - Honeycomb
Android 4.0 - Ice Cream Sandwich
Android 4.1 - Jelly Bean

En cuanto a por qué comienza con la letra C, y dónde están los snacks con A y B, muchos podrían pensar que se debe a la típica denominación en el mundo del software como Alfa y Beta. Pero parece que antes de Cupcake, simplemente las versiones tenían nombre aburridos con códigos alfa numéricos y por tanto comenzaron a utilizar nombres código más fáciles de recordar como se acostumbra en los proyectos tecnológicos.

lunes, 24 de septiembre de 2012

POO: Pilares de la programación orientada a objetos



En un libro aprendí un tip bastante útil para recordar los pilares de la programación orientada a objetos. Solo hay que recordar el acrónimo: PIE. En las entrevistas es muy típico que se pregunte alguno de los principios de la programación orientada a objetos. Si se pregunta de manera general, se puede dar una buena impresión explicando cada uno de los pilares de manera elocuente.

Polymorphism (Poliformismo): la habilidad de una variable de un tipo específico de ser usada para referenciar objetos de diferentes tipos, y automáticamente llamar el método que es específico al tipo del objeto que la variable referencia. El beneficio del poliformismo es que resulta fácil agregar nuevas clases de objetos derivados sin quebrar el código invocador. En cortas palabras, cuando se invoca un método de un objeto sin saber su tipo específico, y sucede lo correcto, a eso llamamos poliformismo. Ejemplo:

public void printArea(Figure figure) {
   System.out.println(figure.getArea());
}

Figure figure = new Square(); // Square extends from Figure
printArea(figure); // Should print axa (a=side of a square);

Inheritance (Herencia): Este debería ser el principio más fácil de explicar. Consiste en incluir el comportamiento (métodos) y estado (variables) de una clase base, en una clase derivada. El beneficio principal de la herencia es que provee un mecanismo formal para la reutilización de código.

Encapsulation (Encapsulación): Se refiere a mantener todos los miembros relacionados (variables y métodos) juntos en un objeto. Los objetos deben ocultar la funcionalidad que es interna del mundo exterior. Esto se realiza al declarar ciertos métodos y variables como privadas en la clase. La buena encapsulación mejora la modularidad del código al prevenir que los objetos interactuen entre sí de maneras insospechadas, lo cual hace más fácil futuros desarrollos y refactorizaciones más sencillas.

jueves, 20 de septiembre de 2012

Resaltador de sintaxis para el blog actualizado

Acaba de actualizar mi resaltador de sintaxis ya que estaba bastante obsoleto. Sigo usando el mismo resaltador, solo que ahora uso la versión más actual aprovechando el hosting de los scripts que provee el autor Alex Gorbatchev.

Estas son las líneas que agregué en el HTML de la plantilla de Blogger (incluye un arreglo de CSS para Chrome):
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shThemeFadeToGrey.css' rel='stylesheet' type='text/css'/>
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCore.css' rel='stylesheet' type='text/css'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shLegacy.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCss.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJava.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPhp.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushSql.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js' type='text/javascript'/>

<style type='text/css'>
.syntaxhighlighter table td.gutter .line {
 padding: 0 5px  !important;
}
</style>

<script language='javascript'>
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.all();
dp.SyntaxHighlighter.HighlightAll('code');
</script>
</body>
</html>

miércoles, 19 de septiembre de 2012

CakePHP: Plugin para subir imágenes

Al fin pude descifrar una manera de subir imágenes en CakePHP 2.0. Probé este plugin de Jose Gonzalez y me funcionó adecuadamente. Puedo subir imágenes que se guardan en una estructura de carpetas fácilmente consultable por el identificador del modelo que se guarda en la base de datos. A parte genera los thumbnails en los tamaños que uno le defina.

Estos fueron los pasos que seguí:

1. Bajar el plugin.Se puede descargar el zip o bajar el código del repositorio de GitHub.

2. Se copia el código en la carpeta /app/Plugin/Upload (dejar la carpeta renombrada con Upload u otro nombre más amigable).

3. Se edita el "bootstrap.php" (/app/Config/) agregando la línea para habilitar el plugin:
CakePlugin::load('Upload');

4. En la base de datos, en la tabla donde se guarda la referencia a la foto, agregamos dos columnas: una para el identificador de la foto, y otra para el identificador del directorio:

CREATE TABLE  `mi_db`.`anuncios` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `titulo` varchar(45) NOT NULL,
  `id_foto` varchar(15) DEFAULT NULL,
  `dir` varchar(250) DEFAULT NULL,
)

5. En el modelo se configura las opciones para la subida de la imagen:

class Anuncio extends AppModel {
     
    public $actsAs = array(
        'Upload.Upload' => array(
            'id_foto' => array(
                'fields' => array(
                    'dir' => 'dir'
                ),                
                'thumbnailSizes' => array(
      'big' = '200x200',
                    'small' =>'120x120',
                    'thumb' =>'80x80'
                ),
                'thumbnailMethod'=> 'php'
            )
        )
    );
}

Observese que se configura los nombre de las columnas que configuramos en la tabla de nuestra base de datos, los tamaños de los thumbnails, y el método para crear los thumbnails (aquí uso php pues es una extensión que viene ya instalado por defecto en la mayoría de los ambientes de desarrollo tipo WAMP).

 6. Agrego el código de la vista:

<?php
 echo $this->Form->create('Anuncio', array('type' => 'file'));
 echo $this->Form->input('id_foto', array('type' => 'file', 'label' => 'Foto'));
 echo $this->Form->input('dir', array('type' => 'hidden'));
 echo $this->Form->end('Guardar Anuncio');
?>

7. ¡Listo! Las imágenes son guardadas dentro de 'webroot/files' (la ruta se puede configurar).


viernes, 14 de septiembre de 2012

Carga selectiva de JavaScript acorde con el Navegador

Interesante problema para resolver. Una funcionalidad de una página especifica no funciona apropiadamente en Chrome al parecer por la versión 1.7 de JQuery. La solución más sencilla es actualizar el sitio con la versión 1.8. Sin embargo por razones de tiempo y presupuesto, cambiar la versión de JQuery involucra probar el sitio de nuevo para validar que ninguna otra funcionalidad con dependencias de JQuery no se vieron afectadas.

La solución planteada entonces es cargar la versión 1.8 solamente en esa página, únicamente cuando el usuario usa el navegador Chrome. Para lograr esto hice un experimento primero (valga la nota de que deben haber soluciones más elegantes a esta) de colocar ambas versiones de JQuery en el HEAD para validar cuál versión de JQuery se utiliza si las dos están cargadas en la página. Algo así como determinar cuál es el orden de precedencia cuando hay funciones repetidas en dos scripts. Al parecer se invoca la última versión que se declara en el código.

Validado este experimento, programé un pequeño código, que con la ayuda de Yepnope, inyecto la versión 1.8 de JQuery cuando el navegador es Chrome.


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
 <script src="yepnope.1.5.4-min.js" type="text/javascript"></script>
 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js" type="text/javascript"></script>
</head>
<body>
<script type="text/javascript"> 
 $(document).ready(function() { 
  $.browser.chrome = /chrome/.test(navigator.userAgent.toLowerCase()); 

  if($.browser.chrome){
   yepnope.injectJs("http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js", function () {
    alert ("JQuery Version: " + $().jquery);
   });   
  } else {
   alert ("JQuery Version: " + $().jquery);
  }
 });  
</script>
</body>
</html>

Corremos el script en FireFox:


Corremos el Script en Chrome:

martes, 21 de agosto de 2012

Versión de Java de un .class


Hace poco tuve un pequeño problema con un código que nos enviaron ya compilado. A la hora de hacer deployment en el servidor obteníamos una excepción de tipo:

javax.servlet.ServletException: Bad version number in .class file

Según lo que leí la excepción se da porque la clase fue compilada con una versión más alta de Java que la que corre en la  máquina virtual. Por ejemplo que la clase fue compilada en Java 6 y la máquina virtual corre Java 5.

Para confirmar esto indague un poco en Google como averiguar la versión de una clase. Para ello existe un comando: javap -verbose ClassName

(El .exe se puede encontrar en la carpeta bin del JDK)

Es conveniente guardar en un archivo el resultado del comando pues tira bastante información. Dentro de esa información hay que buscar la combinación de minor y major version que se encuentra al puro comienzo para determinar la versión de Java utilizada.

major  minor Java platform version 
45       3           1.0
45       3           1.1
46       0           1.2
47       0           1.3
48       0           1.4
49       0           1.5
50       0           1.6

lunes, 20 de agosto de 2012

Provincias, cantones y distritos para seleccionar

Basado en una entrada anterior donde puse a disposición un script para crear las tablas que representan la estructura de provincias, cantones y distritos, he creado un script PHP que genera unos arreglos en JavaScript para poder utilizarlos en selectores de manera que carguen dinámicamente según la selección anterior. Básicamente, si selecciona una provincia, cargue los cantones de esa provincia, y si selecciona un cantón, cargue los distritos de ese cantón.

El script en PHP sería este:

<?php
 mysql_connect("localhost","root","") or die("Unable to connect to SQL server");
 @mysql_select_db("data_base_name") or die("Unable to select database"); 
  
 $provincias_result = mysql_query("select * from PROVINCIA_CR");
  
 echo "var p=new Array();var c=new Array();var d=new Array();";
 while ($provincia_row = mysql_fetch_row($provincias_result)) {
   
  echo "p[" . $provincia_row[0] . "]='" . $provincia_row[1] . "';";
   
  $cantones_result = mysql_query("select * from CANTON_CR where codigo_provincia = "
   . $provincia_row[0]);
   $canton_line = "c[". $provincia_row[0] . "]='"; 
   $distrito_lines = "";
   while ($canton_row = mysql_fetch_row($cantones_result)) {
    $canton_line = $canton_line . $canton_row[2] ."@" .  $canton_row[1] . "~";
     
    $distritos_result = mysql_query("select * from DISTRITO_CR where codigo_canton = "
     . $canton_row[0]);
      
    $distrito_line = "d[". $canton_row[1] . "]='";   
    while ($distrito_row = mysql_fetch_row($distritos_result)) {
      $distrito_line .= $distrito_row[2] ."@" .  $distrito_row[1] . "~";
    }
    $distrito_line=substr_replace($distrito_line ,"",-1); // Remueve último caracter.
    $distrito_lines .= $distrito_line . "';";
   }
   $canton_line=substr_replace($canton_line ,"",-1); // Remueve último caracter.
   echo $canton_line . "';";
   echo $distrito_lines;   
 }
?>
Este script genera un código como este (también dejo a disposición el script generado):
var p=new Array();var c=new Array();var d=new Array();p[1]='San José';c[1]='San José@1~Escazú@1~Desamparados@1~Puriscal@1~Tarrazú@1~Aserrí@1~Mora@1~Goicoechea@1~Santa Ana@1~Alajuelita@1~Vasquez de Coronado@1~Acosta@1~Tibás@1~Moravia@1~Montes de Oca@1~Turrubares@1~Dota@1~Curridabat@1~Pérez Zeledón@1~León Cortés@1';d[1]='Carmen@101~Merced@101~Hospital@101~Catedral@101~Zapote@101~San Francisco de Dos Ríos@101~Uruca@101~Mata Redonda@101~Pavas@101~Hatillo@101~San Sebastián@101';d[1]='Escazú@102~San Antonio@102~San Rafael@102';d[1]='Desamparados@103~San Miguel@103~San Juan de Dios@103~San Rafael Arriba@103~San Antonio@103~Frailes@103~Patarrá@103~San Cristóbal@103~Rosario@103~Damas@103~San Rafael Abajo@103~Gravilias@103~Los Guido@103';d[1]='Santiago@104~Mercedes Sur@104~Barbacoas@104~Grifo Alto@104~San Rafael@104~Candelaria@104~Desamparaditos@104~San Antonio@104~Chires@104';d[1]='San Marcos@105~San Lorenzo@105~San Carlos@105';d[1]='Aserrí@106~Tarbaca o Praga@106~Vuelta de Jorco@106~San Gabriel@106~La Legua@106~Monterrey@106~Salitrillos@106';d[1]='Colón@107~Guayabo@107~Tabarcia@107~Piedras Negras@107~Picagres@107';d[1]='Guadalupe@108~San Francisco@108~Calle Blancos@108~Mata de Plátano@108~Ipís@108~Rancho Redondo@108~Purral@108';d[1]='Santa Ana@109~Salitral@109~Pozos o Concepción@109~
...
Con ese código se puede agregar lógica JavaScript, sazonada con JQuery, para cargar las opciones dinámicamente:

<html>
<head>
 <script src="distritos.js" type="text/javascript"></script>
 <script src="jquery-1.7.1.min.js" type="text/javascript"></script> 
 <script type="text/javascript">  
  var separator1 = "~";
  var separator2 = "@";
  
  function cargarProvincias(provinciaId) {
   $("#" + provinciaId).append($("<option></option>")
    .attr("value", -1).text("Elija una provincia"));  
   for (var key in p) {
    $("#" + provinciaId).append($("<option></option>")
     .attr("value", key).text(p[key]));
   }
  }
  
  function cargarCantones(provincia, cantonId) {
   var provinciaSelectedValue =  $(provincia).val();
   $("#" + cantonId + " option").remove();
   
   if(provinciaSelectedValue != -1) {     
    $("#" + cantonId).append($("<option></option>")
     .attr("value", -1).text("Elija un cantón")); 
    
    var cantones = c[provinciaSelectedValue].split(separator1);
    
    for (var i=0; i < cantones.length; i++) {
     var cantonValuePair = cantones[i].split(separator2);
     $("#" + cantonId).append($("<option></option>")
     .attr("value", cantonValuePair[1]).text(cantonValuePair[0]));
    }    
   } else {
    $("#" + cantonId).append($("<option></option>")
     .attr("value", -2).text("Elija una provincia"));
   }
   cargarDistritos($("#" + cantonId), "distritos");
  }
  
  function cargarDistritos(canton, distritoId) {
   var cantonSelectedValue =  $(canton).val();
   $("#" + distritoId + " option").remove();
   
   if(cantonSelectedValue > 0) {      
    $("#" + distritoId).append($("<option></option>")
     .attr("value", -1).text("Elija un distrito")); 
    
    var distritos = d[cantonSelectedValue].split(separator1);
    
    for (var i=0; i < distritos.length; i++) {
     var distritoValuePair = distritos[i].split(separator2);
     $("#" + distritoId).append($("<option></option>")
     .attr("value", distritoValuePair[1]).text(distritoValuePair[0]));
    }    
   } else if(cantonSelectedValue == -1){
    $("#" + distritoId).append($("<option></option>")
     .attr("value", -1).text("Elija un cantón")); 
   } else if(cantonSelectedValue == -2){
    $("#" + distritoId).append($("<option></option>")
     .attr("value", -1).text("Elija una provincia")); 
   }
  }  
 </script> 
</head>

<body onload="cargarProvincias('provincias')">
 <select id="provincias" onchange="cargarCantones(this, 'cantones');"></select>
 <select id="cantones" onchange="cargarDistritos(this, 'distritos');"><option value="-2">Elija una provincia</option></select>
 <select id="distritos"><option value="-2">Elija una provincia</option></select>
</body>
</html>

Resultado Final:

lunes, 23 de julio de 2012

CakePHP: Carga dinámica de select input con JSON

Continuando con mis experiencias en este aprendizaje del framework CakePHP, quiero poner a disposición una receta en la cual tardé bastante en encontrar y personalizar, de cómo preparar una carga dinámica de un select box a partir de una selección anterior.

En este ejemplo cargo las opciones de raza a partir de una selección previa de una especie.

Ingredientes:


1. Función en el controlador para listar las especies (primer select box):

public function add() {  
   if ($this->request->is('post')) {
       // Código para salvar el anuncio.
    } else {
        $especie = new Especie();         
        $especies = $especie->find('list', array(       
     'fields' => array('Especie.id', 'Especie.nombre')
 ));
        $this->set('especies', $especies);         
    }
}

2. Código del formulario:
Form->create('Anuncio');
 
 echo $this->Form->input('especie', array(
     'type'    => 'select',
     'options' => $especies,
     'empty'   => 'Elija una especie'
 ));
 echo $this->Form->input('raza', array(
     'type'    => 'select',
     'empty' => 'Elija una especie'
 ));    
 echo $this->Form->end('Guardar Anuncio');
?>

3. Función en el controlador para retornar un JSON con las razas pertenecientes a la especie:

    public function getRazasByEspecie() {
     if ($this->request->is('ajax')) { 
      $idEspecie = $this->params['data']['idEspecie'];
       
      $raza = new Raza();
      $razas = $raza->find('all',array(
       'fields' => array('Raza.id', 'Raza.nombre'), 
       'conditions'=>array('Raza.id_especie'=>$idEspecie)));
      
      $this->RequestHandler->respondAs('json');
      $this->autoRender = false;      
      echo json_encode ( $razas );      
     }
    }

Preparación:

Agregar el código JavaScript que una (binding) la siguente lógica al evento de modificación del primer select box. La función hace un llamado Ajax al controlador para pedir las razas. Estas son retornadas en formato JSON el cual es procesado para agregar las nuevas opciones del segundo select box.
$(document).ready(function(){

 $('#AnuncioEspecie').change(function(){
  var selected = $(this).val();
    
  $.ajax({
   type: "POST",
   url: '/Anuncios/getRazasByEspecie',
   data: "idEspecie="+selected,
   dataType: 'json',
   success: function(data){
    
    $('#AnuncioRaza option').remove();
    var $el = $("#AnuncioRaza");
    if (data.length > 1) {
     $el.append($("")
       .attr("value", -1).text("Elija una raza"));
    }
    $.each(data, function(i,items){
     $el.append($("")
       .attr("value", items.Raza.id).text(items.Raza.nombre));          
    });    
   }
  });
 });
});


Consumase al gusto :D!

miércoles, 11 de julio de 2012

ASP: Redireccionamiento para un dominio específico

Si se desea redireccionar el acceso a una página para un dominio específico, en caso hayan varios dominios apuntando al mismo código:
<%
If Request.ServerVariables("SERVER_NAME") = "www.mysite.com" Then
Response.redirect "http://www.redirect.com"
End If
%> 

Apache Tomcat: Habilitando SSI

Parece que por defecto la instalación de Apache Tomcat no trae habilitado los SSI (Server Side Includes). Para habilitarlo hay que hacer tres pasos sencillos:

  1. Renombrar el JAR que se encuentra en "[catalina_home]/server/lib": servlets-ssi.renametojar
  2. Descomentar la configuración del servlet en "[catalina_home]/conf/web.xml"
  3. <!--
      <servlet>
    <servlet-name>ssi</servlet-name>
    <servlet-class>
    org.apache.catalina.ssi.SSIServlet
    </servlet-class>
    <init-param>
    <param-name>buffered</param-name>
    <param-value>1</param-value>
    </init-param>
    <init-param>
    <param-name>debug</param-name>
    <param-value>0</param-value>
    </init-param>
    <init-param>
    <param-name>expires</param-name>
    <param-value>666</param-value>
    </init-param>
    <init-param>
    <param-name>isVirtualWebappRelative</param-name>
    <param-value>0</param-value>
    </init-param>
    <load-on-startup>4</load-on-startup>
    </servlet>
    -->
    
  4. Descomentar la configuración del mapeo del servlet en el mismo archivo y configurar el patrón URL como corresponde al tipo de extensiones que van a tener estos includes
  5. <!--
      <servlet-mapping>
    <servlet-name>ssi</servlet-name>
    <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    -->
    

miércoles, 27 de junio de 2012

CakePHP: Llenando un combo box

Aca estoy iniciando con mis primeros pasos en este framework que hasta el momento pinta muy bien. La documentación que existe en el sitio oficial donde exponen un caso de ejemplo desarrollando un blog es verdaderamente muy útil. Solamente me quedé un poco atascado en el desarrollo de mi propio ejemplo queriendo llenar un combo box.

En mi caso quería tener una pantalla para agregar una mascota seleccionando la raza a la que pertenece.  Para ello necesito poder mostrar las razas que se encuentran en una tabla aparte de la de mascotas.

Al final la solución que encontré fue la de importar la clase modelo "Raza" para poder hacer un "find" y mandar los resultados a la vista utilizando una notación especial para que se ajuste bien a un combo box.

Controlador

<?php
App::import('Model', 'Raza');

class MascotasController extends AppController {
    public $helpers = array('Html', 'Form');
    
    public function add() {  
        if ($this->request->is('post')) {
           // Código para salvar la mascota.
        } else {
            $raza = new Raza();          
            $razas = $raza->find('list', array(       
                  'fields' => array('Raza.id', 'Raza.nombre')
            ));
            $this->set('razas', $razas);         
        }
    }
}

Vista

<h1>Agregar Mascota</h1>
<?php
echo $this->Form->create('Mascota');
echo $this->Form->input('nombre');
echo $this->Form->input('raza', array(
    'type'    => 'select',
    'options' => $razas,
    'empty'   => false
));
echo $this->Form->end('Guardar Mascota');
?>

miércoles, 6 de junio de 2012

Imprimir contenido en ventana aparte

Código mostrando como mandar a imprimir un contenido en una ventana aparte cerrandola inmediatamente. Funciona para todos los navegadores (en teoría).
<html>
<head>
<script language="javascript">
function printDiv() { 
 var divToPrint = document.getElementById('printArea');
 newWin= window.open();
 newWin.document.write(divToPrint.innerHTML); 
 newWin.document.close();
 newWin.focus();
 newWin.print();
 newWin.close();
}
</script>

</head>

<body>
 <div id="printArea">
  Print all content here!
 </div>
 <a href="#" onclick="printDiv()">Print!</a>
</body>

</html>

lunes, 30 de abril de 2012

JavaScript: Parar/Iniciar un Gif animado


Hace un poco surgió la pregunta en el equipo si se podría controlar con JavaScript el ciclo de un Gif animado. Controlar al menos el parar y reiniciar la animación.

En mi búsqueda por Internet encontré que no se puede hacer directamente, pero sí se puede usar un truco que es reemplazar la imagen del Gif animado con una imagen estática. Inclusive encontré un código con Canvas que permite generar dinámicamente una imagen estática de un Gif animado. Solo funciona si el browser soporta HTML5.

Este sería el código:

<html>
<head>
<script>
function is_gif_image(i) {
    return /^(?!data:).*\.gif/i.test(i.src);
}

function freeze_gif(src) {
 i = document.getElementById(src);
    var c = document.createElement('canvas');
    var w = c.width = i.width;
    var h = c.height = i.height;
    c.getContext('2d').drawImage(i, 0, 0, w, h);
    try {
        i.src = c.toDataURL("image/gif"); // if possible, retain all css aspects
    } catch(e) { // cross-domain -- mimic original with all its tag attributes
        for (var j = 0, a; a = i.attributes[j]; j++)
            c.setAttribute(a.name, a.value);
        i.parentNode.replaceChild(c, i);
    }
}

function unfreeze_gif(id, src) {
 i = document.getElementById(id);
    i.src = src;
}

</script>
</head>
<body>
<img id="rotator" src="rotation.gif" />
<br />
<input value="Freeze!" type="button" onclick="freeze_gif('rotator')" />
<input value="Unfreeze!" type="button" onclick="unfreeze_gif('rotator', 'rotation.gif')" />
</body>
</html> 

martes, 24 de abril de 2012

Linux Shell: Uso en disco duro

Este comando de Unix/Linux está genial para listar el espacio usado en disco duro de todas las carpetas del primer nivel donde se ejecuta. En particular me fue útil para averiguar el espacio que estamos usando en una cuenta FTP.

du -hc --max-depth=1


miércoles, 28 de marzo de 2012

Apache: Habilitando SSI

Los SSI o "Server Side Includes" son directivas que nos permiten en un servidor apache, incluir un archivo dentro de otro. Esto se hace del lado del servidor como el mismo nombre lo indica.

<!--#include virtual="includes/page_top_display.html" -->

Esta feature de Apache no viene habilitado por defecto. Hay que hacerlo manualmente editando el httpd.conf. Se puede hacer de la siguiente manera usando la siguiente configuración simple:

1. En el bloque de <ifmodule mime_module> se agrega las siguiente líneas:
<IfModule mime_module>
AddType text/html .shtml
AddOutputFilter INCLUDES .shtml
</IfModule mime_module>

2. Dentro de alguno de los directorios se agrega la siguiente línea "Options +Includes":
<Directory "c:/wamp/www/">  
Order Allow,Deny
Allow from all
Options +Includes
</Directory>

lunes, 19 de marzo de 2012

PHP: Probar conexión a base de datos

Script sencillo para probar que hay conexión a la base de datos:


<?php
$host = '<TYPE_HOST>';
$user = '<TYPE_USER>';
$pass = '[TYPE_PASS>';
$db = '<TYPE_DB>';

mysql_connect($host,$user,$pass) or die('error connection');
mysql_select_db($db) or die('error database selection');

echo "Connection successful!";
?>

lunes, 12 de marzo de 2012

TortoiseSVN: Conceptos básicos

¿Qué es SVN?

¿Has estado alguna vez en una situación donde tenías que trabajar en un archivo que alguien más estaba modificando al mismo tiempo? ¿Perdiste tus cambios producto de alguna falta de coordinación? ¿Has salvado alguna vez un archivo y después querido regresar a una versión anterior, deseando poder ver como lucía el archivo en un determinado lugar del tiempo? Para poder hacer esto no se necesita una máquina del tiempo. El Control de Versiones es el arte de manejar cambios de información que pueden ayudar a resolver casos como los anteriores.

TortoiseSVN es un cliente de código abierto para el sistema de control de versiones Subversion. TortoiseSVN administra los archivos y directorios a través del tiempo. Los archivos son guardados en un repositorio central, similar a cualquier repositorio ordinario de archivos, con la excepción de que recuerda cada cambio que se haya hecho sobre archivos y directorios. Esto permite recuperar versiones viejas de los archivos que trabajamos, y examinar el historial para obtener información como quién cambió los datos y cuándo lo hizo.

Conceptos básicos

Los sistemas de control de versiones son diseñados para guardar y dar un seguimiento de los cambios hechos a través del tiempo. Existen dos métodos más comunes de hacer versionamiento. Uno es el “Lock-Modify-Unlock” (Bloquear-Modificar-Desbloquear). Con este método los usuarios bloquean los archivos que van a editar para evitar que algún otro usuario los modifique. La desventaja de esta solución es que los usuarios pueden olvidar desbloquear archivos, o pueden innecesariamente bloquear archivos si los usuarios están trabajando en diferentes áreas de un archivo.

TortoiseSVN utiliza el método “Copy-Modify-Merge” (Copiar-Modificar-Unir) para crear copias de trabajo de los archivos para los usuarios. De esta manera los usuarios trabajan en paralelom, modificando sus propias copias privadas de los archivos. Finalmente, las copias privadas son fusionadas/unidas en una nueva versión final. Durante este proceso de unión de cambios puede pasar que hayan conflictos (estos serán vistos más adelante). TortoiseSVN también soporta la capacidad de bloquear archivos y carpetas si se requiere. Lo importante es que sin importar que tan bueno sea el sistema de control de versiones, la comunicación entre los miembros del equipo sigue siendo clave para mantener un buen flujo de trabajo.

Repositorios

Subversion es un sistema centralizado para compartir información. Su núcleo es un repositorio, el cual es un centro de almacenamiento de datos. El repositorio guarda información en la forma de un árbol de sistema de archivos - la típica jerarquía de archivos y directorios. Cualquier número de clientes se conectan al repositorio, y luego leen o escriben en esos archivos. Al escribir datos, un cliente hace la información disponible para otros; al leer datos, los clientes reciben información de otros.

Lo que hace el repositorio de Subversion especial es que recuerda cada cambio que se ha escrito en él: cada cambio a cada archivo, inclusive los cambios al directorio, como la adición, borrado y reacomodo de archivos y directorios. Uno puede observar la última versión del sistema de archivos, pero también se tiene la capacidad de ver estados previos del árbol de directorios. Por ejemplo, qué contenía un directorio en día particular, quién fue la última persona que cambió el archivo, y qué cambios les hizo.

Una manera de visualizar el repositorio es verlo como una serie de árboles. Imaginémosnos un arreglo de número de revisiones, comenzando en 0, yendo de izquierda a derecha. Cada número de revisión tiene un árbol de sistema de archivos colgando debajo de él, y cada uno de esos árboles es un foto instantánea de la manera en que el repositorio se veía después de cada "commit" (acción para guardar cambios locales al repositorio).


Copias de trabajo/locales

La copia local o de trabajo, es una área privada sobre la cual se hace toda la edición de los archivos en la estación de cada persona involucrada en el proyecto. Subversion nunca va a incorporar los cambios de otras personas, ni hacerlos disponibles a otros, al menos que explícitamente se le indique que lo haga.

Después de que se hacen cambios a los archivos en la copia local y se verifica que trabajan correctamente, Subversion provee comandos para "publicar" los cambios (al escribirlos en el repositorio) para hacerlos disponibles a otras personas que están trabajando en el mismo proyecto. Si otros miembros del equipo publican sus propios cambios, Subversion pone a disposición comandos para hacer las uniones ("merges") de los archivos que han sido editados por más de una persona a la vez.

Las copias locales contienen unos archivos generados por Subversion (contenidos en las carpetas llamadas ".svn"), que son utilizados por el sistema para mantener los estados actuales de los archivos.

Estados

Cuatro posibles estados de las copias locales:

Sin alterar, y actual (Unchanged, and current)El archivo está sin cambiar en el directorio de trabajo, y no cambios al archivo han sido enviados al repositorio desde la revisión de trabajo. Un commit al archivo no hará nada, y una actualización al archivo tampoco hará nada.
Cambiado localmente, y actual (Locally changed, and current)El archivo ha sido cambiado en el directorio de trabajo, y no cambios al archivo han sido enviados al repositorio desde la revisión base. Hay cambios locales a los cuales no se les ha hecho una acción de commit al repositorio, por lo cual un commit al archivo terminará publicando los cambios exitosamente, y una actualización al archivo no haría nada.
Sin cambiar, y desactualizado (Unchanged, and out-of-date)El archivo no ha sido cambiado en el directorio de trabajo, pero ha sido cambiado en el repositorio. El archivo debería ser eventualmente actualizado para tener la versión más reciente. Una acción de commit no haría nada, y una actualización del archivo traería a la copia local, los últimos cambios hechos en el repositorio.
Cambiado localmente, y desactualizado (Locally changed, and out-of-date)El archivo ha sido cambiado tanto en el directorio de trabajo, como en el repositorio. Una acción de commit al archivo terminaría con un error de desactualización ("out-of-date error"). El archivo debería ser actualizado primero; un comando decactualización intentará hacer una unión de los cambios públicos con los cambios locales ("merge"). Si Subversión no puede completar la unión de una manera plausible automáticamente, dejará al usuario que él mismo resuelva los conflictos.


Menús Contextuales
TortoiseSVN proporciona acceso a los distintos comandos a traves de menús contextuales que se integran con el sistema de ficheros de Windows. Al dar clic derecho sobre un archivo o carpeta aparecen distintas opciones dependiendo de si el archivo o la carpeta está sincronizados con un repositorio.

lunes, 27 de febrero de 2012

Libro de Patrones de Diseño en JavaScript

Quisiera recomendar este libro publicado de manera gratuita por Addy Osmani, reconocido JavaScript Ninja que aborda el tema de Patrones de Diseño en JavaScript. Apenas he comenzado a leerlo, pero hasta ahora da buenos signos de ser una buena lectura.

Existe una tendencia como desarrolladores en pensar que, como JavaScript no tiene un soporte robusto de POO, podemos programar sin mayor estructuración nuestra funcionalidad de Front-End. Lo básico es que las funciones de JavaScript se encuentren en archivos separados. No obstante es importante considerar que hay mecanismos para emular un desarrollo que haríamos con lenguajes más formales.



jueves, 2 de febrero de 2012

Pasando selección entre formularios con JavaScript

Hace poco tuvimos que resolver un problema algo trivial pero que hasta ahora me había tocado resolver. Consiste en poder seleccionar un valor de un "dropdown list" en una página con un formulario, y que ese mismo valor se seleccione en otra página con otro formulario pero con el mismo "dropdown list". Todo con puro código HTML y JavaScript.


La solución propuesta, a sabiendas de que pueda existir otra mejor, consiste en utilizar el método GET en el primer formulario.

<form method="get" action="part-b.html">
<select name="perro">
<option value="">Seleccion un perro...</option>
<option value="boxer">Boxer</option>
<option value="pastor">Pastor Aleman</option>
</select>
<input type="submit" />
</form>

De esta manera podemos leer el query string con una función que ya he utilizado varias veces, extraemos el parámetro y usamos una sentencia "abra kadabra" de JQuery para seleccionar el "dropdown list" con el parámetro.
<html>
<head>
<script src="jquery.min.js"></script>
<script type="text/javascript">
function getUrlVars()
{
var vars = [], hash;
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}
</script>
</head>
<body>

<form method="get" action="d.html">

<select name="perro" id="perros">
<option value="boxer">Boxer</option>
<option value="pastor">Pastor Aleman</option>
</select>
<input type="submit" />
</form>

<script type="text/javascript">
var perro = getUrlVars()["perro"];
$('#perros option[value=' + perro + ']').attr("selected", "selected");
</script>
</body>
</html>

lunes, 30 de enero de 2012

Evaluación corta de Patrones de Diseño GoF


He estado dando un curso de patrones de diseño en la empresa en la que laboro como parte del programa de capacitación interna. Quiero dejar a disposición del público una prueba corta de 15 preguntas (en inglés) de selección única, para todo aquel que quiera evaluar que tanto conoce de los patrones clásicos de GoF.

martes, 24 de enero de 2012

Función JavaScript para leer valores del Query String


Normalmente es muy sencillo leer los parámetros y valores del Query String con un lenguage de servidor. Digase por ejemplo en PHP "$_GET['param']". Pero hace poco necesitaba hacer esto mismo pero del lado de JavaScript. Me encontré esta función que hace el trabajo:

function getUrlVars()
{
var vars = [], hash;
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}

He aquí un ejemplo sencillo que lee el parámetro "error" para determinar si hay que deplegar un mensaje de error en la pantalla de autenticación.


var error = getUrlVars()["error"];

if (error == "1") {
$("#error_message1").show();
} else if(error == "2") {
$("#error_message2").show();
}

lunes, 16 de enero de 2012

Cerrando una ventana en Firefox con JavaScript

Un compañero estaba teniendo problemas con un requerimiento que pidió el cliente que consistía simplemente en agregar un link que cerrara la ventana cuando se seleccionaba.

Una simple línea en JavaScript: "window.close();" pensabamos que era suficiente para hacer el trabajo, pero resulta que Firefox no permite esta operación porque lo percibe como una llamada poco segura. Algo que no debería permitirsele al navegador por defecto (similar a la capacidad de un script de leer archivos del cliente).

Googleando por un rato descubrí que hay una manera de habilitar estas llamadas poco seguras usando esta línea: "netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserWrite");".

Así que hice mi código de ejemplo que hiciera una excepción para FF:

<html>
<head>
<script language="Javascript">
function closeWindow() {
if (navigator.userAgent.indexOf("Firefox")!=-1){
netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserWrite");
alert("This will close the window");
window.open('','_self');
window.close();
} else {
this.focus();self.opener = this;self.close();
}
}
</script>
</head>
<body>
<a href="javascript:closeWindow();">Close Window</a>
</body>
</html>


Cuando abrí la página localmente e intenté cerrarla con el enlace que me conecta a la función JavaScript, me apareció el siguiente mensaje de confirmación:


Si le doy permitir, efectivamente la ventana se cierra. El problema está cuando uno coloca el script en un servidor web porque FF ni siquiera muestra este mensaje sino que de una niega el acceso. Esto se puede ver en la consola de errores:



Pareciera que existe todo un proceso en Mozilla para certificar scripts que intentan hacer este tipo de llamadas para obtener privilegios del navegador. Si los scripts no están certificados, FF interpreta el sitio como no seguro para hacer este tipo de llamadas.

La conclusión en mi caso particular, es que parace demasiado trabajo para agregar una opción que en sí no parece tan natural para una aplicación web. Lo mejor es dejarlo habilitado solamente para los navegadores que lo soportan.

jueves, 12 de enero de 2012

Herramientas: ArgoUML

Otra opción para crear diagramas UML: ArgoUML.

No lo he explorado mucho, pero parece que la interface fue creada con Java Swing lo cual en particular me produce una sensación de lento que arrastro de las primeras veces que trabajaba con Swing.

En todo caso puede ser una mejor alternativa a StarUML debido a que este no ha sido actualizado en mucho tiempo y tiene problemas para generar código e ingeniería inversa, con versiones de Java más arriba de 1.4.