viernes, 25 de junio de 2010

Diseño Web: Fondo Disuelto

Cuando uno comienza sus primeros pasos en el diseño web generalmente lo hace como decimos en Costa Rica, a la "chambona". Esto quiere decir que montamos nuestras páginas a como caigan las cosas. Colocamos imágenes sin medir tamaños o sin tener el toque creativo del cual carecemos la mayoría que nos dedicamos principalmente a la programación.

Recientemente aprendí una técnica que muchos dirán que es básica, pero para este servidor no lo era y nunca está demás dejar documentado algo que para mi es imprecindible para diseñar de manera optimizada para la carga.

Si queremos que nuestra página luzca un poco más atractiva, podemos usar en lugar de fondos con un color plano, fondos con efectos como el fondo disuelto.

Primero debemos generar una imagen con este tipo de fondo. Usemos la herramienta Gimp para construirla. Tomemos la herramienta de mezcla:


Escogemos después el modo de disolver:


Arrastramos el cursor en nuestra imagen de arriba hacia abajo para generar el color disuelto en esa dirección. Ahora viene el truco para la optimización de carga. En lugar de generar un tamaño de acuerdo al espacio que queremos llenar en nuestra página, escalamos la imagen a un ancho de 4 pixeles dejando el tamaño de largo igual:


Quedamos con una imgen que luce así:


Ya en nuestro CSS usamos la propiedad de "repeat" para generar esta misma línea a traves de todo el ancho del fondo que deseamos cubrir. Al usar "repeat-x" nos ahorramos bytes de carga lo cual nos ayuda en la experiencia usuario.

#footer{
background: url(images/disuelto.jpg) left top repeat-x;
}


martes, 22 de junio de 2010

Documentum: seleccionar documentos con la ruta

Últimamente he tenido que investigar un poco sobre el Administrador de Contenido "Documentum", específicamente con DQL que es el lenguage de consulta del CMS.

Dejo a disposición un script para seleccionar los documentos especifícados con el nombre del archivo y la ruta del directorio. Como la carpeta y el documento se almacenan en dos tablas distintas, se necesita hacer un join entre estas dos tablas más un par de sentencias que a la verdad todavía no estoy seguro que hacen pero así estaba el código que usé de referencia.

select d.r_object_id, d.object_name, d.r_version_label, f.r_folder_path,
d.r_modify_date
from dm_document d, dm_folder f
where d.i_folder_id = f.r_object_id and d.i_position = -1
and f.i_position = -1
and d.r_object_id in (
select r_object_id from dm_document where (
( Folder('/') and ( object_name in ('a', 'b', 'c')))
or (Folder('/') and ( object_name in ('d', 'e', 'f')))
)
) enable(row_based)

miércoles, 2 de junio de 2010

Renombrar base de datos en MySQL

Parece ser que antes existía un comando en MySQL para renombrar una base de datos pero fue quitado por cuestiones de seguridad.

RENAME {DATABASE | SCHEMA} db_name TO new_db_name; 

This statement was added in MySQL 5.1.7 but was found to be dangerous and was removed in MySQL 5.1.23. It was intended to enable upgrading pre-5.1 databases to use the encoding implemented in 5.1 for mapping database names to database directory names (see Section 8.2.3, “Mapping of Identifiers to File Names”). However, use of this statement could result in loss of database contents, which is why it was removed. Do not useRENAME DATABASE in earlier versions in which it is present.


Existe una manera alternativa pero solo funciona con el motor MyiSAM. Me tomó un poco de tiempo pero encontré un script para poder renombrar con InnoDB. El script requiere que exista una BD con el nuevo nombre que se va utilizar.


DROP PROCEDURE IF EXISTS RenameDatabase;
DELIMITER |
CREATE PROCEDURE RenameDatabase(
IN oldname CHAR (64), IN newname CHAR(64)
)
BEGIN
DECLARE version CHAR(32);
DECLARE sname CHAR(64) DEFAULT NULL;
DECLARE rows INT DEFAULT 1;
DECLARE changed INT DEFAULT 0;
IF STRCMP( oldname, 'mysql' ) <> 0 THEN
REPEAT
SELECT table_name INTO sname
FROM information_schema.tables AS t
WHERE t.table_type='BASE TABLE'
AND t.table_schema = oldname
LIMIT 1;
SET rows = FOUND_ROWS();
IF rows = 1 THEN
SET @scmd = CONCAT( 'RENAME TABLE ', oldname, '.', sname,
' TO ', newname, '.', sname );
PREPARE cmd FROM @scmd;
EXECUTE cmd;
DEALLOCATE PREPARE cmd;
SET changed = 1;
END IF;
UNTIL rows = 0 END REPEAT;
IF changed > 0 THEN
SET @scmd = CONCAT( "UPDATE mysql.db SET Db = '",
newname,
"' WHERE Db = '", oldname, "'" );
PREPARE cmd FROM @scmd;
EXECUTE cmd;
DROP PREPARE cmd;
SET @scmd = CONCAT( "UPDATE mysql.proc SET Db = '",
newname,
"' WHERE Db = '", oldname, "'" );
PREPARE cmd FROM @scmd;
EXECUTE cmd;
DROP PREPARE cmd;
SELECT version() INTO version;
IF version >= '5.1.7' THEN
SET @scmd = CONCAT( "UPDATE mysql.event SET db = '",
newname,
"' WHERE db = '", oldname, "'" );
PREPARE cmd FROM @scmd;
EXECUTE cmd;
DROP PREPARE cmd;
END IF;
SET @scmd = CONCAT( "UPDATE mysql.columns_priv SET Db = '",
newname,
"' WHERE Db = '", oldname, "'" );
PREPARE cmd FROM @scmd;
EXECUTE cmd;
DROP PREPARE cmd;
FLUSH PRIVILEGES;
END IF;
END IF;
END;
|
DELIMITER ;

viernes, 21 de mayo de 2010

Unas cuantas operaciones de archivo || Construyendo un Sincronizador

Recientemente en el proyecto que he estado trabajando en el último par de meses, tuvimos que pensar en una manera de resolver el problema de mantener actualizado unas copias locales de repositorios donde los originales se encuentran en servidores remotos. Debido al tipo de operaciones que necesitamos ejecutar, no podíamos darnos el lujo de hacerlo directamente en los servidores remotos. Si usted está pensando ahora mismo "di mae! Use SVN", bueno, digamos que nuestro cliente no lo tiene y no hay posibilidad cercana de que lo instale por nosotros. Sólo tenemos acceso a las unidades de recurso compartido donde está el sistema de archivos.

Este tipo de operación que requerimos se conoce (al menos así es como lo llamamos), como sincronización de directorios. En nuestro caso es necesario para mantener la versión más actualiza posible de los archivos remotos. La sincronización es muy útil cuando el costo de copiar todo el directorio es muy alto. Por ejemplo, si tenemos un directorio remoto con 200 GB, no sería eficiente copiar todo cada vez que tengamos que actualizar la copia local.

Investigué un poco para encontrar una herramienta que podría hacer lo que quería, y me encontré algunas buenas, pero con el único inconveniente que necesitaba algo que pudiera personalizar a nuestros procesos. Así que empecé a jugar un poco con la clase java.io.File y me di cuenta de que podía programar un sincronizador en poco tiempo.

En este post quiero compartir con ustedes algunas operaciones útiles de la clase File a la luz del problema que mi equipo tenía que resolver.
Pongamos un ejemplo de lo que el sincronizador tiene que hacer. Supongamos que tenemos este directorio remoto:

gsolano_remote
+ 20100514
++ calculations.xls
+ 20100514
++ HelloWorld.java
+ readme.txt


Y tenemos esta copia local que tiene que ser actualizada:

gsolano_local
+ 20100514
++ calculations.xls
++ deletelater
+ bck-ups
+ readme.txt
+ dir.txt


Si comparamos estos 2 directorios, la copia local tendría que ejecutar las siguientes acciones (en paréntesis):

gsolano_local
+ 20100514
++ calculations.xls
++ deletelater (remover)
+ bck-ups (remover)
+ readme.txt (actualizar)
+ dir.txt (remover)
+ 20100514 (agregar)
++ HelloWorld.java (agregar)


La lógica que se necesita codificar para ejecutar estas acciones es bastante simple.
  1. Listamos los archivos del directorio fuente(gsolano_remote).
  2. Listamos los archivos del directorio destino(gsolano_local).
  3. Comparamos los dos listados y extraemos:
  • Lista de nuevos archivos a copiar de la fuente al destino.
  • Lista de archivos que tienen que ser actualizados porque fueron modificados en el directorio fuente.
  • Archivos y directorios que ya no existen en el directorio fuente y que por tanto tienen que ser borrados.
Una vez que obtenemos estas listas solo tenemos que ejecutar las respectivas acciones de copiado y borrado.

Examinemos primero cómo escanear los archivos de un directorio:

package gsolano;

import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

public class Dir {

/**
* Returns a list of all file paths relative to the provided path.
* @param path
* @return list of relative paths.
*/
public static Map<String, Long> scan(String path) {
Map<String, Long> fileList = new LinkedHashMap<String, Long>();
scanFiles(path.toLowerCase(), path, fileList);
return fileList;
}

/**
* Method for recursively scan.
* @param rootSource
* @param path
* @param fileList
*/
private static void scanFiles(String rootSource, String path, Map<String, Long> fileList) {
File folder = new File(path); // This is the root directory.
// List files from first level of root directory.
File[] listOfFiles = folder.listFiles();

if (listOfFiles.length == 0) {
// Used to keep record of empty folders.
fileList.put(path.toLowerCase().replace(rootSource, "")
+ File.separator + ".", new Long(0));
}
else {
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) { // Is it a file?
try {
// Add it to the file list with the last modified date.
fileList.put(listOfFiles[i].getAbsolutePath().toLowerCase()
.replace(rootSource, ""), listOfFiles[i].lastModified());

} catch (Exception e) {
e.printStackTrace();
}
} else if (listOfFiles[i].isDirectory()) { // Is it a directory?
try {
// Recursively call for new found directory.
System.out.println(listOfFiles[i]);
scanFiles(rootSource, listOfFiles[i].getCanonicalPath(), fileList);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

}
}


En este código comenzamos a explorer las capacidades de la clase File. La primera es la habilidad de listar archivos de un directorio. Simplemente creamos una instancia de la clase File con el path de un directorio y luego usamos la función “listFiles()”.

File folder = new File(path);
File[] listOfFiles = folder.listFiles();

Ahora, esta function solamente obtiene los archivos y directories del primer nivel. La función no lista los directorios de los siguiente subniveles; es por eso que en la clase “Dir” el escaneo de archivos trabaja con una función recursiva.

Para determinar si se necesita ejecutar una llamada recursiva, se utiliza la función “isFile()” y “isDirectory()”. Si el archivo que se lee es un directorio (suena extraño ¿cierto?) entonces se hace la llamada recursiva. Si es un archivo se agrega a la lista.

En esta clase usamos también la función “lastModified()” para guardar la última fecha de modificación de cada uno de los archivos escaneados. La fecha se usa para determinar si el archivo de la fuente cambió provocando que se tenga que actualizar en el directorio destino.

Antes de saltar a la clase principal, echémole una mirada a la clase para copiar archivos. Modifiqué un poco esta clase que encontré en Internet :

package gsolano;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopy {

/**
* Copies one file from the source to specified target.
* @param fromFileName
* @param toFileName
* @param overrideFiles
* @throws IOException
*/
public static void copy(String fromFileName, String toFileName,
boolean overrideFiles) throws IOException {

File toFile = new File(toFileName);
if (toFile.exists() && !overrideFiles) {
return;
}
File fromFile = new File(fromFileName);

if (!fromFile.exists())
throw new IOException("FileCopy: " + "no such source file: "
+ fromFileName);
if (!fromFile.isFile())
throw new IOException("FileCopy: " + "can't copy directory: "
+ fromFileName);
if (!fromFile.canRead())
throw new IOException("FileCopy: " + "source file is unreadable: "
+ fromFileName);

if (toFile.isDirectory()) {
toFile = new File(toFile, fromFile.getName());
}
if (toFile.exists()) {
if (!toFile.canWrite()) {
throw new IOException("FileCopy: "
+ "destination file is unwriteable: " + toFileName);
}
String parent = toFile.getParent();
if (parent == null)
parent = System.getProperty("user.dir");
File dir = new File(parent);
if (!dir.exists())
throw new IOException("FileCopy: "
+ "destination directory doesn't exist: " + parent);
if (dir.isFile())
throw new IOException("FileCopy: "
+ "destination is not a directory: " + parent);
if (!dir.canWrite())
throw new IOException("FileCopy: "
+ "destination directory is unwriteable: " + parent);
} else {
// Create directory structure.
new File(toFile.getParent()).mkdirs();
}
createCopy(toFile, fromFile);
}

/**
* Writes the copy from source to target.
* @param toFile
* @param fromFile
* @throws FileNotFoundException
* @throws IOException
*/
private static void createCopy(File toFile, File fromFile)
throws FileNotFoundException, IOException {
FileInputStream from = null;
FileOutputStream to = null;
try {
from = new FileInputStream(fromFile);
to = new FileOutputStream(toFile);
byte[] buffer = new byte[4096];
int bytesRead;

while ((bytesRead = from.read(buffer)) != -1)
to.write(buffer, 0, bytesRead); // write
} finally {
if (from != null)
try {
from.close();
} catch (IOException e) {
;
}
if (to != null)
try {
to.close();
toFile.setLastModified(fromFile.lastModified());

} catch (IOException e) {
;
}
}
}
}

La clase FileCopy utilize 6 funciones más de la clase File:

1. “exists()”: usada para doble chequear que el archivo existe en el directorio fuente.

2. “canRead()”: usado para determiner si el archivo puede ser leido del directorio fuente. “canWrite():”: usado para determiner si el archive destino puede ser sobre escribido. Lo usamos cuando queremos actualzar un archivo.

3. “getParent()”: obtiene el path del directorio padre.

4. “mkDirs()”: tengo que confesar que esta es mi favorita; crea toda la jerarquía de directorios de un path.

5. “setLastModifiedDate()”: cuando terminamos de copiar el archivo en el directorio destino, queremos dejar la misma fecha de modificación del archivo fuente.

Para concluir con la clase “Synchronizer” solo tenemos que mencionar una función y una constante:

+ “delete()”: borra el archivo o directorio

+ “File.separator”: character dependiente del sistema usado para separar los directories de un path. En este ejemplo el separador es la barra inclinada hacia atrás o “back-slash” (“\”).

La clase “Synchronizer” hace todos los pasos requeridos para sincronizar 2 directorios. La lista de copias y borrados son calculados con simples comparaciones de colecciones (conjuntos y mapas).

Me gustaría poder probar si el código funciona en otro sistema operativo. Teóricamente sí funciona.


package gsolano;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
*
* Class used to synchronize two directories. One directory (source)
* is used as base of another directory (target).
* The class determines the operations required to leave the target
* with the same structure as the source.
*
* @author gsolano
*
*/
public class Synchronizer {

public static void main(String[] args) {
Synchronizer.run(args[0], args[1]);
}

public static void run(String source, String target) {
System.out.println("Scanning source directory...");
Map<String, Long> sourceFiles = Dir.scan(source);
System.out.println("[DONE]");

System.out.println("Scanning target directory...");
Map<String, Long> targetFiles = Dir.scan(target);
System.out.println("[DONE]");

List<String> newFilesToCopy = getNewFilesToCopy(sourceFiles.keySet(), targetFiles.keySet());
System.out.println("Total new files to copy: " + newFilesToCopy.size());

List<String> filesToUpdate = getFilesToUpdate(sourceFiles, targetFiles);
System.out.println("Total files to update: " + filesToUpdate.size());

List<String> filesToRemove = getFilesToRemove(sourceFiles.keySet(), targetFiles.keySet());
System.out.println("Total files to remove: " + filesToRemove.size());

List<String> dirsToRemove = getDirectoriesToRemove(sourceFiles.keySet(), targetFiles.keySet());
System.out.println("Total dirs to remove: " + dirsToRemove.size());

System.out.println("Copying new files...");
for(String fileToCopy : newFilesToCopy) {
try {
FileCopy.copy(source + File.separator + fileToCopy,
target + File.separator + fileToCopy, false);
} catch (IOException e) {
System.out.println("Couldn't copy file: " + fileToCopy + "(" + e.getMessage() + ")");
}
}

System.out.println("Updating files...");
for(String fileToUpdate : filesToUpdate) {
try {
FileCopy.copy(source + File.separator + fileToUpdate,
target + File.separator +fileToUpdate, true);
} catch (IOException e) {
System.out.println("Couldn't copy file: " + fileToUpdate + "(" + e.getMessage() + ")");
}
}

System.out.println("Removing files from target...");
for(String fileToRemove : filesToRemove) {
new File(target + fileToRemove).delete();
}

System.out.println("Removing directories from target...");
for(String dirToRemove : dirsToRemove) {
new File(target + dirToRemove).delete();
}
}

/**
* Return the list of directories to be removed. A directory is removed
* if it is present in the target but not in the source.
* @param sourceFiles
* @param targetFiles
* @return
*/
private static List<String> getDirectoriesToRemove(Set<String> sourceFiles,
Set<String> targetFiles) {
List<String> directoriesToRemove = new ArrayList<String>();

Set<String> sourceDirs = buildDirectorySet(sourceFiles);
Set<String> targetDirs = buildDirectorySet(targetFiles);

for(String dir : targetDirs) {
if (!sourceDirs.contains(dir)) {
directoriesToRemove.add(dir);
}
}
return directoriesToRemove;
}

/**
* Return the list of files to be removed.
* A file is removed if it is present in the target
* but not in the source.
* @param sourceFiles
* @param targetFiles
* @return
*/
private static List<String> getFilesToRemove(Set<String> sourceFiles,
Set<String> targetFiles) {
List<String> filesToRemove = new ArrayList<String>();

for (String filePath : targetFiles) {
if (!sourceFiles.contains(filePath) &&
!filePath.endsWith(File.separator + ".")) {
filesToRemove.add(filePath);
}
}
return filesToRemove;
}

/**
* Gets the the list of files missing in the target directory.
* @param sourceFiles
* @param targetFiles
* @return
*/
private static List<String> getNewFilesToCopy(Set<String> sourceFiles,
Set<String> targetFiles) {
List<String> filesToCopy = new ArrayList<String>();

for (String filePath : sourceFiles) {
if (!targetFiles.contains(filePath)) {
if(!filePath.endsWith(File.separator + ".")) {
filesToCopy.add(filePath);
}
}
}
return filesToCopy;
}

/**
* Gets the list of files to be updated according to the last
* modified date.
* @param sourceFiles
* @param targetFiles
* @return
*/
private static List<String> getFilesToUpdate(Map<String, Long> sourceFiles,
Map<String, Long> targetFiles) {
List<String> filesToUpdate = new ArrayList<String>();
Iterator<Map.Entry<String, Long>> it = sourceFiles.entrySet().iterator();

while (it.hasNext()) {
Map.Entry<String, Long> pairs = it.next();
String filePath = pairs.getKey();
if (targetFiles.containsKey(filePath) &&
!filePath.endsWith(File.separator + ".")) {
long sourceModifiedDate = sourceFiles.get(filePath);
long targetModifiedDate = targetFiles.get(filePath);

if(sourceModifiedDate != targetModifiedDate) {
filesToUpdate.add(filePath);
}
}
}
return filesToUpdate;
}

/**
* Returns the set of directories contained in the set of file paths.
* @param files
* @return Set of directories representing the directory structure.
*/
private static Set<String> buildDirectorySet(Set<String> files) {
Set<String> directories = new HashSet<String>();
for(String filePath : files) {
if (filePath.contains(File.separator)) {
directories.add(filePath.substring(0,
filePath.lastIndexOf(File.separator)));
}
}
return directories;
}
}

Salida: