Comparando diferencias entre ficheros con java-diff-utils

Comparando diferencias entre ficheros con java-diff-utils.


0. Índice de contenidos.


1. Introducción

Existen determinadas ocasiones en nuestro día a día donde necesitamos comparar diferencias entre ficheros. Si trabajamos con sistemas de control de versiones (CVS, Subversion, Git…) este hecho se repite aún con mayor frecuencia, sobre todo cuando tenemos un conflicto en un fichero. Normalmente este proceso se suele realizar a mano, con ayuda de herramientas que suelen traer integradas los clientes de dichos sistemas de control de versiones (véase el caso de Tortoise).

Sin embargo, pueden existir ocasiones en que necesitemos automatizar este proceso de comparación de ficheros. Pongamos el caso de una aplicación que necesita informar al usuario de que hubo cambios en una versión de un fichero con respecto a la anterior y listarle esas diferencias del mismo modo que hacen los clientes de sistemas de control de versiones.

En este tutorial veremos cómo identificar cambios entre ficheros de manera automática con ayuda de java-diff-utils, una librería Java Open Source que se distribuye bajo Licencia Apache 2.0.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.2 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: Mac OS Snow Leopard 10.6.7
  • Entorno de desarrollo: Intellij Idea 11.1 Ultimate.
  • java-diff-utils 1.2.1
  • JDK 1.6
  • JUnit 4.10
  • Apache Maven 3.0.3

3. El escenario.

El ejemplo que vamos a ver es el siguiente. Tenemos dos ficheros a los que llamaremos originalFile.txt y revisedFile.txt. Queremos poder listar las diferencias entre ambos de forma que: podamos saber qué modificaciones ha habido con respecto al original, qué nuevas líneas se han metido y cuáles se han borrado. Además queremos saber las líneas en las que se produjo el cambio en el fichero revisedFile.txt con respecto a originalFile.txt, el número de líneas de las que están compuestos los cambios y el texto de los cambios.

Por tanto los requisitos serán:

  • Poder obtener un listado de modificaciones en el fichero revisedFile.txt con respecto a originalFile.txt donde se indique en qué línea empieza el cambio, cúantas líneas tiene el cambio y el texto del que está compuesto.
  • Poder obtener un listado de inserciones en el fichero revisedFile.txt con respecto a originalFile.txt donde se indique en qué línea se realizó la inserción, cúantas líneas se insertaron y el texto insertado.
  • Poder obtener un listado de líneas eliminadas en el fichero revisedFile.txt con respecto a originalFile.txt y el número de cada línea que se eliminó.

Nuestro fichero originalFile.txt será el siguiente:

Y nuestro fichero revisedFile.txt será el este:

Como vemos hay cambios considerables entre los dos ficheros. En concreto:

  • En revisedFile.txt se ha eliminado la línea 1 (Line 1) del fichero original.
  • En revisedFile.txt se ha modificado la línea 2 (línea 3 en el original) añadiendo la cadena de caracteres ” with changes”.
  • En revisedFile.txt se ha modificado la línea 4 (línea 5 en el original) añadiendo la cadena de caracteres ” with changes and” y añadiendo una línea nueva “a new line”.
  • En revisedFile.txt se ha añadido la línea 7 (“new line 6.1″) del fichero original.
  • En revisedFile.txt se ha modificado la línea 11 (línea 10 en el original) añadiendo la cadena de caracteres ” with changes”.

Por tanto tenemos 3 grupos de cambios en el fichero revisedFile.txt con respecto a originalFile.txt:

  • Modificaciones: líneas 2, 4 (línea 4 modificada y 5 añadida) y 11 del fichero revisedFile.txt
  • Inserciones: línea 7 del fichero revisedFile.txt
  • Eliminaciones: línea 1 del fichero originalFile.txt no está en revisedFile.txt

Y alguno puede estar pensando ahora mismo: “Ya podías haber puesto un ejemplo con menos diferencias, que ya nos hacemos una idea”. Pues sí, yo también lo estoy pensando, pero así vemos mejor toda la potencia de esta librería… :)


4. Comparando tipos de diferencias.

LLegados a este punto ya tenemos claro lo que tenemos que hacer y qué resultado esperamos en el ejemplo que hemos propuesto. Ahora vamos a ver cómo se hace todo esto con java-diff-utils.

Lo que vamos a hacer es crear una clase FileComparator que contenga tres métodos públicos: getChangesFromOriginal, getInsertsFromOriginal y getDeletesFromOriginal, que nos devolverán la lista de modificaciones, inserciones y eliminaciones en el fichero revised con respecto al original.

Estas listas de cambios nos serán devueltas en forma de Chunk (clase propia de la librería) que no es más que una clase con la información relativa al cambio, como puede ser la posición desde la que empieza el cambio o las líneas que lo componen.

Nuestra clase quedaría de la siguiente forma:

Prestemos atención a la línea 54. El método diff de la clase DiffUtils recibe los ficheros original y revised como colecciones de líneas y devuelve un objeto “Patch”.

El objeto “Patch” contiene una lista de Deltas (clase Delta) que no es más que la relación entre el fichero original y el revised con respecto a un cambio. Por tanto, la lista de Deltas será la lista de cambios entre el fichero original y el fichero revised. Cada Delta contendrá dos Chunk el relativo al fichero original y al revised.

Vamos a listar todos estos conceptos tan raros que acaban de surgir en un momento para que quede más claro:

  • Chunk: “trozo” de fichero que ha cambiado (caso del fichero revised) o sobre el que se han hecho cambios (caso del fichero original). Contiene información relativa a este cambio como dónde empieza o de cuántas líneas está compuesto.
  • Delta: mantiene la relación entre el Chunk del fichero original y el Chunk del fichero revised e indica que tipo de cambio es: modificación, inserción o eliminación.
  • Patch: contiene el listado de todos los Deltas de dos ficheros comparados.

Como puede verse, el método getChunksByType, lo único que hace es, partiendo de la lista de Deltas (cambios) devolver únicamente los cambios con respecto al fichero original (chunks del fichero revised) filtrando por alguno de los tres tipos que maneja: modificaciones, inserciones o eliminaciones.

Los métodos públicos: getChangesFromOriginal, getInsertsFromOriginal y getDeletesFromOriginal, llaman al método getChunksByType filtrando por cada tipo de cambio que corresponda.

Pues esto es todo lo que necesitamos para tener nuestro comparador de cambios entre ficheros. Fácil ¿no?.


5. Probando el ejemplo.

Pues lo último que queda es probar si este invento funciona o no. Para ello vamos a testear (JUnit) nuestra clase probando con los dos ficheros que presentamos en el punto 3.

Realizamos 3 test, uno por cada uno de los métodos públicos de la clase FileComparator. La clase de test es la siguiente:

Como vemos, en cada test lo que se está haciendo es llamar al método correspondiente que se quiere testear y comprobar si el número de cambios del tipo que corresponda es el esperado.

Luego se comprueba que cada Chunk comience en la línea que se espera. La clase Chunk contiene un método getPosition que devuelve la posición en el fichero donde empiezan los cambios de la misma forma en que lo hace un array o una colección (0 la primera posición, 1 la segunda, etc…) por lo que para obtener la línea donde empieza debemos sumar 1 al valor que retorna.

Además, por cada Chunk comprobamos que tenga el número de líneas y texto esperados.

Lanzamos los test y todo funciona como queremos… 😀


6. Referencias.


7. Conclusiones.

En este tutorial hemos visto lo sencillo que es obtener de forma automática diferencias entre ficheros gracias a la librería java-diff-utils. Por ponerle un “pero” diría que le falta un poco de documentación. En su defensa destacar que es bastante intuitiva de utilizar.

Como siempre, recordaros que podéis hacer lo que queráis con el código fuente expuesto en este tutorial.

Espero que este tutorial os haya sido de ayuda. Un saludo.

Miguel Arlandy

marlandy@autentia.com

Twitter: @m_arlandy