Quantcast
Channel: Nuxeo Blogs » Product & Development
Viewing all 161 articles
Browse latest View live

[Q&A Friday] Remotely Searching for a Document Using Tags

$
0
0
Remotely searching a document using tags

Remotely searching a document using tags

Today dedacosta asks if it’s possible to search documents remotely using tags. First let’s talk about tags.

The tag service uses two important concepts: a tag object, and a tagging action. Both are represented as Nuxeo documents.

A tag is a document type representing the tag itself (but not its association to specific documents). It contains the usual dublincore schema, and in addition has a specific tag schema containing a tag:label string field.

A tagging is a relation type representing the action of tagging a given document with a tag. (A relation type is a document type extending the default relation document type; it works like a normal document type except that it’s not found by NXQL queries on document). The important fields of a tagging document are relation:source which is the document ID, relation:target which is the tag ID, and dc:creator which is the user doing the tagging action.

Both tag and tagging documents managed by the tag service are unfilled, which means that they don’t have a parent folder. They are therefore not visible in the normal tree of documents, only queries can find them. In addition they don’t have any ACLs set on them, which means that only a superuser (and the tag service internal code) can access them.

If you want to test this, tag a document and run the following query:

curl -H 'Content-Type:application/json+nxrequest'  -H "X-NXDocumentProperties: *" -X POST -d '{"params":{"query":"SELECT * FROM Tag WHERE tag:label = \"tag3\" AND ecm:isProxy = 0"}}' -u Administrator:Administrator http://localhost:8080/nuxeo/site/automation/Document.Query

This will return the tag document created for the tag labeled ‘tag1′ as a Json output:

{ "entity-type" : "documents",
  "entries" : [ { "changeToken" : "1365178419352",
        "contextParameters" : {  },
        "entity-type" : "document",
        "facets" : [ "HiddenInNavigation" ],
        "lastModified" : "2013-04-05T16:13:39.35Z",
        "path" : "tag",
        "properties" : { "dc:contributors" : [ "Administrator" ],
            "dc:coverage" : null,
            "dc:created" : "2013-04-05T16:13:39.35Z",
            "dc:creator" : "Administrator",
            "dc:description" : null,
            "dc:expired" : null,
            "dc:format" : null,
            "dc:issued" : null,
            "dc:language" : null,
            "dc:lastContributor" : "Administrator",
            "dc:modified" : "2013-04-05T16:13:39.35Z",
            "dc:nature" : null,
            "dc:publisher" : null,
            "dc:rights" : null,
            "dc:source" : null,
            "dc:subjects" : [  ],
            "dc:title" : null,
            "dc:valid" : null,
            "tag:label" : "tag"
          },
        "repository" : "default",
        "state" : "undefined",
        "title" : "tag",
        "type" : "Tag",
        "uid" : "5063b2fe-6abe-44c5-ac20-2b19e2922833"
      } ]
}

This is the Tag document. Now if you want to retrieve the tagging document, you can do it with the following query:

curl -H 'Content-Type:application/json+nxrequest'  -H "X-NXDocumentProperties: *" -X POST -d '{"params":{"query":"SELECT * FROM Tagging WHERE tag:label = \"tag3\" AND ecm:isProxy = 0"}}' -u Administrator:Administrator http://localhost:8080/nuxeo/site/automation/Document.Query

Here’s the associated Json output:

{ "entity-type" : "documents",
  "entries" : [ { "changeToken" : "1365178419354",
        "contextParameters" : {  },
        "entity-type" : "document",
        "facets" : [ "HiddenInNavigation" ],
        "lastModified" : "2013-04-05T16:13:39.35Z",
        "path" : "tag",
        "properties" : { "dc:contributors" : [ "Administrator" ],
            "dc:coverage" : null,
            "dc:created" : "2013-04-05T16:13:39.35Z",
            "dc:creator" : "Administrator",
            "dc:description" : null,
            "dc:expired" : null,
            "dc:format" : null,
            "dc:issued" : null,
            "dc:language" : null,
            "dc:lastContributor" : "Administrator",
            "dc:modified" : "2013-04-05T16:13:39.35Z",
            "dc:nature" : null,
            "dc:publisher" : null,
            "dc:rights" : null,
            "dc:source" : null,
            "dc:subjects" : [  ],
            "dc:title" : null,
            "dc:valid" : null,
            "relation:predicate" : null,
            "relation:source" : "0cbb4117-1e86-4092-9278-cd04ecdea35c",
            "relation:sourceUri" : null,
            "relation:target" : "5063b2fe-6abe-44c5-ac20-2b19e2922833",
            "relation:targetString" : null,
            "relation:targetUri" : null
          },
        "repository" : "default",
        "state" : "undefined",
        "title" : "tag",
        "type" : "Tagging",
        "uid" : "4979c4cb-d253-41b9-8696-014eb9e44e1e"
      } ]
}

As you can see this document contains two interesting metdatas: relation:source and relation:target. The first one is the id of the tagged document and the second one is the id of the Tag document.

Now that you know more about the Tags, let’s go back to the original question. How to search documents remotely using tags. It’s really easy to do especially since Florent added the tag support for NXQL. So starting from 5.7 you can do queries like this:

Select * From Document WHERE ecm:tag  ='tag1'
Select * From Document WHERE ecm:tag IN ('tag1','tag4')

Note that this is not available in CMISQL, and that NXQL query currently cannot be ran using SOAP webservices. And unfortunately the 5.7 hasn’t been released yet so we have to do otherwise. This can be overcome easily using a custom operation. From Nuxeo IDE, you just have to create a new operation from the wizard and implement it.

@Operation(id = TagSearch.ID, category = Constants.CAT_DOCUMENT, label = "Tag Search", description = "Search documents using a tag.")
public class TagSearch {

public static final String ID = "Document.TagSearch";

@Context
protected TagService tagService;

@Context
protected CoreSession coreSession;

@Param(name = "tagLabel", required = true)
protected String label;

@OperationMethod
public DocumentModelList run() throws Exception {
    if (!tagService.isEnabled()) {
        throw new ClientException("The tag service is not enabled.");
    }
    List<String> docIds = tagService.getTagDocumentIds(coreSession, label, coreSession.getPrincipal().getName());
    DocumentRef[] documentRefs = new DocumentRef[docIds.size()];
    for (int i = 0; i < docIds.size(); i++) {
        documentRefs[i] = new IdRef(docIds.get(i));
    }
    DocumentModelList result = coreSession.getDocuments(documentRefs);
    return result;
}
}

Here I am directly using the tag service. Under the hood, it uses NXQL queries on tag and tagging documents just like we saw at the beginning. Also this is using content automation, so you can call it remotely thanks to the REST binding. But it does not solve our issue with SOAP webservice. I actually tried to do a CMIS query with a join like this:

SELECT Doc.cmis:objectId FROM cmis:document Doc JOIN Tagging Tgg ON Tgg.relation:source = Doc.cmis:objectId JOIN Tag Tg ON Tgg.relation:target = Tg.cmis:objectId WHERE Tg.tag:label = 'tag

But this cannot work because the tag document type is not exposed in CMIS. We did this because it’s a ‘system’ document. Meaning end users have nothing to do with it, hence it being not available through CMIS. So unfortunately there is no out of the box or easy solution if you want to use SOAP. You have to define your own SOAP based webservice, which is not as simple as using content automation IMHO.

The post [Q&A Friday] Remotely Searching for a Document Using Tags appeared first on Nuxeo Blogs.


[Nuxeo Community] Meet Sylvain Chambon

$
0
0
Meet Sylvain Schambon

Meet Sylvain Chambon

Today we meet Sylvain Chambon, head of the Java Integration Division at Open Wide, and contributor of the Kerberos authentication plugin. You have probably heard of Nuxeo partner Open Wide already, as we did a webinar together about the EasySOA research project. I had the opportunity to interview him at the Documation conference where he gave a standing-room only presentation about how he used the Nuxeo Platform to build a project management app. The good news is that he will give this presentation again as a webinar on April 24th.

The audio is available, with the background noise of a very busy Documation trade show.

Listen to Sylvain’s interview (in French)

Hi Sylvain. You’re working for a systems integrator called Open Wide. Are you working in the Enterprise Content Management field specifically?

Actually I’m working in the Java integration unit, across domains. We’re organized by technology. I am specialized in Java-based software. There is a unit that does Java development, and another unit that I manage that does integration of complete solutions from pre-existing software like Nuxeo or Liferay, for instance.

What kind of projects do you usually work on from a functional perspective? When using Nuxeo, do you use document management, social collaboration, or digital asset management?

We are working on three projects at the moment. All of them are document management projects, with a strong emphasis on collaborative work. So, we use Nuxeo’s document management and social collaboration components. The main focus is good old document management, with an emphasis on business process automation, so that we are building a business document management application that’s more than just a simple repository. The strength of Nuxeo is that you can automate business processes and lifecycles and have something that is completely adapted to the organizational processes.

Open Wide

I’d like to find out more about your development processes with the Nuxeo Platform. What can you tell us about the fact that you can use Nuxeo Studio and/or Java and XML to configure and extend the platform?

So it’s always Studio AND Java, because Studio allows us to do 80% of what we want to achieve. For extending the Platform and more advanced development work, we need to write Java code.

Sometimes it’s much faster for me to write code in Java and it is easier to maintain the changes over time, because I have a source code management tool. I can see the history, the changes between two commits, etc. The difference between coding from scratch and using Nuxeo Studio is that you can’t see the differences between two commits in Studio, to find out exactly what has been modified. In general, Nuxeo’s development model (with extension points and bundles) may not be intuitive upon first glance, but as you dive a bit deeper, you understand the huge potential. And the fact that Studio generates a bundle, which itself contains only contributions to extension points, and that you can read this XML configuration, is incredibly powerful.

In a project we did recently, Nuxeo loaded 734 bundles — it was completely modular. We have a functional richness tied to this, and we can easily extend Nuxeo in one way or the other. I mean you can add functionalities or remove those you don’t want, or change them in a very clean manner. This is a really good point when you ask yourself how to maintain these applications over time. We are reassured because of the platform’s architecture.

We’re not going to ask ourselves existential questions about version upgrades, hot fix installations, you know, things like that. Globally it becomes routine, which is not the case for other products. Voila.

Panolyonpont

By Nicolas.F (Own work) GFDL or CC-BY-SA-3.0-2.5-2.0-1.0, via Wikimedia Commons

Another great thing about your work with Nuxeo is your first code contribution — the Kerberos authentication module. Is Open Wide open source all the way?

Open Wide is only open source. The open in Open Wide means open source. Contributing the code we write is part of the company’s DNA. It’s also a completely rational choice in that we don’t want to duplicate the version management and maintenance that a vendor does naturally, and better than we do.

And don’t forget that what you contribute is then maintained by Nuxeo :)

Exactly! That’s another way of saying it.

If you could have three wishes for the Nuxeo Platform, what would they be?

- I’d like to see an improvement in the rendering quality of the preview module. This comes from the choice to have an HTML-based preview instead of an image-based preview.

- I’d like to have the drag-and-drop upload form customizable with Studio.

- I’d like to have all of the elements from the social collaboration and digital asset management modules exposed in Studio’s application template, so we don’t have to redefine the picture type, for instance.

You live in Lyon, France. What is Lyon like?

Actually, I’m in Paris 80% of the time, and Lyon 20%, and I spend a lot of time in the TGV (the fast train in France). But Lyon is a beautiful city; I’ve lived there for six years. A good thing is that it’s more compact than Paris, so you can do everything on foot, very easily. Plus, you eat really well there!

What do you do in your free time, other than coding and contributing to Nuxeo?

Well, I code and I contribute to Nuxeo every weekend. This is all I do every weekend. [Laughs] No, seriously though… I read a lot. I have a three year old son and a full-time job. I also participate in micro-publishing projects for science fiction books by helping with the proofreading and reviewing process.

Can you give us a favorite book or author?

I just finished a science fiction book by Laurent Whale, called The Stars Don’t Care (Les Etoiles s’en Balancent) and it was “une pure merveille” – a joy to read.

01. Panorama de Lyon pris depuis le toit de la Basilique de Fourvière

By Otourly (Own work) GFDL or CC-BY-SA-3.0-2.5-2.0-1.0, via Wikimedia Commons

The post [Nuxeo Community] Meet Sylvain Chambon appeared first on Nuxeo Blogs.

Nuxeo Drive – Desktop Synchronization

$
0
0

We just released a beta version of Nuxeo Drive, a desktop synchronization tool for the Nuxeo content repository. We thought that a little story would be the best way of presenting this plugin, that you can yourself install it on your running Nuxeo 5.6 instance that is up to date with all hot fixes (through HF14). (This version is not compatible with a 5.7-SNAPSHOT Nuxeo server).

John Doe works as a sales rep in a pharmaceutical company. He is frequently at customer sites, presenting the product catalog. When he started with the company, they only had one drug on the market. The company has grown, the product line has grown, and the product documentation is frequently updated. Often, he doesn’t have the latest documents when on site. He is tempted to use one of many consumer cloud file sync services, such as Dropbox or Box, but knows that it is against policy and would violate basic security rules, putting his company’s intellectual property at risk.

John Doe has heard about Nuxeo Drive, the desktop file synchronization tool that simplifies the way you access documents stored in a Nuxeo content repository. John asks his sys admin to install the Nuxeo Marketplace package in the corporate Nuxeo Platform instance, a quick 2 min + server restart operation.

Easy client set up

John can easily see that the plugin is active because there is a new action button (TODO: insert icon) on every folder in Nuxeo. Then John goes to the Home tab, and notices a new sub tab on the left, called “Nuxeo Drive.” From that tab, he sees the “Download the Nuxeo Drive Client” section, and clicks on the Mac OS X link, which is next to the Windows package. Installation takes only a few seconds — he only needs to choose the location of the Nuxeo Drive folder. Then when starting Nuxeo Drive for the first time, John is prompted for connection information to his Nuxeo application. That’s a one-time task; after that, the URL persists and authentication is token-based.

Quick folder hierarchy synchronization

Synchronize your documents

John now has a Nuxeo Drive folder on his desktop, but it is empty! He remembers seeing the synchronization icon in Nuxeo, and goes to the product presentations folder and clicks on the Drive sync icon. Immediately he can see under the local Nuxeo Drive folder that the synchronized node appears, as well as all the subfolders and files they contained. It was a big folder with multiple sub levels and thousands of documents, so it takes a little time, but John is impressed by the speed! And he can see on the taskbar a new Drive icon. Clicking on it gives him a status: 42 pending documents.

Offline access to documents

Unfortunately, John has to stop his experiment as he has a meeting in 30 minutes and he has not left the office yet. He closes his laptop and jumps into his car. There is no traffic and John arrives 10 minutes early for the meeting. This gives him time to review his slides. He finds them in the set of synchronized files in the Nuxeo Drive folder. Double clicking on the file launches PowerPoint as usual. He notices some spelling errors and fixes them, then saves the presentation. He also had to open and edit a second PowerPoint file caleld “animations.ppt,” where corporate drawings are stored.

Resume synchronization and conflict management

Desktop Synchronization

Back at the office, John is back online. The systray notification says 44 pending operations, actually the 42 pending downloads + the 2 uploads of files that were modified when he was offline. John is happily surprised at the robustness of the synchronization process. A few seconds after, the systray says “folder up to date.” Johns checks how the presentation was updated on the Nuxeo server. He browses the Nuxeo application and sees that a new minor version was created that includes his modifications. In the folder where the other file, the one with the animations is located, he now sees two files:  ”animations.ppt” and “animations.ppt (John Doe – 2013/04/08).ppt.” That’s because a conflict was detected at synchronization time, and Drive services let the users do the merge, keeping the two files safe.

Conflict resolution and new Live Edit action

Someone had updated the animations.ppt file while John was on the road. To resolve the conflict, Johns opens both files (either from the local Drive folder, or from Nuxeo using the new  ”Live Edit” action button, that now relies on Nuxeo Drive software) and uses a PowerPoint comparison/merge tool for resolving the conflict. He updates animations.ppt with his own modifications. Finally, he deletes animations.ppt (John Doe – 2013/04/01).ppt.

Managing all the synchronized root folders

John finds this new feature so useful that he starts clicking on plenty of folders he wants to synchronize. He noticed he can see the complete list of synchronized folders from the /Home/Nuxeo Drive screen and is even able to remove some of the synchronized nodes from there, so he doesn’t worry about synchronizing too many of them.

Documents and folders

John starts using Nuxeo Drive for more of his daily work. He discovers that removing files and folders locally will remove them on the server. Moving, renaming, deleting files on the server will do the same locally — all this is very simple and intuitive!

Token deletion and multiple devices

After an important presentation for a potential customer, John forgot his briefcase and laptop, probably in the lobby. As he doesn’t want someone else to have a way of always getting synced with the corporate Nuxeo document database, he uses the token revocation feature from the Nuxeo application, located in the Home/Nuxeo Drive tab. He also understands that on his new computer, he will just need to set up the client again, and it will already be configured. Nuxeo Drive can be used on multiple devices. It is a pragmatic tool for keeping your content repository available, whether you’re online or offline.

The post Nuxeo Drive – Desktop Synchronization appeared first on Nuxeo Blogs.

Want to try customizing the new DAM with Studio?

$
0
0

Nuxeo Digital Asset ManagementAs you may already know, we are working on the next version of our Digital Asset Management module. One of the pieces of big news is that it has been designed to be fully customizable by Nuxeo Studio, our online customization tool.

Some work on the Studio side is scheduled to enable some DAM-customizable features; however, a lot can already be done when you know what resources to use in Studio (document facets, form layouts, etc.).

To help with the learning curve, we have released an application template that you can import in your Studio project to test DAM customization.

A Studio application template is composed of projects and samples that you can use as a basis for your own project. To find out more, you can have a look at the Studio documentation on application templates.

What can you do with it?

The application template redeclares elements so that you can use, customize or override them with Studio. Here are a few things you can do.

Document model

Do you want to declare new document types that you will be able to see in DAM? No problem; the template just creates one to show you how it is done.

You can extend generic DAM types (picture or video). You can also choose DAM facets for your document type definition tabs, like “Picture” or “HasVideoPreview” for instance. You may also go to container types and allow the AssetLibrary so that you will be able to create your document directly at the root of the asset library.

It is possible to declare what document types can be created directly from the DAM interface using the right XML extension point. Look at the DAMEnableNewSampleCustomPic XML extension in your Studio project to see how to use it.

User interface (layout and actions)

Several layouts are now used in DAM. Thanks to the application template, you can now customize:

  • the bulk edit form (damBulkEdit)
  • the thumbnail layout (DAMThumbnailLayout) of assets
  • the right panel (DAMRightPanelLayout): take a look at the XML extension if you want to see how to add form layouts.

And finally, new action categories are available so that you can put actions anywhere in the DAM interface:

  • DAM View Actions
  • DAM Current Selection List
  • DAM Search Results Actions

How can you try it?

Are you ready to try it? It’s easy! In your Studio project (you can start a trial if you do not have a project already), go to Settings and versioning > Application Templates and import the template called Nuxeo DAM default, and you are good to go.

Make sure your Studio project is targeting the 5.7 dev version. If not, you can change it in Settings And Versioning > Project Settings.

Then get the latest build of the platform here: Static Snapshot. Install DM and DAM modules then deploy your Studio project in there and enjoy!

One last word: Please remember that you are trying a development version of Nuxeo DAM, so it’s a work in progress, and there may be impacts on this application template.

Enhanced by Zemanta

The post Want to try customizing the new DAM with Studio? appeared first on Nuxeo Blogs.

[Nuxeo Tech Report] News from the Developer Front #6

$
0
0

Hi everyone, and welcome to this new tech report. It’s a rather short one this week — we’re mostly consolidating what you’ve read about in the previous report. Oh, and we released Nuxeo Drive :D

Nuxeo Drive

Drive 1.0.1 released

Drive 1.0.1 was release last week.

Next steps

There is a known issue when several files are created on the server: because the server may be busy, there is a time shift between the audit log event date and the time where the audit entry is actually persisted. This time shift can lead the client to ‘loose some events’ and then not see some files.

To fix that, we should:

  • Review the polling system: based on the audit sequence ID rather than the time window
  • Prepare the upgrade of the client side database
  • Make the client able to detect when a database update is needed.

Oracle issue

There is a date formatting issue when doing Hibernate queries on Audit logs. => We should simply use Hibernate queryMaker to avoid that (AuditReader provides a method to allow parameters merge and will handle that!).

Technical tasks

Tomcat DS/Hibernate upgrade/Seam patch/H2 upgrade + MVCC branch

As already raised in previous tech reports, we still have several branches holding important infrastructure changes. We’re starting the merge of these branches.

  • NXP-10926: H2 upgrade + H2 MVCC mode + use H2 in memory so speed up tests
  • Tomcat Pool and ‘real’ XA mode + upgrade hibernate + patch Seam
  • Improvements on test framework and error management (need to create ticket)
  • Tomcat upgrade.

BIRT

A BIRT upgrade has been scheduled. The target is:

  • upgrade to BIRT 4.2
  • make an ODA connector for NXQL so that we can use NXQL from within BIRT designer.

PreSales sandbox

The PreSales team has started a sandbox where they code some extensions that can be useful for building demos and POCs. See nuxeo-presales-prototyping-toolkit.

Part of this work is also to fill the potential holes we can have in the API or in the Extension Point system. That’s why we did a quick review with Benjamin and a lot of the work should be directly merged inside the trunk:

  • missing Operations
  • Operation with missing parameters
  • new Publisher extensions

Automation

As already explained in previous reports, changes in Automation API are one of the cornerstone of the next release.

Automation improvements

  • do some non-regression validation tests
  • merge Olivier’s changes on JSON marshaling

Angular Layout/CRUD Automation/Automation

Damien has started working on:

  • adding a REST CRUD API
  • improving the JS client and make integration with AngularJS
  • wrapping the Layout system

Layouts/Widgets/Studio/DAM

Widgets

Tabs widgets

We are almost ready to have a Widget that renders tabs defined by actions and allows Ajax Switching. This will be useful for DAM, but more globally, this would good for CAP too. So, the next steps include:

  • finishing the implementation
  • use by default in Nuxeo CAP tab system
  • may be merging with changes on RESTDocumentLink for conversation management NXP-11331
  • updating Selenium tests

Action types // Widget types

We are now very close to being able to use WidgetTypes to define Action types. The idea is basically to make action types more than a simple string attribute, but associate it with a set of property that can be defined by the user. Doing so would be good:

  • for Studio, action configuration would be easier since we can have a form
  • for documentation and showcase
  • for Ajax behavior

Ajax Rerender and duplicated IDs

Depending on layout and widgets config there may some cases where RichFaces + Facelet infrastructure fails to correctly manage Ids. For now, this is a problem that:

  • can be avoided via small workarounds in most cases
  • can not be fixed in Nuxeo
  • can not be easily fixed in JSF/Facelets

The JSF duplicate ID issue on ajax rerender has been identified, and fixed by using the nxu:repeat tag (revisited) and making it create new sub-components when it detects that the iteration list has changed. Otherwise existing components are reused and their ID is not reset, leading to potential duplicate IDs. See https://jira.nuxeo.com/browse/NXP-11434.

Anahide will try to submit the problem to RichFaces/JSF2 community to see if we have some feedback.

DAM

Box listing

The Box listing is used to manage the DAM thumbs view inside a ContentView. This works in DAM, but there is still some work to be done:

  • integration inside CAP/DM to manage the ‘BigIcon view’
  • standardize the DAM selection use case (clicking on the Thumb in DAM changes the currentDocument)
  • Studio editor UI to be adapter to Box listing display
    • this is not strictly needed for now, but this would be really better.

Selection system

For ContentView the current selection model is built on the use of the DocumentListsManager. This system that is also used for Worklist and Clipboard becomes an issue for selection:

  • when DAM and DM show the same ContentView but with different filters
  • when there are several ContentView in the same page

If we allow to have several selection lists, this should impact the Copy/Past/Move actions available on ContentView.

Next steps

The next steps on DAM include:

  • Tab Widget integration
  • Html5 DnD integration for mass Import
    • start from existing code
    • add Canavas based preview if we have time
    • add support for async processing
  • ‘Ajax Permlink’ (already discussed but not done)
  • functional testing

Connect/Studio

Advanced Studio

Part of the ongoing work is about unlocking advanced feature so that people using studio don’t end up with a lot of XML Override in the XML extensions. This typically includes:

  • Extended ContentView configuration
  • Widgets configuration
  • Selection configuration

Having all these new options:

  • will unlock feature for advanced users
  • may confuse basic users and lead them into problem
    • problems we will have to fix via support

This may be worth adding a disclaimer saying something like ‘use advanced settings only if you now what you are doing, support won’t be able to easily help you if you mess up something’.

The post [Nuxeo Tech Report] News from the Developer Front #6 appeared first on Nuxeo Blogs.

[Q&A Friday] How to Access a Resource Bundle with MVEL in Content Automation

$
0
0
Access resource bundle label with MVEL

Access resource bundle label with MVEL

There’s an interesting question today from milonette, asking how to access labels with MVEL. Sometimes the Content Automation API doesn’t provide everything you need out of the box, hence the question. But the good news is this API is extensible.

While some functions are already available in the automation context, there is currently nothing to access the resource bundles. We’ll need to add our own function into it. This is quite easy to do. First we need to write the new function we need, then we’ll write an operation that adds this function to the content automation context.

Our localized function needs different parameters. We want the locale, the name of the resource bundle, the key of our message and some optional parameters. Here’s a simple implementation:

package org.nuxeo.sample;

import java.util.Locale;

import org.nuxeo.common.utils.i18n.I18NUtils;

public class LocalizationFunctions {

    public static String localize(String bundleName, String localeStr, String key, String... params) {
        Locale locale = Locale.forLanguageTag(localeStr);
        return I18NUtils.getMessageString(bundleName, key, params, locale);
    }

}

Now that I have my function, I need to create an operation that will make this available within the automation context:

package org.nuxeo.sample;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Context;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;

@Operation(id = AddLocalizationFunctions.ID, category = Constants.CAT_SCRIPTING, label = "AddLocalizationFunctions", description = "Add new function namespace to automation context.")
public class AddLocalizationFunctions {

    public static final String ID = "AddLocalizationFunctions";

    public static final String LOCALIZATION_FUNCTION_NAME = "LocFn";

    protected static final Log log = LogFactory.getLog(AddLocalizationFunctions.class);

    @Context
    protected OperationContext ctx;

    @OperationMethod
    public void run() {
        ctx.put(LOCALIZATION_FUNCTION_NAME, new LocalizationFunctions());
    }

}

This should do the trick. Now if I do an operation chain starting with this operation, I’ll be able to use this function:

    <chain id="test2">
      <operation id="AddLocalizationFunctions"/>
      <operation id="Seam.AddInfoMessage">
        <param type="string" name="message">expr:LocFn.localize("messages", "en", "label.permalink.description",{"param1", "param2"})</param>
      </operation>
    </chain>

Notice the use of an inline MVEL array to comply with String… params argument. Now you can go wild and add any functions you want. In the future we’ll try to make this extensible through an extension point.

The post [Q&A Friday] How to Access a Resource Bundle with MVEL in Content Automation appeared first on Nuxeo Blogs.

[Monday Dev Heaven] Nuxeo and Atlassian HipChat Integration

$
0
0
HipChat

Atlassian HipChat

Being big Atlassian fans here at Nuxeo, we recently started using HipChat. It’s an enterprise chat room. One of the cool things about HipChat is its very simple web API. It makes it really easy to send notifications to a chat room.

To show you how dead easy it is, I did a project showing how to send Nuxeo events to a Hipchat room using their web API. I did it using Nuxeo IDE to generate my plugin structure, using the Nuxeo Plugin Project and Nuxeo Listener wizards. My listener only listens to documentCreated and documentModified events. Each time they occur, we send a small message to a HipChat room, containing the URL of the document, its title, date and creator. The code is really simple (especially because most of it is generated by Nuxeo IDE):

/*
 * (C) Copyright ${year} Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     ldoguin
 */

package org.nuxeo.sample;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.EventListener;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.platform.url.DocumentViewImpl;
import org.nuxeo.ecm.platform.url.api.DocumentView;
import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
import org.nuxeo.runtime.api.Framework;

/**
 * @author ldoguin
 */
public class HipChatListener implements EventListener {

    public static Log log = LogFactory.getLog(HipChatListener.class);

    public static final String MESSAGE_TEMPLATE = "Document <a href=\"%s\">%s</a> has been modified by %s on the %s";

    public static final String BASE_URL = "https://api.hipchat.com/v1/rooms/message?format=json&auth_token=%s";

    public void handleEvent(Event event) throws ClientException {
        // First we make sure that the event is about a document
        EventContext ctx = event.getContext();
        if (!(ctx instanceof DocumentEventContext)) {
            return;
        }
        DocumentEventContext docCtx = (DocumentEventContext) ctx;
        DocumentModel doc = docCtx.getSourceDocument();
        // Once we have the document, we can start building the message we'll send to HipChat

        // First we build the document's URL
        DocumentView docView = new DocumentViewImpl(doc);
        DocumentViewCodecManager docLocator = Framework.getLocalService(DocumentViewCodecManager.class);
        String nuxeoUrl = Framework.getProperty("nuxeo.url");
        String docUrl = docLocator.getUrlFromDocumentView(docView, true,
                nuxeoUrl + "/");
        // Than the message body
        Date date = new Date(event.getTime());
        String chatMessage = String.format(MESSAGE_TEMPLATE, docUrl,
                doc.getTitle(), docCtx.getPrincipal().getName(),
                date.toString());
        log.debug(chatMessage);
        try {
            // now we can send it to HipChat
            sendMessage(chatMessage);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void sendMessage(String message) throws IOException {
        // We retrieve every needed property from nuxeo.conf
        String roomId = Framework.getProperty("hipchat.roomId");
        String authToken = Framework.getProperty("hipchat.authToken");
        String userName = Framework.getProperty("hipchat.userName");
        String notify = Framework.getProperty("hipchat.notify");
        String color = Framework.getProperty("hipchat.color");

        // then we do the actual rest call to HipChat web API
        String url = String.format(BASE_URL, authToken);
        HttpClient client = new HttpClient();
        PostMethod post = new PostMethod(url);
        try {
            post.addParameter("from", userName);
            post.addParameter("room_id", roomId);
            post.addParameter("message", message);
            post.addParameter("color", color);
            post.addParameter("notify", notify);
            post.getParams().setContentCharset("UTF-8");
            client.executeMethod(post);
        } finally {
            post.releaseConnection();
        }
    }
}

What it does is simply relay the modify or create events to a room. This is the simplest integration possible, but there are many other cool use cases you can implement. You could have a dedicated room per Nuxeo group, you could integrate this with the notification module and send personal notifications, etc. Let us know what you think about this — are you using the same kind of tools to collaborate? Would like to see integrations between other collaboration tools and Nuxeo?

The post [Monday Dev Heaven] Nuxeo and Atlassian HipChat Integration appeared first on Nuxeo Blogs.

[Q&A Friday] How to add tagging capability during drag’n'drop

$
0
0
How to add tagging capability during Drag and Drop

How to add tagging capability during drag’n'drop

Here’s a question that comes back often, asked by bruce: How to add tag capability during drag’n'drop?. Since Thierry added HTML5 drag’n'drop to the Nuxeo Platform, it’s possible to fill in metadata right after the import, and apply them to all the imported documents. But I understand it can be frustrating not being able to add tags. And fortunately the drag’n'drop service for content capture is extensible. The full code sample is available on GitHub.

So here’s how it works. When dragging files in a content view for at least 2 seconds, you’ll be prompted with a Select import operation choice. Those select operations are actually actions. It means you can add as any as you want through the action extension point. These actions have a specific behavior.

The ID of the action must, for instance, be the ID of the operation or the operation chain that will be executed when dropping the files. The link will display an iFrame itself displaying the layout passed as query parameter (layout=dndTagsEdit). The schema query parameter (schema=dc) is used to specify which schema you need to update with the chosen layout. Take a look at the documentation for the other specificities of those actions.

<?xml version="1.0"?>
<component name="org.nuxeo.sample.dnd.actions.contrib">

  <require>org.nuxeo.ecm.platform.actions</require>

  <extension target="org.nuxeo.ecm.platform.actions.ActionService"
    point="actions">

    <action id="Chain.FileManager.ImportWithMetaDataAndTagsInSeam"
      link="${org.nuxeo.ecm.contextPath}/dndFormCollector.faces?schema=dc&#038;layout=dndTagsEdit"
      order="30" label="label.smart.import.with.mdTags"
      help="desc.smart.import.with.md.mdTags">
      <category>ContentView</category>
      <filter-id>create</filter-id>
    </action>

  </extension>

</component>

There are two things different from the classic import with metadata: the layout and the operation chain. The operation chain as an intermediate operation called Document.TagDocument. It’s responsible for the actual tagging of every document.

/*
 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     ldoguin
 */

package org.nuxeo.sample.dnd;

import java.io.Serializable;
import java.util.List;

import org.jboss.seam.contexts.Contexts;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Context;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.collectors.DocumentModelCollector;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.platform.tag.TagService;

/**
 * @author ldoguin
 */
@Operation(id = TagDocument.ID, category = Constants.CAT_DOCUMENT, requires = Constants.SEAM_CONTEXT, label = "TagDocument", description = "")
public class TagDocument {

    public static final String ID = "Document.TagDocument";

    @Context
    protected CoreSession session;

    @Context
    protected OperationContext ctx;

    @Context
    TagService tagService;

    @OperationMethod(collector = DocumentModelCollector.class)
    public DocumentModel run(DocumentModel doc) throws Exception {
        if (!isSeamContextAvailable()) {
            return doc;
        }
        List tags = getDndTagsFormAction().getDndTags();
        for (Serializable tag : tags) {
            tagService.tag(session, doc.getId(), (String) tag,
                    ctx.getPrincipal().getName());
        }
        return doc;
    }

    public static boolean isSeamContextAvailable() {
        return Contexts.isSessionContextActive();
    }

    public static DndTagsFormActionBean getDndTagsFormAction() {
        return (DndTagsFormActionBean) Contexts.getConversationContext().get(
                "dndTagsFormActions");
    }

}

As you can see, it retrieves the list of tags from a SEAM bean. It’s the backing bean of a new widget I have defined in my custom layout dndTagsEdit. This layout is almost the same as the original one, except it displays the tag widget. Take a look at the code and the documentation – it’s pretty easy to understand.

The post [Q&A Friday] How to add tagging capability during drag’n'drop appeared first on Nuxeo Blogs.


Make It Red and Uppercase with Nuxeo Studio

$
0
0

Building a wonderful content-centric application with a perfect taxonomy, with damn smart workflows, with incredible automation chains, etc. is great, sure. Still. You still need to paint it, if you see what I mean.

Nuxeo Studio lets you easily define the main branding of your application: login screen, colors used for text, background, links, … It is documented here. That said, you also often need to be more context-specific. For example, you may want to display a text widget in red so the user knows it is an important part of the layout. Or you may also want to force data entry with upper case letters for example.

Let’s take the following example: A workflow…

blogta2-01-Workflow

… has a “name” workflow variable. The form of its “First task” node displays this variable in a text widget…

blogta2-02-DropField

… and you want it to be displayed in red.

So, you set its “Style class” property to “doItRed”…

blogta2-03-StetStyleClass

…you save, and in the branding you create the doItRed style:

bogta2-00-Red-StyleInBranding

Then, you test your work(1).

. . . [here, I'm waiting while you are testing] . . .

You probably noticed something very unfortunate happened: It does not work as expected. The value we entered in the “Name” field is displayed in black. We want it red, right? So, how can we do this? How can we change the style of a widget?

Before explaining how you can customize this widget, I’d like to say you could have used one of the predefined classes provided by Nuxeo (and explained in the Nuxeo UI Style Guide.) For example, by using “warning” or “error”, our text would have been displayed in red. But here, we want it red-red, so we have an example of customization that can’t use a predefined class.

If you need the thing to be red now, immediately, can’t wait, emergency-emergency, then you may have the idea to use !important:

.doItRed {
    color:red !important;
}

And well, yes, it works. But the thing is: You don’t want to use it. Really. I believe !important has been added to the specification when the CSS team was spending time in Vegas with The Hangover folks. If you start using it, you’ll soon find yourself using it everywhere and at some point in a short future, nothing will work at all, your user interface will be broken. Instead of using it, it is better to understand why your style is not used. So, we remove this !important thing, save, update our project in the server and run it.

A web inspector is the perfect tool for the purpose of understanding what happened to our style.  So, at runtime, we display the inspector and select our widget. The right pane clearly shows that the class has been overridden (it is in strikethrough):

blogta2-04-WebInspectorStyle

What happened here is that the default nuxeo theme, “galaxyDefaultCssNNN” (with NN being a number dynamically evaluated by the server) has been applied after our own class, hence the fact that our red color is not used.

What can we do to display our widget in red? We must (besides not using The Suffix That Must Not Be Named) use a CSS definition that will have a higher priority than the Galaxy one. Using the DOM ID of the widget would work, but it is complicated to get this ID, since it is generated at runtime (we don’t know this ID at the time we are adding the class in Studio). The best solution is to define our style by using the class hierarchy that is applied to our widget:

.classOfAncestor [...] .classOfParent .classOfElement {...}

So, we take look at the web inspector and find the classes:

blogta2-05-CheckInheritance

It is good to be specific in the path and get the right ancestors. In our example, we want to apply the style only in the context of a task, so we walk the path up to “single_tasks_block”. The CSS we must define is:

.single_tasks_block .dataInput .fieldColumn .doItRed {
    color:red;
}

If you want to apply your red style in a wider area than a table, don’t hesitate to use the framework CSS classes: .nxHeader, .nxMainContainer, .nxFooter. These classes are called in all the pages of the platform.

There is one last thing to do actually: Our doItRed class becomes a place holder that we use for the “Style class” widget property. We can leave it empty. So, here is our CSS definition in the branding:

blobta2-00-StyleInBranding

Save. Update(2). Run. Enjoy.

We can now move a step further. Because at the beginning of the article, I was talking about displaying the field in red, but also as upper case. We want every character, every key hit by the user to be displayed as upper case.

How you implement this in Nuxeo Studio depends on what you need. Either you just want to display data entry in upper case, or you want the field itself, its content, to hold only upper case letters. Just displaying uppercase is quite easy — we add the appropriate text-transform style:

.single_tasks_block .dataInput .fieldColumn .doItRed {
    color:red;
    text-transform:uppercase;
}

That’s all. CSS handles the conversion at runtime. I’ll add that we should then rename our class, because giving useful names is useful(3). If we do rename the class, we will not forget to change the “Style class” widget property in our task form.

Also notice that it is only about how the data is displayed: CSS does not change the actual content. In our example if the user enters “john” in the name workflow variable, it will be displayed as “JOHN”, but the variable itself still holds “john”.

If we want the transformation to be persistent, then we have to move to something different. Here is my suggestion: To stay with the CSS transformation for the display, and to add a “Transform to Uppercase” automation chain to the workflow. In our example, we added a basic “OK” button to the “First task” form. This button does nothing but move to “Second task”. We can now add an automation chain to the transition, which will be run when the user hits this “Ok” button. In the “Transitions” tab of the node properties dialog, click the “Create” button:

blogta2-06-createChain

We name the chain “upperCaseName”. Our challenge is to set the name field to uppercase. It is a workflow variable. So we use the “Workflow context > Set context variable” operation chain. We can safely remove the first, default action (“Fetch > Context document”) that was created by Studio because we are not – in this example – using the document.

Here is what our chain looks like:

blogta2-07-EmptyChain

The “name” field is obvious: our workflow variable’s name is “name” (funny, isn’t it?).

Now what should we put in the “value” field of this chain to transform the characters to uppercase? You can use here a MVEL expression. Not everything is allowed in this context, but to make a long story short, basically, everything that is put between @{ and } is evaluated as a Java expression. Some expressions are very specific to Nuxeo, but you can also use a bit of Java here. And even more interesting for us, we can use its String class. And guess what? The String class has a toUpperCase() method whose function is quite clear in my opinion. This means that when you have a string, you just have to write…

myString.toUpperCase()

…to return a capitalized string. And a hard coded expression is also valid. I mean something like…

"nuxeo studio rocks".toUpperCase()

…returns “NUXEO STUDIO ROCKS”.

So basically, what we have to do now is to get the current value of the name workflow variable, and to transform it to upper case. Getting its value is done using the helper drop down menus of the expression dialog. We select “Workflow Variables” in first drop-down, then ["var"] in the second, and then we click “Insert”…

blogta2-11-Expression

…this inserts the expression:

@{WorkflowVariables["var"]}

We replace “var” with “name”, since it is the name of our variable. The expression between the curly brackets is evaluated at runtime and returns a string: The characters the user entered. Because it is a string, we can call the toUpperCase() Java String method. We just need to be careful and to let the whole thing inside the curly brackets:

@{WorkflowVariables["name"].toUpperCase()}

We’re done with our automation chain, which contains actually one single operation:

blogta2-12-ExpressionFinal

We save, update and run. After starting a workflow on a document, here is my first task. I entered everything in lower case, “working well”. The CSS transformation displays it as we want:

blogta2-13-WorkWellTask1

Then, when I hit the “OK” button, the chain is run and “working well” is transformed using Java. So its value is now “WORKING WELL”:

blogta2-14-WorkWellTask2

Isn’t it wonderful that you can customize your application in such detail with Nuxeo Studio?

And don’t forget to add comments in your CSS to make it readable and easy to follow for you team(5).

(1) Yes. Checking that what we do works as expected is part of our lives
(2) Update the project in the server. Something we sometimes forget. Then we wonder why our changes don’t work. Come on.
(3) Yes, that’s what I wrote: Useful is useful.
(4) If you are not very familiar with Java, it is not a problem at all. Finding the appropriate function requires a simple Internet search on keywords like “java upper case”. You’ll get an answer such as “use the toUpperCase method of the String class”
(5) Lise Kemen helped me a lot on this article. She is our CSS super specialist (see the Nuxeo UI Style Guide for example). She also she really insisted for this “comment your class” to be added. And when Lise really insists, well, you know, you just follow the orders.

Enhanced by Zemanta

The post Make It Red and Uppercase with Nuxeo Studio appeared first on Nuxeo Blogs.

Extend Nuxeo Drive Series #1 – Override Operations

$
0
0
Extend Nuxeo Drive

Extend Nuxeo Drive

Like most parts of the Nuxeo Platform, we designed Nuxeo Drive as something extensible. And there are indeed different ways you can customize it to fit your needs. I’ll be writing more posts about how to extend Nuxeo Drive in the coming weeks. Today we’ll take a look at what you can do by simply overriding the different operations used by Nuxeo Drive.

Let’s take a very simple example. When Drive detects a conflict, the file is renamed a certain way. This is done using the NuxeoDrive.GenerateConflictedItemName operation. So what we can do is override this operation to specify our own way of renaming a file. Right now what it does is split the name of the file and the extension. Then we generate the contextSection. It’s the user’s first and last name concatenated with the current date formatted as “yyyy-MM-dd hh-mm”. Then it puts them back together.

To override it, you need to declare your operation as usual, but don’t forget to use the exact same ID as the operation you want to override. Here I’m changing the date format to “dd-MM-yyyy hh-mm” and adding the user’s email address.

/*
 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     ldoguin
 */

package org.nuxeo.sample;

import java.text.SimpleDateFormat;

/**
 * @author ldoguin
 */
// Use the same ID as the operation to override
@Operation(id = NuxeoDriveGenerateConflictedItemName.ID, category = Constants.CAT_SERVICES, label = "Nuxeo Drive: Generate Conflicted Item Name")
public class NuxeoDriveCustomGenerateConflictedItemName {

    @Context
    protected OperationContext ctx;

    @Param(name = "name")
    protected String name;

    @Param(name = "timezone", required = false)
    protected String timezone;

    @OperationMethod
    public Blob run() throws Exception {

        String extension = "";
        if (name.contains(".")) {
            // Split on the last occurrence of . using a negative lookahead
            // regexp.
            String[] parts = name.split("\\.(?=[^\\.]+$)");
            name = parts[0];
            extension = "." + parts[1];
        }
        NuxeoPrincipal principal = (NuxeoPrincipal) ctx.getPrincipal();
        String userName = principal.getName(); // fallback
        if (!StringUtils.isBlank(principal.getLastName())
                && !StringUtils.isBlank(principal.getFirstName())) {
            // build more user friendly name from user info
            userName = principal.getFirstName() + " " + principal.getLastName();
        }
        Calendar userDate;
        if (timezone != null) {
            userDate = Calendar.getInstance(TimeZone.getTimeZone(timezone));
        } else {
            userDate = Calendar.getInstance();
        }
        // I'll use a French date format
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy hh-mm");
        dateFormat.setCalendar(userDate);
        String formatedDate = dateFormat.format(userDate.getTime());
        // and I'll add the user's email
        String contextSection = String.format(" (%s - %s - %s)", userName,
                principal.getEmail(), formatedDate);
        String conflictedName = name + contextSection + extension;
        return NuxeoDriveOperationHelper.asJSONBlob(conflictedName);
    }

}

Don’t forget the require tag and the replace)”true” attribute to make sure your operation override the default one:

<component name="org.nuxeo.sample.NuxeoDriveCustomGenerateConflictedItemName">

  <require>org.nuxeo.drive.operations.NuxeoDriveGetRootsOperation</require>

  <extension target="org.nuxeo.ecm.core.operation.OperationServiceComponent"
    point="operations" >
      <operation replace="true" class="org.nuxeo.sample.NuxeoDriveCustomGenerateConflictedItemName" />
   </extension>

</component>

That’s just a simple example, but you can do a lot more. Every call from Drive to the server are made through Content Automation. So that means you can override pretty much everything it does. All these operations are located in the nuxeo-drive-operations bundle. Most of them are using the FileSystemItemManager and NuxeoDriveManager services. I’ll write more about this in the next posts.

The post Extend Nuxeo Drive Series #1 – Override Operations appeared first on Nuxeo Blogs.

Extend Nuxeo Drive Series #2 – Override existing adapter parameters

$
0
0

Last week I started a series of blog post on how to extend Nuxeo Drive. Today I’ll write about factories and adapters. Every file or folder on the client file system is represented on the server side using adapters. Those adapters are associated to a particular document through a factory.

  - FileSystemItem  // Represents any file on the fs
    | -- FileItem    // A File
         |-- DocumentBackedFileItem // Default adapter implementation for non-folderish document
    | -- FolderItem  // A Folder
         | -- AbstractVirtualFolderItem // Represents a folder on the client side, that is not backed by an actual document
             | -- DefaultTopLevelFolderItem // The default Nuxeo Drive folder, containing your synchronized documents.
         | -- DocumentBackedFolderItem // Default adapter for folderish document
              | -- DefaultSyncRootFolderItem // Adapter for the root synchronized doc. (put simply, the docs where you clicked on the sync button)
              | -- UserWorkspaceTopLevelFolderItem  // User workspace based implementation of the top level
              | -- UserWorkspaceSyncRootParentFolderItem // User workspace based implementation of the synchronization root parent

We can take the default configuration as an example to better understand this.

A look at the default configuration

Using Drive on the client side, the first entry point is the Nuxeo Drive folder. It contains all the documents you have synchronized. This folder is represented on the server side through the DefaultTopLevelFolderItem adapter, and returned by the DefaultTopLevelFolderItemFactory factory. This configuration is declared in the org.nuxeo.drive.service.FileSystemItemAdapterService#topLevelFolderItemFactory extension point:

  <extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
    point="topLevelFolderItemFactory">

    <topLevelFolderItemFactory
      class="org.nuxeo.drive.service.impl.DefaultTopLevelFolderItemFactory">
      <parameters>
        <parameter name="folderName">Nuxeo Drive</parameter>
      </parameters>
    </topLevelFolderItemFactory>

  </extension>

As you can see, you can change the folderName parameter to something else. Some might prefer ‘My Drive’ or ‘My Nuxeo Files’ as name for your sync documents. The default implementation simply lists all the synchronization roots, which is to say all the document where you clicked on the Sync Drive icon. You can imagine different implementations like one that would display the content of your personal workspace along your synchronization roots. You could also have a hard-coded implementation that shows the same documents for every Nuxeo users. That’s really up to you :) . It doesn’t even have to be documents. You could have a folder called My Directories that would have a CSV file per directories declared in Nuxeo. (lots of coding involved but why not?)

Anyway, inside the default Nuxeo Drive folder you will see your synchronized roots and their children. This is managed by the org.nuxeo.drive.service.FileSystemItemAdapterService#fileSystemItemFactory extension point. Every one of these factories has an order attribute. They are resolved from the lowest to the highest. The first factory that returns an adapter is used. As you can see the lowest order is set on the defaultSyncRootFolderItemFactory. It will be returned if the adapted document has the DriveSynchronized facet. And the thing is, each time you click on the sync button, this facet is added to the document. As the child documents won’t have this facet, defaultSyncRootFolderItemFactory won’t match and the defaultFileSystemItemFactory will be used.

  <extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
    point="fileSystemItemFactory">

    <fileSystemItemFactory name="defaultSyncRootFolderItemFactory"
      order="10" facet="DriveSynchronized"
      class="org.nuxeo.drive.service.impl.DefaultSyncRootFolderItemFactory" />
    <fileSystemItemFactory name="defaultFileSystemItemFactory"
      order="50" class="org.nuxeo.drive.service.impl.DefaultFileSystemItemFactory">
      <parameters>
        <parameter name="versioningDelay">3600</parameter>
        <parameter name="versioningOption">MINOR</parameter>
      </parameters>
    </fileSystemItemFactory>

  </extension>

As you can see the defaultFileSystemItemFactory factory takes some parameters. They describe the default versioning behaviour. The adapter returned by the factory is DocumentBackedFileItem, which has a versionIfNeeded method called when the associated file is changing. This method first looks if the document needs to be versioned. This will occur if the current contributor is different from the last contributor or if the last modification was done more than 3600 seconds ago. As you have already guessed, 3600 is not arbitrary and comes from the versioningDelay parameter of the factory. If the doc needs to be versioned, the following code is executed:

            doc.putContextData(VersioningService.VERSIONING_OPTION,
                    factory.getVersioningOption());

We put the versioningOption parameter in the document context map. This information will be retrieved and used next time the document is saved. So by default, we do minor increments. Possible values are MINOR, MAJOR and NONE.

Customize the default configuration

Here’s an example of custom configuration of the default factories (no, we’re not going to write a complete new adapter or factory just yet). Let’s say I don’t want my Drive folder to be called Nuxeo Drive. I want it to be called ‘My Favorite Nuxeo Files’. And I also want to upgrade the major increment version number each time a document is modified using drive. This can be done using the following contributions.

<?xml version="1.0"?>
<component name="org.nuxeo.sample.drive.adapters" version="1.0">

  <!-- Make sure your contribution is registered after the default one -->
  <require>org.nuxeo.drive.adapters</require>

  <!-- Override the folderName parameter. No need to specify the class again as there can be only one topLevelFolderItemFactory. -->
  <extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
    point="topLevelFolderItemFactory">
    <topLevelFolderItemFactory>
      <parameters>
        <parameter name="folderName">My Favorite Nuxeo Files</parameter>
      </parameters>
    </topLevelFolderItemFactory>
  </extension>

  <!-- Override the versioningOption parameter. We need to keep the name of the contribution we want to override. The parameters will be merge with the existing one.-->
  <extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
    point="fileSystemItemFactory">
    <fileSystemItemFactory name="defaultFileSystemItemFactory">
      <parameters>
        <parameter name="versioningOption">MAJOR</parameter>
      </parameters>
    </fileSystemItemFactory>

  </extension>
</component>

Next week we’ll dig deeper into these factories and adapters.

The post Extend Nuxeo Drive Series #2 – Override existing adapter parameters appeared first on Nuxeo Blogs.

Extend Nuxeo Drive Series #3 – How to synchronize a document without attached binaries

$
0
0

As we’ve seen last week in the Extend Nuxeo Drive series, files and folders are represented server side using adapters. By default it’s the DocumentBackedFileItem adapter that is used for simple, non-folderish documents. When you get the file from a document, the document BlobHolder is used. It’s a document adapter used to get or set the main file of a document. It means that the file you see on the desktop while using Drive has been retrived with this code:

    BlobHolder bh = documentModel.getAdapter(BlobHolder.class);
    Blob b = bh.getBlob();

It also means that when you modify a file on the desktop, it’s updated to the document using the BlobHolder.setBlob method. Once you know that, you can do some interesting stuff without having to write Nuxeo Drive factories or adapters (sorry, not yet, maybe in the next post :-) ).

Here’s a fairly simple example. The book document type available in the BookTraining-Day3 studio template has no binary field, only metadata. So we have to think of a specific blob holder. We can easily represent it as a CSV file like this:


"Name","Value"
"Author","Soulcie"
"Borrowed By","Administrator"
"Category","comics/manga/managa1;comics/manga;"
"ISBN","1307483092"
"Publication date","5/23/13 12:00 AM"
"Rating","5"

Now imagine that when the user modifies some of the values, we update the document’s metadata. This will happen in the BlobHolder.setBlob method. Here’s a simple implementation of a BookBlobHolder:

package org.nuxeo.sample;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder;
import org.nuxeo.ecm.core.api.impl.blob.StringBlob;
import org.nuxeo.ecm.core.api.model.PropertyException;
import org.nuxeo.ecm.core.schema.DocumentType;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.ListType;
import org.nuxeo.ecm.core.schema.types.Type;
import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
import org.nuxeo.ecm.core.schema.types.primitives.DateType;
import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
import org.nuxeo.ecm.core.schema.types.primitives.LongType;
import org.nuxeo.ecm.core.schema.types.primitives.StringType;
import org.nuxeo.runtime.api.Framework;

import au.com.bytecode.opencsv.CSVReader;
import au.com.bytecode.opencsv.CSVWriter;

public class BookBlobHolder extends DocumentBlobHolder {

    public static final String[] HEADERS = { "Name", "Value" };

    public enum BookProperties {
        AUTHOR("Author", "bk:author"), BORROWED_BY("Borrowed By",
                "bk:borrowedBy"), CATEGORY("Category", "bk:category"), ISBN(
                "ISBN", "bk:isbn"), PUBLICATION_DATE("Publication date",
                "bk:publicationDate"), RATING("Rating", "bk:rating");

        private String propertyName;

        private String name;

        private BookProperties(String name, String propertyName) {
            this.name = name;
            this.propertyName = propertyName;
        }

        public void setProperty(DocumentModel doc, String stringValue)
                throws PropertyException, ClientException {
            DocumentType docType = Framework.getLocalService(
                    SchemaManager.class).getDocumentType(doc.getType());
            Serializable value = convertStringValue(docType, propertyName,
                    stringValue);
            doc.setPropertyValue(propertyName, value);
        }

        public String getPropertyValue(DocumentModel doc)
                throws PropertyException, ClientException {
            DocumentType docType = Framework.getLocalService(
                    SchemaManager.class).getDocumentType(doc.getType());
            Serializable value = doc.getProperty(propertyName).getValue();
            if (value == null) {
                return "";
            }
            return convertValueToString(docType, propertyName, value);
        }

        public String[] getLine(DocumentModel doc) throws PropertyException,
                ClientException {
            String[] line = new String[2];
            line[0] = name;
            line[1] = getPropertyValue(doc);
            return line;
        }

        public static BookProperties fromString(String name) {
            if (name != null) {
                for (BookProperties b : BookProperties.values()) {
                    if (name.equalsIgnoreCase(b.name)) {
                        return b;
                    }
                }
            }
            return null;
        }

        protected Serializable convertStringValue(DocumentType docType,
                String fieldName, String stringValue) {
            if (docType.hasField(fieldName)) {
                Field field = docType.getField(fieldName);
                if (field != null) {
                    try {
                        Serializable fieldValue = null;
                        Type fieldType = field.getType();
                        if (fieldType.isListType()) {
                            Type listFieldType = ((ListType) fieldType).getFieldType();
                            if (listFieldType.isSimpleType()) {
                                fieldValue = stringValue.split(";");
                            } else {
                                fieldValue = (Serializable) Arrays.asList(stringValue.split(";"));
                            }
                        } else {
                            if (field.getType().isSimpleType()) {
                                if (field.getType() instanceof StringType) {
                                    fieldValue = stringValue;
                                } else if (field.getType() instanceof IntegerType) {
                                    fieldValue = Integer.parseInt(stringValue);
                                } else if (field.getType() instanceof LongType) {
                                    fieldValue = Long.parseLong(stringValue);
                                } else if (field.getType() instanceof DoubleType) {
                                    fieldValue = Double.parseDouble(stringValue);
                                } else if (field.getType() instanceof BooleanType) {
                                    fieldValue = Boolean.valueOf(stringValue);
                                } else if (field.getType() instanceof DateType) {
                                    fieldValue = SimpleDateFormat.getInstance().parse(
                                            stringValue);
                                }
                            }
                        }
                        return fieldValue;
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return null;
        }

        protected String convertValueToString(DocumentType docType,
                String fieldName, Serializable value) {
            if (docType.hasField(fieldName)) {
                Field field = docType.getField(fieldName);
                if (field != null) {
                    try {
                        String stringValue = null;
                        Type fieldType = field.getType();
                        if (fieldType.isListType()) {
                            Type listFieldType = ((ListType) fieldType).getFieldType();
                            if (listFieldType.isSimpleType()) {
                                String[] arrayValue = (String[]) value;
                                StringBuilder sb = new StringBuilder();
                                for (int i = 0; i < arrayValue.length; i++) {
                                    sb.append(arrayValue[i]);
                                    sb.append(";");
                                }
                                stringValue = sb.toString();
                            } else {
                                List<String> listValue = (List<String>) value;
                                StringBuilder sb = new StringBuilder();
                                for (String string : listValue) {

                                    sb.append(string);
                                    sb.append(";");
                                }
                                stringValue = sb.toString();
                            }
                        } else {
                            if (field.getType().isSimpleType()) {
                                if (field.getType() instanceof DateType) {
                                    Calendar date = (Calendar) value;
                                    stringValue = SimpleDateFormat.getInstance().format(
                                            date.getTime());
                                } else {
                                    stringValue = String.valueOf(value);
                                }
                            }
                        }
                        return stringValue;
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return null;
        }
    }

    public BookBlobHolder(DocumentModel doc, String path) {
        super(doc, path);
    }

    @Override
    public Blob getBlob() throws ClientException {
        try {
            StringWriter sw = new StringWriter();
            CSVWriter csw = new CSVWriter(sw, ',', '"');
            List<String[]> allLines = new ArrayList<String[]>();
            allLines.add(HEADERS);
            for (BookProperties bookProperty : BookProperties.values()) {
                allLines.add(bookProperty.getLine(doc));
            }
            csw.writeAll(allLines);
            csw.flush();
            csw.close();
            sw.close();
            Blob b = new StringBlob(sw.toString(), "text/csv");
            String filename = doc.getTitle() + ".csv";
            b.setFilename(filename);
            return b;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void setBlob(Blob blob) throws ClientException {
        if (blob == null) {
            return;
        }
        CSVReader csvReader = null;
        try {
            csvReader = new CSVReader(blob.getReader(), ',', '"');
            List<String[]> lines = csvReader.readAll();
            for (String[] line : lines) {
                String name = line[0];
                BookProperties bookProperty = BookProperties.fromString(name);
                if (bookProperty != null && line.length == 2) {
                    String value = line[1];
                    bookProperty.setProperty(doc, value);
                }
            }
            csvReader.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String getHash() throws ClientException {
        return doc.getId() + getModificationDate().toString();
    }

}
<?xml version="1.0"?>
<component name="org.nuxeo.sample.drive.bh.adapters">

  <extension
    target="org.nuxeo.ecm.core.api.blobholder.BlobHolderAdapterComponent"
    point="BlobHolderFactory">
    <blobHolderFactory name="bookBh" docType="Book"
      class="org.nuxeo.sample.BookBlobHolderFactory" />
  </extension>

</component>

There’s a lot of boring code to get and set the values of the different properties used by the Book document type. All types are not supported, but it’s a start. It handles all scalar properties and simple lists of scalar item (using ‘;’ char as a separator). Now you will see all your Book documents as CSV files on the desktop. See, just with BlobHolder you can do some interesting things :)

The post Extend Nuxeo Drive Series #3 – How to synchronize a document without attached binaries appeared first on Nuxeo Blogs.

[Nuxeo Tech Report] News from the Developer Front #7

$
0
0

Hi everyone, and welcome to this new tech report. Today we have some news about Nuxeo Drive and Content Automation.

Nuxeo Drive

The first release of Drive is out and we did a quick review of the main tasks and main limitations that should be addressed in the next few weeks.

Identified issues / tasks

Adapter system: documentation and convergence

The Drive server side model is built on top of a pluggable adapter system so that mapping between file system items and document types can be configured.

The Adapter implementation:

  • holds the logic for getBlob, delete, etc.
  • hides the logic of differences between real documents and roots

The Adapter mapping system provides:

  • per instance mapping since binding is not limited to doc types
  • a complex adapter class hierarchy

This model is not simple and not easy to contribute, but we already had similar issues with similar modules (WSS / WebDav, Importer, Publisher).

We can not easily reduce the complexity needed to handle all use cases, but we should manage convergence between the Drive Adapter system and other file system-based adapters we have for WSS and WebDav.

Automation Operations: better JSON serialization

Drive uses a set of dedicated Operations. These operations do not return DocumentModel but the JSON serialization of the Adapter objects.

For now, the Operation:

  • handles JSON serialization inside the code
  • returns a Blob type

This does make sense for 5.6, but with the ongoing work on Automation in 5.7, this is not good:

  • serialization should be part of the JAX-RS/Automation marshaling infrastructure
  • Operation signature should return the Adapter object and not a Blob.

File system local mapping: handling multi-filling

The local database identifies resources using a compound id ‘adapterFactory|repo|id’, this system allows to handle both DocumentModel backed items as well as Virtual resources, however, the multi-filling use case (the same file at several places) is not yet handled correctly.

This may seem like a low priority use case, but this will actually be required to handle

  • virtual Folders defined by queries
  • LiveEdit working directory integration (see later)

Transaction issue: NXP-10964

Drive revealed a transaction issue with Automation: client may receive the result of an operation and start a new one before the first one is really commited. This problem is not specific to Drive and can be visible for other Automation clients doing extensive usage of API. This means this is something that should not be addressed in Drive, but directly in Automation, at least in 5.7 as part of the big changes in Automations.

Packaging and release issue: move clients archive to Marketplace package

In the current build system, the Drive JSF modules depends on the client installer builds to be able to embed them. This is an issue because it make the release process complicated and this forces Drive to out of the set of standard addons. We must move the client installers packaging inside the Marketplace Package so that we can break the dependency issue.

File upload

The file upload currently consumes a lot of memory because of the Multi-Part format usage. File upload should be moved to the Automation Batch API: this should allow to fix the memory issue without having to change the HTTPlib.

TODO List

Client and update policy

For now the client setups are hosted directly inside the Nuxeo server. In the future, we may want to have the client setups hosted on something like drive.nuxeo.com or updates.nuxeo.com, pretty much as we used to do for the Firefox plugins.

The ideal update process should:

  • be driven from the client side
    • in most of the cases the Nuxeo Server won’t have access to internet whereas the client will
  • fetch a descriptor about possible updates (XML or JSON file)
  • take into account the Nuxeo Drive Server side version to check what is the most up to date client version
  • download the update if needed
    • we don’t need a real auto-update, downloading and restarting the installer is ok

Scan system

The current client side implementation does a full filesystem scan every 5 seconds to search for update:

  • this is not efficient when there are a lot of files
  • this consumes CPU and IO (and battery) for nothing

This means that very quickly Drive client becomes a pain, so we must fix this quickly. To optimize that we should simply subscribe to FileSystem events:

  • wait for event in the local Drive sub dirs
  • start the scan only after an update

Ben can probably give some help on that (NXP-9583).

Http Proxy

In most companies, Nuxeo Drive will need to go through proxies to be able to reach Nuxeo server. While simple proxy configuration is trivial, this may be more complicated for people using .PAC

We had such issues with LiveEdit, possible leads may be:

  • use native Win32 API?
  • leverage some QT Win32 bindings?

Automation

The work is moving forward and will be restarted early next week:

  • with Damien on REST
  • with Vlad on Marshaling

Solaris support

Florent commited this week an initial support for Solaris in 5.7.

For now, the fix is “just about” the Process Manager, if we really want to have support for Solaris, we should:

  • check other external dependencies under Solaris
    • OpenOffice + JOD, pdf2html, imagegamick, ffmpeg…
  • add Solaris slaves on CI

Basically, this is doable, but there is a cost. Before going further we should wait for input from the community side: is Solaris a real requirement?

VCS Cache

Ben restarted the work on VCS Caching to realign the alternative cache implementation on 5.7. It now works with EhCache with several configuration options:

  • Single cache (no MVCC like before)
  • XA mode (MVCC done by EHCache)
  • XA mode + Ram Disk cache

We did some benchmark, but so far the performance with EhCache are not really better:

  • EhCache has by itself an overhead (about 10x slower than pure SoftRef)
  • Disk storage is 100x slower than SoftRef (Serialization cost)
  • XA mode is about 100x slower than SoftRef
  • XA mode and Disk storage is 120x slower than SoftRef

In addition, for now we have worked at the RowMapper level, the pristine cache that is managed by the PersistenceContext is also:

  • a kind of cache
  • something that uses lot of memory and slows down the GC

As long as we have this pristine cache using weak reference, we won’t be able to see the real benefit of the EhCache integration. It means that we should also move the pristine cache to EhCache, which also mean that we must review the invalidation system. This could become very handy with clusters.

Pluggable Login Page

This is a work in progress that was started in the context of OpenId contribution. In addition, Lise has support request that ask how the Login Page could be better skinned. This shows that we should finish the work and do the required Studio integration.

External Providers

In the context of the OpenId work, the Login Page is now configurable via an Extension point. (cf NXP-10918)

We must still:

  • make the new login.jsp the default in Nuxeo
  • align Studio configuration on that

Login Page and Studio config

As already identified we should align Studio on the new Login.jsf screen:

  • remove the studio specific login.jsp
  • create a new Builder

In addition, we may want to

  • allow to configure iFrame URL/source in Studio
  • make CSS configurable: add a custom CSS just for the login page

See NXS-1466.

JSF and JS Widget: the select2 example

Thanks to the sponsoring of a small project (OOPlay), we started a module to integrate select2 javascript widgets as a Nuxeo Widget.

The tricky part is of course to align state management so that the widgets behave correctly:

  • in case of validation errors
  • in case of ajax requests
  • inside lists

Thanks to some wizardry of Anahide on nxu:ValueHolder (NXP-11533) we now have a simple integration sample for building hybrid JSF/JS widgets without having to write JSF components.

For now the result is visible in nuxeo-select2-integration. This should probably end up merged inside default Nuxeo, maybe along with other JS-based widgets!

Connect / Studio

Connect 1.16 / Studio 2.11

This next release will include some changes:

  • Nuxeo Connect front end UI
    • AngularJS + Automation to provide better screens and more features for our clients
  • New Studio Look (more CSS wizardary by Lise)
  • DAM features.

The post [Nuxeo Tech Report] News from the Developer Front #7 appeared first on Nuxeo Blogs.

Extend Nuxeo Drive Series #4 – Customize the Nuxeo Drive hierarchy

$
0
0

Keeping on with the extend Nuxeo Drive series, I’ll write about TopLevelFolderItemFactory. Just make sure you read the second post of the series first. The TopLevelFolderItemFactory is used to represent the Nuxeo Drive folder. Its default implementation displays the list of your synchronization root. This is the list of documents where you clicked on the sync icon. You can actually change this to display other things. You could, for instance, display the content of your personal workspace only. To do that we need a new TopLevelFolderItemFactory and the underlying adapter. Let’s get to it.

The New Top Level Item Factory

Instead of displaying all of the synchronized roots, we want to display the children of the user’s personal workspace. So we need to declare a new TopLevelFolderItemFactory. This is done, as usual, by contributing to an extension point. As there can be only one top level item factory, this contribution will override the previous one, but only if it is deployed after the previous contribution. This is why you see the “require” tag.

<?xml version="1.0"?>
<component name="org.nuxeo.sample.adapters.userworkspace"
  version="1.0">
  <require>org.nuxeo.drive.adapters</require>
  <extension target="org.nuxeo.drive.service.FileSystemItemAdapterService"
    point="topLevelFolderItemFactory">
    <topLevelFolderItemFactory
      class="org.nuxeo.sample.UserWorkspaceOnlyTopLevelFactory">
      <parameters>
        <parameter name="folderName">Nuxeo Drive</parameter>
      </parameters>
    </topLevelFolderItemFactory>
  </extension>
</component>

As you can see, we have a new class called UserWorkspaceOnlyTopLevelFactory. This is our new factory that will return our new adapter. It’s pretty much the same one as the default DefaultTopLevelFolderItemFactory, except that the adaptDocument method will return an instance of our new adapter, that isFileSystemItem will verify if the adapted document is indeed a UserWorkspace and the most important change, getTopLevelFolderItem returns the adapted user’s personal workspace.

/*
 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     Antoine Taillefer <ataillefer@nuxeo.com>
 */
package org.nuxeo.sample;

import java.security.Principal;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.drive.adapter.FileSystemItem;
import org.nuxeo.drive.adapter.FolderItem;
import org.nuxeo.drive.hierarchy.userworkspace.adapter.UserWorkspaceHelper;
import org.nuxeo.drive.service.FileSystemItemManager;
import org.nuxeo.drive.service.TopLevelFolderItemFactory;
import org.nuxeo.drive.service.impl.AbstractFileSystemItemFactory;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.repository.RepositoryManager;
import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService;
import org.nuxeo.runtime.api.Framework;

/**
 * @author Antoine Taillefer
 */
public class UserWorkspaceOnlyTopLevelFactory extends AbstractFileSystemItemFactory
        implements TopLevelFolderItemFactory {

    private static final Log log = LogFactory.getLog(UserWorkspaceOnlyTopLevelFactory.class);

    protected static final String FOLDER_NAME_PARAM = "folderName";

    protected static final String DEFAULT_FOLDER_NAME = "Nuxeo Drive";

    protected String folderName = DEFAULT_FOLDER_NAME;

    @Override
    public void handleParameters(Map<String, String> parameters)
            throws ClientException {
        // Look for the "folderName" parameter
        String folderNameParam = parameters.get(FOLDER_NAME_PARAM);
        if (!StringUtils.isEmpty(folderNameParam)) {
            folderName = folderNameParam;
        } else {
            log.info(String.format(
                    "Factory %s has no %s parameter, you can provide one in the factory contribution to avoid using the default value '%s'.",
                    getName(), FOLDER_NAME_PARAM, DEFAULT_FOLDER_NAME));
        }
    }

    @Override
    public boolean isFileSystemItem(DocumentModel doc, boolean includeDeleted)
            throws ClientException {
        // Check user workspace
        boolean isUserWorkspace = UserWorkspaceHelper.isUserWorkspace(doc);
        if (!isUserWorkspace) {
            log.trace(String.format(
                    "Document %s is not a user workspace, it cannot be adapted as a FileSystemItem.",
                    doc.getId()));
            return false;
        }
        return true;
    }

    @Override
    protected FileSystemItem adaptDocument(DocumentModel doc,
            boolean forceParentItem, FolderItem parentItem)
            throws ClientException {
        return new UserWorkspaceOnlyTopLevelFolderItem(getName(), doc, folderName);
    }

    @Override
    public FolderItem getVirtualFolderItem(Principal principal)
            throws ClientException {
        return getTopLevelFolderItem(principal);
    }

    @Override
    public String getFolderName() {
        return folderName;
    }

    @Override
    public void setFolderName(String folderName) {
        this.folderName = folderName;
    }

    @Override
    public FolderItem getTopLevelFolderItem(Principal principal)
            throws ClientException {
        DocumentModel userWorkspace = getUserPersonalWorkspace(principal);
        return (FolderItem) getFileSystemItem(userWorkspace);
    }

    protected DocumentModel getUserPersonalWorkspace(Principal principal)
            throws ClientException {
        UserWorkspaceService userWorkspaceService = Framework.getLocalService(UserWorkspaceService.class);
        RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
        // TODO: handle multiple repositories
        CoreSession session = getSession(
                repositoryManager.getDefaultRepository().getName(), principal);
        DocumentModel userWorkspace = userWorkspaceService.getCurrentUserPersonalWorkspace(
                session, null);
        if (userWorkspace == null) {
            throw new ClientException(String.format(
                    "No personal workspace found for user %s.",
                    principal.getName()));
        }
        return userWorkspace;
    }

    protected CoreSession getSession(String repositoryName, Principal principal)
            throws ClientException {
        return getFileSystemItemManager().getSession(repositoryName, principal);
    }

    protected FileSystemItemManager getFileSystemItemManager() {
        return Framework.getLocalService(FileSystemItemManager.class);
    }

}

The New Top Level Item Adapter

The goal of this factory is to get the user’s personal workspace, and return the appropriate TopLevelFolder adapter, UserWorkspaceOnlyTopLevelFolderItem. Its implementation is rather simple. It extends the default DocumentBackedFolderItem, but overrides some of its methods. The first three methods to override are rename, move and delete, inherited from AbstractFileSystemItem. While our user workspace is indeed a filesystem item, it’s also the top level element. So it makes no sense to move, rename or delete it. Let’s throw an UnsupportedOperationException instead of the default implementation.

The other method we need to override is the getChildren method. But just be sure to register the UserWorkspace as a synchronization root in case it hasn’t been already. The actual getChildren logic is still the one from DocumentBackedFolderItem.

/*
 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     Antoine Taillefer <ataillefer@nuxeo.com>
 */
package org.nuxeo.sample;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.drive.adapter.FileSystemItem;
import org.nuxeo.drive.adapter.FolderItem;
import org.nuxeo.drive.adapter.impl.DocumentBackedFolderItem;
import org.nuxeo.drive.service.NuxeoDriveManager;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.runtime.api.Framework;

/**
 * User workspace only based implementation of the top level {@link FolderItem}.
 * <p>
 * Implements the following tree:
 *
 * <pre>
 * Nuxeo Drive
 *  |-- User workspace child 1
 *  |-- User workspace child 2
 *  |-- ...
 * </pre>
 *
 * @author Antoine Taillefer
 */
public class UserWorkspaceOnlyTopLevelFolderItem extends
        DocumentBackedFolderItem {

    private static final long serialVersionUID = 1L;

    private static final Log log = LogFactory.getLog(UserWorkspaceOnlyTopLevelFolderItem.class);

    protected DocumentModel userWorkspace;

    public UserWorkspaceOnlyTopLevelFolderItem(String factoryName,
            DocumentModel userWorkspace, String folderName)
            throws ClientException {
        super(factoryName, null, userWorkspace);
        name = folderName;
        canRename = false;
        canDelete = false;
        this.userWorkspace = userWorkspace;
    }

    protected UserWorkspaceOnlyTopLevelFolderItem() {
        // Needed for JSON deserialization
    }

    @Override
    public void rename(String name) throws ClientException {
        throw new UnsupportedOperationException(
                "Cannot rename the top level folder item.");
    }

    @Override
    public void delete() throws ClientException {
        throw new UnsupportedOperationException(
                "Cannot delete the top level folder item.");
    }

    @Override
    public FileSystemItem move(FolderItem dest) throws ClientException {
        throw new UnsupportedOperationException(
                "Cannot move the top level folder item.");
    }

    @Override
    public List<FileSystemItem> getChildren() throws ClientException {
        // Register user workspace as a synchronization root if it is not
        // already the case
        if (!getNuxeoDriveManager().isSynchronizationRoot(principal,
                userWorkspace)) {
            getNuxeoDriveManager().registerSynchronizationRoot(principal,
                    userWorkspace, getSession());
        }
        // let DocumentBackedFolderItem handle the getChildren like usual
        return super.getChildren();
    }

    protected NuxeoDriveManager getNuxeoDriveManager() {
        return Framework.getLocalService(NuxeoDriveManager.class);
    }
}

My Synchronized Personal workspace

Now our Nuxeo Drive folder should contain only the children of our User Workspace. But you can still synchronize other documents. And it will actually work. It works because when you sync a document, you add the DriveSynchronized facet to it. And there is still an active factory for those. So now you should be wondering why they still show up on the desktop while our UserWorkspaceOnlyTopLevelFolderItem only returns the children of the personal workspace.

There is a reasonable explaination for that. Adding the facet triggers an event, which will be retrieved by your Nuxeo Drive client. Instead of asking for the top level item adapter chilren (which could be costly), it calls the getParentItem of the newly synced document’s adapter (the default is DefaultSyncRootFolderItemFactory). As you can see, its implementation returns the top level folder:

    protected FolderItem getParentItem(DocumentModel doc) throws ClientException {
        FileSystemItemManager fileSystemItemManager = Framework.getLocalService(FileSystemItemManager.class);
        Principal principal = doc.getCoreSession().getPrincipal();
        return fileSystemItemManager.getTopLevelFolder(principal);
    }

Hence Drive considering it as a child of our top level folder. We have different solutions to avoid this issue. We could disable the defaultSyncRootFolderItemFactory or remove all the Drive Sync icons. Removing the icons seems to be the best solution. We don’t want users to click on the icon and then see nothing happening in their Nuxeo Drive folder. To disable the icons, we have to override the actions contribution:

<?xml version="1.0" encoding="UTF-8"?>
<component name="org.nuxeo.drive.actions.hierarchy.userworkspace">
  <require>org.nuxeo.drive.actions</require>
  <extension target="org.nuxeo.ecm.platform.actions.ActionService"
    point="actions">
    <action id="driveSynchronizeCurrentDocument" enabled="false" />
    <action id="driveUnsynchronizeCurrentDocument" enabled="false" />
    <action id="driveNavigateToCurrentSynchronizationRoot" enabled="false" />
  </extension>
</component>

With this configuration, your users will only see the content of their personal workspace. And they won’t be able to add any other synchronization roots.

The post Extend Nuxeo Drive Series #4 – Customize the Nuxeo Drive hierarchy appeared first on Nuxeo Blogs.

Nuxeo / WebRatio Integration

$
0
0

We’ve been working on a project using WebRatio to build a Web application that is integrated with the Nuxeo Platform to store documents. We wanted to share some insights gained from this project.

WebRatio Integration Overview

WebRatio is a Web application builder based on Eclipse. In this tool, you can define:

  • Entities: Objects managed by the application
  • Views
  • Units: Links between Views with input, output and logic – a controller.

We decided to create a special Nuxeo unit linked directly to some operations or automation chains.

Here is the final application structure:

0

You can see that each box with a blue title defines a View, and boxes with a red title define a Unit.

We started to use a unit to execute Groovy scripts:

2

And finally, we created a special Nuxeo Unit that embeds the automation chain request:

1

And coupling management between the View and the Nuxeo Unit:

9
All works fine. As you can see, we start first with simple connection.

Security: Portal SSO Activation

We chose to use Portal SSO, plugin that enables a shared key between Nuxeo and WebRatio.

We added the following contribution to enable the Portal SSO login module in Nuxeo for an Automation REST request:

<component name=”ro.forwardsoftware.authentication.service.contribution”>

<require>org.nuxeo.ecm.platform.ui.web.auth.defaultConfig</require>
<require>org.nuxeo.ecm.automation.server.auth.config</require>

<extension
target=”org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService”
point=”authenticators”>
  <authenticationPlugin name=”PORTAL_AUTH”>
  <loginModulePlugin>Trusting_LM</loginModulePlugin>
  <parameters>
    <parameter name=”secret”>${nuxeo.shared.key}</parameter>
    <parameter name=”maxAge”>60</parameter>
  </parameters>
  </authenticationPlugin>
</extension>

<!– Include Portal Auth into authentication chain –>
<extension point=”specificChains” target=”org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService”>

  <specificAuthenticationChain name=”Automation”>
    <urlPatterns>
       <url>(.*)/automation.*</url>
    </urlPatterns>

    <replacementChain>
      <plugin>AUTOMATION_BASIC_AUTH</plugin>
      <plugin>PORTAL_AUTH</plugin>
      <plugin>ANONYMOUS_AUTH</plugin>
    </replacementChain>
  </specificAuthenticationChain>

</extension>

</component>

This component is enabled by a template (deployed with the Marketplace package) in our case.

But you can just:

  • replace the “${nuxeo.shared.key}” by your own shared key
  • and put this XML in a named file portal-sso-config.xml into $NUXEO_HOME/nxserver/config/ folder.

The WebRatio client just executes this to request Nuxeo:

HttpAutomationClient client = new HttpAutomationClient("http://server.name.fr/nuxeo/site/automation");

// if Portal SSO call
client.setRequestInterceptor(new PortalSSOAuthInterceptor(sharedKey, username));
Session session = client.getSession();
// if standard call
// Session session = client.getSession("user", "password");

Target Protype

The goal of the prototype was to manage cases.
There are two kinds of documents:

* A Case that stores some metadata and is linked to several Case Documents,
* A Case Document that stores a file and some metadata and is attached to a Case

The customer chose to store Cases in the WebRatio database and Case Documents in Nuxeo. They chose this because they are planning to create a workflow through WebRatio. So they wanted to have entities defined in WebRatio for that.

Prototype

Here is the view to create a new Case Document (stored in Nuxeo):

3

The Case Document is stored in Nuxeo with the Blob file and attached to the Case Object stored in the WebRatio database.

Here is the Case view that expose each Case stored in WebRatio:

4

And the detailed Case, that exposes each Case Document attached (without the version label, for those that understand this joke):

5

And finally, the detailed Case Document:

6

We’ve added some actions below: lock, unlock, and edit. We’ve also implemented a search view.

We asked the WebRatio architect about implementing persistence of WebRatio entities with Nuxeo, and this is totally possible as WebRatio manages XSD definitions and entity persistence is pluggable. So we could have used the CMIS integration while we wait for the CRUD REST/SOAP automation planned for Nuxeo 5.7.1 (around the end of June) to implement this.

Finally, this integration project took a grand total of 5 days, with Nuxeo training included. It was a really productive week.

The post Nuxeo / WebRatio Integration appeared first on Nuxeo Blogs.


Exploring Audit Tables and Nuxeo VCS

$
0
0

As you may perhaps know, VCS stands for Visible Content Store. It means that Nuxeo stores its documents in a SQL database, using a table for each schema or complex property type.

This enables various things:

  • Put indices on some columns in order to tune some particular queries
  • Access the tables to query data directly.

In this post, I will show you something else I did on a project in order to fill some data into a schema directly in SQL. I will use PostgreSQL for this example, but it should also work with other backends.

Querying nxp_log can be difficult

The problem here is to compute some statistics from the nxp_logs table that holds the data of the audit. Each row of the table holds the id of the document (log_doc_uuid), the category of the event (log_event_category) and the id of the event (log_event_id).

If we want to know how many times a given document has been modified, it’s quite simple in SQL:

select count(*) from nxp_logs
 where log_event_id = 'documentModified'
   and log_doc_uuid = '01431fe7-9154-4522-92eb-0dc0d8caa241'

Now, if we want to have this type of data for all documents, a simple GROUP BY statement will do the trick.

select log_doc_uuid, count(*) from nxp_logs
  where log_event_id = 'documentModified'
 group by log_doc_uuid

The real problem comes when you want to have statistics for a lot of different log_event_ids and a lot of documents. We won’t be able to query data with a simple query to have stats onto one line. We want to have something like a table that holds this data:

id cnt_modified cnt_locked cnt_checkin last_event_date
docid1 4 0 2 2013-05-15 13:42
docid2 3 1 0 2013-05-12 17:35

Moreover, when querying on a large document repository (such as several million lines in NXP_LOG) it can make your application server suffer a lot!

The solution

We want a statistic table, so let’s create it:

CREATE TABLE doc_stats
(
  id character varying(36) NOT NULL,
  cnt_modified bigint,
  cnt_locked bigint,
  cnt_checkin bigint,
  last_event_date timestamp without time zone,
  CONSTRAINT doc_stats_pk PRIMARY KEY (id)
)

Now we have to fill it with some values. As we said, it’s not easy to find a direct query to fill it, so we will begin by creating a view on the nxp_logs table in order to have some data directly ready:

CREATE OR REPLACE VIEW vw_doc_stats AS
 SELECT dc.id as id, last_date.lastevent as last_event_date, events.log_event_id as log_event_id, events.cnt as cnt
   FROM dublincore dc,
    -- Extract last_event_date
    ( SELECT log.log_doc_uuid, max(log.log_event_date) AS lastevent
           FROM nxp_logs log
          GROUP BY log.log_doc_uuid) last_date,
    -- Extract count of events
    ( SELECT log.log_doc_uuid, log.log_event_id, count(log.log_doc_uuid) AS cnt
           FROM nxp_logs log
          WHERE log.log_event_id IN ('documentCheckin', 'documentModified', 'documentLocked')
          GROUP BY log.log_event_id, log.log_doc_uuid) events
  WHERE dc.id = last_date.log_doc_uuid AND dc.id = events.log_doc_uuid
  ORDER BY last_date.lastevent DESC, dc.id;

The execution of this view will give you several rows per document — one for each eventId.

id last_event_date log_event_id cnt
docid1 2013-05-15 13:42 documentModified 4
docid1 2013-05-15 13:42 documentCheckin 2

The next thing we have to do now is to normalize that data in our doc_stats table. We will do it with a stored procedure that will iterate on the vw_doc_stats view:

CREATE OR REPLACE FUNCTION nx_update_doc_stats() RETURNS integer AS $$
DECLARE
    stat RECORD;
    node RECORD;
    nodeids integer[];
BEGIN
    RAISE NOTICE 'Refreshing Doc stats';
    TRUNCATE TABLE doc_stats;

    INSERT INTO doc_stats (id, last_event_date, cnt_commit, cnt_checkin, cnt_lock)
    SELECT DISTINCT id,last_event_date,0,0,0 FROM vw_doc_stats;

    FOR stat IN SELECT * FROM vw_doc_stats LOOP
        CASE stat.log_event_id
          WHEN 'documentModified' THEN
            UPDATE doc_stats SET cnt_modified = stat.cnt WHERE id=stat.id;
          WHEN 'documentCheckin' THEN
            UPDATE doc_stats SET cnt_checkin = stat.cnt WHERE id=stat.id;
          WHEN 'documentLocked' THEN
            UPDATE doc_stats SET cnt_lock = stat.cnt WHERE id=stat.id;
        END CASE;

    END LOOP;

    RAISE NOTICE 'Done refreshing Doc stats table.';
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

That’s it! Now when you call SELECT nx_update_doc_stats(); it will update your doc_stats table. It takes about 10 seconds for Postgres to update the stat table with about 10k documents.

In order to launch this command periodically, you can use:

  • a cron task: very efficient and simple, but it will add some packaging work
  • use the Nuxeo scheduler: same efficiency, but is included in the existing packaging.

The frosting on the cake

Yeah that’s very cool, we have a SQL table that holds our normalized data but… how can we use it now? One solution could be to use raw JDBC and native query. But as we are on top of VCS, we can do something smarter.

In fact, document schemas are stored in SQL tables. They have an id that references the id of the fragment in the hierarchy table and other columns that refer to the schema properties. The doc_stats table is created like that so we will be able to map a schema on this table:

      <?xml version="1.0" encoding="UTF-8"?>

        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
          xmlns:nxs="http://www.nuxeo.org/ecm/project/schemas/doc_stats"
          targetNamespace="http://www.nuxeo.org/ecm/project/schemas/doc_stats">

          <xs:element name="last_event_date" type="xs:date" />
          <xs:element name="cnt_modified" type="xs:integer" />
          <xs:element name="cnt_locked" type="xs:integer" />
          <xs:element name="cnt_checkin" type="xs:integer" />

        </xs:schema>

Now let’s bind this schema to a dynamic facet:

<component name="org.nuxeo.ecm.quota.core.types">

  <extension target="org.nuxeo.ecm.core.schema.TypeService" point="schema">

    <schema name="doc_stats" prefix="dstats"
      src="schemas/doc_stats.xsd"/>
  </extension>


  <extension target="org.nuxeo.ecm.core.schema.TypeService" point="doctype">

    <facet name="DocStats">
      <schema name="doc_stats" />
    </facet>
  </extension>
</component>

With all of this configuration, for a given document it’s very easy to get its statistics.

 DocumentModel doc = session.getDocument(new PathRef("/default-domain/workspaces/myDoc"));
 doc.addFacet("DocStats");
 seesion.getProperty("dstats:cnt_modified").getValue(Long.class)

VCS cache problem

There is still a small problem here. Since VCS has an internal cache mecanism, it will cache the values and not update them when we want. We have to find a hack to invalidate the VCS cache… at the SQL level.

When set up in cluster mode, VCS creates two tables that hold the invalidation state. The cluster_nodes table holds the list of all cluster nodes and thecluster_invals hold the list of fragments to invalidate for each node. When one node updates a document, it puts an entry in this table to tell the other nodes to invalidate their own data on the document.

We will set up our server in cluster mode and our stored procedure will act as a cluster node and tell every node to invalidate their cache on the doc_stats fragment. In fact, it’s quite simple with this SQL query:

INSERT INTO cluster_invals (nodeid,id,fragments,kind)
SELECT nodeid,id, '{doc_stats}',1 from cluster_nodes,doc_stats;

This is a query that makes a cross join between cluster_nodes and doc_stats and inserts the result into the invalidation table.

Adding this query in at the end of our procedure will tell VCS to invalidate the cache for that data.

Deployment

In order to deploy our view and stored procedure, there is a way since Nuxeo 5.7 to run some SQL DDL at the start of the Nuxeo instance. It’s just about another configuration of the repository. The documentation is here.

Conclusion and possible enhancements

In this post we saw how to query the audit table and normalize its data into a dedicated document’s schema. In this case, the data came from inside Nuxeo, but we could imagine that external systems can also fill our data with an ETL system, for instance.

Here we update the data by a scheduler, that means our data won’t be up to date between every update, but for the statistics it’s not that important. To enhance that, we could imagine using a trigger on the audit table that does quite the same job for every inserted row.

The post Exploring Audit Tables and Nuxeo VCS appeared first on Nuxeo Blogs.

First Steps with AngularJS and Nuxeo Content Automation

$
0
0

Nowadays, single page HTML5 apps are becoming more and more common. In this world, there is a JavaScript framework developed by Google that caught our attention. In fact, there is no surprise here: at the last Devoxx conference, AngularJS was pretty much everywhere!

Here at Nuxeo we tried to use it for an internal application and in this post, I will explain how we did it and what choices we made.

A Development Workflow is Required

One of the strengths of AngularJS is its capacity to be tested. What we would like to find is a way to have the same Test Driven Development workflow when we develop in JavaScript. The good news is that some people already made some tools for this:

  • Jasmine for testing
  • Karma to automate tests
  • Bower to fetch web packages
  • Grunt to run tasks (like ant or make)

One team at Google made a very cool tool that assembles all these called Yeoman. This tool offers a workflow and an opinionated way of doing things. Yeoman provides application templates and a CLI to make scaffolding.

After having installed Yeoman, run the following commands. We will use all defaults except that we won’t use Bootstrap with Compass. (Bootstrap is a CSS framework that can compile with scss files, but we will prefer LESS since it is its original form.)

dmetzler@Antartica ~/src/angular-blog$ npm install -g generator-angular
dmetzler@Antartica ~/src/angular-blog$ yo angular
 Would you like to include Twitter Bootstrap? (Y/n)
 If so, would you like to use Twitter Bootstrap for Compass (as opposed to vanilla CSS)? (Y/n) n
 Would you like to include angular-resource.js? (Y/n)
 Would you like to include angular-cookies.js? (Y/n)
 Would you like to include angular-sanitize.js? (Y/n)</em>

That’s it! Now you can run your HTML app by running:

grunt test
grunt server

It will open your browser with a simple web page saying that everything is running!

There are a lot of things that are happening under the hood here. For instance, try to open the file app/views/main.html and make some changes in the HTML: every time you will save the file, the page will be refreshed automatically to reflect your changes (it’s based on livereload).

Binding With Content Automation

The way to access Nuxeo data from the outside is by using Content Automation. For the purpose of this example, we will try to fetch all entries from the continent vocabulary and show it into the UI.

As I said before, we want a TDD coding style, so the first thing we will do is create a test. We will edit the file test/spec/services/directory.coffee.

describe 'Directory', ->

  $httpBackend = undefined
  continents = [
    {"id":"europe","label":"label.directories.continent.europe"},
    {"id":"africa","label":"label.directories.continent.africa"},
    {"id":"north-america","label":"label.directories.continent.north-america"},
    {"id":"south-america","label":"label.directories.continent.south-america"},
    {"id":"asia","label":"label.directories.continent.asia"},
    {"id":"oceania","label":"label.directories.continent.oceania"},
    {"id":"antarctica","label":"label.directories.continent.antarctica"}
  ]

  describe '#query a directory', ->

    beforeEach ->
      angular.module("test", ["services.nuxeo"]).constant "NUXEO_CONFIG", nuxeo_config =
        contextPath: "/nuxeo"
      module "test"

    beforeEach module('services.nuxeo')
    beforeEach inject(($httpBackend,NUXEO_CONFIG) ->
      @httpBackend = $httpBackend

      $httpBackend.when('POST',[NUXEO_CONFIG.contextPath,'/site/automation/Directory.Entries'].join(""),
        '{"params":{"directoryName":"continent"}}').respond 200, continents

    )

    afterEach ->
      @httpBackend.verifyNoOutstandingExpectation()
      @httpBackend.verifyNoOutstandingRequest()

    it 'should be able to retrieve continents', inject((NuxeoDirectory, $httpBackend) ->
      resolved = false
      promise = NuxeoDirectory("continent").query("blobs")
      expect(promise).not.toBe undefined
      conts = promise.then( (conts)->
        expect(conts.length).toBe 7
        expect(conts[0].id).toBe "europe"
        resolved = true
      )
      $httpBackend.flush()
      expect(resolved).toBe true

    )

This code is written in CoffeeScript. It’s not mandatory for AngularJS, but I find that it gives a clearer view of the code. Yeoman takes care of compiling this code in JavaScript every time it is modified. The compiled file will be in the tmp folder of our app: .tmp/spec/services/directory.js.

This test just verifies that when we make a call to the NuxeoDirectory service, it makes an HTTP request to the /nuxeo/site/automation/Directory.Entries automation endpoint. All the mocking stuff is well documented in AngularJS documentation.

In order to execute this test, we will have to make a small change in the Karma configuration. Karma is the tool that is used to execute our Jasmine test. Its configuration file can be found here: karma.conf.js. In order to add our compiled file to tests we will change the files definition:


files = [
  JASMINE,
  JASMINE_ADAPTER,
  'app/components/angular/angular.js',
  'app/components/angular-mocks/angular-mocks.js',
  'app/scripts/*.js',
  'app/scripts/**/*.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js',
  '.tmp/spec/**/*.js'  //In order to execute compiled coffee tests
];

If we run grunt test now, it will of course fail. We now need to implement our service. This will be done through the app/services/nuxeo.coffee file. You can find the entire file on GitHub and I will only comment on some lines.

In fact the NuxeoDirectory service is just a pre-configured call to the NuxeoAutomation service:

.factory("NuxeoDirectory", ['NuxeoAutomation', (NuxeoAutomation) ->   
  NuxeoDirectory = (dirName) ->
    Directory  = {}

    Directory.query = ->
      NuxeoAutomation("Directory.Entries", {directoryName: dirName}).query("blobs")

    Directory
])

The automation service is quite the same as angular-resource

  # First we define the request that is a common Automation request 
  request = 
    method: 'POST',
    url: url, 
    headers: 
      'Content-Type':'application/json+nxrequest'
    data: 
      params: params

  # We call the request and return a promise 
  $http(request).then((response) ->                        
    data = response.data
    if data == "null"
      return null

    # Depending on the datatype (list of objects or object) 
    # we return what is returned by the server
    if data
      switch returnType
        when "documents","blobs" 
          angular.forEach data, (item) ->                
            value.push(new Resource(item));
        else
          angular.copy(data,value)  

    return value
  , (response) ->
    $q.reject(response.data.cause.cause.message)
  )            

Now we can use our service in every controller like this:

.controller('MainCtrl', ['$scope','NuxeoDirectory', function ($scope, NuxeoDirectory) {
  $scope.continents = NuxeoDirectory("continent").query()
}]);

Note that the result is a promise, so it will not immediately have its content populated. If you use it in you JavaScript code, you’ll have to use the then() construct. Fortunately, the template bindings know how to deal with promises, and you can use it directly like this:

<h3>List of continents</h3>
<ul>
  <li ng-repeat="continent in continents">{{continent.id}}</li>
</ul>

The complete source code of this project is available on the angular-nuxeo-blog GitHub project.

Conclusion

In this post, we have seen how to call a Content Automation operation from an AngularJS application. Of course you have access to all operations that are exposed by the automation server. By developing your own operation, you can quickly expose your business logic. The problem here is that we are only using the POST HTTP verb and we are not doing true REST development. We have some work pending to expose a true REST API on top of Nuxeo, exposing resources and then operations on them.

The post First Steps with AngularJS and Nuxeo Content Automation appeared first on Nuxeo Blogs.

[Q&A Friday] How to Attach Files to Documents with REST API

$
0
0

Today we have a common question asked by sk90:, How can I attach files to documents through REST API?.

In his example, he uses our old REST API based on restlets. This API is deprecated and we now use Content Automation. Following are some examples for doing this.

Java Automation Client

This is an example using nuxeo-automation-client in a unit test. The test deploys a basic Nuxeo in a Jetty. Thanks to the EmbeddedAutomationServerFeature test feature, everything we need is deployed (core doc types, content automation etc…).

The first thing to do is create a File document at the root. Then we create a Blob with the automation-client API. Then comes the Blob.Attach request. Finally we test that the blob has been attached to the document.

package org.nuxeo.sample.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.automation.client.Constants;
import org.nuxeo.ecm.automation.client.Session;
import org.nuxeo.ecm.automation.client.model.Blob;
import org.nuxeo.ecm.automation.client.model.StringBlob;
import org.nuxeo.ecm.automation.test.EmbeddedAutomationServerFeature;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.Jetty;

import com.google.inject.Inject;

@RunWith(FeaturesRunner.class)
@Features(EmbeddedAutomationServerFeature.class)
@Jetty(port = 18080)
public class AutomationTest {

	@Inject
	CoreSession coreSession;

	@Inject
	Session session;

	@Test
	public void testAttachDocument() throws Exception {
		// Create the document and get its ID
		DocumentModel fileDocument = coreSession.createDocumentModel("/",
				"testFile", "File");
		fileDocument = coreSession.createDocument(fileDocument);
		String documentID = fileDocument.getId();
		coreSession.save(); // flush changes to the embedded database

		// create a file and attach it to the document
		Blob b = new StringBlob("myFile.txt", "file content", "text/plain");
		session.newRequest("Blob.Attach")
				.setHeader(Constants.HEADER_NX_VOIDOP, "true").setInput(b)
				.set("document", documentID).execute();
		coreSession.save();// flush changes to the embedded database

		// retrieve the document again and make sure the blob has been set
		fileDocument = coreSession.getDocument(fileDocument.getRef());
		org.nuxeo.ecm.core.api.Blob nxBlob = (org.nuxeo.ecm.core.api.Blob) fileDocument.getPropertyValue("file:content");
		assertNotNull(nxBlob);
		assertEquals(b.getFileName(), nxBlob.getFilename());
	}
}

PHP Automation Client

Here’s a sample taken from the nuxeo-automation-php-client repository. It lists the available workspaces and lets you choose a file to upload. Then it creates the document in the selected workspace using the filename. Next, it attaches the file to the document in another request. We use the same Blob.Attach operation for that. You’ll find more details in our documentation.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<?php
/*
 * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     Gallouin Arthur
 */
?>
<html>
<head>
    <title>B4 test php Client</title>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/>
    <link rel="stylesheet" media="screen" type="text/css" title="Design" href="design.css"/>
</head>
<body>
Create a file at the path chosen with file path and attach the blob chosen in<br/>
the blob path field to it.<br/>

<form action="B4.php" method="post" enctype="multipart/form-data">
    <table>
        <tr>
            <td>Blob Path</td>
            <td><input type="file" name="blobPath"/></td>
        </tr>
        <tr>
            <td>File Path</td>
            <td><?php

                include ('../NuxeoAutomationClient/NuxeoAutomationAPI.php');

                $client = new NuxeoPhpAutomationClient('http://localhost:8080/nuxeo/site/automation');

                $session = $client->getSession('Administrator', 'Administrator');

                $answer = $session->newRequest("Document.Query")->set('params', 'query', "SELECT * FROM Workspace")->sendRequest();

                $array = $answer->getDocumentList();
                $value = sizeof($array);
                echo '<select name="TargetNuxeoDocumentPath">';
                for ($test = 0; $test < $value; $test++) {
                    echo '<option value="' . current($array)->getPath() . '">' . current($array)->getTitle() . '</option>';
                    next($array);
                }
                echo '</select>';
                ?></td>
        </tr>
        <tr>
            <td><input type="submit" value="Submit"/></td>
        </tr>
    </table>
</form>
<?php

/**
 *
 * AttachBlob function
 *
 * @param String $blob contains the path of the blob to load as an attachment
 * @param String $filePath contains the path of the folder where the fille holding the blob will be created
 * @param String $blobtype contains the type of the blob (given by the $_FILES['blobPath']['type'])
 */
function attachBlob($blob = '../test.txt', $filePath = '/default-domain/workspaces/document', $blobtype = 'application/binary') {

    //only works on LINUX / MAC
    // We get the name of the file to use it for the name of the document
    $ename = explode("/", $blob);
    $filename = end($ename);
    $client = new NuxeoPhpAutomationClient('http://localhost:8080/nuxeo/site/automation');

    $session = $client->getSession('Administrator', 'Administrator');

    $properties = "dc:title=". $filename;

    //We create the document that will hold the file
    $answer = $session->newRequest("Document.Create")->set('input', 'doc:' . $filePath)->set('params', 'type', 'File')->set('params', 'name', end($ename))->set('params', 'properties', $properties)->sendRequest();

    //We upload the file
    $answer = $session->newRequest("Blob.Attach")->set('params', 'document', $answer->getDocument(0)->getPath())
            ->loadBlob($blob, $blobtype)
            ->sendRequest();
}

if (!isset($_FILES['blobPath']) AND $_FILES['blobPath']['error'] == 0) {
    echo 'BlobPath is empty';
    exit;
}
if (!isset($_POST['TargetNuxeoDocumentPath']) OR empty($_POST['TargetNuxeoDocumentPath'])) {
    echo 'TargetNuxeoDocumentPath is empty';
    exit;
}
if ((isset($_FILES['blobPath']) && ($_FILES['blobPath']['error'] == UPLOAD_ERR_OK))) {
    $targetPath = '../blobs/';
    if (!is_dir('../blobs'))
        mkdir('../blobs');
    move_uploaded_file($_FILES['blobPath']['tmp_name'], $targetPath . $_FILES['blobPath']['name']);
}

attachBlob($targetPath . $_FILES['blobPath']['name'], $_POST['TargetNuxeoDocumentPath'], $_FILES['blobPath']['type']);
unlink($targetPath . $_FILES['blobPath']['name']);

?></body>
</html>

The post [Q&A Friday] How to Attach Files to Documents with REST API appeared first on Nuxeo Blogs.

Recent Projects from the Nuxeo Community

$
0
0

There is some wicked awesome stuff being developed by the members of the Nuxeo Community, and I need to share this with all of you!

openwide-nuxeo-commons

Our friends from Open Wide have been doing several projects with Nuxeo Platform so far, and they have extracted some useful and interesting pieces of code from this. The result is openwide-nuxeo-commons. It’s already tagged for 5.7.1 so I invite you to test this along with the new release :) Here’s a small overview of its content taken from the project’s ReadMe.

Core

  • openwide-nuxeo-constants: Various constants exposed on Java classes, mainly to ease the manipulation of documents.
  • openwide-nuxeo-tests-helper: Thin helper to set up tests.
  • openwide-nuxeo-utils: Miscellaneous utility methods, plus an extension point to display your project version.

Features

  • openwide-nuxeo-property-sync: Synchronizes properties from documents to their children.
  • openwide-nuxeo-avatar-importer: Watches a given folder to import its contents as avatars.
  • openwide-nuxeo-ecm-types-ordering: Customizes the appearance of the doctype selection pop-up.
  • openwide-nuxeo-document-creation-script: An alternative to the Content Template service.
  • openwide-nuxeo-generic-properties: Generic extension point to store simple data.

Studio

  • How To Create a faceted search form

Kudos to Open Wide for releasing this in LGPL :)

nuxeo_dart_automation_sample

Nelson Silva just started a Dart + Content Automation sample. The Dart web app connects to a Nuxeo server and fetches the list of operations available. It’s a work in progress, but it gives a very good idea of what you can do with Content Automation coupled with any web technology.

nuxeo-userprofile-oauth2

Nelson works at InEvo, where they do different projects based on the Nuxeo Platform. One of the modules they have developed can be used to retrieve user profile information from several social sites using the OAuth2 API. Right now only Linkedin has been implemented, but they plan to integrate Facebook and Google+. One of the goals of this module is of course to let people register to a Nuxeo Instance automatically using their social accounts.

The post Recent Projects from the Nuxeo Community appeared first on Nuxeo Blogs.

Nuxeo Platform 5.7.1 is out!

$
0
0

Nuxeo Platform 5.7.1 is out! This is the first Fast Track version, using the newly launched versioning system, with Fast Tracks on odd numbered releases and Long Term Support on even numbered releases. And it’s available for download right now!

Our awesome developers have worked hard since releasing the 5.6. A lot has changed since then, in every aspect of the platform. Please take a look at the release notes to get a complete overview of the changes made. Following are some highlights of the 5.7.1.

Nuxeo Drive

Nuxeo Drive enables bidirectional synchronization of content between the local desktop and the Nuxeo content repository, on premise or in the Cloud. It works with all applications built on the Nuxeo Platform, including Document Management, Digital Asset Management, Case Management, or a custom content-centric application. In addition, the Nuxeo Drive framework can be used to build sync-enabled rich clients or server applications.
Nuxeo DAM

DAM Revamped

The Digital Asset Management interface had been redesigned from the ground up using Content Views, Layouts and Widgets, so that every part of the DAM screen (search panel, results panel, asset view) is easy to customize using Nuxeo Studio.

Marketplace Packages

We’ve added new packages and updated most of them. Among them you’ll find of course Nuxeo Drive, the Unicolors flavor set, the template rendering addon, the smart search, the new Jenkins Report plugin (it helps us keep track of our build on our CI), the Quota plugin where we made a lot of improvements, and the permissions audit plugin to generate exports of your users and groups and their permissions.

Mobile

We developed a mobile app based on Apache Cordova, bringing multi-OS support. It can be used as a framework to build your own mobile application based on the Nuxeo Platform. Using Nuxeo WebEngine and JQuery Mobile, it provides the following features: saved search, browsing, upload and download of documents, and integration with native mobile API.

Under the Hood

We are continually improving the platform performance and its extensibility. We now support Oracle on Amazon RDS and Microsoft SQL Server 2012. We’ve also worked a lot on monitoring. A new tool has been developed using Yammer Metrics, with data accessible from JMX, Graphite or any backend supported by Metrics.

The automation client is also fully OSGI, and you can deploy it on any container. We also worked on JSON Marshalling. You can send POJO as input or parameters. We added new operations for directory management, document proxy and DAM.

The UI framework has evolved too, especially the action framework. We also have some new widgets and we’ve replaced every richfaces panel by fancybox to give a more unified experience.

What’s Next?

Our next Fast Track release will be a 5.7.2 in roughly 6 weeks. The 5.8 will follow in October, around the time of Nuxeo World.

The post Nuxeo Platform 5.7.1 is out! appeared first on Nuxeo Blogs.

Viewing all 161 articles
Browse latest View live