domingo, 28 de febrero de 2010

Un sueño bizarro

Pues hoy en la clásica siesta de sábado por la tarde tuve un sueño muy bizarro, que raya en lo surrealista, el cual relataré aquí porque la verdad no quisiera olvidarlo, va a ser muy complicado de relatar, así que de antemano me disculpo por los horrores de redacción.

El sueño comienza conmigo haciendo cola en un aeropuerto, la verdad no se de donde, aunque según yo era de algún lugar soviético, delante de mí estaba un alemán y una fila interminable de pasajeros aguardando algo, seguramente el check point de seguridad, aunque no vi ningún arco o detector de metales, la fila se formaba con las clásicas vallas de cinta en zig zag como en todo aeropuerto, y también detrás de mí se encontraba una larga fila de personas. La fila en sí estaba estática, y yo me encontraba platicando con el alemán, no recuerdo en que idioma pero estabamos teniendo una conversación fluida e intrascendente acerca del país y de como habíamos disfrutado el viaje.

De repente se acercan unas personas trajeadas, traje negro algunos y otros pardo oscuro, eso si de lentes oscuros y bastante altos, preguntando si alguno de nosotros quería comprar algo en las tiendas de Duty Free. La persona atrás de mí, le mencionó a uno de los trajeados que le gustaría comprar un par de botellas de alcohol para llevar a su patria, en ese momento el señor trajeado le puso una esposa en su muñeca, y se puso él la otra en la suya, de tal manera que estuvieran atados, y se lo llevó, otro señor de atrás manifestó el mismo interés y el otro señor trajeado hizo lo mismo, lo esposó y se lo llevó. En ese momento el alemán me comenta algo así como "es impresionante el control que tienen sobre el alcohol aquí", y yo le mencioné algo como "si, tienen normas muy estrictas definitivamente".

En eso comenzó la fila a moverse, pero muy rápido, yo iba siguiendo al alemán a trote rápido, y de pronto me puse a correr porque todos iban muy rápido, incluso se fue moviendo tan rápido que dejé de seguir la línea y empecé a pasarme por debajo de las cintas porque de plano ya estaban vacías. La siguiente escena, era una especie de sala de espera, con sillas naranjas de plástico medio ergonómicas, como si fueran al estilo de un salón de clase, el alemán se sentó delante de mí, y yo le comenté algo como "va a ser genial volar en una aerolínea rusa, nunca me he subido a alguno de sus aviones", el sin siquiera voltear hacia atrás me mencionó "si, supe que tienen los nuevos aviones Zero" (cabe mencionar que los Zero son aviones de guerra de la segunda guerra mundial), yo le pregunté confundido "Zero? como son esos?", inclina un poco la cabeza hacia atrás pero fue como si hablara al techo "Son como un DC-9, dos motores", y volvió hacia delante.

Yo tomé una revista, y me puse a leerla, naturalmente no podía distinguir nada, ni fotos ni letras (odio que en los sueños pase eso), pero de repente se acerca una amiga, que llamaremos M, y me menciona "ven conmigo, te voy a llevar por un atajo sino no vamos a llegar al abordaje", salgo corriendo con ella y entramos a un pasillo. El pasillo más bien era como un túnel, tenía unos rieles enmedio y estabamos caminando por una pequeña plataforma en la orilla derecha, ibamos platicando varias cosas, las cuales la verdad no recuerdo, y de pronto el túnel se ensancha, hasta ser como un cuarto muy amplio, soportado por columnas, pero con el techo bastante bajo, y lleno de rieles por todos lados, incluso había algunos trenes detenidos y otros tantos caminaban a lo lejos.

M me menciona "ten cuidado con las barras guía, no las toques por que te electrocutas", y empezamos a brincar por entre las vías, al principio eran paralelas, todas las vías yendo hacia nuestra misma dirección, pero pronto hubieron otras transversales, luego diagonales, hasta que llegó un punto que todo parecía una telaraña de vías, y luego había tan poca separación entre estas que empecé a caminar de puntitas para no tocarlas. De pronto veo una especie de Y donde se empieza a bifurcar un pasillo, éste sin vías, y le digo "ven por acá, seguro llegamos más rápido".

Caminamos por el pasillo, y al fondo vemos un punto de control de pasaportes, con varios policias atendiendo a los viajeros, el problema es que llegamos por detrás, es decir, veíamos a los policias sentados de espaldas, y los viajeros del otro lado de los escritorios, M me dice "no podemos pasar por ahí, ven por acá", y viramos ligeramente a la izquierda, donde había una puerta de cristal, pasamos por ella, y llegamos a un pasillo ya más estilo aeropuerto, bien iluminado, con macetas con plantas y demás, el punto de control nos quedaba a mano derecha pero tomamos por la izquierda dirigiéndonos a otra puerta de cristal. Ella se detuvo justo enfrente de la puerta, y me dice "Espera, voy al baño", y se dirige hacia el otro lado, hacia el punto de control, en eso, había una persona de intendencia y le dice un piropo en español, algo así como "Mamacita, etc etc" y yo le respondo en voz alta "Tranquilo wey, que hablamos español", el me voltea a ver con despecho, baja la cabeza, y sigue limpiando.

M se pierde un poco después del punto de control, y vuelve a regresar, como si hubiera ido al baño instantaneamente, pero en mi sueño, aunque la seguía reconociendo como mi amiga y no noté ningún cambio, físicamente se veía como otra amiga, D, ya no dijimos nada, cruzamos la puerta de cristal, y yo tomé un carrito de equipaje, de esos de aeropuerto, y caminamos un poco por un pasillo todavía más iluminado, y no tenía techo, se veía un cielo claro con nubes a lo lejos.

Caminamos un corto tramo y empezamos a ver muchas tiendas de campaña, a la mitad de dicho pasillo, todas eran rojas, y afuera de ellas habían varios chavos dormidos en sleeping bags justo afuera de ellas, todos los sleepings eran rojos o morados. Al principio fuimos sacándoles la vuelta, yo con mi carrito y todo, pero después cargué el carrito e ibamos brincando entre los sleepings.

Ibamos también platicando D y yo, no recuerdo de que cosas, pero muy a gusto, mientras brincábamos los sleeping y yo en mis pensamientos tratando de mantener el equilibrio con el carrito para que no se me cayera y fuera a desgraciarle la cara y el sueño a alguien. De pronto todo se desvanece en blanco, y en la siguiente escena, estaba visualizando el avión, ya volando, era como una especie de DC-10, la cola era azul marino y el resto del avión era blanco, una de las ventanillas tenía un contorno ligeramente más oscuro, y en eso escucho la voz de D (no la veía a ella ni a nadie más, solo veía el avión volar en el cielo), "oye, como le hicimos para vernos desde fuera?", y yo le respondí "no se".

Desperté, y la verdad se me quedó muy grabado, no se que trascendencia ni que significado pueda tener, pero está divertido no? :P

miércoles, 24 de febrero de 2010

A DSL for a data importing tool

Continuing with our OpenTaps examples, we want to generate some seed data for our application, the good thing is that the OfBiz framework offers some tooling to import data nice and easy, the not so nice part of it, is that it must be done using xml files, like the example below:


<entity-engine-xml>

<Product productId="SUPPLY-001" productTypeId="SUPPLIES" productName="Pencil" internalName="Pencil" description="Office pencil (demo supply)."
taxable="N" chargeShipping="N" autoCreateKeywords="N" isVirtual="N" isVariant="N" createdDate="2007-01-01 00:00:00.000" createdByUserLogin="admin"/>

<ProductPrice productId="service1" productPriceTypeId="DEFAULT_PRICE" productPricePurposeId="PURCHASE" currencyUomId="USD" productStoreGroupId="_NA_" fromDate="2008-01-01 00:00:00" price="10.00"/>
</entity-engine-xml>
This will insert a new Product entity (or update an existing one, if the primary key already exists), modifying the fields described in the attributes of the product tag (productId, productTypeId, productName, etc.), and will also modify or create a ProductPrice entity. It sounds pretty simple (and it really is), but to be honest, what would happen if we want to have a huge amount of data, and also provided by people that isn't exactly technical, it would be really hard to write all that xml, so lets use the power of groovy and a few more tools, to create a DSL that will help us parse an spreadsheet file (in this case OpenOffice calc) and automatically generate the data xml we need.

First of all, lets organize how we are going to import the data:
  • Data will be located in an OpenOffice Calc file, each sheet will represent one or more entities
  • Each column will correspond to one field (or more) of our entity
  • Each row will be a complete object
  • Our DSL will help us determine how our sheet will be parsed to generate an OO representation
  • Then we will export that OO representation into xml files
So, for this task, we will need the following libs and frameworks:
  • Groovy, I really used Grails because I wanted to make an small app with a nice frontend for the conversion
  • JOpenDocument so we can parse OpenOffice Calc files in a nice way
  • OpenTaps, so we can test our data files
Now, we need to define a domain model that will be used by our DSL to store the configuration of how to parse the Calc file, so we will create two java classes, the first one we will name it ODSNode and it will store the generic information about the sheet we are parsing, it will be like this:


public class ODSNode {
//Constants we will use later
public static final String SHEET_NAME_ATTR="name";
public static final String OFBIZ_NAME_ATTR="entity";
public static final String START_ROW_ATTR="startRow";


private int startRow=1;
private int index=0;

private String sheetName;
private String ofbizEntity;

private List<ODSElement> elements = new ArrayList<ODSElement>();
//getters and setters here...
}


The fields will have the following meaning:
  • startRow -> This is the row at which we will start parsing to get the data from the sheet
  • index -> The order in which the results will be put, so maybe some data in other sheets will have precedence than this one
  • sheetName -> The name of the sheet we will be parsing
  • ofbizEntity -> The name of the entity we will be populating from the data parsed
The node will be composed of many elements that correspond to the fields of the entity, so we will create the file ODSElement:


public class ODSElement {
//Some constants we will use later
public static final String COLUMN_ATTR="name";
public static final String INDEX_ATTR="index";
public static final String OFBIZ_ATTR="attribute";
public static final String CONSTANT="constant";

private String columnName;
private int columnIndex;
private Object constant;
private String ofbizColumn;
}


The fields have the following meaning:
  • columnName -> I'm not using it right now, just as a placeholder to be aware of which column I'm modifying
  • columnIndex -> The corresponding location of the column inside the sheet
  • constant -> In some cases we won't parse information from the spreadsheet and we will insert a constant value for all the rows in our entity
  • ofbizColumn -> This is the real name of the column or entity attribute for the Ofbiz Framework
Now, taking advantage of the great class BuilderSupport provided by groovy, we will create our own Builder, called ODSOfbizBuilder (I'll only show the methods I actually use):


public class ODSOfbizBuilder extends BuilderSupport{

private List<ODSNode> nodes = new ArrayList<ODSNnode>();

private static final String SHEET_NODE ="sheet";
private static final String COLUMN_NODE ="column";

//Return the list of nodes generated by this builder
public List<ODSNode> getNodes() {
return nodes;
}

//Create the corresponding node or element
@SuppressWarnings("unchecked")
@Override
protected Object createNode(Object name, Map attributes) {
System.out.println(name+" "+attributes);
Object obj=null;
if(name.equals(SHEET_NODE)){
ODSNode node = new ODSNode();
node.setSheetName(attributes.get(ODSNode.SHEET_NAME_ATTR).toString());
node.setOfbizEntity(attributes.get(ODSNode.OFBIZ_NAME_ATTR).toString());
if(attributes.containsKey(ODSNode.START_ROW_ATTR)){
node.setStartRow(Integer.parseInt(attributes.get(ODSNode.START_ROW_ATTR).toString()));
}
obj = node;
nodes.add(node);
}
else if(name.equals(COLUMN_NODE)){
ODSElement element = new ODSElement();
element.setColumnIndex(Integer.parseInt(attributes.get(ODSElement.INDEX_ATTR).toString()));
element.setColumnName(attributes.get(ODSElement.COLUMN_ATTR).toString());
element.setOfbizColumn(attributes.get(ODSElement.OFBIZ_ATTR).toString());
if(attributes.containsKey(ODSElement.CONSTANT)){
element.setConstant(attributes.get(ODSElement.CONSTANT));
}
obj =element;
}
else{
throw new IllegalArgumentException("Nodes must be named "+SHEET_NODE+" or "+COLUMN_NODE);
}
return obj;
}

//Add the elements to the corresponding node
@Override
protected void setParent(Object parent, Object node) {
if(parent instanceof ODSNode && node instanceof ODSElement){
((ODSNode) parent).addElement((ODSElement) node);
}
}

}


This class will allow us to parse our DSL and actually create a list of nodes that will help us parse a Calc file and get all data about them, so a Groovy code using that DSL would look like this:


def builder = new ODSOfbizBuilder()
builder.base{
sheet(name:'CatalogoUnidades',entity:"Uom",index:0){
column(name:"Constante",index:-1,attribute:"uomTypeId",constant:"WEIGHT_MEASURE")
column(name:"Unidad",index:0,attribute:"description")
column(name:"Simbolo",index:1,attribute:"abbreviation")
column(name:"ID",index:2,attribute:"uomId")
}
}


So now that we have the template ready, lets create now the domain model to store in memory the data parsed, first we will create an OfbizEntity class that will store the generic data about our entity:


public class OfbizEntity implements Comparable<OfbizEntity>{

private String entity;
private int index;
private Set<OfbizData> data = new TreeSet<OfbizData>();
//Getters and setters here
}


The class will store the name of the entity, and the order in which it should be stored in the xml file, and of course a set of OfbizData objects which will have the data we will store, and will have the following code in it:


public class OfbizData implements Comparable<OfbizData>{

private String attribute;
private String value;
//getters and setters here
}


So now we will create a class named ODSReader that will read our spreadsheet and create our model in memory:


public class ODSReader {

public List<OfbizEntity> parseFile(File spreadsheetFile,ODSOfbizBuilder builder) throws IOException{
//Here we will store all our data
List<OfbizEntity> data = new ArrayList<OfbizEntity>();
//Let's open the Calc file
SpreadSheet spreadSheet = SpreadSheet.createFromFile(spreadsheetFile);
//Let's get the nodes we parsed from our builder earlier
List<ODSNode> nodes = builder.getNodes();

for(ODSNode node : nodes){
//We need to get the sheet referenced by its name
Sheet sheet = spreadSheet.getSheet(node.getSheetName());
if(sheet == null){
throw new IllegalArgumentException("Sheet with name "+node.getSheetName()+" not found");
}
//lets get the last row with data
int lastRow = sheet.getRowCount();
int startRow = node.getStartRow();
int valueCount = 0;
//for each of the parsed sheets we need the collection of entities
Set<OfbizEntity> entities = new TreeSet<OfbizEntity>();
for(int row = startRow;row<lastrow;row++){ valuecount="0;" ofbizentity="" entity="new" odselement="" element="" string="" if="" column="" is="" a="" we="" just="" put="" the="" constant="" value="" in="" there=""><0||element.getconstant()!=null){ value =" element.getConstant().toString();" cell =" sheet.getCellAt(element.getColumnIndex()," value="cell.getValue().toString();" tuple =" new">0&&valueCount>0){
entities.add(entity);

}
}
//We used a set to ensure there are no duplicates and now we add all its elements to our list
data.addAll(entities);
}
return data;
}
}


So now we can load the data in our calc file, to memory, now it is time to persist it in an Xml file, for that we will help ourselves with the excellent groovy StreamingMarkupBuilder, so lets create our OfbizEntityGenerator groovy class:


lass OfbizEntityGenerator {

String output="outputDemoData.xml"

//We receive the list of entities and parse them into an xml file
public StreamingMarkupBuilder generateOfBizDataModel(List<OfbizEntity> entities){
StreamingMarkupBuilder builder = new StreamingMarkupBuilder()
println entities.size()
def writable = builder.bind{
"entity-engine-xml"{
entities.each{
ent ->
//Here we write a comment for our entity
mkp.comment("Data for entity ${ent.entity}")
//We write the entity with its attributes
"${ent.entity}"(asMap(ent))
}
}
}

def outputString = indentXml(writable)

//And finally we write it to a physical file
def file = new FileWriter(output)

file.write(outputString)
file.flush()
file.close()
}
//We just convert our list of objects to a map, not very elegant to be honest
Map asMap(OfbizEntity entity){
def map = [:]
entity.data.each{
node ->
map[node.attribute] = node.value
}
return map
}
//We use this helper method to have a nice indentation of our xml file
String indentXml(xml) {
def factory = TransformerFactory.newInstance()
factory.setAttribute("indent-number", 2);

Transformer transformer = factory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, 'yes')
StreamResult result = new StreamResult(new StringWriter())
transformer.transform(new StreamSource(new ByteArrayInputStream(xml.toString().bytes)), result)
return result.writer.toString()
}
}


So the complete usage example of our DSL and utils would be like this:


def reader = new ODSReader()
File file = new File("/citsa/opentaps/myfile.ods");
def builder = new ODSOfbizBuilder()
builder.base{
sheet(name:'CatalogoUnidades',entity:"Uom",index:0){
column(name:"Constante",index:-1,attribute:"uomTypeId",constant:"WEIGHT_MEASURE")
column(name:"Unidad",index:0,attribute:"description")
column(name:"Simbolo",index:1,attribute:"abbreviation")
column(name:"ID",index:2,attribute:"uomId")
}
}
def result = reader.parseFile(file, builder)
OfbizEntityGenerator generator = new OfbizEntityGenerator()
generator.output = "products.xml"
generator.generateOfBizDataModel(result)


And having the following ods file:



It will generate the following xml file:


<?xml version="1.0" encoding="UTF-8"?>
<entity-engine-xml>
<Uom abbreviation="g" description="gramo" uomId="1.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="lb" description="libra" uomId="2.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="kg" description="kilogramo" uomId="3.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="M" description="miles de semilas" uomId="4.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="ml" description="mililitro" uomId="5.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="l" description="litro" uomId="6.0" uomTypeId="WEIGHT_MEASURE"/>
<Uom abbreviation="docena" description="docena" uomId="7.0" uomTypeId="WEIGHT_MEASURE"/>
</entity-engine-xml>


Cool isn't it? now try to do it with a spreadsheet with a couple thousand rows, and you will see the real benefit of this.

Now tell OpenTaps that you have a new file, copy the xml (for our example it will be named products.xml) and paste it into your hot-deploy/citsaProduct/data folder and edit your ofbiz-component.xml to add the following line


<entity-resource type="data" reader-name="ext" loader="main" location="data/products.xml"/>


Finally, to tell OpenTaps to load it, just run the following script in your opentaps home:

java -Xmx384M -jar ofbiz.jar -install -readers=ext

And voilá, you can load a ton of seed data without much effort

More references:

Modifying and creating new data entities (OpenTaps)

So now we will create some new data entities for our OpenTaps installation.

Our main products are seeds, and, as you already know, they are living beings that need some special care and control while handling them. Not all seeds grow and some may die as time passes, so we need to control the germination rate of the seeds we handle.

Each variety of seed has different tolerances for the germination rate, so, some kind of seeds may be sold with a 99% of germination rate, while others would be ok with just an 80%. So we need a new field for our product entity to handle the minimum germination rate accepted for each of our products.
So we will create two new files in our citsaProduct module:



  • entitymodel.xml -> Here we will describe our new entities, and their data types and fields
  • entitygroup.xml -> Here we will configure entity groups, it usually works if we want to use different databases for our entities, but for now we will use only one database.
So first of all, lets add a new field to the Product entity, but the problem is that Product is defined in the file applications/product/entitydef/entitymodel.xml but we don't want to modify that file, it would cause trouble in future upgrades, so fortunately the framework lets us extend an existing entity with new fields without touching the original configuration file, so lets modify our own entitymodel.xml and add the following text:


<?xml version="1.0" encoding="UTF-8"?>
<entitymodel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ofbiz.org/dtds/entitymodel.xsd">

<extend-entity entity-name="Product">
<field name="germinationRate" type="fixed-point"/>
</extend-entity>

</entitymodel>

As simple as that, we use the tag extend-entity to add new fields and keys to an existing entity, in this case, we are adding a new field named germinationRate with a data type of fixed-point

Look at how the data types don't look like SQL data types, the framework has some common or generic definitions that will be translated to true SQL data types at runtime, you can look the real data types by looking at the xmls located in the folder /opentaps-1.4/framework/entity/fieldtype

Now we want to make a new entity named "Package", just to make an example on how to create new entities, so lets modify again our entitymodel.xml file and add the following text:


<?xml version="1.0" encoding="UTF-8"?>
<entitymodel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ofbiz.org/dtds/entitymodel.xsd">

<entity entity-name="Package" package-name="org.ofbiz.citsa">
<field name="packageTypeId" type="id-ne"/>
<field name="name" type="name"/>
<field name="shortName" type="name"/>
<prim-key field="packageTypeId"/>
</entity>

<extend-entity entity-name="Product">
<field name="germinationRate" type="fixed-point"/>
</extend-entity>

</entitymodel>
Finally, we have to modify the entitygroup.xml file to tell it to recognize our new Package entity:



<?xml version="1.0" encoding="UTF-8"?>
<entitygroup xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ofbiz.org/dtds/entitygroup.xsd">
<entity-group entity="Package" group="org.ofbiz"/>
</entitygroup>

As a last step, we will need to modify the file ofbiz-component.xml of our module, so we can tell the Ofbiz Framework to load our new data entities:


<?xml version="1.0" encoding="UTF-8" ?>
<ofbiz-component name="citsaProduct"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ofbiz.org/dtds/ofbiz-component.xsd">

<resource-loader name="main" type="component"/>

<classpath location="build/classes/*" type="dir"/>
<classpath location="config" type="dir"/>

<entity-resource loader="main" location="entitydef/entitymodel.xml" reader-name="main" type="model"/>
<entity-resource loader="main" location="entitydef/entitygroup.xml" reader-name="main" type="group"/>

</ofbiz-component>


After all this steps we should run the following script:

ant make-base-entities

This will generate Hibernate entity files in the folder hot-deploy/opentaps-common/src/entities that we will later use in our code

Finally, we just need to restart our server, and it should alter the Product table and create a new table named "Package"

Creating a new module (OpenTaps)

Welcome to my new blog, I hope Blogger offers a better interface and tools than Roller, so, let's get started with a new series of OpenTaps entries, this will be a little more focused on customizing an existing instance of Opentaps, while trying to guarantee some code independence so you can gracefully upgrade to new versions without much trouble.

Just a little quick introduction, the customization I'm making is for my family business, which is a seed trading company, so while its business model is quite common, it has a few differences that make it interesting and a nice example to use for our exercises.

So, first of all, we are going to work with the Product module, we don't want to directly modify it so we will make a new module based on it, to do this, we will make a folder in the hot-deploy directory, and for this example we will name it citsaProduct:

On it, we will create the following new files and folders:
  • ofbiz-component.xml -> This file contains all our main configuration of our new module, basically where to load the services, webapps, data, etc.
  • build.xml -> This file contains the Ant configuration to compile and build our module, for now, copy the build.xml from the application/party module, and it should be enough
  • src (folder) -> Our java sources will be located here, although for now it will be empty
  • data (folder) -> Here we will put our custom seed data, in a later blog post I'll show how to create a small app to quickly create this data from a spreadsheet file
  • entitydef (folder) -> Here we will put the configuration of our new and custom data entities
So, first of all we have to edit our ofbiz-component.xml file, for now lets put the following content:


<?xml version="1.0" encoding="UTF-8" ?>
<ofbiz-component name="citsaProduct"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ofbiz.org/dtds/ofbiz-component.xsd">
<resource-loader name="main" type="component"/>
<classpath location="build/classes/*" type="dir"/>
<classpath location="config" type="dir"/>
</ofbiz-component>


The resource-loader tag helps us have a common base from where to look for the files that we configure here

The classpath tag will tell the Ofbiz Framework (in which OpenTaps is based on) where to find java classes, jars and other configuration files and put them in the classpath.

We also have to tell the Ofbiz Framework to load the module at application start, so we need to modify the file hot-deploy/component-load.xml


<component-loader xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/component-loader.xsd">
<load-component component-location="opentaps-common"/>
<load-component component-location="crmsfa"/>
<!-- Some more components here.... -->
<!-- CITSA -->
<load-component component-location="citsaProduct"/>
</component-loader>
So you should be able to run startofbiz.sh and look in the logs for an entry like this:

2010-02-11 16:04:30,113 (main) [ ComponentContainer.java:218:INFO ] Loading component : [citsaProduct]

So that means that our module is loading