Your message has been sent.
This article has been saved to your account.
Go to my account
This article has been emailed to your Kindle.
Send this article
Complete the form below to send this article, Implementing a Microsoft .NET Application using the Alfresco Web Services, to a friend (or to yourself). We will never share your details (or your friend's) with anyone. For more information, read our Privacy Policy.
This article by Ugo Cei and Piergiorgio Lucidi, authors of the book Alfresco 3 Web Services, will teach you how to use the Alfresco Web Services API from your Microsoft .NET application. It will also show you how to configure your development environment using the open source IDE SharpDevelop. The .NET sample is a standard application that performs operations against the repository.
(For more resources on Alfresco, see here.)
For the first step, you will see how to set up the .NET project in the development environment. Then when we take a look at the sample code, we will learn how to perform the following operations from your .NET application:
- How to authenticate users
- How to search contents
- How to manipulate contents
- How to manage child associations
Setting up the project
In order to execute samples included with this article, you need to download and install the following software components in your Windows operating system:
- Microsoft .NET Framework 3.5
- Web Services Enhancements (WSE) 3.0 for Microsoft .NET
- SharpDevelop 3.2 IDE
The Microsoft .NET Framework 3.5 is the main framework used to compile the application, and you can download it using the following URL:
http://www.microsoft.com/downloads/details.aspx?familyid=333325fd-ae52-4e35-b531-508d977d32a6&displaylang=en.
Before importing the code in the development environment, you need to download and install the Web Services Enhancements (WSE) 3.0, which you can find at this address:
http://www.microsoft.com/downloads/details.aspx?FamilyID=018a09fd-3a74-43c5-8ec1-8d789091255d.
You can find more information about the Microsoft .NET framework on the official site at the following URL: http://www.microsoft.com/net/. From this page, you can access the latest news and the Developer Center where you can find the official forum and the developer community.
SharpDevelop 3.2 IDE is an open source IDE for C# and VB.NET, and you can download it using the following URL:
http://www.icsharpcode.net/OpenSource/SD/Download/#SharpDevelop3x.
Once you have installed all the mentioned software components, you can import the sample project into SharpDevelop IDE in the following way:
Now you should see a similar project tree in your development environment:

More information about SharpDevelop IDE can be found on the official site at the following address: http://www.icsharpcode.net/opensource/sd/. From this page, you can download different versions of the product; which SharpDevelop IDE version you choose depends on the .NET version which you would like to use. You can also visit the official forum to interact with the community of developers.
Also, notice that all the source code included with this article was implemented extending an existent open source project named dotnet. The dotnet project is available in the Alfresco Forge community, and it is downloadable from the following address:
http://forge.alfresco.com/projects/dotnet/.
Testing the .NET sample client
Once you have set up the .NET solution in SharpDevelop, as explained in the previous section, you can execute all the tests to verify that the client is working correctly. We have provided a batch file named build.bat to allow you to build and run all the integration tests. You can find this batch file in the root folder of the sample code.
Notice that you need to use a different version of msbuild for each different version of the .NET framework. If you want to compile using the .NET Framework 3.5, you need to set the following path in your environment:
set PATH=%PATH%;%WinDir%\Microsoft.NET\Framework\v3.5
Otherwise, you have to set .NET Framework 2.0 using the following path:
set PATH=%PATH%;%WinDir%\Microsoft.NET\Framework\v2.0.50727
We are going to assume that Alfresco is running correctly and it is listening on host localhost and on port 8080. Once executed, the build.bat program should start compiling and executing all the integration tests included in this article. After a few seconds have elapsed, you should see the following output in the command line:
...
...
...
*****************
* Running tests *
*****************
NUnit version 2.5.5.10112
Copyright (C) 2002-2009 Charlie Poole.
Copyright (C) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A.
Vorontsov.
Copyright (C) 2000-2002 Philip Craig.
All Rights Reserved.
Runtime Environment -
OS Version: Microsoft Windows NT 5.1.2600 Service Pack 2
CLR Version: 2.0.50727.3053 ( Net 2.0 )
ProcessModel: Default DomainUsage: Single
Execution Runtime: net-2.0
............
Tests run: 12, Errors: 0, Failures: 0, Inconclusive: 0, Time: 14.170376
seconds
Not run: 0, Invalid: 0, Ignored: 0, Skipped: 0
********
* Done *
********
As you can see from the project tree, you have some of the following packages:
- Search
- Crud
- Association
The Search package shows you how to perform queries against the repository.
The Crud package contains samples related to all the CRUD operations that show you how to perform basic operations; namely, how to create/get/update/remove nodes in the repository.
The Association package shows you how to create and remove association instances among nodes.
Searching the repository
Once you have authenticated a user, you can start to execute queries against the repository. In the following sample code, we will see how to perform a query using the RepositoryService of Alfresco:
RepositoryService repositoryService = WebServiceFactory.
getRepositoryService();
Then we need to create a store where we would like to search contents:
Store spacesStore = new Store(StoreEnum.workspace, "SpacesStore");
Now we need to create a Lucene query. In this sample, we want to search the Company Home space, and this means that we have to execute the following query:
String luceneQuery = "PATH:\"/app:company_home\"";
In the next step, we need to use the query method available from the RepositoryService. In this way, we can execute the Lucene query and we can get all the results from the repository:
Query query =
new Query(Constants.QUERY_LANG_LUCENE, luceneQuery);QueryResult queryResult =
repositoryService.query(spacesStore, query, false);
You can retrieve all the results from the queryResult object, iterating the ResultSetRow object in the following way:
ResultSet resultSet = queryResult.resultSet;
ResultSetRow[] results = resultSet.rows;
//your custom list
IList<CustomResultVO> customResultList =
new List<CustomResultVO>();
//retrieve results from the resultSet
foreach(ResultSetRow resultRow in results)
{
ResultSetRowNode nodeResult = resultRow.node;
//create your custom value object
CustomResultVO customResultVo = new CustomResultVO();
customResultVo.Id = nodeResult.id;
customResultVo.Type = nodeResult.type;
//retrieve properties from the current node
foreach(NamedValue namedValue in resultRow.columns)
{
if (Constants.PROP_NAME.Equals(namedValue.name))
{
customResultVo.Name = namedValue.value;
} else if (Constants.PROP_DESCRIPTION.Equals(namedValue.name))
{
customResultVo.Description = namedValue.value;
}
}
//add the current result to your custom list
customResultList.Add(customResultVo);
}
In the last sample, we iterated all the results and we created a new custom list with our custom value object CustomResultVO.
More information about how to build Lucene queries can be found at this URL: http://wiki.alfresco.com/wiki/Search.
Performing operations
We can perform various operations on the repository. They are documented as follows:
Authentication
For each operation, you need to authenticate users before performing all the required operations on nodes. The class that provides the authentication feature is named AuthenticationUtils, and it allows you to invoke the startSession and endSession methods:
String username = "johndoe";
String password = "secret";
AuthenticationUtils.startSession(username, password);
try{
}
finally
{
AuthenticationUtils.endSession();
}
Remember that the startSession method requires the user credentials: the username as the first argument and the password as the second.
Notice that the default endpoint address of the Alfresco instance is as follows:
http://localhost:8080/alfresco
If you need to change the endpoint address, you can use the WebServiceFactory class invoking the setEndpointAddress method to set the new location of the Alfresco repository.
(For more resources on Alfresco, see here.)
CRUD operations
Inside the Crud package of the sample project, you can see how to invoke basic operations to manipulate contents. In this section, we will see how to perform all the following operations on nodes:
- Create
- Update
- Copy
- Move
- Remove
Creating nodes
In order to create nodes in the repository, you need to create a CMLCreate object. You need to use the Content Manipulation Language of Alfresco to manipulate content using the Web Services API. So, in this case, we want to create a node with some basic properties without any associated content.
The first object defined in the sample is the Store object. The Store is used to define where we want to create the new node. The SpacesStore is the store for all the latest versions of nodes:
Store spacesStore = new Store(StoreEnum.workspace, "SpacesStore");
Then we need to provide properties for the new node:
String name = "AWS Book " + DateTime.Now.Ticks;
String description =
"This is a content created with a sample of the book";
//custom value object
CreateSampleVO createSampleVo = Builder.BuildCreateSampleVO(name,
name, description);
The Builder class shows you an example of how you can create your custom object getting the new properties value (BuildCreateSampleVO method). Also, inside the same class, there is another method to create the NamedValue array required in the CMLCreate object. In the following snippet, you can see the Builder class:
public static class Builder
{
public static NamedValue[] BuildCustomProperties(CreateSampleVO
createSampleVo)
{
NamedValue[] properties = new NamedValue[3];
properties[0] = Utils.createNamedValue(Constants.PROP_NAME,
createSampleVo.Name);
properties[1] = Utils.createNamedValue(Constants.PROP_TITLE,
createSampleVo.Title);
properties[2] = Utils.createNamedValue(Constants.PROP_
DESCRIPTION,
createSampleVo.Description);
return properties;
}
public static CreateSampleVO BuildCreateSampleVO(String name,
String title, String description)
{
CreateSampleVO createSample = new CreateSampleVO();
createSample.Name = name;
createSample.Title = title;
createSample.Description = description;
return createSample;
}
In the next step, we are building the ParentReference object, which is the parent of the new node:
try
{
ParentReference parent = new ParentReference(
spacesStore,
null,
"/app:company_home",
Constants.ASSOC_CONTAINS,
"{" + Constants.NAMESPACE_CONTENT_MODEL + "}" + name
);
//build properties
NamedValue[] properties = Builder.BuildCustomProperties(createSam
pleVo);
Constants.ASSOC_CONTAINS is the default child association cm:contains, which is defined in the default content model in Alfresco. This association allows you to add new nodes (children) in spaces (parents).
In order to create a new node, you need to use the CMLCreate object. For this object, you need to provide a parent for the new node, a content type for the node, and all the node properties:
//create operation
CMLCreate create = new CMLCreate();
create.id = "1";
create.parent = parent;
create.type = Constants.TYPE_CONTENT;
create.property = properties;
Constants.TYPE_CONTENT is a constant value that allows you to define new generic content in Alfresco with the following QName value: cm:content.
Now we have a complete CMLCreate object, that is, an operation for the CML language. We need to encapsulate this object in a new generic CML object to execute the operation:
CML cml = new CML();
cml.create = new CMLCreate[]{ create };
Finally, we can invoke the update method of the RepositoryService to perform the operation against the repository:
UpdateResult[] result = WebServiceFactory.getRepositoryService().
update(cml);
Creating content
In the previous paragraph, you learned how to create nodes with properties, but if you need to store a node with a related content—for example, a document—you need to upload the file to the repository.
To accomplish this task, you can follow one of two methods:
- Using the CMLWriteContent object
- Using the ContentService
Creating content using CML
If you want to encapsulate the content in a unique request for the creation of the node and its content, you have to add an instance of the CMLWriteContent object inside the previous CML request.
For the first step, you need to create an instance of the Reference object to point to the new node contained in the previous CMLCreate object:
//create the node reference
Reference reference = new Reference();
reference.store = spacesStore;
reference.path = "/app:company_home/cm:"+ISO9075.Encode(name);"/app:
company_home/cm:"+ISO9075.Encode(name);
The path field is provided encoding the space names in the ISO 9075 format because Alfresco stores node paths using XPath. Alfresco provides you with a utility class ISO9075 to allow you to encode and decode space names in the correct way.
In the following section, you need to provide a Predicate object as the group of the involved nodes. In this case, we have only a node reference:
//create the predicate
Predicate predicate = new Predicate();
predicate.Items = new Reference[]{ reference };
In this step, we are creating the ContentFormat to create all the indexes correctly for the current content:
//set mime type and encoding for indexing
ContentFormat format = new ContentFormat(mimeType, encoding);
Now that we have all the needed objects, we can build the CMLWriteContent object, and we can add this instance to the CML object.
//write operation
CMLWriteContent writeContent = new CMLWriteContent();
writeContent.format = format;
writeContent.where = predicate;
writeContent.property = Constants.PROP_CONTENT;
writeContent.content = new ASCIIEncoding().GetBytes("This is the
content for the new node");
//build the CML object
CML cml = new CML();
cml.create = new CMLCreate[]{ create };
cml.writeContent = new CMLWriteContent[]{ writeContent };
Finally, we can execute the update method:
UpdateResult[] result = WebServiceFactory.getRepositoryService().
update(cml);
Creating content using ContentService
Another way to create content is to use the ContentService as a second request after you performed the creation of the node. So, in this case, we are creating content at two occasions: during the first request we will create the node with all the properties and during the second request we will upload the content.
Let's say that we have created and performed a previous CMLCreate operation in the repository:
//perform a CML update for the CMLCreate operation
UpdateResult[] result = WebServiceFactory.getRepositoryService().
update(cml);
From the previous operation, we can get the new node reference:
//get the new node reference
Alfresco.ContentWebService.Reference referenceForContent = Alfresco.
ContentWebService.Reference.From(
result[0].destination);
In the next section, we create the ContentFormat object:
Alfresco.ContentWebService.ContentFormat format = new Alfresco.
ContentWebService.ContentFormat(mimeType, encoding);
Finally, we can get ContentService from WebServiceFactory, and then we can use it to upload the content. We have to provide the node reference, the content property, and the stream for the content:
Alfresco.ContentWebService.Content content = WebServiceFactory.
getContentService().write(
referenceForContent,
Constants.PROP_CONTENT,
new ASCIIEncoding().GetBytes(
"This is the content for the new node"),
format
);
Notice that creating content using ContentService can cause problems with the atomicity of the operation. If you have a problem during the execution of the write method, the node can exist in the repository and not the content.
Updating nodes
Once you have stored content in the repository and you need to update one of these existent nodes, you must provide an instance of the CMLUpdate object. To update a node in Alfresco, you can:
- Perform a clean update of the node and its content
- Create a new version for the involved node
Updating nodes without versioning
In the following sample code, you can see how to update existent properties values for a node previously created using the CMLUpdate object:
//create a predicate with the first CMLCreate result
Reference referenceForNode = result[0].destination;
Predicate sourcePredicate = new Predicate(
new Reference[]{ referenceForNode },
spacesStore,
null
);
Reference referenceForTargetSpace = result[1].destination;
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
To create a parent reference, as you can see in the previous snippet, you must provide:
- The Store object for the destination
- The path of the parent node
- The type of the association that you want to use between the space and the node
- The child name for the current instance of the association
In the following section, we are creating the new properties values for the node:
name = "AWS Book - Changed by CMLUpdate " + DateTime.Now.Ticks;
createSampleVo.Name = name;
createSampleVo.Title = name;
createSampleVo.Description = "Changed by CMLUpdate " + description;
Now, we need to create a CMLUpdate object to set all the new properties:
//update node
CMLUpdate update = new CMLUpdate();
update.property = Builder.BuildCustomProperties(createSampleVo);
update.where = sourcePredicate;
Finally, we can execute the update method to perform the CML operation:
CML cmlUpdate = new CML();
cmlUpdate.update = new CMLUpdate[]{ update };
//perform a CML update
WebServiceFactory.getRepositoryService().update(cmlUpdate);
Updating nodes with versioning
If you need to create a new version of an existing node, you can add the versionable aspect to the node. In this way, the repository will change the behavior for the involved node, creating a new version for each update. The repository will keep all the old versions for the node, creating a version history.
Assuming that we have an existing node previously created in the repository, we have to provide the same object seen in the previous paragraph, but by adding a new CML object named CMLAddAspect. This new object will add a new behavior for the node, and specifically, it will add the versionable aspect:
//create a predicate with the first CMLCreate result
Reference referenceForNode = result[0].destination;
Predicate sourcePredicate = new Predicate(new Reference[]{
referenceForNode }, spacesStore, null);
Reference referenceForTargetSpace = result[1].destination;
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
name = "AWS Book - Changed by CMLUpdate " + DateTime.Now.Ticks;
createSampleVo.Name = name;
createSampleVo.Title = name;
createSampleVo.Description =
"Changed by CMLUpdate " + description;
In the following snippet, we will create the CMLAddAspect object providing the QName of the aspect and the group of all the involved nodes (sourcePredicate). This is the object that will add the new behavior for this node:
CMLAddAspect aspect = new CMLAddAspect();
aspect.aspect = Constants.ASPECT_VERSIONABLE;
aspect.where = sourcePredicate;
Finally, we can add CMLAddAspect to the CML object, and then invoke the update method:
//update node
CMLUpdate update = new CMLUpdate();
update.property = Builder.BuildCustomProperties(createSampleVo);
update.where = sourcePredicate;
CML cmlUpdate = new CML();
cmlUpdate.addAspect = new CMLAddAspect[]{ aspect };
cmlUpdate.update = new CMLUpdate[]{ update };
//perform a CML update WebServiceFactory.getRepositoryService().
update(cmlUpdate);
Copying nodes
Creating copies for nodes is an operation that can be performed using another CML object defined for this feature, which is named CMLCopy. As in the previous samples, we are assuming that we have an existing node in the repository and we want to copy it to a different space.
We start by getting the node reference of the existing node at its new target space:
//create a predicate with the first CMLCreate result
Reference referenceForNode = result[0].destination;
Predicate sourcePredicate = new Predicate(new Reference[]{
referenceForNode }, spacesStore, null);
//reference for the space
Reference referenceForTargetSpace = result[1].destination;
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
Next we create the CMLCopy instance setting the where field with the group of source nodes that we want to copy from and the to field with the target space for all the copied nodes:
//copy content
CMLCopy copy = new CMLCopy();
copy.where = sourcePredicate;
copy.to = targetSpace;
Finally, we create the array of CMLCopy objects. Then we perform this operation using the update method:
CML cmlCopy = new CML();
cmlCopy.copy = new CMLCopy[]{copy};
//perform a CML update to move the node WebServiceFactory.
getRepositoryService().update(cmlCopy);
Moving nodes
The move operation is performed using the CMLMove object. In the same way, as we saw in the previous paragraph, you have to provide two mandatory objects:
- A predicate for the group of source nodes that you want to move
- A target space for the destination
In this first step, you need to create a predicate for the involved node:
//create a predicate with the first CMLCreate result
Reference referenceForNode = result[0].destination;
Predicate sourcePredicate = new Predicate(new Reference[]{
referenceForNode }, spacesStore, null);
Then we create a parent reference as the new target space for the node:
//create a reference for space
Reference referenceForTargetSpace = result[1].destination;
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
Next we create the CMLMove object, and we can set all the required fields:
//move content
CMLMove move = new CMLMove();
move.where = sourcePredicate;
move.to = targetSpace;
Finally, we create the CML object by setting the move operation. Then we perform the following operation using the update method:
CML cmlMove = new CML();
cmlMove.move = new CMLMove[]{move};
//perform a CML update to move the node WebServiceFactory.
getRepositoryService().update(cmlMove);
Removing nodes
CMLDelete is the CML object that can be used to remove existing nodes in the repository. In the following sample, you will see how to remove a node from the repository. The unique field required for this operation is the predicate for the involved nodes:
//create a predicate
Reference reference = result[0].destination;
Predicate predicate = new Predicate(new Reference[]{ reference },
spacesStore, null);
In next section, we create the CMLDelete object, setting the where field with the previous predicate:
//delete content
CMLDelete delete = new CMLDelete();
delete.where = predicate;
Finally, we build the CML object to use the update method:
CML cmlRemove = new CML();
cmlRemove.delete = new CMLDelete[]{delete};
//perform a CML update to remove the node WebServiceFactory.
getRepositoryService().update(cmlRemove);
Managing child associations
In this section, we will see how to manage child associations. You learned that a child association is a bidirectional association between a parent and its children.
The two CML objects used to manage child associations are:
- CMLAddChild.
- CMLRemoveChild.
CMLAddChild is used to add children to a parent, and CMLRemoveChild is used to remove children from a parent.
Adding child nodes
In the following sample, we want to associate two existent nodes. sourcePredicate is the predicate for the child nodes and targetSpace is the parent node:
Reference referenceForNode = result[0].destination;
Predicate sourcePredicate = new Predicate(new Reference[]{
referenceForNode }, spacesStore, null);
Reference referenceForTargetSpace = result[1].destination;
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
In the next section, we will create the CMLAddChild object, setting the sourcePredicate and the targetSpace as its fields:
//add child
CMLAddChild addChild = new CMLAddChild();
addChild.where = sourcePredicate;
addChild.to = targetSpace;
CML cmlAddChild = new CML();
cmlAddChild.addChild = new CMLAddChild[]{ addChild };
//perform a CML update to add the node WebServiceFactory.
getRepositoryService().update(cmlAddChild);
After the execution of the update method, all the involved nodes are associated by a new association instance. The child name will be the identifier for the new association instance.
Removing child nodes
Once you have associated nodes with one or more parents, you can remove children from a parent using the CMLRemoveChild object. You have to define:
- A predicate for all the nodes that you want to remove (where)
- A target space for the parent (Item)
In the following snippet, you can see how to get these required objects:
//reference for the target space
ParentReference targetSpace = new ParentReference();
targetSpace.store = spacesStore;
targetSpace.path = referenceForTargetSpace.path;
targetSpace.associationType = Constants.ASSOC_CONTAINS;
targetSpace.childName = name;
Reference refUpdate = resultAddChild[0].destination;
Predicate nodeToRemove = new Predicate(new Reference[]{ refUpdate },
spacesStore, null);
Now we can create the CMLRemoveChild instance, and we can perform the operation:
//remove child
CMLRemoveChild removeChild = new CMLRemoveChild();
removeChild.Item = targetSpace;
removeChild.where = nodeToRemove;
CML cmlRemoveChild = new CML();
cmlRemoveChild.removeChild = new CMLRemoveChild[]{ removeChild };
//perform a CML update to remove the node WebServiceFactory.
getRepositoryService().update(cmlRemoveChild);
Summary
In this article, we discussed an example of a .NET application built on top of Alfresco using the Alfresco Web Services API. The article introduced an overview about how to set up the sample project in your development environment using the open source IDE SharpDevelop. Then it describes how to manipulate contents using .NET. Specifically, you learned the following topics:
- How to set up the sample project in your .NET environment
- How to perform queries using the Lucene query language
- How to perform basic operations to manipulate content
- How to add and remove child nodes from parents
Further resources on this subject:
- Alfresco Developer Guide [book]
- Alfresco 3 Web Content Management [book]
- Alfresco 3 Enterprise Content Management Implementation [book]
- Advanced Collaboration using Alfresco Share [article]
- Installing Alfresco Software Development Kit (SDK) [article]
- The Model-View-Controller pattern and Configuring Web Scripts with Alfresco [article]
- Overview of REST Concepts and Developing your First Web Script using Alfresco [article]
About the Author :
Piergiorgio Lucidi
Piergiorgio Lucidi is an open source ECM Specialist at Sourcesense. Sourcesense is a European open source systems integrator providing consultancy, support, and services around key open source technologies.
He works as Software Engineer, and he has 8 years of experience in the areas of Enterprise Content Management (ECM), system integrations, web, and mobile applications. He is an expert in integrating ECM solutions in web and portal applications.
He contributes as PMC member, Project Leader, and Committer at the Apache Software Foundation for the project Apache ManifoldCF; he also contributes on ECM connectors such as CMIS, Alfresco, and ElasticSearch. He is a Project Leader and Committer of the JBoss Community, and he contributes to some of the projects of the JBoss Portal platform.
He is a Speaker at conferences dedicated to ECM, Java, Spring Framework, and open source products and technologies.
He is an Author, Technical Reviewer, and Affiliate Partner at Packt Publishing, for whom he wrote the technical book Alfresco 3 Web Services. As Technical Reviewer, he contributed to both Alfresco 3 Cookbook and Alfresco Share. As Affiliate Partner, he writes and publishes book reviews on his website Open4Dev (http://www.open4dev.com/).
Ugo Cei
Ugo Cei is Solutions Delivery Manager at Sourcesense Italy. He has over 20 years of experience in the IT sector. His areas of expertise include Web application development, content management systems, database, and search technologies. He has a Ph.D. in Engineering from the University of Pavia, Italy.Ugo is a long-time active contributor to numerous Open Source project and a member of the Apache Software Foundation.Besides his interests in computer-related matters, Ugo is a passionate photographer. He sometimes dreams of leaving the IT field to pursue his passion full-time, and travel the world with a camera.



Post new comment