Archive for the '16581' Category

AjCoRe, un simple Repositorio de Contenido (2) Almacenando en Stores

Thursday, December 22nd, 2011

Anterior Post

En mi semana sabática hice algunos avances en mi proyecto de código abierto AjCore, una implementación simple de un repositorio de contenido escrito en C#:

https://github.com/ajlopez/AjCoRe

Si Content Repository es un nuevo concepto para Uds., pueden ver mis enlaces http://delicious.com/ajlopez/contentrepository y la introducción al tema escrita por Roy Fielding JSR-170 overview .

Los conceptos principales: hay Workspaces. Cada workspace tiene un Root Node (nodo raíz). Cada Node puede tener Child Nodes (nodos hijos), y Properties. Una Property tiene un Name y un Value:

En el diagrama de arriba, nodos y espacios de trabajo están represetnados por interfaces. La idea es tener diferentes implementaciones de esas abstracciones. En mi anterior post presenté dos implementaciones: una representando un sistema de archivos/directorios, permitiendo solamente operaciones de lecturas. Y otra implementación, más extensible, usando nodos arbitrarios en memoria. Desde entonces, agregué el soporte de grabar y recuperar nodos arbitrarios usando un almancén, un lugar de persistencia.

Ahora, el proyecto está así:

donde:

A: Es la implementación de las clases concretas de workspaces y nodes en memoria, con el nuevo soporte OPCIONAL de tener un store.

B: La implementación de solo lectura de espacio de trabajo representando un directorio, donde los nodos son más directorios y archivos.

C: El nuevo IStore (abstracto) y la primer implementación concreta, usando una jerarquía de directorios y archivos XML para persistir las propiedades de un nodo.

D: El soporte de transacciones que dura la vida de una sesión.

Veamos las nuevas capacidades de almacenamiento. La interfaz  Stores.IStore:

public interface INode
{
    string Name { get; }
    INode Parent { get; }
    PropertyList Properties { get; }
    NodeList ChildNodes { get; }
    string Path { get; }
}

Stores.Xml.Store es la primer implementación de esta abstracción. Uds. podrían agregar otras, por ejemplo usando JSON, una base de datos relacional, o un NoSQL. Pueden crear, remover nodos y actualizar sus propiedades, dentro de una sesión, igual que en el anterior posts. Pero ahora, el espacio de trabajo puede ser inyectado con una implementación de IStore, que comienza a usarse entonces automáticamente. Ejemplo (ver más detalle en el código de los tests):

// Reference store in a directory
Store store = new Store("c:\\myworkspace");
// Workspace using that store to retrieve root node and their descendant
// (lazy loading)
Workspace workspace = new Workspace(store, "myws");
// Session accesing that workspace
Session session = new Session(workspace);
// You can use session to get the root node
// in case you have no direct workspace reference
INode root = session.Workspace.RootNode;
// Updates are made into a transaction
using (var tr = session.OpenTransaction())
{
    // Accessing a node
    INode node = root.ChildNodes["father"];
    // Changing a property
    session.SetPropertyValue(node, "Name", "Adam");
    
    // Creating a nodes    
    INode newnode = session.CreateNode(node, "newson", new Property[] {
                        new Property("Name", "Abel")
                    });
    
    // Removing a node
    session.RemoveNode(newson);
    tr.Complete();
}

El tr.Complete()  es el encargado de actualizar los archivos XML con las propiedades de los nodos que cambiaron. Un archivo de ejemplo:

<?xml version="1.0" encoding="utf-8"?>
<Properties>
  <Name>John</Name>
  <Age type="int">35</Age>
  <Male type="bool">true</Male>
  <Hired type="datetime">2000-01-01T00:00:00</Hired>
  <Height type="double">167.2</Height>
  <Salary type="decimal">120000.5</Salary>
</Properties>

El código en Transaction.Complete:

var nodestoupdate = 
    this.operations.Where(op => !(op is RemoveNodeOperation)).Select(op => op.Node).Distinct();
var nodestodelete = 
    this.operations.Where(op => op is RemoveNodeOperation).Select(op => op.Node).Distinct();
nodestoupdate = nodestoupdate.Except(nodestodelete);
foreach (var node in nodestoupdate)
    this.store.SaveProperties(node.Path, node.Properties);
foreach (var node in nodestodelete)
    this.store.RemoveNode(node.Path);

Podría mejorarla, tomando en cuante que algunas implementaciones de IStore podría preferir actualizar propiedades, en lugar de un nodo completo (por ejemplo, una implementación que use como almacén una base de datos).

Quiero escribir otra implementación de IStore usando JSON (debería pensar cómo guardar los tipos originales de cada propiedad). Otro trabajo pendiente: recuperar un node desde un espacio de trabajo sabiendo su path completo, y permitir hacer consultas de nodos (¿usando XPath? Estoy todavía decidiendo).

Si ven los commit en el repositorio, verán cómo fue implementado todo esto de almacenamiento usando TDD (Test-Driven Development).

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez

AjCoRe, un simple Repositorio de Contenido (1) Primeros Pasos

Wednesday, December 7th, 2011

Siguiente Post

Hace unos años, me topé con el proyecto Apache Jackrabbit, de código abierto que implementa la JSR170 (vean mis primeros links (2008) en http://delicious.com/ajlopez/jsr170), pero no le presté mucha atención. La semana pasada, en una lista de correo privada, el tema de repositorio de contenido apareció de nuevo, así que estuve leyendo algunos enlaces:

http://en.wikipedia.org/wiki/Content_repository
http://en.wikipedia.org/wiki/Content_repository_API_for_Java
http://www.jcp.org/en/jsr/detail?id=170
http://jcp.org/en/jsr/detail?id=283

Más enlaces en

http://delicious.com/ajlopez/contentrepository

El primer “paper” que leí fue la introducción de Roy Fieldieng:

http://www.day.com/content/dam/day/whitepapers/JSR_170_White_Paper.pdf

El segundo (más detallado, es una especificación) es la JSR283:

http://download.oracle.com/otndocs/jcp/content_repository-2.0-pfd-oth-JSpec/

Despues de leer por arriba ambos “paperes”, empecé a pensar cómo implementar esas ideas (usando .NET; hay un proyecto de código abierto en .NET, vean SenseNet). No ví el código o la API definida en esos JSRs. Quiero llegar a una clara y simple idea de qué es lo esencial, cuáles son los conceptos núcleo a implementar. Así que el pasado fin de semana, me dediqué a un code kata: mis primeros pasos en AjCoRe, un simple repositorio de contenido:

https://github.com/ajlopez/AjCoRe

usando TDD para practicarlo (pueden ver la evolución del código en los logs de Git).

Los principales puntos son:

– Hay Workspaces identificado por nombre
– Todo Workspace tiene un Root Node
– Un Node tienen propiedades
– Una Property tiene un nombre y un valor (un valor simple, como String, DateTime, int, no es un objeto complejo)
– Un Node puede tener Child Nodes (una enumeración que puede estar vacía)

Inicialmente, en mis primeros tests, yo podía crear un Node directamente usando un constructor público. Pero en el estado actual, prefiero usar una entrada controlada para las principales operaciones, una Session. El código cliente crea una Session que maneja una Workspace.

Ahora, tengo DOS implementaciones de Workspace y Nodes (luego de un paso de refactoreo, tengo ahora un INode y un IWorkspace, ambas interfaces, y clases concretas que las implementan). Como prueba de concepto (mencionado en el “paper” de Fielding), implementé un directorio en un FileSystem, represantado por nodes de sólo lectura, con nodes FileNode y DirectoryNode. Algunas pruebas:

[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void RootNodeProperties()
{
    Workspace workspace = new Workspace("fs", "fs");
    INode root = workspace.RootNode;
    DirectoryInfo info = new DirectoryInfo("fs");
    Assert.AreEqual(info.Extension, root.Properties["Extension"].Value);
    Assert.AreEqual(info.FullName, root.Properties["FullName"].Value);
    Assert.AreEqual(info.Name, root.Properties["Name"].Value);
    Assert.AreEqual(info.CreationTime, root.Properties["CreationTime"].Value);
    Assert.AreEqual(info.CreationTimeUtc, root.Properties["CreationTimeUtc"].Value);
    Assert.AreEqual(info.LastAccessTime, root.Properties["LastAccessTime"].Value);
    Assert.AreEqual(info.LastAccessTimeUtc, root.Properties["LastAccessTimeUtc"].Value);
    Assert.AreEqual(info.LastWriteTime, root.Properties["LastWriteTime"].Value);
    Assert.AreEqual(info.LastWriteTimeUtc, root.Properties["LastWriteTimeUtc"].Value);
    Assert.AreEqual("fs", workspace.Name);
    Assert.IsNotNull(workspace.RootNode);
    Assert.AreEqual(string.Empty, workspace.RootNode.Name);
    Assert.IsNull(workspace.RootNode.Parent);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetFilesFromRoot()
{
    Workspace workspace = new Workspace("fs", "fs");
    INode root = workspace.RootNode;
    Assert.IsNotNull(root.ChildNodes["TextFile1.txt"]);
    Assert.IsNotNull(root.ChildNodes["TextFile1.txt"]);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetFileProperties()
{
    Workspace workspace = new Workspace("fs", "fs");
    INode root = workspace.RootNode;
    INode file = root.ChildNodes["TextFile1.txt"];
    FileInfo info = new FileInfo("fs/TextFile1.txt");
    Assert.AreEqual(info.Extension, file.Properties["Extension"].Value);
    Assert.AreEqual(info.FullName, file.Properties["FullName"].Value);
    Assert.AreEqual(info.Name, file.Properties["Name"].Value);
    Assert.AreEqual(info.CreationTime, file.Properties["CreationTime"].Value);
    Assert.AreEqual(info.CreationTimeUtc, file.Properties["CreationTimeUtc"].Value);
    Assert.AreEqual(info.LastAccessTime, file.Properties["LastAccessTime"].Value);
    Assert.AreEqual(info.LastAccessTimeUtc, file.Properties["LastAccessTimeUtc"].Value);
    Assert.AreEqual(info.LastWriteTime, file.Properties["LastWriteTime"].Value);
    Assert.AreEqual(info.LastWriteTimeUtc, file.Properties["LastWriteTimeUtc"].Value);
}
[TestMethod]
[DeploymentItem("Files/FileSystem", "fs")]
public void GetDirectoriesFromRoot()
{
    Workspace workspace = new Workspace("fs", "fs");
    INode root = workspace.RootNode;
    Assert.IsNotNull(root.ChildNodes["Subfolder1"]);
    Assert.IsNotNull(root.ChildNodes["Subfolder2"]);
}

Algunas notas:

Workspace usado en el código de arriba es la clase AjCoRe.FileSystem.Workspace .

– Su constructor toma dos argumentos: el nombre del workspace en el repositorio de contenido, y el nombre del directorio (puede ser relativo) a representar.

– Los objetos INode son instancias de las clases concretas FileNode or DirectoryNode.

– Las propiedades de un archivo o directorio se ven reflejadas como valores simples en propiedades. Los valores se toman desde objetos FileInfo  DirectoryInfo de System.IO)

– La propiedad DirectoryNode ChildNodes es calculada dinámicamente: cada invocación a ella calcula cuáles son los archivos y directorios del directorio representado por el nodo:

public NodeList ChildNodes
{
    get
    {
	NodeList nodes = new NodeList();
	foreach (var di in this.info.GetDirectories())
	    nodes.AddNode(new DirectoryNode(this, di.Name, di));
	foreach (var fi in this.info.GetFiles())
 	    nodes.AddNode(new FileNode(this, fi.Name, fi));
	return nodes;
    }
}

Es tiempo de presentar las dos abstracciones principales que fueron surgiendo de este trabajo inicial, INode:

public interface INode
{
    string Name { get; }
    INode Parent { get; }
    PropertyList Properties { get; }
    NodeList ChildNodes { get; }
    string Path { get; }
}

y la interface IWorkspace:

public interface IWorkspace
{
    string Name { get; }
    INode RootNode { get; }
}

Veqn que no necesité tener un identificar único para un nodo dentro de un workspace (como piden las JSR). Cada Node tiene un Path que consiste en los nombres concatenados de la serie de nodos padres, usando / como separador). Debería implementar la recuperación de un nodo en particular usando su trayectoraria. Tampoco necesité todavía tener un NodeType por nodo, describiendo las propiedades obligatorias o sus características. Estoy siguiendo el principio YAGNI 😉

La otra implementación concreta de IWorkspace/INode maneja nodos y propiedades en memoria. Los nodos pueden ser creados y removidos por código, y los valores de las propiedades pueen cambiar. Es mi principal implementación y que quiero extender. La pieza principal a agregar: un IStore que pueda recuperar y grabar los nodos modificados en un almacen de persistencia, con varias implementaciones: en base de datos, en bases NoSql, en archivos JSON dentro de directorios que reflejan la jerarquía de nodos, en la nube, en Azure Blob Storage, en Azure Tables, etc…

La creación de un  AjCoRe.Base.Workspace:

Workspace workspace = new Workspace("ws1", null);

El segundo parémetro es la lista de propiedades para colocar en el nuevo Root Node (que tiene nombre string vacío).

Podemos obtener una sesión:

Session session = new Session(workspace);

Podemos navegar la jerarquía de nodos como en el anterior ejemplo, usando workspace.RootNode y la enumeración ChildNodes. PERO, para modificarlos, debemos usar una transacción:

INode node = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
    session.SetPropertyValue(node, "Name", "Adam");
    session.SetPropertyValue(node, "Age", 800);
    
    tr.Complete();
}

TENEMOS que completar la transacción explícitamente con tr.Complete(). Si no lo hacemos, las propiedades cambiadas volverán a sus anteriores valores. La creación y remoción de nodos también es manejada por la transacción. Podemos crear un nuevo nodo con sus valores iniciales de propiedades:

INode root = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
    INode node = session.CreateNode(root, "person1", new List<Property>()
    {
        new Property("Name", "Adam"),
        new Property("Age", 800)
    });
    
    tr.Complete();
}

O podemos agregar, cambiar, remover (colocando su valor en null) propiedades dentro de una transacción:

INode root = session.Workspace.RootNode;
using (var tr = session.OpenTransaction())
{
    session.SetPropertyValue(root, "Name", "Adam");
    session.SetPropertyValue(root, "Age", 800);
    tr.Complete();
}

Entonces, usando la sesión como punto de entrada a todas las modificaciones, puedo llevar un Unit of Work de esos cambios, sin necesidad de estar observando las propiedades y nodos que el programador recupera (si hubiera adoptado esta otra manera, debería estar vigilando a cada momento cómo un programador obtiene un nodo y lo modifica). Tengo planes de implementar algo como Software Transactional Memory para soportar concurrencia (tengo algo de código así en AjTalk y en AjSharp).

Próximos posts: detalles de implementación (transacciones, factorías de sesiones, registro de los workpsaces, etc…)

Próximos pasos: implementar persistencia en un almacén, y transacciones concurrentes.

Nos leemos!

Angel “Java” Lopez

http://www.ajlopez.com

http://twitter.com/ajlopez